搭建全协议DNS服务器

DNS污染现状

从没有一个国家像我们一样拥有如此严重的DNS缓存投毒,而与此对抗的最有力手段则是加密DNS查询。

DNS劫持与污染是GFW与各ISP服务商的常见手段,常用于屏蔽网站与广告引流。DNS服务器负责将域名解析为对应的IP地址,这个过程默认使用UDP的53号端口进行通讯,而这种未经加密的数据包极易被修改伪造,把域名指向不正确的IP地址,从而造成DNS劫持。

操作系统一般会有一层DNS缓存用于加快解析速度,在查询过一个域名后,得到的数据会被缓存下来,下一次查询时直接将数据返回而无需再次发起DNS请求。但如果第一次查询得到的数据是被劫持过的虚假数据,那系统将在缓存有效期内持续提供错误结果,这个现象称为 DNS缓存投毒 。这种DNS污染行为不仅针对用户终端系统,对于公共DNS服务器(Public DNS,大型公司或组织设立)和本地DNS服务器(Local DNS,运营商提供或局域网自建)同样有效,它们一般都会配置缓存,因此也会被污染。

处于国内的DNS服务器在对国外网站发起执行DNS迭代查询时,由于域名的权威服务器位于海外,数据包不可避免的要经过防火长城,而部署于其上的审查机制就会生效,一旦匹配到黑名单的解析请求,马上伪装成目标服务器返回虚假的查询结果,由于查询者接受最先得到的解析结果,无论真正的UDP数据包无论有没有返回都会被丢弃,造成错误的DNS解析。这个过程是GFW屏蔽网站的最基本手段,它会波及到绝大多数的境内DNS服务器,进而导致全国性的DNS污染,严重的时候甚至会扩散到周边国家。

同样的,ISP服务商也可以对DNS数据包进行劫持,这种行为一般用于广告引流,将被劫持的域名指向自己的服务器IP,提供广告或其他内容获利。由于用户的所有流量都流经ISP服务商的网络,即使不使用运营商分配的DNS服务器,返回的数据包仍然可能被劫持,这种行为在大多数用户不察觉的情况下发生,因此常规DNS解析难以保证安全性。

DNS规范在制定的时候并未考虑过如此严重的劫持行为,直接使用UDP数据包明文通信,即使后面各组织为此做出不少努力,这个缺陷也从来没有被真正弥补过。IETF曾在1997年推出DNSSEC这种签名机制,它被列入了互联网标准化文档中,是最早大规模部署的DNS安全协议,但是时至今日,全世界启用完整DNSSEC的域名仅有24.66%,而在中国仅有1.18%的覆盖率,位列全球倒数第二,足可见形势不容乐观。你可以在APNIC(亚太互联网络信息中心)官网上看到全球的DNSSEC覆盖情况与国内的DNSSEC分析

全球DNSSEC覆盖状态

另一方面,DNSSEC仅仅使用了信任链签名机制,它能保证查询结果的准确性,这种方案在大多数网络下已经能保证足够的可用性,但在GFW面前却如螳臂当车,因为DNS签名机制虽不能被伪造,但可以被截断,也就是直接丢弃该数据包,或返回一个无签名的虚假数据包。此外,即便启用了DNSSEC,请求时也仅能保证迭代查询的正确性,本机发起的递归查询返回的结果仍可能被劫持,更何况大量普通用户使用运营商的DNS服务器,这种情况下本地DNS服务器就是内鬼,DNS签名机制形同虚设。

所以,在每况愈下的环境中,为了对抗DNS劫持与污染,各种DNS加密查询协议应运而生,目前主流的有 DNS-over-TLSDNS-over-HTTPSDNSCrypt,还有萌芽阶段的 DNS-over-QUIC

DNS各协议简介

以下几种DNS查询机制是目前公开的协议,有规范化的文档与实现,后面将搭建支持以下全部协议的DNS服务器

Plain DNS

Plain DNS即常规DNS,它的格式为单个IPv4或IPv6地址,查询时默认使用UDP或TCP的 53 端口通讯,必要时可手动指定其他端口(需要系统或软件支持)。该请求信息未经加密,易被伪造,是目前使用范围最广的查询方式,上文所提及的DNS劫持与污染均基于该机制实现,目前正逐步被其他加密DNS查询所替代。

DNS-over-TLS

DNS-over-TLS简称 DoT,是一种基于TLS进行报文加密的DNS请求方式,查询时默认使用TCP的 853 端口进行通讯,在TLS握手以后再执行DNS请求与应答,TLS可以保证通信双方数据的保密性和完整性,这种DNS请求方式几乎不可能被劫持,且由于请求直接从用户终端发起,ISP也无法对其内容进行修改。

由于TLS证书上有CA的签名,如果系统的根证书信任未被篡改过,那就可以保证通讯服务器不是GFW伪造的,因此GFW只能阻断指定域名或IP的TCP连来拦截DoT,这种行为最多只能让DNS查询超时,但无论如何都不会得到错误的结果。

相较于常规DNS查询,DoT明显有保密性和完整性的优势,但代价就是更高的时延,理论上至少为常规DNS的四倍。同时,DNS服务器需要接受多次TSL连接,在高并发的环境下对性能也有更高的要求。

DoT延时较高的问题在实际使用中感知不强,因为整个解析过程最多也就几百毫秒,相比于网页的加载时间并不明显,况且系统中也存在DNS缓存,短时间内重复查询与常规DNS无异。而相比于这些,更应该关心的是能否查到正确的DNS解析结果,这才是DoT的意义所在。

