Article
它根本没走代理:一次 Codex + WSL 的网络排查
最近把工作环境挪到了 WSL,用 Codex 的时候三天两头超时,界面上一直转圈 Reconnecting... / Request timed out。第一反应当然是,WSL 里代理没配好嘛。结果这个”当然”,让我甩锅甩了大半天,最后被自己打脸打得挺疼。记一下这次排查,也给同样墙内 + WSL 的朋友提个醒。
第一个想当然
WSL2,mirrored(镜像)网络模式,好处是 127.0.0.1 直接通到 Windows 上的代理。于是我把环境变量配好:
export HTTP_PROXY=http://127.0.0.1:7890
export HTTPS_PROXY=http://127.0.0.1:7890
curl 一测,秒通,心想这不就搞定了。结果切回 Codex,照样超时。
甩锅时间:是不是节点在抖?
代理明明是通的,那问题肯定在别处。我想当然地怀疑是国内代理节点不稳,长连接被掐,尤其我平时开着比较高的推理档,单轮跑挺久,连接开得久,撞上节点抽风的概率也高。
于是我认真测了一圈:短请求、持续 73 秒的活跃流、对 chatgpt.com 空闲挂 80 秒的隧道,全都稳稳的,一点没断。
打脸。代理链路健康得很,根本不是节点的问题。我在一个错误的方向上浪费了不少时间,全凭”想当然”。
真相:它根本没走代理
停止猜测,去看真实行为。翻 Codex 的日志,最底层的报错长这样:
connect error for 199.59.148.89:443: os error 110 (Operation timed out)
199.59.x 是 Twitter 的 IP 段,日志里还有个 Facebook 的 IPv6。这俩显然都不是 chatgpt.com,而是 GFW 的 DNS 污染,把域名解析投毒成了一堆被墙的 IP。
但更关键的是下一步。我用 ss -tnp 看了下 Codex 进程的实时连接:
192.168.x.x:xxxxx -> 199.59.148.89:443
[wsl-ipv6]:xxxxx -> [2a03:2880:...]:443
全是直连外网 :443,没有一条连 127.0.0.1:7890。
到这里就全明白了:Codex 的 HTTP 客户端压根不读 HTTP_PROXY。 它自己在本地解析 DNS(拿到被污染的假 IP),然后直连,直连必然被墙,于是超时。curl 用同一套环境变量能乖乖走代理,Codex 不能,因为新版本换了自研的网络栈,不再认这套约定了(老版本是好好读的,这算是个回归)。
之前所有的排查之所以是错的,就是因为我一直默认”它会走代理”。可它压根就没有。
那我把 DNS 换干净行不行?
既然是 DNS 污染,那我换个干净的解析器,拿到真实 IP 不就好了?
我又去测了一把:就算拿到 chatgpt.com 真实的 Cloudflare IP,不走代理、带上正确的 SNI 直连,依然超时。GFW 对 chatgpt.com 是 DNS 污染 + SNI/IP 层封锁的双保险。
所以单改 DNS 没用。问题的本质是:流量必须从境外节点出去,而 Codex 是直连的,DNS 只能改它”连哪个 IP”,改不了”直连”这个动作本身。
(顺便解答一个疑问:为什么我在 Windows 上从来没这问题?因为 Windows 上代理软件的系统代理 / TUN 模式,会透明地接管所有程序的直连流量。进了 WSL 没人接管,Codex 这种”不听话”的程序就露馅了。)
解决:让不听话的程序也无处可逃
既然靠环境变量指挥不动它,那就换个思路:不问它愿不愿意,直接在网络层把它的流量抢过来。 这就是 TUN 透明代理。
我在 WSL 里跑了个 sing-box:代理出站 + TUN 入站 + fake-ip DNS。TUN 会把所有出站 TCP(包括 Codex 的直连)透明地劫持进代理;fake-ip 顺手把 DNS 污染也一起绕过了。域名被解析成一个占位的假地址,TUN 拦下来再还原成域名交给节点去连,本地根本不做真实解析。
配置骨架大概长这样(节点信息用占位符):
{
"dns": {
"servers": [
{ "type": "https", "tag": "proxy-dns", "server": "1.1.1.1", "detour": "proxy" },
{ "type": "fakeip", "tag": "fake", "inet4_range": "198.18.0.0/15" }
],
"rules": [{ "query_type": ["A", "AAAA"], "action": "route", "server": "fake" }]
},
"inbounds": [
{ "type": "tun", "auto_route": true, "stack": "gvisor" }
],
"outbounds": [
{ "type": "proxy_here", "tag": "proxy", "server": "your-node.example", "server_port": 0, "password": "***" }
],
"route": {
"rules": [{ "protocol": "dns", "action": "hijack-dns" }],
"final": "proxy"
}
}
这里我踩了一个关键的坑,值得单独拎出来:DNS 必须真的被劫持进 tun 才行。 WSL 默认的 DNS 是个私网地址,它不会走进 tun,于是应用照旧拿到污染 IP 丢给节点,节点当然连不上(日志里表现为 remote error: ... i/o timeout,我一度以为是节点坏了)。解法是把 resolv.conf 指到一个公网 DNS(比如 8.8.8.8),公网地址才会被路由进 tun,被 sing-box 拦下来换成 fake-ip。这个 8.8.8.8 其实只是个”路由标记”,真实流量根本不会发到它那儿。
验证也很直白,用 --noproxy 忠实地模拟 Codex “无视代理直连”的行为:
# DNS 应返回 fake-ip(198.18.x),而不是污染 IP
getent ahosts chatgpt.com
# 直连模拟应该秒通(403/401 是没带 auth 的预期响应,重点是"快")
curl --noproxy '*' -o /dev/null -w "%{http_code} %{time_total}s\n" https://chatgpt.com/backend-api/me
看到 fake-ip、看到毫秒级的 403,就说明这条透明通道通了。回到 Codex,不再转圈。
一些收获(和吐槽)
- 环境变量代理不是万能的。
HTTP_PROXY本质是一句”请你自觉走代理”的约定,程序认不认全看它自己。curl / git / npm 认,但不是所有程序都认,遇到不认的,你设一百遍也没用。 - 对付”不听话”的程序,TUN 透明代理才是终极答案。 它不跟程序商量,直接在网络层接管。想清楚这一层,很多”明明配了代理却不生效”的怪问题都能收敛到同一个解法。
- 排查别停在猜测上。 我前面把时间全耗在”是不是节点抖”这种想当然里,真正一锤定音的是
ss -tnp和日志里那行os error 110,去看程序到底在连谁,真相就藏在那里。以后再遇到网络诡异,我大概会先看连接、再开口猜。 - 最后吐槽一句 Codex:老版本明明规规矩矩读
HTTP_PROXY,新版本换了网络栈就不认了,对墙内 + WSL 的用户挺不友好。希望哪天修回来,到时候这套 sing-box 就能光荣下岗了。