接上篇的内容,我要从公网访问家中的内网服务,这完全依赖 DDNS 脚本能快速更新家里当前的 IPv6 地址,但是经过一段时间的使用,我发现时不时会有连不上的问题,具体体现在路由器重启,或者 WAN 口重新拨号之后,客户端的 DDNS 总是不能及时刷新最新的 IPv6 地址,那么问题出在哪里。
地址如何变化
首先 IPv6 地址是怎么变的,当路由器重启或者手动重启路由器的 WAN 口,一般都会获取新的 IPv6 地址,旧地址失效。但如果不主动重启 WAN 口呢?
经过一段时间的观察,在 OpenWrt 日志看到如下内容:
注意到这边联通为了防止长时间连接,会每 48 小时主动断开 PPPoE 会话然后重新拨号,自然的 IPv6 地址会发生变化。
为什么客户端 DDNS 没有刷新
路由器端的 IPv6 RA 我配置的 SLAAC 模式,客户端的 DDNS 脚本用的是 aliyun-ddns。因为我在路由器上配置了 OpenClash 的 Fake-IP 模式,从在线 API 获取 IPv6 总会有各种问题,所以开启了 CHECKLOCAL
从本地网卡获取 IP。
注意到当 WAN 重新拨号之后,路由器的确向客户端下发了新的 IPv6 前缀,但此时旧的 IPv6 地址依然没有失效,客户端同时拥有新旧两个 IPv6 地址,而 DDNS 脚本依然读取的是旧地址,所以无法及时刷新解析。
如何快速废弃旧地址
现在的问题就来到了如何在下发新前缀之后快速废弃掉旧的 IPv6 地址。这里就要先了解 IPv6 的两个重要参数:valid_lifetime
和 preferred_lifetime
。
valid_lifetime
(有效生命周期)- 表示该地址在多长时间内是“合法的(valid)”。
- 只要这个时间没过,该地址就仍然存在于系统中,可能用于接收已有连接的数据包。
- 即使过了
preferred_lifetime
变成了“弃用地址”,只要没过valid_lifetime
,这个地址还是可用的。
preferred_lifetime
(首选生命周期)- 表示地址在这段时间内是“首选的(preferred)”。
- 首选状态的地址可以用于新建连接。
- 一旦过期,就变成“弃用的(deprecated)”地址:不再用于新建连接,但仍可用于维持已有连接(直到
valid_lifetime
结束)。
所以此时我产生了两个修改思路:
- 把
preferred_lifetime
配置为 48 小时,这样运营商重新拨号之后旧地址的preferred_lifetime
应该会到期,系统优先选择preferred_lifetime
没有到期的新 IP。 - 或者让
valid_lifetime
尽可能短,这样旧地址就会快速到期并废弃掉。
但是实际发现并不可行。
先说方法 1,当前 DDNS 脚本并不会优先选择 preferred_lifetime
未到期的地址,虽然似乎只要我自己重新写一个 DDNS 脚本去筛选 preferred_lifetime
就可以解决问题,但是我懒(。
而方法 2 呢,这么干会让客户端的 IPv6 地址频繁发生变动,感觉并不是什么很优雅的解决方案。
随后我又想,那么在路由器 WAN 重新拨号的时候,发一条 RA 报文把旧地址的 valid_lifetime
变成 0 不就好了。然后我就被 ChatGPT 的幻觉骗了(
GPT 像模像样地告诉我可以用 ubus call odhcpd send_ra
自己发送 RA,然后我兴致勃勃地研究了半天,最后发现根本没有这玩意...
继续研究 OpenWrt 的配置,终于让我找到了相对完美的解决方案。
首先 LAN 口的配置中并不能直接设置 valid_lifetime
,只能指定 preferred_lifetime
,而 valid_lifetime
会由系统自动计算,会比当前 preferred_lifetime
更长。
而配置中还有一项叫做 ra_useleasetime
,在 luci 界面中叫做 遵守 IPv4 有效期
,此项为 1 时,会把 DHCPv4 的租约时间作为 IPv6 的 valid_lifetime
。
并且我发现当 WAN 重新拨号时,会发送 RA 广播将旧 IPv6 地址的 preferred_lifetime
置为 0。
那问题就很好解决了,开启 ra_useleasetime
,并把 valid_lifetime
和 preferred_lifetime
设置为一样的值,这里我就改为默认的 12h。此时只要一个 IPv6 地址没有废弃,那么它的 preferred_lifetime
必不为 0,而当 WAN 重新拨号时,旧地址的 preferred_lifetime
就会变成 0,而我要做的就是写一个脚本定时检查,移除 preferred_lifetime
为 0 的地址,就可以快速废弃失效地址。
#!/bin/bash
IFACE="br0"
ip -6 addr show dev "$IFACE" | \
grep -B1 "preferred_lft 0sec" | \
grep inet6 | \
awk '{print $2}' | while read -r addr; do
echo "[IPv6 Cleanup] Removing deprecated address: $addr from $IFACE"
ip -6 addr del "$addr" dev "$IFACE"
done