lan-direct 是做什么的?
将应用程序的流量通过ISP路由出去,而你,即代理继续留在VPN上。
同时适用于Claude Code和Codex——其机制是基于每套接字绑定,这与具体的调用框架无关。
不变原则
绝不修改路由表、WinINET/系统代理设置或VPN客户端的配置。只允许:
- 设置子进程作用域内的环境变量,或
- 传递每工具源绑定标志。
这两种方式都是基于每套接字且自愿加入的,因此它们不会影响你自己的连接。这正是本技能的核心价值——如果你使用了route add,你就破坏了它(参见末尾的逃生舱口部分)。
不要建议断开VPN连接。这正是本技能旨在避免的问题。
工作流程
1. 检测与验证
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\lan-detect.ps1
在Linux或macOS上,请使用对应的脚本——相同的JSON输出,相同的退出码约定:
bash scripts/lan-detect.sh
输出JSON:lanIp、gateway、lanDns、subnet、tunnelActive、vpnAdapter、directEgress、lanEgress、fakeIpDns、bypassWorks。仅当bypassWorks为true时退出码为0——它通过两次实时探测来证明绕过生效,而非推断。
tunnelActive: false-> 没有VPN在运行。说明情况并停止;无需绕过。bypassWorks: false且隧道活跃 -> 转到下方的故障排除部分。不要继续。流量会悄无声息地停留在VPN上,而你可能会报告一个错误的测试通过。
2. 选择机制
Node后端(包括Next.js)?跳过代理 - 直接跳转到“按栈配方”下的Node配方。一个环境变量即可,无需代理进程,并且是唯一能够连接原生fetch的方式。
其他所有情况:启动绑定到LAN的代理。服务器进程没有--interface标志,因此这是它获得ISP出口的唯一方式。
node scripts/lan-proxy.mjs --bind <lanIp> --port 8899 # 在后台运行
node scripts/lan-proxy.mjs --bind <lanIp> --port 8899 --dns <lanDns> # 如果 fakeIpDns: true
它只在127.0.0.1上监听,并记录它隧道传输的每个主机,这样你就可以看到应用实际调用了什么。
3. 重启后端并将其指向代理
HTTP_PROXY=http://127.0.0.1:8899
HTTPS_PROXY=http://127.0.0.1:8899
NO_PROXY=localhost,127.0.0.1,<subnet>
NO_PROXY很重要:没有它,后端对其本地服务的调用会被无故推送到代理。 (基于TCP的本地数据库——MySQL、Postgres、Mongo——不是HTTP,无论如何都不会受影响。)
在启动后端命令时设置这些变量,而不是全局设置。参见下方的按栈配方——对于某些栈,仅设置HTTP_PROXY是不够的。
4. 执行与确认
访问原本因出站调用失败而无法访问的端点。确认两者:
- 调用现在成功,并且
- 代理日志显示了它连接的主机。
如果没有匹配的日志行,说明请求从未经过代理。
5. 清理
停止代理,在不设置环境变量的情况下重启后端。无需撤销其他操作——没有修改任何全局状态。
在Git Bash中使用pkill -f lan-proxy不会 杀死Windows的node.exe。请使用:
Get-CimInstance Win32_Process -Filter "Name='node.exe'" |
Where-Object { $_.CommandLine -like '*lan-proxy*' } |
ForEach-Object { Stop-Process -Id $_.ProcessId -Force }
残留的实例无害,但会导致下次启动时因端口冲突而失败。
按栈配方
| 栈 | 如何向其提供代理 |
|---|---|
Python requests / httpx | 环境变量,直接使用 |
Go, .NET HttpClient | 环境变量,直接使用 |
| PHP CLI | libcurl 从进程环境读取 http_proxy / https_proxy |
| Node (包括 Next.js) | 不使用代理 - 使用 lan-bind.mjs 预加载,见下文 |
| PHP under Apache/XAMPP | 见下文 - 服务不会继承你的 shell 环境 |
| curl, wget, npm, ssh, git | 跳过代理,直接绑定(见下方的直接绑定) |
Node - 使用预加载,而非代理
原生fetch运行在Node内置的undici上,它在Node <= 23时忽略 HTTP_PROXY,并且不接受任何https.Agent。在Node 22.20上已验证:纯环境变量、NODE_USE_ENV_PROXY=1和--use-env-proxy都无效——代理日志仍为空,流量悄无声息地通过VPN发出。这会影响Next.js、openai SDK以及任何基于原生fetch的库。
完全跳过代理,使用预加载脚本,它绑定进程中每个出站套接字:
LAN_BIND=<lanIp> node --import "file:///<skill-dir>/scripts/lan-bind.mjs" script.js
LAN_BIND=<lanIp> NODE_OPTIONS="--import file:///<skill-dir>/scripts/lan-bind.mjs" npm run dev
<skill-dir> 是本技能的基础目录,使用正斜杠——在Claude Code下通常是C:/Users/<you>/.claude/skills/lan-direct,或在Codex下是C:/Users/<you>/.agents/skills/lan-direct。--import在Windows上需要绝对的file:// URL;带有反斜杠的Windows路径会失败。
NODE_OPTIONS会传播给forked workers,因此它覆盖了Next.js和Vite开发服务器。
这能够同时连接原生fetch、openai SDK、axios、node-fetch、got以及原生的http/https——无需依赖,无需代理进程,也无需HTTP_PROXY。它会跳过回环地址目标,因此本地数据库或开发服务器可以继续正常工作。
仅当预加载不可用时:Node >= 24 尊重设置了环境变量的 NODE_USE_ENV_PROXY=1,并且axios在任何版本上都尊重它们。
PHP under Apache / XAMPP
在httpd.conf中使用SetEnv/PassEnv只会填充$_SERVER;libcurl读取的是进程环境,因此它不会拾取这些变量。请使用以下方法之一:
- 从一个已经导出
http_proxy/https_proxy的shell中启动Apache(例如xampp\apache_start.bat),或 - 在
curl_init()之前使用putenv('https_proxy=http://127.0.0.1:8899');,或 - 直接使用
curl_setopt($ch, CURLOPT_PROXY, '127.0.0.1:8899');。
file_get_contents()不会接受以上任何一种方法——它需要一个包含'proxy' => 'tcp://127.0.0.1:8899'的流上下文。
直接绑定
当工具可以绑定自己的套接字时,无需代理:
| 工具 | 标志 |
|---|---|
| curl | --interface <lanIp> |
| wget | --bind-address=<lanIp> |
| npm | --local-address=<lanIp> |
| ssh | -b <lanIp> |
| Chromium / Playwright | 不存在绑定标志 - 请使用8900端口的浏览器MCP,见下文 |
浏览器 / Playwright MCP
Chromium没有源绑定标志,因此浏览器只能通过代理到达ISP。使用lan-browser.mjs:它是一个Playwright MCP服务器,由它启动并拥有自己的代理,因此浏览器不可能在没有代理的情况下启动。
本技能附带了它——scripts/install.ps1会注册它:
powershell -NoProfile -ExecutionPolicy Bypass -File <skill-dir>\scripts\install.ps1
哪些主机使用ISP 来自三个合并的源,因此这几乎不需要重新安装:技能目录中的lan-hosts.txt(随附的默认配置,git pull时会被替换)、~/.lan-direct-hosts(用户自定义,永远不会被覆盖)以及启动时的--lan-hosts a,b。每行一条规则,允许#注释,支持后缀匹配。
当用户希望另一个主机经由ISP路由时,只需在~/.lan-direct-hosts中添加一行,然后告诉他们重启浏览器——不要编辑lan-hosts.txt,它会在下次更新时被覆盖。
如果用户要求手动注册它,请注意--必须能够通过PowerShell的参数绑定器——将参数作为数组分发,或者从bash运行它:
# 从克隆中运行
claude mcp add playwright-split --scope user -- \
node <skill-dir>/scripts/lan-browser.mjs --lan-hosts ".ir"
# 从npm运行 - 在重新安装时路径不会移动,因此当用户安装了包时首选此方式
claude mcp add playwright-split --scope user -- npx -y lan-direct browser
当包已安装时,其他CLI子命令:lan-direct detect会为平台选择正确的检测脚本,而lan-direct route <host>会回答“这个主机会经由ISP还是隧道?”而无需启动浏览器。当用户报告站点无法加载时,可以使用这个命令——错误的规则和宕机的站点从外部看来是完全一样的。
该服务器会检测LAN IP,如果lan-detect报告绕过功能损坏,它会拒绝启动;如果已有代理在监听,它会复用现有的代理;并在MCP退出时关闭它启动的进程。其自身的日志输出到stderr;stdout保持干净以用于JSON-RPC通信。
它使用8900端口,特意与上面用于后端测试的8899端口不同。复用的依据仅为端口号,因此共享端口号可能会让一个绑定所有流量的后端代理捕获浏览器,并将其所有流量经由ISP路由。
在一个浏览器中实现混合浏览。 --lan-hosts让代理为每个主机做决策:匹配的主机通过ISP出口,其余所有请求保持未绑定状态,走隧道。因此,“检查hoosha和youtube”无需切换工具——hoosha通过ISP出口,youtube通过VPN,在同一个会话中。代理日志会在每一行显示决策:
CONNECT lan hoosha.com:443
CONNECT vpn youtube.com:443
保持列表准确:如果缺少一个国内主机,它会走隧道并以空回复的方式失败;如果一个外国主机被错误地添加到列表中,它会走ISP并可能被过滤。当一个站点以类似死连接的方式失败时,在假定站点宕机之前,先检查日志选择了哪条路由。
为什么使用显式列表而不是自动检测:连接一个被封锁的主机通过隧道在TCP层面是成功的(测量值:connect@14ms),并且大约860毫秒后才会失败,此时代理已经返回了200 Connection Established。回退机制意味着需要缓冲并重放TLS握手,并且每次国内请求都要付出这个延迟代价。
Playwright 通过代理路由回环地址。 Chrome通常绕过本地主机,但当设置了代理时,Playwright会覆盖这个行为,因此http://127.0.0.1:3000会到达lan-proxy.mjs而不是直接连接。这已在代理日志中验证。代理的回环防护是确保本地开发服务器可达的原因——没有它,每个这样的请求都会因EADDRNOTAVAIL而失败。不要添加--proxy-bypass=<-loopback>;它在这里什么都不改变,只会混淆视听。
故障排除
directEgress 等于 lanEgress 且隧道活跃。 VPN正在强制执行杀开关或严格路由(一种阻止绑定套接字的WFP过滤器)。在VPN客户端中修复它,而不是绕过它:禁用严格路由,或者为你需要的主机添加一个直接/绕过路由规则。nekoray/sing-box:关闭strict_route,或添加一个direct出站规则。
在Linux上,即使没有杀开关也可能发生这种情况:源地址本身并不能像在Windows上那样覆盖路由表,因此策略路由会将流量保留在隧道内。修复方法是添加一条规则,将源自LAN IP的流量发送到物理网关——ip rule add from <lanIp> table <n>加上该表中的默认路由。这需要root权限并且是一个系统范围的更改,因此请解释清楚并让用户决定。本技能中的其他所有内容都保持无需特权且限于进程作用域。
fakeIpDns: true。 操作系统解析器正在返回仅对隧道内部有意义的198.18.x.x占位符。请传递--dns <lanDns>给代理,以便它通过LAN自身的DNS进行解析。
出口探测没有返回任何内容。 无法连接到探测主机。请检查网络链接,然后重新运行。
逃生舱口
如果一个后端确实无法设置代理(例如一个没有代理支持的编译后二进制文件),唯一的备选方案是主机特定的静态路由:
route add <targetIp> mask 255.255.255.255 <gateway> metric 1 # 需要管理员权限
route delete <targetIp> # 撤销,始终如此
这是全局的。 它会将该目标地址的所有进程都从隧道中拉出——包括你自己,如果你曾经连接过同一个主机的话。需要提权,会一直存在直到被删除,并且很容易被遗忘。在使用前询问用户,并在同一个会话中将其删除。