大概讲一讲关于防污染 DNS 的一点想法。

DNS污染简介

关于DNS污染,前人已经说的很清楚了,所以在此不再赘述,直接引用相关论述。

DNS(Domain Name System)污染是GFW的一种让一般用户由于得到虚假目标主机IP而不能与其通信的方法,是一种DNS缓存投毒攻击DNS cache poisoning)。其工作方式是:对经过GFW的在UDP端口53上的DNS查询进行入侵检测,一经发现与关键词相匹配的请求则立即伪装成目标域名的解析服务器(NS,Name Server)给查询者返回虚假结果。由于通常的DNS查询没有任何认证机制,而且DNS查询通常基于的UDP是无连接不可靠的协议,查询者只能接受最先到达的格式正确结果,并丢弃之后的结果。对于不了解相关知识的网民来说也就是,由于系统默认使用的ISP提供的NS查询国外的权威服务器时被劫持,其缓存受到污染,因而默认情况下查询ISP的服务器就会获得虚假IP;而用户直接查询境外NS(比如OpenDNS)又可能被GFW劫持,从而在没有防范机制的情况下仍然不能获得正确IP。然而对这种攻击有着十分简单有效的应对方法:修改Hosts文件。但是Hosts文件的条目一般不能使用通配符(例如*.blogspot.com),而GFW的DNS污染对域名匹配进行的是部分匹配不是精确匹配,因此Hosts文件也有一定的局限性,网民试图访问这类域名仍会遇到很大麻烦。

——《深入理解GFW:DNS污染

/images/2019/gfw_dns/process.png

引自《翻墙路由器的原理与实现

对于DNS污染,最简单的因应方法便是修改Hosts文件,但Hosts文件的条目一般不能使用通配符,手机、ipad等移动设备很难修改Hosts文件。

因此,使用防污染DNS是一个更好的应对措施。

目前流行的防污染DNS项目分析

防DNS污染最简单的方法便是使用国外的递归DNS服务器。

虽然53端口已经被污染,无法直接使用,但通过更换端口,使用TCP查询, 使用 DNS over TLS、DNS over HTTPS、dnscrypt-proxy 等手段, 仍能通过国外的公共递归DNS服务器,获取到无污染的DNS结果,从而实现防DNS污染。

但这种做法的缺点是对CDN不友好,无法获得本地CDN节点。 虽然现在有了 ECS(EDNS Client Subnet) ,但支持ECS的公共DNS不多,截止2017年只有OpenDNS、Google Public DNS支持ECS。

因此对于CDN这个问题,绝大多数防污染DNS项目通过国内、国外分流来解决。

下面具体分析几个项目:

dnsmasq-china-list

项目地址:https://github.com/felixonmars/dnsmasq-china-list

既要防污染又要DNS友好,最简单的办法便是分区解析,国内的域名使用国内的公共DNS,国外域名使用国外DNS。 这样一来,岂不是既解决了DNS污染又对CND友好。

但要让国内域名国内解析,你需要知道哪些域名是国内的,可是域名不像IP,无法直接判断区域。 dnsmasq-china-list 这个项目便是为了这个问题而开设,这个项目收集了大量国内网站的域名,为国内国外域名分流解析提供了重要依据。

因为这个项目影响重大,很多防污染DNS项目都基于 dnsmasq-china-list,所以将其首先介绍。


但这个项目的缺点也十分明显:

  • 列表条目过多,仅 accelerated-domains.china.conf 一个文件就有 1.89 MB。

  • 更新频率过高,使用者必须不断下载最新的配置文件。

/images/2019/gfw_dns/dnsmasq-china-list-commits.png

可怕的更新频率

ChinaDNS

项目地址:https://github.com/shadowsocks/ChinaDNS

ChinaDNS 这个项目最大的特点是无须体积巨大的国内域名列表,只需要一个国内IP列表就可以了。

相比域名列表,IP列表体积小了很多,IPIP 公布的中国大陆地区 IP 列表体积仅为 85.7 KB,比之前的域名列表小太多了。 而且 IP 分配相对固定,也无需经常更新 IP 列表。

