From 2fc1a709255052e64a09da81f5709724771800e2 Mon Sep 17 00:00:00 2001 From: "Jiaxiao (mossaka) Zhou" Date: Wed, 25 Feb 2026 18:00:15 +0000 Subject: [PATCH 1/2] fix: always set NO_PROXY to bypass Squid for localhost When HTTP_PROXY is set but NO_PROXY is not, HTTP clients (Go net/http, Python requests, curl, etc.) voluntarily route localhost-bound requests through Squid, which rejects them with 403 because localhost is not in the domain allowlist. This broke test frameworks that start local servers (go/echo, python/uvicorn, deno/fresh). The fix unconditionally sets NO_PROXY with localhost, 127.0.0.1, ::1, 0.0.0.0, plus the Squid and agent container IPs. The enableHostAccess and enableApiProxy branches now append to this baseline instead of conditionally creating it. Also adds an iptables RETURN rule for 0.0.0.0 as defense-in-depth. Co-Authored-By: Claude Opus 4.6 --- containers/agent/setup-iptables.sh | 3 ++- src/docker-manager.test.ts | 31 ++++++++++++++++++++++++++++++ src/docker-manager.ts | 18 ++++++++++------- 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/containers/agent/setup-iptables.sh b/containers/agent/setup-iptables.sh index 0f7e05b14..15f406ab7 100644 --- a/containers/agent/setup-iptables.sh +++ b/containers/agent/setup-iptables.sh @@ -56,10 +56,11 @@ if [ "$IP6TABLES_AVAILABLE" = true ]; then ip6tables -t nat -F OUTPUT 2>/dev/null || true fi -# Allow localhost traffic (for stdio MCP servers) +# Allow localhost traffic (for stdio MCP servers and test frameworks) echo "[iptables] Allow localhost traffic..." iptables -t nat -A OUTPUT -o lo -j RETURN iptables -t nat -A OUTPUT -d 127.0.0.0/8 -j RETURN +iptables -t nat -A OUTPUT -d 0.0.0.0 -j RETURN if [ "$IP6TABLES_AVAILABLE" = true ]; then ip6tables -t nat -A OUTPUT -o lo -j RETURN ip6tables -t nat -A OUTPUT -d ::1/128 -j RETURN diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index d9ce8fe0e..87b6b2daf 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -1194,6 +1194,37 @@ describe('docker-manager', () => { }); }); + describe('NO_PROXY baseline', () => { + it('should always set NO_PROXY with localhost entries', () => { + // Default config without enableHostAccess or enableApiProxy + const result = generateDockerCompose(mockConfig, mockNetworkConfig); + const agent = result.services.agent; + const env = agent.environment as Record; + expect(env.NO_PROXY).toContain('localhost'); + expect(env.NO_PROXY).toContain('127.0.0.1'); + expect(env.NO_PROXY).toContain('::1'); + expect(env.NO_PROXY).toContain('0.0.0.0'); + expect(env.no_proxy).toBe(env.NO_PROXY); + }); + + it('should include agent IP in NO_PROXY', () => { + const result = generateDockerCompose(mockConfig, mockNetworkConfig); + const agent = result.services.agent; + const env = agent.environment as Record; + expect(env.NO_PROXY).toContain('172.30.0.20'); + }); + + it('should append host.docker.internal to NO_PROXY when host access enabled', () => { + const configWithHost = { ...mockConfig, enableHostAccess: true }; + const result = generateDockerCompose(configWithHost, mockNetworkConfig); + const agent = result.services.agent; + const env = agent.environment as Record; + // Should have both baseline AND host access entries + expect(env.NO_PROXY).toContain('localhost'); + expect(env.NO_PROXY).toContain('host.docker.internal'); + }); + }); + describe('allowHostPorts option', () => { it('should set AWF_ALLOW_HOST_PORTS when allowHostPorts is specified', () => { const config = { ...mockConfig, enableHostAccess: true, allowHostPorts: '8080,3000' }; diff --git a/src/docker-manager.ts b/src/docker-manager.ts index 6f6e926c5..d7262534a 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -363,7 +363,15 @@ export function generateDockerCompose( logger.debug('COPILOT_GITHUB_TOKEN set to placeholder value (early) to prevent --env-all override'); } - // When host access is enabled, bypass the proxy for the host gateway IPs. + // Always set NO_PROXY to prevent HTTP clients from proxying localhost traffic through Squid. + // Without this, test frameworks that start local servers (e.g., go/echo, python/uvicorn, + // deno/fresh) get 403 errors because Squid rejects requests to localhost (not in allowed domains). + // Include the agent's own container IP because test frameworks often bind to 0.0.0.0 and + // test clients may connect via the container's non-loopback IP (e.g., 172.30.0.20). + environment.NO_PROXY = `localhost,127.0.0.1,::1,0.0.0.0,${networkConfig.squidIp},${networkConfig.agentIp}`; + environment.no_proxy = environment.NO_PROXY; + + // When host access is enabled, also bypass the proxy for the host gateway IPs. // MCP Streamable HTTP (SSE) traffic through Squid crashes it (comm.cc:1583), // so MCP gateway traffic must go directly to the host, not through Squid. if (config.enableHostAccess) { @@ -371,18 +379,14 @@ export function generateDockerCompose( const subnetBase = networkConfig.subnet.split('/')[0]; // e.g. "172.30.0.0" const parts = subnetBase.split('.'); const networkGatewayIp = `${parts[0]}.${parts[1]}.${parts[2]}.1`; - environment.NO_PROXY = `localhost,127.0.0.1,${networkConfig.squidIp},host.docker.internal,${networkGatewayIp}`; + environment.NO_PROXY += `,host.docker.internal,${networkGatewayIp}`; environment.no_proxy = environment.NO_PROXY; } // When API proxy is enabled, bypass HTTP_PROXY for the api-proxy IP // so the agent can reach the sidecar directly without going through Squid if (config.enableApiProxy && networkConfig.proxyIp) { - if (environment.NO_PROXY) { - environment.NO_PROXY += `,${networkConfig.proxyIp}`; - } else { - environment.NO_PROXY = `localhost,127.0.0.1,${networkConfig.proxyIp}`; - } + environment.NO_PROXY += `,${networkConfig.proxyIp}`; environment.no_proxy = environment.NO_PROXY; } From d38f98fb256f65327a6039bb45e0977bf444b5b0 Mon Sep 17 00:00:00 2001 From: "Jiaxiao (mossaka) Zhou" Date: Wed, 25 Feb 2026 18:11:21 +0000 Subject: [PATCH 2/2] fix: sync NO_PROXY/no_proxy after additionalEnv override Address review feedback: when --env overrides NO_PROXY (or no_proxy) without setting the other casing, HTTP clients that prefer the other casing would still route through Squid. After additionalEnv is applied, detect the inconsistency and sync both variables with clear precedence. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/docker-manager.test.ts | 22 ++++++++++++++++++++++ src/docker-manager.ts | 12 ++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index 87b6b2daf..fc432532e 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -1223,6 +1223,28 @@ describe('docker-manager', () => { expect(env.NO_PROXY).toContain('localhost'); expect(env.NO_PROXY).toContain('host.docker.internal'); }); + + it('should sync no_proxy when --env overrides NO_PROXY', () => { + const configWithEnv = { + ...mockConfig, + additionalEnv: { NO_PROXY: 'custom.local,127.0.0.1' }, + }; + const result = generateDockerCompose(configWithEnv, mockNetworkConfig); + const env = result.services.agent.environment as Record; + expect(env.NO_PROXY).toBe('custom.local,127.0.0.1'); + expect(env.no_proxy).toBe(env.NO_PROXY); + }); + + it('should sync NO_PROXY when --env overrides no_proxy', () => { + const configWithEnv = { + ...mockConfig, + additionalEnv: { no_proxy: 'custom.local,127.0.0.1' }, + }; + const result = generateDockerCompose(configWithEnv, mockNetworkConfig); + const env = result.services.agent.environment as Record; + expect(env.no_proxy).toBe('custom.local,127.0.0.1'); + expect(env.NO_PROXY).toBe(env.no_proxy); + }); }); describe('allowHostPorts option', () => { diff --git a/src/docker-manager.ts b/src/docker-manager.ts index d7262534a..2b9236d4f 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -451,6 +451,18 @@ export function generateDockerCompose( Object.assign(environment, config.additionalEnv); } + // Normalize NO_PROXY / no_proxy after additionalEnv is applied. + // If --env overrides one casing but not the other, HTTP clients that prefer the + // other casing (e.g., Go uses NO_PROXY, Python requests uses no_proxy) would + // still route through Squid. Sync them with NO_PROXY taking precedence. + if (environment.NO_PROXY !== environment.no_proxy) { + if (config.additionalEnv?.NO_PROXY) { + environment.no_proxy = environment.NO_PROXY; + } else if (config.additionalEnv?.no_proxy) { + environment.NO_PROXY = environment.no_proxy; + } + } + // Pass DNS servers to container for setup-iptables.sh and entrypoint.sh const dnsServers = config.dnsServers || ['8.8.8.8', '8.8.4.4']; environment.AWF_DNS_SERVERS = dnsServers.join(',');