翻墙 - DNS污染的原理以及应对策略
Published: 2022-03-09 11:44:48

### DNS以及DNS污染的成因（dnscrypt-proxy + dnsmasq方案）
我们都知道，互联网上的主机之间依靠数字IP地址进行通讯，但人类对这种毫无意义的纯数字的IP很不敏感，因为太难记了。于是就有了 “域名” 这个概念，它由具有一定意义的英文字母组成，比如qq.com。现在问题来了，人类能读懂的域名机器不懂，机器懂的数字IP人类又记不住，那就需要一个媒介将两者的关系绑定起来，这个媒介就叫做DNS服务器。

当我们在浏览器输入qq.com的时候，电脑并不会立刻和腾讯服务器连接上，而是先询问DNS服务器（请注意，所有的DNS服务器自身一定是以数字IP示人），请告诉我qq.com的IP地址是多少，DNS服务器回答：125.39.52.26，有了这个IP，我们才能正常浏览qq.com。

以上是大部分“正常国家”的互联网访问模式，但别忘了我们是有中国特色的社会主义国家，注定有些地方会比较有“特色”，这就是所谓的GFW. 而域名污染则是GFW其中的一个重要功能之一，那什么又是域名污染呢？当我们在浏览器输入google.com的时候，这个字符串首先会传递到你的宽带服务商为你指定的默认DNS服务器里，然后查询google.com对应的IP，GFW在这一步做了手脚，它会返回一个虚假的google.com的IP地址，导致你的浏览器无法正常与google服务器连接。

也许你曾经听说，DNS服务器好像是可以自定义设置的，那咱们不用宽带商提供给我的默认DNS服务器，而是自己设一台可信的DNS服务器不就得了，比如google公司本身也提供DNS服务，其DNS服务器地址是8.8.8.8，那我在Windows的网卡设置里将DNS服务器设为8.8.8.8不就万事大吉了？

没错，曾经有一段时间这个想法是可行的，但是很快8.8.8.8这个IP就被GFW封锁了。再后来，GFW想出了一个更卑鄙的方法，那就是不直接封杀这个IP，而是弄一台假的8.8.8.8！当你ping 8.8.8.8的时候，它确实是通的，让你产生一种错觉，既然它是通的，那么由它解析回来的结果也是可信赖的吧。如果你真的这么想，那说明你还是图样图森破了，这些假的域名服务器会为你返回各种各样稀奇古怪的IP地址，并且无一例外，全是假的。

那咱们的方案又是如何做到防止域名污染的呢？

3、dnscrypt-proxy 与 dnsmasq
首先是摒弃宽带服务器给你安排的默认DNS服务器，用自己搭建的DNS服务器取而代之，本文选择dnscrypt-proxy + dnsmasq 作为在本机搭建DNS服务器的方案。

dnscrypt-proxy是什么？首先它是一个搭建DNS服务器的程序，但它也并非仅仅是一个程序这么简单，它还是一个为了防止域名污染公益项目。由OpenDNS主导（思科公司的子公司），它在全球部署了许多可靠的DNS服务器，彼此间通过加密通道传输信息。

利用dnscrypt做域名解析好处有二：首先，作为一个不掺杂任何政治目的的公益项目，从dnscrypt解析回来的IP可以认为是真实可靠的，而不是杜撰出来的虚假IP。其次，它的数据传输相对安全，外人并不知道我在浏览哪个网站。

dnsmasq


那么dnsmasq又是什么呢？它也是一个搭建DNS服务器的程序，它不仅可以用来搭建DNS服务器，也可以用来搭建DHCP服务器。那么你可能会问，既然dnscrypt已经能提供足够强大的DNS服务了，那我们就直接用它做DNS服务器就行了，为什么还需要多来一个dnsmasq呢？表面上看起来后者似乎有些多余。

答案是访问速度问题，因为我们在访问国内网站的时候，没必要劳师动众地把dnscrypt祭出来，它的服务器毕竟在国外，域名解析速度比较慢。在访问国内网站的时候，我们随便找一个国内的公众DNS服务器就行了，著名的114，阿里，百度，清华，这些都能提供DNS服务，虽然他们返回的国外域名的IP地址肯定是有问题的，但是对于国内域名，一般来说还算靠谱。关键是在国内访问他们的速度比dnscrypt快得多。

于是，我们在本机就搭建了两个DNS服务器，其目的是为了实现：

所有域名，首先都发送到dnsmasq通道，如果是国内域名，dnsmasq调用上层国内DNS服务器（如114，阿里），返回国内域名所对应的IP，达到高速解析的效果。