ChinaDNS 的基本思路是:

  1. 配置时设置一个国内DNS、一个国外DNS。

  2. 当收到DNS请求时,同时向国内国外两个DNS发起请求。

  3. 根据国内DNS的解析结果,向用户返回最终结果。如果国内DNS解析结果为国内IP,则返回国内DNS结果。如果国内DNS解析结果为国外IP,则返回国外DNS解析结果。

分析上述流程,可知ChinaDNS分流基于一个核心假设,那就是被GFW污染域名一定会返回非大陆IP。

这个假设目前是成立的,但并没有什么客观原因保证它一定成立。 也许哪一天,GFW 把 Google、Twitter、Facebook 都返回成翻墙警告页,向宵小们一展厉害国国威,也不是没有可能。


最后,ChinaDNS项目的国内IP列表是根据Whois数据得来的。

WHOIS数据仅表示某个IP被哪个机构注册,但无从知晓该IP被用在何处,这就导致许多非运营商自己注册的IP地址无法被正确分类。ipip.net是最早开始做BGP/ASN数据分析的公司之一,数据准确性甩其它库几条街。 所以建议直接使用IPIP的中国大陆地区 IP 列表

Pcap_DNSProxy

项目地址:https://github.com/chengr28/Pcap_DNSProxy

直接过滤掉GFW伪造的DNS包,没有什么好说的。 直接贴上官方的介绍吧。

Pcap_DNSProxy 是一个基于 WinPcap/LibPcap 用于过滤 DNS 投毒污染的工具,提供便捷和强大的包含正则表达式的修改 Hosts 的方法,以及对 DNSCurve/DNSCrypt 协议、并行和 TCP 协议请求的支持。多服务器并行请求功能,更可提高在恶劣网络环境下域名解析的可靠性。

我的一点想法

被GFW污染的域名毕竟是少数,那为什么不采用黑名单机制,仅将被污染域名发往国外DNS进行解析,其它域名则在本地进行解析。

由于GFW的DNS污染模块存在缺陷,无论请求的记录是什么(AAAA、MX、NS…),返回的均为A记录。 因此会日志中留下相关记录,所以使用黑名单机制可以根据日志自行发现被污染域名,而无须依赖外部列表。

/images/2019/gfw_dns/bind-log.thumbnail.png

GFW留下的相关日志


本人尝试在本地使用 bind 自行搭建使用黑名单机制的防污染DNS,下面简单介绍一下实现过程, 相关代码可以到 Github 下载。

判断一个域名是否遭到DNS污染

使用黑名单机制的第一步便是判断一个域名是否遭到DNS污染。

第一节可知GFW的DNS污染是基于旁路抢答实现的。 GFW为了实现无死角全覆盖的DNS污染,直接劫持了53端口,对所有可疑请求进行抢答。 因此可以利用这一点,主动探测被污染域名。

  1. 找一个境外未开放53端口的主机。

  2. 向这个主机发起DNS查询。

  3. 由于其未开放53端口,不可能做出DNS响应。所有响应均为伪造的。

未被污染域名:

~ % dig www.4399.com @example.com

; <<>> DiG 9.13.7 <<>> www.4399.com @example.com
;; global options: +cmd
;; connection timed out; no servers could be reached

被污染域名:

~ % dig www.google.com @example.com

; <<>> DiG 9.13.7 <<>> www.google.com @example.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 24322
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

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

;; ANSWER SECTION:
www.google.com.         176     IN      A       31.13.79.17

;; Query time: 43 msec
;; SERVER: 93.184.216.34#53(93.184.216.34)
;; WHEN: Thu Feb 28 21:22:32 CST 2019
;; MSG SIZE  rcvd: 48

~ % dig cmx.im @example.com

; <<>> DiG 9.13.7 <<>> cmx.im @example.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 53543
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;cmx.im.                                IN      A

;; ANSWER SECTION:
cmx.im.                 196     IN      A       31.13.64.49

;; Query time: 41 msec
;; SERVER: 93.184.216.34#53(93.184.216.34)
;; WHEN: Thu Feb 28 21:22:42 CST 2019
;; MSG SIZE  rcvd: 40

防火墙的缺陷:

~ % dig cmx.im AAAA @example.com

