从底层原理开始讲清楚DNS泄露,小白也能看懂【已完结】

之前就在L站看过不少讲dns泄露的帖子,但发现大多是教了怎样做,而没有讲为什么要这样操作,导致出了问题也不知道如何调整相关操作,我也一直困惑于此。最近看完了油管博主 不良林 DNS相关的视频,自己做了些笔记,顺便发个帖子,希望能帮助的和我有类似困惑的朋友。

本文使用的代理软件是clash verge rev 1.6.6。

前置知识

计算机网络五层结构

20240922_174759_219_copy

系统代理

启用系统代理后,遵循系统代理规则的软件就会将访问网络的请求交给 clash。

系统代理只是一种行业内的非强制性“约定”,并非所有程序都遵守,完全靠软件开发者自发。实际上除了浏览器,很多软件都不走系统代理。

打开clash的系统代理,当我们在浏览器中访问网页发起HTTP请求后(比如访问 google.com ),访问 google.com 请求就被交给 clash。clash 会先根据分流规则判断是否需要走代理,这里访问的是 google,显然需要走代理。于是 clash 将收到的数据进行加密封装后,这里clash运行在网络协议中的应用层,再加上传输层的头、网络层的头等,然后经过家里的路由器,最终发到代理服务器(即富强的节点)。

之后就由节点来完成访问谷歌网页的过程,并作为我们和谷歌的中转,我们和谷歌之间的双向数据传输都要通过刚刚的路径。于是借助节点,完成了我们访问外网的需求。

TUN模式

系统代理小节讲过,实际上很多软件不走系统代理,那么TUN模式就派上用场了。

TUN模式的原理是,代理程序(clash)会创建一张虚拟网卡,将所有网络请求重定向到这张虚拟网卡,代理程序从虚拟网卡中读取并处理这些网络请求。

TUN模式工作在网络层,这里的“所有网络请求”指的即是在网络层接管所有网络流量。由于路由器也是工作在网络层的设备,所以通过软路由的方式同TUN模式类似(下面的示意图就是以软路由器为例,所以可以看到小猫咪是在路由器的位置,而不是电脑里)。

打开clash的TUN模式,当我们在浏览器中访问网页发起HTTP请求后(比如访问 google.com ),访问 google.com 请求会按照正常流程根据协议栈进行封装,先后加上传输层的头、网络层的头,由于clash会接管网络层所有流量,于是流量又来到了clash。之后还是根据分流规则判断是否走代理。假设判断走代理,clash处理完加密等操作后,流量就从clash的虚拟网卡流向物理网卡,后面的过程就和平时上网一样了。

DNS的过程

本机向互联网请求外部网页时,比如访问 baidu.com ,需要将 baidu.com 这种方便人类阅读的域名转换成 baidu.com 网页所在的服务器在互联网中的真正的地址,也就是我们常说的ip地址。而DNS协议就是完成这一转换的。

本机的浏览器访问 baidu.com 时,在检查浏览器DNS缓存和操作系统DNS缓存(对应Windows系统中的hosts文件)中不存在 baidu.com 的项后,便会发出一条DNS查询请求。如果接下来经过的所有DNS服务器中都不存在 baidu.com 的DNS缓存,这条DNS查询请求会先后经过家里的路由器(一般是本机的默认DNS服务器)、运营商DNS(比如中国电信)、中间一系列DNS服务器(运营商一般会配置多个上游DNS服务器),最终到达baidu权威DNS服务器(看名字也知道这台DNS服务器能够解析 baidu.com )。baidu权威DNS服务器便会把含有 baidu.com 对应的ip地址的数据包原路返回传输给本机,拿到这个ip地址后你就能够访问 baidu.com 了。原路返回的过程中,经过的那些DNS服务器也会缓存下来baidu的这条 dns 查询结果,下次查询时dns查询请求就不需要跑那么远一直跑到权威DNS服务器。

DNS泄露

什么是DNS泄露

当开启系统代理访问 google.com 时,浏览器的请求被 clash 接管,之后 clash 会按照分流规则来处理这条请求,此时分两种情况:

一种情况是有类似 DOMAIN-SUFFIX, google.com, 节点1 这种分流规则,这种情况匹配到规则后,就会直接把由远程的代理服务器来接管所有的网络请求相关的操作(包括DNS查询)。

另一种情况,没有上面说的那种规则,一般的机场配置文件的rules(即分流规则)部分的最后两行一般是:

 - GEOIP,CN,DIRECT
 - MATCH,代理节点1

这种情况,前面没有能够匹配 google.com 的规则,那最终就会来试图匹配这条规则 GEOIP,CN,DIRECT 。这条规则的意思是,遇到中国ip就走直连不走代理,google.com 是域名但这里需要的是ip地址,所以当匹配到这条规则时会使用本机默认的DNS服务器进行DNS查询。由于这个DNS查询是没有经过我们节点的代理的,DNS又是明文,这样我们的运营商便知道了我们试图访问 google.com

最后 GEOIP,CN,DIRECT 这条规则没有匹配上(因为google.com的ip不是中国ip。其实由于GFW的存在google.com的DNS结果会被污染,不会拿到真正的google的ip,但这里被污染的ip一般也是非国内的ip),访问google的请求会被最后一行 MATCH,代理节点1 匹配,所以访问 google 还是能够走代理,我们能正常用google(代理节点会重新进行DNS,拿到真正的、未被污染的google的ip)。但由于那个DNS查询,运营商知道了我们想访问google,这就是我们常说的 DNS泄露

虽然一般人也不干什么坏事知道也没什么,但我们搭了代理本是想啥都通过代理访问,这咋有个环节漏了,直接通过运营商走了,多少有点不爽。

ipleak等检测DNS泄露的网站的原理

