fix: make browser relay bind address configurable (#39364) (thanks @mvanhorn)

This commit is contained in:
Peter Steinberger 2026-03-08 19:14:59 +00:00
parent e883d0b556
commit d3111fbbcb
8 changed files with 51 additions and 3 deletions

View File

@ -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

View File

@ -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.
---

View File

@ -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

View File

@ -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 dont 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

View File

@ -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,
);
});

View File

@ -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`,

View File

@ -250,6 +250,8 @@ export const FIELD_HELP: Record<string, string> = {
"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":

View File

@ -118,6 +118,7 @@ export const FIELD_LABELS: Record<string, string> = {
"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",