今天排了一个挺有意思的 IPv6 DDNS 小事故。
现象很简单:一个平时可以从外面 SSH 的虚拟机域名,突然开始卡住。ssh -6 user@host 没有马上报错,而是一直等到超时。DNS 还能解析,机器也确实在跑,所以一开始很容易误判成防火墙、SSH 服务、Cloudflare、路由器其中某个环节坏了。
真正的根因是:家里的公网 IPv6 前缀变了,但虚拟机里还残留着旧前缀下的静态地址。
这套 DDNS 原来的策略是“每台设备固定一个好记的后缀”,例如路由器用 ::1,某台 Mac 用 ::14,虚拟机用 ::21。脚本会在设备上找一个以这个后缀结尾的全局 IPv6,然后把它更新到 DNS。
这个想法平时很好用,坏就坏在一个边界条件:
- 运营商重新分配了新的 IPv6 前缀。
- 设备通过 RA/SLAAC 拿到了新前缀地址。
- 但旧的静态
old-prefix::21/64还挂在网卡上,而且生命周期是 forever。 - DDNS 脚本继续优先选择“看起来最像目标”的
::21。 - 于是 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 地址”里按后缀猜答案,而应该先确定当前真实前缀。
现在改成了这样的顺序:
- 从当前默认 IPv6 接口或路由器 LAN 接口读取正在使用的公网
/64前缀。 - 用这个当前前缀拼出固定后缀,例如
current-prefix::21。 - 只有这个地址存在且不是 tentative/deprecated,才允许发布到 DNS。
- Linux/NetworkManager 设备可以顺手把连接配置同步到新地址。
- macOS 设备只发布当前接口上已经存在的固定后缀地址,不再退回临时地址或旧前缀地址。
- 路由器脚本只从 LAN 口当前有效公网前缀拼自己的
::1。
这次顺手也把几台设备都统一成这个策略:移动设备自己维护自己的 DDNS,路由器只维护路由器自己的记录。这样设备带出门时不会被家里路由器的旧地址“代表”,家里前缀变动时也不容易被旧静态地址误导。
一个小经验:IPv6 DDNS 里“固定后缀”很好用,但前提是脚本必须先锚定当前前缀。否则旧地址残留时,那个最像正确答案的地址,往往正是最坑的答案。