Skip to content

Commit d3111fb

Browse files
committed
fix: make browser relay bind address configurable (#39364) (thanks @mvanhorn)
1 parent e883d0b commit d3111fb

File tree

8 files changed

+51
-3
lines changed

8 files changed

+51
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai
3030
- 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.
3131
- 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.
3232
- 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.
33+
- 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.
3334
- 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.
3435

3536
## 2026.3.7

docs/gateway/configuration-reference.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2354,6 +2354,7 @@ See [Plugins](/tools/plugin).
23542354
// headless: false,
23552355
// noSandbox: false,
23562356
// extraArgs: [],
2357+
// relayBindHost: "0.0.0.0", // only when the extension relay must be reachable across namespaces (for example WSL2)
23572358
// executablePath: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
23582359
// attachOnly: false,
23592360
},
@@ -2370,6 +2371,7 @@ See [Plugins](/tools/plugin).
23702371
- Control service: loopback only (port derived from `gateway.port`, default `18791`).
23712372
- `extraArgs` appends extra launch flags to local Chromium startup (for example
23722373
`--disable-gpu`, window sizing, or debug flags).
2374+
- `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.
23732375

23742376
---
23752377

docs/tools/browser.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,19 @@ Notes:
328328

329329
- This mode relies on Playwright-on-CDP for most operations (screenshots/snapshots/actions).
330330
- Detach by clicking the extension icon again.
331+
- 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.
332+
333+
WSL2 / cross-namespace example:
334+
335+
```json5
336+
{
337+
browser: {
338+
enabled: true,
339+
relayBindHost: "0.0.0.0",
340+
defaultProfile: "chrome",
341+
},
342+
}
343+
```
331344

332345
## Isolation guarantees
333346

docs/tools/chrome-extension.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ Debugging: `openclaw sandbox explain`
161161

162162
- Keep the Gateway and node host on the same tailnet; avoid exposing relay ports to LAN or public Internet.
163163
- Pair nodes intentionally; disable browser proxy routing if you don’t want remote control (`gateway.nodes.browser.mode="off"`).
164+
- 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.
164165

165166
## How “extension path” works
166167

src/browser/extension-relay.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1202,4 +1202,23 @@ describe("chrome extension relay server", () => {
12021202
},
12031203
RELAY_TEST_TIMEOUT_MS,
12041204
);
1205+
1206+
it(
1207+
"restarts the relay when bindHost changes for the same port",
1208+
async () => {
1209+
const port = await getFreePort();
1210+
cdpUrl = `http://127.0.0.1:${port}`;
1211+
1212+
const initial = await ensureChromeExtensionRelayServer({ cdpUrl });
1213+
expect(initial.bindHost).toBe("127.0.0.1");
1214+
1215+
const rebound = await ensureChromeExtensionRelayServer({
1216+
cdpUrl,
1217+
bindHost: "0.0.0.0",
1218+
});
1219+
expect(rebound.bindHost).toBe("0.0.0.0");
1220+
expect(rebound.port).toBe(port);
1221+
},
1222+
RELAY_TEST_TIMEOUT_MS,
1223+
);
12051224
});

src/browser/extension-relay.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -234,12 +234,20 @@ export async function ensureChromeExtensionRelayServer(opts: {
234234

235235
const existing = relayRuntimeByPort.get(info.port);
236236
if (existing) {
237-
return existing.server;
237+
if (existing.server.bindHost !== bindHost) {
238+
await existing.server.stop();
239+
} else {
240+
return existing.server;
241+
}
238242
}
239243

240244
const inFlight = relayInitByPort.get(info.port);
241245
if (inFlight) {
242-
return await inFlight;
246+
const server = await inFlight;
247+
if (server.bindHost === bindHost) {
248+
return server;
249+
}
250+
await server.stop();
243251
}
244252

245253
const extensionReconnectGraceMs = envMsOrDefault(
@@ -998,12 +1006,13 @@ export async function ensureChromeExtensionRelayServer(opts: {
9981006

9991007
const addr = server.address() as AddressInfo | null;
10001008
const port = addr?.port ?? info.port;
1009+
const actualBindHost = addr?.address || bindHost;
10011010
const host = info.host;
10021011
const baseUrl = `${new URL(info.baseUrl).protocol}//${host}:${port}`;
10031012

10041013
const relay: ChromeExtensionRelayServer = {
10051014
host,
1006-
bindHost,
1015+
bindHost: actualBindHost,
10071016
port,
10081017
baseUrl,
10091018
cdpWsUrl: `ws://${host}:${port}/cdp`,

src/config/schema.help.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,8 @@ export const FIELD_HELP: Record<string, string> = {
250250
"Starting local CDP port used for auto-allocated browser profile ports. Increase this when host-level port defaults conflict with other local services.",
251251
"browser.defaultProfile":
252252
"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.",
253+
"browser.relayBindHost":
254+
"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.",
253255
"browser.profiles":
254256
"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.",
255257
"browser.profiles.*.cdpPort":

src/config/schema.labels.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export const FIELD_LABELS: Record<string, string> = {
118118
"browser.attachOnly": "Browser Attach-only Mode",
119119
"browser.cdpPortRangeStart": "Browser CDP Port Range Start",
120120
"browser.defaultProfile": "Browser Default Profile",
121+
"browser.relayBindHost": "Browser Relay Bind Address",
121122
"browser.profiles": "Browser Profiles",
122123
"browser.profiles.*.cdpPort": "Browser Profile CDP Port",
123124
"browser.profiles.*.cdpUrl": "Browser Profile CDP URL",

0 commit comments

Comments
 (0)