斐讯N1 – 自建防污染DNS

阅读本文的前提是已经阅读并且实施了《斐讯N1 – 利用redsocks+ss实现全局翻墙》中的TCP全局翻墙。

虽然电脑借助N1作为默认网关可以实现全局翻墙,但是这个实现这仅仅服务于TCP协议。

然而翻墙的一个重要环节就是DNS解析的污染问题,国内网络运营商因为政策原因基本上对google.com等境外站点都进行了DNS篡改,所以即便TCP层面全局翻墙了,但如果你解析到的是一个错误IP,那么无论如何也是无法打开谷歌的。

分析问题

我们知道谷歌的开放DNS是8.8.8.8,如果你不经过翻墙直接利用默认的UDP协议请求8.8.8.8解析google.com,你将得到一个被国内运营商流量拦截污染过的错误IP:

请求这个域名肯定是不通的:

UDP的DNS查询流量篡改非常严重,就不要指望它了。

但是我们知道DNS其实也支持TCP端口,我更换了一台服务器(因为N1盒子已经全局TCP翻墙,影响实验)利用TCP请求8.8.8.8解析google.com,发现可以得到正确IP:

使用TCP就一劳永逸了吗?你还是太小看GFW了!如果你重复的多试着解析几次,就会发现偶尔返回了一个错误的IP给你:

架构思路

在我们实现了redsocks+ss的TCP全局翻墙后,我们其实已经拥有了一个很重要的能力:

所有从斐讯N1发起或者经过斐讯N1的TCP流量,都会自动走翻墙流程出去。

这个能力太棒了,这意味着:我们在斐讯N1上强制TCP请求8.8.8.8解析google.com话,这个TCP DNS请求会全自动翻墙,不会被GFW识别。

回到斐讯N1上,多次尝试解析,发现IP解析稳定没有错误:

当然DNS走TCP翻墙的坏处就是延迟高,这个是代价。

因此,我的架构是:

内网电脑如果将N1作为网关,那么发往任何DNS服务器的UDP请求其目标port都是53,都可以被N1的iptables识别。

内网电脑如果将DNS直接指向N1,那么必然所有的DNS UDP请求的目标IP是斐讯N1,目标port也是53。

我们只需要在N1上搭建一个DNS server监听在UDP 53端口,对于网关用法利用iptables将UDP流量改写到本机DNS server;而对于DNS直接指向的用法,则不需作特殊处理。

DNS server收到UDP请求后,按TCP协议upstream查询到8.8.8.8,因为TCP协议已经做了自动翻墙,所以毫无疑问不会受到GFW干扰。

搭建unbound

这是一款DNS server,选择它的原因是它支持upstream TCP协议,这样才能走TCP自动翻墙的流程。

不选择dnsmasq的原因是因为它不支持upstream TCP。

首先是安装unbound:

apt update

apt install unbound

然后作必要的配置:

因为现在是所有DNS请求都是翻墙解析的,所以上述配置中刻意设置了多个与响应速度优化的配置:

  • cache-min-ttl:默认缓存结果时间是来自upstream DNS应答中的TTL;这个配置可以确保缓存时间不会少于3600秒。
  • prefetch:在缓存过期之间,主动去upstream DNS刷新结果,这样就不会因为过期穿透而导致解析卡顿。

配置启动:

然后@localhost指定通过本机的unbound作为dns服务器,测试解析google.com域名:

root@aml:~# dig @localhost google.com

; <<>> DiG 9.10.3-P4-Debian <<>> @localhost google.com
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 18356
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;google.com. IN A

;; ANSWER SECTION:
google.com. 143 IN A 172.217.164.110

;; Query time: 0 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Fri Oct 25 11:18:18 CST 2019
;; MSG SIZE rcvd: 55

首次访问比较慢,因为unbound会TCP upstream走自动翻墙出去。

但是结果会按照dns应答的ttl缓存到unbound里,因此接下来一段时间的访问就很快了。

iptables拦截53流量

接下来需要拦截所有目标端口是53的UDP流量,改写到unbound端口上。

这样无论内网电脑原先指定的DNS服务器是谁,都会走统一的unbound + redsocks + ss的流程TCP翻墙解析。

所以我作了如下2个规则:

理论上原本发给192.168.2.1的DNS查询会被改写给斐讯N1(IP:192.168.2.102),实测发现如下报错:

可见,dig命令是收到了应答,但是应答的来源IP变成了192.168.2.102,而不是当时请求的目标192.168.2.1,所以dig客户端认为这是一个恶意的应答因此报错。

明明在去程的时候REDIRECT作了DNAT,为什么iptables没有对UDP回程应答进行源IP恢复呢?这是因为UDP和TCP不一样,UDP是无状态的,因此linux内核根本不知道回程包和去程包的对应关系。

这个问题可能没有很好的解决方案,所以我iptables下掉了这两个规则,放弃了拦截任意目标DNS server UDP请求的想法,只能让客户端直接DNS指向N1了。

配置DNS服务器

因为斐讯N1本身也是通过主路由的DHCP广播分配到的DNS服务器地址,为了让斐讯N1使用本机的unbound解析域名,可以通过2个方法实现。

  • 修改N1的dhcp client配置,指定127.0.0.1作为DNS server。
  • 修改主路由器的dhcp server配置,指定N1的局域网IP作为所有内网设备的DNS server。

下面是修改N1自己的dhcp client配置,令N1上发起的DNS查询均发给本机unbound:

即打开prepend domain-name-servers那一行。

重启斐讯N1你会看到/etc/resolve.conf中优先使用了127.0.0.1进行域名解析,也就会走到unbound实现自动翻墙。

现在,我只要在N1上直接curl google.com访问成功,包括apt包管理、golang依赖下载、nodejs依赖下载等功能均已实现全局翻墙:

后续我会实验一下直接修改主路由器的dhcp server配置,直接下发斐讯N1的IP地址作为DNS server,在此不做验证。

但是要注意,主路由的DHCP配置将影响所有内网设备,这种全局DNS翻墙解析的延迟是比较高的,我建议还是不要这样处理。

如果哪台电脑希望全局翻墙,那就将那台电脑的网关和DNS都指向N1即可。

最后

祝玩的愉快,有问题留言或者添加我的微信交流即可。

如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~