不过DoT也有其弱点所在,它默认使用853接口,一旦GFW决定大规模封杀它的使用,只需要对853端口的出境流量严格审查或直接切断,即使一部分系统或软件支持切换端口,但也仅仅是治标不治本。这是DoT相较于DoH的最明显缺点,当然两者在不同环境下各有优势,不可一概而论。

DoT服务的格式为 tls://$域名tls://$IP,IP地址若为IPv6,需要括上 [] 符号。此外,搭建DoT服务时需要有一张针对域名或IP地址的TLS证书,它同时也可以用在HTTPS认证上。

DNS-over-HTTPS

DNS-over-HTTPS简称 DoH,是一种基于HTTPS协议进行数据传输和加密的DNS请求方式,查询时默认使用TCP的 443 端口进行通讯。直观讲就是在DNS外面套一层HTTPS,或者说把DNS请求变成HTTPS请求,从而达到加密的效果。

相比于DoT,DoH使用HTTPS进行连接,由于HTTPS协议也是由TLS加密的,因此DoH和DoT在本质上是相同的,即DoH也可以保证数据的保密性和完整性。不同的是,DoT默认使用853端口,DoH则默认使用HTTPS标准的443端口。

在逻辑上,DoH的实现更加简单,只需把正常的DNS查询报文先处理为 base64url 编码,接在 dns-query 后的HTTP请求参数中发起GET请求即可,这个过程在RFC8484中有详细说明

DoH相较于DoT有一些独有优势,一方面由于它默认走443端口,可以与其他网页流量混合在一起,出境时在GFW看来与网页访问没有明显区别。当然,它没有到达某些加密代理工具的反检测高度,在需要的时候GFW只需做一次主动探测,或对于DNS服务器发起重放攻击,就可以立刻识别出目标服务器的性质,但无论如何,DoH的隐蔽性和反检测性都是高于DoT的。另一方面,由于请求包含在HTTP流量中,DoH请求可以透过CDN加速,即使它是动态请求需要实时回源,但在大部分网络环境下CDN的边缘节点会有更好的数据路由,加快请求速度。

DoH同时也有一些DoT不具备的缺点,譬如它的基于HTTP协议,延迟相较于DoT会稍高一些,不过这一点几乎可以忽略不计。在网络环境差的情况下,DoH性能也会一定程度上低于DoT,总之,两者各有优劣胜负所在。

DoT服务的格式一般为 https://$域名/dns-queryhttps://$IP/dns-query,IP地址如果为IPv6地址,需要括上 [] 符号。与DoT相同,DoH机制也需要针对域名或IP的TLS证书,这个机制可以保证客户端在查询时不被劫持到错误的服务器上。

DNS-over-QUIC

QUIC是 Quick UDP Internet Connection 的缩写,是由谷歌制定的一种传输层协议。它基于UDP协议,融合了TCP、TLS、HTTP/2等协议的特性,在UDP的不可靠性上实现了可靠的加密链路,得益于UDP的低延迟与相对效率更高的特性,QUIC相比于传统TCP+TLS有着更低的连接延迟、更高的传输效率、更稳定的传输效果。

QUIC可以简单理解为在UDP协议上重新实现了HTTP/2,即 QUIC = UDP + TLS + HTTP2,但比原来的协议栈有着各方面的优势,因此,在2018的IETF会议上,HTTP-over-QUIC被定义为HTTP/3。当然,现阶段QUIC还没有形成标准RFC文档,仍处于测试阶段,但已经有不少浏览器与服务根据草案开始支持该协议。

基于QUIC的DNS查询称为DNS-over-QUIC,简称 DoQ,默认使用UDP的 7848853 端口(QUIC基于UDP协议)。相比于DoH与DoT,它拥有QUIC协议的优势,重点表现在低延迟上,缓解了前两者一直被诟病的卡顿问题。在QUIC协议中,首次连接需要1-RTT的延迟,此后连接时可以达到0-RTT的连接延迟,远胜于TCP+TLS的3-RTT连接延迟,而DoH与DoT都没有脱离传统TCP协议栈,在延迟上逊于QUIC协议。在一些测试中,DoQ在弱网络中相比DoH或DoT有着明显优势,体验上可以接近传统的UDP查询。

另一方面,使用特定默认端口的DoQ很容易GFW识别并拦截,与DoT相同,这个问题可以通过修改端口缓解,但它并不受一些系统与客户端的支持。幸运的是,一旦HTTP/3能定稿并普及开来,基于该协议的DNS查询就能兼具低延迟与隐蔽性的优势了,但这或许需要很长一段时间。

当下,QUIC还是一个全新的协议,标准文档也还未出炉,支持的服务很少,而支持DoQ的软件就更少了,当下也仅有AdGuard的系列软件有对它启用实验性的支持。DoQ服务的格式一般为 quic://$域名quic://$IP

DNSCrypt

DNSCrypt在目标上与DoT、DoH、DoQ类似,都是将DNS查询放在加密链路上传输,防止被窃听或劫持,但是它的实现方式略有不同。相比于前三者,它不需要借助于域名工作,也不依赖于被信任的TLS证书,它仅仅提供了一种针对性的协议用于加密客户端与DNS服务器之间的通信。

