标签: Conntrack

  • 深入 Netfilter 陷阱排查:高并发 SNAT 引发的 nf_conntrack 满载丢包与 nftables Flowtable 加速实战

    核心结论:高并发网关出现 nf_conntrack: table full, dropping packet 导致 502 和延迟雪崩时,盲目调大 nf_conntrack_max 只会加剧 CPU SoftIRQ 负载。治本之策是针对内网信任流量在 raw 表下发 NOTRACK 规则,并全面向 nftables 迁移,利用 Flowtable 实现 Fast-path 连接绕过,能将网络栈包转发的 CPU 消耗降低 40% 以上。

    案发现场:网关节点的 99 线雪崩

    排查过程中,某 Kubernetes 生产集群的 Ingress/NAT 网关节点(Ubuntu 22.04, Kernel 5.15)遭遇突发性高延迟。业务侧反馈 API 接口 99 线延迟从 20ms 飙升至 3s 以上,并伴随大量 502 错误。

    登录节点,top 显示部分 CPU 核心的 si(SoftIRQ)长期被打满至 100%。敲下 dmesg -T | tail,系统日志里被一行致命报错刷屏:

    [Fri ...] nf_conntrack: nf_conntrack: table full, dropping packet
    [Fri ...] nf_conntrack: nf_conntrack: table full, dropping packet
    

    通过 sysctl net.netfilter.nf_conntrack_count 确认,当前连接跟踪表中的条目数已经逼近配置的 262144 上限。这是一个极其典型的 Netfilter 性能瓶颈。

    抓取 conntrack 现场状态进行分析:

    conntrack -L -o extended | awk '{print $4}' | sort | uniq -c | sort -nr
    

    输出显示,绝大多数记录处于 TIME_WAIT 和 UDP 的 UNREPLIED 状态。由于业务采用短连接模式,并发极高,NAT 模块强依赖 conntrack 表进行 Tuple 映射。一旦表满,内核网络栈别无选择,只能将新到达的 SYN 包直接 Drop,引发应用层的重传与超时风暴。

    为什么单纯调大 nf_conntrack_max 无法根治丢包问题?

    遇到这个报错,很多人的第一反应是无脑修改 /etc/sysctl.conf,将 nf_conntrack_max 调到 100 万甚至 200 万。这是极其危险的做法,等于饮鸩止渴。

    Netfilter 在内核中维护连接状态使用的是哈希表。这个哈希表的大小由 nf_conntrack_buckets 决定。如果你只放大了 max(最大连接数),而不按比例放大 buckets(哈希桶数量),会导致单个哈希桶下的链表变得极长。

    对于每一个经过网络栈的包,内核都需要遍历这条链表来查找匹配的五元组。当链表过长时,查找算法的时间复杂度从 O(1) 退化为 O(N)。更致命的是,修改 conntrack 记录需要加自旋锁(Spinlock)。高并发下,漫长的遍历加上激烈的锁竞争,会直接导致内核消耗大量 CPU 在软中断(ksoftirqd)上,系统 Load Average 飙升,网卡丢包率进一步恶化。

    合理的应急配置比例应当是 nf_conntrack_max = nf_conntrack_buckets * 4。 应急修复命令如下:

    # 1. 调整哈希桶大小 (必须先调桶,再调 Max)
    echo 262144 > /sys/module/nf_conntrack/parameters/hashsize
    
    # 2. 调大最大连接数和降低超时时间
    sysctl -w net.netfilter.nf_conntrack_max=1048576
    sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=300
    sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait=30
    

    但这仅仅是续命,并不能解决 CPU 开销问题。

    防御性降级:Raw 表的 NOTRACK 机制

    网关节点并非所有流量都需要进行 NAT。比如同网段的 Pod 到 Pod 通信,或者内部 Consul/Prometheus 的探测流量,完全可以跳过连接跟踪。

    在 iptables 时代,我们在 raw 表下发 NOTRACK 规则,将这部分信任流量提前短路:

    # 对于源和目的都在内网 10.0.0.0/8 网段的包,不进入 conntrack
    iptables -t raw -I PREROUTING -s 10.0.0.0/8 -d 10.0.0.0/8 -j CT --notrack
    iptables -t raw -I OUTPUT -s 10.0.0.0/8 -d 10.0.0.0/8 -j CT --notrack
    

    避坑指南:一旦给数据包打了 NOTRACK 标签,它在后续的 filter 表中就不属于 ESTABLISHED 或 RELATED 状态,而是 UNTRACKED。如果你的 INPUT 链默认策略是 DROP,且只有 -m state --state ESTABLISHED,RELATED -j ACCEPT,那么这些内网包会被统统丢弃。必须补上一条:

    iptables -I INPUT -m state --state UNTRACKED -j ACCEPT
    

    架构演进:全面向 nftables 与 Flowtable Fast-path 迁移

    iptables 最大的架构缺陷在于其规则的线性遍历机制(Linear evaluation)。哪怕你有一万条匹配规则,数据包也要从第一条匹配到最后一条。而 nftables 原生支持 Set 和 Map(类似字典),能在 O(1) 复杂度内完成匹配。

    更重要的是,从 Linux Kernel 4.16 开始,nftables 引入了革命性的 Flowtable 机制(软件 Fast-path),并在 Kernel 5.3 引入了硬件卸载(Hardware Offload)。

    在传统的 Netfilter 路径中,每一个包都要经历完整的 PREROUTING -> FORWARD -> POSTROUTING 链条。 启用 Flowtable 后,对于建立好的双向连接(ESTABLISHED),nftables 会直接在 Ingress 钩子处截获数据包,将其直接推送到对应的出口网卡(Egress),完全绕过 Netfilter 核心网络栈和复杂的路由查找。

    落地配置:构建基于 nftables 的加速网关

    首先,清空老旧的 iptables 规则,确保模块未被深度占用(此处需极度谨慎,确保断网可恢复)。编写 /etc/nftables.conf:

    #!/usr/sbin/nft -f
    
    flush ruleset
    
    table inet filter {
        # 1. 定义 flowtable,挂载在 ingress 钩子,监听内网 eth0 和外网 eth1
        flowtable fastpath {
            hook ingress priority 0;
            devices = { eth0, eth1 };
        }
    
        chain forward {
            type filter hook forward priority 0; policy accept;
    
            # 2. 只有处于 established 状态的连接,才卸载到 flowtable 加速
            ip protocol { tcp, udp } flow add @fastpath
    
            # 基础的连接跟踪允许
            ct state established,related accept
            ct state invalid drop
        }
    }
    
    table ip nat {
        chain prerouting {
            type nat hook prerouting priority 0; policy accept;
        }
    
        chain postrouting {
            type nat hook postrouting priority 100; policy accept;
    
            # 3. 对出公网的流量执行伪装 (SNAT)
            oifname "eth1" masquerade
        }
    }
    

    应用规则:

    nft -f /etc/nftables.conf
    

    性能验证: 应用 nftables Flowtable 后,再次在晚高峰压测。

    1. 查看 CPU si 开销,从之前的单核满载 100% 断崖式下降至 15% 左右。

    2. 观察 conntrack_count,虽然总条目仍然在 10 万级别,但大量的 established 包走的是 Fast-path,不再反复冲击 Netfilter 锁。

    3. P99 延迟从数百毫秒稳定回落至 5ms 以内,丢包彻底消失。

    常见问题 (FAQ)

    Q1:如何平滑从 iptables 迁移到 nftables,不导致生产环境长连接中断? A: 生产环境切忌直接 apt-get purge iptables。Linux 提供了兼容层 iptables-nft。首先替换底层后端,让 iptables 命令实际生成 nftables 规则。跑通后,使用 iptables-save > rules.txt,再通过 iptables-restore-translate -f rules.txt > rules.nft 转换为原生 nftables 语法。最后,在维护窗口 nft -f rules.nft 覆盖并原子化应用新规则集,连接跟踪状态在内核态是共享的,ESTABLISHED 状态的连接不会因此断开。

    Q2:启用 nftables Flowtable 后,为什么用 tcpdump 抓包,部分数据包神秘消失了? A: 这是正常现象。Flowtable 在 Ingress 阶段(网卡驱动收到包后极早期的阶段)就将包转发出去了,直接绕过了协议栈的大部分拦截点(包括 AF_PACKET)。这意味着处于 Fast-path 的数据包可能无法被 tcpdump 捕获到。如果你必须抓包排障,需要临时在 nft 规则中注释掉 flow add @fastpath,让流量回退到慢速路径(Slow-path)。

    Q3:masquerade 和直接写 snat to 性能上有差别吗? A: 有明显差别。masquerade 是专门针对动态 IP 设计的,每次处理 NAT 映射时,它都会去动态查询出口网卡当前的 IP 地址。如果你的网关是静态 IP,务必在 nftables/iptables 中使用 snat to 192.168.x.x 这样的明确指令。静态 SNAT 免去了查路由表获取本机 IP 的开销,在高并发场景下性能表现更好。

    Q4:为什么 UDP 流量也会引发 conntrack 爆满,UDP 不是无连接的吗? A: UDP 在协议栈确实无连接,但 Netfilter 是状态防火墙,它必须为 UDP 维持一个伪连接状态(包含五元组),否则无法进行返回包的 NAT 映射。DNS 解析(短 UDP 请求)是最常见的导火索。如果业务侧不复用 DNS 连接,海量的源端口会导致 conntrack 表被大量状态为 UNREPLIED (默认超时 30 秒) 的 UDP 条目塞满。解决方案是优化业务代码,或在内核调低 net.netfilter.nf_conntrack_udp_timeout_stream 和 timeout 参数。

  • 深入 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-legacy 与 nftables (或 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_tables 和 x_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。