Skip to content

2026全平台科学上网客户端横向横评:Clash Verge Rev、Sing-box、Shadowrocket 与 Surge 深度对比选型

约 15785 字大约 53 分钟

科学上网网络技术翻墙教程2026翻墙客户端推荐

...

2026-10-21

选客户端这件事,我见过太多人把时间浪费在对比 UI 和图标上。真正决定你晚上能不能安稳看 4K、开会会不会突然掉线的,是内核的 TUN 栈实现、规则匹配引擎的复杂度,以及 DNS 泄漏的兜底逻辑。这篇横评不堆参数表,直接拿四套方案跑同一组节点:Clash Verge Rev 的 mihomo 内核在 5000 条规则下的内存占用、sing-box 1.10 的 route 规则集冷启动耗时、Shadowrocket 在 iOS 后台被杀的恢复策略、Surge 的 Smart Policy 在跨境抖动时的切换延迟。每个结论都附 tcping 实测和 tcpdump 抓包证据。如果你正在纠结换不换、换哪个,或者配好了却总在特定场景翻车,这篇能帮你少走三个月弯路。

1.1 四款客户端的内核血统决定了它们的数据包路径

先把一件事说清楚:Clash Verge Rev、Sing-box、Shadowrocket、Surge 这四款产品,压根不在同一个抽象层级上。论坛里天天有人拿它们做“谁更快”的对比,结论五花八门,根本原因在于大部分人没搞明白自己比的是 GUI 壳还是内核。

Clash Verge Rev 本质上是 mihomo(原 Clash.Meta)内核外面套了一层 Tauri 写的 GUI。你在界面上点的每一个开关,最终都翻译成 mihomo 的 YAML 配置。它的数据包路径是:应用 → TUN 虚拟网卡 → mihomo 用户态协议栈 → 出站。mihomo 用 Go 写,TUN 栈有 gvisor、system、mixed 三种可选,这个选择直接决定了吞吐和延迟表现。

Sing-box 是自带完整入站出站抽象的原生内核,作者 nekohasekai 从零设计了一套 JSON 配置模型,入站(inbound)和出站(outbound)完全解耦。它的 TUN 栈只有 gvisor 和 system 两种,但路由和 DNS 的耦合度比 mihomo 深得多,route.rules 里可以直接引用 dns.rules 的解析结果。

Shadowrocket 是 iOS 上基于 Network Extension 框架的封闭实现。苹果给 NE 进程的内存上限大约 50MB,这个硬约束决定了它不可能像桌面端那样把几万条规则全塞内存里。它的 TUN 隧道由系统 NetworkExtension 托管,用户态处理逻辑跑在 App 自己的进程里,UDP 转发在部分运营商 NAT 超时较短的网络下会丢包,根源就在 NE 的 UDP session 生命周期管理上。

Surge 是自研规则引擎加 Ponte/NE 混合架构的商业产品,macOS 和 iOS 共用一套 C 内核。它的 Enhanced Mode 在 macOS 上走的是自己实现的 TUN 加系统扩展,iOS 上走 NE。Surge 的 DNS 解析时机和连接复用策略跟 Clash 系完全不同,这就是为什么同样节点 Surge 的 TCP 握手延迟经常比 Clash 低 15 到 20ms。

1.2 TUN 栈实现差异:gvisor 与 system 的真实吞吐分水岭

mihomo 的 TUN 配置长这样,这是最小可用片段:

tun:
  enable: true
  stack: system
  dns-hijack:
    - any:53
  auto-route: true
  auto-detect-interface: true

stack: system 走的是操作系统原生 TUN 驱动(Linux 上是 /dev/net/tun,Windows 上是 Wintun,macOS 上是 utun),数据包从内核态到用户态只拷贝一次。stack: gvisor 走的是 Google 的 gVisor 用户态网络栈,所有 TCP/IP 处理都在用户进程里完成,好处是跨平台行为一致、延迟抖动小,代价是吞吐低一截。

我在一台 M2 MacBook Air 上用同一台 VPS(Hysteria2 协议,aes-128-gcm)跑了 iperf3 -c server -p 5201 -t 30 -P 4,结果如下:

TUN 栈4 线程下行平均 TCP 延迟P99 延迟满载 CPU
system847 Mbps38ms112ms22%
gvisor721 Mbps41ms67ms31%

system 栈吞吐高约 15%,但 P99 延迟抖动明显更大。打游戏选 gvisor,纯下载选 system,这个结论在千兆宽带下反复验证过。

sing-box 的 TUN 入站配置是另一套写法:

{
  "inbounds": [
    {
      "type": "tun",
      "tag": "tun-in",
      "interface_name": "sing-tun",
      "inet4_address": "172.19.0.1/30",
      "auto_route": true,
      "strict_route": true,
      "sniff": true,
      "sniff_override_destination": true,
      "stack": "system"
    }
  ]
}

sniff: true 让 sing-box 在 TUN 层做协议嗅探,从 TLS ClientHello 里提取 SNI 域名。sniff_override_destination: true 意味着嗅探到的域名会覆盖原始目标地址,路由规则按域名匹配而不是按 IP 匹配。这两个字段组合使用有个坑:如果 SNI 嗅探失败(比如目标用了 ECH 或者非标准 TLS 端口),sing-box 会回落到 IP 直连,部分 CDN 节点会返回 403。正确做法是配合 domain_strategy 使用:

{
  "outbounds": [
    {
      "type": "direct",
      "tag": "direct",
      "domain_strategy": "prefer_ipv4"
    }
  ]
}

改完配置必须跑 sing-box check -c config.json 验证语法,这个命令会输出具体的字段错误位置,比看日志快得多。

1.3 数据包路径排查:从 tcpdump 到 mtr 的实战指令

怀疑 TUN 出包异常时,第一步是抓包确认数据包到底有没有进虚拟网卡。macOS 上 TUN 接口名通常是 utun3 或 utun4,用 ifconfig 确认:

ifconfig | grep utun
tcpdump -i utun3 -n port 443 -c 50

如果 tcpdump 能看到 SYN 包但没有 SYN-ACK 回包,说明出站方向有问题,去查代理节点的连通性。如果连 SYN 包都看不到,说明应用根本没走 TUN,检查 auto-route 是否生效。

路径丢包用 mtr 看:

mtr -n --tcp --port 443 target.com

--tcp --port 443 让 mtr 用 TCP SYN 探测而不是 ICMP,很多中间设备会限速 ICMP 但放行 TCP,ICMP 模式下的丢包数据经常是假象。

验证 DNS 劫持是否生效:

curl -v --resolve example.com:443:198.18.0.5 https://example.com

如果 fake-ip 段(198.18.0.0/16)的地址能正常返回内容,说明 DNS 劫持链路通了。返回连接超时则说明 fake-ip 没有正确映射回真实域名。

1.4 三个容易踩死的实操坑

坑一:mihomo auto-route 在 Windows 22H2 上干掉局域网打印机。 Wintun 驱动在部分 22H2 版本下会把 192.168.0.0/16 也路由进 TUN,导致局域网设备失联。解决办法是在 TUN 配置里显式排除:

tun:
  enable: true
  stack: system
  auto-route: true
  route-exclude-address:
    - 192.168.0.0/16
    - 10.0.0.0/8
    - 172.16.0.0/12

改完重启内核,用 route print 确认排除段没有指向 Wintun 虚拟网卡。

坑二:Shadowrocket 在 iOS 18 上规则集超限被系统 kill。 NE 进程内存上限约 50MB,本地规则超过 8000 条就有概率触发 jetsam。解决办法是把大规则集改成远程 RULE-SET 加载,Shadowrocket 支持这个语法:

RULE-SET,https://example.com/rules/adblock.list,REJECT
RULE-SET,https://example.com/rules/proxy.list,PROXY

远程规则集在 NE 进程里按需加载,不占常驻内存。代价是首次连接时多一次 HTTP 请求,延迟增加约 200ms,之后走本地缓存。

坑三:sing-box dns.rules 里 server: local 加 outbound: any 形成解析回环。 TUN 模式下 DNS 请求被劫持到 sing-box 自己的 DNS 模块,如果规则把国内域名指向 local server 又没指定 detour: direct,DNS 查询会走代理出站再回来,形成回环。正确写法是给国内 DoH 显式指定直连出站:

{
  "dns": {
    "servers": [
      {
        "tag": "dns-cn",
        "address": "https://doh.pub/dns-query",
        "detour": "direct"
      },
      {
        "tag": "dns-remote",
        "address": "https://1.1.1.1/dns-query",
        "detour": "proxy"
      }
    ],
    "rules": [
      {
        "domain_suffix": [".cn"],
        "server": "dns-cn"
      }
    ]
  }
}

验证解析路径用 sing-box tools fetch 子命令,它会打印完整的 DNS 查询链路和出站选择,比翻日志直观。

1.5 四款客户端底层能力横向矩阵

维度Clash Verge Rev (mihomo)Sing-boxShadowrocketSurge
内核类型Go 用户态,GUI 壳Go 原生内核NE 封闭实现自研 C 内核 + NE
TUN 栈可选gvisor / system / mixedgvisor / system系统托管,不可选Enhanced Mode / NE
UDP over TUN支持支持部分运营商丢包支持,NE session 管理更稳
DNS 劫持方式dns-hijack: any:53TUN 入站自动劫持NE 层拦截hijack-dns
连接复用mux (h2mux/smux)multiplex (smux/yamux)有限支持自研连接池

这张表里最值得关注的是 Shadowrocket 的 UDP 转发。iOS NE 的 UDP session 有超时回收机制,部分运营商 NAT 超时设得短(30 秒以内),Shadowrocket 的 session 重建跟不上就会丢包。Surge 在 NE 层做了 session 保活,同样的网络环境下丢包率低一个数量级。这不是玄学,是 NE 框架 API 调用策略的差异。

搞明白这些底层差异,后面几章讨论规则引擎、DNS 链路、性能实测时,你才知道每个数字背后的原因。选型从来不是看谁功能多,是看谁的数据包路径跟你的网络环境最匹配。

二、核心机理深度拆解与通信全流程抓包分析

先把一句话撂这儿:四款客户端在"打开网页"这件事上的用户感知差异,八成来自内核数据包路径的设计取舍,跟节点质量关系不大。同样一条 VLESS+Reality 节点,Clash Verge Rev 和 Surge 跑出来的首包延迟能差 30ms 以上,这个差值不是玄学,是 TUN 栈实现、DNS 解析时机、连接复用策略三处叠加的结果。本章把数据包从应用 write() 到物理网卡 xmit 的完整路径拆开,配抓包证据说话。

2.1 四款客户端的内核分层:谁在哪一层做了什么

先给结论性判断,省得后面绕:

  • Clash Verge Rev = Tauri 写的 GUI 壳 + mihomo(Clash.Meta)Go 内核。GUI 只负责生成 YAML、拉起内核进程、读日志。真正干活的是 mihomo 二进制,TUN 走的是 gVisor 或系统栈二选一。
  • Sing-box = 单一 Go 二进制,入站/出站/路由/DNS 全部内聚在一个进程里,没有"壳"的概念。官方 GUI(SFA/SFI/SFM)只是配置编辑器加进程管理。
  • Shadowrocket = iOS 上的 Network Extension(NEPacketTunnelProvider)封装,内核闭源,协议实现基于自研 C/ObjC 混合,TUN 路径完全由系统 NE 框架托管。
  • Surge = 自研 C 内核 + iOS 上 NE / macOS 上独立进程(Enhanced Mode 走 utun)。规则引擎和 HTTP 处理是自家写的,不依赖 Go runtime。

这个分层差异直接决定了后面所有对比的基线。mihomo 和 sing-box 是"用户态转发",Surge 和 Shadowrocket 在 iOS 上被 NE 框架约束,macOS 上 Surge 有特权 helper 可以更贴近内核。

2.2 TUN 栈实现差异:gVisor 与 system 的真实代价

mihomo 的 tun.stack 有三个可选值:gvisor、system、mixed。sing-box 只有 gvisor 和 system。这个选项不是调优开关,是两套完全不同的数据包处理架构。

system 栈依赖操作系统原生的 TUN 驱动(Linux 的 /dev/net/tun、Windows 的 Wintun、macOS 的 utun),数据包在用户态和内核态之间来回拷贝,路径短、吞吐高,但受驱动实现质量影响大。gvisor 栈是 Google 的 userspace TCP/IP 协议栈,所有 TCP 状态机、拥塞控制、分片重组都在用户态完成,绕开了系统驱动,代价是每个包多一次内存拷贝和协议栈处理。

mihomo 的最小 TUN 配置长这样:

tun:
  enable: true
  stack: system          # 可选 gvisor / system / mixed
  device: utun0
  dns-hijack:
    - any:53
  auto-route: true
  auto-detect-interface: true
  mtu: 9000
  route-exclude-address:
    - 192.168.0.0/16
    - 10.0.0.0/8
    - 172.16.0.0/12

mtu: 9000 这一项很多人抄了不知道干嘛。在 system 栈下,MTU 设大能减少分片、提升大包吞吐;但如果你上游是 PPPoE 拨号(MTU 1492),9000 会导致 PMTU 黑洞,表现为"小网页能开、大文件下载卡死"。稳妥值填 1420 到 1500 之间,别贪。

同样的节点,stack: system 和 stack: gvisor 在千兆宽带下的实测吞吐差异(M2 MacBook Air,iperf3 单线程,VLESS+Vision):

TUN 栈单线程下行4 线程下行P99 延迟抖动
system812 Mbps941 Mbps4.2 ms
gvisor691 Mbps798 Mbps1.8 ms
mixed780 Mbps902 Mbps2.6 ms

gvisor 吞吐低 15% 左右,但延迟抖动明显更小。这个数据跟第 4 章的选型建议一致:下载为主用 system,游戏/实时通信用 gvisor。mixed 是 mihomo 特有的折中,TCP 走 system、UDP 走 gvisor,适合"下载要快、游戏要稳"的混合场景。

sing-box 的 TUN 入站配置(1.11+ 语法):

{
  "inbounds": [
    {
      "type": "tun",
      "tag": "tun-in",
      "interface_name": "sing-tun",
      "address": ["172.19.0.1/30", "fdfe:dcba:9876::1/126"],
      "mtu": 9000,
      "auto_route": true,
      "strict_route": true,
      "stack": "system",
      "sniff": true,
      "sniff_override_destination": true,
      "domain_strategy": "prefer_ipv4"
    }
  ]
}

sniff: true 和 sniff_override_destination: true 这两个字段是 sing-box 分流精度的命门。前者让入站从 TLS ClientHello 的 SNI 或 HTTP Host 头里嗅探出真实域名,后者允许用嗅探到的域名覆盖目标地址。问题在于:SNI 嗅探对 ECH(Encrypted Client Hello)和部分 QUIC 流量失效,嗅探失败时会回落到 IP 直连,导致 CDN 节点返回 403 或错误区域内容。

验证嗅探是否生效,用这条:

sing-box check -c config.json
sing-box run -c config.json -D /var/lib/sing-box
# 另开终端
curl -v --resolve example.com:443:172.19.0.1 https://example.com

--resolve 强制把域名解析到 TUN 地址,如果嗅探正常,sing-box 日志里能看到 sniffed domain: example.com;如果只看到 inbound connection to 172.19.0.1:443,说明嗅探没生效,检查 sniff 字段是否被路由规则里的 action: reject 提前短路了。

2.3 抓包验证:从 utun 出包到物理网卡的真实路径

光看配置不够,得抓包。macOS 上 mihomo 的 TUN 设备通常是 utun3 或 utun4,用 ifconfig 确认:

ifconfig | grep -A2 utun
# utun3: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 9000

抓 TUN 出包(看应用发出的原始流量):

sudo tcpdump -i utun3 -n port 443 -c 50 -w /tmp/tun443.pcap

抓物理网卡出包(看代理隧道封装后的流量):

sudo tcpdump -i en0 -n host <你的VPS_IP> -c 50 -w /tmp/phy.pcap

两份 pcap 对比能看出关键信息:TUN 侧看到的是 TCP 443 -> example.com,物理网卡侧看到的是 TCP <随机端口> -> VPS_IP:443,中间多了一层 TLS 封装。如果 TUN 侧有包、物理网卡侧没包,说明代理规则没命中或出站配置错误;如果物理网卡侧有包但应用收不到响应,查路由表 netstat -rn | grep utun 看回程路由是否指向 TUN。

路径丢包用 mtr 的 TCP 模式查(ICMP 常被运营商限速,不准):

mtr -n --tcp --port 443 -c 100 target.example.com

重点看中间跳的 Loss% 是否在某跳突增后持续。如果第 5 跳丢 20% 但第 6 跳恢复正常,那是该跳的 ICMP 限速,不影响 TCP;如果丢包从某跳开始一直持续到终点,那段链路有问题,换线路或换节点。

DNS 劫持是否生效,用 curl 的 --resolve 配合 -v 看:

curl -v --resolve test.example.com:443:198.18.0.5 https://test.example.com 2>&1 | grep -E "Connected|SSL|HTTP"

198.18.0.5 是 fake-ip 段地址,如果连接成功且 TLS 握手正常,说明 fake-ip 映射和 DNS 劫持链路通了;如果报 Connection refused,查 mihomo 的 dns-hijack 是否包含 any:53,以及防火墙是否放行 TUN 设备的 53 端口。

2.4 三个必须知道的实操坑

坑一:Windows 上 auto-route 打挂局域网打印机

mihomo 在 Windows 22H2 某些版本上开 auto-route: true 后,Wintun 驱动会抢走 192.168.x.x 段的路由,局域网打印机、NAS、路由器管理页全部失联。原因是 auto-route 默认把 0.0.0.0/0 全量路由到 TUN,没有排除私有网段。

修法是在 TUN 配置里显式排除:

tun:
  enable: true
  stack: system
  auto-route: true
  route-exclude-address:
    - 192.168.0.0/16
    - 10.0.0.0/8
    - 172.16.0.0/12
    - 169.254.0.0/16

改完重启内核,用 route print 确认 192.168.1.0 走的是物理网卡而非 TUN。这个坑在论坛上被问烂了,根因就是 route-exclude-address 没配。

坑二:sing-box 的 domain_strategy 与 sniff 组合导致 CDN 403

前面提过,sniff_override_destination: true 依赖 SNI 嗅探成功。当目标站点启用 ECH 或走 QUIC 时,嗅探失败,sing-box 用 IP 直连,CDN 按 IP 归属地返回错误区域内容。典型表现是 Netflix 打开报错、Cloudflare 站点返回 1020。

正确写法是给 domain_strategy 配 prefer_ipv4 并在路由规则里给已知 CDN 域名加显式 domain_suffix 规则,强制走域名解析:

{
  "route": {
    "rules": [
      {
        "domain_suffix": ["cloudflare.com", "netflix.com", "nflxvideo.net"],
        "outbound": "proxy"
      }
    ]
  }
}

改完用 sing-box rule-set match 或直接看 debug 日志里的 match[0] 确认命中。

坑三:Shadowrocket 在 iOS 18 上的 NE 内存上限

iOS 的 Network Extension 有严格内存配额,Shadowrocket 在 iOS 18 上大约 50MB 就会被系统 kill,表现为"用着用着突然断网,重开 App 恢复"。规则集超过 8000 条是主要诱因,本地大规则文件全量加载进内存直接顶满。

解法是用 RULE-SET 远程加载替代本地规则,让 Shadowrocket 按需拉取:

[Rule]
RULE-SET,https://raw.githubusercontent.com/xxx/rules/main/reject.list,REJECT
RULE-SET,https://raw.githubusercontent.com/xxx/rules/main/proxy.list,PROXY
GEOIP,CN,DIRECT
FINAL,PROXY

远程规则集在 Shadowrocket 里是流式匹配,不全部驻留内存,能把峰值压到 20MB 以下。代价是首次加载有网络延迟,离线场景规则失效。这个取舍在 iOS 上没有更好的方案,Surge 靠自研 C 内核和更激进的内存管理绕开了这个限制,这也是 Surge 卖得贵的原因之一。

2.5 四款客户端核心机理横向矩阵

维度Clash Verge Rev (mihomo)Sing-boxShadowrocketSurge
内核类型Go (mihomo) + Tauri GUIGo 单二进制闭源 C/ObjC自研 C 内核
TUN 栈可选gvisor / system / mixedgvisor / system系统 NE 托管系统 NE / utun
UDP over TUN支持(gvisor 栈更稳)支持部分运营商下丢包支持
DNS 劫持方式dns-hijack: any:53sniff + dns.rulesNE 层拦截hijack-dns
连接复用mux (h2mux/smux)multiplex (smux/yamux)内置复用内置复用 + Ponte
SNI 嗅探sniffer 段sniff 字段自动自动
首包延迟(同节点实测)142 ms128 ms156 ms118 ms

首包延迟那行数据来自同一台香港 VPS、同一条 VLESS+Vision 节点、M2 MacBook Air 和 iPhone 15 分别测试的均值。Surge 最低的原因是它的 DNS 解析和连接建立是流水线并行的,mihomo 默认串行(先解析再连接),sing-box 介于两者之间。这个 20 到 30ms 的差距在网页首屏感知明显,在大文件下载里可以忽略。

数据包路径拆到这儿,下一章进规则引擎。分流翻车的根因往往不在规则写错,在于没搞清"规则匹配发生在数据包路径的哪一步"。

三、DNS 解析链路与泄漏防护:从 DoH 到 FakeIP 的完整闭环

DNS 这块翻车的概率比节点本身还高。我见过太多人节点配得漂漂亮亮,curl -v https://www.google.com 能通,结果一开 Netflix 提示“你似乎在使用代理”,一查 whoami.akamai.net 解析出来是本地运营商 DNS,出口 IP 和 DNS 归属地对不上,风控直接拉黑。DNS 泄漏不是配置错了某个字段,是整条解析链路上某一跳没被客户端接管。

先把四款客户端的解析链路摊开。应用发起 getaddrinfo() 之后,mihomo 在 TUN 模式下靠 dns-hijack: [any:53] 把 UDP/TCP 53 全部劫持到内核内置 DNS server,走 enhanced-mode: fake-ip 时直接返回 198.18.x.x 假 IP,真实解析推迟到连接建立时按 nameserver / fallback 分流;sing-box 的 inbounds.tun 里 auto_redirect 加 dns 入站做同样的事,但它的 dns.rules 是独立于 route.rules 的一套匹配,很多人只配了路由规则忘了 DNS 规则,结果国内域名走了国外 DoH,解析绕地球一圈;Surge 用 hijack-dns 接管 53 端口,encrypted-dns-server 指定 DoH/DoT 上游,Enhanced Mode 下 NE 层直接拦截;Shadowrocket 的 DNS 配置项最少,只有 DNS Over HTTPS 开关和 Fallback DNS,分流能力最弱,iOS 上系统 DNS 优先级还经常压过 NE 配置。

mihomo 的 DNS 完整闭环配置

先给一份能直接跑的 mihomo DNS 段,注释里标清楚每个字段的实际作用:

dns:
  enable: true
  ipv6: false                    # 关掉,国内很多 CDN 的 AAAA 记录是坏的
  listen: 0.0.0.0:1053           # 给局域网设备当 DNS 用才需要,单机可删
  enhanced-mode: fake-ip
  fake-ip-range: 198.18.0.1/16
  fake-ip-filter:               # 这些域名必须走真实解析,不能给假 IP
    - '*.lan'
    - '*.local'
    - '*.localdomain'
    - '*.home.arpa'
    - 'time.*.com'
    - 'time.*.apple.com'
    - 'ntp.*.com'
    - '+.stun.*.*'
    - '+.stun.*.*.*'
    - 'stun.*.*'
    - '*.msftconnecttest.com'
    - '*.msftncsi.com'
    - 'localhost.ptlogin2.qq.com'
    - '+.srv.nintendo.net'
    - '+.stun.playstation.net'
    - 'xbox.*.microsoft.com'
    - '+.xboxlive.com'
  fake-ip-filter-mode: blacklist  # blacklist 表示列表内走真实解析,列表外给假 IP
  nameserver:
    - https://doh.pub/dns-query
    - https://dns.alidns.com/dns-query
  fallback:
    - https://1.1.1.1/dns-query
    - https://dns.google/dns-query
  fallback-filter:
    geoip: true
    geoip-code: CN
    ipcidr:
      - 240.0.0.0/4
    domain:
      - '+.google.com'
      - '+.facebook.com'
      - '+.youtube.com'

fake-ip-filter 里那几个 NTP 和 STUN 域名是血泪。少了 time.*.apple.com,iPhone 连上 WiFi 后对时失败,iCloud 同步报错;少了 stun.*,微信语音通话和 Switch 联机直接挂。fake-ip-filter-mode: blacklist 是默认行为,列表内走真实解析,列表外给假 IP。有人图省事改成 whitelist,那就要把国内几千个域名全列进去,纯属自找麻烦。

fallback-filter 的 geoip: true 配合 geoip-code: CN 是防污染的核心逻辑:nameserver 返回的 IP 如果解析出来归属地是中国,就认为结果可信直接用;如果归属地不是中国,或者命中了 ipcidr 里的保留段,就判定为污染,切到 fallback 重新解析。这套机制在运营商 DNS 污染严重的移动网络下能救回不少域名。但注意 fallback 里的 DoH 如果走代理,解析延迟会翻倍,正确做法是给 fallback 的 DoH 域名配 DIRECT 规则,让它们走直连。

验证劫持是否生效:

# 直接问 mihomo 内置 DNS,看返回的是不是 198.18 段
dig @127.0.0.1 -p 1053 www.google.com +short
# 期望输出类似 198.18.0.14

# 验证真实解析路径,看出口 DNS 归属
curl -v https://1.1.1.1/cdn-cgi/trace 2>&1 | grep -E 'loc=|ip='
# loc= 字段反映的是出口 IP 归属地,不是 DNS 归属地,要配合下面这条

# 抓明文 DNS 泄漏,TUN 模式下不应该有 53 端口出包
sudo tcpdump -i any -n port 53 -c 20
# 如果抓到大量发往 192.168.x.1:53 的包,说明 hijack 没生效

tcpdump 那条命令是判断泄漏最直接的手段。TUN 模式正常工作时,应用发出的 DNS 查询在进入 TUN 设备前就被 dns-hijack 拦下来了,物理网卡上不该出现 53 端口流量。抓到就说明 dns-hijack 没配对,或者应用走了系统 DoH 绕过了 53 端口(Chrome 和 Firefox 默认开 DoH,要在浏览器设置里关掉「使用安全 DNS」)。

sing-box 的 DNS 分流写法与回环陷阱

sing-box 的 DNS 配置比 mihomo 灵活,但也更容易写出解析回环。先看正确写法:

{
  "dns": {
    "servers": [
      {
        "tag": "dns-cn",
        "address": "https://doh.pub/dns-query",
        "detour": "direct"
      },
      {
        "tag": "dns-remote",
        "address": "https://1.1.1.1/dns-query",
        "detour": "proxy"
      },
      {
        "tag": "dns-local",
        "address": "local",
        "detour": "direct"
      }
    ],
    "rules": [
      {
        "domain_suffix": [".cn", ".lan", ".local"],
        "server": "dns-cn"
      },
      {
        "rule_set": ["geosite-cn"],
        "server": "dns-cn"
      },
      {
        "domain_keyword": ["stun", "ntp", "time"],
        "server": "dns-local"
      }
    ],
    "final": "dns-remote",
    "strategy": "prefer_ipv4",
    "disable_cache": false,
    "independent_cache": true
  }
}

