同一台主机上的两台虚拟机通过网桥访问 LAN,而放在 ip forward 链里的规则显示出一个从不移动的计数器。帧是真实存在的,流量也确实在通过,只是规则不在它的路径上。比较 Linux 暴露桥接流量的两种方式,既能解释这种沉默,也能解释为什么加载一个旧模块是比看上去更糟的答案。

穿过同一台主机的两条路径

数据包可以两种截然不同的方式离开这台主机,而差别决定了哪些钩子会看到它。如果目的地在物理端口之外,且帧在两个网桥端口之间交换,那么主机转发的是以太网帧;路由代码不会被询问,也不会发生任何被路由的跳。如果目的地就是主机本身,或者是通过隧道接口到达的网络,那么数据包会被路由,IP 层的钩子确实生效。请记住这个岔路,因为后续关于链和族的每个问题都取决于帧走的是哪条路径。

这个区分就是一条什么都不统计的规则背后的全部故事。从 veth9a1 到 enp3s0 的桥接帧穿过的是网桥,而不是 IP 协议栈,因此 ip forward 链根本没有可匹配的东西。旧示例中的接口名并不能确定你走的是哪条路径,所以第一步是看拓扑,而不是看规则集。

隧道增加了值得命名的第三种情况。TUN 设备把 IP 数据包交给主机,因此它们被路由,并确实穿过 IP 链;而接到网桥上的 TAP 设备交付的是帧,行为与桥接的情况相同。决定这一点的是设备类型,而不是产品名称。

比较能看到桥接帧的两个族

Linux 通过 nftables 的 bridge 族支持原生网桥过滤,检查并丢弃在途帧并不需要任何旧模块。内核自己的网桥文档直接给出了这个建议,在搜索结果让你加载模块之前,这值得先知道。

Observation pointWhat it seesWhat it costs
nftables bridge family chainsEthernet frames forwarded between bridge ports, in both directionsRules must be written in bridge syntax with link-layer matches
Legacy br_netfilter plus an ip chainBridged IPv4 and IPv6 packets, re-exposed through IP-layer hooksExisting host rules may begin matching traffic they never saw before
inet chain for routed trafficPackets the host routes, including traffic arriving from a TUN deviceNo visibility into purely switched frames

用 nft 命令代替 iptables 本身并不能把规则放进正确的族。在 bridge 族中声明的链能看到帧;在 ip、ip6 或 inet 中声明的链看不到,无论它是哪个二进制文件创建的。族是链的属性,而不是输入它的工具的属性。

旧路径的工作方式不同,这正是应该比较、而不是凭习惯选边的原因。加载 br_netfilter 并启用 bridge-nf-call 设置,会把桥接的 IPv4 和 IPv6 流量重新通过 IP 层的钩子暴露出来,于是普通的 iptables 规则终于能匹配到它。帧本身没有变,变的是它们经过的钩子集合,这意味着主机上每一条已有规则都多了一批新的受众。

在添加规则之前先读清实际路径

三条命令能确定你在哪里:网桥接在什么上、旧模块是否处于活动状态,以及 bridge 族里已经有哪些链。在写任何东西之前先运行它们,因为答案决定了你要扩展两种设计中的哪一种。跳过这一步,正是正确的规则落进一条无人经过的链的方式。

bridge link show
lsmod | grep br_netfilter
nft list ruleset | grep -A4 "chain forward"

输出把几种情况分得很清楚。列在网桥下面的端口告诉你哪些接口在交换;lsmod 的结果为空意味着桥接帧在 IP 层上完全不可见;bridge 族里已经存在 forward 链,则意味着策略已经有主人,你的规则应该放在那里,而不是新建一条自己的链。

然后加一条容易回读的规则,带计数器、不改判定,并让一条有代表性的流穿过网桥。计数器开始移动,证明帧到达了规则。它只能证明这一点,所以还要顺着被拒绝的路径再走一遍,因为接受一切的策略同样会让计数器移动。

nft add rule bridge filter forward iifname "veth9a1" oifname "enp3s0" counter accept

旧模块改变行为的地方

在正在运行的网桥上启用旧路径,并不是中性地增加可见性。此前对桥接流量不可达的规则会变得可达,包括为另一个接口写的主机规则,而 ip forward 链里的默认 drop 策略可能突然作用在原本原样通过的帧上。在通过那座网桥管理的主机上,结果是没有任何控制台的失联。这不是假设的故障模式;它是在你刚刚加宽的路径所到达的机器上加宽钩子集合的普通后果。

更安全的顺序是先确定哪个族拥有策略,把规则写在那里,并在改动任何 sysctl 之前保留一条独立的恢复路径。如果两个族都持有规则,就把哪个是权威写清楚;两套重叠的策略,正是数据包在一处被接受、在另一处被丢弃、却没有任何一个文件可读的原因。

隔离本身仍然需要完整的设计。允许一个源到一个目的地,只能证明一条流是通的,而需求通常是一台虚拟机不能到达它的邻居或主机。拒绝规则上的计数器比接受规则上的计数器更重要,因为接受计数器本来就是你预期会动的那个。

最后,把主机入站的情况单独拿出来。发往网桥自身 IP 的流量到达的是 input,而不是 forward,无论你选哪个族,forward 里的规则永远不会看到它。三种情况、两个族、一个教训:路径决定钩子,钩子决定你的规则到底存不存在。

相关文章