Linux 上开启转发后 IPv6 默认路由消失了
开启 IPv6 转发会让内核忽略上游的 router advertisements,学到的默认路由随之到期。用 accept_ra 修正。
为了让 Debian 网关转发 VPN 客户端的流量,我在它上面设置了 net.ipv6.conf.all.forwarding=1,大约半小时后这台机器就再没有 IPv6 默认路由了。上游路由器活着,link-local 邻居仍有回应,uplink 也还持有自己的全球地址。真正停掉的是内核在该接口上继续接受 router advertisements 的意愿。路由不是在我敲下 sysctl 的那一刻消失的,而是在 RA 学来的默认路由寿命耗尽、又没有任何东西刷新它的时候消失的。
乍看之下没有任何东西是坏的。转发正常工作,旧路由还在缓存里时客户端也能通,故障比造成它的改动来得更晚。正因为这份延迟,问题常被归咎于上游,或归咎于一条根本没参与的防火墙规则。
动任何 sysctl 之前先读路由表
先用 ip -6 route show default,读属性而不只是网关。RA 学来的默认路由会标着 proto ra,带有优先级,并显示 expires 倒计时。手工添加的路由两者都没有,这个差别告诉你路由归哪个机制管。
$ ip -6 route show default
default via fe80::1 dev eth0 proto ra metric 1024 expires 1432sec hoplimit 64 pref medium然后看地址:ip -6 addr show dev eth0。以小时计的优先寿命和有效寿命的全球地址,会比以分钟计的 router lifetime 的默认路由活得更久。地址在而默认路由不在,就是宣告问题的典型特征。如果连地址也没了,就要先追链路、前缀信息或重复地址检测。
为什么转发会关掉 RA 处理
RA 是类型 134 的 ICMPv6 包,从 hop limit 255 的 link-local 源发出。内核使用它之前会先查接口的 accept_ra。默认值是 1,而 1 并不表示“是”。它的意思是:仅当本主机不转发时才接受。设成 forwarding=1 后该条件不再成立,于是通告在其中的前缀或默认路由器被考虑之前就被丢弃。
取值有三种,只有第三种能挺过转发:
accept_ra | forwarding=0 | forwarding=1 |
|---|---|---|
| 0 | ignored | ignored |
| 1 (default) | accepted | ignored |
| 2 | accepted | accepted |
另外两个开关决定实际会安装什么。accept_ra_defrtr 控制通告里的默认路由器是否变成路由,accept_ra_pinfo 控制前缀信息是否用于地址配置。关掉 accept_ra_defrtr 是让转发主机保留 SLAAC 地址而不学默认路由的常规做法。这是与 accept_ra 不同的杠杆,想要地址但不要路由时就用它。内核 IP sysctl 参考 把三者都列了出来。
用三条命令确认
先读运行中的值,而不是你以为已加载的文件:
sysctl net.ipv6.conf.eth0.forwarding net.ipv6.conf.all.forwarding \
net.ipv6.conf.eth0.accept_ra net.ipv6.conf.eth0.accept_ra_defrtr其次证明通告仍在到达。按 ICMPv6 类型字节过滤就够了,也省得去读一堵邻居发现流量的墙:
tcpdump -ni eth0 'icmp6 and ip6[40] == 134'如果几分钟内什么都没出现,accept_ra 就不是你的问题。上游停止通告了,路径上有东西在过滤 ICMPv6,或者那接口根本不在你以为的网段上。第三,自己索要一条通告并读它的字段:
rdisc6 eth0rdisc6 在 Debian 和 Ubuntu 上由 ndisc6 包提供。在输出里先看 router lifetime 和优先级,再改别的。router lifetime 为 0 的通告是要求移除默认路由器的指令,任何 accept_ra 取值都不能把它变成可用路由。这也是抓出桥接模式上游 CPE 的办法:它只发前缀,从不把自己当路由器。
只让 uplink 成为宽容接口
如果上游本就该是默认路由器,那就只在它上面接受通告,尽管转发开着,别处一律不接受:
# /etc/sysctl.d/60-ipv6-router.conf
net.ipv6.conf.all.forwarding = 1
net.ipv6.conf.eth0.accept_ra = 2
net.ipv6.conf.eth0.accept_ra_defrtr = 1划定范围很重要,因为 Linux 里的 IPv6 转发是一个全局开关,不是每条链路一个。往 net.ipv6.conf.wg0.forwarding 写 1,再读 net.ipv6.conf.all.forwarding:全局值翻转,每个接口都开始转发。你不可能在隧道上是路由器、在 LAN 上又是普通主机。能限定的是接受范围。把 accept_ra=2 留在 uplink,隧道和 LAN 接口保持 1 或 0,这样从隧道飘进来的通告就装不上一条指向隧道内的默认路由。
临时的 sysctl -w 撑不过下次开机,而 systemd-networkd 和 NetworkManager 都会各自按自己的配置写这些值。在 networkd 里,把 IPv6Forwarding=yes 和 IPv6AcceptRA=yes 设进 uplink 的 .network 文件;networkd 出于同样的内核原因把 IPv6Forwarding 记作全局。用 NetworkManager 时,uplink 上的 ipv6.method=auto 才是驱动 accept_ra 的东西,所以要在 nmcli connection up 之后重读 sysctl,别相信第一次检查。
静态方案 ip -6 route add default via fe80::1 dev eth0 确实可用,当上游路由器有稳定 link-local 地址时是合理答案。要说清楚你放弃了什么。静态默认路由永不刷新也永不撤回,所以上游换掉之后它仍指向一个死掉的邻居。RA 路由安全地失败,静态路由则悄悄地失败。
测转发出去的包,而不是网关自己的包
网关自己的 ping 能在任何客户端都用不了的路由上成功,也能因为网关就是源而成功。从已连接的客户端测,然后问网关它会为该客户端的地址做什么:
# on the VPN client
ping -6 -c 3 2001:4860:4860::8888
# on the gateway, using a client address
ip -6 route get 2001:4860:4860::8888 from fd42:20::2然后等一个 router lifetime 再看。你想要的稳态是一条标着 proto ra、expires 值不断重置的默认路由,外加 all.forwarding=1。如果 expires 只在倒数,说明通告仍在某处被丢弃,常见祸首是网络管理器在开机 sysctl 跑完之后又改回 accept_ra。
收尾前确认四件事:默认路由是 proto ra 且在刷新,accept_ra=2 只作用于 uplink,重启后和重启管理器后转发仍是 1,并且是客户端而不是网关完成 IPv6 ping。如果路由健康而客户端仍失败,下一个嫌疑是回程路径或上游防火墙,而不是通告。