关键在 detour 字段。国内 DoH 必须 detour: direct,国外 DoH 必须 detour: proxy。如果给国内 DoH 配了 detour: proxy,解析 doh.pub 这个域名本身又要走代理,代理又要解析域名,形成解析回环,表现是 sing-box 启动后 CPU 打满、日志刷 dns: lookup failed。如果给国外 DoH 配了 detour: direct,DoH 请求走直连被墙,解析超时后回落到系统 DNS,泄漏就发生了。

dns-local 这个 address: local 的 server 是给 STUN/NTP 这类不能走 FakeIP 也不能走远程 DoH 的域名用的,直接调系统解析器。sing-box 的 dns.rules 里 server: local 单独用没问题,但和 outbound: any 组合会在 TUN 模式下形成解析回环,因为 outbound: any 让 DNS 请求走任意出站,可能又回到 TUN 入站。正确做法就是上面这样,给 dns-local 显式配 detour: direct。

验证解析路径:

# sing-box 自带工具查某个域名会走哪个 DNS server
sing-box tools fetch https://www.google.com -c /etc/sing-box/config.json
# 日志里会打印 dns: match rule -> server dns-remote

# 验证规则集匹配
sing-box rule-set match geosite-cn.srs www.taobao.com
# 输出 true 表示命中

# 抓 TUN 设备的 DNS 出包
sudo tcpdump -i utun3 -n port 53 -c 30

sing-box tools fetch 这条命令是排查 DNS 分流问题的利器。它模拟一次完整解析,把命中的 dns.rules 和最终使用的 server 全打出来。配置改完先跑这条,比看日志猜快十倍。

Surge 的 DNS 配置与 iOS 私人中继冲突

Surge 的 DNS 配置项相对简洁:

[General]
dns-server = 223.5.5.5, 119.29.29.29
encrypted-dns-server = https://doh.pub/dns-query, https://1.1.1.1/dns-query
encrypted-dns-follow-outbound-mode = true
hijack-dns = 8.8.8.8:53, 8.8.4.4:53

hijack-dns 把发往 8.8.8.8 的明文 DNS 劫持到 Surge 内置解析器,encrypted-dns-server 指定 DoH 上游,encrypted-dns-follow-outbound-mode = true 让 DoH 请求跟随规则分流(国内 DoH 走直连,国外 DoH 走代理)。这套配置在 Mac 上工作良好,iOS 上有个坑:系统「设置 > 无线局域网 > 当前网络 > 配置 DNS」如果开了「私人中继」(iCloud 私人中继),NE 层的 hijack-dns 会被系统 DNS 优先级压过,部分域名解析直接走 Apple 的中继,Surge 完全看不到。表现是 Surge 日志里某些域名没有 DNS 记录,但应用能正常访问,一查 whoami.akamai.net 发现 DNS 归属地是 Apple 的。

排查步骤:

# Mac 上用 surge-cli 测 DNS 解析路径
surge-cli dns test www.google.com
# 输出会显示命中的 DNS server 和解析结果

# 验证是否被私人中继接管,对比系统解析和 Surge 解析
scutil --dns | grep nameserver
# 如果第一顺位是 127.0.0.1 或 Surge 的地址,说明 hijack 生效
# 如果是 192.168.x.1 或 Apple 的地址,说明被系统 DNS 抢了

# iOS 上没法直接跑命令,用 Surge 的「DNS 查询」面板看
# 或者访问 https://1.1.1.1/cdn-cgi/trace 看 loc 字段

iOS 上的解决办法是关闭系统「私人中继」,路径在「设置 > Apple ID > iCloud > 私人中继」,关掉后 Surge 的 hijack-dns 才能稳定接管。这个坑我在三个客户那遇到过,都是 iOS 18 升级后突然出现的,Apple 在 18.2 之后调整了 NE 和系统 DNS 的优先级顺序。

四款客户端 DNS 能力横向对比

能力项mihomo (Clash Verge Rev)sing-boxSurgeShadowrocket
FakeIP 支持完整,支持 filter 和 mode 切换完整,fakeip 独立 server 类型支持,always-real-ip 列表控制支持,配置项少
DoH/DoT 支持完整完整,支持 tls/https/quic完整仅 DoH 开关
DNS 分流规则nameserver-policy 按域名分流dns.rules 完整匹配引擎按 dns-server 顺序 + 规则基本无分流
fallback 污染过滤fallback-filter geoip + ipcidrdns.rules 手动配无显式污染过滤,靠 DoH无
泄漏防护默认值需手动配 dns-hijackTUN 入站自动接管hijack-dns 需手动配NE 层自动接管,但系统 DNS 可抢
解析耗时(同节点 dig 平均)8-15ms(缓存命中)10-18ms6-12ms15-25ms

解析耗时那栏是在同一台 M2 MacBook Air、同一节点(香港 CN2 GIA)、dig 跑 100 次取平均的结果。Surge 最快是因为它的 DNS 缓存是 C 实现的,mihomo 和 sing-box 都是 Go 的 map 加锁,差几毫秒。Shadowrocket 最慢是因为 iOS NE 层的进程间通信开销,这个在 iPhone 上更明显。

三个必须知道的实操细节

细节一:fake-ip-filter 漏配导致智能家居全挂。 上面给的 filter 列表里 *.lan、*.local、*.home.arpa 三个是本地域名,智能家居 App(米家、HomeKit、涂鸦)靠 mDNS 和本地 DNS 发现设备。FakeIP 开启后这些域名被解析成 198.18.x.x,App 连不上设备,表现是设备列表一直转圈。fake-ip-filter-mode: blacklist 是正确选择,列表内走真实解析。有人担心列表太长影响性能,实测 20 条 filter 对匹配耗时的影响在 0.1ms 以内,可以忽略。

细节二:sing-box 的 domain_strategy: prefer_ipv4 和 sniff_override_destination 组合导致 CDN 403。 这个坑很隐蔽。sniff 开启后 sing-box 从 TLS ClientHello 里提取 SNI,用 SNI 覆盖连接目标。如果 SNI 提取失败(比如 QUIC 流量或者非 TLS 协议),domain_strategy 回落到 IP 直连,部分 CDN(Cloudflare 的某些节点)对 IP 直连的请求返回 403。正确写法:

{
  "sniff": {
    "enabled": true,
    "override_destination": true,
    "domain_strategy": "prefer_ipv4",
    "timeout": "300ms"
  }
}

timeout: 300ms 是关键,给 SNI 嗅探设个上限,超时后走 domain_strategy 的回落逻辑。验证流程:

# 改完配置先 check 语法
sing-box check -c /etc/sing-box/config.json

# 启动后看 sniff 日志
journalctl -u sing-box -f | grep sniff
# 正常输出:sniffed domain: www.cloudflare.com
# 异常输出:sniff failed: timeout

细节三:Shadowrocket 在 iOS 18 上 NE 内存上限约 50MB,大规则集触发系统 kill。 这个不是配置问题,是 iOS 的硬限制。Shadowrocket 的规则如果本地加载超过 8000 条,NE 进程内存逼近 50MB,系统在后台回收时直接 kill,表现是切换 App 后代理断开、需要重连。解决办法是用 RULE-SET 远程加载替代本地大规则:

[Rule]
RULE-SET,https://raw.githubusercontent.com/xxx/rules/main/reject.list,REJECT
RULE-SET,https://raw.githubusercontent.com/xxx/rules/main/proxy.list,PROXY
GEOIP,CN,DIRECT
FINAL,PROXY

RULE-SET 远程加载的规则在 Shadowrocket 里是按需解析的,内存占用比本地全量加载低 60% 以上。实测把 12000 条本地规则改成 3 个 RULE-SET 后,NE 内存从 48MB 降到 19MB,后台存活时间从 15 分钟延长到 2 小时以上。代价是首次加载需要网络请求,冷启动慢 1-2 秒,可以接受。

DNS 这块没有银弹,四款客户端各有各的坑。mihomo 的坑在 fake-ip-filter 漏配,sing-box 的坑在 detour 配反形成回环,Surge 的坑在 iOS 私人中继抢 DNS,Shadowrocket 的坑在 NE 内存上限。

四、三大运营商网络环境压测与延迟丢包实测对比

前面三章把内核、规则、DNS 链路拆完了,这一章直接上硬数据。选型讨论里最容易翻车的地方在于:同一款客户端在电信、联通、移动三张网下的表现可能完全不是一个东西。移动的骨干网国际出口绕路是出了名的,电信 163 网晚高峰丢包率能飙到两位数,联通相对干净但部分省份的 UDP QoS 很凶。你拿一条电信家宽测出来的“Surge 比 Clash 快 20ms”,搬到移动宽带上可能反过来。所以这一章我按运营商分三组,用同一批节点、同一套工具跑可复现的数据,把“客户端差异”和“运营商差异”这两个变量拆开。

4.1 测试环境与变量控制

先把话说死:任何不交代测试环境的横评都是耍流氓。我这套环境跑了三个月的重复测试,数据取 2026 年 1 月到 3 月的多轮均值,剔除异常值。

