From d3111fbbcbc4b985b6b3a931a12124798c813602 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 8 Mar 2026 19:14:59 +0000 Subject: [PATCH] fix: make browser relay bind address configurable (#39364) (thanks @mvanhorn) --- CHANGELOG.md | 1 + docs/gateway/configuration-reference.md | 2 ++ docs/tools/browser.md | 13 +++++++++++++ docs/tools/chrome-extension.md | 1 + src/browser/extension-relay.test.ts | 19 +++++++++++++++++++ src/browser/extension-relay.ts | 15 ++++++++++++--- src/config/schema.help.ts | 2 ++ src/config/schema.labels.ts | 1 + 8 files changed, 51 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25add7a3d71..0e1c7c6f48d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai - Browser/CDP: normalize loopback direct WebSocket CDP URLs back to HTTP(S) for `/json/*` tab operations so local `ws://` / `wss://` profiles can still list, focus, open, and close tabs after the new direct-WS support lands. (#31085) Thanks @shrey150. - Browser/CDP: rewrite wildcard `ws://0.0.0.0` and `ws://[::]` debugger URLs from remote `/json/version` responses back to the external CDP host/port, fixing Browserless-style container endpoints. (#17760) Thanks @joeharouni. - Browser/extension relay: wait briefly for a previously attached Chrome tab to reappear after transient relay drops before failing with `tab not found`, reducing noisy reconnect flakes. (#32461) Thanks @AaronWander. +- Browser/extension relay: add `browser.relayBindHost` so the Chrome relay can bind to an explicit non-loopback address for WSL2 and other cross-namespace setups, while preserving loopback-only defaults. (#39364) Thanks @mvanhorn. - Context engine registry/bundled builds: share the registry state through a `globalThis` singleton so duplicated bundled module copies can resolve engines registered by each other at runtime, with regression coverage for duplicate-module imports. (#40115) thanks @jalehman. ## 2026.3.7 diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index ca6a3681410..538b80f6138 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -2354,6 +2354,7 @@ See [Plugins](/tools/plugin). // headless: false, // noSandbox: false, // extraArgs: [], + // relayBindHost: "0.0.0.0", // only when the extension relay must be reachable across namespaces (for example WSL2) // executablePath: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser", // attachOnly: false, }, @@ -2370,6 +2371,7 @@ See [Plugins](/tools/plugin). - Control service: loopback only (port derived from `gateway.port`, default `18791`). - `extraArgs` appends extra launch flags to local Chromium startup (for example `--disable-gpu`, window sizing, or debug flags). +- `relayBindHost` changes where the Chrome extension relay listens. Leave unset for loopback-only access; set an explicit non-loopback bind address such as `0.0.0.0` only when the relay must cross a namespace boundary (for example WSL2) and the host network is already trusted. --- diff --git a/docs/tools/browser.md b/docs/tools/browser.md index e1372a08b9d..633b2633a33 100644 --- a/docs/tools/browser.md +++ b/docs/tools/browser.md @@ -328,6 +328,19 @@ Notes: - This mode relies on Playwright-on-CDP for most operations (screenshots/snapshots/actions). - Detach by clicking the extension icon again. +- Leave the relay loopback-only by default. If the relay must be reachable from a different network namespace (for example Gateway in WSL2, Chrome on Windows), set `browser.relayBindHost` to an explicit bind address such as `0.0.0.0` while keeping the surrounding network private and authenticated. + +WSL2 / cross-namespace example: + +```json5 +{ + browser: { + enabled: true, + relayBindHost: "0.0.0.0", + defaultProfile: "chrome", + }, +} +``` ## Isolation guarantees diff --git a/docs/tools/chrome-extension.md b/docs/tools/chrome-extension.md index 964eb40f37b..ce4b271ae9c 100644 --- a/docs/tools/chrome-extension.md +++ b/docs/tools/chrome-extension.md @@ -161,6 +161,7 @@ Debugging: `openclaw sandbox explain` - Keep the Gateway and node host on the same tailnet; avoid exposing relay ports to LAN or public Internet. - Pair nodes intentionally; disable browser proxy routing if you don’t want remote control (`gateway.nodes.browser.mode="off"`). +- Leave the relay on loopback unless you have a real cross-namespace need. For WSL2 or similar split-host setups, set `browser.relayBindHost` to an explicit bind address such as `0.0.0.0`, then keep access constrained with Gateway auth, node pairing, and a private network. ## How “extension path” works diff --git a/src/browser/extension-relay.test.ts b/src/browser/extension-relay.test.ts index b95dea6c9f2..f6e14ee8803 100644 --- a/src/browser/extension-relay.test.ts +++ b/src/browser/extension-relay.test.ts @@ -1202,4 +1202,23 @@ describe("chrome extension relay server", () => { }, RELAY_TEST_TIMEOUT_MS, ); + + it( + "restarts the relay when bindHost changes for the same port", + async () => { + const port = await getFreePort(); + cdpUrl = `http://127.0.0.1:${port}`; + + const initial = await ensureChromeExtensionRelayServer({ cdpUrl }); + expect(initial.bindHost).toBe("127.0.0.1"); + + const rebound = await ensureChromeExtensionRelayServer({ + cdpUrl, + bindHost: "0.0.0.0", + }); + expect(rebound.bindHost).toBe("0.0.0.0"); + expect(rebound.port).toBe(port); + }, + RELAY_TEST_TIMEOUT_MS, + ); }); diff --git a/src/browser/extension-relay.ts b/src/browser/extension-relay.ts index 603ae579c12..5a87670605e 100644 --- a/src/browser/extension-relay.ts +++ b/src/browser/extension-relay.ts @@ -234,12 +234,20 @@ export async function ensureChromeExtensionRelayServer(opts: { const existing = relayRuntimeByPort.get(info.port); if (existing) { - return existing.server; + if (existing.server.bindHost !== bindHost) { + await existing.server.stop(); + } else { + return existing.server; + } } const inFlight = relayInitByPort.get(info.port); if (inFlight) { - return await inFlight; + const server = await inFlight; + if (server.bindHost === bindHost) { + return server; + } + await server.stop(); } const extensionReconnectGraceMs = envMsOrDefault( @@ -998,12 +1006,13 @@ export async function ensureChromeExtensionRelayServer(opts: { const addr = server.address() as AddressInfo | null; const port = addr?.port ?? info.port; + const actualBindHost = addr?.address || bindHost; const host = info.host; const baseUrl = `${new URL(info.baseUrl).protocol}//${host}:${port}`; const relay: ChromeExtensionRelayServer = { host, - bindHost, + bindHost: actualBindHost, port, baseUrl, cdpWsUrl: `ws://${host}:${port}/cdp`, diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index 50d9502ffea..ec02d1d106f 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -250,6 +250,8 @@ export const FIELD_HELP: Record = { "Starting local CDP port used for auto-allocated browser profile ports. Increase this when host-level port defaults conflict with other local services.", "browser.defaultProfile": "Default browser profile name selected when callers do not explicitly choose a profile. Use a stable low-privilege profile as the default to reduce accidental cross-context state use.", + "browser.relayBindHost": + "Bind IP address for the Chrome extension relay listener. Leave unset for loopback-only access, or set an explicit non-loopback IP such as 0.0.0.0 only when the relay must be reachable across network namespaces (for example WSL2) and the surrounding network is already trusted.", "browser.profiles": "Named browser profile connection map used for explicit routing to CDP ports or URLs with optional metadata. Keep profile names consistent and avoid overlapping endpoint definitions.", "browser.profiles.*.cdpPort": diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index f8961d9e8dd..ec9e8eb0c52 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -118,6 +118,7 @@ export const FIELD_LABELS: Record = { "browser.attachOnly": "Browser Attach-only Mode", "browser.cdpPortRangeStart": "Browser CDP Port Range Start", "browser.defaultProfile": "Browser Default Profile", + "browser.relayBindHost": "Browser Relay Bind Address", "browser.profiles": "Browser Profiles", "browser.profiles.*.cdpPort": "Browser Profile CDP Port", "browser.profiles.*.cdpUrl": "Browser Profile CDP URL",