你可以将它理解为一个独立的协议,不依赖于既有的HTTP、TLS等协议,有着自己的一套机制用于保证数据的传输安全与身份认证。它的v2版本在2013年发布,官网介绍称其是“迄今为止部署最多的加密DNS协议”。在该协议中,DNS服务器上会生成一对公私钥,并将公钥派发给客户端用于认证与加密,因此,DNSCrypt客户端必须明确信任所选提供者的公钥,想使用哪个DNSCrypt服务器,就需要预先安装该服务器的公钥,而不是通过常规的受信任证书颁发机构列表签名获取信任。

DNSCrypt协议是一个自由开放协议,不属于任何个人或组织,官网上也有它详细的规范文档,不过由于作者没有申请将其列入标准化文档,因此目前并不是一个正式的协议,在大规模的应用中存在一定的局限性。尽管如此,它却是目前使用量最大的加密DNS查询协议,有不少基于它实现的客户端与服务。

使用DNSCrypt时客户端需要三个必备参数:服务器IP地址、公钥、证书名称,其中IP地址若为IPv6需要括上 []。由于多个参数在共享时并不方便,一般使用 DNS Stamp 将其封装为 sdns://··· 的格式,当然DNS Stamp不仅仅可用于封装DNSCrypt,同样也支持DoT、DoH、DoQ等协议,你可以在这里看到它的完整介绍。

公共DNS服务器

Public DNS,即公共DNS服务器,一般由大型互联网公司推出,节点分布全球各地,延迟较低,用户数量大,具有一定的安全性和抗攻击性。目前有不少公共DNS服务,在不同地区的使用体验不尽相同,这里有一份较为完整的全球公共DNS列表,上面列举了绝大多数常用的公共DNS服务及对应地址。

国内常用公共DNS

国内首选阿里或腾讯DNS,污染较小且稳定性好,且阿里DNS更有完整IPv6支持

阿里DNS

$alidns_ip 为阿里DNS的IPv4地址

  • IPv4地址:223.5.5.5223.6.6.6

  • IPv6地址:2400:3200::12400:3200:baba::1

  • DoT地址:tls://dns.alidns.comtls://$alidns_ip

  • DoH地址:https://dns.alidns.com/dns-queryhttps://$alidns_ip/dns-query

腾讯DNS

  • IPv4地址:119.29.29.29119.28.28.28

  • DoT地址:tls://dot.pubtls://dns.pub

  • DoH地址:https://doh.pub/dns-queryhttps://dns.pub/dns-query

百度DNS

  • IPv4地址:180.76.76.76

  • IPv6地址:2400:da00::6666

114 DNS

  • IPv4地址:114.114.114.114114.114.115.115

国外常用公共DNS

在国内使用延迟较高,国外服务器首选Cloudflare DNS或Google DNS。

Cloudflare DNS

$cfdns_ip 为Cloudflare DNS的IP地址,IPv6地址需要用 [] 括起。

  • IPv4地址:1.1.1.11.0.0.1

  • IPv6地址:2606:4700:4700::11112606:4700:4700::1001

  • DoT地址:tls://$cfdns_ip

  • DoH地址:https://dns.cloudflare.com/dns-queryhttps://$cfdns_ip/dns-query

Google DNS

$googledns_ip 为Google DNS的IP地址,IPv6地址需要用 [] 括起,DoH查询的Host仅支持IPv4地址。

  • IPv4地址:8.8.8.88.8.4.4

  • IPv6地址:2001:4860:4860::88882001:4860:4860::8844

  • DoT地址:tls://dns.googletls://$googledns_ip

  • DoH地址:https://dns.google/dns-queryhttps://$googledns_ipv4/dns-query

测试方法

在搭建DNS服务器时,我们需要有一个简单的方式确保服务是正常工作的,这里建议使用dnslookup进行测试,它是一个专用于多协议DNS调试的命令行工具,基于MIT许可证开源,支持所有已知的DNS协议,包括常规DNS、DoH、DoT、DoQ以及DNSCrypt。它类似于dig工具,但是更纯粹易用,使用时只需指定目标域名和DNS服务器地址,同时也提供了指定DNS服务器的IP地址、输出JSON格式、跳过证书验证等功能。

获取工具

你可以直接下载它的Release到本地使用。Windows系统下直接把 dnslookup.exe 解压到 C:\Windows\System32\ 下即可,Linux系统可按如下命令操作:

# v1.4.5是当下的最新版本,实际下载时需要到release页获取最新链接
shell> wget "https://github.com/ameshkov/dnslookup/releases/download/v1.4.5/dnslookup-linux-amd64-v1.4.5.tar.gz"
···
# 解压并安装
shell> tar xf dnslookup-linux-amd64-v1.4.5.tar.gz
shell> mv linux-amd64/dnslookup /usr/bin/
shell> rm -rf dnslookup-linux-amd64-v1.4.5.tar.gz linux-amd64/

安装完毕后使用命令 dnslookup --help 输出使用说明,可用于确认是否安装成功。

shell> dnslookup --help
dnslookup v1.4.5
Usage: dnslookup <domain> <server> [<providerName> <serverPk>]
<domain>: mandatory, domain name to lookup
<server>: mandatory, server address. Supported: plain, tls:// (DOT), https:// (DOH), sdns:// (DNSCrypt), quic:// (DOQ)
<providerName>: optional, DNSCrypt provider name
<serverPk>: optional, DNSCrypt server public key

使用示例

这里使用AdGuardHome提供的DNS服务进行测试,后期可能会被GFW屏蔽

常规DNS

在国内网络查询谷歌、推特等被封锁的域名会受到污染,这里使用 baidu.com 进行查询。