听过DNS泄露这个词的朋友肯定也听过 ipleak 这个网站,这个网站可以检测是否有 DNS泄露发生,下面讲一下检测的原理。

当访问 ipleak.net 时,这个网站会在后台发送一系列dns请求,并且每个请求的域名都不一样。这些域名实际上都是完全随机生成的,因此可以保证两点:①域名是唯一的;②域名不会在一系列中间dns服务器的缓存中存在,这保证了DNS查询请求可以最终到达ipleak权威DNS服务器。基于以上两点,ipleak便能够定位到哪些dns查询是你这台电脑请求的。

比如我访问这个网站时随机生成了一个12138.ipleak.net,发送了一条DNS查询请求,最终发送到了ipleak权威DNS服务器,由于这个12138是这我电脑上生成的,独一无二的,那么ipleak就知道这条是我这台电脑发起的请求。实际上,ipleak的权威DNS服务器会记录下DNS查询请求的DNS服务器的ip地址(在整条DNS查询链路中则对应着倒数第二个DNS服务器),这个信息也会同步给ipleak网站,这样,ipleak便知道了你请求的DNS服务器的ip地址。由于一般会有多个上游DNS服务器,所以ipleak也不止随机生成一个地址进行DNS请求,经过多次这样的DNS请求,ipleak就会得到你所有的上游DNS服务器的ip地址。

于是这个网站就会把这些DNS服务器的ip地址都列出来,如果你有DNS泄露,那么肯定会在这列出来的ip里看到中国ip,这就说明发生了DNS泄露。如果没有漏的话,就是所有DNS请求都走的代理服务器,一般会是和你节点的ip的位置一致,比如香港节点,那这些ip也是香港的。

Netflix等网站的检测

奈飞这种对地区要求高的网站也可能在后台偷偷发送DNS请求,这样如果存在DNS泄露,负责跟奈飞权威DNS服务器对话的那台DNS服务器的ip的归属地就会和当前访问奈飞网站的代理节点的ip不是同一个地区,就会判定你在使用代理工具。导致无法观看视频甚至直接封号。

试图解决DNS泄露

接着“什么是DNS泄露”这一小节继续。

配置好分流规则

刚刚我们在讲述 DNS泄露时提到,只有在没有 DOMAIN-SUFFIX, google.com, 节点1 这种分流规则时,才会发生这种情况,那么只要将我们平时要访问的外国网站都配置好分流规则,这个问题就可以解决了。

但其实这还是不完美的,毕竟我们有可能某天就会访问一个新外国网站,这样就需要每次都要手动去修改规则。所以这个解决方法适合平时主要访问国内网站比较多、访问国外网站基本上就是那么固定的几个的人。

no-resolve

我们可以在 GEOIP,CN,DIRECT 这条规则后加一个 no-resolve 关键字,就变成GEOIP,CN,DIRECT,no-resolve ,这个关键字的作用是,当触发 dns 解析来检查域名的 目标IP 是否匹配规则时,加上 no-resolve 后就可以跳过 dns 解析。

这样确实可以解决上面提到的DNS泄露问题,因为直接没有那一步DNS解析了嘛。但是这样做会带来一个新的问题。

我们把上面的例子中的 google.com 换成 baidu.com ,当 baidu.com 遇到 GEOIP,CN,DIRECT,no-resolve 这条规则时,由于是 baidu.com 是域名,就不进行DNS解析了。

由于直接跳过了 GEOIP 这个规则,最终 baidu.com 会被最后一行的 MATCH 匹配。但是我们访问 baidu.com(国内的网站)是不需要走节点的,走节点既会让上网速度变慢,还会多用很多流量。当然,我们可以给国内经常访问的那些网站手动添加直连规则,这样就不会匹配到倒数第二行的 GEOIP 规则(规则将按照从上到下的顺序匹配,列表顶部的规则优先级高于其底下的规则)。

由于需要手动配置国内网站的直连规则,所以这个解决方法适合平时访问国外网站较多的人。

