Skip to content

fix(gateway,proxy): bypass Windows proxy for localhost gateway connections#73474

Closed
DhtIsCoding wants to merge 6 commits intoopenclaw:mainfrom
DhtIsCoding:fix/windows-proxy-bypass-for-localhost
Closed

fix(gateway,proxy): bypass Windows proxy for localhost gateway connections#73474
DhtIsCoding wants to merge 6 commits intoopenclaw:mainfrom
DhtIsCoding:fix/windows-proxy-bypass-for-localhost

Conversation

@DhtIsCoding
Copy link
Copy Markdown
Contributor

@DhtIsCoding DhtIsCoding commented Apr 28, 2026

Summary

Fix an issue where OpenClaw CLI fails to connect to the local gateway at ws://127.0.0.1:18789 or ws://localhost:18789 when Windows proxy environment variables are inherited by WSL.

Problem

When running OpenClaw CLI in WSL on Windows, connections to the local gateway timeout because the HTTP/WebSocket client respects the Windows proxy settings even for localhost connections.

Changes

src/gateway/client.ts

  • Use isLoopbackHost instead of isLoopbackIpAddress for better localhost detection

src/infra/net/proxy/proxy-lifecycle.ts

  • Clear proxy environment variables when OpenClaw proxy is inactive but system proxy may be set

Testing

Build: pnpm build - PASSED

@openclaw-barnacle openclaw-barnacle Bot added gateway Gateway runtime size: XS labels Apr 28, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 28, 2026

Greptile Summary

This PR attempts to fix Windows proxy env vars being inherited by WSL and breaking gateway loopback connections. The 127.0.0.1 case is correctly improved by the env-var clearing in proxy-lifecycle.ts, but two P1 defects affect the localhost fix and the env-var restore logic.

  • localhost connections will throw: createDirectGatewayAgent now returns a direct agent for localhost, but isGatewayLoopbackControlPlaneUrl (in proxy-lifecycle.ts line 374) still uses isLoopbackIpAddress and rejects \"localhost\", causing dangerouslyBypassManagedProxyForGatewayLoopbackControlPlane to throw for any ws://localhost:… URL.
  • http_proxy/https_proxy are not restored after bypass: lowercaseKeys duplicates keys already in ALL_PROXY_ENV_KEYS; the second save loop overwrites the snapshots with undefined, so these vars are permanently deleted from process.env after any bypass call, breaking system proxy routing for all non-gateway traffic in the same process.

Confidence Score: 3/5

Not safe to merge — the localhost path throws an error and the env-var restore is broken, corrupting proxy state for non-gateway connections.

Two independent P1 defects on the changed code paths: one causes an exception for the primary use-case (localhost gateway URLs) and the other permanently destroys http_proxy/https_proxy process-wide after each bypass call. Both need fixes before merging.

src/infra/net/proxy/proxy-lifecycle.ts (both the isGatewayLoopbackControlPlaneUrl guard and the duplicate-key save/restore); src/gateway/client.ts (consequential on the guard fix).

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/gateway/client.ts
Line: 102-103

Comment:
**`localhost` bypass throws instead of connecting**

`createDirectGatewayAgent` now returns a non-`undefined` agent for `ws://localhost:18789` (via `isLoopbackHost`), which causes `dangerouslyBypassManagedProxyForGatewayLoopbackControlPlane` to be called with that URL. However, `isGatewayLoopbackControlPlaneUrl` in `proxy-lifecycle.ts` (line 374) still calls `isLoopbackIpAddress(url.hostname)`, which returns `false` for `"localhost"`, so the function throws `"proxy: dangerous Gateway control-plane bypass is loopback-only"`. Any connection attempt to `ws://localhost:18789` will fail with an exception rather than being fixed.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/infra/net/proxy/proxy-lifecycle.ts
Line: 392-414

Comment:
**Duplicate keys corrupt save/restore for `http_proxy` and `https_proxy`**

`ALL_PROXY_ENV_KEYS` already includes `"http_proxy"` and `"https_proxy"` (via `PROXY_ENV_KEYS`). The first save-and-delete loop correctly snapshots their values and removes them from `process.env`. The second loop then overwrites `savedProxyEnv["http_proxy"]` and `savedProxyEnv["https_proxy"]` with `undefined` (the env vars are already gone), so the restore loop for `ALL_PROXY_ENV_KEYS` sees `undefined` and calls `delete` instead of restoring the original values. After any gateway bypass call, `http_proxy` and `https_proxy` are permanently cleared for all subsequent connections in the process — silently breaking system proxy routing for non-gateway traffic.

