标签: 防火墙

  • 深入 Netfilter 排查:海量 iptables 规则引发的 CPU SoftIRQ 暴涨与 nftables 匿名集合 O(1) 重构实战

    结论先行:将 5 万条 iptables 顺序拦截规则重构为原生 nftables 的匿名集合(Set/Map)后,宿主机网卡软中断(SoftIRQ)CPU 开销从 100% 暴降至 2%,网络 P99 延迟从 200ms 恢复至 2ms。在海量 IP 拦截与路由过滤场景下,彻底抛弃 iptables 的 O(N) 线性遍历,改用 nftables 基于红黑树与哈希表的 O(1) 匹配,才是现代网关防御的唯一出路。

    近期在排查某边缘网关集群性能问题时,发现部分节点在 PPS(每秒包数)仅 10 万左右时,Load Average 飙升至 40+,网络出现严重丢包。该节点主要承担流量清洗清洗和恶意 IP 封禁任务,安全组件会通过脚本动态下发拦截规则。

    案发现场:ksoftirqd 满载与 perf 剖析

    登录问题机器(Ubuntu 22.04, Kernel 5.15.0, iptables v1.8.7),通过 mpstat -P ALL 1 观察,发现多个 CPU 核心的 %soft 指标长期顶在 100%,而 %usr 和 %sys 极低。这说明 CPU 周期全部消耗在了内核态网络软中断处理上。

    直接挂载 perf 抓取热点函数:

    perf record -a -g -F 99 -- sleep 10
    perf report
    

    调用栈结果异常刺眼:

    -   78.42%     ksoftirqd/2  [kernel.kallsyms]   [k] nft_do_chain
       - 77.10% nft_do_chain
          - 68.32% nft_cmp_eval
          - 4.21% nft_counter_eval
    

    排查防火墙规则,发现安全组件通过 iptables -A INPUT -s -j DROP 累积下发了高达 5.2 万条黑名单规则。

    为什么 iptables-nft 包装层依然无法拯救 SoftIRQ 雪崩?

    很多人有个误区,认为在较新的 Linux 发行版中,iptables 已经默认切换到了 iptables-nft(即底层调用 nftables API),性能应该自动得到了优化。这是极其致命的错觉。

    iptables-nft 仅仅是一个语法翻译层。当你执行 5 万次 iptables -A INPUT -s IP -j DROP 时,翻译层会在 nftables 中原样生成 5 万条独立的规则。

    在内核 net/netfilter/nf_tables_core.c 的 nft_do_chain() 函数执行时,Netfilter 依然会像老版本一样,对数据包进行 O(N) 的线性遍历匹配。这意味着:

    1. 合法流量的深渊:正常用户的包如果不匹配任何黑名单,必须遍历完这 5.2 万条规则,直到命中链底部的默认 ACCEPT。

    2. SYN 洪水的放大器:虽然通常会在链表顶部配置 iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT,但对于新建连接(SYN)或恶意扫描包,Conntrack 无法命中,依然会陷入 5.2 万次 nft_cmp_eval 的循环对比中。

    单包处理时间的微秒级膨胀,在 10 万 PPS 的乘数效应下,瞬间抽干了 ksoftirqd 的处理能力,导致网卡 Ring Buffer 溢出,触发连环丢包。

    nftables 重构实战:利用 Set/Map 实现 O(1) 降维打击

    要解决这个问题,必须抛弃 iptables 的扁平化规则思维,改用 nftables(推荐版本 >= 1.0.2)原生的集合(Set)数据结构。内核会自动为集合选择合适的数据结构(针对离散 IP 默认使用 rhashtable,针对网段使用 pipapo)。

    1. 编写原生 nftables 配置

    清空旧的 iptables 规则,并加载优化的 nftables 配置:

    # file: /etc/nftables.conf
    flush ruleset
    
    table inet filter {
        # 预先定义一个巨大的哈希集合用于存放黑名单 IP
        set blacklist_ipv4 {
            type ipv4_addr
            flags interval
            auto-merge
            size 100000 # 预分配空间,避免频繁扩容引起的自旋锁竞争
        }
    
        chain input {
            type filter hook input priority filter; policy accept;
    
            # 1. 优先放行已建立的连接,跳过后续检查 (Conntrack 加速)
            ct state established,related accept
    
            # 2. 无效包直接丢弃
            ct state invalid drop
    
            # 3. O(1) 极速匹配黑名单,命中即丢弃
            ip saddr @blacklist_ipv4 drop
    
            # 4. 本地回环与 SSH 放行等常规规则...
            iifname "lo" accept
            tcp dport 22 accept
        }
    }
    

    2. 动态管理海量 IP 且不重载规则

    传统 iptables 增删规则时会对整个链表加锁(xtables_lock),并发极差。而在 nftables 中,向 set 添加元素是轻量级的原子操作,完全不会影响其他规则的执行:

    # 批量导入 5 万个黑名单 IP(执行耗时仅需几十毫秒)
    nft add element inet filter blacklist_ipv4 { 1.1.1.1, 2.2.2.0/24, 3.3.3.3 }
    
    # 删除特定 IP
    nft delete element inet filter blacklist_ipv4 { 1.1.1.1 }
    

    3. 性能验证

    重构上线后,再次压测 30 万 PPS 的新建连接风暴:

    • CPU %soft 消耗直接降至 2% ~ 3%。

    • perf top 中 nft_do_chain 的开销跌出前十。

    • 通过 nft list set inet filter blacklist_ipv4 查看,五万个 IP 被压缩在一次底层的哈希查找中,耗时几近于零。

    底层原理:Pipapo 与 rhashtable 的威力

    在 nftables 的源码实现中(net/netfilter/nft_set_hash.c),当你定义了一个不带 interval(网段)标志的独立 IP set 时,内核默认初始化 rhashtable(可动态扩容的哈希表)。查找复杂度为绝对的 O(1)。

    如果带有 interval 标志(例如混合了 CIDR 网段拦截),内核会使用 pipapo (PIle PAcket POlicies) 算法(net/netfilter/nft_set_pipapo.c)。这是一种针对多维范围匹配高度优化的算法,通过预构建位图(Bitmaps),将多级网段的匹配转换为内存连续的位运算操作,且对 CPU Cache 极其友好。这解释了为何无论黑名单是 5 千还是 5 万,单包处理时延的抖动曲线始终保持平滑。

    常见问题 (FAQ)

    Q1: 在向 nftables 的 Set 中大批量添加 IP 时,会阻塞现有的网络连接吗? 不会。nftables 底层通过 RCU (Read-Copy Update) 机制保障数据的并发读写。对于数据包匹配链路(读侧),RCU 提供了无锁访问能力;而在控制平面批量更新 Set 元素时(写侧),只会发生极短的锁竞争。这与传统 iptables 重载整个规则链引发的剧烈抖动有本质区别。

    Q2: 既然设置了 ct state established accept,为什么之前用 iptables 时,正常流量还是会感觉到高延迟? 因为网络环境并非绝对纯净。即使正常长连接被 Conntrack 放行,但在公网环境中,机器每秒都会遭受大量的垃圾扫描包、伪造包或恶意 SYN 包。这些无状态包无法命中 established,必定会落入后续的 5 万条 iptables 规则中进行线性遍历,进而抢占大量 CPU 资源,导致正常连接的软中断处理被系统调度延后,最终体现为 P99 延迟飙升。

    Q3: 如何将现有的业务脚本从 iptables 平滑迁移到 nftables? 千万不要依赖 iptables-nft 来容忍旧脚本。正确的做法是:

    1. 重写网关的基线规则,用 nft -f 加载带有 Set 的配置结构。

    2. 将现有的封禁/解封脚本,从 iptables -I INPUT -s $IP -j DROP 替换为 nft add element inet filter blacklist_ipv4 { $IP }。

    3. 如果依赖 fail2ban 等第三方工具,需将 action 配置修改为 nftables-multiport 或直接修改其默认的 backend 为 nftables。