油管博主不良林的视频(https://youtu.be/fqREM6b25SY?list=PL5TbbtexT8T3JJdJAy73A0T2NXZL2JEJY&t=773 ,点开视频链接即可跳转到对应位置)中提供了一个订阅转换链接的方式,实现了 所有ip规则后面加no-resolve 和 手动添加了大部分国内网站的DIRECT规则。 但如果遇到国内小众网站并不在添加之列的话,这些网站还是会走代理节点。

透明代理下的DNS泄露

透明代理,透明的意思是什么?我的理解是,在开启系统代理时,浏览器会按照程序写好的规则,将原本直接要发送的请求转交给系统代理。而在TUN模式下,clash 会创建一张虚拟网卡,这张虚拟网卡会接管所有流量,而这时浏览器还是像往常没有开启TUN模式下的动作一样,把流量交给网卡就可以了。对于浏览器(或其他流量,比如游戏的udp流量),它们还是跟往常一样把流量交给网卡,它们并不知道 clash 干了什么,对它们来说并不知道之后的流量会经过代理。

上面讲的都是在系统代理的情况下,软路由/TUN模式/手机上的clash(这三种情况类似),又有所不同。

一些前置知识

根据TCP/IP 协议中所说的,「在应用发起 TCP 连接时,会先发出一个 DNS question(发一个 IP Packet),获取要连接的服务器的 IP 地址,然后直接向这个 IP 地址发起连接」。TUN模式下应用程序是不会感知到代理客户端的存在的,会正常发起 TCP 连接,所以在拿到 DNS 解析结果之前,连接是不能建立的。

一般代理工具会劫持所有53端口(53是DNS协议的默认端口)的请求,DNS是由代理工具完成的(上面讲过的系统代理是由本地DNS进行解析),也就是说 IP 是由代理工具提供的。这个ip可以是真实解析再提供(即redir-host模式),也可以是提供一个假的 IP 骗终端先发起连接(即fake-ip模式)。

redir-host模式

访问 google.com 浏览器首先发起DNS请求试图获取google的ip,如上所述DNS请求的数据包会被clash的虚拟网卡截获,需要先拿到一个IP才可以发起连接。在redir-host模式下,clash会返回给浏览器一个真实ip,所以clash会向配置文件(配置如下,nameserver即是为clash虚拟网卡配置的DNS服务器)中写好的两个上游DNS服务器同时发起DNS请求,谁最先返回就用谁的结果。

dns:
  enable: true
  enhanced-mode: redir-host
  nameserver:
  	- 114.114.114.114
  	- 223.5.5.5

假设 233.5.5.5 返回了一个被污染过的ip地址5.5.5.5,clash会维护一个域名-ip的映射表,查到了 google.com 的ip是5.5.5.5后会存储在映射表中,然后将DNS查询结果返回给浏览器。

浏览器拿到google.com的ip后,就可以发起请求了,请求到达clash后,clash根据ip(5.5.5.5)查询到对应的域名是 google.com(为什么需要查询?),然后进行分流,假设存在 DOMAIN,goolge.com, 代理节点1 这样的规则,那么这个请求后续就由代理节点1来代理了。后续代理节点1会重新进行DNS查询,来获得未被污染的google的ip。

由于必须返回一个真实的ip后,浏览器才能发起连接,所以必须会进行一次DNS查询,那就必然会产生DNS泄露。

redir-host模式的另一个问题

假设浏览器又试图访问 youtube.com,流程和访问 google.com 类似,查询到的被污染的ip也是5.5.5.5,也存储在了映射表中。但这次查询映射表时,对应的域名就有两个了,clash也不知道这次浏览器需要访问的到底是google.com还是youtube.com

HTTP请求中由youtube.com这个域名,但由于clash没有类似v2ray流量探测的功能,所以无法得知这个域名,只能通过映射表去查询。

改进后的redir-host

为了解决这个问题,redir-host模式不再进行远程DNS,而是在clash这里完成DNS后,将得到的5.5.5.5这个ip地址直接传输给节点,这样就不需要查看映射表。但是又会有另一个问题——5.5.5.5是被污染过的ip,并不是真正的google的ip,拿到这个ip的代理节点没法访问到google。

这时,我们可以修改上面的dns配置:

dns:
  enable: true
  enhanced-mode: redir-host
  nameserver:
  	- 114.114.114.114
  	- 223.5.5.5
  fallback:
  	- https://1.1.1.1/dns-query

fallback下主要放境外的、不会被污染的dns服务器,加入fallback后,当clash发起dns请求时,nameserver和fallback中的所有dns服务器会并发查询,当查询的ip的归属地不是中国(cn)时,则会使用fallback响应的结果,如果是cn则使用nameserver的结果。

使用国外加密的DNS服务器加上fallback,这样就解决了上面提到的拿到被污染的ip的问题。国内的域名也会使用国内的DNS服务器请求的结果。

当fallbakc不为空时,会默认启用 fallback-filter (即fallback-filter 的 geoip 项为 true),且geoip-code为 cn 。所以上面的配置等价于:

fallback:
  - https://1.1.1.1/dns-query
fallback-filter:	
  - geoip: true
  - geoip-code: CN

geoip为true代表启用geoip判断,geoip-code 为 CN 意思是,除了 中国 IP,其他的 IP 结果会被视为污染,这些ip会采用 fallback结果。

所以只用加上fallback就可以实现“当查询的ip的归属地不是中国时就使用 fallback 中的响应”。

另外,如果你怕geoip的数据库不全,这里其实还可以自己手动添加一些域名(使用domain)或者ip段(使用ipcidr):

fallback:
  - https://1.1.1.1/dns-query
fallback-filter:	
  geoip: true
  geoip-code: CN
  domain:
    - '+.google.com'
    - '+.youtube.com'
  ipcidr:
  	- 240.0.0.0/4

但是这里实际上只解决了ip污染的问题,实际上是会发生DNS泄露的。因为fallback和nameserver是并发请求,假设请求的域名的ip是外国ip,那实际上这个dns请求还是被发给了nameserver中的国内DNS服务商,只是最后实际使用的是fallback的请求。

其实fallback中还可以加上 geosite 。

  fallback-filter:
    geoip: true
    geoip-code: CN
    geosite:
      - gfw
    ipcidr:
      - 240.0.0.0/4
    domain:
      - '+.google.com'
      - '+.youtube.com'

geosite 列表的内容被视为已污染,匹配到 geosite 的域名,将只使用 fallback解析,不去使用 nameserver。这样就能够避免在 gfw 内的域名向nameserver中的中国DNS服务器发起DNS请求,从而避免DNS泄露。

nameserver-policy 和 fallback-filter

使用 geosite: -gfw 后,会在 clash verge rev 中看到 “使用nameserver-policy代替geosite(geosite将在未来移除)” 这样的提示:

image-20240926190226945

貌似官方更推荐使用 nameserver-policy 来代替 fallback 的功能。

fallback的功能上面已经讲过了,如果没有配置 geosite ,会并发同时请求 fallback 和 nameserver ,根据geoip数据库判断是否为中国ip,以此判断是采用 fallback 还是 nameserver 的结果。配置了 geosite 的话,则遇到 gfw 内的域名时只会请求 fallback ,不使用 nameserver。

而 nameserver-policy 的功能就要更强大一些。nameserver-policy 加入了命中时要交给哪个 DNS 服务器解析的逻辑,也可以延伸出更多更个性化的 DNS 分流,而不像 fallback-filter 一样,只能区分是否要用 fallback 解析比如实现同样的功能。

以下面的配置(为方便讲解配置有简化,无法直接使用)为例:

dns:
  nameserver:
  - tls://8.8.4.4
  - tls://1.1.1.1
  nameserver-policy:
    +.notion.com: tls://dns.jerryw.cn
    geosite:cn:
    - 223.5.5.5
    - 114.114.114.114

nameserver-policy 可以直接指定域名查询的解析服务器,优先于 nameserver/fallback 查询 ,也可使用 geosite。例子中实现的就是访问 notion 的域名使用 tls://dns.jerryw.cn 这个DNS服务器,访问中国地区的域名使用 233.5.5.5 和 114 这两个国内DNS来解析。而剩下的没匹配到的国外域名则交给 nameserver 中的两个国外DNS来解析。

为什么是上面用的是 geosite:gfw,这里用的是 geosite:cn?geosite 具体有哪些类别?

答:原本 geosite.dat 类别:domain-list-community/data at master · v2fly/domain-list-community · GitHub
另外,本项目(GitHub - Loyalsoldier/v2ray-rules-dat: 🦄 🎃 👻 V2Ray 路由规则文件加强版,可代替 V2Ray 官方 geoip.dat 和 geosite.dat,适用于 V2Ray、Xray-core、mihomo(Clash-Meta)、hysteria、Trojan-Go 和 leaf。Enhanced edition of V2Ray rules dat files, applicable to V2Ray, Xray-core, mihomo(Clash-Meta), hysteria, Trojan-Go and leaf. )中 geosite.dat 除了包含原本的所有类别,也包含 README 里新增的类别。

再谈 redir-host 模式

TUN模式的redir-host模式下,不进行远程DNS(远程DNS即,由代理节点进行DNS解析),所有的DNS都从本地发起,但使用的又是境外加密 DNS服务器,这样获取到的ip是与本机地理位置相近的CDN服务器,而实际上我们访问外网实际上使用的是代理节点,代理节点的地理位置一般较远。

再说简单一点就是,由于DNS是在中国请求的,DNS请求到的ip地址离中国很近,但是后续访问这个ip地址是由代理节点去访问的,这样获取的就不是最优的ip地址。

此外,国内直接访问国外的DNS服务器,速度较慢。带加密的DNS服务器,加密又会增加延迟。

由于 redir-host 问题确实太多,redir-host已经被官方弃用,现在基本都是用fake-ip模式。看了我自己的几个订阅,配置文件中 dns 的 enhanced-mode 项也都是配置为fake-ip。

fake-ip模式

通过之前的讲解我们知道,在 redir-host 模式下,每次需要先进行DNS请求拿到ip后才能建立连接。

假设请求的域名是外国域名,对于改进前的redir-host,拿到的如果是什么ip无所谓,就算是被污染的ip(5.5.5.5),但总是要查映射表转换成域名 google.com 的,最终还是要通过远端的代理来进行DNS。

对于改进后的redir-host,不再进行远程DNS,直接在本地DNS后把拿到的ip给代理节点,如果使用的是国外的加密DNS服务器速度会很慢,如果使用国内DNS服务器有被污染的可能。等待DNS完成后才能发起连接。

于是就可以想到,或许可以先生成一个假的ip先建立连接,fake-ip就诞生了。

访问 google.com 浏览器首先发起DNS请求试图获取google的ip,TUN模式下需要先拿到一个IP才可以发起连接,在 fake-ip 模式下,clash会先返回一个假的IP(随机生成,保证ip能和域名一一对应,会建立一个域名-ip映射表。具体返回的ip范围由fake-ip-range参数决定,一般使用私有ip段)给终端,让终端能够发起连接。终端发起请求后,通过查找映射表知道要访问的是 google.com ,查询路由规则匹配了DOMAIN规则,于是走代理节点访问。后续也由远程的代理节点完成DNS解析。

fake-ip 模式的一些小问题

由于fake-ip毕竟是假的ip,在某些情况下还是会出现一些小问题。

比如访问baidu时缓存了假的ip,一段时间后clash意外退出,而电脑中换成的假ip还未过期,再次访问baidu时就会出错。虽然clash讲dns响应中的ttl值设置成1s,但应用程序并不一定会遵循响应中的ttl值。

还有一些程序会开启DNS重绑保护,当识别到获取的是一个私有ip,则认为出现了非法DNS劫持而被丢弃。比如这就会造成Windows系统显示无网络,可以通过配置fake-ip-filter参数解决。

实操解决DNS泄露

终于,讲解了这么多原理,终于到了实操解决问题的阶段,想必大家经过前面的讲解,心里也多少有一些解决问题的思路了。实操的第一步,我们来明确一下我们的基本目标,我们当然是想,既让正常上网不太受影响(原本能快速访问的国内网站的速度不能变慢太多),又能防止DNS泄露。

以下内容是在fake-ip模式下的解决方案,在跟着以下解决方案做之前,还需要完成最后一小节中的“一些其他操作”,这样才能在最终的演示中不发生DNS泄露。

方案一:黑名单 + 自主配置DNS

可以看到,fake-ip模式下,如果配置了DOMAIN规则,那本地设置的DNS就完全不会参与。有没有发现,“查找映射表知道要访问的是google.com”后,就和系统代理下是一样了!同样是查询路由规则,然后匹配了就走代理节点访问。

因此,要解决DNS泄露,按照之前讲过的思路之一,可以配置国外域名的黑名单,尽量覆盖国外域名,让它们走节点,本地设置的DNS完全不会用到,自然谈不上DNS泄露。

如果是没有被domain覆盖到的国外域名,那还是会通过本地设置的DNS泄露。但我们可以使用前面讲过的DNS配置:

dns:
  nameserver:
  - tls://8.8.4.4
  - tls://1.1.1.1
  nameserver-policy:
    geosite:cn:
    - 223.5.5.5 # aliyun DNS
    - 114.114.114.114

这样未被黑名单覆盖的域名就交给了加密国外DNS服务器来解析,可以避免泄露。

我们再来看看国内域名,一般规则中也会有国内域名直连的规则,那么就分两种情况。第一种情况是被直连规则匹配了,那就直接走直连了。第二种情况是,没匹配,最终走了GEOIP,CN规则,这时便会查询一次DNS,由于是国内域名,nameserver-policy 中 geosite:cn 决定了它会走我们设置好的国内的DNS服务器。可见国内网站的访问速度也不会太受影响。

如何配置clash的dns

这里给出我自己完整的DNS配置:

dns:
  enable: true
  ipv6: false
  listen: 0.0.0.0:53
  enhanced-mode: fake-ip
  fake-ip-range: 198.18.0.1/16
  nameserver:
  - tls://8.8.4.4
  - tls://1.1.1.1
  prefer-h3: true
  nameserver-policy: # 国内的DNS服务器没必要使用doh/dot加密,不加密速度更快
    geosite:cn:
    - system
    - 223.5.5.5
    - 114.114.114.114
    - 180.184.1.1
    - 119.29.29.29
    - 180.76.76.76
  proxy-server-nameserver: # 解析代理节点的DNS服务器
  - tls://8.8.4.4
  fallback: []
  fake-ip-filter: # fake-ip的副作用,导致Windows上会显示无网络,屏蔽微软的一些域名即可解决
  - dns.msftncsi.com
  - www.msftncsi.com
  - www.msftconnecttest.com

由于一般不直接修改配置文件,所以我再放上我自用的用来修改DNS的脚本(如何使用clash的脚本,可以看我之前写过的这个帖子 每次使用 ChatGPT 都要切换全局模式?手把手教你怎样给 OpenAI 配置 Clash 分流规则【纯小白向】 )。

function main(content) {
  const isObject = (value) => {
    return value !== null && typeof value === 'object'
  }

  const mergeConfig = (existingConfig, newConfig) => {
    if (!isObject(existingConfig)) {
      existingConfig = {}
    }
    if (!isObject(newConfig)) {
      return existingConfig
    }
    return { ...existingConfig, ...newConfig }
  }

  // 删除原来的配置文件中 dns的 fallback 和 fallback-filter 项(已过时)
  delete content.dns.fallback
  delete content.dns['fallback-filter']

  const cnDnsList = [
    'system',
    '114.114.114.114', 
    '223.5.5.5', // 阿里
    '119.29.29.29', // 腾讯
    '180.184.1.1', // 字节
    '180.76.76.76' // 百度
  ]

  const foreignDnsList = [
    'tls://8.8.4.4',
    'tls://1.1.1.1',
  ]

  const dnsOptions = {
    'enable': true,
    'nameserver': foreignDnsList, // 默认的域名解析服务器
    'nameserver-policy': {
      'geosite:cn': cnDnsList
    },
    'proxy-server-nameserver': [
      'tls://8.8.4.4',
    ],
  }

  // 用更精简的GEO数据库代替原版的GEO数据库,下面的操作是更换GEO数据库的下载地址
  // 原因详见这篇博客(https://lainbo.dev/clash-config)的“解决 GEOIP, CN 问题”这一小节

  // GitHub加速前缀
  const githubPrefix = 'https://fastgh.lainbo.com/'

  // GEO数据GitHub资源原始下载地址
  const rawGeoxURLs = {
    geoip: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip-lite.dat',
    geosite: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat',
    mmdb: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country-lite.mmdb',
  }

  // 生成带有加速前缀的GEO数据资源对象
  const accelURLs = Object.fromEntries(
    Object.entries(rawGeoxURLs).map(([key, githubUrl]) => [key, `${githubPrefix}${githubUrl}`]),
  )

  const otherOptions = {
    'unified-delay': true,
    'tcp-concurrent': true,
    'profile': {
      'store-selected': true,
      'store-fake-ip': true,
    },
    'sniffer': {
      enable: true,
      sniff: {
        TLS: {
          ports: [443, 8443],
        },
        HTTP: {
          'ports': [80, '8080-8880'],
          'override-destination': true,
        },
      },
    },
    'geodata-mode': true,
    'geo-auto-update': true,
    'geo-update-interval': 24,
    'geodata-loader': 'standard',
    'geox-url': accelURLs,
    'find-process-mode': 'strict',
  }
  content.dns = mergeConfig(content.dns, dnsOptions)

  return { ...content, ...otherOptions }
}

为什么我的国内DNS服务器不使用doh/dot,而是直接用普通的无加密的udp,例如 233.5.5.5 。因为这样速度更快。

而且我使用国外加密DNS来兜底,对于国外域名都会走国外加密DNS服务器;即使有国内域名既没有被路由规则中的直连规则匹配,又没有被 nameserver-policy 的 geosite:cn 规则匹配,那也只是走国外的DNS,速度慢一点而已(还是没法解析的话可以手动添加一下规则)。这样配置能够保证绝大多数国内域名走国内DNS服务器。

最终,我们来看看这个方案访问 DNS Leak Test - BrowserLeaks 的结果。这里我使用的代理节点是香港节点。可以看到完全没有境内ip。

但是这个方案可能无法通过netflix网站的检测,为什么呢?相信看到这里读者应该也可以自己知道答案了。因为如果netflix没有被DOMAIN规则覆盖,那最终会通过nameserver服务器发起DNS请求,假设现在走的是香港节点,但8.8.4.4这个DNS服务器地理位置在美国,自然就识别到你在使用代理了。

如果想实现完全由代理节点来请求DNS,可以看方案二。

但是方案二会让一些小众国内网站(未被rule-set中的直连规则覆盖到)走代理,我自己平时主要访问国内网站,我也不访问netflix这种网站,所以我用的是方案一。

方案二:no-resolve + 配置国内域名直连

这个方案的思路就是,将所有ip相关的规则全部加上 no-resolve ,这样国外的域名即使没有被DOMAIN规则匹配,在遇到IP类规则时也不会进行DNS解析,直到最终被最后一行的MATCH匹配,走代理节点。

但是我们肯定也不希望国内的域名都走节点,这样既使上网速度变慢,又费我们机场的流量,所以我们还需要配置国内域名直连的规则。

这种方案下,只有被直连规则匹配的国内域名才会走直连,才会用到本地DNS,其他情况下DNS请求全部是由代理节点发起的。分析如下:对于国外域名,全部走代理节点,DNS也是由代理节点来发起。对于国内域名,没有被直连规则匹配的会走代理节点,DNS也是代理节点发起;被直连规则匹配的就走直连,会使用本地DNS。

因此,netflix这种网站,会走代理,DNS也是代理发起,便不存在DNS泄露导致的问题。

如何修改clash的路由规则

下面放出我自己在用的配置路由规则的脚本(如何使用clash的脚本,可以看我之前写过的这个帖子 每次使用 ChatGPT 都要切换全局模式?手把手教你怎样给 OpenAI 配置 Clash 分流规则【纯小白向】 )。主要关注规则部分,前面我放了一些自己常用的网站的规则,比如linuxdo系列、AI系列。最后一部分是RULE-SET规则,包含直连规则和走代理的规则。可以注意到,所有ip类规则(包括含有IP类规则的RULE-SET),我都在后面加上了 no-resolve

function addRules(params, USGroupName, JPGroupName, SGGroupName, HKGroupName, ProxyGroupName) {
  // 设置openai用哪个节点访问
  const forOpenai = "myGPT";

  // 创建一个规则数组
  const rules = [

    // linux.do
    //"DOMAIN-SUFFIX,linux.do,DIRECT",
    "DOMAIN-SUFFIX,linux.do," + ProxyGroupName,
    "DOMAIN-SUFFIX,oaifree.com,DIRECT",
    "DOMAIN-SUFFIX,oai2b.com,DIRECT",
    "DOMAIN-SUFFIX,oaipro.com,DIRECT",
    "DOMAIN-SUFFIX,fuclaude.com,DIRECT",

    // AI
    // openai
    "GEOSITE,openai," + forOpenai,
    "RULE-SET,OpenAI," + forOpenai,
    // claude
    "DOMAIN,servd-anthropic-website.b-cdn.net," + USGroupName,
    "DOMAIN,cdn.usefathom.com," + USGroupName,
    "DOMAIN-SUFFIX,anthropic.com," + USGroupName,
    "DOMAIN-SUFFIX,claude.ai," + USGroupName,
    // gemini api
    "DOMAIN-SUFFIX,aistudio.google.com," + USGroupName,
    "DOMAIN-SUFFIX,generativelanguage.googleapis.com," + USGroupName,

    // RULE-SET
    "RULE-SET,private,DIRECT",
    "RULE-SET,reject,REJECT",
    "RULE-SET,icloud,DIRECT",
    "RULE-SET,apple,DIRECT",
    "RULE-SET,proxy," + ProxyGroupName,
    "RULE-SET,direct,DIRECT",
    "RULE-SET,lancidr,DIRECT,no-resolve",
    "RULE-SET,cncidr,DIRECT,no-resolve",
    "RULE-SET,telegramcidr," + ProxyGroupName + ",no-resolve",
    "GEOIP,LAN,DIRECT,no-resolve",
    "GEOIP,CN,DIRECT,no-resolve",
    // "RULE-SET,lancidr,DIRECT",
    // "RULE-SET,cncidr,DIRECT",
    // "RULE-SET,telegramcidr," + ProxyGroupName,
    // "GEOIP,LAN,DIRECT",
    // "GEOIP,CN,DIRECT",

    "MATCH," + ProxyGroupName

  ];

  // 用新的rules代替原来的rules
  params["rules"] = rules;
}

function main(params) {
  const USGroupName = addProxyGroup(params, "🇺🇲 美国节点", /美国|US|United States|America|🇺🇸/);
  const JPGroupName = addProxyGroup(params, "🇯🇵 日本节点", /日本|JP|Japan|🇯🇵/);
  const SGGroupName = addProxyGroup(params, "🇸🇬 新加坡节点", /新加坡|狮城|SG|Singapore|🇸🇬/);
  const HKGroupName = addProxyGroup(params, "🇭🇰 香港节点", /香港|HK|Hong Kong|HongKong|🇭🇰/);
  const ProxyGroupName = addProxyGroup1(params, "🚀 节点选择", /节点选择/, USGroupName, JPGroupName, SGGroupName, HKGroupName);

  addGPTProxyGroup(params, USGroupName, JPGroupName, SGGroupName, HKGroupName);
  // 添加规则
  addRules(params, USGroupName, JPGroupName, SGGroupName, HKGroupName, ProxyGroupName);
  // 添加 rule-providers
  addRuleProviders(params);

  return params;
}

function addProxyGroup(params, groupName, regex) {
  // 判断是否已经存在某个代理组(如"美国节点")的名称
  const group = params["proxy-groups"].find((e) => regex.test(e.name) || e.name === groupName);
  if (group === undefined) {
    const proxiesByRegex = params.proxies.filter((e) => regex.test(e.name)).map((e) => e.name);
    // 判断proxiesByRegex是否为空数组
    if (proxiesByRegex.length === 0) {
      return null; // 没有匹配的代理,直接返回
    }
    params["proxy-groups"].push({
      name: groupName,
      type: "url-test",
      url: "http://www.gstatic.com/generate_204",
      interval: 86400,
      proxies: proxiesByRegex,
    });
    return groupName; // 没有的话就创建一个新的代理组
  }
  // 有了的话直接返回这个代理组的名称
  return group.name;
}

// 添加一个名称为 “节点选择”的代理组
function addProxyGroup1(params, groupName, regex, USGroupName, JPGroupName, SGGroupName, HKGroupName) {
  // 判断是否已经存在某个代理组的名称为 “节点选择”
  const group = params["proxy-groups"].find((e) => regex.test(e.name) || e.name === groupName);
  if (group === undefined) {
    // 将USGroupName, JPGroupName, SGGroupName, HKGroupName这些代理组添加进 group 这个代理组,先判断是否为null
    const proxiesByRegex = [];
    if (USGroupName !== null)
      proxiesByRegex.push(USGroupName);
    if (HKGroupName !== null)
      proxiesByRegex.push(HKGroupName);
    if (SGGroupName !== null)
      proxiesByRegex.push(SGGroupName);
    if (JPGroupName !== null)
      proxiesByRegex.push(JPGroupName);

    // 判断proxiesByRegex是否为空数组
    if (proxiesByRegex.length === 0) {
      return null; // 没有匹配的代理,直接返回
    }
    // 把新生成的这个代理组放在最前面
    params["proxy-groups"].unshift({
      name: groupName,
      type: "url-test",
      url: "http://www.gstatic.com/generate_204",
      interval: 86400,
      proxies: proxiesByRegex,
    });
    return groupName; // 没有的话就创建一个新的代理组
  }
  // 有了的话直接返回这个代理组的名称
  return group.name;
}

function addGPTProxyGroup(params, USGroupName, JPGroupName, SGGroupName, HKGroupName) {
  // 创建一个名为"myGPT"的代理组并存储到局部变量myGPTGroup中
  const myGPTGroup = {
    name: "myGPT",
    type: "select",
    proxies: [],
  };

  // 为名为myGPT的代理组添加代理
  if (USGroupName !== null)
    myGPTGroup.proxies.push(USGroupName);
  //  if (HKGroupName !== null)
  //    myGPTGroup.proxies.push(HKGroupName);
  if (SGGroupName !== null)
    myGPTGroup.proxies.push(SGGroupName);
  if (JPGroupName !== null)
    myGPTGroup.proxies.push(JPGroupName);

  // 如果myGPTGroup的proxies是空数组,则搜索所有可用节点并添加
  if (myGPTGroup.proxies.length === 0) {
    const allProxies = params.proxies.map(proxy => proxy.name);
    myGPTGroup.proxies = allProxies;
  }

  // 最终将myGPTGroup添加到params["proxy-groups"]中
  params["proxy-groups"].push(myGPTGroup);
}



function addRuleProviders(params) {
  // 如果 params 中没有 rule-providers,则创建它
  if (!params["rule-providers"]) {
    params["rule-providers"] = {};
  }

  // 添加 OpenAI rule provider
  params["rule-providers"]["OpenAI"] = {
    type: "http",
    behavior: "classical",
    url: "https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/OpenAI/OpenAI_No_Resolve.yaml",
    path: "./ruleset/OpenAI.yaml",
    interval: 86400
  };

  // Reject rule provider
  params["rule-providers"]["reject"] = {
    type: "http",
    behavior: "domain",
    url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/reject.txt",
    path: "./ruleset/reject.yaml",
    interval: 86400
  };

  // iCloud rule provider
  params["rule-providers"]["icloud"] = {
    type: "http",
    behavior: "domain",
    url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/icloud.txt",
    path: "./ruleset/icloud.yaml",
    interval: 86400
  };

  // Apple rule provider
  params["rule-providers"]["apple"] = {
    type: "http",
    behavior: "domain",
    url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/apple.txt",
    path: "./ruleset/apple.yaml",
    interval: 86400
  };

  // Proxy rule provider
  params["rule-providers"]["proxy"] = {
    type: "http",
    behavior: "domain",
    url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/proxy.txt",
    path: "./ruleset/proxy.yaml",
    interval: 86400
  };

  // Direct rule provider
  params["rule-providers"]["direct"] = {
    type: "http",
    behavior: "domain",
    url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/direct.txt",
    path: "./ruleset/direct.yaml",
    interval: 86400
  };

  // Private rule provider
  params["rule-providers"]["private"] = {
    type: "http",
    behavior: "domain",
    url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/private.txt",
    path: "./ruleset/private.yaml",
    interval: 86400
  };

  // GFW rule provider
  params["rule-providers"]["gfw"] = {
    type: "http",
    behavior: "domain",
    url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/gfw.txt",
    path: "./ruleset/gfw.yaml",
    interval: 86400
  };

  // TLD-not-CN rule provider
  params["rule-providers"]["tld-not-cn"] = {
    type: "http",
    behavior: "domain",
    url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/tld-not-cn.txt",
    path: "./ruleset/tld-not-cn.yaml",
    interval: 86400
  };

  // Telegram CIDR rule provider
  params["rule-providers"]["telegramcidr"] = {
    type: "http",
    behavior: "ipcidr",
    url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/telegramcidr.txt",
    path: "./ruleset/telegramcidr.yaml",
    interval: 86400
  };

  // CN CIDR rule provider
  params["rule-providers"]["cncidr"] = {
    type: "http",
    behavior: "ipcidr",
    url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/cncidr.txt",
    path: "./ruleset/cncidr.yaml",
    interval: 86400
  };

  // LAN CIDR rule provider
  params["rule-providers"]["lancidr"] = {
    type: "http",
    behavior: "ipcidr",
    url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/lancidr.txt",
    path: "./ruleset/lancidr.yaml",
    interval: 86400
  };

  // Applications rule provider
  params["rule-providers"]["applications"] = {
    type: "http",
    behavior: "classical",
    url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/applications.txt",
    path: "./ruleset/applications.yaml",
    interval: 86400
  };

}

这里使用到了RULE-SET,用法可参照 GitHub - Loyalsoldier/clash-rules: 🦄️ 🎃 👻 Clash Premium 规则集(RULE-SET),兼容 ClashX Pro、Clash for Windows 等基于 Clash Premium 内核的客户端。

最后可以来看一下这种方案的结果,可以看到全部都是香港ip或台湾ip。由于我使用的是香港节点,这些请求都走了代理节点,由代理节点来完成DNS解析,所以全部都是香港ip或者地理位置相近的台湾ip。由于并没有走我们配置的8.8.4.4这些DNS服务器,所以这次没有美国ip。

一些其他操作

关闭浏览器的安全DNS

我们想要的是使用自己在clash配置文件中配置的dns,所以这个地方也要关掉。

禁用多宿主 DNS 解析

Windows 系统默认使用多宿主 DNS 解析,会使用所有的网卡发起请求,那肯定不行,开启TUN模式后我们希望的是 clash 接管所有请求,包括DNS。在组策略编辑器中关闭就好。

Clash 里有一个设置叫严格路由:

这个功能的作用(官方解释)是:

在 Linux 中: 让不支持的网络无法到达;将所有连接路由到 tun。 它可以防止地址泄漏,并使 DNS 劫持在 Android 上工作。

在 Windows 中: 添加防火墙规则以阻止 Windows 的 普通多宿主 DNS 解析行为 造成的 DNS 泄露。 它可能会使某些应用程序(如 VirtualBox)在某些情况下无法正常工作。

我同时开启“多宿主解析”和“严格路由”后会出现微信朋友圈图片无法加载的bug( 使用 clash verge rev,刷微信朋友圈时,图片加载不出来 ),只开启一个bug就不存在。但我只开启多宿主解析访问ipleak网站还是会泄露,只开启严格路由就不会泄露。所以我的选择是只开启严格路由。

WebRTC

WebRTC是一个支持网页浏览器进行实时语音对话或视频对话的API,它有一个漏洞会导致泄露用户真实IP,即使开了代理。可以使用 [WebRTC Control](https://chromewebstore.google.com/detail/WebRTC Control/fjkmabmdepjfammlpliljpnbhleegehm) 这个浏览器扩展解决这个问题。

关掉浏览器的QUIC功能

由于UDP在有些场景下必须使用真实的ip,所以目前clash处理udp流量的域名时,即使是使用fake-ip模式也会发起DNS请求,比如基于UDP的QUIC协议,启用了QUIC的浏览器访问油管这种支持QUIC的网站就会发起DNS请求,导致DNS泄露。

另外,观看 Youtube 、Netflix 等流媒体时,Chrome 会试图使用 QUIC 协议进行连接,或将部分 TCP 连接转换为 QUIC 。全国多地运营商对 UDP 协议非常不友好,权重很低,限制非常严重,体验就会非常差,缓冲和加载速度会被大幅度影响。

所以推荐关闭 QUIC 协议。操作:

地址栏输入 chrome://flags/#enable-quicedge://flags/#enable-quic,更改为 Disabled。重启浏览器配置生效。

tun模式的一个小缺陷

开启tun模式后,ping 命令拿不到真实的延迟。因为ping使用的是ICMP协议,这个协议在网络层,和IP协议是一层的,TUN模式只能接管网络层IP协议的流量。

参考:

常见名词 - Clash Verge Rev Docs

DNS配置 - 虚空终端 Docs

https://www.youtube.com/watch?v=qItL005LUik&list=PL5TbbtexT8T3JJdJAy73A0T2NXZL2JEJY&pp=iAQB

Clash meta(mihomo):DNS配置从入门到入土

关于Clash等现代代理软件的规则和dns配置问题

源IP隐藏和DNS泄露问题

DNS泄露?不对 应该是 DNS出口查询!

Clash Verge Series Best Practices - Lainbo

关于 Clash 科学上网的最佳实践

https://blog.skk.moe/post/what-happend-to-dns-in-proxy/

参考配置 | Clash 知识库

请教下 clash 的 DNS 是这么配置么? - V2EX

GitHub - Loyalsoldier/clash-rules: 🦄️ 🎃 👻 Clash Premium 规则集(RULE-SET),兼容 ClashX Pro、Clash for Windows 等基于 Clash Premium 内核的客户端。

GitHub - blackmatrix7/ios_rule_script: 分流规则、重写写规则及脚本。

顺便在这里放一下我的另外两个和代理有关的高赞帖:

再打个广告,放一下我另一个电子书科普帖,也是高赞帖:

298 个赞

谢谢你 小叶 辛苦了 码上看

5 个赞

很好的文章!

4 个赞

不良林的这个视频我也看了,看了楼主的笔记又加深了记忆

6 个赞

感谢大佬的分享,收藏了

4 个赞

厉害了,小心心已点

2 个赞

看完了,感谢分享

这个网站 DNS 检测在维护中

确实,我前天写文的时候还能正常用,今天发现在维护了

1 个赞

楼主写的太清楚了,通俗易懂, 赞:+1::+1::+1:

2 个赞

好文 感谢分享 娓娓道来 解决了之前的几个困惑 :+1:

2 个赞

写的很不错,原来只是一知半解,现在多了解很多,启动后续! 支持一下

2 个赞

等一个tun模式解决方案

1 个赞

这技术贴必须收藏

感谢热佬分享,支持支持~

4 个赞

感谢热佬分享知识,学到了

5 个赞

感谢大佬!先马后看

4 个赞

讲的非常通俗易懂 :+1:
我觉得检测泄露还是应该用Wireshark一类的软件
另外可以再讲一下如何避免浏览器直接发出DNS请求

5 个赞

这个我不太懂诶,大佬要不简单讲讲

3 个赞

我用的是firefox,需要在about:config里关闭 media.peerconnection.enabled,就是WebRTC,然后在设置里开启代理DNS查询,其他浏览器就不知道了

4 个赞