shell> dnslookup baidu.com 176.103.130.130
dnslookup v1.4.5
dnslookup result:
;; opcode: QUERY, status: NOERROR, id: 37745
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;baidu.com.     IN       A

;; ANSWER SECTION:
baidu.com.      33      IN      A       220.181.38.148
baidu.com.      33      IN      A       39.156.69.79

DNS-over-TLS

shell> dnslookup google.com tls://dns.adguard.com
dnslookup v1.4.5
dnslookup result:
;; opcode: QUERY, status: NOERROR, id: 37703
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;google.com.    IN       A

;; ANSWER SECTION:
google.com.     163     IN      A       216.58.197.14

DNS-over-HTTPS

shell> dnslookup youtube.com https://dns.adguard.com/dns-query
dnslookup v1.4.5
dnslookup result:
;; opcode: QUERY, status: NOERROR, id: 60383
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;youtube.com.   IN       A

;; ANSWER SECTION:
youtube.com.    199     IN      A       172.217.161.206

DNS-over-QUIC

shell> dnslookup facebook.com quic://dns.adguard.com
dnslookup v1.4.5
dnslookup result:
;; opcode: QUERY, status: NOERROR, id: 65467
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;facebook.com.  IN       A

;; ANSWER SECTION:
facebook.com.   296     IN      A       185.60.216.35

DNSCrypt

shell> dnslookup twitter.com sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIj
IuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20
dnslookup v1.4.5
dnslookup result:
;; opcode: QUERY, status: NOERROR, id: 27774
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;twitter.com.   IN       A

;; ANSWER SECTION:
twitter.com.    168     IN      A       104.244.42.1

安装服务

部署AdGuardHome服务提供多协议的DNS服务,如果想实现无污染DNS,则服务器必须在国外

AdGuard是一个常用的广告拦截器,AdGuardHome是其下的一个项目,它部署在路由器或服务器上,可以在DNS解析层面拦截广告。虽然AdGuard是商业化软件,但AdGuardHome却是免费的,它基于GPL3.0协议开源,没有付费增强的商业化机制,且社区与交流群活跃,项目更新速度快。

AdGuardHome官网上,大部分介绍均为去广告功能,虽然名义上如此,但它是目前唯一一个能提供全协议的DNS服务工具,我们可以把它当作DNS服务使用,侧重其加密解析功能。它可以自定义拦截与重写列表,对不同的客户端进行管理,对不同的域名与请求来源进行统计,更有完善的图形化界面。所以说,去广告只是一个可选的附加功能,甚至默认的广告拦截规则也是开源的,需要的时候也可以编写自己的规则。

AdGuardHome使用Go语言编写,如果本机有Go编程环境也可以克隆源码自行编译。不过,它的Github仓库中有编译好的二进制资源,安装时直接下载对应版本的Release即可,一般有以下两种安装方式:

注意,AdGuardHome默认占用系统TCP的3000端口,如果该端口上存在其他服务(如Gitea等)会导致启动失败,遇到该情况请先暂时关闭该端口上的服务,安装后AdGuardHome可以修改为在其他端口上运行。

一键安装

AdGuardHome提供了全自动的脚本安装,只需运行安装脚本,即可一键安装最新稳定版(Latest release)。不过,由于Github在国内经常受到干扰,脚本获取与安装可能出现运行缓慢或异常报错,因此大陆地区的VPS可以尝试手动安装的方式。

# 运行安装脚本
shell> curl -sSL https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh
···
[info] AdGuard Home will be installed to /opt/AdGuardHome
[info] Downloading package from https://static.adguard.com/adguardhome/release/AdGuardHome_linux_amd64.tar.gz -> AdGuardHome_linux_amd64.tar.gz
[info] Unpacking package from AdGuardHome_linux_amd64.tar.gz -> /opt
···
AdGuard Home is successfully installed and will automatically start on boot.
···
··· [info] AdGuard Home is available on the following addresses:
··· [info] Go to http://127.0.0.1:3000
··· [info] Go to http://172.17.36.87:3000
··· [info] Action install has been done successfully on linux-systemd
[info] AdGuard Home is now installed and running.
···
# 安装完成

运行该命令后,脚本会把AdGuardHome默认安装到 /opt/AdGuardHome 下,如果不习惯也可以将其链接到 /etc 目录

# 创建一个软链接
shell> ln -s /opt/AdGuardHome/ /etc/
# 进入AdGuardHome文件夹
shell> cd /etc/AdGuardHome

脚本同时会创建一个 systemctl 服务,你可以通过如下命令查看运行情况

# 出现下列语句即服务运行正常
shell> systemctl status AdGuardHome
···
Active: active (running) ···
···

AdGuardHome默认监听TCP的3000号端口,可以通过以下命令查看它的占用情况

# 查询AdGuardHome在TCP端口的监听情况
shell> netstat -tlnp | grep AdGuardHome
tcp6       0      0 :::3000                 :::*                    LISTEN      22587/AdGuardHome
# 可以确定AdGuardHome在全局监听TCP的3000号端口

手动安装

在AdGuardHome项目的Release页,下载当前系统对应版本的安装包,解压到指定目录并配置为系统服务即可。

# 下载安装包
shell> wget https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.104.3/AdGuardHome_linux_amd64.tar.gz
···
# 解压到 /opt 目录下
shell> tar xf AdGuardHome_linux_amd64.tar.gz -C /opt/
# 删除下载的包
shell> rm -f AdGuardHome_linux_amd64.tar.gz
# 创建软连接到/etc下
shell> ln -s /opt/AdGuardHome/ /etc/

