Skip to content
Mashiro
Go back

How One nft Wildcard Rule Took Down Every VM on My PVE Host

这事发生过两次。第一次是 8 月 31 日,当时没细查,把 pve-firewall 一停就恢复了,我也就没再管。第二次是 9 月 7 日凌晨,同样的症状又来了,这次逼着我把根因挖了出来。

What happened

凌晨 05:55,我刚在 /etc/pve/firewall/cluster.fw 里重新打开 PVE 防火墙。几乎同一时刻,宿主机上跑 Teleport 的那台 VM(下面叫 VM A)就断了外网,etcd 集群丢了两个 raft 对端,日志刷屏 request timed out

登进 VM 一看,四块网卡都是 UP,IP、路由、ARP 表全都正常,能 ping 通宿主机,但两条上联线路一个网关都 ping 不通。宿主机自己出网没问题,ping 1.1.1.1 通,ping VM 也通。再看同一台宿主机上的另一台 VM(VM B),一样出不去。

所以不是单台 VM 的毛病,也不是上游断了,问题卡在宿主机这一层。

Tracking it down

06:01 开始查。既然宿主机自己能出网、VM 到宿主机也通,唯独”VM 穿过宿主机去外面”这段不通,那就盯着 forward 方向看。

宿主机上除了 PVE 自己的防火墙,还有一张我 8 月 22 日写的 nft 表,这里叫它 routerfw,规则文件在 /etc/nftables.d/router.nft。它的 forward 链里有一条 iifname "vm*" oifname "vm*" drop,看到的第一眼就觉得可疑。

为了确认流量是不是真的进了这条链,我在链首插了两条只计数不判决的规则:

nft insert rule inet routerfw forward iifname "vmbr*" oifname "vmbr*" counter comment "diag-vmbr"
nft insert rule inet routerfw forward counter comment "diag-all"

然后让 VM A 分别走两条线路 ping 网关,回来读计数:

counter packets 838 bytes 613268 comment "diag-all"
iifname "vmbr*" oifname "vmbr*" counter packets 838 bytes 613268 comment "diag-vmbr"

838 个包进了链,838 个包都是 vmbr 进、vmbr 出。再往下一条就是 iifname "vm*" oifname "vm*" drop。到这里基本清楚了:vm*vmbr0 也匹配进去了。

Where that rule came from

先交代一下这张表是干什么的,不然后面的事说不清。

这台宿主机的 IPv6 不是上游直接给一个 on-link 的 /64 那种。上游分了一个 /48 给我,但它是路由过来的:宿主机和上游之间走一条单独的 VLAN 做传输链路,两头用一对 ULA 地址组成 /126 点对点互联,整个 /48 的下一跳就是宿主机。宿主机自己在 lo 上挂了 /48 里的一个 /128,再给这个 /48 加了一条 blackhole 路由兜底,免得没分配出去的子网在两边来回打转。

这样一来 VM 就不能直接桥到传输 VLAN 上,因为那条链路上只有两个地址,VM 的地址根本不在那个网段里。宿主机必须自己当路由器:从 /48 里切 /64 给每台 VM,自己做每个 /64 的网关。

具体实现用的是 PVE 的 SDN。建了一个 simple zone,每台需要 IPv6 的 VM 一个 vnet,PVE 会为每个 vnet 生成一个没有物理口的小网桥,名字就是 vnet 的名字,我起的是 vm101、vm102 这种。宿主机在小网桥上持有 <VM 的 /64>::1,SDN 的 dnsmasq 负责 RA 和 DHCPv6,VM 的网卡挂上去就能拿到地址和网关。

宿主机既然在传输链路上直接面对上游,又替 VM 转发流量,就得有防火墙。PVE 自带的防火墙当时是关着的,我就自己写了一张 nft 表,也就是 routerfw。它有两条链:

写 forward 链的时候,“VM 那一侧的接口”在我脑子里就是 vm101、vm102 这几个小网桥,用 vm* 一把抓再自然不过。至于宿主机上还有 vmbr0、vmbr1 这些 PVE 默认的网桥,名字也是 vm 开头,当时完全没想到。

更没想到的是,这条规则原本就不该碰到 vmbr0 上的流量。它是为三层路由写的,而 vmbr0 上的 VM 流量走的是二层桥接,两边本来井水不犯河水。这个”本来”在 PVE 防火墙打开的那一刻就不成立了。

