mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
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 <140950904+SARAMALI15792@users.noreply.github.com> Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
This commit is contained in:
@@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Changes
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@@ -290,6 +290,10 @@ describe("resolveClientIp", () => {
|
||||
});
|
||||
|
||||
describe("resolveGatewayListenHosts", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "non-loopback host passthrough",
|
||||
@@ -312,11 +316,28 @@ describe("resolveGatewayListenHosts", () => {
|
||||
expected: ["127.0.0.1"],
|
||||
},
|
||||
] as const)("resolves listen hosts: $name", async ({ host, canBindToHost, expected }) => {
|
||||
vi.spyOn(process, "platform", "get").mockReturnValue("linux");
|
||||
const hosts = await resolveGatewayListenHosts(host, {
|
||||
canBindToHost,
|
||||
});
|
||||
expect(hosts).toEqual(expected);
|
||||
});
|
||||
|
||||
it("skips ::1 on Windows even when IPv6 is bindable", async () => {
|
||||
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
const canBindToHost = vi.fn().mockResolvedValue(true);
|
||||
const hosts = await resolveGatewayListenHosts("127.0.0.1", { canBindToHost });
|
||||
expect(hosts).toEqual(["127.0.0.1"]);
|
||||
expect(canBindToHost).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("still includes ::1 on non-Windows when IPv6 is bindable", async () => {
|
||||
vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
|
||||
const canBindToHost = vi.fn().mockResolvedValue(true);
|
||||
const hosts = await resolveGatewayListenHosts("127.0.0.1", { canBindToHost });
|
||||
expect(hosts).toEqual(["127.0.0.1", "::1"]);
|
||||
expect(canBindToHost).toHaveBeenCalledWith("::1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("pickPrimaryLanIPv4", () => {
|
||||
|
||||
@@ -330,6 +330,12 @@ export async function resolveGatewayListenHosts(
|
||||
if (bindHost !== "127.0.0.1") {
|
||||
return [bindHost];
|
||||
}
|
||||
// Windows: uv_tcp_bind6 creates a dual-stack socket (no UV_TCP_IPV6ONLY), which
|
||||
// also accepts ::ffff:127.0.0.1 connections. Binding both ::1 and 127.0.0.1 on
|
||||
// the same port causes non-deterministic TCP routing → HTTP requests hang silently.
|
||||
if (process.platform === "win32") {
|
||||
return [bindHost];
|
||||
}
|
||||
const canBind = opts?.canBindToHost ?? canBindToHost;
|
||||
if (await canBind("::1")) {
|
||||
return [bindHost, "::1"];
|
||||
|
||||
Reference in New Issue
Block a user