手动配置systemctl服务

# 创建并写入下述配置
shell> vim /etc/systemd/system/AdGuardHome.service

填入以下配置

[Unit]
Description=AdGuard Home: Network-level blocker
ConditionFileIsExecutable=/opt/AdGuardHome/AdGuardHome
After=syslog.target network-online.target

[Service]
StartLimitInterval=5
StartLimitBurst=10
ExecStart=/opt/AdGuardHome/AdGuardHome "-s" "run"

WorkingDirectory=/root

Restart=always
RestartSec=10
EnvironmentFile=-/etc/sysconfig/AdGuardHome

[Install]
WantedBy=multi-user.target

重载systemctl,开启AdGuardHome服务并设置为开机启动

shell> systemctl daemon-reload
shell> systemctl start AdGuardHome
shell> systemctl enable AdGuardHome
Created symlink from /etc/systemd/system/multi-user.target.wants/AdGuardHome.service to /etc/systemd/system/AdGuardHome.service.

查看AdGuardHome服务运行状态

# 出现下列语句即服务运行正常
shell> systemctl status AdGuardHome
···
Active: active (running) ···
···

AdGuardHome管理界面默认监听TCP的3000端口,如果服务器防火墙不拦截该端口,可以直接通过 服务器IP:3000 的方式访问,也可以按下文的方式配置反向代理。

# 查询AdGuardHome在TCP端口的监听情况
netstat -tlnp | grep 3000
tcp6       0      0 :::3000                 :::*                    LISTEN      22587/AdGuardHome
# 可以确定AdGuardHome在全局监听TCP的3000号端口

配置服务

后文使用 dns.dnomd343.top 作为示例域名

配置反向代理

配置 dns.dnomd343.top 解析到当前服务器,通过Nginx实现反向代理本地3000端口。

shell> cd /etc/nginx/conf.d/
shell> vim dns.conf

写入以下配置,注意这里需要TLS证书,申请方式可见acme.sh使用说明

server {
    listen 80;
    server_name dns.dnomd343.top;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name dns.dnomd343.top;
    ssl_certificate /etc/ssl/certs/dnomd343.top/fullchain.pem;
    ssl_certificate_key /etc/ssl/certs/dnomd343.top/privkey.pem;

    location / {
        proxy_pass http://localhost:3000;
    }
}
# 重启nginx
shell> nginx -s reload

此时访问 https://dns.dnomd343.top,你的访问数据会被代理到AdGuardHome提供的服务端口上

如果配置正确会显示如下网页

Step-1

点击下一步后开始配置端口

Step-2

网页管理界面监听接口按情况分类,如果是反向代理模式可以选 lo - 127.0.0.1,否则可以选所有接口,端口将默认的80改为3000。DNS服务器选择监听所有接口,端口为53不变用于提供常规DNS服务。不过,如果你的服务器上运行有其他DNS软件(BIND等),53端口会可能被占用,你也可以按需要改成其他端口。

Step-3

输入用户名和密码后下一步

Step-4

这一步会显示一些提示,上方的监听地址分别为本机回环地址和内网IP

Step-5

到这一步就配置完成了,点击进入仪表盘

注意,此时网页会跳到 http://dns.dnomd343.top:3000/login.html 的登陆页面,如果你的防火墙没有放行3000端口,这里将会出现无法访问的错误,需要手动转到 https://dns.dnomd343.top/login.html 的反向代理页面。

如果不想访问dns.dnomd343.top时直接跳出AdGuardHome的登录页,可以将其隐藏在 /admin 下,注意这里必须配置 login.html 页面的跳转,否则访问 https://dns.dnomd343.top/admin 将会跳转到 https://dns.dnomd343.top/login.html 导致404错误。

shell> vim /etc/nginx/conf.d/dns.conf

修改配置文件为以下内容

server {
    listen 80;
    server_name dns.dnomd343.top;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name dns.dnomd343.top;
    ssl_certificate /etc/ssl/certs/dnomd343.top/fullchain.pem;
    ssl_certificate_key /etc/ssl/certs/dnomd343.top/privkey.pem;

    location /admin/ {
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://localhost:3000/;
    }

    location /login.html {
        return 301 https://$server_name/admin/login.html;
    }
}
# 重启Nginx
shell> nginx -s reload

此时访问 https://dns.dnomd343.top 将会是nginx的默认主页,而访问 https://dns.dnomd343.top/admin 将会跳转到AdGuardHome的登录页面。

登录界面

输入设置的用户名和密码后进入仪表盘。

仪表盘界面

你可以在这里调整AdGuardHome的各项配置以满足需求。

配置DNS上游

在设置 - DNS设置中可以设置上游DNS服务器,当AdGuardHome收到合法的DNS请求后,会按配置通过规则以及host等模块拦截或改写一部分查询,剩余的请求就会转发给上游DNS服务器。与下游查询协议类似,AdGuardHome同样也支持多种上游DNS查询协议,即常规DNS、DoH、DoT、DoQ和DNSCrypt等,通过这些协议向大型公共DNS请求数据。

由于服务器在国外基本不受到DNS污染,可以直接使用常规DNS查询,一般情况下可用直接设置为常用公共DNS的IP地址,如Cloudflare DNS(1.1.1.1)、Google DNS(8.8.8.8)、Quan9 DNS(9.9.9.9)等,你也可以在上文公共DNS服务器的列表中选择其它服务或协议。

在配置完成后,AdGuardHome就已经能正常运作了,为了测试服务的可用性,可以发起常规DNS查询测试是否配置成功。

