部分学术站点(如 www.sciencedirect.com)使用了 Akamai 等 CDN 服务,其 IP 地址经常发生变化。我们基于 IP 地址的路由策略不能及时捕捉到这些变化,有时这些站点会使用国外 VPS 访问,导致论文无法下载。

受到 dnsmasq --ipset 的启发,我写了一个小程序,抓取 VPN 服务器上的 DNS 回复包,提取 A 记录查询中的域名和 A 记录(IPv4 地址)。如果域名匹配上学术站点列表中的任意域名,就把解析出的 IPv4 地址添加一条路由规则到指定的路由表里。

没有使用 dnsmasq --ipset 的原因是:

  1. dnsmasq 不稳定,经常挂掉,因此换用了 bind9 做递归 DNS 服务器。
  2. 不是所有人都使用 LUG 递归 DNS,Linux 上的 openvpn 客户端不修改 DNS,还有一些人设置了静态 DNS 服务器。
  3. 某些操作系统和浏览器会缓存 DNS 查询结果,比如在没挂 VPN 的时候访问了学术站点,挂上 VPN 再访问,就不再次解析这些域名了。由于 DNS 记录 TTL 的限制,这不是个大问题。
  4. ipset 中 IP 条目的过期问题,dnsmasq 倒是把 IP 加进去了,但没有留下时间戳和 URL 信息,什么时候移出来呢?

我自己的小程序 dnsroute 目前能够解决问题 2(因为 dnsroute 是基于抓包的,不管用哪里的 DNS 都会被抓到),经过改进后也能解决问题 4。

代码算是刚起了个头,期末考之后再做改进。
https://git.ustclug.org/boj/dnsroute

写这个的过程中先后掉进几个坑,搞了一下午 + 一晚上:

  1. 用 Python 的 pyshark,倒是挺快编出来了,但 pyshark 是基于 tshark 的,tshark 自己不能抓包,是读取 dumpcap 输出到 /tmp 的抓包文件。pyshark 持续运行,抓包文件就会不断增长,一会儿就把 tmpfs 撑爆了。此路不通,前功尽弃。
  2. 用 C 写,希望创建一个 tun device,使用 iptables 规则把 UDP 53 端口的包镜像到 tun device 上。但发现 iptables -j TEE 比较难用,镜像出来的 UDP 53 端口包总是跑到别的网卡去。因此只好换成 libpcap,用传统的抓包方法。
  3. 希望使用 ioctl 直接调用内核 API 来添加路由规则,但 ioctl 只支持向默认路由表中插入规则。
  4. 希望使用 RTNETLINK 直接调用内核 API 来添加路由规则,参考了 iproute2 源码和 netlink 文档,还是没弄清楚这些结构之间的关系,最终弃疗。还是 fork + execlp 了 ip route 命令。
  5. 解析 DNS 数据包的时候指针动来动去,不小心写错了两处,花了很大力气才调试出来。早知道就借用 tcpdump 或者 wireshark 的库了。

dnsroute 的日志输出节选:

IP 64.238.147.22 TTL 3600 for domain books.acm.org matching acm.org
IP 23.59.134.153 TTL 24 for domain www.sciencedirect.com matching sciencedirect.com
IP 184.26.203.60 TTL 20 for domain www.sciencedirect.com matching sciencedirect.com
IP 64.238.147.53 TTL 11869 for domain dl.acm.org matching acm.org
IP 64.238.147.56 TTL 11869 for domain dl.acm.org matching acm.org

感谢 stephen 的建议。

1 月 18 日对 dnsroute 进行了压力测试。在极限负载下,dnsroute 每秒可处理 2k ~ 2.5k 次匹配上特征的 DNS 请求,且其 CPU 占用率与 bind9 递归 DNS 服务器接近,此时系统的瓶颈是 fork + exec ip route 命令(都怪没用 RTNETLINK)。

极限负载下 DNS 规则的处理速度(瓶颈在调用 ip 命令,看 Forks)
极限负载下 dnsroute 的 CPU 占用率与 bind9 接近