Why it worked for nine days

这条规则从 8 月 22 日就在,中间正常跑了九天。它之所以一直没发作,是因为桥接流量默认根本不经过 forward 链。

VM 的 tap 口和物理上行口都挂在 vmbr0 上,VM 发出去的包在二层直接被网桥转到物理口,整个过程跟经过一台交换机没区别。iptables 和 nft 的 forward 链是三层的钩子,看不见这些包。

但 Linux 的 br_netfilter 模块有两个开关:

net.bridge.bridge-nf-call-iptables
net.bridge.bridge-nf-call-ip6tables

一旦置 1,网桥上的二层转发包会先被送进三层的 forward hook 走一遍规则,再回到网桥继续转发。PVE 防火墙正是靠这个机制工作的,它的 PVEFW-FORWARD 链要用 physdev 匹配 fwbr 端口来过滤 VM 流量,所以 pve-firewall 一启动就会主动把这两个值改成 1。PVE 默认的 sysctl 里它们是 0。

更要命的是,借道 forward hook 的时候,内核报告的 iifnameoifname 都是网桥本身。包实际上是从 tap 口进、物理口出,但在规则眼里就是”vmbr0 进、vmbr0 出”。真正的端口信息只有通过 physdev 才拿得到。

再加上 nft 的接口名通配符只做前缀匹配,vm*vmbr0vmbr1 统统成立。于是每一个桥接 VM 的包都命中第一条 drop。这张表是 inet 族,IPv4 和 IPv6 一起管,所以 IPv4 也跟着一起被丢了。

两次断网的时间点也对上了:都是 pve-firewall 被启动、bridge-nf-call-iptables 变成 1 的那一刻。

Why the symptoms were so misleading

回头看,这组现象每一项单独看都指向别处:

测试结果原因
宿主机 ping 外网走 output hook,不经过 forward
宿主机 ping VM同上
VM ping 宿主机宿主机侧走 input hook
VM ARP 网关ARP 不是 IP 包,br_netfilter 不管
VM ping 网关或外网不通穿过网桥出去,被 forward 链丢掉
VM 的网卡、网桥、fdb、路由全正常问题根本不在这几层

VM 里能查的都正常,网桥也正常,唯独跨网桥的三层流量凭空消失。很容易怀疑到上游线路或者 VM 内部去,而真正的位置是宿主机上一条跟 VM 毫无关系的路由规则。

The fix

在 routerfw 的 forward 链最前面加一条放行,让桥接流量直接跳过这张表:

chain forward {
    type filter hook forward priority filter; policy accept;
    iifname "vmbr*" oifname "vmbr*" accept comment "bridged VM traffic, not for routerfw"
    iifname "vm*" oifname "vm*" drop
    ...
}

06:08 先热插一条进去,VM 网络立刻恢复,etcd 日志出现 peer became active。注意 shell 会吃掉引号,命令行里要转义:

nft insert rule inet routerfw forward iifname \"vmbr*\" oifname \"vmbr*\" accept

06:15 写进文件,nft -f /etc/nftables.d/router.nft 重载,持久化完成。

这条 accept 只结束 routerfw 这一条链的判定,PVE 自己的 PVEFW-FORWARD 是独立的链,照常生效。修完之后 PVE 防火墙保持开启,VM A 两条线路都能出网,WireGuard 两个 peer 握手恢复,Teleport 没重启,VM B 也恢复了。

顺带一提,网关本身不回 ICMP,宿主机和 VM 都 ping 不通它,这是上游的策略,不是故障。排查时差点被这个带偏。

Lessons

写接口名通配符的时候要想想前缀会不会撞上别的东西。vm*vmbr* 这种,写的时候觉得理所当然,出事的时候才发现它们是一家的。要么显式列出接口名,要么在链首把 vmbr 排除掉。

宿主机上只要挂了自定义的 forward 规则,开 PVE 防火墙之前就得意识到 br_netfilter 会把桥接流量也送进来。这两个东西看起来互不相干,实际上共用同一个 hook。

第一次出事的时候用关防火墙绕过去,问题就被盖住了,才有了第二次。绕过之后还是得回头查根因。

最后是一个排查的经验:宿主机正常、VM 到宿主机正常、VM 到网关不通、多台 VM 同时中招。下次再看到这组信号,先查 nft list rulesetbridge-nf-call-iptables,别急着怀疑线路。


Share this post: