摘要
Windows 上的 iKuuu VPN 可以正常访问 OpenAI API,但同一台机器中的 WSL2 运行 Codex CLI 时请求超时。这个问题不应简单归结为“WSL2 不能使用 Windows VPN”,而应拆成四个问题:
1. VPN 提供的是 TUN 接管,还是 HTTP/SOCKS/mixed 代理入口;
2. WSL2 能否访问 Windows 网络栈或 Windows 侧的代理端口;
3. Codex CLI 是否获得了正确的代理环境变量;
4. 多个 VPN 是否争用了同一个监听端口。
本文记录一种可复现的实践:当 iKuuu 只有 TUN、没有可供 CLI 使用的 HTTP/SOCKS 端口时,在 Windows 上运行一个只监听 127.0.0.1 的 HTTP CONNECT 转发器,再让 WSL2 中的 Codex CLI 通过它建立 HTTPS 隧道。
WSL2 中的 Codex CLI
│ HTTPS_PROXY=http://127.0.0.1:17892
▼
Windows 本地 HTTP CONNECT 转发器
│ Windows 出站 TCP
▼
Windows 路由 / iKuuu TUN
▼
OpenAI API关键前提是:Windows 进程直接访问目标地址时,确实会经过 iKuuu 的 TUN 路径。如果 VPN 只接管浏览器代理流量,并不接管普通 TCP 出站连接,那么这个转发器不会自动获得 VPN 能力,应该改用 VPN 自己提供的代理端口。
一、TUN、代理端口和控制端口不是一回事
TUN 模式
TUN 通常通过虚拟网卡和路由规则接管系统流量。应用程序照常建立 TCP 连接,是否经过 VPN 由操作系统路由和 VPN 驱动决定。
TUN 本身不是可以填入 HTTP_PROXY 的端口。
HTTP、SOCKS 和 mixed 模式
代理模式会在某个端口监听,例如:
HTTP 代理:127.0.0.1:7892
SOCKS5 代理:127.0.0.1:7891
mixed 代理:127.0.0.1:7890这类端口才可以被支持它的客户端使用。
控制端口不是代理端口
Clash 类程序的控制 API 端口通常只用于查询状态、切换配置或发送管理请求,例如 127.0.0.1:9090。它不一定接受 HTTP CONNECT,也不一定能转发 HTTPS。
因此,不能因为端口开放,就直接配置:
export HTTPS_PROXY=http://127.0.0.1:9090除非程序文档明确说明 9090 是 HTTP 代理端口。
二、先验证 Windows 与 WSL2 的出站差异
Windows PowerShell:
curl.exe -4 -I https://api.openai.comWSL2:
curl -4 -I https://api.openai.com如果 Windows 成功、WSL2 直连超时,说明账号、域名和 Windows 侧 VPN 通常不是首要嫌疑,应该重点排查 WSL2 的出站路径。
API 根路径不一定适合用 HEAD 作为业务测试。经过 CONNECT 代理时,重点观察是否出现:
HTTP/1.1 200 Connection Established这表示代理隧道已建立。之后 API 返回 401、404、405 或 421,可能只是认证、路径或请求方法问题,并不等价于网络不通。
三、配置 WSL2 网络模式
在 Windows 用户目录的 .wslconfig 中,可以使用:
[wsl2]
networkingMode=mirrored
dnsTunneling=true
autoProxy=true修改后重启 WSL2:
wsl --shutdown三个选项解决的问题不同:
networkingMode=mirrored:改善 WSL2 与 Windows 网络栈、局域网和部分 VPN 场景的互通性;dnsTunneling=true:让 DNS 解析更接近 Windows 网络环境;autoProxy=true:尝试同步 Windows 已存在的系统 HTTP 代理信息。
边界也很重要:autoProxy 只能同步“已经存在且可表达为 HTTP 代理”的配置。它不会把任意 TUN VPN 自动转换成 WSL2 可用的 HTTP/SOCKS 代理。
本文假定 mirrored networking 下,WSL2 可以访问 Windows 的 127.0.0.1:17892。若使用 NAT 模式,应改用 WSL2 可达的 Windows 主机地址,并先验证可达性,不能无条件照抄 127.0.0.1。
四、创建 iKuuu 的本地 CONNECT 入口
另一款 VPN 已经使用 7892,所以为 iKuuu 使用独立端口 17892。
将下面脚本保存为 Windows 文件,例如 C:\Tools\wsl-http-proxy-17892.js:
const net = require('node:net');
const HOST = '127.0.0.1';
const PORT = Number(process.env.WSL_HTTP_PROXY_PORT || 17892);
function writeError(client, status, message) {
if (!client.destroyed) {
client.end(`HTTP/1.1 ${status}\r\nConnection: close\r\n\r\n${message}\n`);
}
}
const server = net.createServer((client) => {
let requestBuffer = Buffer.alloc(0);
let handled = false;
const onData = (chunk) => {
if (handled) return;
requestBuffer = Buffer.concat([requestBuffer, chunk]);
const end = requestBuffer.indexOf('\r\n\r\n');
// 请求头可能被拆成多个 TCP 数据包,继续等待完整请求头。
if (end < 0) {
if (requestBuffer.length > 64 * 1024) {
handled = true;
writeError(client, '431 Request Header Fields Too Large', 'Headers too large');
}
return;
}
handled = true;
client.removeListener('data', onData);
const header = requestBuffer.subarray(0, end).toString('latin1');
const [requestLine] = header.split('\r\n');
const [method, target] = requestLine.split(' ');
if (!method || !target || method.toUpperCase() !== 'CONNECT') {
writeError(client, '405 Method Not Allowed', 'CONNECT is required');
return;
}
const separator = target.lastIndexOf(':');
const host = separator > 0 ? target.slice(0, separator) : '';
const port = Number(separator > 0 ? target.slice(separator + 1) : '');
if (!host || !Number.isInteger(port) || port < 1 || port > 65535) {
writeError(client, '400 Bad Request', 'Invalid CONNECT target');
return;
}
const upstream = net.connect({ host, port });
upstream.once('connect', () => {
if (client.destroyed) {
upstream.destroy();
return;
}
client.write('HTTP/1.1 200 Connection Established\r\n\r\n');
// 请求头之后的剩余数据属于隧道数据,不能丢弃。
const remainder = requestBuffer.subarray(end + 4);
if (remainder.length > 0) upstream.write(remainder);
client.pipe(upstream);
upstream.pipe(client);
});
upstream.once('error', () => {
if (!client.destroyed) writeError(client, '502 Bad Gateway', 'Upstream connection failed');
});
upstream.once('close', () => {
if (!client.destroyed) client.end();
});
client.once('close', () => upstream.destroy());
};
client.on('data', onData);
client.once('error', () => client.destroy());
});
server.on('error', (error) => {
console.error(`Proxy failed to listen on ${HOST}:${PORT}:`, error.message);
process.exitCode = 1;
});
server.listen(PORT, HOST, () => {
console.log(`HTTP CONNECT proxy listening on http://${HOST}:${PORT}`);
});这个程序不实现 VPN 协议,也不修改 iKuuu。它只接收 CONNECT 请求,再由 Windows 进程连接目标地址并双向转发。能否走 iKuuu,取决于 Windows 对该出站连接的实际路由。
启动:
node C:\Tools\wsl-http-proxy-17892.js检查监听状态:
Get-NetTCPConnection -State Listen -LocalPort 17892预期监听地址是 127.0.0.1:17892,而不是 0.0.0.0:17892。只监听回环地址可以避免把开放式 CONNECT 转发器暴露给局域网。
五、在 WSL2 中配置 Codex CLI
不要假设 ~/.codex/.env 会被 Codex CLI 自动加载。代理是否生效,应以当前 shell 的环境变量和实际连接测试为准。
将以下内容加入 WSL2 的 ~/.bashrc;如果登录 shell 不读取 .bashrc,也放入 ~/.profile:
export HTTP_PROXY=http://127.0.0.1:17892
export HTTPS_PROXY=http://127.0.0.1:17892
export ALL_PROXY=http://127.0.0.1:17892
export NO_PROXY=localhost,127.0.0.1,::1重新打开终端后检查:
env | grep -i proxy
codex --version
curl -4 -v -I https://api.openai.com重点观察:
- 是否连接到
127.0.0.1:17892; - 是否收到
200 Connection Established; - 隧道建立后 TLS 是否继续完成;
- 是否出现
Connection refused、502 Bad Gateway或 DNS 错误。
如果 WSL2 连接代理被拒绝,先检查 Windows 进程和监听地址;如果得到 502,说明转发器收到了请求,但 Windows 到目标地址的出站连接失败。
不同客户端对代理变量的支持并不完全一致。HTTP_PROXY 和 HTTPS_PROXY 通常最重要;ALL_PROXY 只是支持它的客户端的兜底配置,不能据此断言 Codex 必然使用它。必要时可以同时设置小写版本:
export http_proxy="$HTTP_PROXY"
export https_proxy="$HTTPS_PROXY"
export all_proxy="$ALL_PROXY"
export no_proxy="$NO_PROXY"六、设置 Windows 登录自动启动
手动启动 Node 只能验证方案。要让 Windows 登录后自动启动,可注册当前用户任务:
$node = (Get-Command node.exe).Source
$script = 'C:\Tools\wsl-http-proxy-17892.js'
$action = New-ScheduledTaskAction `
-Execute $node `
-Argument ('"' + $script + '"') `
-WorkingDirectory (Split-Path -Parent $script)
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME
Register-ScheduledTask `
-TaskName 'Codex WSL local proxy' `
-Action $action `
-Trigger $trigger `
-Description 'Local CONNECT relay for WSL Codex CLI' `
-Force验证:
Get-ScheduledTask -TaskName 'Codex WSL local proxy'
Get-NetTCPConnection -State Listen -LocalPort 17892如果任务失败,先检查 Node 和脚本的绝对路径。此转发器只需要监听当前用户的回环端口,通常不需要开放防火墙入站规则。
七、与另一款 VPN 共存
假设另一款 VPN 使用 7892:
切换到另一款 VPN:
export HTTP_PROXY=http://127.0.0.1:7892
export HTTPS_PROXY=http://127.0.0.1:7892
export ALL_PROXY=http://127.0.0.1:7892切换回 iKuuu:
export HTTP_PROXY=http://127.0.0.1:17892
export HTTPS_PROXY=http://127.0.0.1:17892
export ALL_PROXY=http://127.0.0.1:17892环境变量不会自动识别当前激活的是哪一个 VPN。可以在 ~/.bashrc 中封装:
use_ikuuu() {
export HTTP_PROXY=http://127.0.0.1:17892
export HTTPS_PROXY=http://127.0.0.1:17892
export ALL_PROXY=http://127.0.0.1:17892
}
use_other_vpn() {
export HTTP_PROXY=http://127.0.0.1:7892
export HTTPS_PROXY=http://127.0.0.1:7892
export ALL_PROXY=http://127.0.0.1:7892
}确认端口归属:
Get-NetTCPConnection -State Listen -LocalPort 7892,17892 |
Select-Object LocalAddress,LocalPort,OwningProcess再根据 PID 确认进程:
Get-Process -Id <PID>普通 TCP 端口不能被两个进程同时监听。两个 VPN 都配置成 7892 时,后启动的程序可能失败,也可能导致 CLI 连接到错误的代理。
八、故障排查清单
Windows 端口没有监听
Get-NetTCPConnection -State Listen -LocalPort 17892没有输出表示转发器没有成功启动。
WSL2 无法连接端口
curl -v -x http://127.0.0.1:17892 -I https://api.openai.comConnection refused 通常指向监听进程、绑定地址或 WSL2 到 Windows 的可达性;502 通常说明转发器工作了,但 Windows 到目标地址失败。
Windows 也无法访问
先在 Windows 中直接访问目标地址,确认 iKuuu 已开启且 Windows 侧请求成功。Windows 直连都失败时,修改 WSL2 的代理变量没有意义。
Codex 没有继承变量
如果 Codex 通过 IDE、桌面快捷方式或脚本启动,启动环境可能不同于交互式 Bash。应在实际启动入口配置变量,而不能只修改当前终端的 .bashrc。
NO_PROXY 配置过宽
NO_PROXY 可以包含 localhost、127.0.0.1 和 ::1,但不应包含 api.openai.com,否则请求会绕过代理。
九、安全与可靠性边界
这个示例是简化的本机 CONNECT relay,不是完整的生产级代理服务器,没有认证、访问控制、连接数限制、审计和目标白名单。建议:
- 不要把监听地址改成
0.0.0.0,除非明确需要局域网访问; - 不要把控制 API 端口当作代理端口;
- 正式长期运行时,优先使用成熟的本地代理软件或 VPN 自带代理入口;
- 定期确认实际的进程、端口和路由,避免流量悄悄切换到另一款 VPN。
本文使用 -4 是为了先排除 IPv6 路由问题。稳定运行前,还应按实际环境验证 IPv6、DNS、证书握手以及 Codex 的真实 API 请求,而不只看一次 curl 结果。
结论
问题的核心不是“WSL2 完全不能使用 Windows VPN”,而是三个层次没有自动衔接:
- iKuuu 的 TUN 是 Windows 网络接管机制,不等于 HTTP/SOCKS 代理端口;
- mirrored networking 能改善互通,但不会凭空生成代理入口;
- Codex 是否走代理,取决于它启动时实际获得的环境变量以及客户端本身的代理支持。
当 Windows 侧 iKuuu 只有 TUN、WSL2 直连 OpenAI 超时、另一款 VPN 又占用 7892 时,可以为 iKuuu 增加一个只监听 127.0.0.1:17892 的 CONNECT 转发器,并在 WSL2 中将 HTTP_PROXY 和 HTTPS_PROXY 指向它。这样既复用了 Windows 侧已经验证可用的网络路径,也避免了端口冲突。
最终验证链路应当是:
WSL2 环境变量
→ 127.0.0.1:17892 可达
→ CONNECT 返回 200
→ Windows 目标连接成功
→ iKuuu TUN 路由生效
→ Codex CLI API 请求成功只有每一段都成立,才能确认问题真正解决。