The `lowercaseKeys` array should only contain keys not already present in `ALL_PROXY_ENV_KEYS`. The only genuinely new key here is `"all_proxy"`.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(proxy): clear system proxy env vars ..." | Re-trigger Greptile

Comment thread src/gateway/client.ts
Comment on lines +102 to 103
if (!isLoopbackHost(hostname)) {
return undefined;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 localhost bypass throws instead of connecting

createDirectGatewayAgent now returns a non-undefined agent for ws://localhost:18789 (via isLoopbackHost), which causes dangerouslyBypassManagedProxyForGatewayLoopbackControlPlane to be called with that URL. However, isGatewayLoopbackControlPlaneUrl in proxy-lifecycle.ts (line 374) still calls isLoopbackIpAddress(url.hostname), which returns false for "localhost", so the function throws "proxy: dangerous Gateway control-plane bypass is loopback-only". Any connection attempt to ws://localhost:18789 will fail with an exception rather than being fixed.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/client.ts
Line: 102-103

Comment:
**`localhost` bypass throws instead of connecting**

`createDirectGatewayAgent` now returns a non-`undefined` agent for `ws://localhost:18789` (via `isLoopbackHost`), which causes `dangerouslyBypassManagedProxyForGatewayLoopbackControlPlane` to be called with that URL. However, `isGatewayLoopbackControlPlaneUrl` in `proxy-lifecycle.ts` (line 374) still calls `isLoopbackIpAddress(url.hostname)`, which returns `false` for `"localhost"`, so the function throws `"proxy: dangerous Gateway control-plane bypass is loopback-only"`. Any connection attempt to `ws://localhost:18789` will fail with an exception rather than being fixed.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread src/infra/net/proxy/proxy-lifecycle.ts Outdated
Comment on lines +392 to +414
const lowercaseKeys = ["http_proxy", "https_proxy", "all_proxy"];
for (const key of lowercaseKeys) {
savedProxyEnv[key] = process.env[key];
delete process.env[key];
}
try {
return run();
} finally {
for (const key of ALL_PROXY_ENV_KEYS) {
if (savedProxyEnv[key] !== undefined) {
process.env[key] = savedProxyEnv[key];
} else {
delete process.env[key];
}
}
for (const key of lowercaseKeys) {
if (savedProxyEnv[key] !== undefined) {
process.env[key] = savedProxyEnv[key];
} else {
delete process.env[key];
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Duplicate keys corrupt save/restore for http_proxy and https_proxy

ALL_PROXY_ENV_KEYS already includes "http_proxy" and "https_proxy" (via PROXY_ENV_KEYS). The first save-and-delete loop correctly snapshots their values and removes them from process.env. The second loop then overwrites savedProxyEnv["http_proxy"] and savedProxyEnv["https_proxy"] with undefined (the env vars are already gone), so the restore loop for ALL_PROXY_ENV_KEYS sees undefined and calls delete instead of restoring the original values. After any gateway bypass call, http_proxy and https_proxy are permanently cleared for all subsequent connections in the process — silently breaking system proxy routing for non-gateway traffic.

The lowercaseKeys array should only contain keys not already present in ALL_PROXY_ENV_KEYS. The only genuinely new key here is "all_proxy".

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/net/proxy/proxy-lifecycle.ts
Line: 392-414

Comment:
**Duplicate keys corrupt save/restore for `http_proxy` and `https_proxy`**

`ALL_PROXY_ENV_KEYS` already includes `"http_proxy"` and `"https_proxy"` (via `PROXY_ENV_KEYS`). The first save-and-delete loop correctly snapshots their values and removes them from `process.env`. The second loop then overwrites `savedProxyEnv["http_proxy"]` and `savedProxyEnv["https_proxy"]` with `undefined` (the env vars are already gone), so the restore loop for `ALL_PROXY_ENV_KEYS` sees `undefined` and calls `delete` instead of restoring the original values. After any gateway bypass call, `http_proxy` and `https_proxy` are permanently cleared for all subsequent connections in the process — silently breaking system proxy routing for non-gateway traffic.

The `lowercaseKeys` array should only contain keys not already present in `ALL_PROXY_ENV_KEYS`. The only genuinely new key here is `"all_proxy"`.

How can I resolve this? If you propose a fix, please make it concise.

@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 28, 2026

Codex review: needs maintainer review before merge.

Keep this PR open. Current main still implements and documents literal-loopback-IP-only Gateway proxy bypass behavior, while this PR proposes a focused but security-sensitive broadening to localhost plus no-snapshot proxy-env cleanup for Windows/WSL. The discussion shows active maintainer review, not obsolete work.

Maintainer follow-up before merge:

Keep this PR open for normal maintainer review. If maintainers accept localhost as a Gateway control-plane loopback bypass target, land a focused implementation that updates source, tests, and docs/security/network-proxy.md, with explicit no-snapshot env clear/restore regression coverage. If maintainers keep the literal-IP security boundary, close this PR with the documented workaround of using ws://127.0.0.1:18789 or ws://[::1]:18789.

Best possible solution:

Keep this PR open for normal maintainer review. If maintainers accept localhost as a Gateway control-plane loopback bypass target, land a focused implementation that updates source, tests, and docs/security/network-proxy.md, with explicit no-snapshot env clear/restore regression coverage. If maintainers keep the literal-IP security boundary, close this PR with the documented workaround of using ws://127.0.0.1:18789 or ws://[::1]:18789.

Acceptance criteria:

  • pnpm test src/gateway/gateway-misc.test.ts src/infra/net/proxy/proxy-lifecycle.test.ts
  • pnpm test src/infra/net/proxy/external-proxy.e2e.test.ts
  • pnpm exec oxfmt --check --threads=1 src/gateway/client.ts src/infra/net/proxy/proxy-lifecycle.ts src/gateway/gateway-misc.test.ts src/infra/net/proxy/proxy-lifecycle.test.ts docs/security/network-proxy.md
  • pnpm check:changed in Testbox before handoff if the code/docs change is accepted

What I checked:

  • Current main direct Gateway agent excludes localhost: createDirectGatewayAgent() still gates the explicit direct WebSocket agent with isLoopbackIpAddress(hostname), so ws://localhost:... does not take the direct Gateway control-plane path on current main. (src/gateway/client.ts:104, a1197b907524)
  • Current main only calls the proxy bypass when a direct agent exists: Gateway WebSocket construction wraps createWebSocket in dangerouslyBypassManagedProxyForGatewayLoopbackControlPlane() only when createDirectGatewayAgent() returned an agent, so the localhost exclusion is behaviorally relevant. (src/gateway/client.ts:296, a1197b907524)
  • Current main proxy guard rejects localhost: isGatewayLoopbackControlPlaneUrl() still returns isLoopbackIpAddress(url.hostname), and the no-snapshot branch returns run() without clearing inherited proxy env vars. (src/infra/net/proxy/proxy-lifecycle.ts:374, a1197b907524)
  • Current tests encode literal-IP-only behavior: The Gateway test expects no direct agent for ws://localhost:1, and the proxy lifecycle test expects ws://localhost:18789 to throw loopback-only. (src/gateway/gateway-misc.test.ts:108, a1197b907524)
  • Current docs document the current restriction: The network proxy docs say the direct Gateway control-plane path is limited to literal loopback IP URLs and that localhost routes like ordinary hostname-based traffic. Public docs: docs/security/network-proxy.md. (docs/security/network-proxy.md:153, a1197b907524)
  • PR discussion is active maintainer review: Provided GitHub context shows Greptile flagged early defects, then BradGroux said the Windows/WSL direction and localhost boundary look reasonable if the no-snapshot branch clears/restores the full relevant proxy env surface. The PR has later commits, so this is active review context rather than cleanup-close evidence. (c7ca8be2e4fe)

Likely related people:

  • steipete: Local blame on src/gateway/client.ts, src/infra/net/proxy/proxy-lifecycle.ts, their tests, and docs/security/network-proxy.md points the current literal-loopback-IP behavior and docs to Peter Steinberger's recent current-main commits. (role: recent maintainer/current-main owner; confidence: high; commits: a5cb171d73ea, 8d58ad4c15cd; files: src/gateway/client.ts, src/infra/net/proxy/proxy-lifecycle.ts, src/gateway/gateway-misc.test.ts)
  • jesse-merhi: Provided feat(security): support operator-managed network proxy routing #70044 context identifies the merged operator-managed network proxy routing feature as the origin of this proxy lifecycle surface, including the narrow Gateway loopback bypass contract this PR changes. (role: feature-history owner; confidence: medium; commits: e19ce89414d0; files: src/infra/net/proxy/proxy-lifecycle.ts, src/gateway/client.ts, docs/security/network-proxy.md)
  • BradGroux: Reviewed this exact PR discussion and gave the current product/security direction: localhost can be reasonable for Windows/WSL if the no-snapshot branch clears and restores the full relevant proxy env surface. (role: active maintainer reviewer; confidence: medium; files: src/infra/net/proxy/proxy-lifecycle.ts, src/gateway/client.ts)
  • joshavant: Provided ClawSweeper context links joshavant to prior loopback restriction/proxy hardening discussion around this Gateway/proxy boundary, making them a useful routing candidate if maintainers revisit the security contract. (role: adjacent feature-history reviewer; confidence: medium; commits: 63f0b97f4640, 4a28101903a1, 7ae2008fee01; files: src/infra/net/proxy/proxy-lifecycle.ts, docs/security/network-proxy.md)

Remaining risk / open question:

  • The PR broadens a security-sensitive Gateway control-plane proxy bypass from literal loopback IPs to localhost; docs and tests should make that policy change explicit before merge.
  • The provided diff changes localhost behavior but visible test changes do not yet prove the no-snapshot env branch clears and restores HTTP_PROXY, HTTPS_PROXY, NO_PROXY, all_proxy, and ALL_PROXY across success and throw paths.
  • The PR imports isLoopbackHost from gateway/net into infra/net/proxy; maintainers may prefer moving the generic hostname helper into shared/net if layering/import-cycle checks object.

Codex review notes: model gpt-5.5, reasoning high; reviewed against a1197b907524.

…tions

- Use isLoopbackHost instead of isLoopbackIpAddress in createDirectGatewayAgent
- Use isLoopbackHost in isGatewayLoopbackControlPlaneUrl for proper localhost handling
- Clear proxy environment variables when OpenClaw proxy is inactive but system proxy may be set
- Remove duplicate keys from lowercaseKeys to prevent env var save/restore corruption

Fixes: Windows proxy inherited by WSL causing gateway connection timeouts
@DhtIsCoding DhtIsCoding force-pushed the fix/windows-proxy-bypass-for-localhost branch from ac2dd10 to e384d7c Compare April 29, 2026 06:24
@BradGroux
Copy link
Copy Markdown
Member

Thanks for working on this. The direction makes sense for the Windows plus WSL failure mode: both ws://127.0.0.1 and ws://localhost Gateway control-plane calls should avoid an inherited proxy, and the added tests cover the intended localhost behavior in both the Gateway client and managed proxy bypass paths.

I want one follow-up before this is merge-ready. In dangerouslyBypassManagedProxyForGatewayLoopbackControlPlane, the new no-snapshot branch only clears ALL_PROXY_ENV_KEYS plus lowercase all_proxy. The reported failure is about Windows proxy env leaking into WSL and the code comments around this helper already treat HTTP, HTTPS, and NO_PROXY env as part of the proxy surface. Please clear and restore the full relevant proxy env set in this branch as well, not only the all-proxy keys. That keeps the no-snapshot fallback aligned with the managed proxy snapshot path and avoids leaving HTTP_PROXY, HTTPS_PROXY, NO_PROXY, or their lowercase variants active during the direct loopback call.

Once that is tightened, the localhost boundary itself looks reasonable. The important security constraint is still preserved because non-loopback URLs continue to throw loopback-only, and the bypass remains scoped to Gateway control-plane loopback traffic rather than general outbound requests.

@steipete
Copy link
Copy Markdown
Contributor

Thanks @DhtIsCoding. I landed the accepted version of this directly on main in bdcd543, so I’m closing this PR as superseded rather than merging the stale branch.

What landed:

  • src/gateway/client.ts now treats ws://localhost:... like the existing loopback-IP Gateway control-plane path, so local Gateway clients get an explicit direct agent instead of inheriting proxy routing.
  • src/infra/net/proxy/proxy-lifecycle.ts now allows the narrow control-plane bypass for localhost, 127.0.0.1, and [::1], while still rejecting non-loopback URLs.
  • The bypass now temporarily clears and restores the full relevant proxy env surface, including HTTP_PROXY, HTTPS_PROXY, lowercase variants, ALL_PROXY/all_proxy, NO_PROXY, global-agent vars, and OPENCLAW_PROXY_ACTIVE, addressing the review concern about partial env cleanup.
  • docs/security/network-proxy.md, CHANGELOG.md, and regression coverage were updated.

Verification run before landing:

  • pnpm test src/infra/net/proxy/proxy-lifecycle.test.ts src/gateway/gateway-misc.test.ts -- --reporter=verbose
  • pnpm test src/infra/net/proxy/external-proxy.e2e.test.ts -- --reporter=verbose
  • pnpm exec oxfmt --check --threads=1 src/gateway/client.ts src/infra/net/proxy/proxy-lifecycle.ts src/gateway/gateway-misc.test.ts src/infra/net/proxy/proxy-lifecycle.test.ts
  • pnpm tsgo:core
  • pnpm tsgo:core:test
  • git diff --check

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gateway Gateway runtime size: XS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants