WSL 之旅 2:Clash Verge 探索

前情提要

上一回我们通过修改 MTU 的方式解决了无法通过 curl 下载和无法连接学校内网的问题。这让我对网络代理,尤其是 Clash Verge 的 TUN 模式,有了不少兴趣。不过由于时间关系,也一直没来得及学。

恰好最近又遇到一堆网络问题,正好趁着解决这些问题的机会把 Clash Verge 弄明白一点,重整所有的 Clash Verge 配置,也方便以后的各种工作。

问题

因为我的默认搜索引擎是谷歌搜索,所以我的 Clash Verge 是开机自启的。通常而言,只要是使用机场没有 timeout 的节点,总是能连接得上谷歌搜索。然而,同属于谷歌的 Gemini 却会时不时发神经,显示“Gemini 目前不支持您所在的地区”,尽管我挂的是美国节点。奇异搞笑的是,如果我换了相同机场的另一个美国节点,Gemini 就又能连上了。

还有,我在连接英国节点的时候不管走哪个中转站,Codex 都经常遇到 stream disconnected before completion 的问题;然而,这个问题在连接美国节点时却很少见,几乎消失了。

再比如说,即使配置了全局规则和 Fake IP Filter,不把 MTU 下调到 1350,还是无法连接校园网内网。但是我不是在全局规则和过滤器里配置了访问内网域名时流量直连了吗!

遇到的问题太多了。今天在这里我们集中解决一下。

Clash Verge 原理

要解决这些问题,就必须理解 Clash Verge 在电脑上究竟起了什么作用。换而言之,我们需要弄明白,“发送-接受”的交互过程中发生了什么。

名词解释

我当然希望你看完这些!不过,如果懒得看,你当然也可以跳过这一部分,先看下面的代理规则;遇到不会的地方再回来补一补吧~

系统代理与 TUN 模式

Clash Verge 有两种启动模式。一种叫系统代理,一种叫 TUN 模式(虚拟网卡模式)。通俗的讲,系统代理通常只能用于在客户端中访问外网,而 TUN 模式则几乎可以在所有情况下都访问到外网。不过,这是什么原因呢?

原理在于,系统代理是一种“君子协议”,而 TUN 模式是一种“强制命令”。

系统代理的作用是开启一个本地端口(通常是 localhost:7890),并通告给所有软件,Clash Verge 将会从这个端口接受流量,然后转发给你的机场服务器。一些比较识相的程序,比如说大部分浏览器和部分的日常软件,都会主动遵循这个协议,把流量发给这个端口。然而,也有一部分软件,比如大部分的命令行工具,比如各种游戏,再比如一些老旧的软件,就不遵守这个协议。于是,它们的流量就会直连原网络,无法被代理软件接管。

为什么它们不遵守呢?老旧的软件纯粹是因为没跟上时代。而命令行软件,比如 curlgitwget 这些,通常是极简、跨平台的底层工具,设计上就不会主动读取这些配置。它们习惯于调用底层的网络接口直连目标服务器。也就是说,除非你在终端里显式地配置环境变量(如 export http_proxy=...),否则它们对系统代理完全视而不见。而游戏客户端不支持,则是为了追求极速,避免流量经过中转服务器造成延迟。

另外,操作系统配置的 HTTP 系统代理通常支持 TCP 协议而不支持 UDP 协议。TCP 协议类似接一根管道打长途电话,数据在管道内通过流式传输,可靠性高,所以经常用于网页浏览、文件下载、API 请求、发送邮件这些无法容忍数据错误的场景。而 UDP 协议类似寄明信片,不需要建立连接,直接把数据打包、贴上目的地地址就丢出去。所以,UDP 协议虽然可靠性不高,却绝对非常高效,通常用于实时网络游戏、语音通话、视频直播、DNS 查询这样追求极致速度的场景。

TUN 模式的工作原理是在操作系统中创建一张虚拟网卡并修改系统的路由表,将系统所有的网络出站流量强制重定向到这张虚拟网卡上。由于它直接在网络层拦截 IP 数据包,所以电脑的所有流量都会被强制捕获。而且,由于直接代理了网卡,它当然支持 UDP 协议。

规则模式、全局模式、直连模式

这三个规则都在上面两种模式的管辖下。也就是说,在开启 TUN 模式的情况下,即使使用直连模式,仍然是通过虚拟网卡转发流量的——只不过不转发到远程机场服务器。

