需求:一边连着公司 OpenVPN 访问内网代码仓库,一边用 v2rayN 访问 ChatGPT 和 Codex。
结果:两个一起开,内网仓库就打不开了。这篇文章记录了完整的排查过程——包括两个我一开始判断错的方向、三个真正的坑,以及最终稳定运行的配置方案。
〇、TL;DR(赶时间的看这里)
如果你也遇到类似场景,先直接对照这几条:
- 别开 TUN 模式。 TUN 网卡的 interface metric 最低,Windows 会优先使用它上面配的 DNS,
而代理软件的 DNS 上游是公网 DNS/DoH——在公司 VPN 环境下全部不通,导致所有域名解析超时。 - 像 Codex / ChatGPT 这类桌面应用,不读 Windows 系统代理。 它们需要环境变量或专属配置文件。
- 系统代理一开,hosts 文件就等于”失效”了。 浏览器不会自己解析域名,而是把请求直接甩给代理,
你 hosts 里指向127.0.0.1的本地开发域名会被代理走掉,访问到真实的公网站点。 - 最终方案:v2rayN 用 PAC 模式(自定义 PAC 识别本地域名),Codex 用
~/.codex/.env,
清空全局环境变量。三条通道互相独立,谁也不干扰谁。
一、环境与现象
1.1 环境
| 组件 | 说明 |
|---|---|
| 公司 VPN | OpenVPN Connect,分流模式(split tunnel) |
| 代理客户端 | v2rayN(Windows),混合入站端口 20808 |
| 代理内核 | sing-box |
| 需要代理的工具 | 浏览器访问 ChatGPT、Codex 桌面版 |
| 需要直连的资源 | 公司内网 Git 仓库、本地 PHP 开发项目 |
1.2 现象
- 只开 OpenVPN:内网仓库正常,ChatGPT 打不开。
- 只开 v2rayN(TUN 模式):ChatGPT 正常,内网仓库打不开。
- 两个同时开:内网仓库打不开,且几乎所有域名都解析失败。
最迷惑的是第三点:不是”内网访问慢”,而是 DNS 全面瘫痪,连公网域名都解析不出来。
二、坑 1:TUN 模式把 DNS 打成了黑洞
2.1 最初(错误)的判断
第一反应是:”TUN 开起来后会劫持 53 端口,把发往公司 DNS 的查询也吃掉了。”
这个判断听起来合理,但是错的。实际测试数据推翻了它。
2.2 实测对照
在两种状态下分别执行 nslookup、curl、route print,得到关键差异:
| 检查项 | TUN 关闭 | TUN 开启 |
|---|---|---|
nslookup 默认服务器 |
100.100.2.136(公司 VPN 网卡) |
172.18.0.2(TUN 网卡) |
| 内网域名解析 | ✅ 返回 172.16.10.20 |
❌ 超时 |
curl -I https://git.corp.example.com |
✅ 302 Found |
❌ Could not resolve host |
| 公网域名解析 | ✅ 正常 | ❌ 超时 |
2.3 真正的根因
问题不在”端口劫持”,而在 Windows 的 DNS 服务器选择机制。
- sing-box 创建的 TUN 网卡,其 interface metric 被设为 0(最低)。
- Windows 在选择 DNS 服务器时,优先使用 metric 最低的网卡上配置的 DNS。
- 于是
nslookup的默认服务器变成了 TUN 网卡的172.18.0.2。 - 而 sing-box 收到的所有查询,都会被转发到它的 DNS 上游——公网 DNS 或 DoH。
- 在公司 VPN 环境下,公网 DNS 全部不通 → 全线超时。
关键推论:这不是“53 端口被劫持”。因为去往 100.100.2.136 的查询按路由表本来就会走 OpenVPN
(100.64.0.0/10 比 TUN 的 0.0.0.0/0 更具体,最长前缀匹配优先),根本不会进 TUN。
2.4 教训
“TUN 抢 DNS” 和 “TUN 劫持 53 端口” 是两件事。
前者是 Windows 的网卡优先级问题,后者是代理软件的 DNS 劫持功能。
混为一谈会让你在错误的方向上排查很久。
顺便记一下当时的路由表关键条目:
1 | 172.16.0.0/16 -> 172.31.0.1 (公司网段,注意是 /16 不是 /12) |
三、坑 2:Codex 根本不读系统代理
3.1 现象
开了系统代理之后:
- ✅ 浏览器能打开 ChatGPT 网页版
- ❌
ping api.openai.com一直超时 - ❌ Codex 桌面版连不上
一开始以为是 DNS 污染(解析出来的是 Bitly / Facebook 网段的 IP,是典型污染特征),
但后来发现:用 curl 走代理访问 api.openai.com,返回的是 OpenAI 真实的错误响应
(http_unsupported,说明链路是通的)。
所以问题不在网络,而在有些程序压根不读系统代理。
3.2 根因
Windows 上的”代理”其实有两条完全独立的通道:
| 通道 | 机制 | 谁在用 |
|---|---|---|
| 系统代理 | WinINET 设置(注册表 ProxyEnable / AutoConfigURL) |
浏览器、部分桌面程序 |
| 环境变量 | HTTP_PROXY / HTTPS_PROXY |
Node.js、Rust、Go 写的命令行工具 |
Codex 属于后者。
而且更麻烦的是——Codex 桌面版是 MSIX 打包应用(装在 C:\Program Files\WindowsApps\),
这意味着:
- 它没有开始菜单
.lnk,你没法改快捷方式 - 用
.bat包一层启动也不可靠——MSIX 应用激活时,环境块由系统重新生成,不继承调用者的自定义变量
所以”写个启动脚本给它单独设变量”这条常规思路,在这里是走不通的。
3.3 解法:.codex/.env
Codex 官方提供了一个专门解决这个问题的机制。官方规范原文:
At startup, Codex attempts to load
${CODEX_HOME}/.envif it exists.
Variables whose names start withCODEX_are ignored when loading.env.
Other variables (e.g.,OPENAI_API_KEY,HTTP_PROXY, etc.) may be loaded.
也就是说:Codex 启动时会自己去读 ~/.codex/.env,里面的代理变量会被加载进 Codex 自己的进程。
其他程序完全不知道这个文件存在。
这比设全局环境变量干净得多:
| 全局环境变量 | .codex/.env |
|
|---|---|---|
| Codex | ✅ 走代理 | ✅ 走代理 |
| PHP / curl / Guzzle | ⚠️ 也被牵连 | ✅ 不受影响 |
| git / npm / pip | ⚠️ 也被牵连 | ✅ 不受影响 |
| 作用域 | 整个用户会话 | 只有 Codex 进程树 |
💡 小提示:
.env里以CODEX_开头的变量会被 Codex 忽略(安全设计),
所以别试图在那里设CODEX_HOME之类的配置。
四、坑 3:系统代理一开,hosts 就”失效”了
4.1 现象
本地开发环境里,hosts 文件挂了一批域名指向 127.0.0.1:
1 | 127.0.0.1 laravel.io |
开了系统代理之后,这些本地项目全部访问不了——浏览器打开 http://laravel.io,
看到的居然是真实的公网 laravel.io。
4.2 根因
系统代理模式下,hosts 文件等于不存在。
原因在于流程:
1 | 浏览器 → 系统代理已开启 → 不解析域名,直接把请求甩给 127.0.0.1:20808 |
而当时系统代理的例外列表是这样的:
1 | localhost;127.*;10.*;172.16.*;...;172.31.*;192.168.* |
清一色是 IP 段,一个域名都没有。
所以:请求根本没走到”解析域名”这一步,就被代理接管了。hosts 完全没机会生效。
4.3 为什么 PAC 模式能解决
因为「PAC 模式」和「自动配置系统代理」是同一个通道的两种写法(都写进 Windows 系统代理),
唯一的区别是——走不走代理由谁决定:
| 自动配置系统代理 | PAC 模式 | |
|---|---|---|
| 判定依据 | 系统代理例外列表(只认 IP/网段) | PAC 脚本(认域名,可以写逻辑) |
| 本地域名 | ❌ 只能逐个写死 | ✅ 可以写规则自动判定 |
PAC 脚本在浏览器发出请求之前运行,返回 DIRECT 时浏览器就会自己去解析域名——
这一解析就会读到 hosts,问题解决。
4.4 顺带一提:PHP 的 curl 也会读环境变量
排查过程中还发现一条隐藏的坑:
PHP 的 curl 扩展(以及 Guzzle)会主动读取
http_proxy环境变量。
而 Windows 的环境变量不区分大小写——你设的HTTP_PROXY对 PHP 来说就是http_proxy。
也就是说,如果用 php artisan serve / PHP-FPM 跑项目,项目里任何一次 curl 请求都会被送进代理。
解法是完善 NO_PROXY,把公司域名和本地开发域名都包含进去。
五、最终方案:三条通道各司其职
5.1 架构
整个链路里有三条完全独立的通道:
1 | 浏览器 Codex PHP / curl / git |
三条通道唯一的共同依赖是:v2rayN 正在运行(入站端口在监听)。
⚠️ 重要认知:v2rayN 的系统代理开关,跟 Codex 没有任何关系。
Codex 走的是.env那条线。所以系统代理设成 PAC、自动配置、甚至关闭,
Codex 都照用不误——只要 v2rayN 在跑。
5.2 v2rayN:切换到 PAC 模式
在 v2rayN 里设置:设置 → 参数设置 → 系统代理
- 系统代理类型:
PAC 模式 - 自定义 PAC 文件路径:指向下面这个文件
PAC 采用黑名单式策略:默认全部走代理(行为跟原来一致),只把本地/内网目标放行直连。
5.3 PAC 脚本
1 | // ===================================================================== |
第 5 条是这个脚本的精髓:以后你在 hosts 里加任何新域名,不用改 PAC,它会自动识别。
如果发现网页加载变慢,把
AUTO_DETECT_HOSTS改成false即可退回纯列表判断。
5.4 Codex:专属 .env
创建 C:\Users\<用户名>\.codex\.env(Windows)或 ~/.codex/.env(macOS/Linux):
1 | # Codex loads this file at startup (CODEX_HOME/.env). |
改完必须完全退出 Codex 再重启 —— Windows 上是右键托盘图标 → 退出,
光关闭窗口不算。环境变量只在进程启动时读一次。大小写各写一份是为了兼容不同库的读取习惯(Windows 下大小写不敏感,值一样,无害)。
5.5 清理全局环境变量
如果之前为了应急设过全局的 HTTP_PROXY / HTTPS_PROXY,现在可以清掉了:
1 | :: 清除(当前会话生效,不需要管理员) |
清掉之后,PHP、git、npm 之类的工具就完全不受代理影响了。
六、完整配置清单
| 组件 | 设置 | 说明 |
|---|---|---|
| OpenVPN | 保持连接 | 分流模式,内网路由由它下发 |
| v2rayN | 保持运行 + 选好节点 | 必须 |
| v2rayN 系统代理类型 | PAC 模式 + 自定义 PAC 路径 | 二选一,不要同时开自动配置 |
| Codex | ~/.codex/.env |
专属,不影响别人 |
| 全局环境变量 | 清空 | 关键一步 |
| TUN 模式 | 关闭 | 它会抢 DNS |
七、验证清单
| 检查项 | 命令 / 操作 | 期望结果 |
|---|---|---|
| 内网路由存在 | route print -4 |
有 172.16.0.0/16 -> <VPN网关> |
| 内网域名真实解析 | nslookup git.corp.example.com |
返回内网 IP,不是 198.18.x.x |
| 内网仓库可达 | curl -I https://git.corp.example.com |
302 |
| 全局变量已清 | 新开 cmd,echo %HTTPS_PROXY% | 输出 %HTTPS_PROXY%(未定义) |
| 浏览器上 ChatGPT | 访问 chatgpt.com | ✅ |
| 本地项目可访问 | 浏览器打开 http://myapp.test | 你自己的项目,不是公网站点 |
| Codex 可用 | 打开对话 | ✅ |
八、避坑速查表
| 症状 | 大概率原因 | 处理 |
|---|---|---|
| 开了代理后所有域名都解析不了 | TUN 网卡抢了系统 DNS | 关掉 TUN;或把 TUN 网卡的 DNS 改成内网 DNS |
| 内网能 ping 通但域名解析失败 | 同上 | 同上 |
| 浏览器能上 ChatGPT,但某个桌面应用不行 | 该应用不读系统代理 | 找它的环境变量注入方式(如 .codex/.env) |
| 命令行工具(curl/git)不走代理 | 同上,命令行工具只读环境变量 | 设 HTTP_PROXY / HTTPS_PROXY |
| 本地 hosts 域名被解析成公网站点 | 系统代理例外列表只有 IP,没有域名 | 改用 PAC 模式,或把域名加进例外列表 |
| PHP 项目请求异常 | PHP 的 curl 读了 http_proxy |
完善 NO_PROXY |
| 装了 Clash 但节点跑不起来 | — | 不必换工具,v2rayN 的 PAC 模式就够了 |
九、总结
这次排查最大的收获,是把 Windows 上”代理”这件事彻底理清了:
Windows 上的代理不是一个开关,而是三条互不相干的通道:
系统代理(浏览器)、环境变量(命令行 / 部分应用)、以及直连。搞清楚某个程序属于哪条通道,问题就解决了一半。
至于另外两个坑:
- TUN 的杀手锏不是劫持,是 metric。 它把网卡优先级设到最高,顺带把系统 DNS 也接管了。
- hosts 和系统代理是互斥的。 只要请求交给了代理,域名解析这一步就不在你机器上发生了,
hosts 自然无从生效。PAC 之所以能救回来,是因为它让浏览器自己去解析域名。
最终这套配置跑了很久都很稳定。如果你也在折腾类似的环境,希望这篇能帮你少走几个小时的弯路。