今天在 macOS 上踩了一整天 Clash Verge 的坑。核心现象不是浏览器打不开 GitHub,而是终端里的 git 命令连不上远程仓库:开了虚拟网卡(TUN)模式之后,git fetch / git pull 直接失败;关了 TUN、走系统代理又正常。中间还被 Fake-IP、DNS 覆写、Git SSH/HTTPS 三层误导了一圈。把过程记下来,避免下次再绕。

现象

最先撞上的是终端 git:

Connection closed by 198.18.0.6 port 443
fatal: Could not read from remote repository.

为了对照,再用 curl 测了一下裸访问:

* Connected to github.com (198.18.0.4) port 443
* LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to github.com:443
curl: (35) LibreSSL SSL_connect: SSL_ERROR_SYSCALL ...

关键信号是目标 IP 落在 198.18.0.0/16。这正是 Clash Fake-IP 段,说明 DNS 其实已经被劫持了;真正坏掉的是后面那一段“虚拟握手成功、真实转发失败”。所以问题看起来像“git 终端命令失效”,根子却在 TUN 数据面。

误判一:以为是 Git 没配代理

第一反应很容易是:

DNS 覆写对 Git 不生效,是因为没给 Git 配代理。

这不对。DNS 覆写 / TUN 接管和“有没有给 Git 配 http.proxy”是两层事:

  • TUN 真正生效时,Git、curl、浏览器都不需要各自配代理,流量在系统路由层就被接管。
  • Git 的 http.proxy / https.proxy 只对 HTTPS remote 生效。
  • 如果 remote 是 git@github.com:...,走的是 SSH,这两个配置项完全读不到。

所以“给 Git 配了代理仍然失败”,并不证明代理坏了,更可能是:

  1. remote 其实是 SSH;
  2. 或者 TUN 根本没真正接管流量。

误判二:看到 Fake-IP 就以为 DNS 坏了

DNS 配置大致是这样:

enhanced-mode: fake-ip
fake-ip-range: 198.18.0.1/16

于是 nslookup github.com / curl -v 都会看到:

github.com -> 198.18.0.4

很多人会立刻觉得“DNS 解析错了”。在 Fake-IP 模式下,这恰恰是正常表现。预期链路是:

curl
  -> 连 198.18.0.4
  -> Clash 查 Fake-IP 映射表
  -> 还原真实 GitHub IP
  -> 走代理节点转发

真正异常的是:连接被直接打到 198.18.0.4:443,Clash 没有把这个 Fake-IP 连接接管并转发出去。也就是说,DNS 劫持成功了,数据面失败了

误判三:给 Git 配了 HTTP 代理,却在测 SSH remote

当时执行了:

git config --global http.proxy  http://127.0.0.1:7897
git config --global https.proxy http://127.0.0.1:7897
git fetch

仍然报:

Connection closed by 198.18.0.6 port 443

这句 Connection closed by <ip> port <port>SSH 客户端 的报错格式,不是 HTTP/curl 的。先确认 remote:

git remote -v

如果是 git@github.com:xxx/xxx.git,那刚才配的 HTTP 代理天然无效。

两条修法:

方案 A:改成 HTTPS remote

git remote set-url origin https://github.com/<user>/<repo>.git
git fetch

这样就能吃到 http.proxy。推送时用 GitHub PAT,不要用账号密码。

方案 B:保留 SSH,给 SSH 单独走代理

Clash 混合端口同时支持 HTTP 和 SOCKS5,可用系统自带 nc

Host github.com
    HostName github.com
    User git
    ProxyCommand nc -X 5 -x 127.0.0.1:7897 %h %p

如果本来就有 ssh.github.com + Port 443 这类国内常见绕行配置,把 ProxyCommand 加进现有 Host 块即可。

验证:

ssh -T git@github.com
git ls-remote https://github.com/git/git.git

第二条专门验证 HTTPS 代理是否通,不依赖当前仓库 remote。

