坑边闲话:Claude Code 和 Codex 本质上是包了一层 HTTP 客户端的 LLM CLI,但很多工程师在内网机器上跑它们时,代理就是配不起来。然而,问题通常不在工具本身,而在对网络层的理解不够清晰。

1. 为什么需要代理·

1.1 LLM CLI 的本质是 HTTP API client·

Claude Code 和 Codex CLI 对外的所有交互都是标准 HTTPS 请求:

  • Claude Code 调用 api.anthropic.com;
  • Codex 调用 api.openai.com.

它们没有实现任何自定义协议,也没有内置的 NAT 穿透能力。这意味着:只要 HTTP 层的出口受限,工具就直接不可用

这和浏览器访问被墙的网站是同一个问题,只是没有 GUI 来提示你。

1.2 典型受限场景·

  • 内网环境:实验室或企业网络通常通过防火墙和统一出口访问外网,直连 api.anthropic.com 往往被拦截,或者根本没有路由。这种情况下即使机器能上内网,LLM 工具也是废的。
  • 地理限制:Anthropic 和 OpenAI 都对部分国家和地区限制服务访问。如果 IP 段在封锁列表内,API 请求会直接返回 403 或连接超时,和网络本身的连通性无关。
  • 流量审计与调试:做安全研究、逆向分析或协议调试时,往往需要抓取 LLM 工具发出的具体 HTTP 请求:请求头、body、响应内容。把流量导入一个可控代理(比如 mitmproxy)是最直接的方式。
  • 链路稳定性:直连远端 API 会受 BGP 路由抖动、运营商 QoS 策略等因素影响,延迟方差大。自建隧道可以固定链路,把不稳定性控制在可预期的范围内。

1.3 代理的工程含义·

从系统设计角度看,代理是一个可控的网络出口层(egress control layer). 通过它可以做到:

  • 流量可观测(日志、审计)
  • 出口 IP 可控(规避封锁)
  • 链路可替换(不改客户端配置切换后端)
  • 访问策略可执行(ACL、速率限制)

这和反向代理在入口方向提供的能力是对称的。

2. 服务端代理体系搭建·

2.1 整体架构·

服务端需要解决两个不同层次的问题:突破网络限制(隧道层)和对外提供标准代理接口(代理服务层)。两层职责分离,互不耦合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
+-----------------------------+
| Remote Server |
| (VPS with non-China IP) |
| |
| +----------------------+ |
| | Outbound Access | |
| | (Direct Internet) | |
| +----------+-----------+ |
| | |
| +----------+-----------+ |
| | Proxy Service | |
| | sing-box / tinyproxy| |
| | SOCKS5: 1080 | |
| | HTTP: 1081 | |
| +----------+-----------+ |
| | |
| +----------+-----------+ |
| | Tunnel Endpoint | |
| | WireGuard / VLESS | |
| +----------+-----------+ |
+-----------------------------+
|
(encrypted tunnel)
|
+-----------------------------+
| Local Machine |
| (inner network) |
+-----------------------------+

2.2 隧道层·

隧道层的作用是在受限网络和远程服务器之间建立一条加密的点对点链路。常用方案:

  • WireGuard:基于 UDP 的现代 VPN 协议,内核原生支持(Linux 5.6+),配置简单,性能好。适合网络管理员可以放行 UDP 端口的场景。
  • sing-box VLESS/VLESS-Reality:基于 TLS 的代理协议,流量特征接近普通 HTTPS,抗干扰能力更强。适合 UDP 被限速或 DPI 检测严格的环境。

两者并不互斥。以下是两种常见部署:

  • 路由没有被阻断,可使用 WireGuard 建立基础 VPN 隧道
  • 路由被 GFW 或其他类似的防火墙阻断,可使用 sing-box 创建混淆隧道,然而 sing-box (vless/vless-reality) 一般是单向隧道,在隧道内部套 WireGuard 可以解决双向连接问题,不过这属于小众需求,并不在本文讨论范畴之内。

隧道建立后,本地机器会获得一个 VPN IP(比如 192.168.88.x 这样的 WireGuard 地址段),后续所有代理流量都通过这个 IP 访问。

2.3 代理服务层·

隧道解决的是"能不能连过去"的问题,代理服务层解决的是连过去之后用什么协议暴露服务的问题。