如果dnsmasq发现是国外域名，则放弃解析，转交给dnscrypt，通过加密通道访问其部署在全球的可靠的DNS服务器，返回没有污染的、真实的IP。

4、智能识别国内/国外域名
新的问题又来了：dnsmasq 怎么知道哪些域名是国内域名，哪些又是国外域名呢？

这就要说到gfwlist这个项目了，它将那些被GFW列入黑名单的国外域名收集起来，导成一个文件供大家下载。

但 dnsmasq 是不能直接读懂这份文件，所以咱们还需要 dnsmasq-gfwlist 这个Python写的程序，它的作用就是将原始的gfwlist文件转换为dnsmasq能读懂的格式。

好了，现在dnsmasq终于能知道哪些域名是国内域名，哪些域名是国外域名了，基本实现了智能域名解析的目的。

域名解析这一块基本算是完成了，接下来还要解决流量问题。首先，机场给我们的流量是有限的，其次，不是所有流量都需要梯子，否则将严重影响访问速度。下一步我们要实现的是：国外网站走代理服务器，国内网站直连就好。

5、使用IPSET 对不同的流量进行定向路由


这时候ipset登场了，这东东又是干什么的？

ipset是linux系统下路由管理工具iptables的扩展，它允许创建一次匹配整个“地址”集的防火墙规则。这个官方解释挺拗口的，不容易理解，不急，先看dnsmasq的gfwlist文件，里面由几千个这样的内容组成：

server=/instagram.com/127.0.0.1#30053 这一行是关于DNS的，表示将instagram.com这个域名解析任务转交给本机30053端口去完成（30053就是dnscrypt）

ipset=/instagram.com/gfwlist 
这一行与DNS解析无关，而是将instagram.com这个域名解析后的地址放到ipset的gfwlist这张表里去

第一步，我们需要用ipset命令创建了一张表（你也可以理解为一个集合），名字叫gfwlist，请注意，这个表与上面说的那个"gfwlist文件" 名字是一样的，但性质完全不同。"gfwlist文件"存放的是国外域名，或者说GFW黑名单，而这里的"gfwlist表"存放的是这些域名所对应的IP地址。

那么，这个IP地址集合又有啥用呢？

我们知道，在Linux系统中，网络流量是可以被分流的（路由），通常使用iptables命令去完成，通过这个命令，我们可以实现定向路由：

凡不在gfwlist中的IP，走直连通道

凡被收入gfwlist中的IP，走翻墙通道

总结
最后再总结一下完整流程：

用户输入一个域名

dnsmasq介入，开始对域名进行解析

dnsmasq通过gfwlist文件做两件事：

不在gfwlist中的域名，自己解析，在gfwlist的域名，交给dnscrypt解析
在gfwlist中的域名，将解析后的IP地址，保存在ipset的gfwlist表（集合）中
iptables 对数据进行分流（定向路由），凡在gfwlist集合中的流量，走翻墙通道，否则走直连通道。

基本架构：
环境说明：本文运行环境是Centos 8.2, 官网：https://www.centos.org/

可以在官网下载ISO映像，将映像刻录到U盘启动，在实体机或虚拟机安装皆可。

本节需要配置两个DNS服务：

dnsmasq 为主DNS服务器，负责国内域名解析，通过阿里DNS获得IP，一旦发现国外域名，转交给dnscrypt-proxy处理

dnscrypt-proxy 为辅DNS服务器，负责加密传输，获取真实的境外IP

首先安装相关依赖包、工具：

```
dnf install epel-release -y
dnf install git -y
dnf install -y gcc gettext autoconf libtool automake make pcre-devel asciidoc xmlto udns-devel c-ares-devel libev-devel libsodium-devel mbedtls-devel net-tools wget bind-utils nano
```

2、配置 dnscrypt-proxy：
使用dnf安装dnscrypt-proxy：

dnf install -y dnscrypt-proxy
修改dnscrypt-proxy主配置文件：

nano /etc/dnscrypt-proxy/dnscrypt-proxy.toml
注意这行, 确保中括号里面不要有任何内容即可:

listen_addresses = [] 
[存盘退出]
修改dnscrypt-proxy的socket配置文件:

nano /usr/lib/systemd/system/dnscrypt-proxy.socket
将Socket这一节整体替换为下面这样：

