[WSLC] Support network.proxy via cooperative env vars - #685
Conversation
Enable `network.proxy` for the WSLC backend using the same cooperative env-var model as Bubblewrap (Linux) and Seatbelt (macOS): inject HTTP_PROXY / HTTPS_PROXY / ALL_PROXY (+ lowercase aliases) into the container environment, after stripping any caller-supplied proxy entry so a config cannot supply conflicting values or re-enable a NO_PROXY bypass. Implements roadmap items microsoft#19 and microsoft#25. The WSLC container runs inside the WSL2 VM -- a separate kernel with its own loopback, behind NAT -- so a loopback proxy address names the container itself, never the Windows host where the proxy listens. Rather than rewrite loopback to a gateway IP (which would silently assume the consumer bound the proxy to 0.0.0.0), the parser rejects the shapes that cannot work: `builtinTestServer`, `localhost`, and loopback `url` hosts. It also rejects `defaultPolicy: "block"` (maps to NetworkingMode::None, leaving no interface at all) and host lists alongside a proxy (MXC does not forward them to an external proxy, and WSLC cannot enforce them -- its in-container iptables path needs CAP_NET_ADMIN). The proxy is advisory: WSLC cannot restrict egress to the proxy port, so raw-socket clients bypass it. The run logs a warning saying so. Also hoist `env_cstrings` / `env_ptrs` to function scope. `process_settings` holds raw pointers into those buffers until `WslcCreateContainer` consumes them much later, but they were block-scoped and dropped first -- the same lifetime requirement `script_cstr` and `_cwd_cstr` are already hoisted for. Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 40eb13a2-1a2c-465d-9733-51d800dc43c6
There was a problem hiding this comment.
Pull request overview
Adds cooperative proxy environment-variable support to the WSLC backend.
Changes:
- Injects sanitized proxy variables into WSLC processes.
- Adds backend-specific proxy validation and warnings.
- Updates tests, examples, SDK types, and documentation.
Show a summary per file
| File | Description |
|---|---|
tests/configs/wslc_network_proxy_env.json |
Adds a proxy hygiene test configuration. |
src/core/wxc_common/src/config_parser.rs |
Enables and validates WSLC proxy configurations. |
src/backends/wslc/common/src/wsl_container_runner.rs |
Applies proxy environment variables and fixes buffer lifetimes. |
src/backends/wslc/common/src/policy_mapping.rs |
Builds sanitized WSLC process environments. |
sdk/node/src/types.ts |
Documents WSLC proxy constraints. |
docs/wsl/wsl-container-getting-started.md |
Documents cooperative proxy usage. |
docs/schema.md |
Notes WSLC proxy URL requirements. |
docs/linux-wsl-roadmap-june-2026.md |
Updates proxy roadmap status and limitations. |
Review details
- Files reviewed: 8/8 changed files
- Comments generated: 2
- Review effort level: Medium
| if containment == ContainmentBackend::Wslc | ||
| && policy.network_proxy.address.as_ref().is_some_and(|addr| { | ||
| matches!(addr.host(), "127.0.0.1" | "::1" | "[::1]" | "localhost") | ||
| }) |
There was a problem hiding this comment.
Good catch — fixed in 5f9701b. This was a real hole: 127.0.0.2 (and the rest of 127.0.0.0/8) passed validation and would have been injected as an unreachable HTTP_PROXY.
Replaced the literal match with an is_local_only_host() helper that strips the bracketed IPv6 form, parses the host as IpAddr, and tests is_loopback(). Two adjacent cases the literal match also missed, now covered:
- IPv4-mapped IPv6 (
::ffff:127.0.0.1) —Ipv6Addr::is_loopback()returnsfalsefor it, so it needs an explicitto_ipv4_mapped()check. - Unspecified (
0.0.0.0,::) — also resolves to the local host, so it is unreachable in exactly the same way.
localhost is now compared case-insensitively.
The rejection test now covers 127.0.0.2, 127.255.255.254, LocalHost, [::ffff:127.0.0.1], 0.0.0.0 and [::]. I also added proxy_routable_url_accepted_with_wslc as a guard against over-rejection, so addresses that merely look loopback-ish (10.0.0.5, 172.16.0.1, the hostname 127-0-0-1.example.com, [2001:db8::1]) are asserted to still pass.
| (Linux) and Seatbelt (macOS) backends use. Any caller-supplied proxy variable | ||
| (including `NO_PROXY`) is stripped first so sandboxed code cannot override or | ||
| bypass the configured proxy. |
There was a problem hiding this comment.
You're right, and thanks for catching it — #652 predates this by 12 days and covers the same ground. I missed it; I worked from the roadmap doc (#19/#25) and didn't check for an existing PR. Apologies for the duplicated effort.
Having read #652, it is the better of the two and I'd suggest closing this one in favour of it:
- It puts the shared logic in
wxc_common/src/proxy_env.rsand refactors Bubblewrap onto it. Mine copied the key lists into the WSLC backend, which is a third place to drift. - It has a real end-to-end test (
run_wslc_proxy_test.ps1+wslc_network_proxy.json) that proves injection and scrubbing on actual hardware. I couldn't run WSLC at all — I'm on macOS — so mine is unit-tested only.
One substantive difference worth flagging before you requeue #652, because I think my side is the wrong call and it would have broken your test:
- [WSLC] Add cooperative HTTP/HTTPS proxy support #652 rejects only the
localhost/builtinTestServerforms (viaoriginal_url.is_some()), so aurlon127.0.0.1is allowed — which is exactly whatwslc_network_proxy.jsonrelies on, with the marker proxy running inside the container where loopback is reachable. - This PR additionally rejected every loopback
url, on the assumption the proxy always runs on the Windows host. That assumption is wrong: it forecloses the in-container topology you've actually validated.
So #652's narrower check is the correct one. The only thing I'd offer from here is the FTP_PROXY observation — GA item #25 lists it, but neither PR strips it (both mirror Bubblewrap/Seatbelt). That's fine as a deliberate cross-backend decision, just worth stating explicitly somewhere rather than leaving #25 looking fully closed.
Regarding the original comment on this line: I did fix the overstatement in 5f9701b (scoped the claim to the eight variables actually stripped, and noted FTP_PROXY is not among them) — but that's moot if this PR closes. Happy to close it; leaving that to you since it's your feature.
There was a problem hiding this comment.
Going ahead a closing it, thanks!
Address review feedback on PR microsoft#685. The loopback guard matched three literal host strings, so any other local-only address passed validation and would be injected as an unreachable HTTP_PROXY: `127.0.0.2`, anything else in 127.0.0.0/8, IPv4-mapped `::ffff:127.0.0.1` (for which `Ipv6Addr::is_loopback()` is false), and the unspecified `0.0.0.0` / `::`. Replace the literal match with `is_local_only_host()`, which strips the bracketed IPv6 form, parses the host as an `IpAddr`, and tests `is_loopback()` / `is_unspecified()` plus the IPv4-mapped case. Hostname comparison for `localhost` is now case-insensitive. Extend the rejection test to cover 127.0.0.2, 127.255.255.254, mixed-case LocalHost, `[::ffff:127.0.0.1]`, 0.0.0.0 and `[::]`, and add a positive test so routable addresses that merely look loopback-ish (10.x, 172.16.x, a 127-prefixed hostname, a global IPv6) are not over-rejected. Also scope the docs' env-var hygiene claim to the eight variables actually stripped, instead of implying every proxy variable is removed -- FTP_PROXY is deliberately left alone to stay in sync with Bubblewrap and Seatbelt. Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 40eb13a2-1a2c-465d-9733-51d800dc43c6
|
I already had a pr out for cooperative proxy support in wslc- #652 |
| if host.eq_ignore_ascii_case("localhost") { | ||
| return true; | ||
| } |
Important
Likely superseded by #652. @SohamDas2021 pointed out that #652 already implements WSLC cooperative proxy support and predates this PR by 12 days. I missed it — I worked from the roadmap doc rather than checking open PRs.
#652 is the stronger implementation: it factors the shared logic into
wxc_common/src/proxy_env.rsand moves Bubblewrap onto it (this PR instead copied the key lists into the WSLC backend, creating a third place to drift), and it ships an end-to-end Windows test that proves injection and scrubbing on real hardware, which I could not run.There is also a behavioural conflict: this PR rejects every loopback proxy
url, assuming the proxy runs on the Windows host. #652 allows them, and its E2E test deliberately runs the proxy inside the container at127.0.0.1:8888, where loopback is reachable. #652's narrower check (reject only thelocalhost/builtinTestServerforms) is the correct one — my broader rejection would break that working topology.Recommend closing this in favour of #652. Left open pending the maintainer's call. See this thread.
📖 Description
Enables
network.proxyfor the WSLC backend using the same cooperative env-var model as Bubblewrap (Linux) and Seatbelt (macOS). Implements roadmap items #19 (proxy env-var injection) and #25 (env-var hygiene) fromdocs/linux-wsl-roadmap-june-2026.md.What it does
policy_mapping::build_process_env()strips every caller-suppliedHTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXY(+ lowercase) fromrequest.env, then injects the configured proxy URL into the six inject keys.NO_PROXYis deliberately never re-injected, so a config cannot carve out a bypass.WslcSetProcessSettingsEnvVariablespath inwsl_container_runner.rs.config_parser.rsaddswslcto the proxy-capable backends.Why some shapes are rejected rather than accepted
The WSLC container runs inside the WSL2 VM — a separate kernel with its own loopback, behind NAT — so a loopback proxy address names the container itself, never the Windows host where the proxy listens (documented at
docs/linux-wsl-roadmap-june-2026.md, the "NAT reachability" note). Rewriting loopback to the VM-visible gateway IP was considered and rejected: it would silently assume the consumer bound the proxy to0.0.0.0rather than127.0.0.1, an unverifiable assumption. Instead the parser fails fast:{ "localhost": <port> }, or aurlon127.0.0.1/::1/[::1]/localhost{ "builtinTestServer": true }unix-test-proxyis a Unix binary while WSLC is Windows-only.defaultPolicy: "block"WslcContainerNetworkingMode::None— no network interface at all, so nothing could reach the proxy.allowedHosts/blockedHostsalongsideproxySo the accepted shape is
network.proxy.urlwith a VM-routable address plusdefaultPolicy: "allow".Limitations
iptablesneedsCAP_NET_ADMIN, which the SDK'sPrivilegedflag does not grant — roadmap Forward the environment provided from the CLI. #20, SDK-blocked), so raw-socket clients that ignoreHTTP_PROXYreach the network directly. The run logs a warning saying exactly this.FTP_PROXY/ftp_proxyare not stripped, even though GA item Fix bfscfg argument quoting for paths with spaces #25 mentions them. The key list deliberately stays byte-identical to Bubblewrap's and Seatbelt'sPROXY_ENV_KEYSso the three cooperative-proxy backends don't drift; addingFTP_PROXYshould be one change across all three. Noted as a deviation in the roadmap doc rather than silently claimed as done.Drive-by fix (same code path)
env_cstrings/env_ptrswere block-scoped, butprocess_settingsholds raw pointers into those buffers untilWslcCreateContainerconsumes them much later — the exact lifetime requirementscript_cstrand_cwd_cstrare already hoisted for, and called out by the surrounding comment. Hoisted to function scope to match.The new log line reports
host:portrather thanProxyAddress::to_url(), since anetwork.proxy.urlmay embed userinfo that must not reach the log.🔗 References
docs/linux-wsl-roadmap-june-2026.md(status updated in this PR)backends/bubblewrap/common/src/bwrap_command.rs,backends/seatbelt/common/src/seatbelt_runner.rs🔍 Validation
Automated:
cargo test -p wxc_common— 400 passed. Includes 6 new parser tests covering the acceptedurlshape, all three rejection classes (builtinTestServer,localhost, loopbackurlincl.[::1]), and a regression test that WSLC host lists without a proxy still parse.cargo fmt --all -- --check— clean.cargo clippy -D warnings— clean natively and forx86_64-pc-windows-msvc, including-p wxc -p mxc_engine --features wslc.node scripts/versioning/{validate-configs,check-schema-versions,check-schema-codegen,check-sdk-types-codegen,check-rust-toolchain-sync}.jsandscripts/check-version-sync.js— all OK. No wire-model or schema changes, so no codegen drift.tests/configs/wslc_network_proxy_env.jsonvalidates against the dev schema and was confirmed to parse through the realload_requestpath.Review round (commit
5f9701b) — both Copilot review comments addressed:127.0.0.2(and the rest of127.0.0.0/8) passed validation and would have been injected as an unreachableHTTP_PROXY. Replaced withis_local_only_host(), which parses the host as anIpAddrand testsis_loopback()/is_unspecified(), also covering IPv4-mapped::ffff:127.0.0.1(for whichIpv6Addr::is_loopback()isfalse) and case-insensitivelocalhost. Rejection test extended; addedproxy_routable_url_accepted_with_wslcto guard against over-rejection. (Note: per the banner above, this widening is itself the wrong call — see [WSLC] Add cooperative HTTP/HTTPS proxy support #652.)FTP_PROXY/ftp_proxyare not among them.cargo test -p wxc_commonnow 401 passing.Not covered — please flag if you want these before merge:
WslcSetProcessSettingsEnvVariablesround-trip and a live VM-routable proxy are unverified.tests/configs/wslc_network_proxy_env.jsonis designed for exactly this: it seeds a hostileHTTP_PROXYplusNO_PROXYinprocess.envand prints the surviving proxy vars, so it demonstrates both injection (Bump minimatch from 10.2.2 to 10.2.4 in /sdk #19) and hygiene (Fix bfscfg argument quoting for paths with spaces #25) in one run.cargo test -p wslc_commonwas not executed — thewindowscrate does not build natively on macOS. The addedbuild_process_envunit tests were verified by extracting the function and its tests verbatim into a standalonerustc --testharness (5 passed); they should be run for real on a Windows host.✅ Checklist
Cargo.lock, thedependency-feed-checkcheck passes (see docs/pull-requests.md)Notes on the unchecked items: there is no tracking issue for this work (it is driven by the roadmap doc), and
.github/copilot-instructions.mdneeded no update — it has no WSLC row in the backend table, and this PR changes no build commands, architecture, or conventions.Cargo.lockis untouched (no new dependencies).Documentation updated:
docs/wsl/wsl-container-getting-started.md(new "Cooperative proxy" section with the constraint table),docs/schema.md, roadmap status for #19/#25, and thenetwork.proxyJSDoc insdk/node/src/types.ts.📋 Issue Type
Microsoft Reviewers: Open in CodeFlow