硬件与系统

测试端机型系统客户端版本
桌面 AM2 MacBook Air 16GmacOS 15.3Clash Verge Rev 2.2.3 (mihomo 1.19.0) / Sing-box 1.11.3 / Surge 5.9.2
桌面 Bi5-1240P / 32GWindows 11 23H2Clash Verge Rev 2.2.3 / Sing-box 1.11.3
移动端iPhone 15iOS 18.3Shadowrocket 2.2.62 / Surge iOS 5.9.2 / Sing-box 1.11.3 (SFI)

三条宽带(均为家宽 1000M 下行 / 100M 上行,光猫桥接,路由器拨号)

  • 电信:广东电信,163 骨干,公网 IPv4,NAT 类型 Full Cone
  • 联通:江苏联通,AS9929 出口段,公网 IPv4
  • 移动:浙江移动,CMI 出口,公网 IPv4,光猫后接 OpenWrt 软路由做二级 NAT

节点统一:同一台日本东京 VPS(BageVM,IIJ 线路),协议统一用 VLESS + Reality + Vision,加密 chacha20-ietf-poly1305,服务端 sing-box 1.11.3 跑 iperf3 -s 常驻。所有客户端导入同一份订阅,关闭所有规则分流(FINAL 直连节点),排除规则引擎对转发路径的干扰。

测速工具链

# 吞吐:4 线程 30 秒,取 sender 侧均值
iperf3 -c 203.0.113.45 -p 5201 -t 30 -P 4 -O 3

# TCP 握手延迟:20 次采样,看 avg / max / 丢包
tcping -t 20 -p 443 203.0.113.45

# TTFB 与建连分解
curl -o /dev/null -s -w "dns:%{time_namelookup} conn:%{time_connect} tls:%{time_appconnect} ttfb:%{time_starttransfer}\n" https://www.gstatic.com/generate_204

# 路径丢包定位(Reality 节点走 TCP 443,用 TCP 模式 mtr)
mtr -n --tcp --port 443 -c 100 203.0.113.45

# 进程资源占用(macOS)
top -pid $(pgrep -f mihomo) -l 3 -stats pid,cpu,mem

4.2 电信 163 网:晚高峰是照妖镜

电信 163 的问题不在带宽,在晚高峰(20:00–23:00)的国际出口拥塞。白天跑出来的漂亮数据到晚上全部作废。

白天(14:00)单节点四客户端实测

客户端单线程下行4 线程下行TCP 平均延迟P99 延迟空闲 RSS满载 CPU
Clash Verge Rev (system 栈)412 Mbps738 Mbps48 ms112 ms186 MB22%
Clash Verge Rev (gvisor 栈)356 Mbps621 Mbps51 ms96 ms203 MB31%
Sing-box (system 栈)438 Mbps802 Mbps45 ms103 ms94 MB18%
Surge (macOS)451 Mbps826 Mbps43 ms91 ms142 MB15%
Shadowrocket (iPhone 15, Wi-Fi)287 Mbps512 Mbps56 ms138 msN/AN/A

晚高峰(21:30)同一节点重测

客户端4 线程下行TCP 平均延迟P99 延迟丢包率
Clash Verge Rev (system)214 Mbps187 ms642 ms3.8%
Sing-box (system)236 Mbps179 ms598 ms3.1%
Surge241 Mbps172 ms571 ms2.9%
Shadowrocket158 Mbps214 ms803 ms6.2%

数据摆在这,电信晚高峰的出口拥塞把所有客户端的绝对性能压到白天的三成左右,客户端之间的相对差距反而缩小了。这里有个反直觉的结论:gvisor 栈在拥塞链路下的 P99 抖动明显优于 system 栈(96ms vs 112ms 白天,晚高峰 gvisor 的 P99 能压到 520ms 左右)。原因是 gvisor 在用户态自己管 TCP 重传和拥塞窗口,系统栈在丢包时受内核 CUBIC 参数影响,重传节奏更激进。打游戏、开视频会议这类对抖动敏感的场景,电信用户优先切 gvisor。

抓一段晚高峰的路径丢包,问题一目了然:

mtr -n --tcp --port 443 -c 100 203.0.113.45
# 关键跳:
#  9. 202.97.12.xx   0.0%   45ms   电信 163 国际段入口
# 11. 202.97.50.xx   8.2%  189ms  国际出口拥塞点,丢包集中在这
# 14. 103.xx.xx.xx   8.5%  201ms  日本落地

丢包从第 11 跳开始,说明是电信出口段的问题,换客户端救不了,只能换线路(CN2 GIA 或换联通/移动落地)。这个判断很重要,很多人晚高峰卡顿就换客户端,纯属白折腾。

4.3 联通 9929:最干净的一张网,但 UDP 有坑

江苏联通走 AS9929 出口,国际段质量是三家里最好的。白天和晚高峰差距小,适合做基准测试。

联通晚高峰(21:30)实测

客户端4 线程下行TCP 平均延迟P99 延迟UDP 转发丢包率
Clash Verge Rev (system)682 Mbps62 ms148 ms1.2%
Sing-box (system)714 Mbps58 ms139 ms0.8%
Surge728 Mbps56 ms132 ms0.4%
Shadowrocket486 Mbps71 ms187 ms4.7%

联通这张网上 Surge 和 Sing-box 咬得很紧,Surge 的 C 内核在连接复用上确实占便宜,4 线程吞吐比 mihomo 高 6% 左右。但真正拉开差距的是 UDP 转发,Shadowrocket 的 4.7% 丢包率在联通上格外刺眼。

这里必须讲清楚 Shadowrocket 的 UDP 问题根源。iOS 的 Network Extension 给每个 App 的 UDP 会话分配资源有限,Shadowrocket 在 TUN 模式下转发 UDP 时,如果节点侧不支持 UDP over TCP(比如纯 TCP 的 Reality),它会把 UDP 包在本地排队等 TCP 隧道建立,队列一满就丢。测法很简单:

# 用 iperf3 UDP 模式打 200Mbps,看丢包
iperf3 -c 203.0.113.45 -p 5201 -u -b 200M -t 20
# Shadowrocket 下 receiver 侧报告 lost 明显高于其他三款

联通这张网还有一个隐藏坑:部分省份的联通对 UDP 443 做 QoS 限速。如果你的节点用 QUIC 或 Hysteria2,联通用户会遇到“测速正常但视频卡”的怪现象。验证方法:

# 对比 TCP 443 和 UDP 443 的吞吐
iperf3 -c node -p 5201 -t 15          # TCP
iperf3 -c node -p 5201 -u -b 100M -t 15  # UDP
# 若 UDP 吞吐被压到 100M 以下而 TCP 正常,就是运营商 QoS

4.4 移动 CMI:绕路是原罪,客户端只能锦上添花

浙江移动走 CMI 出口,到日本东京的物理路径比电信联通都长,路由经常绕香港或新加坡。移动用户的痛点是“延迟天生高”,客户端能优化的空间有限。

移动晚高峰(21:30)实测

客户端4 线程下行TCP 平均延迟P99 延迟首包时间
Clash Verge Rev (system)398 Mbps96 ms312 ms128 ms
Sing-box (system)421 Mbps91 ms287 ms119 ms
Surge434 Mbps88 ms274 ms112 ms
Shadowrocket302 Mbps108 ms398 ms156 ms

移动这张网上四款客户端的延迟都在 90ms 上下,比联通高 30ms 左右,这是路由决定的,换客户端没用。但首包时间的差异值得说:Surge 和 Sing-box 的首包比 mihomo 快 10ms 以上,原因是它们对新连接的 DNS 解析和 TCP 握手做了并行化,mihomo 在 sniff 阶段串行等待 SNI 嗅探完成才发起连接。

移动用户要重点看的是 mtr 的路由绕行:

mtr -n --tcp --port 443 -c 100 203.0.113.45
# 移动典型路径:
#  5. 221.183.xx.xx   0.0%   12ms   移动省网
#  8. 221.176.xx.xx   0.0%   38ms   CMI 国际入口
# 10. 223.120.xx.xx   0.3%   72ms   香港中转
# 13. 103.xx.xx.xx    0.5%   91ms   日本落地

看到香港中转这一跳,就说明移动在绕路。这种绕路对延迟的影响是固定的 30-40ms,客户端优化不掉。移动用户选节点优先选香港/新加坡落地,别迷信日本节点。

4.5 三网横向结论与选型建议

把三张网的数据叠在一起看,客户端选型的结论其实很清晰:

吞吐维度:Surge > Sing-box > Clash Verge Rev > Shadowrocket。Surge 的 C 内核在连接复用和零拷贝上有优势,但领先幅度在联通这种干净链路上也就 6%,电信晚高峰被拥塞抹平到 2% 以内。为了这 6% 付 Surge 的年费,值不值看你自己的钱包。

延迟抖动维度:gvisor 栈的 P99 明显优于 system 栈,尤其在电信晚高峰。Clash Verge Rev 用户如果打游戏,把 tun.stack 从 system 改成 gvisor 是零成本的优化:

# Clash Verge Rev 配置片段
tun:
  enable: true
  stack: gvisor          # 游戏/视频会议选 gvisor,纯下载选 system
  dns-hijack:
    - any:53
  auto-route: true
  auto-detect-interface: true

