场景分析

家用DNS的场景免不了天天折腾,比不上企业服务器24x7不关机。
很多在用PaoPaoDNS+PaoPaoGateway的网友经常问关于DNS故障转移的问题。让我们来先看一个常用的简单FakeIP拓扑:

主路由
主路由
FakeIP网关
FakeIP网关
客户端
客户端
DNS服务
DNS服务
FakeIP池
11.0.0.1=qq.com
......
........
FakeIP池...
被分流的域名
被分流的域名
静态路由11.0.0.0/8 下一跳是192.168.1.200
静态路由11.0.0.0/8 下一跳是192.168.1.200
192.168.1.1
192.16...
192.168.1.200
192.16...
192.168.1.2
192.16...
qq.com
qq.com
qq.com
.....
qq.com...
DNS查询
DNS查询
11.0.0.1
11.0.0...
域名命中分流列表
域名命中分流列表
返回解析11.0.0.1
返回解析11.0.0.1
192.168.1.53
192.16...
向FakeIP网关查询
向FakeIP网关查询
返回解析11.0.0.1
返回解析11.0.0.1
请求
请求
Viewer does not support full SVG 1.1
DNS查询过程可以简化如下:
PaoPaoDNS10.10.10.8PaoPaoGateway10.10.10.3CUSTOM_FORWARDFakeIP CIDRPC10.10.10.100DNS查询/请求

在这个场景里面,DHCP下发客户端的DNS是PaoPaoDNS,“DNS故障"可以有几种情况:

  • PaoPaoGateway炸了,不返回FakeIP。但这在常用的拓扑下,通常只影响国外域名查询。
  • PaoPaoDNS的递归组件炸了,但PaoPaoDNS内部可以回落到其他查询结果
  • PaoPaoDNS所在的宿主机炸了,这回就真的是没网了。<-最需要故障转移高可用的一集

如果给客户端下发备用DNS有用吗?没有用,多个DNS下发到客户端的实际行为不确定,大多数时候都是随机查询并不是故障转移。既然PaoPaoDNS所在的宿主机炸了,那么自然也要跳出宿主机去解决问题——比如在一些稳定不关机的嵌入式设备,或者可执行二进制的硬路由器(经典openwrt硬路由)上跑一个简单的DNS转发器,他的任务很简单,当PaoPaoDNS可用的时候使用PaoPaoDNS,当PaoPaoDNS不可用的时候回落到运营商或者其他公共DNS————由于给客户端下发的DNS是路由器的DNS,因此你就算把PaoPaoDNS的宿主机砸了也不会断网。理想的拓扑简化如下:

PaoPaoDNS10.10.10.8PaoPaoGateway10.10.10.3FakeIP CIDRISP DNS223.5.5.5Router10.10.10.1PC10.10.10.100Fallback

实现平滑的DNS故障转移

很多开源DNS服务端项目,比如openwrt里面自带的dnsmasq就有DNS故障转移的功能————比如按顺序查询(strict-order参数)。但实际用起来一点也不平滑,表现为:

  • 当DNS故障的时候,查询结果明显巨大的延迟,上网就感觉到卡。因为触发故障转移的条件往往非常苛刻,比如直到遇到SERVFAIL甚至5秒的查询超时后才切换备用查询服务器。也就是你搭建的DNS炸了之后,每次查询都要忍受巨大的延迟————有些应用程序甚至等不了这么久就马上报错。
  • 无有效结果的响应不会被视作DNS查询失败。这个很好理解,就像你打开网页返回了404被认为是正常的。比如当PaoPaoGateway炸了的时候,CUSTOM_FORWARD仍会返回失败的消息。
  • 当搭建的DNS服务器恢复正常了不能及时切回来。这个不仅是DNS服务端缓存,也是相对于客户端而言的,就FakeIP场景而言,假设在故障的时候没有返回FakeIP而是其他IP,DNS或者其他服务恢复之后,之前的IP结果在客户端就会有通常最多10分钟左右的缓存,这就造成换回来也很不平滑。

那么要做到平滑的DNS故障转移也很简单:先查询主DNS,在一个较小的查询阈值之内如果没有匹配查询请求的DNS结果,那么就转移到备用DNS上查询,如果有查询结果,把DNS应答的ttl设置为1,让缓存快速过期,待主DNS恢复后,平滑切换回来。实际上用mosdns写配置也不复杂,但为了更便捷的配置和专注于DNS故障转移的用途,方便给下次遇到同样需求的问题直接甩一个程序链接,基于PaoPaoDNS的修改版本的mosdns的基础上写了一个故障转移专用的DNS转发器————mini-ppdns。

mini-ppdns

https://github.com/kkkgo/mini-ppdns
专注于 DNS 故障转移的迷你DNS转发器。 mini-ppdns 是从PaoPaoDNS项目精简修改而来的纯粹转发器,致力于提供极致轻量化且高效的平滑 DNS 故障转移体验。

快速启动

假设你的本地自建DNS是10.10.10.8,你的运营商DNS或者需要故障转移的DNS是223.5.5.5, 那么最简单的命令行启动:

1
mini-ppdns -dns 10.10.10.8 -fall 223.5.5.5

可以指定DNS端口和多个上游:

1
mini-ppdns -dns 10.10.10.8:53,10.10.10.9:53 -fall 223.5.5.5:53,119.29.29.29:53