直连模式就是完全绕过代理。全局模式,就是对于所有发送到 Clash Verge 的流量,软件不进行任何规则判断,将所有的网络流量全部强行通过代理服务器转发。而设置规则模式时,Clash Verge 会根据预设的规则列表,自动判断每个网络请求的去向。对于国内常用网站与 IP,如微信、淘宝、百度,软件会将其识别为国内流量,直接连接;对于境外或受限网站,如 Google、OpenAI,软件会将其识别为代理流量,通过代理服务器中转;甚至对于广告或隐私追踪域名,部分规则会直接将其拦截(Reject)。可以说,规则模式就是日常最适合开启的便利模式。

DNS

当我们访问 baidu.com 的时候,我们怎么连接到百度的服务器?服务器的 IP 地址一般是一个形如 111.63.65.103 的东西(你可以直接通过在浏览器里粘贴这个地址来访问百度)。而域名,只是对于人类而言易读易记忆的字符串。域名和 IP 地址必须通过某种东西挂钩,这个中介就是 DNS 服务器。

常见的明码 DNS 服务器有 Google Public DNS 8.8.8.8,阿里 AliDNS 223.5.5.5,Cloudflare & APNIC 1.1.1.1。它们主要基于 UDP 53 端口运行。

但是,明码 DNS 服务器缺乏任何加密,所以网络运营商可以轻松监视用户访问了哪些域名,甚至抢答 DNS 请求,把网站重定向到其他位置。所以,我们有更安全的 DoT(DNS over TLS)和更安全且更难被识别与封锁的 DoH(DNS over HTTPS)。使用 DoH 时,DNS 请求嵌套在 HTTP 中发出,外部很难区分,因此难以被中间人监听和修改。

路由规则

Clash Verge Rev 中有一个叫全局扩展覆写配置的配置文件。我们通常会在里面写

1
2
3
prepend-rules:
- DOMAIN-SUFFIX,google.com,机场代理
- GEOSITE,cn,DIRECT

之类的东西。

它的意思就是说,当访问 gemini.google.com 这样的网站时,就走叫作“机场代理”的代理组;当访问知名的中国网站时,就走直连路线。

当然,这个设置的优先级在 TUN 模式之下。即使是走直连路线,TUN 模式启动的虚拟网卡还是会劫持所有流量。所以,DIRECT 规则也是照样先通过虚拟网卡再直连,只不过不经过机场服务器而已。

Redir Host 与 Fake IP

Redir Host 模式是真实 IP 模式。当客户端需要访问某个网站时,需要先把域名解析成 IP 地址,而这个过程就需要 DNS 服务器。于是,代理软件先拦截 DNS 请求,并向上游的真实 DNS 服务器发起查询。等待 DNS 服务器返回域名对应的 IP 地址后,代理软件再返回这个 IP 地址。于是,客户端向这个 IP 地址发起 TCP 或 UDP 连接请求。再然后,代理软件拦截这个请求,并根据其目标 IP 地址匹配分流规则。如果命中代理规则,则将请求转发至远端节点;如果命中直连规则,则直接发送。

我们发现,这个过程是链式的,必须先做完上一步才能做下一步,太慢了。而且,除非配置复杂的防污染规则,否则 DNS 查询很容易在通过边界网关的时候被“抢答式注入”,被代理软件采信错误的 DNS 响应包。

目前的改进方法是使用 Fake IP 模式。当客户端需要访问某个网站时,Fake IP 模式下的 Clash 直接给域名分配一个在 198.18.0.1/16 之间的虚假 IP 地址,让客户端直接发起连接请求。同时,代理软件在本地内存的哈希表中记录下 Fake IP 与域名的映射关系。再然后,代理软件根据真实的域名进行分流匹配。如果命中代理规则,它直接将域名打包发给远端服务器,由远端服务器在境外进行真实的 DNS 解析(省了本地 DNS 解析的时间,还能避免 DNS 污染);如果判定为直连,代理软件此时才在本地向国内上游 DNS 发起一次真正的解析,获得真实 IP 后直连。

当然,对于特定的域名,你也可以通过配置 Fake IP Filter 强制它们不使用 Fake IP,必须等待 DNS 服务器返回的真实 IP。常见的有 *.lan*.localtime.*.comwww.msftconnecttest.com 之类。

DNS 管理

