斐讯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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
root@aml:~# dig @8.8.8.8 google.com ; <<>> DiG 9.10.3-P4-Debian <<>> @8.8.8.8 google.com ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 29223 ;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 ;; QUESTION SECTION: ;google.com. IN A ;; ANSWER SECTION: google.com. 60 IN A 46.82.174.69 ;; Query time: 44 msec ;; SERVER: 8.8.8.8#53(8.8.8.8) ;; WHEN: Fri Oct 25 09:56:21 CST 2019 ;; MSG SIZE rcvd: 54 |
请求这个域名肯定是不通的:
1 |
root@aml:~# curl https://46.82.174.69 -H 'Host:www.google.com' -k |
UDP的DNS查询流量篡改非常严重,就不要指望它了。
但是我们知道DNS其实也支持TCP端口,我更换了一台服务器(因为N1盒子已经全局TCP翻墙,影响实验)利用TCP请求8.8.8.8解析google.com,发现可以得到正确IP:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
pi@raspberrypi:~ $ dig +tcp @8.8.8.8 google.com ; <<>> DiG 9.10.3-P4-Raspbian <<>> +tcp @8.8.8.8 google.com ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 52743 ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 512 ;; QUESTION SECTION: ;google.com. IN A ;; ANSWER SECTION: google.com. 33 IN A 172.217.160.110 ;; Query time: 50 msec ;; SERVER: 8.8.8.8#53(8.8.8.8) ;; WHEN: Fri Oct 25 10:00:13 CST 2019 ;; MSG SIZE rcvd: 55 |
使用TCP就一劳永逸了吗?你还是太小看GFW了!如果你重复的多试着解析几次,就会发现偶尔返回了一个错误的IP给你:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
pi@raspberrypi:~ $ dig +tcp @8.8.8.8 google.com ; <<>> DiG 9.10.3-P4-Raspbian <<>> +tcp @8.8.8.8 google.com ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 40131 ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 512 ;; QUESTION SECTION: ;google.com. IN A ;; ANSWER SECTION: google.com. 178 IN A 216.58.200.238 ;; Query time: 39 msec ;; SERVER: 8.8.8.8#53(8.8.8.8) ;; WHEN: Fri Oct 25 10:02:33 CST 2019 ;; MSG SIZE rcvd: 55 |
架构思路
在我们实现了redsocks+ss的TCP全局翻墙后,我们其实已经拥有了一个很重要的能力:
所有从斐讯N1发起或者经过斐讯N1的TCP流量,都会自动走翻墙流程出去。
这个能力太棒了,这意味着:我们在斐讯N1上强制TCP请求8.8.8.8解析google.com话,这个TCP DNS请求会全自动翻墙,不会被GFW识别。
回到斐讯N1上,多次尝试解析,发现IP解析稳定没有错误:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
root@aml:~# dig +tcp @8.8.8.8 google.com ; <<>> DiG 9.10.3-P4-Debian <<>> +tcp @8.8.8.8 google.com ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 58026 ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 512 ;; QUESTION SECTION: ;google.com. IN A ;; ANSWER SECTION: google.com. 299 IN A 172.217.164.110 ;; Query time: 777 msec ;; SERVER: 8.8.8.8#53(8.8.8.8) ;; WHEN: Fri Oct 25 10:05:30 CST 2019 ;; MSG SIZE rcvd: 55 |
当然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
然后作必要的配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
root@aml:~/redsocks# cat /etc/unbound/unbound.conf # Unbound configuration file for Debian. # # See the unbound.conf(5) man page. # # See /usr/share/doc/unbound/examples/unbound.conf for a commented # reference config file. # # The following line includes additional configuration files from the # /etc/unbound/unbound.conf.d directory. include: "/etc/unbound/unbound.conf.d/*.conf" # 结果缓存 msg-cache-size: 100m rrset-cache-size: 100m # 结果缓存时间1小时 cache-min-ttl: 3600 # 在过期前自动刷新 prefetch: yes # 禁用ipv6 do-ip6: no # 监听在所有网卡的53端口, tcp/udp同时服务 interface: 0.0.0.0 port: 53 # 允许所有来源IP的请求, 默认只允许Localhost access-control: 0.0.0.0/0 allow # 采用tcp转发给上游8.8.8.8 tcp-upstream: yes forward-zone: name: "." forward-addr: 8.8.8.8@53 |
因为现在是所有DNS请求都是翻墙解析的,所以上述配置中刻意设置了多个与响应速度优化的配置:
- cache-min-ttl:默认缓存结果时间是来自upstream DNS应答中的TTL;这个配置可以确保缓存时间不会少于3600秒。
- prefetch:在缓存过期之间,主动去upstream DNS刷新结果,这样就不会因为过期穿透而导致解析卡顿。
配置启动:
1 2 |
systemctl enable unbound systemctl start unbound |
然后@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个规则:
1 2 |
iptables -t nat -A PREROUTING -p udp --dport=53 -j REDIRECT --to-ports 53 iptables -t nat -A OUTPUT -p udp --dport=53 -j REDIRECT --to-ports 53 |
理论上原本发给192.168.2.1的DNS查询会被改写给斐讯N1(IP:192.168.2.102),实测发现如下报错:
1 2 3 4 |
root@aml:~# dig google.com ;; reply from unexpected source: 192.168.2.102#53, expected 192.168.2.1#53 ;; reply from unexpected source: 192.168.2.102#53, expected 192.168.2.1#53 ;; reply from unexpected source: 192.168.2.102#53, expected 192.168.2.1#53 |
可见,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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
root@aml:~# cat /etc/dhcp/dhclient.conf # Configuration file for /sbin/dhclient. # # This is a sample configuration file for dhclient. See dhclient.conf's # man page for more information about the syntax of this file # and a more comprehensive list of the parameters understood by # dhclient. # # Normally, if the DHCP server provides reasonable information and does # not leave anything out (like the domain name, for example), then # few changes must be made to this file, if any. # option rfc3442-classless-static-routes code 121 = array of unsigned integer 8; send host-name = gethostname(); request subnet-mask, broadcast-address, time-offset, routers, domain-name, domain-name-servers, domain-search, host-name, dhcp6.name-servers, dhcp6.domain-search, dhcp6.fqdn, dhcp6.sntp-servers, netbios-name-servers, netbios-scope, interface-mtu, rfc3442-classless-static-routes, ntp-servers; #send dhcp-client-identifier 1:0:a0:24:ab:fb:9c; #send dhcp-lease-time 3600; #supersede domain-name "fugue.com home.vix.com"; prepend domain-name-servers 127.0.0.1; |
即打开prepend domain-name-servers那一行。
重启斐讯N1你会看到/etc/resolve.conf中优先使用了127.0.0.1进行域名解析,也就会走到unbound实现自动翻墙。
现在,我只要在N1上直接curl google.com访问成功,包括apt包管理、golang依赖下载、nodejs依赖下载等功能均已实现全局翻墙:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
root@aml:~# curl https://www.google.com -I HTTP/2 200 date: Fri, 25 Oct 2019 06:38:54 GMT expires: -1 cache-control: private, max-age=0 content-type: text/html; charset=ISO-8859-1 p3p: CP="This is not a P3P policy! See g.co/p3phelp for more info." server: gws x-xss-protection: 0 x-frame-options: SAMEORIGIN set-cookie: 1P_JAR=2019-10-25-06; expires=Sun, 24-Nov-2019 06:38:54 GMT; path=/; domain=.google.com; SameSite=none set-cookie: NID=190=VaErnsUhZOFVuBzjNex6-UVk9Ogl9jZw9IKdKquIzqOdTDNZ4R7qT-wJehZC8L6b2Iy8IUxFarz5PoNZzCU6i_-ATH3tw7GHz91orxcYNOxPbjM3MblfelpcLZ-FqPjs14cCKvNqilGW2IG5yWHkDKzkslTGeG9mz_OPzpk9d2c; expires=Sat, 25-Apr-2020 06:38:54 GMT; path=/; domain=.google.com; HttpOnly alt-svc: quic=":443"; ma=2592000; v="46,43",h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000 accept-ranges: none vary: Accept-Encoding |
后续我会实验一下直接修改主路由器的dhcp server配置,直接下发斐讯N1的IP地址作为DNS server,在此不做验证。
但是要注意,主路由的DHCP配置将影响所有内网设备,这种全局DNS翻墙解析的延迟是比较高的,我建议还是不要这样处理。
如果哪台电脑希望全局翻墙,那就将那台电脑的网关和DNS都指向N1即可。
最后
祝玩的愉快,有问题留言或者添加我的微信交流即可。
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~
