Skip to content

Commit 978bc53

Browse files
fix(gateway): skip IPv6 loopback binding on Windows (#69701)
Bind the default loopback gateway listener only to `127.0.0.1` on Windows so libuv dual-stack `::1` behavior cannot wedge localhost HTTP requests. Also keeps non-Windows dual-loopback behavior covered, replaces the redundant Windows passthrough test with guard coverage, and adds the required changelog entry. Fixes #69674. Tests: - pnpm exec oxfmt --check --threads=1 CHANGELOG.md src/gateway/net.ts src/gateway/net.test.ts - pnpm test src/gateway/net.test.ts - pnpm check:changed - GitHub required checks: green Thanks @SARAMALI15792. Co-authored-by: saram ali <[email protected]> Co-authored-by: Brad Groux <[email protected]>
1 parent 30bb88d commit 978bc53

3 files changed

Lines changed: 28 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
1010

1111
### Changes
1212

13+
- Gateway/Windows: bind the default loopback gateway listener only to `127.0.0.1` on Windows so libuv's dual-stack `::1` behavior cannot wedge localhost HTTP requests. (#69701, fixes #69674) Thanks @SARAMALI15792.
1314
- Plugins/migration: emit catalog-backed install hints when `plugins.entries` or `plugins.allow` references an official external plugin that is not installed, so upgraded configs point operators to `openclaw plugins install <spec>` instead of telling them to remove valid plugin config. (#77483) Thanks @hclsys.
1415
- OpenAI/Codex media: advertise Codex audio transcription in runtime and manifest metadata and route active Codex chat models to the OpenAI transcription default instead of sending chat model ids to audio transcription. Thanks @vincentkoc.
1516
- Dependencies: refresh runtime and provider packages including Pi 0.73.0, ACPX adapters, OpenAI, Anthropic, Slack, and TypeScript native preview, while keeping the Bedrock runtime installer override pinned below the Windows ARM Node 24 npm resolver failure.

src/gateway/net.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,10 @@ describe("resolveClientIp", () => {
290290
});
291291

292292
describe("resolveGatewayListenHosts", () => {
293+
afterEach(() => {
294+
vi.restoreAllMocks();
295+
});
296+
293297
it.each([
294298
{
295299
name: "non-loopback host passthrough",
@@ -312,11 +316,28 @@ describe("resolveGatewayListenHosts", () => {
312316
expected: ["127.0.0.1"],
313317
},
314318
] as const)("resolves listen hosts: $name", async ({ host, canBindToHost, expected }) => {
319+
vi.spyOn(process, "platform", "get").mockReturnValue("linux");
315320
const hosts = await resolveGatewayListenHosts(host, {
316321
canBindToHost,
317322
});
318323
expect(hosts).toEqual(expected);
319324
});
325+
326+
it("skips ::1 on Windows even when IPv6 is bindable", async () => {
327+
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
328+
const canBindToHost = vi.fn().mockResolvedValue(true);
329+
const hosts = await resolveGatewayListenHosts("127.0.0.1", { canBindToHost });
330+
expect(hosts).toEqual(["127.0.0.1"]);
331+
expect(canBindToHost).not.toHaveBeenCalled();
332+
});
333+
334+
it("still includes ::1 on non-Windows when IPv6 is bindable", async () => {
335+
vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
336+
const canBindToHost = vi.fn().mockResolvedValue(true);
337+
const hosts = await resolveGatewayListenHosts("127.0.0.1", { canBindToHost });
338+
expect(hosts).toEqual(["127.0.0.1", "::1"]);
339+
expect(canBindToHost).toHaveBeenCalledWith("::1");
340+
});
320341
});
321342

322343
describe("pickPrimaryLanIPv4", () => {

src/gateway/net.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,12 @@ export async function resolveGatewayListenHosts(
330330
if (bindHost !== "127.0.0.1") {
331331
return [bindHost];
332332
}
333+
// Windows: uv_tcp_bind6 creates a dual-stack socket (no UV_TCP_IPV6ONLY), which
334+
// also accepts ::ffff:127.0.0.1 connections. Binding both ::1 and 127.0.0.1 on
335+
// the same port causes non-deterministic TCP routing → HTTP requests hang silently.
336+
if (process.platform === "win32") {
337+
return [bindHost];
338+
}
333339
const canBind = opts?.canBindToHost ?? canBindToHost;
334340
if (await canBind("::1")) {
335341
return [bindHost, "::1"];

0 commit comments

Comments
 (0)