Matrix 首页推荐
Matrix 是少数派的写作社区,我们主张分享真实的产品体验,有实用价值的经验与思考。我们会不定期挑选 Matrix 最优质的文章,展示来自用户的最真实的体验和观点。
文章代表作者个人观点,少数派仅对标题和排版略作修改。
问题所在
DNS 污染和劫持是一个十分恼人的问题。比如在浏览器访问一个正常网站时,有时会加载失败,有时甚至会被重定向到博彩或颜色网站,多半是因为系统使用的 DNS 被污染了,传统的 DNS 协议是通过 udp 明文传输的,攻击者可以在 DNS 请求过程中监控、劫持并修改 DNS 记录。使用一些 DNS 解析工具会发现被污染的网站域名被解析到了错误的 ip 地址。各大运营商下发的默认 DNS 则是被污染的重灾区。
把系统 DNS 修改为其他可信任的公共 DNS 可以缓解污染的问题,这对于访问国内网站可能有些帮助,但由于一些原因,访问国外网站仍有可能被污染,不能从根本上解决问题。
在 Android 系统上,为了解决 DNS 污染的问题,在 Android 9 以后新增了一个名为「私人 DNS」的功能,这个功能本质上是 DNS-over-TLS(以下简称 DoT),顾名思义是将 DNS 请求使用 TLS 加密,加密后的 DNS 请求理论上除了 DNS 服务端和发出请求的客户端,第三方是无法获得 DNS 的请求结果的,更无法劫持并篡改,是一种更为隐私和安全的 DNS 协议。
但 DoT 也并不完美,因为 DoT 每一次 DNS 查询都要建立一个新链接,如果网络条件比较差,可能会导致 DNS 请求积压,从而影响后续的 DNS 请求;另外 DoT 默认使用的端口号是不常见的 853,特征比较明显,也有可能会被识别到。
相比之下 DNS-over-HTTPS(以下简称 DoH)是一个更优秀的 DNS 协议,它不需要像 DoT 那样每次请求都要建立新链接,连接更稳定;而且它将 DNS 请求放进 HTTPS 流量之中,更加难以被识别,HTTPS 本身也自带加密,可以防止劫持和篡改(虽然也可以通过一些手段识别到 DoH 流量,但它的特征没有 DoT 那么明显)。
在 Android 11 以后私人 DNS 其实已经支持了 DoH,不过只适用于 Google 和 Cloudflare 两家的公共 DNS,对于其他家的公共 DNS 仍然只能用 DoT。
解决思路
由于众所周知的原因,Google 和 Cloudflare 的公共 DNS 在国内的互联网环境下访问不太通畅,所以我一直以来都在寻找其他的方法来让 Andorid 用上 DoH。
最简单的方法就是使用第三方 APP,比如 Intra,这些 APP 的工作原理是在本地开启一个 VPN 代理,让本机所有网络流量走这个代理,在 APP 中配置 DoH 的上游,这样所有的 DNS 请求都会经过 DoH 解析了。但是这样的方案需要 APP 一直在后台运行并且开启 VPN,要消耗更多的电量,而且很多国产定制安卓系统后台限制比较严格,APP 后台有被杀掉的风险。
另一个方案是 ROOT 后使用 DNS 模块,比如 dnscrypt-proxy 和 AdGuardHome,这些模块的工作原理是在本地开一个 DNS 服务器,然后通过 iptables 将所有系统的 DNS 请求都转发到本地的 DNS 服务器上,在本地的 DNS 服务器中,就可以配置 DoH 服务商作为上游了。但我发现在使用模块时,如果连接的是我家里的宽带网络,DNS 请求是可以全部由模块接管的,如果使用的是数据流量,模块就无法接管全部 DNS 请求了,我手动屏蔽的广告网站还是可以被打开,后来我发现是 ipv6 的锅,因为这些模块大多数是没有关于 ipv6 DNS 的转发规则的,使用数据流量时,系统获取到了 ipv6 地址,也下发了 ipv6 的 DNS,这就导致了一部分 DNS 请求会走 ipv6 DNS。更难受的是,最近家里的宽带进行了升级,升级过后也有了 ipv6 地址,导致在连接到家里的宽带时,也会出现 DNS 请求被 ipv6 DNS 截胡的情况。
最后我想到一个方法:Android 系统的私人 DNS 优先级非常高,无论被下发了什么 DNS,只要私人 DNS 可用,系统就会使用私人 DNS 来解析 DNS 记录,如果我在本地运行一个 DoT 的服务器,因为只运行在本地,不太可能会出现网络阻塞,然后将服务器的上游配置为可用的 DoH 地址,再在系统的私人 DNS 里面填上我配置好的 DoT 地址,这样不就相当于变相让 Android 系统支持了所有 DoH 地址了吗,在我的一番尝试下,终于实现了我的设想,下面是我的配置方法。
解决方案
前置要求
- 一台使用 KernelSU 或 Magisk 获取了 ROOT 权限的手机;
- 一个域名;
- 一个灵巧的大脑😝。
使用 KernelSU 或 Magisk 是为了能够刷入 AdGuardHome 模块,从而让其在后台运行不会被杀,没有 ROOT 权限的话,使用 Termux 之类的工具让其在后台运行理论上应该也是可行的,但是我不确定没有 ROOT 权限能不能监听 853 端口,我没试过,感兴趣的人可以试试。
因为这个服务只是运行在本地,其实完全没有加密的必要,但是配置 DoT 服务是必须要 ssl 证书进行加密的,所以还是需要一个域名来申请 ssl 证书,同时作为 DoT 的地址。没有域名的话,使用自签证书理论上应该是可行的,但是据说使用自签证书的话系统会有「网络正在受到监控」之类的提示,还是有点烦人的。
申请证书
可以使用 certbot 工具从 Let's Encrypt 申请免费证书,这个过程可以在手机上用 Termux 进行,也可以在电脑上进行,只要最后能找到申请的证书放在哪里就行。在常用的 Linux 发行版中这个工具都可以直接使用包管理器安装。下面是我在 Arch Linux 上申请证书的过程:

