curl 能通、浏览器不通——macOS TUN 代理劫持 LAN 流量的排查全记录
内网联调是家常便饭。某天我在自己的 Mac 上打开 Chrome 访问局域网开发服务器,迎头撞上 ERR_CONNECTION_CLOSED。加了 hosts、折腾了证书、禁用了插件,错误码依次变成 ERR_TUNNEL_CONNECTION_FAILED 再到 ERR_ADDRESS_UNREACHABLE——像在剥洋葱,每剥一层又是一层。与此同时终端里 curl 直接拿到 200 OK,ping 也畅通无阻。
这是我踩过的最迷惑的网络坑之一,根因与证书、hosts、Chrome 插件统统无关。
现象
- 浏览器(Chrome / Safari / Edge)访问局域网 IP 或私有 TLD(
.internal/.lan):ERR_CONNECTION_CLOSED→ 折腾后变ERR_TUNNEL_CONNECTION_FAILED→ 再折腾变ERR_ADDRESS_UNREACHABLE - DevTools 网络面板主文档状态显示「已屏蔽:来源」
- 终端
ping <局域网 IP>全程 0% 丢包 - 终端
curl https://<局域网地址>/正常返回响应 scutil --proxy显示HTTPProxy: 127.0.0.1:<某端口>且HTTPEnable: 1- 但对每一个网络服务执行
networksetup -getwebproxy <SVC>均返回Enabled: No
根因
机器上装了翻墙代理工具(Shadowrocket / Clash / Mihomo / Surge 等),且开启了 TUN 模式。
TUN 模式的实现原理:代理 App 通过 macOS NEPacketTunnelProvider(Network Extension 框架)创建一块虚拟网卡 utunN,并将其设为系统默认路由的 PrimaryInterface。所有 GUI 应用(包括浏览器)发出的 TCP 流量,在进入内核路由之前就被 NE 框架的 flow handler 截获,走进 utun 隧道。
networksetup -setproxybypassdomains 操作的是 macOS「经典代理」配置,TUN 模式完全绕过了这一层,bypass 列表在此场景下形同虚设。
ERR_CONNECTION_CLOSED → ERR_TUNNEL_CONNECTION_FAILED → ERR_ADDRESS_UNREACHABLE,这三个错误码只是 Chrome 连接状态机在不同层面的表达,背后都是同一件事:TUN 代理截走了流量且未能正确转发到局域网目标。每次你「修好一个问题」其实只是触发了下一层的错误展示。
同理,ping 成功不等于 TCP 畅通——ICMP 走的是 raw socket,部分 NE 扩展不拦截,且 ping 本身会走已连接路由(connected route);而 curl 在无 http_proxy 环境变量时通过 en0 的 connected route 直出,不经 NE 截获。不要把这两个成功当作"网络没问题"的证据。
快速诊断
一条命令确认是否是 TUN 代理问题:
echo "=== 系统代理 ===" && scutil --proxy | grep -E "HTTPEnable|HTTPProxy|HTTPPort"
echo "=== 主网卡 ===" && echo 'show State:/Network/Global/IPv4' | scutil | grep PrimaryInterface
echo "=== 默认路由 ===" && netstat -rn -f inet | awk '$1=="default"'
echo "=== 谁在监听代理端口 ==="
PORT=$(scutil --proxy | awk '/HTTPPort/{print $3}')
lsof -nP -iTCP:$PORT -sTCP:LISTEN红旗信号:PrimaryInterface 是 utun8(任意 utunN),lsof 显示是 PacketTunnel / mihomo / sing-box 等进程。命中即确诊。
正解
修复必须在代理工具自身的规则里做,networksetup 对 TUN 模式无效。
Clash / Mihomo / Stash——编辑配置 YAML 的 rules: 区块顶部加入:
rules:
- DOMAIN-SUFFIX,internal,DIRECT
- DOMAIN-SUFFIX,lan,DIRECT
- IP-CIDR,192.168.0.0/16,DIRECT,no-resolve
- IP-CIDR,10.0.0.0/8,DIRECT,no-resolve
# ... 其余规则Surge——[Rule] 区块加:
DOMAIN-SUFFIX,internal,DIRECT,no-resolve
IP-CIDR,192.168.0.0/16,DIRECT,no-resolve
no-resolve 是关键——没有它,代理可能会把 IP 字面量拿去公网 DNS 解析(是的,真的会),然后超时。
如果加完规则仍不生效,备用方案按优先级:
- 直接用 IP 访问(
http://192.168.x.x/),IP-CIDR 规则优先于 DNS 步骤,最可靠 - 用 Firefox,在其自身网络设置里配「不使用代理」的 CIDR,Firefox 在应用层就把流量走 en0,不进 utun
- 关闭 TUN 模式,切回 HTTP/SOCKS 代理模式,此时
networksetupbypass 列表才重新生效
别尝试 kill PacketTunnel 进程——NetworkExtension 框架会在几秒内把它重启。
验证
修完后,在局域网服务器上查访问日志,确认来源 IP 是你 Mac 的 en0 真实 IP(如 192.168.1.50),而不是 utun 隧道网关地址(通常是 198.18.x.x)。真实 IP 出现 = 流量已走 en0,修复生效。
一句话外卖
在 macOS 上,curl 通了不代表浏览器能通——TUN 代理在内核路由之前截获 GUI 流量,
networksetupbypass 列表对它毫无影响,唯一出路是在代理 App 自己的规则里放行局域网 CIDR。