标签: iptables

  • 深入 nftables 规则陷阱排查:NAT Hook 优先级错位引发的 Conntrack 绕过与流量黑洞实战

    核心结论:在从 iptables 向 nftables 迁移时,若自定义 NAT Chain 的 Hook 优先级配置不当(如 PREROUTING 优先级设为低于 -200),会导致数据包在进入 nf_conntrack 跟踪逻辑前触发 NAT 动作。由于 Netfilter 底层 NAT 强依赖连接跟踪上下文,过早执行将引发状态机丢失,导致 DNAT/SNAT 静默失效。解决方案是严格对齐 Netfilter 规范:PREROUTING 必须使用 -100,POSTROUTING 必须使用 100

    排查网络问题,最怕的不是报错,而是没有报错但流量神秘消失。习惯了 iptables 的老兵在初次接触 nftables 时,往往会被其极度灵活的框架反噬。近期在某次基础架构系统大版本升级(CentOS 7 迁移至 Rocky Linux 9.2,Kernel 5.14.0,nftables v1.0.4)过程中,遇到了一个经典的 NAT 击穿现场。

    案发现场:诡异的 SNAT 穿透现象

    某次集群割接后,业务反馈部分跨网段的出口流量间歇性超时。在网关节点(充当 SNAT 出口)上抓包,看到了令人脑溢血的一幕:

    # tcpdump -i eth0 -nn host 10.200.1.50
    14:22:31.102311 IP 10.0.5.10.43521 > 10.200.1.50.80: Flags [S], seq 123456789, win 64240, options [mss 1460]
    14:22:32.105432 IP 10.0.5.10.43521 > 10.200.1.50.80: Flags [S], seq 123456789, win 64240, options [mss 1460]
    

    10.0.5.10 是内网 Pod IP,10.200.1.50 是外部系统 IP。按理说,经过网关节点时,流量应该被 SNAT 成网关的物理网卡 IP,但抓包显示内网 IP 直接裸奔到了公网。由于上级路由没有该内网网段的路由表,数据包被丢弃,业务端表现为 TCP 握手超时。

    检查 nftables 规则集,运维同学写的配置看起来“非常完美”:

    table ip custom_nat {
        chain postrouting {
            type nat hook postrouting priority -250; policy accept;
            ip saddr 10.0.0.0/16 oif "eth0" masquerade
        }
    }
    

    触发匹配的网段、出接口、动作都没错,而且没有任何报错信息。

    为什么自定义优先级的 NAT 链会静默失效?

    要搞清楚这个问题,必须剥开 Netfilter 的内核源码。

    在原生的 iptables 时代,nat 表的 Hook 优先级是内核硬编码固定的。你在 POSTROUTING 链写规则,它必然在 priority 100 执行。但 nftables 把定义权完全交给了用户,允许你在创建 Base Chain 时随意指定 priority 整数值。

    上述配置中,SRE 为了让 SNAT “尽可能早地执行以减少延迟”,将 postrouting 链的 priority 设置为了 -250。这就是灾难的根源。

    Netfilter 的核心数据包流转链路有着严格的优先级排序(定义在 include/uapi/linux/netfilter_ipv4.h):

    • NF_IP_PRI_RAW = -300

    • NF_IP_PRI_CONNTRACK = -200 (关键点)

    • NF_IP_PRI_MANGLE = -150

    • NF_IP_PRI_NAT_DST = -100

    • NF_IP_PRI_FILTER = 0

    • NF_IP_PRI_NAT_SRC = 100

    NAT 的本质依赖于 nf_conntrack(连接跟踪模块)。只有在 Conntrack 建立起一条连接(状态为 NEW)时,NAT 引擎才会去计算并绑定转换地址。后续属于同一条连接的数据包,直接复用 Conntrack 表里的 NAT 绑定信息,根本不会再去遍历 NAT 规则链。

    我们来看内核源码 net/netfilter/nf_nat_core.c 中的 nf_nat_inet_fn 函数(处理 NAT Hook 的核心逻辑):

    unsigned int
    nf_nat_inet_fn(void *priv, struct sk_buff *skb,
                   const struct nf_hook_state *state)
    {
        struct nf_conn *ct;
        enum ip_conntrack_info ctinfo;
    
        /* 获取当前数据包的连接跟踪信息 */
        ct = nf_ct_get(skb, &ctinfo);
        if (!ct) {
            /* 如果拿不到 ct 上下文,直接 ACCEPT 放行,跳过后续所有 NAT 处理! */
            return NF_ACCEPT;
        }
    
        // ... 后续计算和应用 NAT 转换的逻辑
    }
    

    当 priority 设置为 -250 时,这个自定义的 NAT Chain 会在 Conntrack Hook (-200) 之前执行。此时数据包刚进入协议栈,nf_ct_get(skb, &ctinfo) 返回 NULL。内核一看没有连接跟踪上下文,直接返回 NF_ACCEPT 放行数据包,完全跳过了你辛辛苦苦写的 masquerade 规则。这就是内网 IP 发生“裸奔逃逸”的根本原因。

    排查实录:nftrace 链路追踪

    为了验证推论并在现场拿到铁证,我们使用 nftables 自带的神器 nftrace 进行包流向追踪。

    首先,在 raw 链层面给测试包打上 trace 标记:

    nft add table ip debug_trace
    nft add chain ip debug_trace preraw { type filter hook prerouting priority -350\; }
    nft add rule ip debug_trace preraw ip daddr 10.200.1.50 meta nftrace set 1
    

    随后,另开一个终端监听 trace 日志:

    nft monitor trace
    

    发起一次请求,观察 trace 输出(精简版):

    trace id e4b2c1 ip debug_trace preraw packet: iif "eth0" src 10.0.5.10 dst 10.200.1.50 ...
    trace id e4b2c1 ip debug_trace preraw rule ip daddr 10.200.1.50 meta nftrace set 1 (verdict continue)
    trace id e4b2c1 ip custom_nat postrouting packet: oif "eth0" src 10.0.5.10 dst 10.200.1.50 ...
    trace id e4b2c1 ip custom_nat postrouting policy accept (verdict accept)
    

    注意看输出,数据包流经了 custom_nat postrouting,但是没有触发任何 rule 匹配,直接命中了 policy accept退出。因为内核底层 nf_nat_inet_fn 直接 bypass 了规则引擎。

    修复方案极其简单,遵守内核规范即可:

    修改 Base Chain 的优先级至 srcnat 的标准位置(100):

    nft add chain ip custom_nat postrouting '{ type nat hook postrouting priority 100; policy accept; }'
    

    重载规则后,抓包瞬间恢复正常,NAT 转换成功。

    扩展防御:iptables-nft 混用下的优先级重叠踩坑

    在 K8S 等容器环境中,还存在另一个隐蔽的黑洞:iptables-legacynftables (或 iptables-nft) 混用。

    许多 OS 发行版默认采用 iptables-nft(用 iptables 的命令行包装 nftables 底层),Kube-proxy 也因此创建了大量 priority = -100 的 nftables nat 规则。 如果你使用原生 nftables 语法另外创建了一个 table,且优先级也设为 -100,会发生什么?

    根据 Netfilter 设计,相同优先级 Hook 的执行顺序是不确定的(通常取决于内核模块加载顺序或 Table 名称字母序)。如果你的原生 nftables 规则先执行并且返回了 ACCEPT(没有做 NAT),那么 kube-proxy 的 KUBE-SERVICES NAT 规则即便随后执行,也可能因为连接状态的改变而无法生效,导致 NodePort 流量随机超时。

    最佳实践:在任何生产服务器上,绝对不要混用多种防火墙工具。要么全盘接管使用纯正的 nftables,要么彻底清理原生 nft 规则,交由 iptables-nft 代理。使用 lsmod | grep _tables 检查,坚决剔除 ip_tablesx_tables 内核模块。

    常见问题 (FAQ)

    Q1: 修复了 nftables 的 NAT 规则并 reload 后,为什么已有的长连接依然是断网状态? 因为 NAT 操作只在连接建立(ct state NEW)的首包触发。对于已经建立的存量连接,它们在 Conntrack 表里的记录依然是未 NAT(或者错误 NAT)的状态,后续包直接复用该错误记录。排查时必须用 conntrack -D -p tcp --dport <端口> 清除脏连接,强制触发重连与重新 NAT 计算。

    Q2: 怎么快速确认当前系统里挂载了哪些 Netfilter Hook 及其对应的真实优先级? 如果是较新版本的 nftables (>= 1.0.0),可以直接使用命令: nft list hooks 如果是老版本或者想看底层全貌,可以通过 BPF 工具或检查 /proc/net/netfilter/nf_log 间接推断,或者通过 nft list ruleset 提取所有 Base Chain 的 priority 声明逐一盘点。

    Q3: 为什么有时候 SNAT 成功了,抓包看到了网卡公网 IP,但回程包依然被内核无情 DROP? 除了 NAT,最容易忽视的是 rp_filter(反向路由校验)。如果数据包的入口网卡和内核路由表中回程目标不一致(非对称路由),哪怕 NAT 配置完美,数据包也会在 IP 层被直接丢弃。排查时重点检查 sysctl -a | grep rp_filter,在复杂网络拓扑下通常需要设为 2 (松散模式) 或 0

    Q4: 刚从 iptables 迁移到 nftables,发现 CPU softirq/usr 飙升,QPS 下跌怎么查? 极大概率是直接用工具(如 iptables-restore-translate)做了 1:1 的线性转换。iptables 的线性链匹配时间复杂度是 O(N),而 nftables 最大的性能优势是支持匿名集合 (Sets) 和映射字典 (Maps) (O(1) 复杂度)。千万不要在 nftables 里写一万行 ip saddr X.X.X.X accept,应当合并为 ip saddr { 1.1.1.1, 2.2.2.2 } accept

  • 深入 nftables 迁移网络黑洞排查:多 Base Chain 语义陷阱与 Docker 流量阻断实战

    近期在接手一批新上线的 Debian 12 宿主机时,遇到了一个极其隐蔽的网络黑洞问题。业务侧反馈,将服务从 CentOS 7 迁移到新环境后,宿主机自身网络一切正常,但 Docker 容器内的所有 Outbound 流量(包括 DNS 解析、外部 API 调用)全部超时。

    简单看一下背景和结论:为了对齐基础安全基线,系统组在新系统上摒弃了老旧的 iptables,转而使用原生的 nftables 编写了主机防火墙策略,并将 forward 链的默认策略设置为 drop故障的根本原因在于对 nftables 的 Base Chain(基础链)和 accept 动作语义理解不到位。 在配合 iptables-nft(Docker 默认的底层网络驱动)工作时,nftables 中不同表里的 Base Chain 会发生叠加。Docker 规则里的 ACCEPT 仅仅中断了当前链的匹配,报文随后又掉进了原生安全策略的 drop 陷阱中。

    把两种时代的产物混用,又不仔细看 Netfilter 底层 Hook 的运转机制,就像在同一个路口安排了两个互不理睬的交警,一个挥手让你走,另一个直接把你的车按死。

    现场还原与报错表现

    排查过程从最基本的抓包开始。在容器内执行 curl 8.8.8.8,同时在宿主机的几个关键网卡上抓包:

    # 容器内 veth 接口看到 SYN 发出,但没有 SYN-ACK
    tcpdump -i veth_xxx -nn host 8.8.8.8
    
    # docker0 网桥上能看到报文进入
    tcpdump -i docker0 -nn host 8.8.8.8
    
    # 物理网卡 eth0 上毫无动静
    tcpdump -i eth0 -nn host 8.8.8.8
    

    报文在路由判决后,准备进行转发(Forward)时凭空消失了。习惯性地敲下 iptables -nL FORWARD,看到 Docker 生成的规则依然健在:

    Chain FORWARD (policy DROP)
    target     prot opt source               destination         
    DOCKER-USER  all  --  0.0.0.0/0            0.0.0.0/0           
    DOCKER-ISOLATION-STAGE-1  all  --  0.0.0.0/0            0.0.0.0/0           
    ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
    DOCKER     all  --  0.0.0.0/0            0.0.0.0/0           
    ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0           
    ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0           
    

    表面上看,Docker 已经放行了跨网桥的流量。负责实施的同事一口咬定:“Docker 自己管理的 iptables 规则没有任何问题,肯定是内核路由参数 ip_forward 没开!” 然而 sysctl net.ipv4.ip_forward 明晃晃地显示着 1

    抽丝剥茧:nftables 里的“平行宇宙”

    问题出在哪里?在较新的发行版中,iptables 命令实际上只是 iptables-nft 的一个软链接。Docker 以为自己在操作传统的 iptables,实际上底层被翻译成了 nftables 的规则存入内核。

    此时我们看一眼主机上真正生效的全量规则表:nft list ruleset。 精简后的输出如下:

    # 这是 Docker 经由 iptables-nft 生成的表
    table ip filter {
        chain FORWARD {
            type filter hook forward priority filter; policy drop;
            jump DOCKER-USER
            jump DOCKER-ISOLATION-STAGE-1
            oifname "docker0" ct state related,established counter accept
            oifname "docker0" jump DOCKER
            iifname "docker0" oifname != "docker0" counter accept  # <-- 注意这里,Docker 决定 ACCEPT
            iifname "docker0" oifname "docker0" counter accept
        }
    }
    
    # 这是系统组手写的原生 nftables 主机防火墙
    table inet my_sec_firewall {
        chain base_forward {
            type filter hook forward priority filter; policy drop;  # <-- 这里是罪魁祸首
            ct state established,related accept
            # 这里仅仅放行了部分特定网段的内网互访,没有提及 docker0
        }
    }
    

    这里隐藏着一个巨大的语义陷阱:在旧的 iptables 架构中,一个包在一个 Table/Hook 中如果匹配到了 ACCEPT 规则,它的遍历就彻底结束了,直接进入下一阶段。但在 nftables 架构中,你可以定义无数个挂载在同一 Hook 点的 Base Chain(基础链)。

    上述配置中,ip filterinet my_sec_firewall 都注册了针对 forward hook 的 Base Chain,且优先级都是 filter(数值为 0)。

    当容器的流量进入 Netfilter 的 forward hook 时,发生了什么?

    1. 报文进入 Docker 的 FORWARD 链。

    2. 匹配到 iifname "docker0" oifname != "docker0" counter accept

    3. 关键点来了:在 nftables 中,Base Chain 里的 accept 叫做 Verdict: accept。它的完整语义是“停止遍历当前 Base Chain,允许该包继续走向下一个处于同等或更低优先级的 Base Chain”。

    4. 于是,报文带着 Docker 赐予的“通行证”,继续走进了系统组手写的 base_forward 链。

    5. base_forward 链左看右看,发现这条流量不符合任何放行规则,直接走默认策略 policy drop,报文被无情丢弃。

    这就是典型的“知其然而不知其所以然”。抄袭旧时代的防火墙规范,用新语法包装了一下,结果搞出了网络黑洞。

    现场 Debug 铁证:nftrace 的降维打击

    为了让同事彻底死心并理解这个过程,直接上 nftables 的杀手锏工具 nftrace 进行数据包流向跟踪。

    在我们的防火墙表里加一条 trace 规则:

    nft add rule inet my_sec_firewall base_forward meta nftrace set 1
    

    然后在另一个终端启动监听,并再次在容器内触发 curl 8.8.8.8

    nft monitor trace
    

    日志无情地揭露了报文的死亡现场:

    trace id 75b42d1f ip filter FORWARD packet: iif "docker0" oif "eth0" src 172.17.0.2 dst 8.8.8.8 ...
    trace id 75b42d1f ip filter FORWARD rule iifname "docker0" oifname != "docker0" counter packets 12 bytes 720 accept (verdict accept)
    ...
    trace id 75b42d1f inet my_sec_firewall base_forward packet: iif "docker0" oif "eth0" src 172.17.0.2 dst 8.8.8.8 ...
    trace id 75b42d1f inet my_sec_firewall base_forward rule meta nftrace set 1 (verdict continue)
    trace id 75b42d1f inet my_sec_firewall base_forward verdict drop  # <-- 在这里被默认策略处决!
    

    日志清楚地表明,报文先被 Docker 的链 accept,紧接着落入 my_sec_firewall 的链,并命中 drop 策略。

    解决代码与重构建议

    想要修复这个问题非常简单,既然它要过两道关,那就在原生安全策略里把 Docker 的网桥放行即可:

    nft add rule inet my_sec_firewall base_forward iifname "docker0" accept
    

    但是,作为架构师,这种补丁式的做法是不合格的。 因为 Docker 的网络隔离策略(比如容器间不可见、端口映射暴露限制)本身就非常复杂,如果强行用另一套独立表的策略去叠加,极易造成后续排查的灾难。

    最终的整改落地方案:

    1. 停止混用策略:如果系统中存在需要深度接管底层网络的组件(如 Docker、K8S kube-proxy),主机级的防火墙防护应尽量下放给外部设施(如云厂商的安全组、物理防火墙)。

    2. Hook 优先级规避:必须写本机策略时,确保你的防火墙 Base Chain 优先级数值不要和 Docker 的产生竞争。Docker 默认的 priority 是 0。如果你只做简单的黑名单前置拦截,可以建一个 priority -100 的链;如果你想要兜底,可以建一个 priority 100 的链。

    3. 放弃纯净洁癖:不要在运行了遗留 iptables/Docker 逻辑的机器上,强制推行所谓的“纯原生 nftables 架构”。要么让 Docker 禁用 iptables ("iptables": false in daemon.json) 完全靠你自己手写路由转发,要么老老实实顺从 iptables-nft 的兼容模式,把你的安全规则也用 iptables 语法追加进去。

    总结与排查清单

    在系统底层的迭代中,“兼容”往往是最危险的词汇。iptables-nft 给了一个完美的语法兼容幻觉,却暗改了多链并行的核心逻辑。

    同类问题速查清单:

    1. 辨别真伪 iptables:执行 update-alternatives --display iptables 确认系统当前底层是 iptables-legacy 还是 iptables-nft

    2. 全局视角查规则:抛弃 iptables -nL,排查网络不通时必须看全景:nft list ruleset,重点寻找包含 policy drop 的自定义 Base Chain。

    3. 理解 priority 与 accept 的关系:同一 Hook 点存在多个 Base Chain 时,accept 只是“出当前链”,不是“出整个 Hook”。只有 drop 才是真正的一票否决。

    4. 抓包查死因:如果 tcpdump 看到包进了某网卡但出不来,直接开启 nftables trace (meta nftrace set 1) 跟踪,看包死在哪个 Table 的哪条 Rule,比瞎猜高效百倍。

  • 深入 nf_conntrack 满载丢包排查:SNAT 端口耗尽引发的 SYN 阻断与 nftables Flowtable 旁路加速实战

    高并发网关常遇 nf_conntrack: table full 导致 SYN 丢包。盲目调大 nf_conntrack_max 只会加剧内核自旋锁争用与内存开销。根本解法是排查 SNAT 端口耗尽,并从 iptables 彻底迁移至 nftables,利用 Flowtable 机制开启流量卸载(Offload),让 ESTABLISHED 状态报文旁路跳过 Netfilter 核心链,实测可降低 40% 的 sys CPU 并彻底消除连接跟踪瓶颈。

    案发现场:诡异的 99 线毛刺与超时

    排查过程中,某承载了上万并发连接的 K8s Egress NAT 网关节点(Kernel 5.15.0)频繁出现请求超时,监控大盘显示 TCP 99线延迟出现规律性毛刺,Load Average 中的 sys CPU 间歇性飙升到 80% 以上。

    直接上机器看内核日志:

    $ dmesg -T | tail -n 20 | grep conntrack
    [Thu Oct 26 14:12:33 2023] nf_conntrack: nf_conntrack: table full, dropping packet
    [Thu Oct 26 14:12:33 2023] nf_conntrack: nf_conntrack: table full, dropping packet
    

    经典的连接跟踪表爆满导致丢包。看一下当前连接数与上限:

    $ sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max
    net.netfilter.nf_conntrack_count = 262144
    net.netfilter.nf_conntrack_max = 262144
    

    为什么盲目调大 nf_conntrack_max 是一剂毒药?

    遇到 table full,很多人的第一反应是无脑加大 nf_conntrack_max。在低并发场景下这确实管用,但在高吞吐 NAT 网关上,这是一剂毒药。

    nf_conntrack 是基于哈希表实现的。它的核心数据结构由 Hash buckets(桶)和链表组成。当你只调大 nf_conntrack_max 而不调整 nf_conntrack_buckets 时,每个 Hash bucket 下挂载的链表会变得极长。 内核在进行包过滤或 NAT 时,需要遍历链表来匹配五元组。链表越长,查询的开销越大;加上 Hash bucket 的自旋锁(spinlock)争用,在多核高 PPS(Packet Per Second)场景下,CPU 会被 __nf_conntrack_find_get 等函数吃干抹净(表现为软中断 si 和内核态 sy CPU 极高)。

    正确的临时缓解姿势必须是联动调整(保持桶大小为最大连接数的 1/4):

    # 1. 调大 Hash 桶大小(立即生效,不可通过 sysctl 修改)
    $ echo 262144 > /sys/module/nf_conntrack/parameters/hashsize
    # 2. 调大最大连接数
    $ sysctl -w net.netfilter.nf_conntrack_max=1048576
    # 3. 缩短 TIME_WAIT 和 ESTABLISHED 状态的超时时间,加速条目回收
    $ sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=300
    $ sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait=30
    

    但这只是治标。抓包发现,该节点作为 SNAT 网关,真实存在的活跃连接并没有达到 26 万,导致表满的真凶是 SNAT 端口耗尽引发的僵尸连接积压。由于 iptables 的 MASQUERADE 规则,多个内网 Pod 访问外部同一个目标 IP:Port 时,由于源端口池(默认 1024-65535)被快速消耗殆尽,新的 SYN 包在进行 NAT 转换时无法分配到 free tuple,导致连接状态卡死并在 conntrack 表中滞留。

    iptables 时代的穷途末路与 nftables 破局

    只要你还在用 iptables,每一个数据包都要不可避免地穿透 PREROUTING -> FORWARD -> POSTROUTING 整条链。即使是已经建立连接(ESTABLISHED)的报文,也要每次去走一遍 Rule 解析和 Conntrack 状态机。

    Kernel 4.16+ 引入了 nftables 的杀手锏功能:Flowtable (Fast-path Offload)。 它的底层原理极其优雅:对于已经建立连接的 TCP/UDP 流量,Flowtable 会在网卡的 ingress hook 点(非常靠前的位置)直接进行路由转发和 NAT 替换,完全绕过传统的 Netfilter 过滤链和 Conntrack 查询

    实战:将 iptables NAT 迁移至 nftables Flowtable

    不要再用 iptables-nft 这种套壳工具了,直接写原生的 nftables 配置。以下是我们在网关节点上的落地配置,实现内网到外网的 SNAT 并开启 Flowtable 硬件/软件卸载。

    清除老旧规则:

    $ iptables -F && iptables -t nat -F
    $ systemctl stop iptables
    

    编写 /etc/nftables.conf

    flush ruleset
    
    table inet filter {
        # 定义 Flowtable 开启卸载
        flowtable f {
            # 挂载在非常靠前的 ingress 钩子,优先级 0
            hook ingress priority 0;
            # 绑定内外网网卡(根据实际情况修改)
            devices = { eth0, eth1 };
        }
    
        chain forward {
            type filter hook forward priority 0; policy drop;
    
            # 核心逻辑:允许 ESTABLISHED 流量,并将新流量加入 flowtable 'f'
            ip protocol { tcp, udp } flow add @f
    
            # 允许内网 (10.0.0.0/8) 到外网的初始包通过
            iifname "eth0" oifname "eth1" ip saddr 10.0.0.0/8 accept
    
            # 允许已建立连接的回包
            ct state established,related accept
        }
    }
    
    table ip nat {
        chain postrouting {
            type nat hook postrouting priority 100; policy accept;
            # 传统 SNAT/Masquerade,只对首包生效
            oifname "eth1" ip saddr 10.0.0.0/8 masquerade random
        }
    }
    

    应用配置并验证:

    $ nft -f /etc/nftables.conf
    $ nft list ruleset
    

    注意:masquerade random 的加入是为了缓解 SNAT 端口分配的哈希碰撞冲突,配合 Flowtable 能最大程度压榨网关性能。

    性能表现对比

    迁移至 nftables Flowtable 后,使用 perf top 观察内核函数调用:

    • 迁移前ipt_do_tablenf_conntrack_in 长年霸占 Top 3,软中断消耗极大。

    • 迁移后:由于首包建立连接后,后续几十个甚至成百上千个数据包直接从网卡 ingress 进入 nft_flow_offload_eval 后被路由发出,ipt_do_table 直接消失,sys CPU 占用率暴降 40% 以上,dmesg 中再无 table full 报错。

    常见问题 (FAQ)

    Q1:为什么我明明清空了 iptables,用 iptables -L 还能看到一些莫名其妙的规则? 因为较新的 OS(如 Debian 11+, RHEL 8+)默认将 iptables 软链接到了 iptables-nft。这是兼容层,你在 iptables 敲的命令,其实被转换成了 nftables 的内置表。要查看纯正的 iptables 规则,请使用 iptables-legacy -L。在系统层面彻底向 nftables 演进时,强烈建议干掉所有 legacy 和兼容层,统一用 nft 命令行管理。

    Q2:开启 nftables Flowtable 之后,为什么 tcpdump 抓不到部分数据包了? 这是预期行为。Flowtable 提供了 Software Offload 和 Hardware Offload (NIC HW offload)。如果是 Hardware offload(需要网卡驱动支持 tc 卸载),数据包在物理网卡层面就被转发了,根本不会进入内核网络栈,挂在 AF_PACKET 上的 tcpdump 自然抓不到。即使是 Software offload,由于绕过了常规的 Netfilter RX 路径,抓包结果也会呈现“只看到 SYN 包,看不到后续数据流”的现象。排查网络问题时,需要临时禁用 flowtable 规则。

    Q3:在 K8s 中使用 IPVS 模式的 kube-proxy,也会受 nf_conntrack 限制吗? 会。虽然 IPVS 维护了自己的连接管理哈希表,但它仍然深度依赖 Netfilter 框架做底层的包拦截和 NAT 协调(尤其是 nf_conntrack)。K8s 场景下大量短连接(如探针、微服务间 RPC)极易打满 conntrack。除文中提到的调优手段外,建议通过 kube-proxy 启动参数 --conntrack-max-per-core 来合理规划容量,而非手动修改 sysctl,防止被 Kubelet 重置。