nftables cgroup 绕过还需要修正源地址
标记一个 cgroup 会把它送往另一张路由表,而套接字仍保留它已有的地址。分类、路由和地址转换是三个独立步骤。
标记来自某个 cgroup 的流量是容易的部分。在一款应用经物理网卡出去、而其余流量都留在隧道里之前,三个独立决定必须对齐:哪个套接字属于例外,标记触发哪次路由查找,以及数据包发出时携带哪个源地址。多数失败的尝试把第一件做对,第二件做对一半,而从不看第三件。
三个决定,而不是一个
分类回答谁被豁免。路由回答被标记的包改去哪里。源地址选择回答对端会看到什么,以及回包该回到哪里。
这三件事会各自失效。一个包可以被分类、被改道到物理网卡,却仍然带着上游路由器无法回送的私有地址,于是产生一个看起来和防火墙拦截一模一样的超时。分类这一步不会提醒你。路由这一步也不会。
顺序同样重要。分类发生在数据包生成时,路由发生在其后,而地址转换发生在出口处。
如果你说不出三个阶段里哪个坏了,你就不是在调试,而是在重排规则并指望运气。
匹配一个明天仍然存在的 cgroup
nftables 可以检测套接字的 cgroup v2 归属,这确实有用,因为它跟随的是进程而不是目标。陷阱在于标识符。从菜单启动的桌面应用常常活在一个临时 scope 里,下次启动时名字就变了。
nft list ruleset
systemctl status session.slice 2>/dev/null
cat /proc/self/cgroup从某次会话里复制 scope 名字并硬编码,做出来的规则今天能用,明天就悄悄不再匹配。不如把应用放进一个刻意设置的、稳定的执行上下文,比如你自己控制的专用 scope 或 unit,然后匹配它。这样规则能挺过重启和软件包更新。它也会变成同事在没有你的会话历史时也能推理的东西。
还要弄清楚你用的是哪种机制。cgroup v1 的 net_cls 分类和 cgroup v2 的套接字匹配是两回事,文档里名字相近的字段并不能让它们互换。
触发第二次查找的是 route 链
对本地生成的流量来说,链类型决定标记设置之后还会不会发生别的事。filter 类型的 output 链可以给包打标记,而内核会继续使用它已经选好的路由。route 类型的链才是请求重新查找的机制。
table inet bypass {
chain mark {
type route hook output priority mangle; policy accept;
socket cgroupv2 level 2 "app.slice" meta mark set 0x1 comment "app bypass"
}
}
ip rule add fwmark 0x1 lookup 200
ip route add default via 203.0.113.1 dev wlan0 table 200随后标记需要一条把它送进路由表的策略规则,而那张表必须完整:默认路由、应用仍然需要的本地目标,以及真正能应答的上游网关。
要有意识地选择标记值。WireGuard 会为自己的外层传输流量使用自己的 fwmark,不小心撞上同一个数字,可能以难以诊断的方式干扰隧道自身的路由例外。
源地址不会跟着路由走
已经为隧道路径选好源地址的套接字,在改道之后仍保留那个地址。路由变更里没有任何东西重写它,所以数据包带着上游网络无法回送的私有源地址从物理网卡出去。
可见的症状是应用看起来在发数据却永远收不到回应,以及防火墙日志什么都不显示,因为包根本没有回来。
窄范围的修法是:一条只针对该应用标记和它真实出口网卡的源地址转换规则,与已有的 NAT 结合,而不是取代它:
table inet bypass {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
meta mark 0x1 oifname "wlan0" masquerade
}
}只转换这个例外,别的都不碰。一条大范围 masquerade 规则能修好症状,却毁掉你分辨哪股流量走了哪条路的能力。匹配标记、真实出口网卡,如果可以的话再匹配应用自己的地址。宽泛的规则好写,事后却几乎无法审计。
还有哪些东西逃出了例外
域名解析可能来自另一个 cgroup 里的独立 resolver 进程,所以对浏览器套接字做分类并不会分类它的查询。如果 resolver 的查询留在隧道里,而连接却走到隧道外,split-horizon 的应答就可能与路径不匹配。这种不匹配会造成看起来像 DNS 缺陷、实际却不是的故障。
在多数配置里子进程会继承 cgroup,但把自己搬进独立 scope 的辅助进程不会。要检查而不是假定,尤其是任何会自我重新执行的程序。
最后,要决定隧道断开时应该发生什么。悄悄变成完全绕过的例外,与失败即关闭的例外是两种不同的安全姿态,而其中只有一种是你想要的。