LLM CLI 工具识别的是标准代理环境变量(HTTP_PROXYHTTPS_PROXYALL_PROXY),对应 HTTP proxy 和 SOCKS5 两种协议。因此远程服务器需要运行一个支持这两种协议的代理服务。

  • sing-box inbound:sing-box 同时支持 SOCKS 和 HTTP inbound,配置统一,是全栈方案的首选。
  • tinyproxy:轻量级 HTTP proxy,配置文件极简,适合只需要 HTTP 代理的场景。缺点是不支持 SOCKS。

代理服务绑定到 VPN 接口的 IP 上(比如 192.168.88.103),只对 VPN 内的客户端开放,不暴露到公网。

1
2
3
4
5
6
7
8
9
10
11
+------------------------------------------+
| sing-box on Remote Server |
| |
| Inbound: |
| SOCKS5 0.0.0.0:1080 |
| HTTP 0.0.0.0:1081 |
| |
| Outbound: |
| direct -> api.anthropic.com |
| direct -> api.openai.com |
+------------------------------------------+

3. 客户端配置·

3.1 环境变量:最通用的方式·

几乎所有 HTTP 客户端库(Python requests、Node.js undici、Go net/http)都会读取以下环境变量:

1
2
3
export HTTP_PROXY=http://192.168.88.103:1081
export HTTPS_PROXY=http://192.168.88.103:1081
export ALL_PROXY=socks5://192.168.88.103:1080
  • HTTP_PROXY / HTTPS_PROXY 指向 HTTP 代理,本身用明文 HTTP CONNECT 协议和代理握手,但代理和目标服务器之间的 TLS 是完整的。不要因为协议名叫 HTTP 就认为流量不加密。
  • ALL_PROXY 是通配回退,指向 SOCKS5 代理。SOCKS5 工作在更低层,直接代理 TCP 连接,不解析 HTTP 语义,通用性更强。当 HTTP_PROXYALL_PROXY 同时设置时,标准行为是 HTTPS 请求优先用 HTTPS_PROXY,其余回退到 ALL_PROXY.

写入 ~/.bashrc~/.zshrc 让变量持久化;或者写成一个 shell 函数按需激活,避免全局代理影响其他工具

3.2 Claude Code 配置·

Claude Code 支持在 ~/.claude/settings.json 中直接注入环境变量,这样不需要每次手动 export,也不污染全局 shell 环境:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"env": {
"HTTP_PROXY": "http://192.168.88.103:1081",
"HTTPS_PROXY": "http://192.168.88.103:1081",
"ALL_PROXY": "socks5://192.168.88.103:1080"
},
"model": "opus",
"enabledPlugins": {
"codex@openai-codex": true
},
"extraKnownMarketplaces": {
"openai-codex": {
"source": {
"source": "github",
"repo": "openai/codex-plugin-cc"
}
}
}
}

env 字段中的变量会在 Claude Code 启动时注入到它的进程环境中,对它发出的所有 HTTP 请求生效。这比系统级代理更干净——代理范围精确限定在 Claude Code 进程内。

如果 settings.json 已经存在且有其他配置,直接覆盖文件会丢失现有内容。用 jq 做原地合并是更安全的方式:

1
2
3
4
5
6
jq '.env |= (. // {}) * {
"HTTP_PROXY": "http://192.168.88.103:1081",
"HTTPS_PROXY": "http://192.168.88.103:1081",
"ALL_PROXY": "socks5://192.168.88.103:1080"
}' ~/.claude/settings.json > /tmp/claude_settings.json \
&& mv /tmp/claude_settings.json ~/.claude/settings.json

