Upstream 代理:容器里的 TLS 隐身术
先搞清楚这是什么
Upstream 代理(upstream-proxy)是 Claude Code 在远程开发环境(CCR, Claude Code Remote)中实现网络管控的核心组件。它本质上是一个带中间人攻击(MITM)能力的 TLS 代理。
当你在 CCR 容器里执行 curl 或 git push 时,请求并不会直接飞往互联网。相反,它会被路由到一个本地监听的代理服务器,通过 WebSocket 隧道加密传回 Anthropic 的服务器,并在那里被注入必要的企业凭证(如 Datadog API Key)后,再转发给真正的上游目标。
实现机制
双引擎中继(Dual-runtime Relay): 为了适配不同的运行环境,代理中继层实现了两套逻辑。在 Bun 运行时下使用
Bun.listen()并配合手动实现的写入缓冲(防止内核缓冲区满导致丢包);在 Node.js 环境下则回退到经典的net.createServer()。这种设计确保了代理在各种容器镜像中都能稳定运行。硬核 Protobuf 编码: 为了极致的性能和减少依赖,
relay.ts并没有使用庞大的 protobuf 库,而是手动实现了UpstreamProxyChunk的二进制编码。它直接操作Uint8Array,按照[0x0a (tag)] + [varint (length)] + [data]的格式拼接字节流。内存级别的安全防护: 为了防止 Prompt 注入攻击通过
gdb -p等手段在内存堆栈中窃取会话令牌(Session Token),代码通过 Bun FFI 调用了 Linux 原生的prctl(PR_SET_DUMPABLE, 0)。这会禁止其他进程对该进程进行 ptrace 调试。证书注入与环境变量: 启动时,它会从服务端下载专用的 CA 证书,将其与系统原有的 CA Bundle 合并,并强制注入到子进程的
SSL_CERT_FILE、HTTPS_PROXY等环境变量中。这样,即使是 Python 的requests或 Go 的http.Client也能无缝信任这个 MITM 代理。令牌自毁: 一旦代理启动成功并确认可以建立连接,它会立即执行
unlink删除磁盘上的 Token 文件。Token 从此只存在于堆内存中,最大限度缩短了敏感信息的暴露时长。
别踩这些坑
- Fail-open 设计:代理遵循“失败即放行”原则。如果证书下载失败或中继启动报错,它会记录警告并禁用代理,而不是让整个会话崩溃。
- 显式绕过清单:
NO_PROXY列表非常关键,它显式排除了anthropic.com(防止套娃)、github.com以及各大包管理器镜像(npm, PyPI),确保核心通信不受拦截干扰。 - 仅限 HTTPS:该代理主要处理
CONNECT请求,明文 HTTP 默认不参与凭证注入。
推荐阅读路径
如果你对 Claude 如何在受限网络环境下与外界通信感兴趣,建议阅读 src/utils/proxy.ts 了解基础代理配置,或者查看 src/utils/mtls.ts 了解背后的双向 TLS 认证细节。
源码锚点
src/upstreamproxy/upstreamproxy.ts: 容器侧的编排逻辑、prctl调用、CA 合并。
📄 src/upstreamproxy/upstreamproxy.ts — 容器侧的编排逻辑、`prctl` 调用、CA 合并。
prctl: {
args: ['int', 'u64', 'u64', 'u64', 'u64'],
returns: 'int',
},src/upstreamproxy/relay.ts: CONNECT-over-WebSocket 的具体实现,包括手写 Protobuf 编码。
📄 src/upstreamproxy/relay.ts — CONNECT-over-WebSocket 的具体实现,包括手写 Protobuf 编码。
* CONNECT-over-WebSocket relay for CCR upstreamproxy.
*
* Listens on localhost TCP, accepts HTTP CONNECT from curl/gh/kubectl/etc,
* and tunnels bytes over WebSocket to the CCR upstreamproxy endpoint.
* The CCR server-side terminates the tunnel, MITMs TLS, injects org-configured
* credentials (e.g. DD-API-KEY), and forwards to the real upstream.
*
* WHY WebSocket and not raw CONNECT: CCR ingress is GKE L7 with path-prefix
* routing; there's no connect_matcher in cdk-constructs. The session-ingress
* tunnel (sessions/tunnel/v1alpha/tunnel.proto) already uses this pattern.
*
* Protocol: bytes are wrapped in UpstreamProxyChunk protobuf messages
* (`message UpstreamProxyChunk { bytes data = 1; }`) for compatibility with