通过合理配置多组 DNS 服务器、分流策略以及 Fake IP 技术,可以在保证国内网站极速访问的同时防止 DNS 污染。

DNS 服务器配置

Clash 中需要配置的 DNS 服务器大致有这些:

  • default-nameserver:引导 DNS。这里配置的都是直接使用 IPv4 地址的 DNS 服务器。因为接下来配置的 DNS 可以是有加密的 DoT 或 DoH,而这些有加密的服务器通常以域名形式呈现,所以需要无须解析就能直接访问的 DNS 服务器来“引导”它们。
  • nameserver:基础 DNS 服务器。对于未匹配到 nameserver-policy 的普通域名,内核会默认向此项配置的 DNS 发起查询。
  • fallback:后备 DNS 服务器。通常配置为境外无污染的加密 DNS,用于在基础 DNS 被污染时提供正确的海外 IP 结果。
  • proxy-server-nameserver:专用于解析“代理节点服务器域名”的 DNS。为了避免在开启 respect-rules(DNS 本身的网络请求也将严格遵循路由分流规则)时产生“域名需要通过代理节点建立连接 -> 代理节点域名又需要 DNS 解析 -> DNS 请求被规则判定需要走代理”的死锁,此处通常配置国内可直连的纯净 DNS。
  • direct-nameserver:直连出站解析 DNS。
分流策略
  • nameserver-policy:让配置了 nameserver-policy 的域名不走 nameserver,强制走另外指定的 DNS 服务器。
  • direct-nameserver-follow-policy:直连解析是否遵循 nameserver-policy
  • fallback-filter:用来判定 nameserver 的解析结果是否被污染。如果满足以下条件之一,内核就将丢弃 nameserver 的解析结果,转而等待 fallback 的解析结果:
    • geoip:当 geoip: truegeoip-code: 'CN' 时,如果解析出来的 IP 不是中国大陆的 IP,则丢弃该结果。
    • ipcidr:解析出的 IP 属于测试保留网段 240.0.0.0/4 或无效地址 0.0.0.0/32,则丢弃该结果。
    • domain:请求的域名匹配自定义的网站(如谷歌、YouTube 等境外网站),则丢弃该结果。

MTU 与 MSS

数据是以一个一个包的形式传输的。一个包有包头和包身。包身是传输的内容,而包头则存了起点、终点、协议这样的元信息。

网络上对包的大小总是有限制,这个限制一般是 1500 字节。这意味着我们最好不应该传输超过 1500 字节的包,否则,好一点的情况是有人帮忙分包,进而导致速度变慢,坏一点的情况就是某一个节点直接把超重的包丢弃了。

所以我们有 MTU 和 MSS 的概念。MTU 叫最大传输单元,MSS 叫最大分节大小。可以认为,MTU 就是最大包大小,MSS 是最大包身大小。举例来说,如果放在 TCP 协议的语境下,由于 TCP 协议的包头有 40 个字节,所以有 MTU=MSS+40MTU = MSS + 40

那么,把 MTU 设置成 1500 是不是就行了呢?也不尽然。复杂传输中经常是包套着包传输,我们这里发出一个 1500 的包,但是它可能又要经过一层转发,被套成了 1540 个字节,然后在传输中直接超重丢弃了。

执行时序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
客户端发起域名请求

├── [第一阶段:DNS 拦截与 Fake-IP 判定]
│ │
│ ├── 命中 fake-ip-filter?
│ │ ├── 是(不分配假 IP) ──> 挂起请求,发起 [真实 DNS 解析] ──> 客户端拿真实 IP ──> [第二阶段]
│ │ └── 否(常规域名) ──> 内核瞬间分配 Fake-IP(198.18.x.x) 返回给客户端 ──> 客户端发起连接 ──> [第二阶段]
│ │
│ └── 真实 DNS 解析过程(在此阶段或下阶段需要真实 IP 时触发)
│ ├── 匹配 nameserver-policy?
│ │ ├── 是 ──> 强制走对应的特定 DNS
│ │ └── 否 ──> 同时并发向 nameserver 和 fallback 发起解析
│ │ └── 比对 fallback-filter,若被污染则采用 fallback 结果
│ └── 注意:在此期间,若解析 DoH 的域名本身,使用 default-nameserver 进行解析

└── [第二阶段:流量接管与路由规则匹配](客户端向 目标IP/Fake-IP 发起 TCP/UDP 连接)

