你在 FORWARD 链里为 VPN 流量添加了一条规则,又从同一台 Linux 机器上发起请求,计数器却始终为零。规则本身大概没问题。数据包从未走这条路径,因为由机器自身产生的请求不会被它转发。沿着数据包从套接字到网线的生命周期走一遍,就能解释原因,以及规则究竟该放在哪里。

先弄清楚数据包从哪儿诞生

Netfilter 的钩子顺序把情况分成三类,其中只有一类会经过 FORWARD:

OriginPathHook that sees it
Local processOUTPUT then POSTROUTINGOUTPUT, POSTROUTING
Addressed to a local servicePREROUTING then INPUTPREROUTING, INPUT
Received for another hostPREROUTING, FORWARD, POSTROUTINGFORWARD

本地应用的起点在 OUTPUT 路径上,因为内核已经确定该数据包产生于本机,永远不会成为转发的候选。建立隧道并不会改变这一分类;隧道只是数据包可以离开的出口设备,而不是把本地输出变成过境流量的阶段。

两个数据包,一个故事

隧道建立之后,应用的内层数据包与加密后的外层数据包是两个不同的对象。内层数据包承载应用选定的目的地址和载荷。外层数据包把密文送往对端的地址和端口,真正穿过物理接口的是它。

你的策略只涉及其中一种,而选错对象正是规则看起来毫无作用的常见原因。匹配对端外层地址的规则,无论里面应用在做什么,看到的隧道数据包都一样。匹配内层目的地址的规则看到的是你真正关心的流量,但只有当该数据包抵达你安装规则的那个钩子时才会生效。

命名空间会移动边界

容器让这件事更难,但原则没变。应用的请求是容器网络命名空间内部的本地输出。宿主在虚拟以太网接口上收到这份流量并可能转发它,因此即便产生请求的进程从未碰过宿主的 OUTPUT 链,宿主上的 FORWARD 规则也可能有意义。

在挑选钩子之前,先确定规则集和隧道究竟在哪里。容器内的隧道、容器经由宿主路由的宿主侧隧道,以及独立命名空间里的隧道,是三种不同的拓扑,规则的位置也各不相同。要在拥有该设备的命名空间里读规则,而不只是默认命名空间。

先问一句:到底需不需要标记

如果目标只是把一个目的前缀送进隧道,为该前缀加一条路由可能就够了。先核对生效的路由,包括服务端下发的任何东西:被重定向的默认路由会把远多于你设想的窄策略的流量送进 VPN,再多的标记也无法让结果符合初衷。

当判断依赖路由无法表达的东西时,才需要分类,比如来源、用户或进程。对收到的流量,在 PREROUTING 里打标记即可。对本地流量,标记必须发生在输出阶段,并且能在打标后触发一次新的路由查找。用 nftables 就是输出钩子上的 route 类型链;普通的输出过滤链只会写入标记值,路由不变。

用 iptables 时,对应做法在 mangle 表的 OUTPUT 链里,要求相同:标记存在之后必须重新审视路由决策。接着还要有一条策略规则消费该标记,并指向含有可用路由的表。设置 conntrack 标记是第三种独立操作;它把值存在流上,本身并不会把该值放到当前数据包上供路由读取。

把查找和计数器放在一起验证

nft list ruleset
ip route get 198.51.100.25
ip route get 198.51.100.25 mark 0x1

地址和标记只是示例,两条路由查询回答的是不同的问题。第二条问的是:如果数据包带着该标记,内核会怎么做;它并不能证明你的规则集真的打了标记。把它当作假设检验,而不是策略生效的证据。

然后跑一个无害的请求,同时看两样东西:所选钩子上的计数器,以及数据包实际走的出口。计数器涨了但数据包从别处出去,说明标记或表不对。两者都不动,说明数据包根本没到那个钩子,下一个问题就是命名空间或路径。

最后,把测试限定在机制能表达的范围内。一条针对单个地址的路由不会变成地址轮换的服务的长期策略。先定数据包的范围,再检查路由与标记的设计能否在重连之后仍然守住这个范围。

相关文章