简介
逐行阅读源码,超!硬!核!的 Clash DNS 底层原理详解!
(incoming…)
实验环境
| 操作系统 | Windows 11 |
|---|---|
| 系统DNS | 192.168.31.1 |
| 网关IP | 192.168.31.1 |
| 子网IP | 192.168.31.178 |
| 代理核心 | Clash.Meta v1.15.0 |
| 引导UI | clash-verge v1.3.3 |
Protocol: tuic v5
proxies: - { name: "tunic", type: tuic, udp-relay-mode: quic, congestion-controller: bbr, alpn: ['h3', 'spdy/3.1'], reduce-rtt: True, max-udp-relay-packet-size: 1464, server: *, port: *, uuid: *, password: *, ip: * } proxy-groups: - { name: PROXY, type: select, proxies: ["tunic"] }RULE-SET 来自规则上游 Loyalsoldier/clash-rules
当正文中没有明确说明 rules 时使用如下配置:
rule-providers: # -- skip -- rules: - GEOSITE,category-scholar-!cn,PROXY - GEOSITE,category-ads-all,REJECT - GEOSITE,youtube,PROXY - GEOSITE,google,PROXY - GEOSITE,cn,DIRECT - GEOSITE,private,DIRECT - GEOSITE,steam@cn,DIRECT - GEOSITE,category-games@cn,DIRECT - GEOSITE,geolocation-!cn,PROXY - GEOIP,private,DIRECT,no-resolve - GEOIP,telegram,PROXY - GEOIP,CN,DIRECT - DST-PORT,80/8080/443/8443,PROXY - MATCH,DIRECT
实验方法
🦉实验可安全复现
分别在规则模式,全局模式,直连模式下使用预定义的
dns实验配置访问域名:api.dida365.com命中规则
geosite:cnapi.dida36768990.com一个随便敲的域名,假设它不存在。显然,这是个不存在于各个 RULE-SET 中的 target。
www.google.com从上至下,命中首条域名代理规则。
切换到 TUN 模式再测一遍
Recode DNS configuration, logger, wireshark trace
记录请求的触发顺序,阐述 clash DNS 通信的底层逻辑。
无特殊说明统一使用 clash 代称 Clash 和 Clash.Meta
实验在 Win PC 执行,而非软路由等嵌入式设备
每轮实验结束后清除所有 clash 连接,
ipconfig \flushdns清除 DNS 缓存实验中使用 clash-verge 引导 Clash.Meta 代理核心,UI 保持出厂设定
🪖需要注意:Clash DNS Object 有初始值,clash 会用硬编码的初始化参数覆盖我们缺省的字段,具体默认值可看上文的 DNS 默认配置 以及 UnmarshalRawConfig。
实验按以下模块依次行进:
- config.yaml 中的 dns 配置字段
- wireshark 抓包,过滤条件是
dns && ip.dst==192.168.31.178 - 连接日志(可省略)
配置篇
引入 default-nameserver & nameserver
💡前者用于解析后者的域名,后者用于解析我们要访问的域名。
规则模式 - 对照组 | 默认配置
仅设置 enable 字段,此时采用初始化的 DNS 配置。
dns:
enable: true可以看到,核心先并发请求 default-nameserver 中配置的四条 DNS IP 去解析 doh.pub (再使用 https://doh.pub/dns-query 这条 DOH nameserver 配置去解析 api.dida365.com)。
DOH 行进时,域名查询被封装在 HTTPS 请求中,使其本质上成为了 HTTP 协议类型的加密流量。这种类型的请求无法使用 protocol==dns 进行捕获,如 wireshark 的截图所示,看不到查询 api.dida365.com 的响应纪录。

[TCP] 127.0.0.1:64007(msedge.exe) --> api.dida365.com:443 match GeoSite(cn) using DIRECT可以得出如下阶段性结论:
访问域名时遇到直连域名规则,从本地发起 DNS 请求
api.dida365.com是一个在geosite:cn数据库中的规则,其对应了我们的出站策略 DIRECT(详见上文的 rules),也即,下面这两种情况是等价的。rules: - GEOSITE,CN,DIRECT # A - DOMAIN,api.dida365.com,DIRECT # Bdefault-nameserver 仅用来解析 nameserver 字段中配置的域名
这也说明了 default-nameserver 为什么只能写 IP 而不能写域名,这会遇到先有鸡还是先有蛋的问题。但值得一提的是,default-nameserver 支持 HTTPS 写法。
这也意味着,如果 nameserver 中没有配置域名形式的 DOH/DOT/DOQ,则 default-nameserver 对应的业务代码不会被触发。(具体情况看下文的测试)
通过 nameserver 解析我们要访问的域名
规则模式 - 仅设置 nameserver 为 DNS IP
从这里开始我们就不贴规则匹配的日志了,结果都和对照组一样。我们在仅修改 DNS 配置的情况下,同一 target 的出站策略不受影响。
dns:
enable: true
nameserver:
- 223.5.5.5行进步骤如下:
浏览器访问
api.dida365.com,请求通过系统代理进入 clash使用 nameserver 解析我们要访问的域名
可以看到我们向(人为配置的)223.5.5.5 发起了明文 DNS 请求并收到了查询响应。其中,除了我们访问的
api.dida365.com,还能看到其他驻台程序发起的 DNS 请求。显然,这与对照组的结果不一致。
具体来说 ,我们在对照组(默认配置)下使用 DOH https://doh.pub/dns-query 发送域名解析请求,这个请求是一个封装在 HTTPS 数据包中的加密申请,我们无法在过滤条件为
dns的筛选中捕获到这条解析请求。而本组实验设置中使用了未经加密的申请,也即 udp://223.5.5.5:53(行进源码自动补全为 URL),这是一条命中dns过滤规则的请求,我们不仅可以捕获到它,还能看见它的 Message/Information,即,要访问/要解析的站点。由于我们未配置域名形式的 DOH,所以 default-nameserver 相关行进代码未被触发。

规则模式 - 仅设置 nameserver 为 DOH: IP
dns:
enable: true
nameserver:
- https://223.5.5.5/dns-query行进步骤如下:
浏览器访问
api.dida365.com,请求通过系统代理进入 clash使用 nameserver 解析我们要访问的域名
我们向 IP 形式的 DOH 发起了对
api.dida365.com域名解析请求,请求被加密到 HTTPS 数据包中,通过 dns 筛选无法捕获。由于我们未配置域名形式的 DOH,所以 default-nameserver 相关行进代码未被触发。

当我们等待一段时间后,我们可以陆续收到来自系统DNS(网关)返回的 DNS 查询响应。按理说,我们的请求不是被加密了吗?要响应也应该是 HTTPS 类型的协议,再不济也应该是阿里服务器返回的响应啊?(也即
dns && ip.src==223.5.5.5)怎么会有来自本地运营商发来的 DNS 查询响应呢?这涉及到 Windows 操作系统级的 DNS 查询组策略,我们后文细说。

规则模式 - 仅设置 nameserver 为 DOH: domain
dns:
enable: true
nameserver:
- https://dns.alidns.com/dns-query是不是熟悉的感觉又回来了~🐧
行进步骤如下:
浏览器访问
api.dida365.com,请求通过系统代理进入 clash使用 nameserver 解析我们要访问的域名
与对照组的情况一直,这是个加密请求,后文不再赘述。
我们配置了域名形式的 DOH,default-nameserver 相关代码开始执行。

引入 namesever-policy
💡优先级高于 nameserver
规则模式 - 同时设置 nameserver-policy 和 nameserver
dns:
enable: true
nameserver:
- 223.5.5.5
nameserver-policy:
"api.dida365.com":
- 1.0.0.1浏览器访问
api.dida365.com,请求通过系统代理进入 clash命中规则,使用 nameserver-policy 解析域名
api.dida365.com当我们访问
api.dida365.com时优先使用 1.0.0.1 进行域名解析。除此之外所有的 DNS 请求都走 223.5.5.5,比如随后响应的cdn.dida365.com。由于 nameserver 和 nameserver-policy 都不存在域名形式的 DOH,default-nameserver 相关代码不启动

规则模式 - 混用 DOH
dns:
enable: true
nameserver:
- 119.29.29.29 # 腾讯 IPv4 DNS
nameserver-policy:
"api.dida365.com":
- https://dns.alidns.com/dns-query一路看到这你应该能自己总结行进步骤了~🐧
浏览器访问
api.dida365.com,请求通过系统代理进入 clash命中规则,使用 nameserver-policy 解析域名
api.dida365.comnameserver-policy 优先生效,里面有域名形式的 DOH,default-nameserver 启动 ;解析
api.dida365.com的 DNS 请求被加密成 HTTPS,在 dns 筛选中不显示。除此之外所有的 DNS 请求都走 119.29.29.29,比如随后响应的
cdn.dida365.com。nameserver 中没有域名形式的 DOH,default-nameserver 不启动。

规则模式 - 仅设置 nameserver-policy 为 DOH: IP
dns:
enable: true
nameserver-policy:
"api.dida365.com":
- https://223.5.5.5/dns-query浏览器访问
api.dida365.com,请求通过系统代理进入 clash命中规则,使用 nameserver-policy 解析域名
api.dida365.comnameserver-policy 生效,里面没有域名形式的 DOH,default-nameserver 不启动 ;解析
api.dida365.com的 DNS 请求被加密成 HTTPS,在 dns 筛选中不显示。其他的 DNS 请求都走 nameserver ,也即默认值
https://doh.pub/dns-query,这是一条域名形式的 DOH,default-nameserver 启动。显然,走 DOH 的 DNS 请求被加密,在 wireshark 结果中看不到 target 响应

规则模式 - 仅设置 nameserver-policy 为 DOH: domain
dns:
enable: true
nameserver-policy:
"api.dida365.com":
- https://dns.alidns.com/dns-query浏览器访问
api.dida365.com,请求通过系统代理进入 clash命中规则,使用 nameserver-policy 解析域名
api.dida365.comnameserver-policy 生效,里面有域名形式的 DOH,default-nameserver 启动 ;解析
api.dida365.com的 DNS 请求被加密成 HTTPS,在 dns 筛选中不显示。其他的 DNS 请求都走 nameserver ,结果同上一则实验一致。

引入 DNS 分流
💡你如果是一个 Clash 骨灰级玩家或是项目的核心贡献者,也许有这样的感悟:DNS 是 clash 类项目设计最为精妙也是最复杂的机制,甚至没有之一。
规则模式 - 域名规则的解析
添加出站规则,将 api.dida365.com 设为 #PROXY 出站:
rules:
# 在对照规则之前该条添加规则
- DOMAIN,api.dida365.com,PROXY
# -- skip --
- MATCH,PROXY使用 8.8.8.8 标记 api.dida365.com ,使用 223.5.5.5 标记其他请求:
dns:
enable: true
nameserver:
- https://dns.alidns.com/dns-query
- 223.5.5.5
nameserver-policy:
"api.dida365.com":
- https://dns.google/dns-query
- 8.8.8.8在开始该组实验前,我们可以根据上文实验结果提出如下假设:
<A> ✅ 如果浏览器向 api.dida365.com 发出请求,必然触发 nameserver-policy 的域名解析任务;nameserver-policy 内有域名形式的DOH,必然会使用 default-nameserver(并发)解析 dns.google ,最后使用解析到的 DOH IP 和 8.8.8.8 (并发)解析 api.dida365.com。
<B> ✅ (假设没有发生 IP 规则匹配)其他的域名直连请求使用 nameserver 进行解析,显然 nameserver 组内也有域名形式的 DOH,必然使用 default-nameserver (并发)解析 dns.alidns.com ,使用解析到的 DOH IP 和 223.5.5.5 并发查询我们要访问的域名。
抓包结果如下图所示(隐去若干条杂质数据):

🏴☠️ 我们先在这个组实验中抛出 <DNS泄漏> 的概念,具体内容后文细说。
可以得出如下观察纪录:
首先,我们能观察到 default-nameserver 开始解析 dns.alidns.com 域名,说明 nameserver 被触发,说明有其他域名直连请求命中了 DIRECT 域名规则(假设没有发生 IP 规则匹配)。
而当我们不间断地请求 api.dida365.com 时,我们为其配置的 nameserver-policy 并未生效,我们在 <A> 中假象的逻辑链没有发生,也即,clash 并未向 default-nameserver 发起针对 dns.google 的域名解析请求,或者说,本地没有发起对 api.dida365.com 的直连访问请求。
显然,我们将 api.dida365.com 设置为 PROXY 出站后,访问目标站点的工作交由远程服务器完成,自然地,域名解析的工作也由远程服务器完成。本地做的,只是将访问 api.dida365.com 的代理请求呈送至远程服务器,也即,命中 PROXY 域名规则后,clash 会直接跳过 DNS 步骤向远程服务器呈送数据包。
规则模式 - 域名规则的解析 - 验证
- 验证 <1>
dns:
enable: true
nameserver:
- 223.5.5.5
nameserver-policy:
"api.dida36768990.com":
- 8.8.8.8
rules:
- DOMAIN,api.dida36768990.com,DIRECT
# -- skip --
- MATCH,PROXY浏览器访问
api.dida36768990.com,请求通过系统代理进入 clash命中规则,使用 nameserver-policy 解析域名
api.dida365.comnameserver-policy 生效,里面没有域名形式的 DOH,default-nameserver 不启动,直接使用 8.8.8.8 解析
api.dida36768990.com。显然,这是一个不存在的域名,DNS 响应出现 no such name 错误。

- 验证 <2>
dns:
enable: true
nameserver:
- 223.5.5.5
nameserver-policy:
"api.dida36768990.com":
- 8.8.8.8
rules:
# 在对照组之前添加规则
- DOMAIN,api.dida36768990.com,DIRECT
- DOMAIN,api.dida365.com,DIRECT
# -- skip --
- MATCH,PROXY情况1:浏览器访问
api.dida365.com,请求通过系统代理进入 clash未命中 nameserver-policy,切换至 nameserver ,组内没有域名形式的 DOH,default-nameserver 不启动,直接向 223.5.5.5 发起域名解析请求(绿色记录)。
其他命中域名直连规则的请求也是用 nameserver 进行域名解析(橙色记录)。
相关 DNS 请求均未被加密,可以看到抓包记录。
情况2:浏览器访问
api.dida36768990.com,请求通过系统代理进入 clash命中 nameserver-policy,组内没有域名形式的 DOH,default-nameserver 不启动,直接向 8.8.8.8 发起域名解析请求。显然,这是一个不存在的域名,DNS 响应出现 no such name 错误(黄色记录)。

规则模式 - IP 规则的解析
首先修改出站规则,分别使用 223.5.5.5 和 180.76.76.76 标记 api.dida365.com 和 api.dida36768990.com 其余直连请求使用 119.29.29.29。这里为了 trace 纪录干净整洁便于原理分析,就不使用域名 DOH 了。
dns:
enable: true
nameserver:
- 119.29.29.29
nameserver-policy:
"api.dida365.com":
- 223.5.5.5
"api.dida36768990.com":
- 180.76.76.76
rules:
- GEOIP,CN,PROXY
- DOMAIN,api.dida36768990.com,DIRECT
- DOMAIN,api.dida365.com,DIRECT
- MATCH,PROXY浏览器访问
api.dida365.com,请求通过系统代理进入 clash遇到 GEOIP 代理规则,发起本地 DNS 查询
很多小伙们困惑的点就在这了!根据我们上文提到的结论,访问域名时遇到 IP 规则会进行DNS 解析,遇到代理规则会将请求呈送至远程服务器。那这种特殊情况到底是先进行 DNS 解析还是直接将数据包呈送到远程服务器跳过DNS解析呢?
访问域名时遇到 IP 规则,先进行本地 DNS 解析,将要请求的域名解析成 IP,再与 IP 规则集中的 IP 进行匹配,如果命中,出站规则生效,否则进入下一个规则集。
访问域名时遇到域名规则,如果是直连出站,则由本地发起 DNS 查询;如果是代理出站,则跳过 DNS 查询并将代理请求呈送至远程服务器。
命中规则,使用 namesever-policy 解析域名
api.dida365.com如下图所示,本地向 233.5.5.5 发起了针对
api.dida365.com的 DNS 请求,这验证了我们前文说的内容。

规则模式 - IP 规则的解析 - 原理剖析🏴☠️
先阅读上一节,否则你可能看不懂这里写的东西。
dns:
enable: true
nameserver:
- 119.29.29.29
nameserver-policy:
"api.dida365.com":
- 223.5.5.5
"api.dida36768990.com":
- 180.76.76.76
rules:
- GEOIP,CN,PROXY
- DOMAIN,api.dida36768990.com,DIRECT
- DOMAIN,api.dida365.com,DIRECT
- MATCH,PROXY浏览器访问
api.dida36768990.com,请求通过系统代理进入 clash遇到 GEOIP 代理规则,发起本地 DNS 查询
命中规则,使用 namesever-policy 解析域名
api.dida36768990.com组内没有域名形式的 DOH,default-nameserver 不启动,直接使用 180.76.76.76 进行域名解析。显然,我们要访问的是一个不存在的域名,自然无法得到对应的 IP(如下图绿标的 no such name信息),也即,这条通路报错了。

那么 clash 会如何处理这个请求呢?
clash ParseRule 的行进策略是 for-loop with switch-case,节取代码如下:
// 节取
for idx, line := range rulesConfig {
parsed, parseErr := ParseRule(ruleName, payload, target, params, subRules)
if parseErr != nil {
return nil, fmt.Errorf("%s[%d] [%s] error: %s", format, idx, line, parseErr.Error())
}
}// 节取
func ParseRule(ruleName, payload, target string, params []string, subRules map[string][]C.Rule) (parsed C.Rule, parseErr error) {
switch ruleName {
case "DOMAIN":
parsed = RC.NewDomain(payload, target)
case "GEOSITE":
parsed, parseErr = RC.NewGEOSITE(payload, target)
case "GEOIP":
noResolve := RC.HasNoResolve(params)
parsed, parseErr = RC.NewGEOIP(payload, target, noResolve)
case "MATCH":
parsed = RC.NewMatch(target)
parseErr = nil
default:
parseErr = fmt.Errorf("unsupported rule type %s", tp)
}
if parseErr != nil {
return nil, parseErr
}
return
}clash 从上至下逐行读取 rules,每个规则集都会进入到一个 case 子业务链路,我们这里进入的是 GEOIP 的 case,它首先会识别是否存在 no-resolve 标识,再以此决定 NewGEOIP 的具体行为。GEOIP 的规则匹配流程此处不过多赘述,感兴趣的小伙伴可以自行翻阅源码。
那么,根据我们先前的讨论,这里的 parseErr 必然是“有内容的”,也即这个错误传递到上层的 for 循环后会中断循环,使得程序不会行进到下一条 rule,也即 DOMAIN,api.dida36768990.com,DIRECT 。
换句话说,当我们试图将 api.dida36768990.com所对应的 IP 与规则集 GEOIP,CN,PROXY 进行配对时遇到了错误,这个错误首先中断匹配行进,打印错误日志,随后向上一直传导至 main 接口,沿途触发所有的 warning + 级别的日志警告。

规则模式 - IP 规则的解析 - 验证
GEOIP no-resolve only, REQUEST GET
api.dida365.com+api.dida36768990.com跳过 GEOIP 的本地域名解析,直接进入下一个规则集的匹配
dns: enable: true nameserver: - 119.29.29.29 nameserver-policy: "api.dida365.com": - 223.5.5.5 "api.dida36768990.com": - 180.76.76.76 rules: - GEOIP,CN,DIRECT,no-resolve - MATCH,PROXY
GEOIP no-resolve && DOMAIN, REQUEST GET
api.dida365.com+api.dida36768990.comdns: enable: true nameserver: - 119.29.29.29 nameserver-policy: "api.dida365.com": - 223.5.5.5 "api.dida36768990.com": - 180.76.76.76 rules: - GEOIP,CN,DIRECT,no-resolve - DOMAIN,api.dida36768990.com,DIRECT - DOMAIN,api.dida365.com,DIRECT - MATCH,PROXY跳过「访问域名遇到 GEOIP 规则本该进行的本地 DNS 解析」直接进入下一个规则集的匹配,接下来的两条直连域名规则的情况属于已讨论过的正常情况,也即,发起本地 DNS 请求。

规则模式 - 向全局过渡
修改 rules
rules: - MATCH,PROXY

这近似于全局(代理)模式,所有经由 clash 的请求都被呈递到远程服务器,DNS解析由远程服务器完成,本地扫不到经由 clash DNS 的请求纪录。
全局模式 - 全局代理,全局直连与全局拒绝
该模式下,我们可以指定所有请求都 代理/DIRECT/REJECT。其中,代理可以细分为某个代理组或某个代理节点。
代理:所有经由 clash 的请求都被呈递到远程服务器,DNS解析由远程服务器完成,本地扫不到 DNS 请求纪录。具体由哪个服务器处理请求取决于你的配置。
DIRECT:所有经由 clash 的请求都从本地发出,DNS解析行为由 dns 字段定义的策略决定,本地可以扫到未加密的纪录。

REJECT: 所有经由 clash 的请求都被甩入黑洞,此时你访问任何网站都会显示 ERR_CONNECTION_CLOSED 的警告,虽然这并不意味着你没法上网,但至少你浏览器是没法用了。🥹同全局代理,此时 DNS 配置不起作用。
如果与上一节内容作比较,你可以这样理解:
rules: - MATCH,PROXY # 全局代理(使用名为 PROXY 的代理组) - MATCH,DIRECT # 全局直连 - MATCH,REJECT # 全局拒绝Best Practices - DNS 分流配置
想啥呢🎃才开始进阶怎么会有最佳实践配置
想啥呢🐧我们还没开始 <困难> 和 章节内容的介绍~
不过,我们是时候来个阶段性的最佳实践总结了,后续介绍的内容也不会和如下结论冲突。
编排 rules 规则时,先写域名匹配再写 IP 匹配,最后
MATCH兜底。默认出站选择 PROXY 还是 DIRECT 取决于你的性癖上网习惯,进而决定选择 白名单 还是 黑名单 的编排方案。
MATCH,PROXY兜底,建议遵从如上 rules 最佳实践的前提下,先编排 PROXY 出站规则再编排 DIRECT 出站规则MATCH,DIRECT兜底,反之。
优先使用 RULE-SET,只需添加未在上游规则集中出现的规则。
尽管目前这些规则集至少有几十万条数据,但在精心设计的 Mapping 策略下,规则匹配的耗时是忽略不计的。
此外,你可能需要将这条规则
RULE-SET,applications,DIRECT添加到你的 rules 首行。当访问域名遇到 IP 规则 或 域名直连规则时,本地发起 DNS 请求。
需要注意的是,你添加的 classical RULE-SET 可能也包含域名直连规则以及 IP 规则
clash DNS Mapping
- 使用 nameserver-policy 和 nameserver 解析我们即将访问的域名,前者优先启动。
- 需要使用 default-nameserver 解析上述两个策略组中的域名,也即,default-nameserver 需要使用 IP 写法(可以是 IP 形式的 DOH),避免出现鸡蛋问题。
- 这三个策略组的网络请求是并发行进的。如果存在多个并发查询,clash 优先采用最速响应的 IP 作为域名的查询结果。
- 不必修改(覆盖)默认的 default-nameserver,而 nameserver-policy 和 nameserver 尽可能使用 IP 形式的 DOH,省去这个间接的解析步骤。
- 在设置 nameserver-policy 和 nameserver 时使用 DOH,而非 DOT/DOQ。
我们可以凭借上述特性组合出「国外DNS」和「直连DNS」的概念,你可以说这是一个精妙的巧合,但也可以说这是社区智慧的结晶。
Public DNS - 已知的 DNS 提供商
原理篇
💡Let’s analyze this systematically.
Clash flow - DNS
流程图
Clash flow - Proxy
流程图
泄漏篇
DNS 泄漏 - 概念界定
- 用户开启 VPN 后,正常网上冲浪产生的 DNS 请求被 本地 上游 ISP 捕获。
- 因此,本地上游 ISP 知道 <某人> 要解析的 <某域名> 。
DNS 泄漏 - 争议点
VPN 的 DNS 泄漏
在正常情况下,开启 VPN 后,VPN 软件将用户的「访问请求」呈送至远程服务器,访问网站的工作由远程服务器完成,也即,DNS 请求由远程服务器发出。
此时出现了一些不可抗力因素,用户向目标网站发起了直连请求,或者说,本地发起了向「系统网关 → 上游 ISP」的 DNS 查询。有可能是 VPN 软件没有劫持到这个请求,也有可能是用户配置出了问题,总之就是 DNS 请求不仅发往了 VPN 提供的 DNS ,也发往了本地 ISP。
In the case,尽管你配置了 DOH,但作为解析 DNS 请求的上游 ISP 它必然是能看到你要访问的 网址(域名),不然它没法给你查询域名对应的 IP(DNS基本原理不过多赘述)。
Proxy 的 DNS 泄漏
clash 等代理核心出现了规则模式的设定,在合理的配置下:
- 访问国内站点(如
geosite:cn),直接从本机发起请求。 - 访问我们标记的站点(如 youtube),则将请求呈送至远程服务器,代理访问。
绝大多数玩家使用的都是规则模式下的系统代理,例如:
rules:
- DOMAIN-SUFFIX,youtube.com,PROXY
- GEOSITE,CN,DIRECT
dns:
enable: true
nameserver-policy:
"geosite:cn":
- "https://223.5.5.5/dns-query"在这种常见配置下,我们直连访问国内站点(如 bilibili)所产生的 DNS 请求,必然被国内DNS 提供商捕获。这种算 DNS 泄漏 吗?我觉得不算。
根据我们上文提到的定义,DNS泄漏指的是我们使用代理访问 www.youtube.com ,却从本地发送 DNS 请求到国内 ISP 查询 www.youtube.com 对应的 IP ,因此上游知道了你曾访问油管。
显然,在配置正确的情况下不可能出现这种事故 根据我们上文的实验结论,域名请求遇到域名规则,命中规则后,如果是代理出站,我们会将访问请求呈送至远程服务器,DNS 查询由远程服务器完成。
也即,当你配置了 DOMAIN-SUFFIX,youtube.com,PROXY 这条规则时,本地不会发起对 +.youtube.com 的域名解析。(假设已禁用DNS策略组)
这与我们先前提到的 VPN 行为模式不同。我们的意图在于:
- 我们启用了规则模式,旨在避免远程服务器处理我们对国内网站的访问请求。我们希望访问国内网站而从本地发起的 「DNS 请求」经过本地运营商,以此获得更低的网络延迟。
- 不希望本地运营商捕获到我们对标记网点(如
www.youtube.com)的访问请求。
换句话说,一个 DNS 请求过来,运营商必然知道有人正在访问 www.youtube.com,而代理访问的本质在于不让运营商知道访客的真实身份。
DNS 泄漏 - 检测工具
DNS 泄漏 - 检测原理
如果你还不知道什么是 DNS 泄漏可以从前文看起,泄漏的定义大致如下图所示:
- ✅直连访问 bilibili.com ,由本地 PC 向上游 DNS 发送查询
- ✅代理访问 google.com,由远程服务器向国外 DNS 发送查询
- ❌直连访问 google.com,由本地 PC 向上游 DNS 发送查询

实验开始前,编排如下规则:
dns:
enable: true
nameserver:
- 223.5.5.5
nameserver-policy:
"ipleak.net":
- 1.0.0.1
rules:
- GEOSITE,CN,DIRECT
- GEOIP,CN,DIRECT
- MATCH,PROXY以 ipleak.net 爲例说明 DNS 泄漏检测的工作原理。
当你访问 DNS 泄漏检测的站点后,发生了如下事件:
ipleak.net后端服务器向前端页面不断推送新的数据包,这些数据包的 Request URL 都是+.ipleak.net形式,例如:
浏览器发起 Request并构造 DNS 请求查询这些域名对应的 IP
虽然这些域名是随机生成的,但它们都被正确解析到有效的后端端点,我们可以通过 DNS 查询到 Request Domain 对应的(CDN)IP。

显然,这些随机生成的域名不可能存在于任何 GEOSITE,RULE-SET 数据库中(项目维护者也不会添加这些特殊的域名)也就是说,这些访问请求进入 clash 后,走的一定是兜底的
MATCH,PROXY规则(除非你把ipleak.net这个域名配置到规则里了🥹)。我们通常会把兜底规则写在最后一行,意味着每个访问请求都会把你的 rules 扫一遍。在这个过程中,沿途遇到的首条 IP 规则(例如我们配置的
GEOIP)会触发本地 DNS 解析,「你配置的 DNS 的上游DNS」以及最终流向的「权威域名服务器」的 Server IP 会被域名检测网站记录下来,最终打印到前端页面上。访问请求最后命中
MATCH,PROXY代理出站规则,所以远程服务器必然发起 DNS 查询请求,也即,在开启代理的情况下,前端页面会打印出落地机所在区域的国旗以及落地机上游 ISP DNS 信息 。直连访问 - 正常情况
如果你没理解我刚说的东西,你可以尝试不使用代理直接访问
ipleak.net,你可以看到页面被国旗刷屏了。这些 IP 来自你选用的 DNS 以及你上游的 ISP DNS。代理访问 - 正常情况
DNS 请求由远程服务器发起,远程服务器也会有自己的系统 DNS 和上游 ISP,在没有特殊设计网关的情况下,你能看到
ipleak.net页面上不断刷出远程服务器所在地区的国旗以及由远程服务器发起的 DNS 请求经过的 server IP。如果你的远程服务器网络环境比较特殊,你会看到来自多个国家和地区的 server IP。
代理访问 - 异常情况
如果检测站点能够通过记录 DNS server IP 的方式返回发送端 PC 的真实所在地,说明发生了 DNS 泄漏。
这里加了超长的定语:
- ✅本地没有发起 DNS 查询,不可能存在泄漏
- 🚫本地发起 DNS 查询,使用系统 DNS 或国内 DNS,能够通过三要素定位还原出 zero 发送端的大致位置。例如
ipleak.net等公有服务提供的粗糙定位能够显示国旗,但显然有更精密的技术能够做到经纬度级别的定位。 - 👁️🗨️本地发起 DNS 查询,但使用的是经过特殊设计的 DNS 或其他地区的 DNS,不能或几乎不可能通过三要素定位还原出 zero 发送端的大致位置。
🌀迷思与争议:
ipleak.net出现了国旗,我的配置出现致命错误了吗?ipleak.net出现了你所在地的国旗,并不代表你访问 youtube google 的请求会被本地运营商截获。我们上文提到过(如果你使用的是本文提到的配置或是 meta 懒人配置),类似访问 youtube 的请求在 IP 规则之前必然命中 PROXY 出站规则, 本地不会产生对youtube.com的 DNS 解析请求,也就不会有被本地运营商截获这一说。如果你只关注访问 youtube google 等网站的意图数据漏没漏,那你被
ipleak.net等一众检测站点爆菊了也无伤大雅。但必须说明的是,出现了国旗,说明你向上游 ISP 发起了针对「意外站点」的 DNS 请求,ipleak 能够还原你的所在地,这属于 DNS 泄漏的范畴。 如果有某个网站借助 ipleak 等检测服务追溯发送端的大致服务区,你大概率会被精确到市区级运营商。
ipleak.net没有出现国旗,我的配置正常吗?如果你用的是 VPN,你的配置正常。但你用的是 clash,这个问题存在争议。
例如,访问
youtube.com但 rules 里出现了DOMAIN-SUFFIX,youtube.com,DIRECT这种奇怪的东西,或者提前遇到了 IP 规则,那么,本地会发起 DNS 请求。但是,如果你在 nameserver 中配置了https://security.cloudflare-dns.com/dns-query这样的国外(或其他地区)的 DOH,泄漏检测站点是无法记录到你的真实所在地的。显然,这种情况与我们界定的 DNS 泄漏有所冲突。
广义上讲,访问应该被代理的网站的 DNS 请求必须由落地代理产生,而真正的发送端被隐藏起来。如果你认为只要访问被代理的网站时,本地发起了 DNS 请求就算泄漏的话,这种情况属于 DNS 泄漏,你的配置不正常。
狭义上讲,如果你觉得尽管本地发起了 DNS 请求,但只要不被正确溯源就可以接受。那么在兼顾延迟和安全的前提下,选择经过特殊设计的境外 DOH / DOQ 是个不错的选择查看已知的免审查公有 DNS。
如何避免 DNS 泄漏
触类旁通
TUNNEL Mode
Enhanced Mode
DNSMaping
FakeIP: Clash & Meta
FakeDNS: v2ray & sing-box
L3: Loss but deepin magic
软路由 - 物理超度
一叶知秋
处理 Squad EAC 反外挂验证
解锁流媒体
绕过 ChatGPT 网络审查
天外飞仙
基于 Rust 实现 PBQ QUIC-based 拥塞控制算法
Clash.Meta 参考配置
What’s more
术语表
clash rules 直连,代理,拒绝
v2fly / sing-box rules 直连(绕过),代理,阻止
DNS
Clash DNS
- nameserver-policy
- default-nameserver
- nameserver
- proxy-server-nameserver
- fake-ip enhancer
- FQDN
- FakeIP Pool
DNS leak
概念界定:广义、狭义
DOH / DOT / DOQ / ODOH
源码阅读
初始化配置
func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
// config with default value
rawCfg := &RawConfig{
AllowLan: false,
BindAddress: "*",
IPv6: true,
Mode: T.Rule,
GeodataMode: C.GeodataMode,
GeodataLoader: "memconservative",
UnifiedDelay: false,
Authentication: []string{},
LogLevel: log.INFO,
Hosts: map[string]any{},
Rule: []string{},
Proxy: []map[string]any{},
ProxyGroup: []map[string]any{},
TCPConcurrent: false,
FindProcessMode: P.FindProcessStrict,
Tun: RawTun{
Enable: false,
Device: "",
Stack: C.TunGvisor,
DNSHijack: []string{"0.0.0.0:53"}, // default hijack all dns query
AutoRoute: true,
AutoDetectInterface: true,
Inet6Address: []LC.ListenPrefix{LC.ListenPrefix(netip.MustParsePrefix("fdfe:dcba:9876::1/126"))},
},
TuicServer: RawTuicServer{
Enable: false,
Token: nil,
Users: nil,
Certificate: "",
PrivateKey: "",
Listen: "",
CongestionController: "",
MaxIdleTime: 15000,
AuthenticationTimeout: 1000,
ALPN: []string{"h3"},
MaxUdpRelayPacketSize: 1500,
},
EBpf: EBpf{
RedirectToTun: []string{},
AutoRedir: []string{},
},
IPTables: IPTables{
Enable: false,
InboundInterface: "lo",
Bypass: []string{},
},
DNS: RawDNS{
Enable: false,
IPv6: false,
UseHosts: true,
IPv6Timeout: 100,
EnhancedMode: C.DNSMapping,
FakeIPRange: "198.18.0.1/16",
FallbackFilter: RawFallbackFilter{
GeoIP: true,
GeoIPCode: "CN",
IPCIDR: []string{},
GeoSite: []string{},
},
DefaultNameserver: []string{
"114.114.114.114",
"223.5.5.5",
"8.8.8.8",
"1.0.0.1",
},
NameServer: []string{
"<https://doh.pub/dns-query>",
"tls://223.5.5.5:853",
},
FakeIPFilter: []string{
"dns.msftnsci.com",
"www.msftnsci.com",
"www.msftconnecttest.com",
},
},
Sniffer: RawSniffer{
Enable: false,
Sniffing: []string{},
ForceDomain: []string{},
SkipDomain: []string{},
Ports: []string{},
ForceDnsMapping: true,
ParsePureIp: true,
OverrideDest: true,
},
Profile: Profile{
StoreSelected: true,
},
GeoXUrl: RawGeoXUrl{
Mmdb: "<https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/country.mmdb>",
GeoIp: "<https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.dat>",
GeoSite: "<https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite.dat>",
},
}
if err := yaml.Unmarshal(buf, rawCfg); err != nil {
return nil, err
}
return rawCfg, nil
}DNSMode Alias
func (e DNSMode) String() string {
switch e {
case DNSNormal:
return "normal"
case DNSFakeIP:
return "fake-ip"
case DNSMapping:
return "redir-host"
case DNSHosts:
return "hosts"
default:
return "unknown"
}
}type RawDNS struct
type RawDNS struct {
Enable bool `yaml:"enable"`
PreferH3 bool `yaml:"prefer-h3"`
IPv6 bool `yaml:"ipv6"`
IPv6Timeout uint `yaml:"ipv6-timeout"`
UseHosts bool `yaml:"use-hosts"`
NameServer []string `yaml:"nameserver"`
Fallback []string `yaml:"fallback"`
FallbackFilter RawFallbackFilter `yaml:"fallback-filter"`
Listen string `yaml:"listen"`
EnhancedMode C.DNSMode `yaml:"enhanced-mode"`
FakeIPRange string `yaml:"fake-ip-range"`
FakeIPFilter []string `yaml:"fake-ip-filter"`
DefaultNameserver []string `yaml:"default-nameserver"`
NameServerPolicy map[string]any `yaml:"nameserver-policy"`
ProxyServerNameserver []string `yaml:"proxy-server-nameserver"`
}DNS 默认配置
Clash.Meta/config/config.go MetaCubeX/Clash.Meta
DNS: RawDNS{
Enable: false,
IPv6: false,
UseHosts: true,
IPv6Timeout: 100,
EnhancedMode: C.DNSMapping,
FakeIPRange: "198.18.0.1/16",
FallbackFilter: RawFallbackFilter{
GeoIP: true,
GeoIPCode: "CN",
IPCIDR: []string{},
GeoSite: []string{},
},
DefaultNameserver: []string{
"114.114.114.114",
"223.5.5.5",
"8.8.8.8",
"1.0.0.1",
},
NameServer: []string{
"<https://doh.pub/dns-query>",
"tls://223.5.5.5:853",
},
FakeIPFilter: []string{
"dns.msftnsci.com",
"www.msftnsci.com",
"www.msftconnecttest.com",
},
}type Pool struct
// Pool is an implementation about fake ip generator without storage
type Pool struct {
gateway netip.Addr
first netip.Addr
last netip.Addr
offset netip.Addr
cycle bool
mux sync.Mutex
host *trie.DomainTrie[struct{}]
ipnet *netip.Prefix
store store
}GEOSITE,GEOIP 大小写不敏感
// 将国家代码转成小写后再匹配
func LoadGeoSiteMatcher(countryCode string) {
// -- skip --
countryCode = strings.ToLower(countryCode)
// -- skip --
}
// LAN 特例,必须大写才能匹配规则
func LoadGeoIPMatcher(country string) {
// -- skip --
country = strings.ToLower(country)
// -- skip --
}
// ---
countryCode = strings.ToLower(countryCode) 换句话说,以下写法是等价的:
rules:
- GEOSITE,PRIVATE,DIRECT
- GEOSITE,private,DIRECT
rules:
- GEOIP,cn,DIRECT
- GEOIP,CN,DIRECTnew tuic
常见问题汇总
- Windows DNS 策略组问题
- 使用 clash 一顿操作连不上公司内网