# 向配置好的服务器发起常规DNS请求
shell> dnslookup baidu.com 47.242.30.65
dnslookup v1.4.5
dnslookup result:
;; opcode: QUERY, status: NOERROR, id: 26085
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;baidu.com.     IN       A

;; ANSWER SECTION:
baidu.com.      415     IN      A       39.156.69.79
baidu.com.      415     IN      A       220.181.38.148
# 正确返回了百度服务器的IP地址

这里不要用谷歌、油管等黑名单网站测试,否则怎么查询都会返回一个错误的IP。

配置加密DNS

选择设置 - 加密设置,在此处配置上文提及的几种加密DNS协议

加密设置

勾选启用加密,开启AdGuardHome的加密DNS协议支持,在这一页可以配置DoH、DoT与DoQ的选项。如果你当前的TCP/443端口已经被nginx或其他网页服务器占用,右下角会出现如图443端口不可用的报错,此处直接忽略即可。

443端口错误

服务器名称填入用于DoH与DoT的域名,后方的重定向无需勾选,这个功能是将来自80端口的HTTP流量重定向到HTTPS,而我们这部分功能已经交给Nginx处理了。

此处HTTPS端口不同于之前配置的3000端口,这个端口负责提供DoH查询服务,同时也兼具网页管理的功能,总地来讲,可以认为它与此前的3000功能相同,但是在 /dns-query 下额外加入了DoH支持。这里的端口号我们直接将其设置为3001即可,同样不对外暴露,交给Nginx处理。至于DoT与DoQ的端口,则保持默认的 853784 不变,直接对外提供服务。

下方需要我们提供TLS证书和私钥,内容对应上方填写的服务器名称,方式建议选择设置路径而不是粘贴内容,这样子以后更新证书的时候就不需要重新填写。此处还要注意证书私钥的权限问题,一般情况下它会被设置为其他用户不可读的状态,遇到这种情况可以手动配置文件属性,或者将AdGuardHome配置为root用户下运行。

此外,如果你的证书不仅包含域名,还包含服务器IP地址,那将可以以IP地址的形式进行DoH和DoT查询。

在改完并保存后,网页会自动跳转到 https://dns.dnomd343.top:3001/#encryption ,与上文的情况一样,如果防火墙未放行3001端口将会出现连接超时的错误。不过这里我们并不需要这个页面,而是配置Nginx的反向代理来接管3001端口的DoH查询流量,识别并把它转交给AdGuardHome。

shell> vim /etc/nginx/conf.d/dns.conf

修改配置文件为以下内容

server {
    listen 80;
    server_name dns.dnomd343.top;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name dns.dnomd343.top;
    ssl_certificate /etc/ssl/certs/dnomd343.top/fullchain.pem;
    ssl_certificate_key /etc/ssl/certs/dnomd343.top/privkey.pem;

    location /admin/ {
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://localhost:3000/;
    }

    location /login.html {
        return 301 https://$server_name/admin/login.html;
    }

    location /dns-query {
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass https://127.0.0.1:3001/dns-query;
    }
}
# 重启Nginx
shell> nginx -s reload

这种配置下,Nginx一旦接到 https://dns.dnomd343.top/dns-query 的访问,就会把内容转交给本地3001端口上的AdGuardHome处理。另外,/dns-query 部分的 proxy_set_header 参数必须加上,以达到在HTTP头中添加客户端来源端口的目的,否则AdGuardHome的统计中将无法出现真实的客户端IP。

配置DNSCrypt

AdguardHome目前还不支持在前端直接配置DNSCrypt协议,需要你在后台手动改写配置文件,配置文件中将会提供证书名称,私钥公钥等其他信息。

这份配置需要使用dnscrypt工具生成DNSCrypt的密钥对,直接下载最新版Release到本地运行。

# 获取release并解压
shell> wget https://github.com/ameshkov/dnscrypt/releases/download/v2.0.2/dnscrypt-linux-amd64-v2.0.2.tar.gz
···
shell> tar xf dnscrypt-linux-amd64-v2.0.2.tar.gz
# 直接复制安装
shell> mv linux-amd64/dnscrypt /usr/bin/
# 删除残余文件
shell> rm -rf dnscrypt-linux-amd64-v2.0.2.tar.gz linux-amd64/

生成一对密钥,使用 --provider-name 指定证书名,--out 指定输出文件

shell> dnscrypt generate --provider-name '2.dnscrypt-cert.dnomd343.top' --out /opt/AdGuardHome/dnscrypt.yaml
2021/01/26 03:35:40 [info] Generating configuration for 2.dnscrypt-cert.dnomd343.top
2021/01/26 03:35:40 [info] Configuration has been written to /opt/AdGuardHome/dnscrypt.yaml
2021/01/26 03:35:40 [info] Go to https://dnscrypt.info/stamps to generate an SDNS stamp

查看生成的内容

shell> cat /opt/AdGuardHome/dnscrypt.yaml
provider_name: 2.dnscrypt-cert.dnomd343.top
public_key: 358DEA7C201B068DCD8A1DFCF607C951F6FAC2148ACDC602B176A810818B30D1
private_key: 2360AA8CD96516D0E29948F64A51ECF54E95A9924AD342285FA125C0A4156278358DEA7C201B068DCD8A1DFCF607C951F6FAC2148ACDC602B176A810818B30D1
resolver_secret: FD0927401827CF442A7CC530E6F401395A3986F637168F3E7100230184C5F26A
resolver_public: E647313744A35958775E35225BB948C09E35E45BDF939B15D9188CB80F80416F
es_version: 1
certificate_ttl: 0s

