科大镜像站于 2024 年 6 月 8 日 14:53:44 至 15:33:57 出现网络故障,期间校外 IPv4 无法访问,但 IPv6 访问正常。

经过

由于近日网络波动,我尝试调整镜像站服务器上的网络配置来改善一些出口的连接质量。镜像站的主力服务器运行 Debian 12 Bookworm,使用 systemd-networkd 管理网络配置,配置文件如下:

$ ls -1F /etc/systemd/network/
00-enp0.link
01-enp1.link
bond1.netdev
bond1.network
cernet.netdev
cernet.network
enp0.network
enp1.network
mobile.netdev
mobile.network
telecom.netdev
telecom.network
unicom.netdev
unicom.network

$ ls -1F /run/systemd/network/
cernet.network.d/
mobile.network.d/
telecom.network.d/
unicom.network.d/

我自己的一台 Ubuntu 服务器也采用手工维护 systemd-networkd 配置文件的方式管理网络配置,因此根据经验,我在修改完相关文件后,执行 systemctl restart systemd-networkd 重载配置,然后就认为所有配置都正常加载了。

下午 15:19,有同学在 USTCLUG 的群聊中提问「镜像站对校外 IPv4 访问关闭了吗」,我才回去进一步确认情况:

ibug@mirrors4:~$ ip r g 114.114.114.114
RTNETLINK answers: Network is unreachable
2|ibug@mirrors4:~$

检查发现所有对外的 IPv4 的策略路由规则(Policy-Base Routing rules,PBR)和路由表都没有正确加载:

$ ip rule
0:      from all lookup local
1:      from all lookup main suppress_prefixlength 1 proto static
3:      from all oif telecom lookup telecom proto static
3:      from 202.141.160.110 lookup telecom proto static
3:      from 218.104.71.170 lookup unicom proto static
3:      from all oif unicom lookup unicom proto static
3:      from all oif cernet lookup cernet proto static
3:      from 202.38.95.110 lookup cernet proto static
3:      from 202.141.176.110 lookup mobile proto static
3:      from all oif mobile lookup mobile proto static
4:      from all lookup Special suppress_prefixlength 1 proto static
5:      from all lookup Ustcnet proto static
9:      from all lookup default proto static

$ ip r s t Ustcnet | wc -l
14

$ ip r s t Cernet | wc -l
0

$ ip r s t Telecom | wc -l
0

$

由于 table Ustcnet prio 5 这条规则和里面校园网的路由都正常加载了,所以校内访问没有受到影响。

检查 journalctl -u systemd-networkd 发现了一些异常(节选):

Jun 08 14:53:45 mirrors4 systemd[1]: Started systemd-networkd.service - Network Configuration.
Jun 08 14:53:45 mirrors4 systemd-networkd[53721]: bond1: netdev exists, using existing without changing its parameters
Jun 08 14:53:45 mirrors4 systemd-networkd[53721]: telecom: Configuring with /etc/systemd/network/telecom.network.
Jun 08 14:53:45 mirrors4 systemd-networkd[53721]: unicom: Configuring with /etc/systemd/network/unicom.network.
Jun 08 14:53:45 mirrors4 systemd-networkd[53721]: mobile: Configuring with /etc/systemd/network/mobile.network.
Jun 08 14:53:45 mirrors4 systemd-networkd[53721]: cernet: Configuring with /etc/systemd/network/cernet.network.
Jun 08 14:53:45 mirrors4 systemd-networkd[53721]: bond1: Configuring with /etc/systemd/network/bond1.network.
Jun 08 14:53:45 mirrors4 systemd-networkd[53721]: enp1: Configuring with /etc/systemd/network/enp1.network.
Jun 08 14:53:45 mirrors4 systemd-networkd[53721]: enp0: Configuring with /etc/systemd/network/enp0.network.
Jun 08 14:53:45 mirrors4 systemd-networkd[53721]: telecom: Could not set route: Nexthop has invalid gateway. Network is unreachable
Jun 08 14:53:45 mirrors4 systemd-networkd[53721]: telecom: Failed
Jun 08 14:53:45 mirrors4 systemd-networkd[53721]: unicom: Could not set route: Nexthop has invalid gateway. Network is unreachable
Jun 08 14:53:45 mirrors4 systemd-networkd[53721]: unicom: Failed
Jun 08 14:53:45 mirrors4 systemd-networkd[53721]: mobile: Could not set route: Nexthop has invalid gateway. Network is unreachable
Jun 08 14:53:45 mirrors4 systemd-networkd[53721]: mobile: Failed
Jun 08 14:53:45 mirrors4 systemd-networkd[53721]: cernet: Could not set route: Nexthop has invalid gateway. Network is unreachable
Jun 08 14:53:45 mirrors4 systemd-networkd[53721]: cernet: Failed

手动重载四个未正确配置的网络接口后,IPv4 访问恢复正常。

networkctl reconfigure cernet
networkctl reconfigure telecom
networkctl reconfigure mobile
networkctl reconfigure unicom

调查

根据经验Nexthop has invalid gateway 是因为在添加网关路由(route add <PREFIX> via <ADDR> dev <DEV>)的时候,该网关地址还不可达。考虑到 systemd-networkd 在重启的时候会重新配置所有的路由表和路由规则,这个问题可能和我们自己删掉了 kernel 提供的两条默认路由规则有关:

# /etc/systemd/system/systemd-networkd.service.d/override.conf
[Unit]
StartLimitIntervalSec=0

[Service]
#Environment=SYSTEMD_LOG_LEVEL=debug
ExecStartPre=-+/sbin/ip rule delete from all table main pref 32766
ExecStartPre=-+/sbin/ip rule delete from all table default pref 32767

[Install]
Alias=networkd.service

这两条 ExecStartPre 来源于我们的旧服务器的配置方案。旧服务器仍然采用 Debian 默认的 ifupdown 方案,而不是 systemd-networkd,因此路由和 PBR 都依赖我们自己写的 shell 脚本调用 iproute2 来配置。其中配置路由规则的脚本如下:

ip rule flush
ip rule add pref 1 lookup main
# ...
ip rule add pref 9 lookup default

在迁移这些配置的时候,我们本着尽量和旧配置保持一致的原则,加上了这两条 ExecStartPre 作为 ip rule flush 的替代,并且在当时(2020 年)也没有仔细搞清楚所有细节。

在夜间用户量较少的时候,我再次重启 systemd-networkd 服务,复现了相同的问题,然后注释掉了这两条 ExecStartPre,并尝试把删掉的路由规则加回来:

ip ru a table main pref 32766
ip ru a table default pref 32767

但是重启 systemd-networkd 服务的时候,它们又消失了。在 systemd 的 GitHub 仓库中搜索 issue,发现 systemd/systemd#19106,其中提到 systemd-networkd 在启动的时候会清空所有的路由规则(ManageForeignRoutingPolicyRules=yes,systemd v248),但是会跳过 kernel 提供的三条默认规则(protocol kernel),因此重新尝试手动恢复我们自己删掉的两条规则:

ip ru a table main pref 32766 proto kernel
ip ru a table default pref 32767 proto kernel

再次重启 systemd-networkd 服务,这两条规则终于保留了下来,并且 journalctl -u systemd-networkd 中也没有再出现 Nexthop has invalid gateway 的错误,问题就此彻底解决。

思考

注意到故障期间 IPv6 没有受到影响,根据上面的分析,这应该是因为我们没有删掉 kernel 默认的 IPv6 路由规则(32766: from all lookup main),使得 systemd-networkd 能够正常添加 IPv6 路由。

最开始配置这台服务器(2020 年)的时候没有出现相关问题,可能是因为清空路由规则的行为是在 systemd v242 中引入的,而当时我们的服务器运行的还是 Debian 10 Buster,使用的 systemd 版本是 v241,因此没有触发这个问题。再加上镜像站服务器的重启频率仅为一年一次,且重启的全程都有同学关注,一些需要手动维护的配置(如在网络配置失败时 networkctl reconfigure 等)就容易忘记。