参数详解

  • -listen可以指定监听地址和端口,默认是监听所有可监听的私有地址(跳过公网地址):
  • mini-ppdns -dns 10.10.10.8 -fall 223.5.5.5 -listen 127.0.0.1:53
  • -aaaa可以指定是否开启IPv6的aaaa记录(默认为no,屏蔽aaaa):
    mini-ppdns -dns 10.10.10.8 -fall 223.5.5.5 -aaaa=yes
  • -force_fall可以指定某些IP段总是走运营商/故障转移的DNS。:
    mini-ppdns -dns 10.10.10.8 -fall 223.5.5.5 -force_fall=192.168.1.10,192.168.2.0/24
    注:FakeIP场景可以利用这个功能,间接实现某些设备不走代理
  • -qtime可以指定故障转移的延迟阈值(默认为250ms,一般不需要调整):
    mini-ppdns -dns 10.10.10.8 -fall 223.5.5.5 -qtime=250
  • -debug输出详细的调试日志。
  • -d可以在后台运行。
  • -config可以指定加载配置文件,可以配置mini-ppdns.ini如下:
 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
# 本地搭建的主DNS
[dns]
10.10.10.8:53
10.10.10.9:53

# 故障转移备用/运营商DNS
[fall]
223.5.5.5:53
119.29.29.29:53

# 监听地址端口
[listen]
127.0.0.1:53
192.168.1.1:53

# 可以指定某些IP段总是走运营商/故障转移的DNS
[force_fall]
# 支持以下三种写法:单个 IP、CIDR 端、以及特定的 IP Range
# FakeIP场景可以利用这个功能,间接实现某些设备不走代理
192.168.1.10
192.168.2.0/24
192.168.3.2-192.168.3.100

[adv]
# 转移延迟阈值(毫秒)
qtime=250
# 是否开启 IPv6 aaaa记录查询解析(yes/no)
aaaa=no

在openwrt路由器上部署

在openwrt上部署说起来简单也复杂,因为很多openwrt里面有各种神神秘秘的插件互相干扰。其中不少会劫持DNS,此处部署过程仅包含一些常见的坑和注意事项。当然部分过程也适用于其他linux系统。

  • release下载适合你的硬件架构的二进制文件。如果不清楚自己的硬件是什么架构,可以在终端输入uname -m。其中release名字带UPX的是为了给一些储存空间紧张的设备用的,如果你的设备空间充足下正常版本即可。
  • mini-ppdns上传到你的设备,为了方便你可以上传到/usr/sbin/mini-ppdns,加执行权限chmod +x /usr/sbin/mini-ppdns。将你的配置文件储存在/etc/mini-ppdns.ini,然后执行mini-ppdns -config /etc/mini-ppdns.ini看看是否输出正常(比如提示某个端口已经被监听)。
  • 添加自启动脚本。在openwrt上最简单的是编辑 /etc/rc.local,在 exit 0 之前添加你的启动命令,带上 -d 参数,程序会自动到后台,不会阻塞启动。当你修改了局域网的IP段或者配置,你需要重新启动mini-ppdns
1
2
/usr/sbin/mini-ppdns -config /etc/mini-ppdns.ini -d
exit 0

或者写一个守护脚本加计划任务或者修改服务,此处提供了一个参考脚本: https://github.com/kkkgo/mini-ppdns/blob/main/mini-ppdns.sh
使用方法,把脚本上传到/usr/sbin重命名为/usr/sbin/mini-ppdns加执行权限,crontab -e编辑计划任务:* * * * * /usr/sbin/mini-ppdns.sh即可。脚本启动之前检测是否已经存在mini-ppdns进程,如果存在就直接退出,因此可以直接让计划任务每分钟执行来作为守护。执行mini-ppdns.sh restart可以重载配置。

  • 普通linux到这里已经弄完了,但一些linux安装过程中会自带DNS服务器导致占用监听端口,比如Ubuntu,可以禁用自带的DNS解析器:
1
2
3
sudo systemctl stop systemd-resolved
sudo systemctl disable systemd-resolved
#禁用后记得手动编辑/etc/resolv.conf手动写入DNS服务器

当然,openwrt自带dnsmasq,我们需要把他停用。 编辑 /etc/dnsmasq.conf 或在 OpenWrt 管理界面 (网络 -> DHCP/DNS -> 高级设置) 中:

1
2
# 将 DNS 端口设置为 0,彻底禁用 dnsmasq 的 DNS 解析功能(仅保留 DHCP 功能)
port=0
  • 某些dnsmasq的禁用DNS解析会导致DHCP不下发DNS。所以我们还需要手动下发路由器的DNS。
    点击网络-接口-LAN-DHCP服务器-高级设置,在DHCP选项里面,手动设置DHCP的附加选项。下发DNS的选项是6,比如你的路由器IP是10.10.10.1,那么填入6,10.10.10.1。当然,在FakeIP场景下,你也可以顺便填入option 121
  • 某些修改的openwrt版本会有DNS重定向的劫持选项需要手动关闭。【参考1】 【参考2】
  • 在IPv6环境下,需要关闭路由器的DNS的IPv6 DNS下发。

在docker上部署

尽管在这个场景下使用docker部署不太常见,但仍有很多没有开放终端的设备只能跑docker。
以下是非常简单的docker compose的示例配置,可以根据自己的实际环境调整,或者复制给AI转换成你实际的容器环境Cli。
mini-ppdns.ini和对应你设备架构的mini-ppdns二进制放在docker-compose.yml同一目录。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
services:
  mini-ppdns:
    image: public.ecr.aws/docker/library/busybox
    container_name: mini-ppdns
    network_mode: host
    restart: unless-stopped
    volumes:
      - .:/app:ro
    working_dir: /app
    command: [ "./mini-ppdns", "-config", "mini-ppdns.ini" ]

此处定义网络模式是host(直接使用宿主机网络),因此不需要映射端口,可根据自己需要调整。