fix(gvisor): skip iptables and route MCP gateway via NO_PROXY#6401
Conversation
gVisor's isolated userspace netstack is not governed by the host-netns iptables DNAT/RETURN rules that awf-iptables-init installs, so egress (including the MCP gateway bypass at 172.30.0.1) silently failed under gVisor: MCP requests were routed through Squid and rejected with 403, leaving the agent unable to emit safe outputs. Add a usesIptables runtime capability (false for gvisor/sbx) that: - skips the awf-iptables-init container - sets AWF_SKIP_IPTABLES_INIT=1 so the entrypoint skips the init handshake - adds the network gateway + host.docker.internal to NO_PROXY so proxy-aware MCP clients reach the gateway directly Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: be040aef-5830-42b1-a871-4cade09f4585
✅ Coverage Check PassedOverall Coverage
📁 Per-file Coverage Changes (2 files)
Coverage comparison generated by |
There was a problem hiding this comment.
Pull request overview
Adds runtime-aware network handling so gVisor agents can reach MCP gateways without ineffective iptables rules.
Changes:
- Adds the
usesIptablesruntime capability. - Skips iptables initialization and configures gateway
NO_PROXYrouting. - Adds tests and updates gVisor documentation.
Show a summary per file
| File | Description |
|---|---|
src/container-runtime.ts |
Defines iptables runtime capabilities. |
src/container-runtime.test.ts |
Tests capability detection. |
src/services/optional-services.ts |
Conditionally omits iptables initialization. |
src/services/optional-services.test.ts |
Tests the skip marker. |
src/services/agent-environment/proxy-environment.ts |
Adds direct gateway proxy bypasses. |
src/services/agent-environment/proxy-environment.test.ts |
Tests NO_PROXY generation. |
containers/agent/entrypoint.sh |
Skips the readiness handshake when requested. |
docs/gvisor-integration.md |
Documents proxy-based gVisor networking. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comments suppressed due to low confidence (1)
src/services/agent-environment/proxy-environment.ts:27
- This grants every non-iptables runtime direct access to the network gateway even when host access is disabled and the gateway was not allowlisted. Because the same change removes the iptables OUTPUT/port filters, a proxy-aware agent can target any service/port exposed on the host gateway, bypassing both Squid and
allowHostPorts; this also conflicts with strict mode's host-access prohibition. Route MCP through a port-restricted gateway mechanism, or require explicit host/gateway authorization and enforce the allowed ports rather than adding a blanketNO_PROXYhost.
const gatewayNeedsNoProxy = config.enableHostAccess || !runtimeUsesIptables(config.containerRuntime);
if (gatewayNeedsNoProxy) {
const subnetBase = networkConfig.subnet.split('/')[0];
const parts = subnetBase.split('.');
const networkGatewayIp = `${parts[0]}.${parts[1]}.${parts[2]}.1`;
noProxyHosts.push('host.docker.internal', networkGatewayIp);
- Files reviewed: 8/8 changed files
- Comments generated: 4
- Review effort level: Medium
| // Skip the iptables-init container when egress isn't governed by host-netns | ||
| // iptables — either topology-based network isolation, or a runtime whose | ||
| // network stack can't use those rules (e.g. gVisor's isolated netstack). | ||
| const skipIptables = networkIsolation || !runtimeUsesIptables(config.containerRuntime); |
There was a problem hiding this comment.
Thanks — I dug into this and want to correct one premise, then explain the decision.
\u201cNo route to host\u201d does not depend on topology mode. In legacy mode (networkIsolation:false), cli-workflow.ts:86-107 takes the else branch and calls setupHostIptables, which installs the FW_WRAPPER chain and jumps to it from DOCKER-USER (host-iptables-chain.ts:64). DOCKER-USER is in the bridge FORWARD path, and FW_WRAPPER ends in a default-deny REJECT (host-iptables-rules.ts:279-288). That REJECT is what produced the observed No route to host for the gVisor agent on awf-net — it is enforced at the host, is runtime-independent, and does not require the internal topology network. So skipping the container iptables under gVisor does not open a direct outbound internet route: proxy-unaware traffic still hits the host FORWARD default-deny.
Proxy env vars were never the boundary. HTTP_PROXY/NO_PROXY are client-side routing hints the agent fully controls; they can\u2019t be a security control. Squid restricts what proxy-aware clients can reach, but the actual egress boundary is (a) the host DOCKER-USER default-deny in legacy mode and (b) the internal network topology in strict mode. The container-level iptables DNAT that gVisor skips was always defense-in-depth, not the primary enforcement layer. I\u2019ve reworded docs/gvisor-integration.md to say exactly this.
On the host-gateway NO_PROXY entry: the one real nuance is that container\u2192host-gateway (172.30.0.1) is INPUT to the host, not FORWARD, so it isn\u2019t covered by DOCKER-USER. That reachability is a pre-existing property of running with a host-based MCP gateway (it exists for runc host-access too) and is bounded by host INPUT rules, not introduced by this change \u2014 adding the gateway to NO_PROXY only changes routing for cooperating clients, not what\u2019s reachable.
Force-topology / reject: I agree the cleanest long-term posture is running gVisor in strict/topology mode (where the MCP gateway is attached as a topology peer). I deliberately did not force that here because the isolation mode is chosen upstream by gh-aw alongside where it places the MCP gateway; having awf silently override networkIsolation would desync from gh-aw\u2019s gateway placement and can break MCP reachability. The correct fix for the smoke workflows is to move them to sudo: false (strict) on the gh-aw side, which is tracked separately. Happy to add an explicit warning for the legacy + non-iptables-runtime combination if you\u2019d prefer a louder signal in the meantime.
| export function runtimeUsesIptables(runtime: string | undefined): boolean { | ||
| if (!runtime) return true; | ||
| return RUNTIME_REGISTRY[runtime]?.usesIptables ?? true; |
There was a problem hiding this comment.
Fixed in e9a4a6e. runsc (the raw OCI runtime name) is now canonicalized to gVisor capabilities via a RUNTIME_ALIASES table, so resolveDockerRuntime, runtimeNeedsStaticDns, runtimeUsesIptables, and runtimeUsesComposeAgent all treat runsc == gvisor. Also added isGvisorRuntime() and routed the Claude/Bun JIT check through it. Tests cover both names.
| if (includeComposeAgent) { | ||
| assembleSysrootService(params, imageConfig.registry, imageConfig.parsedTag, sysrootActive); | ||
| assembleIptablesInitService(params, networkIsolation); | ||
| assembleIptablesInitService(params, skipIptables); |
There was a problem hiding this comment.
Added in e9a4a6e. compose-generator.test.ts now has a "gVisor runtime" block asserting that a config with networkIsolation: false omits the iptables-init service, retains the agent service, and sets AWF_SKIP_IPTABLES_INIT=1 on the agent env — for both the gvisor and raw runsc names.
| Egress is enforced **solely** through the `HTTP_PROXY`/`HTTPS_PROXY` env vars | ||
| pointing at Squid. Because the iptables NAT bypass for the MCP gateway is gone, | ||
| `proxy-environment.ts` adds the network gateway IP (e.g. `172.30.0.1`) and | ||
| `host.docker.internal` to `NO_PROXY` for non-iptables runtimes, so proxy-aware | ||
| MCP clients (rmcp) connect to the gateway **directly** instead of being routed | ||
| through Squid and rejected with `403 ERR_ACCESS_DENIED`. |
There was a problem hiding this comment.
Reworded in e9a4a6e. The section no longer claims egress is enforced "solely" via proxy env vars. It now distinguishes Squid-routed internet egress from the deliberate direct MCP/host-gateway NO_PROXY exception, and states explicitly that NO_PROXY/HTTP_PROXY are client-side routing hints — not the security boundary — with the enforced boundary being topology (strict mode) or the host DOCKER-USER default-deny FORWARD chain (legacy mode).
- container-runtime: canonicalize raw runsc OCI name to gVisor capabilities via an alias table; add isGvisorRuntime() and use it for the Claude/Bun check. - compose-generator.test: assert gVisor (networkIsolation:false) omits iptables-init, keeps the agent, and sets AWF_SKIP_IPTABLES_INIT (gvisor+runsc). - docs/gvisor-integration: distinguish Squid-routed internet egress from the direct MCP/host-gateway NO_PROXY exception; clarify the enforced boundary is topology (strict) / host DOCKER-USER default-deny (legacy), not proxy env. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: be040aef-5830-42b1-a871-4cade09f4585
|
✅ Copilot review passed with no inline comments. @lpcox Add the |
|
✨ The prophecy is fulfilled... Smoke Codex has completed its mystical journey. The stars align. 🌟 |
|
🔑 Smoke Copilot PAT PAT auth validated. All systems operational. ✅ |
|
✅ Smoke Gemini completed. All facets verified. 💎 Starting smoke test |
|
🔌 Smoke Services — All services reachable! ✅ |
|
✅ Contribution Check completed successfully! |
|
✅ Smoke Copilot BYOK completed. Copilot BYOK mode operational. 🔓 |
|
📰 VERDICT: Smoke Docker Sbx has concluded. All systems operational. This is a developing story. 🎤 |
|
✅ Smoke Copilot BYOK AOAI (Entra) completed. Copilot AOAI BYOK (Entra) mode operational. 🔓 |
|
Chroot tests passed! Smoke Chroot - All security and functionality tests succeeded. |
|
✅ Smoke Copilot BYOK AOAI (api-key) completed. Copilot AOAI BYOK (api-key) mode operational. 🔓 |
|
✅ Smoke Claude passed |
|
📡 Smoke OTel Tracing completed. All tracing scenarios validated. ✅ |
|
❌ Security Guard failed. Please review the logs for details. |
|
✅ Build Test Suite completed successfully! |
|
📰 VERDICT: Smoke Copilot has concluded. All systems operational. This is a developing story. 🎤 |
|
🛡️ Smoke Copilot Network Isolation confirmed the egress allowlist is enforced. ✅ |
Smoke Test: Claude Engine Validation
Overall Result: PASS Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "awmgmcpg"See Network Configuration for more information.
|
|
Smoke test: Copilot network isolation egress EGRESS_RESULT allow=pass deny=pass ✅ Test 1 (allowed domain): Overall: PASS Warning Firewall blocked 2 domainsThe following domains were blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "awmgmcpg"
- "example.com"See Network Configuration for more information.
|
Smoke Test: Copilot PAT Auth — FAIL ❌
Pre-step outputs were not substituted ( Auth mode: PAT (COPILOT_GITHUB_TOKEN) Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "awmgmcpg"See Network Configuration for more information.
|
Smoke Test: Copilot BYOK (Direct) Mode✅ GitHub MCP connectivity Running in direct BYOK mode (COPILOT_PROVIDER_API_KEY) via api-proxy → api.githubcopilot.com Overall Status: PASS Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "awmgmcpg"See Network Configuration for more information.
|
Smoke Test Results
Overall status: FAIL Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "localhost"See Network Configuration for more information.
|
Smoke Test Results
Overall: PASS — engine reachable; template vars not expanded in this run. Author: @lpcox Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "awmgmcpg"See Network Configuration for more information.
|
Smoke Test: Services Connectivity
Overall: FAIL — Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "awmgmcpg"See Network Configuration for more information.
|
|
Looks good overall. One CONTRIBUTING.md item to consider: the PR requirements ask for references to any related issues. If this change corresponds to an issue, please add the issue link in the description. Tests and docs updates are already covered. Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "awmgmcpg"See Network Configuration for more information.
|
|
Smoke test results: PR titles only\n- ✅ Refactor Copilot auth routing to use shared GitHub server host classification\n- ✅ chore: upgrade gh-aw to v0.82.13 (pre-release) and recompile workflows\n- ✅ [CLI Flag Review] 🔍 CLI Flag Consistency Report — 2026-07-19\n- ✅ GitHub title check\n- ✅ File write verification\n- ✅ Build verification\n- Overall status: PASS Warning Firewall blocked 2 domainsThe following domains were blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "awmgmcpg"
- "registry.npmjs.org"See Network Configuration for more information.
|
Chroot Version Comparison Results
Overall: FAILED — Python and Node.js versions differ between host and chroot. Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "awmgmcpg"See Network Configuration for more information.
|
|
GitHub MCP: ✅ Running in direct BYOK mode (AWF_AUTH_TYPE=github-oidc + AWF_AUTH_AZURE_* + COPILOT_PROVIDER_BASE_URL) via api-proxy → Azure OpenAI (Foundry, o4-mini-aw) authenticated via Microsoft Entra Overall: PASS Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "awmgmcpg"See Network Configuration for more information.
|
|
Smoke Test (Direct BYOK Azure OpenAI):
Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "awmgmcpg"See Network Configuration for more information.
|
🏗️ Build Test Suite Results
Overall: 8/8 ecosystems passed — ✅ PASS
Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "awmgmcpg"See Network Configuration for more information.
|
OTEL Smoke Test Results
Overall: ✅ All scenarios pass. Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "awmgmcpg"See Network Configuration for more information.
|
🔥 Smoke Test Results — Docker Sbx
Overall: PASS /cc @lpcox
|
Problem
The
Smoke gVisor Codexworkflow (run 29665726755) failed at "Validate safe outputs were invoked" — the agent never wrote tooutputs.jsonl.Root cause: gVisor's userspace netstack is isolated from the host network namespace, so the host-netns iptables DNAT/RETURN rules that
awf-iptables-initinstalls cannot govern the sandbox's traffic. In particular, the iptables NATRETURNbypass that normally lets the agent reach the MCP gateway (172.30.0.1:8080) directly never fires under gVisor. As a result, MCP requests followedHTTP_PROXYinto Squid and were rejected with403 ERR_ACCESS_DENIED:With the
safeoutputsMCP server unreachable, the agent had no way to emit safe outputs → emptyoutputs.jsonl→ validation step failed. Direct connections (/dev/tcp/github.com/80) also failed with "No route to host" for the same reason.Fix
Introduce a
usesIptablesruntime capability (falseforgvisor/sbx) so runtimes whose network stack can't use host-netns iptables route egress purely through the proxy:src/container-runtime.ts— newusesIptablescapability +runtimeUsesIptables()helper.src/services/optional-services.ts— skip theawf-iptables-initcontainer for non-iptables runtimes and setAWF_SKIP_IPTABLES_INIT=1.containers/agent/entrypoint.sh— honorAWF_SKIP_IPTABLES_INITto skip the init-container ready-file handshake (otherwise the agent would time out andexit 1).src/services/agent-environment/proxy-environment.ts— add the network gateway (172.30.0.1) +host.docker.internaltoNO_PROXYfor non-iptables runtimes, so proxy-aware MCP clients (rmcp) connect to the gateway directly instead of via Squid. This is the piece that fixes the 403.Caveat
Under gVisor there is no iptables DNAT fallback, so proxy-unaware tools (e.g. raw
/dev/tcp) get "No route to host". Egress requires proxy-aware clients. The security posture holds: the only route out is through Squid's allowlist. Documented indocs/gvisor-integration.md.Testing
npm test— 246 suites / 3893 tests passnpm run buildclean,npm run lint0 errorsruntimeUsesIptables,presetSidecarIpEnvVars(gVisor setsAWF_SKIP_IPTABLES_INIT), and newproxy-environment.test.tsfor gVisorNO_PROXYbehaviorsmoke-gvisor*workflows with gh-aw v0.82.13 (latest pre-release) + ranpostprocess-smoke-workflows.ts; lock files were already current. The gVisor smoke workflows buildawffrom branch source, so CI exercises this fix.