-
Notifications
You must be signed in to change notification settings - Fork 15
Description
Problem
Tests that start a local HTTP server and connect to it via the container's own non-loopback IP fail with HTTP 403 Access Denied from Squid.
Affected test suites (from the v4 build-test experiment):
- go/echo — 4 test failures
- cpp/cpr — 1 test failure
- bun/hono — 4 test failures
- deno/hono — 4 test failures
Typical test pattern:
// Test server binds to all interfaces
e := echo.New()
e.GET("/hello", func(c echo.Context) error {
return c.String(200, "Hello")
})
go e.Start(":8080")
// Test client connects using the container's own IP
resp, err := http.Get("http://172.30.0.20:8080/hello")
// → HTTP 403 Access Denied (from Squid!)The same happens when port 80/443 is used and the server listens on 0.0.0.0.
Root Cause Analysis
The iptables rules in containers/agent/setup-iptables.sh correctly bypass the proxy for loopback traffic:
# Allow localhost traffic (for stdio MCP servers)
iptables -t nat -A OUTPUT -o lo -j RETURN
iptables -t nat -A OUTPUT -d 127.0.0.0/8 -j RETURNHowever, there is no bypass for the container's own non-loopback IP (172.30.0.20).
When a test server binds to 0.0.0.0 and the test client connects using the container's own IP (172.30.0.20), the traffic flows like this:
test client → 172.30.0.20:PORT
↓ iptables nat OUTPUT chain
No match: -d 127.0.0.0/8 (not loopback)
No match: -d 172.30.0.10 (not Squid IP)
MATCH: -p tcp --dport PORT → DNAT to 172.30.0.10:3128 ← BUG
↓
Squid receives CONNECT/GET to 172.30.0.20:PORT
→ Not an allowed domain → 403 Access Denied
The same rules that redirect user-specified ports (AWF_ALLOW_HOST_PORTS) also catch self-directed traffic on those ports. Even if no extra ports are configured, ports 80 and 443 are always redirected.
The existing exemptions in place are:
| Destination | Rule | Bypass reason |
|---|---|---|
127.0.0.0/8 |
-d 127.0.0.0/8 -j RETURN |
Loopback / stdio MCP servers |
172.30.0.10 (Squid) |
-d $SQUID_IP -j RETURN |
Prevent redirect loop |
$AWF_API_PROXY_IP |
-d $AWF_API_PROXY_IP -j RETURN |
Direct sidecar access |
host.docker.internal |
-d $HOST_GATEWAY_IP -j RETURN |
MCP gateway traffic |
172.30.0.20 (self) |
MISSING | ← This is the gap |
Proposed Fix
Add a bypass rule for the container's own IP in the NAT OUTPUT chain, placed alongside the existing loopback bypass. The IP should be detected at runtime (not hardcoded) to be robust across configurations:
# Bypass Squid for traffic to the container's own IP
# Test frameworks often bind servers to 0.0.0.0 and connect via the non-loopback IP.
# Without this rule, the DNAT redirect rules catch self-directed traffic and
# route it through Squid, which denies it with 403 (not an allowed domain).
AGENT_IP=$(ip -4 addr show eth0 2>/dev/null | awk '/inet / { split($2,a,"/"); print a[1]; exit }')
if [ -n "$AGENT_IP" ] && is_valid_ipv4 "$AGENT_IP"; then
echo "[iptables] Bypass Squid for self-directed traffic (agent IP: ${AGENT_IP})..."
iptables -t nat -A OUTPUT -d "$AGENT_IP" -j RETURN
# Also add a filter ACCEPT rule so the traffic isn't dropped by the default-deny chain
iptables -A OUTPUT -p tcp -d "$AGENT_IP" -j ACCEPT
fiThis should be inserted immediately after the existing loopback bypass block (around line 66), before the DNS and Squid bypass rules, and before the port DNAT rules (lines 224–225).
Why runtime detection vs. hardcoding 172.30.0.20?
docker-manager.ts today hardcodes agentIp: '172.30.0.20', so 172.30.0.20 would work. However, runtime detection via ip addr is more defensive — it remains correct if the network config ever changes or if --build-local is used with a different subnet.
Security Considerations
This change is safe because:
-
Self-traffic only — The bypass applies only to traffic whose destination is the container's own IP (
172.30.0.20). External IPs are unaffected. -
No new exfiltration path — Any data sent to
172.30.0.20:PORTstays within the container's loopback equivalent; it cannot reach the internet. -
Analogy to loopback — This is functionally equivalent to the existing
127.0.0.0/8bypass. Both cases represent in-container communication that should never be proxied. -
Filter chain still enforces DROP — Adding a NAT RETURN alone would have left the traffic subject to the
iptables -A OUTPUT -p tcp -j DROPrule. The accompanying filter ACCEPT rule is required, scoped to just the agent's own IP. -
No impact on Squid domain enforcement — External HTTP/HTTPS traffic continues to flow through Squid and is subject to the domain allowlist as before.
-
Comparison to existing exemptions — The Squid, API-proxy, and host-gateway bypasses each allow traffic to a different container/host. The self-IP bypass is narrower: it only covers one IP (the agent itself).
Workaround (Until Fixed)
Test code can avoid this by connecting via 127.0.0.1 / localhost instead of the machine's IP or hostname. However, many test frameworks resolve hostnames to non-loopback IPs automatically, making this fragile.
Discovered during the v4 build-test experiment (Feb 2026), running 98 repos through gh-aw agentic workflows.