fix(gateway): allow ws:// to private network addresses (#28670)

* fix(gateway): allow ws:// to RFC 1918 private network addresses

resolve ws-private-network conflicts

* gateway: keep ws security strict-by-default with private opt-in

* gateway: apply private ws opt-in in connection detail guard

* gateway: apply private ws opt-in in websocket client

* onboarding: gate private ws urls behind explicit opt-in

* gateway tests: enforce strict ws defaults with private opt-in

* onboarding tests: validate private ws opt-in behavior

* gateway client tests: cover private ws env override

* gateway call tests: cover private ws env override

* changelog: add ws strict-default security entry for pr 28670

* docs(onboard): document private ws break-glass env

* docs(gateway): add private ws env to remote guide

* docs(docker): add private ws break-glass env var

* docs(security): add private ws break-glass guidance

* docs(config): document OPENCLAW_ALLOW_PRIVATE_WS

* Update CHANGELOG.md

* gateway: normalize private-ws host classification

* test(gateway): cover non-unicast ipv6 private-ws edges

* changelog: rename insecure private ws break-glass env

* docs(onboard): rename insecure private ws env

* docs(gateway): rename insecure private ws env in config reference

* docs(gateway): rename insecure private ws env in remote guide

* docs(security): rename insecure private ws env

* docs(docker): rename insecure private ws env

* test(onboard): rename insecure private ws env

* onboard: rename insecure private ws env

* test(gateway): rename insecure private ws env in call tests

* gateway: rename insecure private ws env in call flow

* test(gateway): rename insecure private ws env in client tests

* gateway: rename insecure private ws env in client

* docker: pass insecure private ws env to services

* docker-setup: persist insecure private ws env

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
Alberto Leal
2026-03-01 23:49:45 -05:00
committed by GitHub
parent d76b224e20
commit 449511484d
16 changed files with 272 additions and 14 deletions

View File

@@ -1,6 +1,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import type { GatewayBonjourBeacon } from "../infra/bonjour-discovery.js";
import { captureEnv } from "../test-utils/env.js";
import type { WizardPrompter } from "../wizard/prompts.js";
import { createWizardPrompter } from "./test-wizard-helpers.js";
@@ -27,8 +28,11 @@ function createPrompter(overrides: Partial<WizardPrompter>): WizardPrompter {
}
describe("promptRemoteGatewayConfig", () => {
const envSnapshot = captureEnv(["OPENCLAW_ALLOW_INSECURE_PRIVATE_WS"]);
beforeEach(() => {
vi.clearAllMocks();
envSnapshot.restore();
detectBinary.mockResolvedValue(false);
discoverGatewayBeacons.mockResolvedValue([]);
resolveWideAreaDiscoveryDomain.mockReturnValue(undefined);
@@ -88,9 +92,12 @@ describe("promptRemoteGatewayConfig", () => {
);
});
it("validates insecure ws:// remote URLs and allows loopback ws://", async () => {
it("validates insecure ws:// remote URLs and allows only loopback ws:// by default", async () => {
const text: WizardPrompter["text"] = vi.fn(async (params) => {
if (params.message === "Gateway WebSocket URL") {
// ws:// to public IPs is rejected
expect(params.validate?.("ws://203.0.113.10:18789")).toContain("Use wss://");
// ws:// to private IPs remains blocked by default
expect(params.validate?.("ws://10.0.0.8:18789")).toContain("Use wss://");
expect(params.validate?.("ws://127.0.0.1:18789")).toBeUndefined();
expect(params.validate?.("wss://remote.example.com:18789")).toBeUndefined();
@@ -119,4 +126,34 @@ describe("promptRemoteGatewayConfig", () => {
expect(next.gateway?.remote?.url).toBe("wss://remote.example.com:18789");
expect(next.gateway?.remote?.token).toBeUndefined();
});
it("allows private ws:// only when OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1", async () => {
process.env.OPENCLAW_ALLOW_INSECURE_PRIVATE_WS = "1";
const text: WizardPrompter["text"] = vi.fn(async (params) => {
if (params.message === "Gateway WebSocket URL") {
expect(params.validate?.("ws://10.0.0.8:18789")).toBeUndefined();
return "ws://10.0.0.8:18789";
}
return "";
}) as WizardPrompter["text"];
const select: WizardPrompter["select"] = vi.fn(async (params) => {
if (params.message === "Gateway auth") {
return "off" as never;
}
return (params.options[0]?.value ?? "") as never;
});
const cfg = {} as OpenClawConfig;
const prompter = createPrompter({
confirm: vi.fn(async () => false),
select,
text,
});
const next = await promptRemoteGatewayConfig(cfg, prompter);
expect(next.gateway?.remote?.url).toBe("ws://10.0.0.8:18789");
});
});