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: 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/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: 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. - 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 ## 2026.3.7

View File

@@ -2354,6 +2354,7 @@ See [Plugins](/tools/plugin).
// headless: false, // headless: false,
// noSandbox: false, // noSandbox: false,
// extraArgs: [], // 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", // executablePath: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
// attachOnly: false, // attachOnly: false,
}, },
@@ -2370,6 +2371,7 @@ See [Plugins](/tools/plugin).
- Control service: loopback only (port derived from `gateway.port`, default `18791`). - Control service: loopback only (port derived from `gateway.port`, default `18791`).
- `extraArgs` appends extra launch flags to local Chromium startup (for example - `extraArgs` appends extra launch flags to local Chromium startup (for example
`--disable-gpu`, window sizing, or debug flags). `--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). - This mode relies on Playwright-on-CDP for most operations (screenshots/snapshots/actions).
- Detach by clicking the extension icon again. - 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 ## 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. - 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"`). - 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 ## How “extension path” works

View File

@@ -1202,4 +1202,23 @@ describe("chrome extension relay server", () => {
}, },
RELAY_TEST_TIMEOUT_MS, 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); const existing = relayRuntimeByPort.get(info.port);
if (existing) { if (existing) {
return existing.server; if (existing.server.bindHost !== bindHost) {
await existing.server.stop();
} else {
return existing.server;
}
} }
const inFlight = relayInitByPort.get(info.port); const inFlight = relayInitByPort.get(info.port);
if (inFlight) { if (inFlight) {
return await inFlight; const server = await inFlight;
if (server.bindHost === bindHost) {
return server;
}
await server.stop();
} }
const extensionReconnectGraceMs = envMsOrDefault( const extensionReconnectGraceMs = envMsOrDefault(
@@ -998,12 +1006,13 @@ export async function ensureChromeExtensionRelayServer(opts: {
const addr = server.address() as AddressInfo | null; const addr = server.address() as AddressInfo | null;
const port = addr?.port ?? info.port; const port = addr?.port ?? info.port;
const actualBindHost = addr?.address || bindHost;
const host = info.host; const host = info.host;
const baseUrl = `${new URL(info.baseUrl).protocol}//${host}:${port}`; const baseUrl = `${new URL(info.baseUrl).protocol}//${host}:${port}`;
const relay: ChromeExtensionRelayServer = { const relay: ChromeExtensionRelayServer = {
host, host,
bindHost, bindHost: actualBindHost,
port, port,
baseUrl, baseUrl,
cdpWsUrl: `ws://${host}:${port}/cdp`, 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.", "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": "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.", "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": "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.", "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": "browser.profiles.*.cdpPort":

View File

@@ -118,6 +118,7 @@ export const FIELD_LABELS: Record<string, string> = {
"browser.attachOnly": "Browser Attach-only Mode", "browser.attachOnly": "Browser Attach-only Mode",
"browser.cdpPortRangeStart": "Browser CDP Port Range Start", "browser.cdpPortRangeStart": "Browser CDP Port Range Start",
"browser.defaultProfile": "Browser Default Profile", "browser.defaultProfile": "Browser Default Profile",
"browser.relayBindHost": "Browser Relay Bind Address",
"browser.profiles": "Browser Profiles", "browser.profiles": "Browser Profiles",
"browser.profiles.*.cdpPort": "Browser Profile CDP Port", "browser.profiles.*.cdpPort": "Browser Profile CDP Port",
"browser.profiles.*.cdpUrl": "Browser Profile CDP URL", "browser.profiles.*.cdpUrl": "Browser Profile CDP URL",