; <<>> DiG 9.13.7 <<>> cmx.im AAAA @example.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 11667
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;cmx.im.                                IN      AAAA

;; ANSWER SECTION:
cmx.im.                 169     IN      A       8.7.198.45

;; Query time: 150 msec
;; SERVER: 93.184.216.34#53(93.184.216.34)
;; WHEN: Thu Feb 28 21:26:51 CST 2019
;; MSG SIZE  rcvd: 40

~ % dig cmx.im MX @example.com

; <<>> DiG 9.13.7 <<>> cmx.im MX @example.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 17918
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;cmx.im.                                IN      MX

;; ANSWER SECTION:
cmx.im.                 130     IN      A       67.228.221.221

;; Query time: 44 msec
;; SERVER: 93.184.216.34#53(93.184.216.34)
;; WHEN: Thu Feb 28 21:26:56 CST 2019
;; MSG SIZE  rcvd: 40

~ % dig cmx.im NS @example.com

; <<>> DiG 9.13.7 <<>> cmx.im NS @example.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12967
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;cmx.im.                                IN      NS

;; ANSWER SECTION:
cmx.im.                 67      IN      A       31.13.83.1

;; Query time: 44 msec
;; SERVER: 93.184.216.34#53(93.184.216.34)
;; WHEN: Thu Feb 28 21:27:01 CST 2019
;; MSG SIZE  rcvd: 40

因此只要有域名列表,便能可以根据上述原理扫描出被污染域名。

Alexa 免费提供了 Top 1,000,000 网站的列表,你可以在这里下载。

使用这个列表,再加上 zdns 这个高效的 DNS 查询工具,便可以快速扫出 Alexa top 1 million 中被DNS污染的域名清单。

初始化

虽然可以根据日志,逐渐发现被污染的域名,但这需要时间。为了良好的用户体验,我们需要初始化。

一切开始之前,你需要安装 zdns 以及 python-tldextract

安装好之后,下载此项目

git clone https://github.com/yingziwu/neatdns.git

根据实际情况,适当修改 env.sh,然后运行 init.sh

bash init.sh

该脚本将会耗时30分钟。

如果你的网络状况良好,可以修改 zdns -threads 参数来缩减扫描时间,-threads 默认为 1000 进程。

脚本运行完毕后,将会在 $BIND_DOMAIN_LIST_PATH 输出被污染的域名列表,请保证 $BIND_DOMAIN_LIST_PATH 所在目录您有写入权限。

当然如果你不想等待这半个小时,你可以直接下载这个列表(在线查看)。

配置 bind

bind 默认配置,无需太多更改,做出如下修改就可以了。

添加日志模块:

logging {
  channel resolverlog {
    file "/var/log/named/resolver.log" versions 5 size 1m;
    severity notice;
    print-time yes;
    print-severity yes;
  };
};

转换域名列表:

#!/usr/bin/env python3

import json

dns_server_list = ['202.38.93.153','202.141.176.93','202.141.162.123']
base_dir = '/etc/bind/'

def gen_zone(domain,dns_server_list):
  temple_1 = 'zone "%s"'
  temple_2 = ' {type forward; forward first; forwarders { %s};};'
  zone = temple_1 % domain
  forwarders = ''
  for dns_server in dns_server_list:
      forwarder = dns_server + ';' + ' '
      forwarders = forwarder + forwarders
  zone = zone + temple_2 % forwarders + '\n'
  return zone

def gen_gfw_zones():
    with open(os.path.join(base_dir,'domain_list_poisoning.json'),'r') as f:
        domain_list = list(set(json.load(f)))
    domain_list.sort()

    with open(os.path.join(base_dir,'named.conf.gfw-zones'),'w') as f:
        for domain in domain_list:
            t = gen_zone(domain,dns_server_list)
            f.write(t)
    return

if __name__ == '__main__':
  gen_gfw_zones()

named.conf 文件中包含 named.conf.gfw-zones 文件:

include "/etc/bind/named.conf.gfw-zones";

定时更新

定时运行 update.sh 即可,建议一小时运行一次。

定时验证,去除无效条目

定时运行 validate.sh 即可,建议两周运行一次。