项目文章联系
← 返回文章列表
技术实践2026-08-12

WSL2 中让 Codex CLI 使用 Windows VPN

摘要

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.com

WSL2:

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.com

Connection 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 请求成功

只有每一段都成立,才能确认问题真正解决。