fix(infra): make browser relay bind address configurable

Add browser.relayBindHost config option so the Chrome extension relay
server can bind to a non-loopback address (e.g. 0.0.0.0 for WSL2).
Defaults to 127.0.0.1 when unset, preserving current behavior.

Closes #39214

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt Van Horn 2026-03-07 18:00:58 -08:00 committed by Peter Steinberger
parent 0692f71c6f
commit 436ae8a07c
7 changed files with 53 additions and 3 deletions

View File

@ -36,6 +36,7 @@ export type ResolvedBrowserConfig = {
profiles: Record<string, BrowserProfileConfig>;
ssrfPolicy?: SsrFPolicy;
extraArgs: string[];
relayBindHost?: string;
};
export type ResolvedBrowserProfile = {
@ -291,6 +292,7 @@ export function resolveBrowserConfig(
? cfg.extraArgs.filter((a): a is string => typeof a === "string" && a.trim().length > 0)
: [];
const ssrfPolicy = resolveBrowserSsrFPolicy(cfg);
const relayBindHost = cfg?.relayBindHost?.trim() || undefined;
return {
enabled,
@ -312,6 +314,7 @@ export function resolveBrowserConfig(
profiles,
ssrfPolicy,
extraArgs,
relayBindHost,
};
}

View File

@ -1168,4 +1168,36 @@ describe("chrome extension relay server", () => {
);
await new Promise<void>((resolve) => blocker.close(() => resolve()));
});
it(
"respects bindHost override to bind on a non-loopback address",
async () => {
const port = await getFreePort();
cdpUrl = `http://127.0.0.1:${port}`;
const relay = await ensureChromeExtensionRelayServer({
cdpUrl,
bindHost: "0.0.0.0",
});
expect(relay.port).toBe(port);
// Relay should be reachable on loopback (0.0.0.0 accepts all interfaces).
const res = await fetch(`http://127.0.0.1:${port}/`);
expect(res.status).toBe(200);
},
RELAY_TEST_TIMEOUT_MS,
);
it(
"defaults bindHost to cdpUrl host when not specified",
async () => {
const port = await getFreePort();
cdpUrl = `http://127.0.0.1:${port}`;
const relay = await ensureChromeExtensionRelayServer({ cdpUrl });
expect(relay.host).toBe("127.0.0.1");
const res = await fetch(`http://127.0.0.1:${port}/`);
expect(res.status).toBe(200);
},
RELAY_TEST_TIMEOUT_MS,
);
});

View File

@ -223,11 +223,13 @@ export function getChromeExtensionRelayAuthHeaders(url: string): Record<string,
export async function ensureChromeExtensionRelayServer(opts: {
cdpUrl: string;
bindHost?: string;
}): Promise<ChromeExtensionRelayServer> {
const info = parseBaseUrl(opts.cdpUrl);
if (!isLoopbackHost(info.host)) {
throw new Error(`extension relay requires loopback cdpUrl host (got ${info.host})`);
}
const bindHost = opts.bindHost ?? info.host;
const existing = relayRuntimeByPort.get(info.port);
if (existing) {
@ -962,7 +964,7 @@ export async function ensureChromeExtensionRelayServer(opts: {
try {
await new Promise<void>((resolve, reject) => {
server.listen(info.port, info.host, () => resolve());
server.listen(info.port, bindHost, () => resolve());
server.once("error", reject);
});
} catch (err) {

View File

@ -117,7 +117,10 @@ export function createProfileAvailability({
if (isExtension) {
if (!httpReachable) {
await ensureChromeExtensionRelayServer({ cdpUrl: profile.cdpUrl });
await ensureChromeExtensionRelayServer({
cdpUrl: profile.cdpUrl,
bindHost: current.resolved.relayBindHost,
});
if (!(await isHttpReachable(PROFILE_ATTACH_RETRY_TIMEOUT_MS))) {
throw new Error(
`Chrome extension relay for profile "${profile.name}" is not reachable at ${profile.cdpUrl}.`,

View File

@ -16,7 +16,10 @@ export async function ensureExtensionRelayForProfiles(params: {
if (!profile || profile.driver !== "extension") {
continue;
}
await ensureChromeExtensionRelayServer({ cdpUrl: profile.cdpUrl }).catch((err) => {
await ensureChromeExtensionRelayServer({
cdpUrl: profile.cdpUrl,
bindHost: params.resolved.relayBindHost,
}).catch((err) => {
params.onWarn(`Chrome extension relay init failed for profile "${name}": ${String(err)}`);
});
}

View File

@ -66,4 +66,10 @@ export type BrowserConfig = {
* Example: ["--window-size=1920,1080", "--disable-infobars"]
*/
extraArgs?: string[];
/**
* Bind address for the Chrome extension relay server.
* Default: "127.0.0.1". Set to "0.0.0.0" for WSL2 or other environments where
* the relay must be reachable from a different network namespace.
*/
relayBindHost?: string;
};

View File

@ -372,6 +372,7 @@ export const OpenClawSchema = z
)
.optional(),
extraArgs: z.array(z.string()).optional(),
relayBindHost: z.string().optional(),
})
.strict()
.optional(),