From 3faaf8984fe26afcdc08cc86e8593d38eae64056 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 22 Mar 2026 14:26:35 -0700 Subject: [PATCH] fix(gateway): guard interface discovery failures Closes #44180. Refs #47590. Co-authored-by: Peter Steinberger --- CHANGELOG.md | 2 ++ src/gateway/net.test.ts | 7 +++++++ src/gateway/net.ts | 7 ++++++- src/infra/infra-runtime.test.ts | 8 ++++++++ src/infra/tailnet.ts | 7 ++++++- src/pairing/setup-code.test.ts | 21 +++++++++++++++++++++ src/pairing/setup-code.ts | 7 ++++++- 7 files changed, 56 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00f0b7a119b..be7e3709075 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -254,7 +254,9 @@ Docs: https://docs.openclaw.ai - Discord/ACP: forward worker abort signals into ACP turns so timed-out Discord jobs cancel the running turn instead of silently leaving the bound ACP session working in the background. - Gateway/openresponses: preserve assistant commentary and session continuity across hosted-tool `/v1/responses` turns, and emit streamed tool-call payloads before finalization so client tool loops stay resumable. (#52171) Thanks @CharZhou. - Android/Talk: serialize `TalkModeManager` player teardown so rapid interrupt/restart cycles stop double-releasing or overlapping TTS playback. (#52310) Thanks @Kaneki-x. +<<<<<<< HEAD - WhatsApp/reconnect: preserve the last inbound timestamp across reconnect attempts so the watchdog can still recycle linked-but-dead listeners after a restart instead of leaving them stuck connected forever. +- Gateway/network discovery: guard LAN, tailnet, and pairing interface enumeration so WSL2 and restricted hosts degrade to missing-address fallbacks instead of crashing on `uv_interface_addresses` errors. (#44180, #47590) ### Breaking diff --git a/src/gateway/net.test.ts b/src/gateway/net.test.ts index 78ec8c05c55..9a3ca471cc4 100644 --- a/src/gateway/net.test.ts +++ b/src/gateway/net.test.ts @@ -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", () => { diff --git a/src/gateway/net.ts b/src/gateway/net.ts index 7a5f2eac76d..f330cf6253a 100644 --- a/src/gateway/net.ts +++ b/src/gateway/net.ts @@ -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; + try { + nets = os.networkInterfaces(); + } catch { + return undefined; + } const preferredNames = ["en0", "eth0"]; for (const name of preferredNames) { const list = nets[name]; diff --git a/src/infra/infra-runtime.test.ts b/src/infra/infra-runtime.test.ts index 97f2336fd11..39894ba48a4 100644 --- a/src/infra/infra-runtime.test.ts +++ b/src/infra/infra-runtime.test.ts @@ -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: [] }); + }); }); }); diff --git a/src/infra/tailnet.ts b/src/infra/tailnet.ts index 704663d79ba..b1395817b88 100644 --- a/src/infra/tailnet.ts +++ b/src/infra/tailnet.ts @@ -25,7 +25,12 @@ export function listTailnetAddresses(): TailnetAddresses { const ipv4: string[] = []; const ipv6: string[] = []; - const ifaces = os.networkInterfaces(); + let ifaces: ReturnType; + try { + ifaces = os.networkInterfaces(); + } catch { + return { ipv4, ipv6 }; + } for (const entries of Object.values(ifaces)) { if (!entries) { continue; diff --git a/src/pairing/setup-code.test.ts b/src/pairing/setup-code.test.ts index 6622f6c010f..065da47bf71 100644 --- a/src/pairing/setup-code.test.ts +++ b/src/pairing/setup-code.test.ts @@ -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(); diff --git a/src/pairing/setup-code.ts b/src/pairing/setup-code.ts index 6a2c5dd0b39..9229b7941c9 100644 --- a/src/pairing/setup-code.ts +++ b/src/pairing/setup-code.ts @@ -118,7 +118,12 @@ function pickIPv4Matching( networkInterfaces: () => ReturnType, matches: (address: string) => boolean, ): string | null { - const nets = networkInterfaces(); + let nets: ReturnType; + try { + nets = networkInterfaces(); + } catch { + return null; + } for (const entries of Object.values(nets)) { if (!entries) { continue;