编辑AdGuardHome的配置文件

shell> vim /opt/AdGuardHome/AdGuardHome.yaml

port_dnscrypt 从0改为5443(也可以是其他自定义端口),dnscrypt_config_file 填入刚刚生成的配置文件路径

配置文件

tls:
  ···
  port_dnscrypt: 5443
  dnscrypt_config_file: /opt/AdGuardHome/dnscrypt.yaml
  ···

重载AdGuardHome

shell> systemctl restart AdGuardHome

如果成功支持了DNSCrypt,那系统的TCP与UDP的5443端口将被AdGuardHome占用

# 查看AdGuardHome是否成功占用5443端口
shell> netstat -tlnpu | grep 5443
tcp6       0      0 :::5443                 :::*                    LISTEN      29538/AdGuardHome
udp6       0      0 :::5443                 :::*                                29538/AdGuardHome
# 成功启用了DNSCrypt

在使用时,我们需要提供 IP地址及端口号 公钥 证书名称 这三个数据,从生成的 /opt/AdGuardHome/dnscrypt.yaml 文件中可以拿到这些信息,其中公钥为 public_key 内容,证书名称为 provider_name

接着,我们需要把这些信息打包成DNS Stamp格式,你可以在这里在线生成sdns链接,填入服务器IP和端口、公钥、证书名称即可,复制右端生成的sdns链接。

DNS Stamp

sdns://AQcAAAAAAAAAETQ3LjI0Mi4zMC42NTo1NDQzIDWN6nwgGwaNzYod_PYHyVH2-sIUis3GArF2qBCBizDRHDIuZG5zY3J5cHQtY2VydC5kbm9tZDM0My50b3A

DoQ多端口支持

AdGuardHome 的DoQ默认监听在UDP的 784 端口,但有部分客户端默认端口为 8853 ,为了保障兼容性,需要同时兼容两个端口。

原理上,我们可以直接使用 iptables 内核模块改写UDP流量目标,将访问8853端口的UDP流量重定向到784端口,这个过程并不是用户层流量转发,因此 AdGuardHome 中记录的客户端IP地址不变(如果借助socat等工具来转发,客户端IP地址会变成 127.0.0.1)。使用root权限执行以下命令:

# 添加重定向
shell> iptables -t nat -A PREROUTING -p udp --dport 8853 -j REDIRECT --to-port 784

# 查看添加结果
shell> iptables -t nat -L
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
REDIRECT   udp  --  anywhere             anywhere             udp dpt:8853 redir ports 784

Chain INPUT (policy ACCEPT)
target     prot opt source               destination
···

# 删除重定向(此处1表示为第1行,应视具体情况修改)
shell> iptables -t nat -D PREROUTING 1

若服务器支持IPv6,需使用ip6tables命令多执行一次,以支持IPv6下的UDP 8853 端口查询

为了方便使用,可执行以下脚本自动添加:

if [ `whoami` = "root" ]; then
  STATUS=$(iptables -t nat -L | grep 8853)
  if [ -z "$STATUS" ]; then
    iptables -t nat -A PREROUTING -p udp --dport 8853 -j REDIRECT --to-port 784
  fi
fi

可将脚本保存到 /etc/profile.d/ 目录下实现开机自启。

# 测试多端口是否正常
shell> dnslookup google.com quic://dns.dnomd343.top:784
···
shell> dnslookup google.com quic://dns.dnomd343.top:8853
···
# 两者均输出正确DNS响应即配置成功

测试服务

到这里已经将全部协议配置好了,我们可以通过AdGuardHome的端口占用情况来回顾各DNS查询协议。

# 列出AdGuardHome占用的全部TCP与UDP端口
shell> netstat -tlnpu | grep AdGuardHome
tcp6       0      0 :::853                  :::*                    LISTEN      24295/AdGuardHome
tcp6       0      0 :::53                   :::*                    LISTEN      24295/AdGuardHome
tcp6       0      0 :::3000                 :::*                    LISTEN      24295/AdGuardHome
tcp6       0      0 :::3001                 :::*                    LISTEN      24295/AdGuardHome
tcp6       0      0 :::5443                 :::*                    LISTEN      24295/AdGuardHome
udp6       0      0 :::53                   :::*                                24295/AdGuardHome
udp6       0      0 :::784                  :::*                                24295/AdGuardHome
udp6       0      0 :::5443                 :::*                                24295/AdGuardHome
  • TCP/853 :DNS-over-TLS

  • TCP/53 :常规DNS

  • TCP/3000 :网页管理界面

  • TCP/3001 :DNS-over-HTTPS,同时也是网页管理界面

  • TCP/5443 :DNSCrypt

  • UDP/53 :常规DNS

  • UDP/784 :DNS-over-QUIC

  • UDP/5443 :DNSCrypt

以上端口中,建议防火墙屏蔽 TCP/3000TCP/3001,借助Nginx反向代理,其他端口均放行。

最后,我们仍然借助dnslookup工具对建成的全协议DNS服务器进行测试。这里目标域名可以任意选择,因为查询过程都是加密链路,基本不受墙的DNS污染影响。

# DoH测试
shell> dnslookup google.com https://dns.dnomd343.top/dns-query
···

# DoT测试
shell> dnslookup google.com tls://dns.dnomd343.top
···

# DoQ测试
shell> dnslookup google.com quic://dns.dnomd343.top
···

