搭建抗污染DNS笔记

被某科学の壁弄得忍无可忍,上周折腾了两天搭建了一个抗污染的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连接通畅,延迟越低越好;

软件:unbound(直接通过apt安装),DNS-over-httpsudp2raw-tunnel,nginx

运行原理:unbound放在境内,提供缓存、ECS递归查询功能 -> udp2raw-tunnel建立内外隧道,防止递归查询被抢答 -> DNS-over-https境外提供ip优化结果

改进:将udp2raw隧道改为nginx反代Google DNS over HTTPS,保证传输安全和减少流量特征

注意:该DNS仅在境内查询时无污染,境外查询由于从外向内过了一次国际出口,会被反向污染(不过机器都在外面了还用啥境内DNS……)

  • 编译安装DNS-over-https
apt install -y unzip gccgo-go
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

如果启动不成功,编辑/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应答

dig.png

  • 安装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: 20 #请求频率限制,防止恶意请求
  num-threads: 2 #设置成核心数*2即可
  msg-cache-size: 256m
  rrset-cache-size: 512m
  module-config: "subnetcache iterator" #ECS缓存功能
  unwanted-reply-threshold: 10000000
  do-not-query-localhost: no
  send-client-subnet: 127.0.0.1
  #client-subnet-always-forward: yes #忘了为啥关了,我这里ECS是正常工作的
  minimal-responses: yes
 
  forward-zone:
    name: "."
    forward-addr: 127.0.0.1 #upstream地址

重启unbound服务,此时unbound将使用本地127.0.0.1:53作为递归查询的dns,我们接下来要把127.0.0.1:53和境外服务器的5380端口连接起来(这是之前DNS-over-https的监听端口)

  • udp2raw-tunnel(该方案存在问题,请略过)
    ~~在两台服务器上同时安装udp2raw
wget https://github.com/wangyu-/udp2raw-tunnel/releases/download/20181113.0/udp2raw_binaries.tar.gz
tar -xzvf udp2raw_binaries.tar.gz
mv udp2raw_amd64_hw_aes /usr/bin/udp2raw
rm -rf udp2raw* version.txt

在境内服务器上:

export remoteIP="127.0.0.1" #输入你的境外DNS服务器ip
echo -e "#!""/bin/bash\n/usr/bin/udp2raw -c -l127.0.0.1:53 -r ${remoteIP}:21 -a -k \"在这里输入密码\" --cipher-mode \"aes128cbc\" --auth-mode \"hmac_sha1\" >> /home/udp2raw.log" > /etc/init.d/udp2raw
chmod +x /etc/init.d/udp2raw
cd /etc/rc5.d/ && ln -s ../init.d/udp2raw S02udp2raw

在境外服务器上:

echo -e "#!""/bin/bash\n/usr/bin/udp2raw -s -l0.0.0.0:21 -r127.0.0.1:5380 -a -k \"在这里输入密码\" --cipher-mode \"aes128cbc\" --auth-mode \"hmac_sha1\" >> /home/udp2raw.log" > /etc/init.d/udp2raw
chmod +x /etc/init.d/udp2raw
cd /etc/rc5.d/ && ln -s ../init.d/udp2raw S02udp2raw

这样保证了重启后服务自动运行,不重启的话手动运行下这两个脚本也行

隧道搭建后,在境内服务器上运行下dig www.google.com @127.0.0.1看看有没有正常工作;~~

一切正常的话完成以上工作后DNS已经可以正常工作了,开放防火墙对应端口,并将本地系统的主DNS改成境内服务器的ip即可。

改进

按照上述操作使用了一段时间之后,在某一天突然后端azure的ip被墙了……感觉可能是udp2raw的某种流量特征触了壁的逆鳞,因此不再使用这种方法,想了想,一种更快的方式是在境外机器上搭建Google DNS的反代,然后用这个反代来当作DNS-over-HTTPs的query服务器,于是改进方法是:在境内服务器上安装unbound,配置同上,同时安装doh-client,并手动修改配置文件使其只能使用你的反代服务器query,同时使其监听本机127.0.0.1,境外服务器上搭建反代,并且做好伪装;目前这种方式工作良好

Last modification:May 3rd, 2019 at 10:47 am
If you think my article is useful to you, please feel free to appreciate

One comment

  1. MiSense

    o-o 斗智斗勇

Leave a Comment