- 运行:
sudo certbot certonly --manual --preferred-challenges=dns --preferred-chain="ISRG Root X1"
命令后面的选项 --preferred-chain="ISRG Root X1"
是为了强制使用 Let's Encrypt 新的根证书,大约在 2021 年,Let's Encrypt 旧的根证书过期了,因为当时的 Android 系统没有及时更新证书链,影响了很多 DoT 服务(包括我当时自建的 DoT),我的表述可能不太准确,具体可以看这里。现在三年多过去了,这个选项可能已经不需要了,不过保险起见还是加上这个选项吧。
- 按照提示输入邮箱地址,用来提醒证书过期,如果不需要,可以直接回车跳过;
- 然后输入 y 并回车表示同意使用条款;
- 接着就是询问是否同意将邮箱地址共享给 EFF,可以根据自己的需要选 y 还是 n;
- 之后输入自己的域名,方便起见可以选择申请域名的通配符证书,意思是这个域名下的所有子域名都可以使用这个证书,如果域名是
example.com
,那么在这里输入*.example.com
即表示申请通配符证书; - 之后需要在 DNS 服务商处给自己的域名添加一条 TXT 记录,用来验证域名是归属你所有的,记录名称是
_acme-challenge
,也就是说创建一条子域名为_acme-challenge.example.com
记录,记录类型是 TXT,记录内容是 certbot 工具生成的随机字符串;

- 创建完记录后,回到终端页面按下回车进行验证,如果 DNS 生效的时间较慢,certbot 可能会提示要重新进行一次 DNS 验证的过程,要使用相同的记录名,但使用不同的随机字符串,记住这时不要删除或修改之前添加的记录,而是新添加一个
_acme-challenge
的 TXT 记录,这是被 DNS 规范所允许的,验证成功后,这些记录就可以删掉了。 - 申请成功后,会得到两个文件,一个是证书文件
fullchain.pem
,另一个是密钥文件privkey.pem
,这两个文件都是必须的,将这两个文件发送到手机上待后续使用。
如果用的域名服务商支持通过 API token 来控制 DNS 记录,比如我在用的 Cloudflare,则可以使用 lego 工具来自动化上面的流程,AdGuardHome 推荐使用 legoagh 脚本,这个脚本可以自动下载安装 lego 并申请通配符证书,同样也是可以在手机上用 Termux 或是在电脑上用 Linux 运行。将 lego.sh 脚本下载到本地并给予运行权限,我用的 DNS 服务商是 Cloudflare,那么就可以运行:
DOMAIN_NAME="example.org" \
EMAIL="you@email" \
DNS_PROVIDER="cloudflare" \
CLOUDFLARE_DNS_API_TOKEN="yourapitoken" \
./lego.sh
域名和邮箱改成自己的,API token 的申请则可以参考我之前的文章。
用 lego 工具申请的证书文件扩展名和 certbot 不太一样,文件名是申请证书的域名,证书文件扩展名是 .crt
,密钥文件扩展名是 .key
。
从 Let's Encrypt 申请的证书只有三个月的期限,不要忘记在三个月到期前重新申请证书。
之后为自己的域名创建一条 A 记录,解析到 127.0.0.1(我也是最近才知道 DNS 可以解析到内网地址,本来我是打算直接修改手机系统的 hosts 文件的),这个子域名就是后面要填到私人 DNS 的地址。

