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..fc432532e 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -1194,6 +1194,59 @@ 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'); + }); + + 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', () => { 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..2b9236d4f 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; } @@ -447,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(',');