Skip to content

Squid proxy rejects IPv6 localhost connections from chroot (transaction-end-before-headers) #1543

@lpcox

Description

@lpcox

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
fi

When 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

  1. Claude Code inside chroot resolves api.anthropic.com — gets both A and AAAA records
  2. Node.js happy eyeballs algorithm tries IPv6 first (RFC 8305)
  3. IPv6 connection to port 443 is NOT caught by iptables DNAT (IPv4-only rules)
  4. If HTTPS_PROXY is somehow not effective (see below), the connection goes directly
  5. Connection arrives at Squid on ::1:3128 (loopback) as raw TCP — no HTTP CONNECT headers
  6. Squid rejects: transaction-end-before-headers
  7. 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 || true

This 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: claude workflow 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions