一个抗污染 DNS 的搭建

DNS 服务是互联网的重要基础服务,不过它的重要性往往被低估。例如,2013年8月,.cn 根域名服务器遭到 DDoS 攻击,导致 .cn 域名无法访问;2014年1月21日,根域名服务器遭到某著名防火墙污染,导致所有国际域名无法访问。很多国际知名网站在中国大陆无法访问,部分原因就是遭受了 DNS 污染,也就是对域名返回了错误的 IP 地址。

要搭建一个抗污染的 DNS,并不是使用 VPN 解析所有域名这么简单。主要有两个问题:

为什么要国内外区分解析

一些大型网站,往往从电信查询到的是电信 IP,教育网查询到的是教育网 IP,国外查询到的是国外 IP。DNS 服务器会根据 DNS 解析请求的来源 IP 给予不同的回复。权威 DNS 服务器最著名的 bind9 软件,就有 “view” 的功能,可以让不同 IP 的 DNS 请求者看到不同的视图。

大型网站一般使用了内容分发网络(CDN),把静态内容分发到遍布世界各地的服务器上;还有企业级的虚拟专用网络(VPN),即使内容不能静态分发,也能使用分布式数据库或者快速路由到中心节点处理。不论网站内部是如何实现的,对用户来说,访问网络上最近的 IP 一般是最好的。

如果使用国外 VPN 解析所有域名,不论是直接使用 VPN 所在运营商提供的递归 DNS 解析服务,还是自己搭建 bind9 等递归 DNS 服务器,网站的权威 DNS 服务器看到的请求来源都是国外 IP,要么会回复一个国外 IP,要么会回复一个该网站认为访问国外最快的国内 IP。例如,从国外查询 mirrors.ustc.edu.cn 得到的就是中国移动的 202.141.176.110。但如果我是教育网用户,访问这个 IP 可能比较慢,因为国内运营商之间的互联互通众所周知的差。

如何既防止 DNS 污染,又使国内网站解析到自己所在运营商的 IP 呢?我们观察 example.com 从根服务器开始递归查询的过程(示意):

  1. 根服务器告诉 .com 是归 x.gtld-servers.net 管的,这一步没有分区域解析
  2. x.gtld-servers.net 告诉 example.com 是归 dns.example.net 管的,这一步没有分区域解析
  3. 查到 dns.example.net 的 IP,在此略去
  4. dns.example.net 告诉 example.com 的 IP 地址,这一步有分区域解析

也就是说,如果 example.com 是国内的,我们只要在向 dns.example.net 发送 DNS 查询时,使用国内 IP 就行了。一般来说,国内网站的 DNS 服务器也在国内,因此只要让所有到国内 IP 的 DNS 请求走运营商线路而不走 VPN,就能将国内网站解析到自己运营商的 IP。当然,对使用了国际 CDN 提供商(如 Amazon、Edgecast)所提供的 CDN 服务的国内网站,上述假设未必成立,因为这些国际 CDN 提供商的 DNS 服务器一般在国外。我还没想到好办法来解决这个问题。

这种做法会不会导致一些域名仍然被污染呢?我认为不会。如果一个域名的 DNS 在国内,就不用劳烦某防火墙了,直接下令停止这个域名的解析就完事了。因此被污染域名的权威 DNS 服务器应该都在国外。由于只有国内 IP 才走运营商线路,这些 DNS 查询应该都是走 VPN 的,应该不会被污染。

国内外区分解析的实现

让国内 IP 走运营商线路,其他 IP 走 VPN,只需修改路由表即可。要生成 CIDR 格式的国内 IP 列表,可以从 APNIC 下载公开的中国大陆 IP 地址分配数据,然后进行简单的字符串处理:

#!/bin/bash
tmp=$(mktemp)
wget -4 http://ftp.apnic.net/apnic/stats/apnic/delegated-apnic-latest -O $tmp
cat $tmp | awk '{FS="|"}{if($2=="CN" && $3=="ipv6"){print $4 "/" $5}}' >chnroutes-v6.txt
cat $tmp | awk '{FS="|"}{if($2=="CN" && $3=="ipv4"){print $4 "/" (32-log($5)/log(2))}}' >chnroutes.txt
rm -f $tmp

设置路由也是非常简单的事:

#!/bin/bash
IPV4_GATEWAY=   # 运营商给的网关 IP
IPV6_GATEWAY=
IPV4_VPN=       # VPN 的网关 IP,如 10.8.0.1
IPV6_VPN=
IPV4_LOCAL=     # 自己的公网 IP
IPV6_LOCAL=
# 上面这部分是要自己填写的

ip route flush table 1000
cat chnroutes.txt | while read prefix comment; do
    if [[ "$prefix" =~ ^[0-9.]+(/[0-9]+)?$ ]]; then
        ip route add $prefix via $IPV4_GATEWAY table 1000
    fi
done
ip rule add from all lookup 1000
ip route replace default via $IPV4_GATEWAY table 1001
ip rule add from $IPV4_LOCAL lookup 1001 # 这是为了保证进来的连接仍然从公网出去而不要走 VPN
ip route replace default via $IPV4_VPN  # 如果 VPN 已经设置了默认路由,就不用这条,如 OpenVPN

ip -6 route flush table 1000
cat chnroutes-v6.txt | while read prefix comment; do
    ip -6 route add $prefix via $IPV6_GATEWAY table 1000
done
ip -6 rule add from all lookup 1000
ip -6 route add default via $IPV6_GATEWAY table 1001
ip -6 rule add from $IPV6_LOCAL lookup 1001 # 这是为了保证进来的连接仍然从公网出去而不要走 VPN
# 注意,下面两行要根据你的情况选一行
ip -6 route add 2000::/3 via $IPV6_VPN            # 如果你的 VPN 支持 IPv6 且没有设置默认路由
ip -6 route add unreachable 2000::/3 table 1000   # 如果你的 VPN 不支持 IPv6

如果你的网络完全不支持 IPv6,直接忽略上述 IPv6 部分的配置即可。如果你的运营商网络支持 IPv6 而 VPN 不支持,一定要把国外的 IPv6 主动封掉,不然通过 IPv6 解析的域名可能就被污染了。顺便说一句,如果不希望使用 IPv6 解析域名,可以在 bind9 的命令行参数中加入 -4。在 Debian 系统中可以修改 /etc/default/bind9,把 OPTIONS=”-u bind” 改为 OPTIONS=”-u bind -4” 即可。

建议使用 bind9 搭建递归 DNS 服务器。请注意修改 /etc/bind/named.conf.options(这是 Debian 下的路径),将 forwarders 大括号全部注释掉,不使用别人的递归 DNS 服务,完全靠自己从根开始逐级解析。bind9 的默认配置是不允许外网使用这个递归 DNS 服务器的,如果自己用当然很好,如果要给其他人用就得修改 allow-query 和 allow-recursion 两块的设置。

用负载均衡提高 VPN 稳定性

一条 VPN 连接可能有间歇的丢包,还随时可能被阻断。例如,我用上文方法搭建的一个抗污染 DNS,一天内 DNS 请求的响应时间如下图所示(所有查询都是查询某国外域名的,测试时没有使用 DNS 缓存)。有 2.3% 的 DNS 查询请求失败了。

CaptureCapture

CaptureCapture

由上图可知,DNS 查询故障(由于 VPN 被丢包或间歇阻断)是均匀分布在一天内各个时段的,几乎可以认为是随机事件。

我们做 Web 服务器时,都知道为了提高可用性(availability),设置多个后端服务器,前端只是个 proxy 做负载均衡。DNS 为什么不能这样?

我尝试建立了三个虚拟机,第一个虚拟机绑定公网 IP,提供 DNS 解析服务;另两个虚拟机只有内网 IP(用虚拟机管理工具分配的),通过 SNAT 访问公网。虚拟机 2 和虚拟机 3 上分别连接一个不同的 VPN,这样两个 VPN 的故障基本可以认为是相互独立的。

在虚拟机 1 里配置 bind9 的 /etc/bind/named.conf.options,使用虚拟机 2 和虚拟机 3 作为后端,并指定这个 bind9 不自己解析域名(否则,当两个后端都解析失败时,bind9 会自己尝试解析,得到的可能是被污染的 IP)。

forwarders { $VM2_IP; $VM3_IP; };
forward only;

在虚拟机 2 和虚拟机 3 里分别按照上文的方法部署 bind9,启动 VPN。不要忘了 allow-query 和 allow-recursion 的设置。

负载均衡的效果:

(由于测量方式不同,请不要将下图中的数字直接与负载均衡前的比较,因此没有把负载均衡前后的两条曲线画到一张图中。平均响应时间应该是降为原来的一半,而没有图中这么夸张。主要看曲线的走势,极端值被大大削减了。)

CaptureCapture

Capture1Capture1

当然,如果您的两个 VPN 都不太稳定,还可以添加第三个、第四个……这个负载均衡的方法是通用的。

诸位看官如果有什么更好的搭建抗污染 DNS 方法,或者质疑本文的一些做法,欢迎讨论。