├── 内核还原域名(若是 Fake-IP 连接,内核自动在内存中将其还原为原始域名,如 google.com)

├── 顺序遍历 prepend-rules 规则
│ │
│ ├── 命中域名规则?(DOMAIN, DOMAIN-SUFFIX, DOMAIN-KEYWORD, GEOSITE 等)
│ │ ├── 是 ──> 执行对应策略(如 GLOBAL 代理 / DIRECT 直连) ──> 结束匹配
│ │ │ * 注意:若策略是 DIRECT,内核会在此刻触发 [真实 DNS 解析] 以获得真 IP 发送流量
│ │ │ * 若策略是 节点代理,内核通常直接将域名封包发给代理服务器,由远端解析(无本地解析延迟)
│ │ └── 否 ──> 继续向下匹配
│ │
│ └── 命中 IP 规则?(GEOIP, IP-CIDR 等)
│ ├── 带有 no-resolve?(如 GEOIP,CN,DIRECT,no-resolve)
│ │ ├── 此时没有真实 IP(处于 Fake-IP 阶段) ──> 无法匹配,直接跳过此规则。
│ │ └── 此时已有真实 IP(因前置触发了真实 DNS) ──> 比对 IP,命中则走对应策略。
│ └── 没有 no-resolve?
│ └── 强制触发 [真实 DNS 解析] ──> 获取真实 IP ──> 比对 IP 段,命中则走对应策略。

└── 若全部规则均未命中 ──> 走向 MATCH 默认规则

问题解决

问题一

问题一可能是 DNS 污染导致,也可能是第一个节点被谷歌 Gemini 识别为机房节点然后 ban 了。如果是 DNS 污染,那么按上面的内容配置就好了。

问题二

这个问题是 DNS 解析与规则配置错误导致的。首先,Codex 向系统请求中转站域名的真实 IP。Clash 的 DNS 模块直接拦截了这个请求,并立刻丢回一个 Fake IP。Codex 拿到 Fake IP 后开始发送加密的流式数据包。Clash 收到发往 Fake IP 的流量后,将其还原为中转站的域名,并开始匹配规则。由于我配置了 DIRECT,Clash 识别出“这个域名需要直连”。

Clash 在后台查询真实 IP 时,其 DNS 请求可能会通过当前活跃的代理节点(美国或英国)发出,或者使用受节点影响的国外 DNS 服务(如 8.8.8.8)。如果该中转站使用了全球 CDN(如 Cloudflare)进行加速。当切换到美国节点时,解析出来的真实 IP 是美国 CDN 节点;切换到英国节点时,解析出来的是英国 CDN 节点。

又因为规则是 DIRECT,所以 Clash 会尝试让本地网络直接连接这些被解析出来的海外 CDN IP。在中国大陆直接连接海外 CDN IP 的网络质量极差,存在严重的边界网关干扰和高丢包率。英国节点的物理距离更远,国际出口带宽通常弱于美国西海岸节点,导致直连英国 CDN IP 的网络质量更差,频繁触发 TCP 重置,从而导致流式传输中断。

比较好的解决方案是配置 nameserver-policy 和直连规则,强制中转站走国内的 DNS 服务再直连。另外,Codex 目前默认开启 websocket 传输,要求极高的稳定性,最好在设置里把它关掉。

问题三

问题三则复杂一点。那时的我还没完全搞明白 Route Exclude Address、MTU、MSS 都是些什么,所以导致了这桩疑案。

在未配置 Fake IP Filter 的 Fake IP 模式下,客户端获取到的是 198.18.0.2 之类的虚拟 IP。此时,流量走的是这样的一条路由路径:客户端应用 \rightarrow Clash TUN 虚拟网卡 \rightarrow Clash 内核 \rightarrow 操作系统普通 Socket \rightarrow VPN 网卡 \rightarrow 物理网络

TCP 握手与数据分包的真实过程

从客户端向 Clash TUN 发出第一次握手的过程中,因为 Clash TUN 网卡的 MTU 默认是 1500,所以客户端与 Clash 协商出的 MSS 是 1460(即 1500 - 40,给 TCP/IP 头部预留 40 字节的位置)。客户端据此认为发送 1500 字节的 IP 包完全没有问题。