(. // {}) 处理 env 字段不存在的情况,* 是 jq 的对象合并操作符,只更新三个代理字段,其余内容保持不变。

注意 enabledPluginsextraKnownMarketplaces 是为了启用 Codex CLI 作为 Claude Code 插件的配置,和代理本身无关,但放在一起是完整的生产配置。

3.3 Codex CLI 配置·

Codex 的配置文件 ~/.codex/config.toml 没有显式的 proxy 字段:

1
2
3
4
5
6
7
8
9
10
11
12
model = "gpt-5.4"
model_reasoning_effort = "high"
personality = "pragmatic"

[projects."/mnt/DapuStor_R5100_RAID-Z1/Develop/large_scale_fw_analysis"]
trust_level = "trusted"

[projects."/mnt/Intel_750_RAID-Z1/openwrt-compile/openwrt"]
trust_level = "trusted"

[plugins."github@openai-curated"]
enabled = true

但 Codex 支持从 ~/.codex/.env 读取环境变量,这是注入代理配置的正确方式:

1
2
3
4
5
tee ~/.codex/.env <<'EOF'
https_proxy="http://192.168.88.103:1081"
http_proxy="http://192.168.88.103:1081"
all_proxy="socks5://192.168.88.103:1080"
EOF

这个 .env 文件会在 Codex 启动时自动加载,不依赖调用方 shell 的环境变量状态。相比在 .bashrc 里全局 export,这种方式更干净——代理范围精确限定在 Codex 进程内,不影响同一 shell 里的其他工具。

如果发现代理没有生效,先检查 ~/.codex/.env 是否存在且格式正确,再确认文件权限(chmod 600 是个好习惯)。

4. 整体链路梳理·

把三层结构串起来看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
+----------------------------+
| Claude Code / Codex CLI |
| (read HTT / ALL_PROXY) |
+-------------+--------------+
|
| HTTP CONNECT / SOCKS5
|
+-------------+--------------+
| Local Proxy listen |
| 192.168.88.103:1080/1081 |
| (WireGuard VPN IP) |
+-------------+--------------+
|
| WireGuard UDP tunnel
| (加密,抗 DPI)
|
+-------------+--------------+
| Remote Server |
| sing-box / tinyproxy |
+-------------+--------------+
|
| direct
|
+-------------+--------------+
| api.anthropic.com |
| api.openai.com |
+----------------------------+

几个实际部署中容易踩的坑:

  • 代理地址必须从客户端可达:如果代理监听在 WireGuard 接口的 IP 上,客户端必须已经建立 WireGuard 隧道才能访问。隧道断了,代理就不可达,LLM 工具会超时而不是给出有意义的错误。
  • sing-box bind 地址:默认配置下 sing-box inbound 可能只绑定 127.0.0.1,需要显式改为 0.0.0.0 或 WireGuard 接口 IP,否则远程客户端连不上。
  • NO_PROXY 的必要性:如果代理是全局的,本地服务(比如 localhost、内网 API)的请求也会被路由到代理,导致内网请求失败。设置 NO_PROXY=localhost,127.0.0.1,10.0.0.0/8 来绕过。

5. 工程意义与延伸思考·

5.1 代理是基础设施·

在内网安全管控严格的环境里,把 LLM 工具的出口流量集中到一个代理节点,本质上是在做 egress control。这套基础设施一旦建好,可以复用到所有需要访问外网 API 的工具——不止是 LLM CLI,curlpip installnpm publish 都走同一条链路,审计和管控都在一个地方。

5.2 可观测性·

代理层天然是流量的汇聚点。在代理上开启访问日志,可以看到哪个工具在什么时间访问了哪个 API endpoint,发出了多少请求,响应时间分布如何。对于排查 LLM 工具的异常行为(重试风暴、意外的大量 API 调用)非常有价值。

5.3 对安全研究场景的价值·

做固件逆向或 fuzzing 时,有时会用 LLM 工具辅助分析——比如让 Claude Code 解释一段反编译出来的代码,或者让 Codex 生成 fuzzing harness。这类工作场景通常在隔离的内网环境里,能把 LLM 工具的网络请求和分析目标的流量完全分开是基本要求,代理层正好提供了这种隔离。

5.4 稳定性的本质·

直连远端 API 意味着链路质量完全由你的 ISP 和对方的 CDN 决定。自建隧道可以把不稳定性控制在隧道内部,通过选择更好的隧道协议、更近的出口节点来改善。LLM 工具的 session 通常有较长的交互时间,链路中途抖动导致的超时比一次性 HTTP 请求痛苦得多——这是稳定性问题比普通 API 调用更值得重视的原因。

总结·

整套方案的核心逻辑并不复杂:LLM CLI 是 HTTP 客户端,HTTP 客户端读环境变量,环境变量指向代理,代理接隧道,隧道打到出口。每一层职责清晰,出了问题按层排查。工程上真正需要花时间的,是把这条链路做稳、做可观测。