刷入模块并进行配置
本来我是打算随便找一个 AdGuardHome 模块,解压缩后删掉里面所有 iptables 规则,再重新打包刷入的。不过一番搜索后我在 Github 上找到了两个现成的模块,一个是 adguardhome-magisk,只在后台运行 AdGuardHome,没有任何 iptables 规则;另一个是AdGuardHomeForRoot,这个模块可以通过配置文件禁用 iptables 规则。
经过我测试,发现第一个模块 adguardhome-magisk 存在一个奇怪的问题,模块内置的 0.107.44 版本的 AdGuardHome 可以正常使用,但是无论是在 WEB 界面自动更新,还是手动替换文件,只要版本比 0.107.45 新,就无法解析并使用 DoH 地址,不太清楚是不是只有我遇到了这个问题,这个模块结构十分简单,只有程序本体和一个简单的启动脚本,不太明白为什么只是更新版本就会导致不可用;而第二个模块 AdGuardHomeForRoot 就没有这个问题,所以我最后还是决定用第二个模块(不过后来我发现这个模块是有阻断 ipv6 DNS 请求的 iptables 规则的,如果不想麻烦,直接用这个模块的 iptables 规则应该也没啥问题🤣)。
下载模块并刷入,在重启系统之前需要对模块进行一些配置,首先禁用模块的 iptables 规则,编辑手机根目录的 /data/adb/agh/settings.conf
,找到 enable_iptables
,把后面的值设为 false;
这个模块对 AdGuardHome 预先做了一些配置,无需再对其做初始化配置,网页后台的用户名和密码默认都为 root,因为 AdGuardHome 在配置文件中对用户密码进行了加密,要想修改密码的话,需要使用 Apache 自带的 htpasswd 工具生成加密后的密码,并在配置文件中进行替换,具体可以看相关的文档,需要先安装 Apache 软件包,这个软件在不同的发行版包名不同,Debian 系(包括 Termux)下的包名大概率是 apache2
,我用的 Arch Linux 包名直接就是 apache
,Windows 下可以去这里下载安装。安装完成后,Linux 下可以在终端运行:
# 把 <USERNAME> 和 <PASSWORD> 改成自己的用户名和密码
htpasswd -B -C 10 -n -b <USERNAME> <PASSWORD>

终端会输出 <USERNAME>:<HASH>
的格式,其中冒号后面的 <HASH>
就是加密后的密码。之后编辑手机上的 /data/adb/agh/bin/AdGuardHome.yaml
文件,找到下面的内容,把要修改的用户名和加密后的密码替换即可:
users:
- name: root
password: $2b$12$D3zeMIBFfzcQTqGqB.k7GOkMvqx1jgsrdiRCn2kwOHl.kNmmPfMom
修改完配置并重启后,在手机浏览器中打开 localhost:3000
就可以对 AdGuardHome 进行配置了。首先在「设置 - DNS 设置」界面配置上游服务器,可以参考 Adguard 提供的已知的 DNS 提供商列表,尽量用 DoH 地址。国内可用的 DoH 服务有阿里 DNS、DNSPod 腾讯 DNS 等,不过这些 DNS 可能仍然存在一定的污染;国外的 DoH 服务在目前大多无法直连了,我测试发现在我的网络环境下 NextDNS 目前似乎还可以直连,如果有条件可以购买境外服务器自建 DoH 服务,这就不是本文的讨论内容了。后备 DNS 服务器是当上游 DNS 服务器无法使用时作为备用的,这个可填可不填,我这里上游配置了 NextDNS,后备配置了阿里 DNS。Bootstrap DNS 服务器只被用来解析上游服务器的 ip 地址,这里只可以填常规的基于 ip 的 DNS 地址,可以填上国内速度比较快的公共 DNS。

然后是配置 DoT,首先把之前申请到的证书保存到手机本地,最好不要保存在用户目录下面,因为安卓手机在开机后第一次解锁屏幕之前,用户分区是有可能被加密的,导致 AdGuardHome 读取不到证书,使 DoT 服务启动失败,我个人为了方便管理就直接放在了模块的运行目录 /data/adb/agh
下了。之后在网页后台的「设置 - 加密设置」界面,勾选启用加密,服务器名称填自己之前解析到 127.0.0.1 的域名;下面的端口除了 DNS-over-TLS 的 853 端口不能改,其他的都基本用不到随便改,不过需要记住自己配置的 HTTPS 端口方便后续访问;证书和密钥部分填入之前保存的证书和密钥所在的路径。一切无误后,点击「保存配置」,就大功告成了。

之后在系统设置的私人 DNS 里面填上自己刚刚配置的域名,就可以使用了,之后也可以在本机的浏览器内输入域名加 HTTPS 端口号来访问 AdGuardHome 的网页后台。因为这个域名之前只解析到了 127.0.0.1,并且 AdGuardHome 监听的地址也只有 127.0.0.1,所以这个 DoT 地址只有本机可用,网页后台也只有本机能打开,局域网内的其他设备是用不了的。

总结
用这种方法,相比于使用第三方 APP 在后台运行,消耗电量更少,我自己使用下来感觉不出明显的耗电增加;相比于使用模块的 iptables 规则,可以防止 DNS 请求被 ipv6 DNS 截胡,用起来也更为灵活,如果不想用加密 DNS 时,只需在设置里面关闭私人 DNS 即可。
> 关注 少数派公众号,解锁全新阅读体验 📰
> 实用、好用的 正版软件,少数派为你呈现 🚀