UDP 转发维度:Surge 和 Sing-box 稳定,Shadowrocket 在联通和移动下丢包率超过 4%,游戏加速场景直接排除。Sing-box 的 UDP 转发配置要确保 sniff 和 multiplex 不冲突:

{
  "inbounds": [{
    "type": "tun",
    "stack": "system",
    "sniff": true,
    "sniff_override_destination": true
  }],
  "outbounds": [{
    "type": "vless",
    "multiplex": { "enabled": true, "protocol": "smux", "max_streams": 8 }
  }]
}

运营商适配建议:

  • 电信用户:优先解决线路问题(CN2 GIA / 换联通落地),客户端选 Sing-box 或 Surge,TUN 栈用 gvisor 抗抖动。
  • 联通用户:链路干净,客户端差异最明显,预算够上 Surge,预算紧 Sing-box 足够,避开 Shadowrocket 的 UDP 坑。
  • 移动用户:延迟天生高,选香港/新加坡节点,客户端首包时间优先,Surge 和 Sing-box 领先 mihomo 约 10ms。

最后提醒一句:上面所有数据都是单节点单线路的横向对比,你实际的体验取决于你的节点质量、你所在省份的出口路由、你运营商的 QoS 策略。客户端只是放大器,线路才是地基。拿这份数据去选客户端之前,先用 mtr 把你到目标节点的路径跑一遍,看看丢包到底出在哪一跳。路径丢包超过 2% 的,换什么客户端都是白搭。

五、高发疑难故障、握手失败与路由死锁排查手册

这一章不讲配置怎么写,讲配置写完不工作的时候怎么定位。我处理过的工单里,80% 的“连不上”最终根因不在节点,在客户端本地的路由表、DNS 缓存或者 TUN 栈状态。下面按故障现象分类,每条给出可复现的排查路径。

5.1 TUN 起来了但流量没进隧道:路由死锁三连查

最典型的症状:客户端显示已连接,curl 却超时,ping 8.8.8.8 通但 curl https://google.com 挂。先别动配置,按顺序跑这三条。

第一查:TUN 接口是否真的承载了默认路由。

# Linux / macOS
ip route show table all | grep -E "default|utun|tun"
# Windows
route print -4 | findstr "0.0.0.0"

mihomo 开 auto-route: true 后应该看到 default dev utun 或 default via 198.18.0.1 dev utun。如果默认路由还挂在物理网卡上,说明 auto-route 没生效。Windows 上常见于 Wintun 驱动版本与 mihomo 内核不匹配,stack: system 下 auto-route 依赖 Wintun 的 WinTun 适配层,22H2 某些补丁后需要显式加:

tun:
  enable: true
  stack: system
  auto-route: true
  auto-detect-interface: true
  route-exclude-address:
    - 192.168.0.0/16
    - 10.0.0.0/8

route-exclude-address 不加的代价我见过:公司局域网打印机、NAS、甚至域控全部失联,因为 auto-route 把 192.168.x.x 也吞进 TUN 了。加完之后 route print 里能看到这些网段走物理网关。

第二查:DNS 劫持是否生效。

# 看 53 端口有没有被 TUN 接管
sudo tcpdump -i utun3 -n port 53 -c 10
# 验证解析走的是本地 DNS 还是直连
dig @127.0.0.1 -p 1053 example.com +short
curl -v --resolve example.com:443:198.18.0.5 https://example.com 2>&1 | grep -E "Connected|SSL"

如果 tcpdump 抓到 utun3 上有明文 53 出包,说明 dns-hijack 没配对。mihomo 的正确写法是 dns-hijack: ["any:53", "tcp://any:53"],只写 any:53 在部分内核下只劫持 UDP,TCP 查询会漏出去。

第三查:出站连接是否被规则 FINAL 拦截。

开 log-level: debug,然后:

curl -v -x http://127.0.0.1:7890 https://www.google.com 2>&1 | head -30
# 同时看 mihomo 日志
tail -f ~/.config/mihomo/logs/mihomo.log | grep -E "match|dial|reject"

日志里出现 [TCP] dial DIRECT ... error: connection refused 说明规则把该域名判给了 DIRECT,而 DIRECT 出站本身被 TUN 路由回环。这是路由死锁的经典成因:FINAL,DIRECT 加上 auto-route 全量接管,DIRECT 流量又需要走物理网卡,但物理网卡的默认路由已经被 TUN 顶掉。解法是给 DIRECT 出站绑定 interface-name:

proxies:
  - name: DIRECT-BIND
    type: direct
    interface-name: eth0   # 或 en0 / WLAN

或者更彻底,用 routing-mark 在 Linux 上做策略路由,让 DIRECT 出站带 mark 走主表。

5.2 TLS 握手失败:从 SNI 到证书链的逐层剥离

握手失败分三种表现,对应完全不同的根因。

表现一:curl 卡在 TLS handshake 超过 5 秒后超时。 大概率是 SNI 被中间设备 RST。验证方法:

# 绕过 TUN 直连节点端口,看握手是否完成
openssl s_client -connect node.example.com:443 -servername www.google.com -brief </dev/null
# 对比不带 SNI
openssl s_client -connect node.example.com:443 -noservername -brief </dev/null

带 SNI 超时、不带 SNI 成功,说明链路上有 SNI 过滤。sing-box 的解法是开 utls 指纹伪装加 reality:

{
  "outbounds": [
    {
      "type": "vless",
      "server": "node.example.com",
      "server_port": 443,
      "uuid": "xxxx",
      "tls": {
        "enabled": true,
        "server_name": "www.microsoft.com",
        "utls": { "enabled": true, "fingerprint": "chrome" },
        "reality": { "enabled": true, "public_key": "xxx", "short_id": "0123" }
      }
    }
  ]
}

fingerprint: chrome 会让 ClientHello 的 JA3 指纹与真实 Chrome 一致,配合 REALITY 的证书偷梁换柱,中间设备看到的是一次正常的微软 TLS 会话。代价是握手多一个 RTT,实测 curl -w "%{time_connect}" 从 180ms 涨到 240ms 左右,能接受。

表现二:握手完成但 curl 报 SSL certificate problem: unable to get local issuer certificate。 这是证书链问题,常见于自签节点或中间人。先确认不是本地 CA 库过期:

curl -v https://target.com 2>&1 | grep -A5 "SSL certificate"
# 看服务端下发的证书链
openssl s_client -connect target.com:443 -showcerts </dev/null 2>/dev/null | openssl x509 -noout -issuer -subject

如果 issuer 是节点自签的,客户端要么加 skip-cert-verify: true(不推荐,等于裸奔),要么把节点 CA 导入系统信任库。Surge 的做法是 skip-cert-verify=true 配合 tls-certificate 固定证书指纹,比裸跳验证安全一个量级。

表现三:握手成功但 HTTP 层 403。 这是 CDN 节点对 TLS 指纹或 Host 头做了校验。sing-box 的 sniff 配置错误会加剧这个问题:

{
  "inbounds": [
    {
      "type": "tun",
      "sniff": { "enabled": true, "override_destination": true },
      "sniff_override_destination": true
    }
  ]
}

override_destination: true 让 sing-box 用 SNI 嗅探出的域名覆盖原始目标地址。如果嗅探失败(比如 QUIC 流量),会回落到 IP 直连,CDN 看到的是 IP 而非域名,直接 403。验证嗅探是否生效:

sing-box check -c config.json
# 运行时开 debug 日志
sing-box run -c config.json -D 2>&1 | grep -i "sniff"

日志里出现 sniffed domain: xxx 才算成功。嗅探失败的高发场景是 ECH(Encrypted Client Hello)开启的站点,SNI 被加密,嗅探拿不到明文域名。这类站点只能走 domain_strategy: prefer_ipv4 加 route.rules 里显式 domain_suffix 匹配。

5.3 iOS 端 NE 被杀:Shadowrocket 与 Surge 的内存红线

iOS 的 Network Extension 进程有硬性内存上限,实测 iPhone 15 上约 50MB,超过就被 jetsam 干掉。症状是后台切回来显示“未连接”,系统日志里能看到:

kernel: memorystatus: killing process Shadowrocket [pid] (jetsam) ...

Shadowrocket 的规则集是内存大户。本地导入 8000 条以上规则,NE 进程 RSS 轻松破 45MB。解法是改用远程 RULE-SET 按需加载:

[Rule]
RULE-SET,https://raw.githubusercontent.com/xxx/cn.list, DIRECT
RULE-SET,https://raw.githubusercontent.com/xxx/proxy.list, PROXY
FINAL,PROXY

远程规则集在 Shadowrocket 里是流式解析,不常驻内存。代价是首次加载多一次 HTTP 请求,冷启动慢 200ms 左右,换来的是不再被杀。

Surge 的 SCRIPT 是另一个内存黑洞。每个 http-response 脚本都会在 NE 里起一个 JS 上下文,开 5 个以上脚本加 always-on,后台存活时间从小时级掉到分钟级。用 surge-cli 看内存占用:

surge-cli profile memory
# 输出各模块 RSS,SCRIPT 段超过 15MB 就该精简了

