被某科学の壁弄得忍无可忍,上周折腾了两天搭建了一个抗污染的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应答
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: 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,重启服务即可

Last modification:August 21, 2020
(๑´ڡ`๑)