-
Notifications
You must be signed in to change notification settings - Fork 18
Squid proxy rejects IPv6 localhost connections from chroot (transaction-end-before-headers) #1543
Description
Problem
Reported in github/gh-aw#23765. When Claude Code runs inside the AWF chroot, connections to the Squid proxy arrive via IPv6 (::1) but Squid only listens on IPv4. Squid receives raw TCP connections without HTTP headers and logs transaction-end-before-headers, causing all API calls from Claude Code to fail.
Squid audit log evidence
Working — API proxy container connects via IPv4:
{"client":"172.30.0.20","host":"api.anthropic.com:443","method":"CONNECT","status":200,"decision":"TCP_TUNNEL"}Failing — Claude Code connects via IPv6 loopback:
{"client":"::1","host":"-","dest":"-:-","method":"-","status":0,"decision":"NONE_NONE","url":"error:transaction-end-before-headers"}49 failed connections over ~4 minutes (matching Claude Code retry behavior with exponential backoff).
Root Cause Analysis
Three issues combine to create this failure:
1. Squid only listens on IPv4
In src/squid-config.ts, the port directive is:
http_port 3128
This binds to 0.0.0.0:3128 (IPv4 only). There is no IPv6 listener ([::]:3128). Any IPv6 connection to port 3128 is rejected at the TCP level.
2. No IPv6 DNAT rules for Squid redirection
In containers/agent/setup-iptables.sh, the DNAT rules that redirect port 80/443 → Squid only use IPv4 iptables:
iptables -t nat -A OUTPUT -p tcp --dport 443 -j DNAT --to-destination ${SQUID_IP}:${SQUID_PORT}There are NO equivalent ip6tables DNAT rules for Squid redirection. IPv6 traffic to port 443 is not intercepted.
3. IPv6 only disabled when ip6tables is unavailable
setup-iptables.sh disables IPv6 via sysctl only as a fallback when ip6tables is not available:
if has_ip6tables; then
IP6TABLES_AVAILABLE=true
else
sysctl -w net.ipv6.conf.all.disable_ipv6=1
fiWhen ip6tables IS available (the common case on ubuntu-latest runners), IPv6 remains enabled but has no DNAT rules to route traffic to Squid. This creates a gap where IPv6 connections bypass the proxy entirely.
What happens
- Claude Code inside chroot resolves
api.anthropic.com— gets both A and AAAA records - Node.js
happy eyeballsalgorithm tries IPv6 first (RFC 8305) - IPv6 connection to port 443 is NOT caught by iptables DNAT (IPv4-only rules)
- If
HTTPS_PROXYis somehow not effective (see below), the connection goes directly - Connection arrives at Squid on
::1:3128(loopback) as raw TCP — no HTTP CONNECT headers - Squid rejects:
transaction-end-before-headers - Claude Code retries 10 times → all fail →
EHOSTUNREACH→ exit 1
An additional factor may be that HTTP_PROXY/HTTPS_PROXY env vars are not properly propagated through the chroot boundary into Claude Code's Node.js process, causing it to attempt direct connections instead of using the proxy.
Proposed Fix
A defense-in-depth approach addressing all three gaps:
Fix 1: Always disable IPv6 in agent container (recommended, security + correctness)
In containers/agent/setup-iptables.sh, unconditionally disable IPv6 in the agent network namespace regardless of ip6tables availability:
# Disable IPv6 to prevent unfiltered bypass — Squid only listens on IPv4
echo "[iptables] Disabling IPv6 to prevent proxy bypass..."
sysctl -w net.ipv6.conf.all.disable_ipv6=1 2>/dev/null || true
sysctl -w net.ipv6.conf.default.disable_ipv6=1 2>/dev/null || trueThis is the most robust fix because:
- Forces all DNS resolution to return IPv4-only (A records)
- Eliminates the IPv6 happy-eyeballs race condition
- Matches the Docker network config (awf-net is IPv4-only, no
enable_ipv6) - Prevents any IPv6-based proxy bypass, even for tools that ignore proxy env vars
Fix 2: Add Squid IPv6 listener (belt-and-suspenders)
In src/squid-config.ts, add a dual-stack listener:
http_port 3128
http_port [::]:3128
This way even if IPv6 traffic somehow reaches Squid, it can handle it. However, Fix 1 alone should be sufficient since there's no reason for IPv6 in the isolated container network.
Fix 3: Verify proxy env var propagation through chroot
Audit containers/agent/entrypoint.sh to ensure HTTP_PROXY, HTTPS_PROXY, and NO_PROXY are exported and visible to the chrooted process. The env vars are currently set to the container IPv4 address (http://172.30.x.10:3128), which is correct, but they must survive the exec chroot /host ... boundary.
Impact
- Affected: Any
engine: claudeworkflow using the AWF chroot (the default execution mode) - Not affected: API proxy sidecar (uses container IPv4 directly), workflows using other engines
- Severity: High — Claude Code workflows are completely non-functional when this occurs
Environment
- gh-aw CLI: v0.65.1
- AWF container: 0.25.5
- Claude Code CLI: 2.1.87
- Runner: ubuntu-latest
References
- Upstream issue: github/gh-aw#23765
- Related fix: github/gh-aw#23614 (chroot key helper — worked, but proxy issue remained)