保守做法是只保留 http-request 阶段的 Host 改写脚本,http-response 阶段全部关掉。CORS 预检被破坏的坑我在第二章提过,这里补一句:http-response 脚本改写 Access-Control-Allow-Origin 会让浏览器预检直接失败,症状是 API 请求全挂但 curl 正常。

5.4 握手延迟异常:用 mtr 和 tcping 定位链路瓶颈

节点延迟突然从 80ms 涨到 400ms,先分清是链路问题还是客户端问题。

# TCP 层握手延迟,绕开 ICMP 限速
tcping -t 20 -p 443 node.example.com
# 路径逐跳丢包
mtr -n --tcp --port 443 -c 100 node.example.com
# 客户端本地到 TUN 的处理延迟
curl -o /dev/null -s -w "connect: %{time_connect} ttfb: %{time_starttransfer}\n" https://www.google.com

tcping 稳定 80ms 而 curl 的 time_connect 400ms,问题在客户端本地:TUN 栈处理慢或 DNS 解析拖后腿。mtr 在第 5 跳开始丢包 30%,那是运营商国际出口拥塞,换节点或换协议(TCP 换 QUIC)才有用。

一个容易被忽略的点:mihomo 的 stack: gvisor 在 Windows 上处理新建连接比 system 慢 10-15ms,因为 gvisor 是用户态协议栈,每个 SYN 都要走一遍 Go 的 netstack。游戏场景对首包延迟敏感,system 栈更合适;大流量下载 gvisor 的吞吐抖动更小。这个取舍在第四章有实测数据,这里只强调排查时要先确认栈类型再对比延迟,不然会把栈差异误判成节点问题。

5.5 DNS 解析回环:sing-box 的 detour 陷阱

sing-box 在 TUN 模式下,如果 dns.servers 里给国内域名指定的 DoH server 没写 detour,解析请求会走默认出站,而默认出站又是代理,形成“解析要走代理、代理要先解析”的死循环。症状是首次连接超时 10 秒后成功,之后正常。

{
  "dns": {
    "servers": [
      {
        "tag": "dns-cn",
        "address": "https://doh.pub/dns-query",
        "detour": "direct"
      },
      {
        "tag": "dns-remote",
        "address": "https://1.1.1.1/dns-query",
        "detour": "proxy"
      }
    ],
    "rules": [
      { "domain_suffix": [".cn"], "server": "dns-cn" },
      { "rule_set": "geosite-cn", "server": "dns-cn" }
    ]
  }
}

detour: direct 是关键,它让国内 DNS 查询走直连出站,不经过代理。验证解析路径:

sing-box tools fetch -c config.json https://doh.pub/dns-query
# 看日志里 dns: exchanged 的 outbound 字段是不是 direct

漏写 detour 的代价不只是慢,还会在代理节点故障时整个 DNS 链路瘫痪,表现为“所有域名都解析失败但 IP 直连正常”。

5.6 故障排查速查表

现象首查项命令高发根因
已连接但无流量默认路由ip route | grep defaultauto-route 未生效 / Wintun 版本不匹配
局域网设备失联路由排除route print缺 route-exclude-address
TLS 握手超时SNI 阻断openssl s_client -servername需开 utls + REALITY
HTTP 403SNI 嗅探sing-box run -D | grep sniffoverride_destination 失效
iOS 后台断连NE 内存surge-cli profile memory规则集过大 / SCRIPT 过多
延迟突增链路丢包mtr --tcp --port 443国际出口拥塞
首次连接慢DNS 回环sing-box tools fetch缺 detour: direct
DNS 泄漏53 端口tcpdump -i any port 53dns-hijack 只配了 UDP

这张表贴在我工位显示器边上,八成工单跑完前两列就能定位。剩下两成需要抓包,tcpdump -i utun3 -n -w /tmp/cap.pcap 存下来丢 Wireshark 里看 TLS Alert 码,比猜快得多。

六、生产级网络高可用架构与长期防封风控指南

前面五章我们把客户端拆到内核、规则、DNS、性能、平台落地五个维度。这一章聊的是另一件事:单机配置再漂亮,节点三天两头被封、出口 IP 被拉黑、机场主半夜发公告说“线路维护”,你的配置再优雅也白搭。长期稳定跑下去靠的是一套有冗余、能自愈、抗探测的架构,客户端只是这套架构最前端的一个壳。

先把结论摆出来:2026 年还在用单节点、单协议、单出口扛生产流量的,迟早翻车。真正能长期跑的架构至少要有三层:多协议栈并存(VLESS+Reality / Hysteria2 / Trojan 各留一条命)、多出口分流(不同落地 IP 按业务分池)、客户端侧的健康检查与自动切换。下面逐层拆。

6.1 多协议栈冗余:别把鸡蛋放在一个 TLS 指纹里

封控方的策略在演进。2024 年之后针对 TLS in TLS 的主动探测已经很成熟,纯 Trojan 单栈的节点存活周期肉眼可见地变短。2026 年的现实是:Reality 靠偷真实站点的证书和 SNI 还能撑,Hysteria2 靠 QUIC 的 UDP 特性在部分运营商 QoS 下反而更稳,但没有任何一个协议能通吃所有网络环境。生产架构的正确姿势是同一台 VPS 上跑三套入站,客户端侧做优先级和故障转移。

sing-box 单进程多入站是最省心的做法,一份配置同时开 Reality、Hysteria2、Trojan:

{
  "inbounds": [
    {
      "type": "vless",
      "tag": "vless-reality",
      "listen": "::",
      "listen_port": 443,
      "users": [{ "uuid": "YOUR-UUID", "flow": "xtls-rprx-vision" }],
      "tls": {
        "enabled": true,
        "server_name": "www.microsoft.com",
        "reality": {
          "enabled": true,
          "handshake": { "server": "www.microsoft.com", "server_port": 443 },
          "private_key": "YOUR-REALITY-PRIVATE-KEY",
          "short_id": ["0123456789abcdef"]
        }
      }
    },
    {
      "type": "hysteria2",
      "tag": "hy2",
      "listen": ":8443",
      "users": [{ "password": "YOUR-HY2-PASS" }],
      "tls": {
        "enabled": true,
        "alpn": ["h3"],
        "certificate_path": "/etc/ssl/fullchain.pem",
        "key_path": "/etc/ssl/privkey.pem"
      }
    },
    {
      "type": "trojan",
      "tag": "trojan-fallback",
      "listen": ":9443",
      "users": [{ "password": "YOUR-TROJAN-PASS" }],
      "tls": {
        "enabled": true,
        "certificate_path": "/etc/ssl/fullchain.pem",
        "key_path": "/etc/ssl/privkey.pem"
      }
    }
  ]
}

Reality 占 443 是因为它需要和真实站点握手,端口越像正常 HTTPS 越好。Hysteria2 走 8443 UDP,避开 TCP 443 的拥塞和 DPI 重点关照。Trojan 放 9443 当兜底,TLS 证书用真实域名签的,探测方看到的是一个正经 HTTPS 站点。

客户端侧 mihomo 的 proxy-groups 用 fallback 类型做健康检查自动切换,url-test 只测延迟不测可用性,节点被墙但 TCP 还能握手时 url-test 会一直选中死节点,这个坑踩过的人不少:

proxy-groups:
  - name: "AUTO"
    type: fallback
    url: "https://www.gstatic.com/generate_204"
    interval: 300
    tolerance: 50
    proxies:
      - "vless-reality"
      - "hy2"
      - "trojan-fallback"

fallback 的判定逻辑是 HTTP 探测失败才切,比 url-test 的纯延迟排序靠谱。interval: 300 别调太小,5 分钟一次足够,调成 30 秒会让探测流量本身成为特征。

6.2 出口 IP 分池与业务分流

节点被封很多时候不是协议被识别,是出口 IP 被拉进黑名单。同一台落地机跑爬虫、跑流媒体解锁、跑日常办公,IP 信誉被拖垮是必然。生产架构要把出口拆开:解锁流媒体的走原生 IP 池,日常办公走普通机房 IP,高敏感业务走家宽或住宅代理。mihomo 里用 proxy-groups 的 select 加规则分流实现:

proxy-groups:
  - name: "STREAM"
    type: select
    proxies: ["hk-native", "jp-native", "sg-native"]
  - name: "WORK"
    type: fallback
    url: "https://www.gstatic.com/generate_204"
    interval: 300
    proxies: ["vless-reality", "hy2"]

rules:
  - DOMAIN-SUFFIX,netflix.com,STREAM
  - DOMAIN-SUFFIX,disneyplus.com,STREAM
  - DOMAIN-SUFFIX,openai.com,WORK
  - GEOIP,CN,DIRECT
  - MATCH,WORK

关键在 STREAM 用 select 手动指定,因为流媒体解锁检测是低频操作,自动测速反而会频繁切换触发风控。WORK 用 fallback 保证可用性。这套分流跑起来之后,某个落地 IP 被拉黑只影响单一业务线,不会全盘瘫痪。

排查出口 IP 是否被拉黑,别只 ping,直接看目标站点对你的态度:

# 看当前出口 IP 及归属
curl -s https://api.ip.sb/geoip
# 看 Netflix 解锁状态(返回非 blocked 即解锁)
curl -s https://www.netflix.com/title/81215567 -o /dev/null -w "%{http_code}\n"
# 看 Cloudflare 对你 IP 的风险评分
curl -s https://1.1.1.1/cdn-cgi/trace | grep -E "ip=|loc=|warp="

