🏗️ Problem & Architecture
CCR containers need outbound HTTP traffic to carry injected credentials and be auditable. Solution: HTTPS CONNECT proxy on 127.0.0.1, tunnelled via WebSocket to Anthropic's gateway. Gateway MITMs TLS, injects org-configured headers, forwards to upstream.
💡 Fails Open by Design
Every step in initUpstreamProxy is wrapped so that any error logs a warning and returns { enabled: false }. A broken proxy must never break a working session.
🔧 Initialization Sequence
Step 1: Guard env vars
Returns early if CLAUDE_CODE_REMOTE or CCR_UPSTREAM_PROXY_ENABLED is not truthy.
Step 2: Read session token
Reads from /run/ccr/session_token, written by container orchestrator.
Step 3: Set non-dumpable
prctl(PR_SET_DUMPABLE, 0) via Bun FFI blocks same-UID ptrace/gdb.
Step 4: Download CA bundle
Fetches MITM proxy CA cert, concatenates with system CA bundle.
Step 5: Start relay
TCP server on ephemeral port. Returns port number.
Step 6: Unlink token file
Deleted AFTER relay confirms listening. Token lives in heap only.
🌐 CONNECT-over-WebSocket Relay
Clients (curl, gh, npm) connect and send HTTP CONNECT requests. Relay upgrades WebSocket to gateway and tunnels bytes bidirectionally. Two-phase connection: accumulate until CRLF CRLF terminates CONNECT header, then pump bytes.
💡 Race Condition Solved
TCP can coalesce CONNECT header and TLS ClientHello into one packet. Bytes after CRLF CRLF go into st.pending[] and flush in ws.onopen. Without this, ClientHello would be silently dropped.
📦 Protobuf Encoding
Bytes wrapped in UpstreamProxyChunk protobuf message. Hand-rolled encoding: tag byte 0x0a (field 1, wire type 2), varint length, payload. Content-Type: application/proto tells server to use binary proto deserialization.
💡 Why Not Plain Bytes
CCR gateway uses NewWebSocketStreamAdapter which expects this framing. Without Content-Type: application/proto, server silently fails with EOF.
🔄 Bun vs Node Runtime
Runtime dispatch: Bun (developer) vs Node (production CCR). Key difference: Node buffers writes unconditionally; Bun's sock.write() does partial kernel write and silently drops remainder. Bun relay tracks unwritten bytes in per-socket writeBuf[].
💡 WebSocket Proxy Agent
CCR containers behind egress gateway. WS upgrade itself must go through HTTP CONNECT proxy. Node uses ws package with explicit agent; Bun's native WebSocket accepts proxy URL directly.
🛡️ Security Model
Prompt Injection Defense
prctl(PR_SET_DUMPABLE, 0) blocks same-UID ptrace, defeating gdb -p $PPID attacks.
Dual Authentication
WS upgrade: Authorization Bearer token. CONNECT tunnel: Proxy-Authorization Basic (inside first protobuf chunk).
TLS Corruption Guard
After 200 Connection Established, plaintext 502 errors are never written - would corrupt active TLS cipher state.
⚡ Env Var Propagation
| Variable | Purpose |
|---|---|
| HTTPS_PROXY / https_proxy | Route HTTPS through relay |
| NO_PROXY / no_proxy | Bypass for loopback, RFC1918, Anthropic API |
| SSL_CERT_FILE | OpenSSL / curl CA trust |
| NODE_EXTRA_CA_CERTS | Node.js TLS trust |
| REQUESTS_CA_BUNDLE | Python requests/httpx trust |
| CURL_CA_BUNDLE | curl CA trust |
💡 HTTPS Only
Only HTTPS_PROXY is set, never HTTP_PROXY. Relay only handles CONNECT - plain HTTP returns 405.