Clash 作为用户态代理软件,其核心机制是“连接终结”。它并不是路由器,不会在 IP 层转发并分片 TCP 报文。客户端发送给 Clash TUN 的 TCP 数据,在进入 Clash 的用户态协议栈后,会被重新组装成完整的应用层数据流。

随后,Clash 调用主机的系统 Socket,向真实的内网服务器发起第二次握手,建立一条全新的 TCP 连接。不妨假定物理出接口(VPN 网卡)的 MTU 为 1350(VPN 协议需要为隧道封装头部预留空间),此时主机的操作系统内核会与内网服务器协商出 1310(即 1350 - 40)的实际 MSS。

当 Clash 将重组后的应用层数据写入发往内网服务器的 Socket 时,主机的操作系统内核会自动按照 1310 字节的限制将数据流切分成一个个 TCP 分段并发送出去。

UDP 流量与 IP 分片责任主体

然而,内网网页加载不只有 TCP 流量,还伴随着大量的 UDP 流量。UDP 协议没有三次握手,也没有 MSS 机制。

当客户端向 Clash TUN 发送一个 1500 字节的 UDP 包时,Clash 的用户态协议栈提取出其 Payload,并使用主机的普通 UDP Socket 将数据发送出去。由于目标是直连的内网 IP,系统路由决定该包必须从 VPN 网卡(MTU 1350)发出。

此时,操作系统内核的 IP 协议栈在发送该 UDP 包时,发现其大小超过了出口 VPN 网卡的 MTU(1350),内核会自动对该 IP 报文进行 IP 分片。进行 IP 分片的责任主体是主机的操作系统,而非虚拟网卡本身。

不幸的是,现代企业内网防火墙与 VPN 网关出于安全和防范分片攻击的考量,默认会直接丢弃所有 IP 分片包。因此,这些被系统分片后的 UDP 报文在进入内网时直接被丢弃,导致依赖 UDP 的网页组件或服务加载失败。

ICMP“需要分片”报文与 PMTUD 机制

如果系统看到的出口 MTU 与实际路径 MTU一致,操作系统通常可以直接按正确大小发送 TCP 分段,不需要依赖后续路由器重新分片。另一种情况是,出口网卡显示的 MTU 较大,但数据在后续路径中又经过额外封装,导致真实路径 MTU 更小。此时,过大的报文可能被路由器丢弃,并通过 ICMP 报文通知发送方降低报文大小。

由于外侧与内网服务器建立连接的发送方是主机的操作系统,因此这个 ICMP 差错报文的目标地址是主机的 VPN 网卡,理应由主机操作系统的内核接收并处理,从而自动降低该 Socket 的发送 MSS。

然而,在实际运行中,这个关键的 ICMP 差错报文往往会被本地系统防火墙或安全软件拦截,导致主机内核无法得知“需要减小发送大小”的信号,继续发送大包,从而形成“TCP 黑洞”,连接彻底卡死。

解决措施与替代方案

所以,我们之前调低入口 TUN MTU 的措施确实改善了这一情况。把入口处的 MTU 调低(例如 1350),客户端在源头就会自动降低 MSS(1310),使得整个链路上发送的原始 TCP 数据分包变小;同时,客户端发送的 UDP 包在进入 TUN 之前就已经在源头被限制了大小,从而避免了在 VPN 出口处发生 IP 分片。

我们也发现了在全局扩展覆写配置中添加规则 IP-CIDR,10.0.0.0/8,DIRECT,no-resolve 或将内网网址加入 fake-ip-filter 无法解决这个问题的原因。只要流量进入了 TUN 网卡,它就已经被操作系统强制接管,并经过了虚拟网卡的封装。此时,该数据包在内核看来已经是“发往 TUN”的流量,它的 MTU 限制在进入 IP-CIDR 规则匹配之前就已经生效了。

正确的解决思路是结合使用 fake-ip-filterroute-exclude-address,先把内网域名解析成内网专属的 IPv4 地址,再把 10.0.0.0/8172.16.0.0/12192.168.0.0/16 之类的 IP 全都排除出 TUN 网卡,自然也就没有这么多问题了。

结尾

这篇文章前前后后写了七天。动笔的时候自己也没有完全搞明白,不过现在基本上是全想通了,也算是“以写促学”。

下面分享我自己的 Clash Verge Rev 配置,自认为优化地很不错,希望能帮到大家。

该文件删去了代理层的部分内容,请对比 Mihomo 完整配置文件后替换性导入。

下载 clash-verge.yaml