382c32f2bc
The backend IP baked into a push-capable device at assign time is auto-derived by subnet-matching a local NIC. That's non-deterministic when two NICs match the device subnet, and null when none does. Surface it: backendIpCandidates() lists all local IPv4 NICs (on-subnet first), GET /api/setup/backend-ips serves them, and the wizard renders an editable Backend push IP dropdown after a successful test (pre-filled with the auto-pick, warns when no NIC is on the device subnet). The chosen IP overrides the auto-pick on assign and is recorded in config.
70 lines
2.7 KiB
TypeScript
70 lines
2.7 KiB
TypeScript
import { networkInterfaces } from "node:os";
|
|
|
|
// Figure out which local IP a device should call back on. For input-push, the
|
|
// device needs OUR address on ITS subnet — pick the local IPv4 interface whose
|
|
// network contains the device's IP. Override with BACKEND_HOST_IP if the
|
|
// auto-pick is wrong (e.g. multi-homed host). See wiki/concepts/device-input-flow.md.
|
|
|
|
export function backendIpForDevice(deviceHost: string): string | null {
|
|
if (process.env.BACKEND_HOST_IP) return process.env.BACKEND_HOST_IP;
|
|
|
|
const ip = deviceHost.split(".").map(Number);
|
|
if (ip.length !== 4 || ip.some((o) => Number.isNaN(o))) return null;
|
|
|
|
for (const ifaces of Object.values(networkInterfaces())) {
|
|
for (const i of ifaces ?? []) {
|
|
if (i.family !== "IPv4" || i.internal) continue;
|
|
const addr = i.address.split(".").map(Number);
|
|
const mask = i.netmask.split(".").map(Number);
|
|
if (addr.length !== 4 || mask.length !== 4) continue;
|
|
const sameNet = ip.every((o, k) => (o & mask[k]!) === (addr[k]! & mask[k]!));
|
|
if (sameNet) return i.address;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Backend port the device should call (the server's listen port). */
|
|
export function backendPort(): number {
|
|
return Number(process.env.PORT ?? 3000);
|
|
}
|
|
|
|
export interface BackendIpCandidate {
|
|
ip: string;
|
|
iface: string;
|
|
/** True if this interface's subnet contains the device IP (the likely one). */
|
|
onDeviceSubnet: boolean;
|
|
}
|
|
|
|
/**
|
|
* List local IPv4 addresses the device could call back on, with the ones on the
|
|
* device's own subnet flagged + sorted first. Lets the admin see/override the
|
|
* auto-pick (important on multi-NIC hosts). BACKEND_HOST_IP, if set, is the only
|
|
* candidate (the deterministic override).
|
|
*/
|
|
export function backendIpCandidates(deviceHost: string): BackendIpCandidate[] {
|
|
if (process.env.BACKEND_HOST_IP) {
|
|
return [{ ip: process.env.BACKEND_HOST_IP, iface: "BACKEND_HOST_IP", onDeviceSubnet: true }];
|
|
}
|
|
|
|
const dev = deviceHost.split(".").map(Number);
|
|
const validDev = dev.length === 4 && !dev.some((o) => Number.isNaN(o));
|
|
const out: BackendIpCandidate[] = [];
|
|
|
|
for (const [iface, ifaces] of Object.entries(networkInterfaces())) {
|
|
for (const i of ifaces ?? []) {
|
|
if (i.family !== "IPv4" || i.internal) continue;
|
|
const addr = i.address.split(".").map(Number);
|
|
const mask = i.netmask.split(".").map(Number);
|
|
const onDeviceSubnet =
|
|
validDev &&
|
|
addr.length === 4 &&
|
|
mask.length === 4 &&
|
|
dev.every((o, k) => (o & mask[k]!) === (addr[k]! & mask[k]!));
|
|
out.push({ ip: i.address, iface, onDeviceSubnet });
|
|
}
|
|
}
|
|
// On-subnet candidates first.
|
|
return out.sort((a, b) => Number(b.onDeviceSubnet) - Number(a.onDeviceSubnet));
|
|
}
|