被某科学の壁弄得忍无可忍,上周折腾了两天搭建了一个抗污染的DNS服务器出来……写篇日志记录一下,顺便分享一下搭建经验;另外作为一名菜物理学生不懂技术细节,因此讲故事的部分只好大量参考了大佬们的文章_(:з)∠)_
话说DNS污染
- PS. 这里有篇有意思的文章,有兴趣可以看一下
在DNS被设计出来的年代,互联网才刚刚出现,网络安全也并不受到重视,所以最初的DNS协议本身几乎没有安全方面的考量,它默认使用的是无连接的UDP协议,也没有任何的加密或认证措施,这使得DNS包伪造变得及其简单。尤其是在攻击者拥有网络基础设施的控制权时,只要向DNS客户端发送一个简单地构造的响应包,并且设置好UDP包的源地址和源端口,客户端没有任何办法来分辨伪造的数据包。虽然DNS也支持使用TCP进行传输,但这也仅仅是略微提高了攻击的难度而已。
壁在出口路由上监控经过的DNS请求,发现有对黑名单内的域名的请求,就会实行相应的封锁措施。
对于使用UDP的DNS请求,壁会伪装成DNS服务器,给查询者返回伪造的DNS响应包。相对于实际的DNS响应,伪造的响应显然能够更早地抵达查询者,并被误认为是DNS服务器的响应,随后抵达的真正的响应包却会被丢弃,这被称为“抢答”;对于使用TCP的情况,壁则是使用了与应对其他明文TCP连接时一样的连接重置的方法,使得TCP的DNS查询得不到回应。
直接查询国外的DNS服务器毫无疑问会受到之前说的封锁措施的影响,DNS抢答只会发生在通信经过部署了GFW的出口路由时,所以直接对国内的DNS服务器的请求不会被抢答。但是这些服务器本身要得知被查询的域名的IP地址,还是要向国外的DNS服务器进行递归查询并存储到缓存中,这一过程会被GFW抢答所影响,所以最终返回给用户的响应也是错误的。这种间接的DNS包伪造被称为DNS缓存污染。
搭建智能的DNS服务器
- PS. 受到fotile96大佬的提点,拜读了这篇文章后得到的启发
从ChinaDNS开始
从前面的内容,我们知道,首先,国内的递归DNS服务器由于缓存被污染,已经无法得到正确的查询结果,所以也就没有办法继续使用,而向境外的DNS服务器的查询请求又会由于在国际出口处上被壁抢答,同样会返回错误的结果。
但是壁并没有drop原有的查询请求,一般来说,这个向外的查询请求是可以正确到达境外的DNS服务器的,并且相应的DNS服务器也会返回正确的结果;因此,通过比较本地DNS(ISP提供的DNS,或者114.114.114.114之类)和可信DNS(一般是通过加密传输的Google或者OpenDNS之类),我们可以整理出一个被封锁的网站黑名单出来(当然也可以整一个白名单出来),ChinaDNS于是根据这个名单选择不同的DNS进行解析,白名单:直接最快DNS解析,黑名单:慢但可信的DNS解析,两者均miss,则都查询一遍,然后考虑要不要加入黑名单;这样基本上达到了兼顾功能(反污染)和效率(国内跳过)的效果。
不过这个思路也有缺点:如果启用白名单,截至2019.04.06,该名单已经包含了58374条记录,对很多路由器上的dnsmasq来说,查询这个列表是个不小的压力,然而实际上列表中的绝大部分域名,一般人从来都不会用到;但如果我们为了减轻路由器压力关闭了白名单,软件可能会非常频繁的去查询那个较慢的可信DNS,由于目前绝大部分大中型网站会在不同区域部署CDN并根据DNS服务器实际位置分别解析以此将用户导向至最快的CDN节点,因此对境外DNS查询的时候有很大概率会返回一个对本地负优化的解析结果。
后者造成的问题可能会非常严重:可能导致用户被强制跳转到对应网站的海外版本网站、在访问视频网站时由于分配到海外DNS服务器导致浏览体验极差等等……这是我们难以接受的。
改进思路
为了对我们的抗污DNS做出改进,我们提出以下需求:
- 不使用庞大的黑名单和白名单
- 确保解析的结果对你的ip进行优化
针对这两点,技术上其实已经有了解决方案,那就是RFC 7871(Client Subnet in DNS Queries, aka edns-client-subnet, ECS); ECS 允许DNS解析的请求方附带一个网络地址,要求DNS服务器做出针对这个地址优化的解析响应。
但是ECS目前的实施并不顺畅,国内大厂有自己的成熟智能解析方案,国外厂商考虑隐私不愿意推进部署,因此Google 提供了一种迂回的解决方案:DNS-over-https,通过 HTTPS 协议可以稳定获取 ECS 响应。
具体实现细节
硬件:一台境内VPS,带宽合适,最好是BGP线路;一台境外VPS,带宽合适,与境内那台VPS连接通畅,延迟越低越好;
(后记:如果你不想多购置一台服务器来跑DNS服务,同样可以在本地机器甚至路由器上运行Unbound或者DoH,这里有另一个项目DNSCrypt就非常适合简单部署在内网环境,它会自己选择最快的upstream对象,并且有缓存功能,开箱即用)
软件:unbound(直接通过apt安装)、DNS-over-https、nginx
运行原理:unbound放在境内,提供缓存功能 -> DNS-over-https给unbound提供IP优化的查询结果(Unbound似乎只能UDP查询) -> 境外的服务器通过反向代理Google Public DNS提供可访问的查询对象
注意:该DNS仅在境内查询时无污染,境外查询由于从外向内过了一次国际出口,会被反向污染(不过机器都在外面了还用啥境内DNS……直接1.0.0.1/8.8.8.8就完了)
编译安装DNS-over-https
curl -fsSL https://raw.githubusercontent.com/capric98/myenv/master/Go/Linux/install.sh | bash source ~/.bashrc wget https://github.com/m13253/dns-over-https/archive/v2.0.1.zip && unzip v2.0.1.zip && rm -rf v2.0.1.zip && cd dns-over-https-2.0.1 make && make install sudo systemctl start doh-client.service sudo systemctl enable doh-client.service
请注意更改对应的版本号并尽量使用较新的版本,似乎在某个版本以前因为Google更改了默认的相应格式导致DoH无法正常工作,因此我自己folk了一个版本并编译使用(clone下来make/make install即可),不清楚目前的版本有没有修复这个问题。
如果启动不成功,编辑/etc/dns-over-https/doh-client.conf,将listen里除了127.0.0.1的部分注释掉并重启服务。如果服务正常运行,并且你没有注释127.0.0.1:53的话,使用dig www.google.com @127.0.0.1应该能得到一串DNS应答
- 安装unbound
apt update
apt install -y unbound
配置unbound(请略去udp2raw部分)
wget http://www.internic.net/domain/named.root -O /etc/unbound/root.hints
编辑/etc/unbound/unbound.conf如下:
server:
username: "unbound"
interface: 172.16.18.168 #改成对应监听地址
verbosity: 1
do-daemonize: no
access-control: 0.0.0.0/0 allow
root-hints: "/etc/unbound/root.hints"
#auto-trust-anchor-file: "/usr/local/etc/unbound/root.key"
do-ip4: yes
do-ip6: no
do-udp: yes
do-tcp: yes
#logfile: "/var/log/unbound"
hide-identity: yes
hide-version: yes
harden-glue: yes
use-caps-for-id: yes
cache-max-ttl: 3600
prefetch: yes
ip-ratelimit: 50 #请求频率限制,防止恶意请求
num-threads: 2 #设置成核心数*2即可
msg-cache-size: 256m
rrset-cache-size: 512m
module-config: "subnetcache validator iterator" #ECS缓存功能
unwanted-reply-threshold: 10000000
do-not-query-localhost: no
send-client-subnet: 127.0.0.1/24 #与你的upstream在同一子网
client-subnet-always-forward: yes
minimal-responses: yes
forward-zone:
name: "."
forward-addr: 127.0.0.1 #upstream地址
重启unbound服务,此时unbound将使用本地127.0.0.1:53作为递归查询的dns,如果你的DoH使用了别的端口或者listen了别的地址(比如127.0.0.10之类),请相对应进行更改。
Nginx
这一部分是后来写的。。详细的配置文件在朋友的机器上,懒得去找相应的登录方式并查看了,内容不过是类似:
location ^~ /dns-query {
proxy_pass https://dns.google;
proxy_ssl_name dns.google;
proxy_set_header Host dns.google;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
如果反代正常的话,回去把doh-client.conf里的upstream.upstream_ietf
全删了,留一个自己反代的地址,然后weight
写100,重启服务即可
4 comments
想想你的文章写的特别好https://www.ea55.com/
Nicely put, Thanks a lot.
你的博客SSL证书过期啦(๑•̀ㅁ•́ฅ)
o-o 斗智斗勇