真正的分水岭:TUN 虚拟栈 vs 系统代理

TUN 开着时,不带代理的 curl:

curl -v https://github.com

会复现同样的 TLS 失败。原因是:

  1. curl 发起连接;
  2. 系统路由把 Fake-IP 流量导向 TUN 设备;
  3. Clash / mihomo 的虚拟 TCP/IP 栈先在本地把 TCP 握手“应答”掉;
  4. 于是你看到 Connected to github.com port 443——这只是连上了虚拟栈,不是连上了真实 GitHub;
  5. 随后 TLS Client Hello 需要走“真实节点转发 + 写回”链路;
  6. macOS 上这一层一旦出问题,表现永远是:握手秒成功,一发数据就断

系统代理模式则完全不同:进程直接连 127.0.0.1:7897 的 loopback socket,不经过虚拟网卡,自然碰不到这层坑。

一个很干净的对照实验:

# TUN 开着,但显式走本地代理端口
curl -x http://127.0.0.1:7897 https://github.com

如果这条能通,而裸 curl https://github.com 不通,就可以基本坐实:问题在 TUN 数据面,不在节点、不在订阅、也不在 DNS 名单本身。

根因组合:GVisor + Fake-IP + DNS 劫持过宽

这次配置里还有两个容易放大问题的点。

1. DNS 劫持写成 any:53

过宽的劫持会把所有 DNS 请求都塞进 Clash:

系统 DNS
  -> TUN
  -> Clash DNS
  -> Fake-IP
  -> 198.18.x.x

建议先改成更保守的:

127.0.0.1:53

或者直接关掉 DNS 劫持做对比测试。

2. TUN 堆栈用了 GVisor

在 macOS 上,GVisor 对 TLS / Git / HTTP2 一类流量偶尔会有兼容问题。优先改成:

TUN 堆栈: Mixed
自动设置全局路由: 开
严格路由: 开
自动选择流量出口接口: 开
DNS 劫持: 127.0.0.1:53

改完后重启 Clash,并清一次系统 DNS 缓存:

sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder

再测:

curl -v https://github.com

如果 Fake-IP 映射和转发都恢复正常,仍然可能看到 198.18.x.x,但 TLS 会继续走下去;如果已经切到非 Fake-IP / 劫持关闭,则更常见的是看到真实公网 IP。

排查顺序建议

以后再遇到“开了 Clash,终端里 git fetch / git pull 连不上远程仓库”,按这个顺序更快:

  1. 看 TUN / 服务模式有没有真正起来,别只看开关亮着。
  2. /etc/hosts 有没有写死 github.com
  3. nslookup github.com:落到 198.18.x.x 说明 Fake-IP 生效;落到公网 IP 说明请求没进 Clash DNS。
  4. 对照实验:
  • curl https://github.com
  • curl -x http://127.0.0.1:7897 https://github.com
  1. 确认 Git remote 是 HTTPS 还是 SSH,别给 SSH remote 配 HTTP 代理还以为代理坏了。
  2. TUN 堆栈从 GVisor 换 Mixed,DNS 劫持从 any:53 收窄到 127.0.0.1:53
  3. 实在不稳,先关 TUN,改用系统代理 + 必要工具的显式代理配置。

一句话结论

今天真正的坑不是“DNS 没对 Git 生效”,也不是“Git 没配代理”,而是:

终端里的 git 命令被 TUN 数据面卡住了:Fake-IP 已经把名字解析进了 Clash,但连接没有被正确转发;再叠加 SSH remote 不吃 HTTP 代理,以及 GVisor / 过宽 DNS 劫持的兼容问题,就会表现为握手成功、TLS 立刻断。

先用 curl -x 对照实验坐实 TUN 问题,再决定是换 Mixed、收窄 DNS 劫持,还是干脆关掉 TUN。Git 侧则先分清 HTTPS / SSH,别把两套代理模型搅在一起。