[Socket]
ListenStream=
istenDatagram=
ListenStream=127.0.0.1:30053
ListenStream=[::1]:30053
ListenDatagram=127.0.0.1:30053
ListenDatagram=[::1]:30053
[存盘退出]
文件都修改完毕后，执行以下命令让他们开机自启：

systemctl start dnscrypt-proxy.socket  
systemctl enable dnscrypt-proxy.socket
systemctl start dnscrypt-proxy.service
systemctl enable dnscrypt-proxy.service
3、配置 dnsmasq：
使用dnf安装dnsmasq

dnf install epel-release -y
dnf install -y dnsmasq
systemctl start dnsmasq
systemctl enable dnsmasq
修改dnsmasq配置文件:

nano /etc/dnsmasq.conf
listen-address=127.0.0.1,192.168.1.1 (监控本机，以及内网IP地址)
expand-hosts (删掉这行前面的#号，即: 取消注释让其生效）
domain=rockage.lan （设置局域网域名）
server=223.5.5.5 （因为dnsmasq对应的是国内域名，因此我们采用阿里DNS）
server=223.6.6.6
address=/rockage.lan/192.168.1.1 (将域名与IP绑定)
dhcp-range=192.168.1.50,192.168.1.100,12h （为局域网的机器自动分配从.50到.100的IP地址）
[存盘退出]
dnsmasq 配置文件语法检查, 如果显示Error表示上述修改内容有误, 请仔细检查:

dnsmasq --test
设置dnsmasq和本机dns的关联

/etc/resolv.conf 文件是本机DNS的设置文件, 由本地守护程序（NetworkManager）维护，因此用户对这个文件所做的任何更改, 在系统重启之后都将被覆盖还原。解决这个问题的方法是给它加一把锁, 防止系统进程对它进行自动修改

首先解锁：

chattr -i /etc/resolv.conf
解锁之后就可以修改了:

nano /etc/resolv.conf
删除这个文件的所有nameserver部分, 或者在前面加#号注释, 只留一行：

nameserver 127.0.0.1 (将nameserver指向本机)
[存盘退出]
编辑完成后需要加锁, 以防被系统进程自动修改:

chattr +i /etc/resolv.conf
查看一下加锁状态: (PS: 下次如需修改此文件，需先解锁再编辑 )

lsattr /etc/resolv.conf
接下来, 打通dnsmasq和本机预定义host的关联: 本机的 /etc/hosts 文件预先存储了一些IP和域名的对应关系，需要将他们指向dnsmasq

nano /etc/hosts
在文件尾部加一行：
127.0.0.1       dnsmasq
[存盘退出]

- 尽管不是必须, 但鉴于修改内容较多，建议重启一次机器:

```bash
reboot
测试
在测试之前首先安装测试工具Dig:

dnf install bind-utils
输入以下命令测试:

dig -x rockage.lan
如果回显：[SERVER: 127.0.0.1#53(127.0.0.1)] 表示OK

在局域网上随便找台机器，

IP设为192.168.1.X (X可以取值2-255), 
网关设为: 192.168.1.1
DNS 设为: 192.168.1.1
然后输入命令: ping rockage.lan 
如果回显:	**来自 192.168.1.1 的回复: 字节=32 时间<1ms TTL=64**，表示OK
现在针对国内的DNS服务就已经设置完毕了，但是如果ping google.com之类的域名，那么结果肯定还是假的.

本节参考文章: https://www.tecmint.com/setup-a-dns-dhcp-server-using-dnsmasq-on-centos-rhel/

4、配置 gfwlist
gfwlist 是一个公益项目, 它将被GFW所屏蔽的网站做了一个汇总, 并以文件方式提供给大家下载。

项目地址：https://github.com/gfwlist/gfwlist ，有兴趣的可以去围观点赞。

dnsmasq-gfwlist.py 是一个Python 程序，它将原始的gfwlist文件转换为dnsmasq能读懂的格式。

首先下载 dnsmasq-gfwlist.py：

wget https://gist.githubusercontent.com/lanceliao/85cd3fcf1303dba2498c/raw/7391429f8fdc5e3f4c82ba98b98767922b8bb473/dnsmasq-gfwlist.py
然后对文件进行一点修改：

nano dnsmasq-gfwlist.py
需要修改的内容不多，只需要将：

mydnsport = '1053' 
改为 
mydnsport = '30053' 
[存盘退出]
PS: 端口号30053是我们之前设置好的dnscrypt-proxy的端口。另外，因为这个文件是Python 2.7写的，注意不能用python 3.8来运行，如果此时你的系统还没有安装Python 2.7, 可以用dnf命令先安装：

dnf install -y python27
接着，开始编译执行dnsmasq-gfwlist.py：

python2.7 dnsmasq-gfwlist.py
注意：这一步需要在梯子已经建好的情况下，因为程序会自动去下载gfwlist文件，下载这个文件需要翻墙。

程序运行完毕，转换后的文件将自动保存在/etc/dnsmasq.d 里，先查看一下文件内容：

cat /etc/dnsmasq.d/gfwlist.conf
如果看到一大堆类似这样的内容：

server=/instagram.com/127.0.0.1#30053      
ipset=/instagram.com/gfwlist
表示文件没有问题，创建成功了。

接着，还需要将它导入dnsmasq，编辑dnsmasq设置文件：

# nano /etc/dnsmasq.conf 
查找 "conf-dir=" 
默认的应该是这样：
conf-dir=/etc/dnsmasq.d,.rpmnew,.rpmsave,.rpmorig
或者这样：
conf-dir=/etc/dnsmasq.d 
只要保证conf-dir没有被注释，另外保证/etc/dnsmasq.d这个路径包含在设置清单内即可。
[存盘退出]
退出后，再做一次语法检查确保配置文件无误：

dnsmasq --test
最后重启dnsmasq并检查启动是否成功：

systemctl restart dnsmasq 
systemctl status dnsmasq
如果出现： dnsmasq: failed to create IPset control socket: Permission denied 这个错误，这个多半是由于selinux没有关闭造成的，现在关闭selinux:

nano /etc/selinux/config
将： SELINUX=enforcing
改为：SELINUX=disabled
[存盘退出]
修改selinux需要重启机器：

reboot
重启后，输入以下命令确认selinux已关闭：

sestatus
如果出现： failed to create listening socket for port 53: Address already in use

首先使用这个命令，查看一下到底是哪个程序占用了53端口：

systemctl list-sockets
如无意外，我猜大概率是跟 dnscrypt-proxy.socket 打架了，解决方法：

请严格按照我上面的dnscrypt-proxy安装方法去做，诚然，主运行端口留空这一点可能会引起大家的疑惑，但dnscrypt-proxy官方文档写得很清楚，socket方式就是这样设置的。

主要检查两个地方：

/etc/dnscrypt-proxy/dnscrypt-proxy.toml 文件中：listen_addresses = [] （这个中括号一定要为空）

/usr/lib/systemd/system/dnscrypt-proxy.socket 文件中：Socket 一节原封不动复制粘贴我的配置

如果显示：Unable to retrieve source [public-resolvers]，这个问题倒不大，因为服务器在境外有时候会出现读取不了的情况，只需要重新restart一下dnscrypt-proxy服务，再用status查看一下，一般来说多刷几次就正常了。

检查完毕后，用 reboot 命令重启，重启后输入：

systemctl status dnscrypt-proxy
systemctl status dnsmasq 
如果两者都显示绿字而没有任何红字，表示设置成功了！ 如果实在问题多多，就用这个命令仔细看看启动log，再具体分析:

journalctl -r -u dnscrypt-proxy.service 
5、设置 ipset
首先是建一个名为 "gfwlist" 的表:

ipset create gfwlist hash:net
注意这个gfwlist和上面我们说的gfwlist文件不是一回事, gfwlist 源文件通过转换后能够被dnsmasq识别，那么，通过dnsmasq 筛选出来的国外域名会转交给 dnscrypt-proxy 做加密解析，最终，这些解析后的IP存放在哪呢？就存放在现在咱们用ipset做的这个名叫gfwlist的表里了。

表建好之后，现在查看一下：

ipset list gfwlist
意料之中，目前表是空的，Members后面什么都没有。

现在输入：

nslookup qq.com
再输入：

ipset list gfwlist
啥变化也没有嘛，为何？因为这是一个国内网站，并没有被gfwlist收录

接着输入：

nslookup facebook.com
再输入：

ipset list gfwlist
神奇的事情发生了！内容有变化，Members后面跟了一个IP，这就对了，因为这是一个被gfwlist收录的域名，所以被ipset过滤了进来！你还可以多试几个网站，比如google.com、twitter.com、youtube.com 等等你懂的系列网站，你会发现Members列表会越来越长。

最终需要做到：

用nslookup命令查询一个国内域名，gfwlist表中的Members不增加；
用nslookup命令查询一个被GFW墙了的域名，gfwlist表中的Members增加其对应的IP地址。
恭喜，本文的所有目标你都实现了！