diff --git a/src/browser/config.ts b/src/browser/config.ts index f6b6e1b6d01..6d24a07a287 100644 --- a/src/browser/config.ts +++ b/src/browser/config.ts @@ -36,6 +36,7 @@ export type ResolvedBrowserConfig = { profiles: Record; 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, }; } diff --git a/src/browser/extension-relay.test.ts b/src/browser/extension-relay.test.ts index b1478feabd4..7208d0dd10b 100644 --- a/src/browser/extension-relay.test.ts +++ b/src/browser/extension-relay.test.ts @@ -1168,4 +1168,36 @@ describe("chrome extension relay server", () => { ); await new Promise((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, + ); }); diff --git a/src/browser/extension-relay.ts b/src/browser/extension-relay.ts index 126bfc8f682..2107fff4a32 100644 --- a/src/browser/extension-relay.ts +++ b/src/browser/extension-relay.ts @@ -223,11 +223,13 @@ export function getChromeExtensionRelayAuthHeaders(url: string): Record { 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((resolve, reject) => { - server.listen(info.port, info.host, () => resolve()); + server.listen(info.port, bindHost, () => resolve()); server.once("error", reject); }); } catch (err) { diff --git a/src/browser/server-context.availability.ts b/src/browser/server-context.availability.ts index 47865903b96..07772c6b598 100644 --- a/src/browser/server-context.availability.ts +++ b/src/browser/server-context.availability.ts @@ -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}.`, diff --git a/src/browser/server-lifecycle.ts b/src/browser/server-lifecycle.ts index 64d10cb7b9f..10a4569095a 100644 --- a/src/browser/server-lifecycle.ts +++ b/src/browser/server-lifecycle.ts @@ -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)}`); }); } diff --git a/src/config/types.browser.ts b/src/config/types.browser.ts index 192fd700bff..57d036bd88c 100644 --- a/src/config/types.browser.ts +++ b/src/config/types.browser.ts @@ -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; }; diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index 62b7f2f1513..de4a503cdbf 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -372,6 +372,7 @@ export const OpenClawSchema = z ) .optional(), extraArgs: z.array(z.string()).optional(), + relayBindHost: z.string().optional(), }) .strict() .optional(),