fix(gateway): guard interface discovery failures

Closes #44180.
Refs #47590.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Peter Steinberger
2026-03-22 14:26:35 -07:00
parent 44bbd2d83d
commit 3faaf8984f
7 changed files with 56 additions and 3 deletions

View File

@@ -360,6 +360,13 @@ describe("pickPrimaryLanIPv4", () => {
vi.restoreAllMocks();
}
});
it("returns undefined when interface discovery throws", () => {
vi.spyOn(os, "networkInterfaces").mockImplementation(() => {
throw new Error("uv_interface_addresses failed");
});
expect(pickPrimaryLanIPv4()).toBeUndefined();
});
});
describe("isPrivateOrLoopbackAddress", () => {

View File

@@ -15,7 +15,12 @@ import {
* Prefers common interface names (en0, eth0) then falls back to any external IPv4.
*/
export function pickPrimaryLanIPv4(): string | undefined {
const nets = os.networkInterfaces();
let nets: ReturnType<typeof os.networkInterfaces>;
try {
nets = os.networkInterfaces();
} catch {
return undefined;
}
const preferredNames = ["en0", "eth0"];
for (const name of preferredNames) {
const list = nets[name];

View File

@@ -240,5 +240,13 @@ describe("infra runtime", () => {
expect(out.ipv4).toEqual(["100.123.224.76"]);
expect(out.ipv6).toEqual(["fd7a:115c:a1e0::8801:e04c"]);
});
it("returns empty address lists when interface discovery throws", () => {
vi.spyOn(os, "networkInterfaces").mockImplementation(() => {
throw new Error("uv_interface_addresses failed");
});
expect(listTailnetAddresses()).toEqual({ ipv4: [], ipv6: [] });
});
});
});

View File

@@ -25,7 +25,12 @@ export function listTailnetAddresses(): TailnetAddresses {
const ipv4: string[] = [];
const ipv6: string[] = [];
const ifaces = os.networkInterfaces();
let ifaces: ReturnType<typeof os.networkInterfaces>;
try {
ifaces = os.networkInterfaces();
} catch {
return { ipv4, ipv6 };
}
for (const entries of Object.values(ifaces)) {
if (!entries) {
continue;

View File

@@ -368,6 +368,27 @@ describe("pairing setup code", () => {
});
});
it("returns a bind-specific error when interface discovery throws", async () => {
const resolved = await resolvePairingSetupFromConfig(
{
gateway: {
bind: "lan",
auth: { mode: "token", token: "tok" },
},
},
{
networkInterfaces: () => {
throw new Error("uv_interface_addresses failed");
},
},
);
expect(resolved).toEqual({
ok: false,
error: "gateway.bind=lan set, but no private LAN IP was found.",
});
});
it("prefers gateway.remote.url over tailscale when requested", async () => {
const runCommandWithTimeout = createTailnetDnsRunner();

View File

@@ -118,7 +118,12 @@ function pickIPv4Matching(
networkInterfaces: () => ReturnType<typeof os.networkInterfaces>,
matches: (address: string) => boolean,
): string | null {
const nets = networkInterfaces();
let nets: ReturnType<typeof os.networkInterfaces>;
try {
nets = networkInterfaces();
} catch {
return null;
}
for (const entries of Object.values(nets)) {
if (!entries) {
continue;