一次 IPv6 DDNS 事故:固定后缀遇上旧前缀残留

今天排了一个挺有意思的 IPv6 DDNS 小事故。

现象很简单:一个平时可以从外面 SSH 的虚拟机域名,突然开始卡住。ssh -6 user@host 没有马上报错,而是一直等到超时。DNS 还能解析,机器也确实在跑,所以一开始很容易误判成防火墙、SSH 服务、Cloudflare、路由器其中某个环节坏了。

真正的根因是:家里的公网 IPv6 前缀变了,但虚拟机里还残留着旧前缀下的静态地址。

这套 DDNS 原来的策略是“每台设备固定一个好记的后缀”,例如路由器用 ::1,某台 Mac 用 ::14,虚拟机用 ::21。脚本会在设备上找一个以这个后缀结尾的全局 IPv6,然后把它更新到 DNS。

这个想法平时很好用,坏就坏在一个边界条件:

  1. 运营商重新分配了新的 IPv6 前缀。
  2. 设备通过 RA/SLAAC 拿到了新前缀地址。
  3. 但旧的静态 old-prefix::21/64 还挂在网卡上,而且生命周期是 forever。
  4. DDNS 脚本继续优先选择“看起来最像目标”的 ::21
  5. 于是 DNS 被继续更新成旧前缀地址,外部连接就一直超时。

这个事故里最有用的判断是把问题拆成四段:

dig AAAA host.example.net
nc -6 -vz host.example.net 22
ip -6 addr show scope global
ip -6 route

如果 DNS 指向的前缀和当前局域网/路由器 RA 下发的前缀不一致,就不要再盯着 sshd 或密钥看了。外部主机能不能连、同网段邻居发现是不是 incomplete、接口上是不是挂着旧前缀,这几个信号会很快把方向拉回来。

修复方案也很明确:DDNS 不应该先从“所有全局 IPv6 地址”里按后缀猜答案,而应该先确定当前真实前缀。

现在改成了这样的顺序:

  1. 从当前默认 IPv6 接口或路由器 LAN 接口读取正在使用的公网 /64 前缀。
  2. 用这个当前前缀拼出固定后缀,例如 current-prefix::21
  3. 只有这个地址存在且不是 tentative/deprecated,才允许发布到 DNS。
  4. Linux/NetworkManager 设备可以顺手把连接配置同步到新地址。
  5. macOS 设备只发布当前接口上已经存在的固定后缀地址,不再退回临时地址或旧前缀地址。
  6. 路由器脚本只从 LAN 口当前有效公网前缀拼自己的 ::1

这次顺手也把几台设备都统一成这个策略:移动设备自己维护自己的 DDNS,路由器只维护路由器自己的记录。这样设备带出门时不会被家里路由器的旧地址“代表”,家里前缀变动时也不容易被旧静态地址误导。

一个小经验:IPv6 DDNS 里“固定后缀”很好用,但前提是脚本必须先锚定当前前缀。否则旧地址残留时,那个最像正确答案的地址,往往正是最坑的答案。