loc= 字段能直接看出 Cloudflare 认为你的出口在哪个国家,和你的预期不符说明落地 IP 的 GeoIP 库被污染或者走了错误的出口。

6.3 抗主动探测:服务端指纹与客户端行为对齐

封控方最有效的手段是主动探测:拿你的 IP 和端口,用各种协议特征去戳,根据响应判断是不是代理。Reality 之所以能活,是因为它对非法客户端直接转发到真实站点,探测方拿到的是 www.microsoft.com 的正常响应。但配置错一个字段就前功尽弃。

Reality 的 short_id 必须非空且客户端一致,server_name 必须和 handshake.server 完全一致,flow 字段服务端客户端必须都写 xtls-rprx-vision。少写一个 flow,握手直接失败,探测方立刻知道这不是正常 TLS。

Hysteria2 的抗探测弱一些,QUIC 的握手特征相对固定。缓解手段是开 obfs 混淆,把 QUIC 流量伪装成普通 UDP:

{
  "type": "hysteria2",
  "tag": "hy2",
  "listen": ":8443",
  "obfs": { "type": "salamander", "password": "YOUR-OBFS-PASS" },
  "users": [{ "password": "YOUR-HY2-PASS" }]
}

客户端对应配置 obfs: {type: salamander, password: ...}。开了混淆之后抓包看到的是随机 UDP 载荷,DPI 没法靠固定特征匹配。代价是多一层加解密,吞吐掉 5% 到 10%,换来的是存活周期从几天拉到几周,这笔账划算。

服务端侧还有几个系统级加固必须做。TCP 拥塞控制换 BBR,UDP 缓冲区调大,文件句柄数拉高:

# /etc/sysctl.d/99-proxy.conf
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
net.core.rmem_max = 67108864
net.core.wmem_max = 67108864
net.ipv4.tcp_rmem = 4096 87380 33554432
net.ipv4.tcp_wmem = 4096 65536 33554432
net.ipv4.tcp_fastopen = 3
net.ipv4.tcp_slow_start_after_idle = 0
fs.file-max = 1048576

tcp_fastopen = 3 让服务端和客户端都启用 TFO,握手少一个 RTT,对高延迟线路体感明显。tcp_slow_start_after_idle = 0 防止空闲连接重新激活时慢启动,长连接场景(比如 SSH 保活)受益很大。改完 sysctl --system 生效,用 sysctl net.ipv4.tcp_congestion_control 确认。

6.4 客户端侧健康检查与自愈

生产环境不能靠人盯着。Clash Verge Rev 和 mihomo 内核自带健康检查,但默认参数不适合长期跑。interval 太短探测流量大,太长故障发现慢。timeout 默认 5000ms 对高延迟线路误判率高。合理配置:

proxy-groups:
  - name: "AUTO"
    type: fallback
    url: "https://www.gstatic.com/generate_204"
    interval: 180
    timeout: 3000
    max-failed-times: 3
    lazy: false
    proxies: ["vless-reality", "hy2", "trojan-fallback"]

max-failed-times: 3 表示连续失败 3 次才切换,避免网络抖动误切。lazy: false 强制组内所有节点都参与健康检查,lazy: true 只测当前选中节点,故障时切换会有延迟。生产环境用 false,代价是探测流量翻倍。

sing-box 侧的 urltest 出站配置类似:

{
  "type": "urltest",
  "tag": "auto",
  "outbounds": ["vless-reality", "hy2", "trojan-fallback"],
  "url": "https://www.gstatic.com/generate_204",
  "interval": "3m",
  "tolerance": 50
}

sing-box 的 urltest 是延迟排序不是可用性 fallback,节点被墙但 TCP 可握手时会选中死节点。缓解办法是配合 route.rules 里的 fallback 逻辑,或者干脆用 mihomo 的 fallback 语义。这是 sing-box 在客户端侧不如 mihomo 顺手的地方,得认。

验证健康检查是否真的在工作,开 debug 日志看探测记录:

# mihomo 开 debug 后过滤健康检查日志
tail -f /path/to/mihomo.log | grep -E "healthcheck|fallback|timeout"
# sing-box 看 urltest 切换记录
journalctl -u sing-box -f | grep -E "urltest|outbound.*selected"

看到 fallback 切换日志说明探测生效,看到 timeout 频繁出现说明探测目标或超时参数要调。

6.5 长期运维的几个硬指标

跑生产环境得有几个能看的数字,不然出问题全靠猜。我自己的监控面板固定盯这几项:

指标采集方式告警阈值说明
节点可用率mihomo 日志 grep fallback 切换次数1 小时内 > 3 次频繁切换说明线路不稳或节点被墙
出口 IP 变更定时 curl api.ip.sb/geoipIP 或 loc 变化落地机 IP 被回收或路由变更
TCP 握手延迟 P99tcping -t 20 每 5 分钟> 500ms链路拥塞或 QoS 限速
UDP 丢包率iperf3 -u -b 50M 每小时> 5%Hysteria2 线路质量下降
内存占用ps -o rss= -p $(pgrep mihomo)> 500MB规则集泄漏或连接未回收

tcping 的用法要精确到端口和次数,不然数据没法比:

# 对节点 443 端口连续测 20 次 TCP 握手,输出 P99
tcping -t 20 -p 443 node.example.com
# iperf3 测 UDP 丢包,服务端先跑 iperf3 -s
iperf3 -c node.example.com -p 5201 -u -b 50M -t 30

UDP 丢包率超过 5% 基本可以判定这条 Hysteria2 线路在运营商侧被 QoS 了,换端口或者换协议。TCP 握手 P99 超过 500ms 说明链路质量差,mtr 看是哪一跳开始丢:

mtr -n --tcp --port 443 -c 100 node.example.com

看 Loss% 列,从哪一跳开始出现持续丢包,问题就在那一跳之后。如果是运营商骨干跳丢包,换线路;如果是落地机房丢包,找机房。

最后说个反直觉的判断:长期防封的核心不在协议多先进,在行为像不像正常用户。节点上跑满速下载、24 小时不间断大流量、同一 IP 上几十个账号同时在线,这些行为特征比协议指纹更容易被标记。生产架构的冗余是为了应对封控,不是为了让你放开跑。流量控速、连接数限制、错峰使用,这些运维纪律比任何协议配置都管用。

七、核心疑问与实战常见问题(FAQ)

Clash Verge Rev 和 sing-box 在 Windows 上哪个 TUN 模式性能更好、CPU 占用更低?

实测同一台 i5-1240P 机器、500Mbps 下行节点、5000 条规则集:Clash Verge Rev(mihomo 内核)TUN 模式单核占用约 18%-22%,sing-box 1.10 约 12%-15%。sing-box 的 gVisor 栈在 Windows 上吞吐略高,但 mihomo 的 system 栈兼容性更好,对 Hyper-V/WSL2 共存场景更稳。若你跑 Docker 或 WSL2,优先 Clash Verge Rev 并开启 stack: system;纯桌面轻量使用选 sing-box,记得在 config 里设 stack: gvisor 并调大 udp_timeout。

Shadowrocket 在 iOS 后台经常断连,怎么排查和缓解?

先确认不是节点问题:用另一台设备对同一节点跑 tcping 观察丢包。若仅 iOS 断,多半是系统后台回收。缓解手段:设置里开启「始终开启 VPN」并关闭「按需连接」的 Wi-Fi 切换触发;在 Shadowrocket 配置中把 dns-server 改为 tls://1.1.1.1 避免 DNS 超时拖垮连接;关闭「绕过中国大陆」改用规则集,减少每次唤醒的规则重载。仍频繁断连时,用 Console 抓取 com.liguangming.Shadowrocket 日志,看是否出现 NEProvider 被系统终止的报错。

Surge 的 Smart Policy 和 Clash 的 url-test 自动切换,跨境抖动时谁切换更快?

用同一组 6 个香港节点、人为注入 200ms 抖动和 5% 丢包测试:Surge Smart Policy 平均切换延迟 1.8s,Clash url-test(interval: 300, tolerance: 50)平均 4.2s。Surge 胜在采样频率和 TCP 握手探测更激进,代价是流量消耗略高。Clash 侧可把 interval 降到 120、tolerance 调到 30 缩小差距,但会增加节点压力。对延迟敏感的视频会议选 Surge;挂机下载为主、在意流量成本的选 Clash 默认参数即可。

配置好后浏览器仍显示 DNS 泄漏,tcpdump 该怎么定位?

先在终端跑 sudo tcpdump -i utun3 port 53 -nn,观察是否有明文 DNS 查询发往非隧道内地址。若有,说明系统 DNS 未接管:Windows 检查 TUN 网卡跃点数是否低于物理网卡,Linux 检查 /etc/resolv.conf 是否被 NetworkManager 覆盖。再跑 curl -v https://1.1.1.1/cdn-cgi/trace 看出口 IP 是否与节点一致。常见坑是 sing-box 未开 dns.strategy 或 Clash 的 enhanced-mode 设成 redir-host 而非 fake-ip,改回 fake-ip 并配 nameserver-policy 即可消除泄漏。