# DNSCrypt测试
shell> dnslookup google.com sdns://AQcAAAAAAAAAETQ3LjI0Mi4zMC42NTo1NDQzIDWN6nwgGwaNzYod_PYHyVH2-sIUis3GArF2qBCBizDRHDIuZG5zY3J5cHQtY2VydC5kbm9tZDM0My50b3A
···

# 以上DNS接口已经停用,仅供示例

写在最后

可用性与普适性

正常情况下,大公司会在世界各地配置多台服务器来应对不同业务,如果直接使用自建的境外服务器做DNS解析,会把域名解析到国外的服务器上,由于国内直连相对较慢,将会导致网页访问卡顿。对于这种情况,可以配置EDNS来缓解这个问题,将来源IP地址一并加入请求中,从而正确地将域名解析到国内服务器上,不过它并不能完善地解决问题,上游DNS不一定支持扩展协议,也不一定就能返回最佳结果。因此,最佳的解决方案应该是建立分流器,将请求分为国内组与国外组,国内网站走国内公共DNS,国外网站走境外无污染DNS,这样就可以相对稳定地提供服务。前者我们使用阿里、腾讯等国内大型公共DNS服务器的DoH或DoT,免去运营商的劫持,而后者就只能靠我们自己的境外DNS服务器来实现,毕竟大部分国外DoH、DoT服务都被防火长城干扰或阻断。

基于上面的思路,不少人的第一反应是用国内服务器做分流,这个方法有诸多问题。首先就是国内三大运营商的网络之间通讯较慢,因此大型网站一般都会在各大运营商网络下设置多台服务器,通过配置DNS把不同网络的请求解析到对应服务器上,这将导致国内服务器统一请求的DNS不能兼容多个运营商,无法解析到最适合的服务器,虽然支持EDNS的服务可以在一定程度上修正这个问题,不过同样也不能确保解析结果的普适性。另一个方面则源自国内的管控,根据相关法律,普通人是不能在境内私自搭建DNS服务器的,没有备案的话就会收到服务商的警告与封锁,虽然搭建DoH服务可以绕过云服务商的监测,但是普适性就大大下降了。综合上述问题,使用境内服务器配置DNS分流并不是一个完善的解决方案。

更常规的做法是在本地路由器或内网设置一个DNS服务器,在其上配置完善的DNS分流机制,即使失去了移动设备外出时的可用性,但也没有上述公网服务器的问题。目前确实有很多基于这方面的实现,结合 DNSmasqSmartDNSChinaDNS 等工具做分流与优选,同时加上去广告、强制重定向等功能,也有封装了完整配置的Docker镜像可以作为容器直接使用。

有效性与必要性

回到问题的本质上,我们要对抗的是DNS污染与劫持,即运营商的劫持和GFW的污染。解决前者很轻松,正常情况下不使用运营商分配的DNS就可以避免,而后者则要棘手得多,我们可以从有效性和必要性这两个方面来讨论。

要知道,GFW在屏蔽域名或IP时有多种手段,针对域名的封锁最基本也最方便的就是DNS污染,以极低的成本就能让普通用户无法正常访问,早期不少屏蔽都是针对DNS的,因而兴起了修改host或是基于加密DNS的抗污染工具,即使到现在,对于部分域名的屏蔽仍是最简单的DNS污染,这也是无污染DNS唯一能解决的封锁方式。而在针对连接方面,封锁IP可以粗暴地从网络层直接切断,我们对此无能为力,但针对域名的连接识别与阻断则稍微复杂。

在早期,大部分网站基于明文HTTP协议,GFW可以对HTTP请求头的host参数或内容敏感词进行匹配。而随着HTTPS的兴起,HTTP数据嵌套在SSL/TLS加密层内,防火长城无法窥探其中内容,但是它可以识别到访问目标IP与连接host,并以此屏蔽黑名单网站。一般情况下,GFW会对TLS连接的SNI进行嗅探,或者读取服务器返回的证书(当然前者成本更低),一旦识别成功就用中间人身份重置TCP连接,从而达到屏蔽作用。在TLS1.3中,新增的ESNI特性通过域前置方式,有效地加密了SNI内容,从而规避掉SNI嗅探,对于这种流量,GFW只有两个选择,不拦截或全部拦截,而显然的,防火长城选择了后者,直接暴力地屏蔽了所有带有ESNI的TLS1.3流量,具体的细节可以关注这篇文章

所以,抗污染DNS服务在面对GFW的连接封锁方面是无力的,其上限也只是抵御一部分屏蔽,而上述TCP阻断等屏蔽措施都是在DNS层面无法解决的。不过,大多数情况下有总比没有强,况且还有为数不少的黑名单域名只享受到了DNS污染的待遇,GFW没有足够的资源或精力去阻断这些连接,因此自建DNS服务还是可以在一定程度上优化体验的。而对于科学上网的需求,则交给SS、SSR、Trojan、VMess、VLESS等更专业的协议去完成,同样的,它们可以把DNS请求转发到远端给海外服务器,算是一举两得,在绕过防火长城屏蔽的同时,也无需再考虑DNS污染问题。

因此,自建DNS的有效性是不可否认的,它确实能抵抗污染与劫持,绕过一部分DNS封锁,但是效果上并不是十全十美,这也就引出了必要性问题,在GFW面前,直接使用代理协议无疑是更为明智的选择,它们绕过了所有封锁的同时还加快了访问速度。不过再说回来,自建DNS也不是一无是处,至少在优化体验的同时能与代理协议互补,总体上还是值得的。