Skip to content

Latest commit

 

History

History
2842 lines (2229 loc) · 109 KB

File metadata and controls

2842 lines (2229 loc) · 109 KB

Linux 内核 nftables 框架深度分析

基于 Linux kernel master 分支源码分析 主要参考文件:

  • net/netfilter/nf_tables_core.c
  • include/net/netfilter/nf_tables.h
  • net/netfilter/nf_tables_api.c
  • net/netfilter/nft_payload.c
  • net/netfilter/nft_set_hash.c
  • net/netfilter/nft_set_rbtree.c
  • net/netfilter/nft_set_bitmap.c
  • net/netfilter/nft_set_pipapo.c
  • net/netfilter/nft_ct.c
  • net/netfilter/nft_compat.c
  • net/netfilter/nft_chain_filter.c
  • net/netfilter/nf_flow_table_core.c
  • net/netfilter/nf_flow_table_ip.c
  • include/uapi/linux/netfilter/nf_tables.h
  • include/net/netfilter/nf_flow_table.h

目录

  1. nftables 概述与历史背景
  2. nftables 与 iptables 的对比
  3. 整体架构图
  4. 核心数据结构
  5. 表达式(expr)系统详解
  6. nft 虚拟机执行引擎
  7. 集合(set)子系统
  8. Netlink 事务接口
  9. Hooks 注册与 Netfilter 集成
  10. 连接跟踪(ct)表达式深入分析
  11. flowtable 软件加速
  12. 安全性与性能设计
  13. nft_compat:iptables 兼容层深度分析
  14. nftables 集合动态更新与超时机制
  15. nftables netdev 表与 ingress/egress hook
  16. pipapo 算法深度解析
  17. bitmap 集合实现
  18. nftables 与 conntrack 深度集成
  19. flowtable 快速路径完整流程
  20. nftables 事务机制完整分析
  21. nftables 调试与追踪
  22. nftables 网络命名空间支持
  23. nftables 有状态对象系统
  24. nftables 内核模块初始化流程
  25. 总结

1. nftables 概述与历史背景

nftables 是 Linux 内核从 3.13(2014年)起引入的下一代数据包过滤框架,用于替代 iptables/ip6tables/arptables/ebtables 分散的多个框架。它由 Patrick McHardy 和 Pablo Neira Ayuso 主导开发,最初由德国 Astaro AG 资助。

nftables 的核心理念:

  • 统一框架:一套代码同时支持 IPv4、IPv6、ARP、以太网桥、NETDEV 等协议族
  • 虚拟机模型:规则被编译为字节码,在专用的 nft 虚拟机上执行,而非硬编码的 match/target 模块
  • 原子事务:所有配置变更通过 Netlink 事务原子提交,不存在配置中间状态
  • 内置集合:原生支持哈希表、红黑树、区间树等数据结构,实现高效的多值匹配

从开发历史看,nftables 解决了 iptables 长达二十年积累的技术债务:每个协议族各自维护独立的 Hook 和 match/target 注册机制,代码高度重复;nftables 通过表达式(expression)系统和虚拟机消除了这种重复。


2. nftables 与 iptables 的对比

2.1 架构对比

iptables 架构(旧)                   nftables 架构(新)
+---------------------------+          +-------------------------------+
| ip_tables (IPv4)          |          |                               |
| ip6_tables (IPv6)         |          |   nf_tables (统一框架)         |
| arp_tables (ARP)          |          |                               |
| eb_tables (bridge)        |          |   表达式虚拟机执行引擎          |
|                           |          |                               |
| 各自的 match/target 模块  |          |   pluggable expr 模块          |
+---------------------------+          +-------------------------------+
        |                                         |
+---------------+                      +---------------------+
| 内核固化的    |                      | 通用 Netlink 事务   |
| iptables 命令 |                      | 接口(nfnetlink)   |
+---------------+                      +---------------------+

2.2 规则表示方式对比

iptables 规则(内核内部表示为固定 ipt_entry 结构):

  • 规则 = ipt_entry(固定头)+ ipt_entry_match[] + ipt_entry_target
  • 每个 match/target 是独立编译的内核模块,接口不统一

nftables 规则(内核内部表示为表达式字节码):

  • 规则 = nft_rule(头部)+ 连续的 nft_expr[] 数组
  • 每个表达式实现统一的 nft_expr_ops 接口(eval、init、dump 等)
  • 执行时按顺序调用每个表达式的 eval() 回调,操作共享寄存器组

2.3 关键差异对比表

特性 iptables nftables
协议族支持 各协议族独立实现 统一框架,family 参数区分
规则格式 固定结构体 虚拟机字节码(表达式链)
多值匹配 需要多条规则或 ipset 内置 set 子系统
配置原子性 非原子(iptables-restore 近似) 完全原子(Netlink 事务)
内核 API setsockopt(IP_SET_REPLACE) nfnetlink(Netlink 子系统)
计数器统计 每条规则固定计数 可选的 counter 表达式,或有状态对象
动态规则更新 重加载整个表 单条规则增删,原子提交
并发安全 整表锁 RCU + 双代(generation)机制

3. 整体架构图

用户空间                            内核空间
==========                          ==========================================

nft 命令行工具                       NFNETLINK 子系统
(libnftnl + libmnl)                  (net/netfilter/nf_tables_api.c)
     |                                    |
     | Netlink socket                     |
     |  (NFNL_SUBSYS_NFTABLES)            v
     +------------------------------> nf_tables_subsys
                                          |
                              +------------------------+
                              |  事务处理层             |
                              |  nft_net->commit_list  |
                              |  nf_tables_commit()    |
                              |  nf_tables_abort()     |
                              +------------------------+
                                          |
                              +------------------------+
                              |  对象管理层             |
                              |  nft_table             |
                              |   +-- nft_chain        |
                              |       +-- nft_rule     |
                              |       |   +-- nft_expr |
                              |       +-- nft_set      |
                              +------------------------+
                                          |
                                          | 提交后激活
                                          v
                              +------------------------+
                              |  数据平面(包路径)      |
                              |                        |
 网卡驱动                     |  nf_hook_ops 注册       |
 skb 入站/出站 ──────────────>|  nft_do_chain()        |
                              |  nft 虚拟机执行         |
                              |  (nft_regs 寄存器组)   |
                              |                        |
                              |  nf_flowtable          |
                              |  (bypass 快速路径)      |
                              +------------------------+
                                          |
                              +------------------------+
                              |  集合子系统             |
                              |  nft_rhash (jhash)     |
                              |  nft_rbtree (rb_root)  |
                              |  nft_bitmap (bitarray) |
                              |  nft_pipapo (位并联)   |
                              +------------------------+

4. 核心数据结构

4.1 nft_table — 表

定义在 include/net/netfilter/nf_tables.h,第 1313 行:

struct nft_table {
    struct list_head    list;        // 链入 nft_net->tables 全局链表
    struct rhltable     chains_ht;   // 链名哈希表,O(1) 按名查链
    struct list_head    chains;      // 链的有序列表(便于遍历)
    struct list_head    sets;        // 本表内的所有集合
    struct list_head    objects;     // 有状态对象(counter/quota/limit)
    struct list_head    flowtables;  // flowtable 列表
    u64                 hgenerator; // 句柄生成器(自增,分配 handle)
    u64                 handle;     // 本表的唯一句柄
    u32                 use;        // 被引用次数
    u16                 family:6,   // 地址族(NFPROTO_IPV4/IPV6/NETDEV 等)
                        flags:8,    // NFT_TABLE_F_DORMANT 等标志
                        genmask:2;  // 生成掩码(用于原子切换)
    u32                 nlpid;      // 绑定此表的 netlink 端口 ID
    char                *name;      // 表名(如 "filter")
    u16                 udlen;      // 用户自定义数据长度
    u8                  *udata;     // 用户自定义数据
    u8                  validate_state; // 验证状态
};

关键设计点

  • chains_ht 使用内核 rhashtable(rhashtable_params nft_chain_ht_params,定义在 nf_tables_api.c 第 58-65 行),以链名为键实现 O(1) 查找
  • genmask 与全局 gencursor 配合实现无锁双代原子切换(详见第 8.4 节)
  • family 字段使得单一数据结构支持所有协议族,消除了 iptables 时代各族重复实现的问题
nft_table 内部结构示意图:

  nftables_pernet
  +------------------+
  | tables (list_head)|
  +------------------+
         |
         v
  +------------------+       chains_ht (rhashtable)
  |   nft_table       |-----> +---+---+---+---+  O(1) 按名查链
  |   family: IPV4    |       | c1| c2| c3|...|
  |   name: "filter"  |       +---+---+---+---+
  |   flags: 0        |
  |   genmask: 0/1    |       chains (list_head,有序遍历)
  |   sets: list_head |-----> chain1 -> chain2 -> chain3
  |   objects: list   |
  +------------------+       sets (list_head)
                      \-----> set1 -> set2 -> ...

4.2 nft_chain / nft_base_chain — 链

nft_chain 定义在 include/net/netfilter/nf_tables.h,第 1144 行:

struct nft_chain {
    struct nft_rule_blob  __rcu *blob_gen_0;  // 当前代(generation 0)规则 blob
    struct nft_rule_blob  __rcu *blob_gen_1;  // 下一代(generation 1)规则 blob
    struct list_head       rules;      // 控制平面规则列表
    struct list_head       list;       // 链入表的 chains 链表
    struct rhlist_head     rhlhead;    // 用于 chains_ht 哈希
    struct nft_table      *table;      // 所属表
    u64                    handle;     // 唯一句柄
    u32                    use;        // JUMP 引用计数
    u8                     flags:5,
                           bound:1,    // 是否已被绑定(binding chain)
                           genmask:2;
    char                  *name;       // 链名
    u16                    udlen;
    u8                    *udata;
    struct nft_rule_blob  *blob_next;       // 提交阶段临时指针
    struct nft_chain_validate_state vstate; // 循环检测验证状态
};

双 blob 设计blob_gen_0blob_gen_1 是链的核心创新。规则在提交前被编译为紧凑的 nft_rule_blob(连续内存的 nft_rule_dp 数组),包路径直接遍历 blob,避免了链表遍历的指针间接访问。

对于挂载到 Netfilter Hook 的"基础链",内核使用 nft_base_chain 包装(nf_tables.h 第 1246 行):

struct nft_base_chain {
    struct nf_hook_ops      ops;           // Netfilter hook 操作结构
    struct list_head        hook_list;     // NETDEV 族多设备支持
    const struct nft_chain_type *type;     // filter/route/nat
    u8                      policy;        // 默认策略(NF_ACCEPT/NF_DROP)
    u8                      flags;
    struct nft_stats __percpu *stats;      // 每 CPU 计数
    struct nft_chain        chain;         // 内嵌的通用链
    struct flow_block       flow_block;    // flowtable 硬件卸载用
};

nft_base_chain 通过 container_of(chain, struct nft_base_chain, chain)nft_chain 反向获取(第 1257 行的 nft_base_chain() 辅助函数)。

4.3 nft_rule / nft_rule_dp — 规则

控制平面规则 nft_rulenf_tables.h 第 1004 行):

struct nft_rule {
    struct list_head  list;
    u64               handle:42,   // 规则句柄(42 位,最大约 4 万亿条规则)
                      genmask:2,   // 双代可见性掩码
                      dlen:12,     // 表达式数据长度(最大 4095 字节)
                      udata:1;     // 是否附带用户数据
    unsigned char     data[]       // 紧随其后的表达式字节码
        __attribute__((aligned(__alignof__(struct nft_expr))));
};

数据平面规则 nft_rule_dpnf_tables.h 第 1075 行):

struct nft_rule_dp {
    u64           is_last:1,   // blob 终止标志
                  dlen:12,     // 表达式数据长度
                  handle:42;   // 用于 trace
    unsigned char data[]
        __attribute__((aligned(__alignof__(struct nft_expr))));
};

nft_rule_dp 是提交阶段将 nft_rule 链表编译为连续内存块(nft_rule_blob)的产物。连续内存布局提升了 CPU 缓存命中率,is_last 标志避免了边界检查中的指针运算。

遍历 blob 中所有规则(nf_tables.h 第 1090 行):

static inline const struct nft_rule_dp *nft_rule_next(const struct nft_rule_dp *rule)
{
    return (void *)rule + sizeof(*rule) + rule->dlen;
}
nft_rule_blob 内存布局:

  +----------------------------+
  | nft_rule_blob              |
  |  size: N                   |
  +----------------------------+
  | nft_rule_dp[0]             |  is_last=0, dlen=X
  |   [nft_expr ops1|data1]    |
  |   [nft_expr ops2|data2]    |
  |   ...                      |
  +----------------------------+
  | nft_rule_dp[1]             |  is_last=0, dlen=Y
  |   [nft_expr ...]           |
  +----------------------------+
  | nft_rule_dp[N-1]           |  is_last=1(终止哨兵)
  +----------------------------+

  所有规则在连续内存中,CPU 预取友好,无链表指针跳转

4.4 nft_expr — 表达式

表达式是 nftables 虚拟机的基本执行单元,定义在 nf_tables.h 第 413 行:

struct nft_expr {
    const struct nft_expr_ops  *ops;   // 操作函数表指针
    unsigned char               data[] // 表达式私有数据(紧随 ops 指针后)
        __attribute__((aligned(__alignof__(u64))));
};

这是一个极为紧凑的设计:ops 指针(8 字节)后紧跟私有数据,整体大小由 ops->size 决定。访问私有数据的内联函数(第 419 行):

static inline void *nft_expr_priv(const struct nft_expr *expr)
{
    return (void *)expr->data;
}

规则内表达式的遍历(nf_tables.h 第 1068 行),通过 ops->size 跳到下一个表达式,实现变长表达式的紧凑排列:

#define nft_rule_for_each_expr(expr, last, rule) \
    for ((expr) = nft_expr_first(rule), (last) = nft_expr_last(rule); \
         (expr) != (last); \
         (expr) = nft_expr_next(expr))

4.5 nft_regs — 寄存器组

nftables 虚拟机的"CPU 寄存器",定义在 nf_tables.h 第 118 行:

struct nft_regs {
    union {
        u32              data[NFT_REG32_NUM];  // 20 个 32 位数据寄存器
        struct nft_verdict verdict;            // 裁决寄存器(与 data[0..3] 共用)
    };
};

对应 UAPI 枚举(include/uapi/linux/netfilter/nf_tables.h 第 22 行):

enum nft_registers {
    NFT_REG_VERDICT,     // 裁决寄存器(16 字节,与 data[0..3] 重叠)
    NFT_REG_1,           // 16 字节寄存器 1(= data[0..3])
    NFT_REG_2,           // 16 字节寄存器 2(= data[4..7])
    NFT_REG_3,
    NFT_REG_4,
    __NFT_REG_MAX,

    NFT_REG32_00 = 8,    // 4 字节寄存器,编号 8–23
    // ...
    NFT_REG32_15,
};

#define NFT_REG_SIZE    16   // 老式 16 字节寄存器
#define NFT_REG32_SIZE  4    // 新式 4 字节寄存器
#define NFT_REG32_COUNT (NFT_REG32_15 - NFT_REG32_00 + 1)  // = 16
nft_regs 寄存器组布局(总 80 字节):

  offset   字节    名称
  ------   ----   ------
   0        16    NFT_REG_VERDICT / NFT_REG_1(verdict.code + verdict.chain)
  16        16    NFT_REG_2
  32        16    NFT_REG_3
  48        16    NFT_REG_4
  ------    以下为 NFT_REG32_xx 小寄存器(与上方重叠,4 字节一格)
   0         4    NFT_REG32_00
   4         4    NFT_REG32_01
   8         4    NFT_REG32_02
  ...
  76         4    NFT_REG32_19

寄存器工作流程:

  • load 阶段payload/meta/ct 等表达式将数据包字段写入数据寄存器
  • compare 阶段cmp/lookup 等表达式读取寄存器,与期望值比较
  • action 阶段immediate/nat 等表达式根据寄存器值设置裁决或修改报文
  • 裁决结果写入 regs.verdict.code,驱动执行引擎的下一步动作

4.6 nft_pktinfo — 数据包信息

nft_pktinfo 是包路径中传递上下文的轻量载体(nf_tables.h 第 29 行):

struct nft_pktinfo {
    struct sk_buff          *skb;      // 数据包缓冲区
    const struct nf_hook_state *state; // hook 状态(net/in/out/hook/pf 等)
    u8                       flags;    // NFT_PKTINFO_L4PROTO | _INNER | _INNER_FULL
    u8                       tprot;    // 传输层协议号(IPPROTO_TCP 等)
    u16                      fragoff;  // IP 分片偏移
    u16                      thoff;    // 传输层头偏移
    u16                      inneroff; // 内层数据包头偏移(隧道解封装)
};

state 字段指向 nf_hook_state,从中可获取 net(网络命名空间)、in/out(接口)、hook(Hook 点编号)、pf(协议族)等信息,通过系列内联辅助函数访问(nft_net()nft_hook()nft_in() 等,均在 nf_tables.h 第 49-70 行)。

4.7 nftables_pernet — 网络命名空间私有数据

nftables 在每个网络命名空间维护独立状态,通过 nftables_pernet 结构管理(nf_tables.h 第 1445 行):

struct nftables_pernet {
    struct list_head    tables;         // 该命名空间所有表
    struct list_head    commit_list;    // 待提交事务列表
    struct list_head    binding_list;   // 绑定链/集合待验证列表
    struct list_head    destroy_list;   // 待销毁对象列表(RCU 后)
    struct list_head    gc_list;        // GC 待处理列表
    struct mutex        commit_mutex;   // 提交互斥锁
    u64                 table_handle;   // 下一个表的句柄值
    unsigned int        base_seq;       // 当前代序列号
    u8                  validate_state; // 全局验证状态
    u8                  gencursor;      // 当前代游标(0 或 1)
};

nft_pernet() 辅助函数(nf_tables.h 第 1461 行)通过 net_generic(net, nf_tables_net_id) 获取:

static inline struct nftables_pernet *nft_pernet(const struct net *net)
{
    return net_generic(net, nf_tables_net_id);
}

5. 表达式(expr)系统详解

5.1 nft_expr_ops 操作表

nft_expr_ops 是每种表达式必须实现的操作接口,定义在 nf_tables.h 第 955 行:

struct nft_expr_ops {
    // 数据平面(热路径)
    void  (*eval)(const struct nft_expr *expr,
                  struct nft_regs *regs,
                  const struct nft_pktinfo *pkt);   // 执行表达式

    // 控制平面
    int   (*clone)(struct nft_expr *dst,
                   const struct nft_expr *src, gfp_t gfp);
    unsigned int  size;                             // 整个 nft_expr 大小

    int   (*init)(const struct nft_ctx *ctx,
                  const struct nft_expr *expr,
                  const struct nlattr * const tb[]);   // 初始化
    void  (*activate)(const struct nft_ctx *ctx,
                      const struct nft_expr *expr);   // 提交时激活
    void  (*deactivate)(const struct nft_ctx *ctx,
                        const struct nft_expr *expr,
                        enum nft_trans_phase phase);  // 事务撤销时停用
    void  (*destroy)(const struct nft_ctx *ctx,
                     const struct nft_expr *expr);    // 销毁私有资源
    int   (*dump)(struct sk_buff *skb,
                  const struct nft_expr *expr, bool reset); // 序列化到 Netlink
    int   (*validate)(const struct nft_ctx *ctx,
                      const struct nft_expr *expr);   // 验证(循环检测)
    bool  (*reduce)(struct nft_regs_track *track,
                    const struct nft_expr *expr);     // 冗余消除优化
    bool  (*gc)(struct net *net,
                const struct nft_expr *expr);         // 集合 GC 时调用
    int   (*offload)(...);     // 硬件卸载
    bool  (*offload_action)(...);
    void  (*offload_stats)(...);

    const struct nft_expr_type  *type;  // 所属类型
    void                        *data;  // 类型私有扩展数据
};

其中 eval 是数据平面热路径,必须尽可能轻量。size 字段决定该表达式在规则字节码中占用的总字节数(包括 ops 指针和私有数据)。

5.2 nft_expr_type 类型注册

每种表达式(如 payload、meta、ct)对应一个 nft_expr_typenf_tables.h 第 904 行),通过 nft_register_expr() 注册到全局链表 nf_tables_expressions。基础类型在 nf_tables_core_module_init() 中批量注册(nf_tables_core.c 第 350-366 行):

static struct nft_expr_type *nft_basic_types[] = {
    &nft_imm_type,        // immediate(立即数)
    &nft_cmp_type,        // compare(比较)
    &nft_lookup_type,     // lookup(集合查找)
    &nft_bitwise_type,    // bitwise(位运算)
    &nft_byteorder_type,  // byteorder(字节序转换)
    &nft_payload_type,    // payload(报文字段提取/修改)
    &nft_dynset_type,     // dynset(动态集合操作)
    &nft_range_type,      // range(范围匹配)
    &nft_meta_type,       // meta(报文元数据)
    &nft_rt_type,         // rt(路由信息)
    &nft_exthdr_type,     // exthdr(IPv6 扩展头)
    &nft_last_type,       // last(记录最近匹配时间)
    &nft_counter_type,    // counter(计数器对象引用)
    &nft_objref_type,     // objref(有状态对象引用)
    &nft_inner_type,      // inner(隧道内层报文处理)
};

5.3 payload 表达式

实现在 net/netfilter/nft_payload.c,是使用最频繁的表达式之一。

私有数据结构(从 init 和执行函数反推):

struct nft_payload {
    enum nft_payload_bases  base;   // LL/NETWORK/TRANSPORT/INNER
    u8                      dreg;   // 目标寄存器
    u8                      offset; // 相对于所选 base 的字节偏移
    u8                      len;    // 提取长度(字节)
};

nft_payload_eval() 核心逻辑(nft_payload.c 第 159-209 行):

void nft_payload_eval(const struct nft_expr *expr,
                      struct nft_regs *regs,
                      const struct nft_pktinfo *pkt)
{
    const struct nft_payload *priv = nft_expr_priv(expr);
    u32 *dest = &regs->data[priv->dreg];

    switch (priv->base) {
    case NFT_PAYLOAD_LL_HEADER:        // 链路层(以太网)头
        offset = skb_mac_header(skb) - skb->data;
        break;
    case NFT_PAYLOAD_NETWORK_HEADER:   // 网络层(IP)头
        offset = skb_network_offset(skb);
        break;
    case NFT_PAYLOAD_TRANSPORT_HEADER: // 传输层(TCP/UDP)头
        offset = nft_thoff(pkt);
        break;
    case NFT_PAYLOAD_INNER_HEADER:     // 隧道内层(GRE/UDP/TCP 封装)
        offset = nft_payload_inner_offset(pkt);
        break;
    }
    skb_copy_bits(skb, offset + priv->offset, dest, priv->len);
}

nft_payload_fast_eval() 是其内联优化版本(nf_tables_core.c 第 144-174 行),仅处理 NETWORK 和 TRANSPORT 层、长度为 1/2/4 字节的常见情况,直接指针访问(而非 skb_copy_bits),避免了越界检查以外的所有开销:

// nf_tables_core.c:144
static bool nft_payload_fast_eval(const struct nft_expr *expr,
                                  struct nft_regs *regs,
                                  const struct nft_pktinfo *pkt)
{
    const struct nft_payload *priv = nft_expr_priv(expr);
    const struct sk_buff *skb = pkt->skb;
    u32 *dest = &regs->data[priv->dreg];
    unsigned char *ptr;

    if (priv->base == NFT_PAYLOAD_NETWORK_HEADER)
        ptr = skb_network_header(skb);
    else {
        if (!(pkt->flags & NFT_PKTINFO_L4PROTO))
            return false;
        ptr = skb->data + nft_thoff(pkt);
    }
    ptr += priv->offset;

    if (unlikely(ptr + priv->len > skb_tail_pointer(skb)))
        return false;  // 回退到通用路径

    *dest = 0;
    if (priv->len == 2)
        *(u16 *)dest = *(u16 *)ptr;
    else if (priv->len == 4)
        *(u32 *)dest = *(u32 *)ptr;
    else
        *(u8 *)dest = *(u8 *)ptr;
    return true;
}

支持 VLAN offload 透明处理(第 42-72 行的 nft_payload_copy_vlan()),以及内层 GRE/IPIP/UDP 隧道解析(第 74-137 行的 __nft_payload_inner_offset())。

payload 还支持写操作(修改报文)和 checksum 修正,通过 NFTA_PAYLOAD_SREG 属性区分读/写语义。

5.4 meta 表达式

meta 表达式(net/netfilter/nft_meta.c)用于匹配报文元信息:

键值(nft_meta_keys) 含义
NFT_META_IIF/OIF 输入/输出接口索引
NFT_META_IIFNAME/OIFNAME 接口名称
NFT_META_PROTOCOL 以太网类型(EtherType)
NFT_META_MARK skb->mark(报文标记)
NFT_META_PKTTYPE 广播/多播/单播
NFT_META_CPU 处理包的 CPU 编号
NFT_META_PRIORITY TC 优先级
NFT_META_SECMARK LSM/SELinux 安全标记
NFT_META_L4PROTO 传输层协议号(不含分片影响)
NFT_META_TIME_NS/DAY/HOUR 时间戳(基于 ktime)

meta 只是从 pkt->skbpkt->state 中读取字段,写入目标寄存器,开销极低。

5.5 ct 连接跟踪表达式

详见第 10 节和第 18 节。

5.6 counter / limit 计数限速

counter 是 nftables 中的有状态对象(nft_object),通过 nft_objref_type 表达式引用。主要字段:

struct nft_counter {
    s64  bytes;    // 字节计数(per-CPU 累加)
    s64  packets;  // 报文计数
};

counter_eval() 使用 u64_stats_update_begin/end 保证 32 位系统上的 64 位计数一致性。

limit 表达式实现令牌桶(token bucket)算法,支持按包/字节两种速率限制,私有数据包含 rate(速率)、burst(突发)、last(上次检查时间)等字段,并用 spinlock 保护更新。

5.7 nat 地址转换

NAT 表达式(net/netfilter/nft_nat.c)工作在 NFT_CHAIN_T_NAT 类型的链上,通过调用 nf_nat_setup_info() 修改连接跟踪的 NAT 映射,并根据寄存器中的地址/端口值完成动态 SNAT/DNAT。

与 iptables 的 MASQUERADE/DNAT target 不同,nftables 的 nat 表达式通过寄存器传递目标地址/端口,允许使用 map 集合实现地址映射表,写法更加灵活。

5.8 cmp 比较表达式及其快速变体

cmp 表达式(net/netfilter/nft_cmp.c)是执行寄存器与常量比较的基础表达式。内核实现了三种变体以适应不同场景:

通用 cmpnft_cmp_eval):支持任意长度的 memcmp 比较,支持 ==!= 操作符,适合 IPv6 地址(16 字节)等大型键的比较。

fast_ops cmpnft_cmp_fast_ops):用于最常见的 32 位单寄存器比较(nf_tables_core.c 第 84-92 行):

// nf_tables_core.c:84
static void nft_cmp_fast_eval(const struct nft_expr *expr,
                               struct nft_regs *regs)
{
    const struct nft_cmp_fast_expr *priv = nft_expr_priv(expr);

    // mask 用于处理不足 32 位的字段(如端口 16 位)
    if (((regs->data[priv->sreg] & priv->mask) == priv->data) ^ priv->inv)
        return;
    regs->verdict.code = NFT_BREAK;  // 不匹配 -> 跳过本规则
}

cmp16 fast_opsnft_cmp16_fast_ops):使用 u64 双字宽比较 128 位数据(IPv6 地址),一次比较完成(nf_tables_core.c 第 94-106 行):

static void nft_cmp16_fast_eval(const struct nft_expr *expr,
                                 struct nft_regs *regs)
{
    const struct nft_cmp16_fast_expr *priv = nft_expr_priv(expr);
    const u64 *reg_data = (const u64 *)&regs->data[priv->sreg];
    const u64 *mask = (const u64 *)&priv->mask;
    const u64 *data = (const u64 *)&priv->data;

    if (((reg_data[0] & mask[0]) == data[0] &&
        ((reg_data[1] & mask[1]) == data[1])) ^ priv->inv)
        return;
    regs->verdict.code = NFT_BREAK;
}

nft_cmp.c 中的 nft_cmp_select_ops() 在初始化时根据比较长度和对齐要求自动选择最优变体。


6. nft 虚拟机执行引擎

6.1 nft_do_chain 主循环

nft_do_chain() 是 nftables 数据平面的核心函数,定义在 net/netfilter/nf_tables_core.c 第 250 行,是被 nf_hook_ops.hook 回调直接调用的入口:

unsigned int nft_do_chain(struct nft_pktinfo *pkt, void *priv)
{
    const struct nft_chain *chain = priv, *basechain = chain;
    const struct nft_expr *expr, *last;
    const struct nft_rule_dp *rule;
    struct nft_regs regs;
    unsigned int stackptr = 0;
    struct nft_jumpstack jumpstack[NFT_JUMP_STACK_SIZE];  // 最大 16 层
    bool genbit = READ_ONCE(net->nft.gencursor);  // 读取当前代标志(RCU 语义)
    struct nft_rule_blob *blob;
    ...

    // 1. 根据当前代标志选择规则 blob
do_chain:
    blob = genbit ? rcu_dereference(chain->blob_gen_1)
                  : rcu_dereference(chain->blob_gen_0);

    rule = (struct nft_rule_dp *)blob->data;

next_rule:
    regs.verdict.code = NFT_CONTINUE;

    // 2. 遍历 blob 中所有规则(is_last 标志终止循环)
    for (; !rule->is_last; rule = nft_rule_next(rule)) {
        // 3. 遍历规则内所有表达式
        nft_rule_dp_for_each_expr(expr, last, rule) {
            // 快速路径:直接函数调用(避免间接跳转)
            if (expr->ops == &nft_cmp_fast_ops)
                nft_cmp_fast_eval(expr, &regs);
            else if (expr->ops == &nft_cmp16_fast_ops)
                nft_cmp16_fast_eval(expr, &regs);
            else if (expr->ops == &nft_bitwise_fast_ops)
                nft_bitwise_fast_eval(expr, &regs);
            else if (expr->ops != &nft_payload_fast_ops ||
                     !nft_payload_fast_eval(expr, &regs, pkt))
                expr_call_ops_eval(expr, &regs, pkt);  // 通用路径

            if (regs.verdict.code != NFT_CONTINUE)
                break;  // 表达式设置了裁决,退出表达式循环
        }

        // 4. 处理规则级裁决
        switch (regs.verdict.code) {
        case NFT_BREAK:
            regs.verdict.code = NFT_CONTINUE;  // 当前规则不匹配,继续下一条
            continue;
        case NFT_CONTINUE:
            continue;  // 规则匹配但未设置终止裁决,继续
        }
        break;  // 其他裁决:跳出规则循环
    }

    // 5. 处理链级裁决
    switch (regs.verdict.code & NF_VERDICT_MASK) {
    case NF_ACCEPT:
    case NF_QUEUE:
    case NF_STOLEN:
        return regs.verdict.code;
    case NF_DROP:
        return NF_DROP_REASON(pkt->skb, SKB_DROP_REASON_NETFILTER_DROP, EPERM);
    }

    // 6. 处理链跳转
    switch (regs.verdict.code) {
    case NFT_JUMP:
        jumpstack[stackptr++].rule = nft_rule_next(rule); // 保存返回点
        fallthrough;
    case NFT_GOTO:
        chain = regs.verdict.chain;
        goto do_chain;
    case NFT_RETURN:
        break;
    }

    // 7. 从跳转栈返回
    if (stackptr > 0) {
        rule = jumpstack[--stackptr].rule;
        goto next_rule;
    }

    // 8. 链遍历完毕,更新链级统计并应用默认策略
    if (static_branch_unlikely(&nft_counters_enabled))
        nft_update_chain_stats(basechain, pkt);

    if (nft_base_chain(basechain)->policy == NF_DROP)
        return NF_DROP_REASON(pkt->skb, SKB_DROP_REASON_NETFILTER_DROP, EPERM);

    return nft_base_chain(basechain)->policy;
}

执行流程示意图:

nft_do_chain()
     |
     v
选择代(genbit=0/1)-> blob_gen_0 或 blob_gen_1
     |
     v
blob->data(连续的 nft_rule_dp 数组)
     |
     +---> rule[0]
     |       |
     |       +---> expr[0].ops->eval()  payload: 读取 IP 源地址到 reg[1]
     |       +---> expr[1].ops->eval()  cmp: reg[1] == 192.168.1.1?
     |       +---> expr[2].ops->eval()  counter: 计数
     |       +---> expr[3].ops->eval()  immediate: verdict = NF_ACCEPT
     |       |
     |       v
     |    verdict == NF_ACCEPT -> return NF_ACCEPT
     |
     +---> rule[1] ... (is_last=1 时停止)
     |
     v
默认策略(NF_ACCEPT 或 NF_DROP)

6.2 裁决码(verdict)语义

裁决码定义在 include/uapi/linux/netfilter/nf_tables.h 第 64 行:

enum nft_verdicts {
    NFT_CONTINUE = -1,  // 继续执行下一个表达式(未做决定)
    NFT_BREAK    = -2,  // 当前规则不匹配,跳转到下一规则
    NFT_JUMP     = -3,  // 跳转到另一条链(保存返回地址)
    NFT_GOTO     = -4,  // 跳转到另一条链(不保存返回地址)
    NFT_RETURN   = -5,  // 从 JUMP 返回调用链
};

这些值与 Netfilter 自身裁决码(NF_ACCEPT=1NF_DROP=0 等)不重叠,nft_do_chain 通过 NF_VERDICT_MASK0x000000ff)区分二者。

NFT_BREAK vs NFT_CONTINUE 的区别NFT_BREAK 发生在某表达式(如 cmp)比较不匹配时,意味着"本条规则整体不匹配",nft_do_chain 重置为 NFT_CONTINUE 并进入下一规则。这比 iptables 的"前缀匹配"语义更清晰。

裁决码流转图:

  NFT_CONTINUE (-1)
      |
      | [表达式链正常执行完毕,规则未设置终止裁决]
      v
  继续下一条规则

  NFT_BREAK (-2)
      |
      | [某 cmp 不匹配,规则整体不匹配]
      v
  重置为 NFT_CONTINUE,继续下一条规则

  NF_ACCEPT (1)
      |
      | [immediate/nat 表达式设置 NF_ACCEPT]
      v
  return NF_ACCEPT -> 包被接受,继续后续内核处理

  NF_DROP (0)
      |
      v
  return NF_DROP_REASON -> 包被丢弃,记录丢包原因

  NFT_JUMP (-3)
      |
      | [跳转到子链,压栈保存返回地址]
      v
  执行子链 -> NFT_RETURN -> 弹栈恢复 -> 继续原链下一规则

  NFT_GOTO (-4)
      |
      | [跳转到子链,不压栈]
      v
  执行子链 -> 子链策略 -> 直接返回(不回到原链)

6.3 快速路径优化

nf_tables_core.c 的执行引擎包含两层快速路径优化:

1. 直接函数调用(避免 Retpoline 间接跳转开销)

expr_call_ops_eval()(第 203-238 行)中,通过静态键 nf_tables_skip_direct_calls 控制是否跳过间接调用。当 CPU 不需要 Retpoline 时,代码展开为一系列直接调用分支,消除了间接跳转的预测失败开销(nf_tables_core.c 第 214-234 行):

#define X(e, fun) \
    do { if ((e) == (unsigned long)(fun)) \
        return fun(expr, regs, pkt); } while (0)

X(e, nft_payload_eval);
X(e, nft_cmp_eval);
X(e, nft_counter_eval);
X(e, nft_meta_get_eval);
X(e, nft_lookup_eval);
#if IS_ENABLED(CONFIG_NFT_CT)
X(e, nft_ct_get_fast_eval);
#endif
X(e, nft_range_eval);
X(e, nft_immediate_eval);
X(e, nft_byteorder_eval);
X(e, nft_dynset_eval);
X(e, nft_rt_get_eval);
X(e, nft_bitwise_eval);
X(e, nft_objref_eval);
X(e, nft_objref_map_eval);

2. 内联 fast_ops 专用变体

对于最常见的三种表达式操作,nft_do_chain 的主循环内直接内联调用(绕过 expr_call_ops_eval):

  • nft_cmp_fast_eval:单寄存器 32 位比较(mask + xor),定义于第 84 行
  • nft_cmp16_fast_eval:16 字节(128 位)比较,定义于第 94 行
  • nft_bitwise_fast_eval:位掩码运算,定义于第 74 行

3. 寄存器跟踪(reduce)优化

在提交阶段(nf_tables_commit_chain_prepare()),遍历规则时通过 nft_regs_track 结构记录每个寄存器的"选择器",如果连续规则从同一字段加载同一寄存器,后续规则可复用前者的结果,合并冗余的 payload/meta 加载操作。

4. 链级统计优化

链级统计(nft_update_chain_stats)通过 static_branch_unlikely(&nft_counters_enabled) 保护(nf_tables_core.c 第 340-341 行)。只有当至少一条链开启了 stats 计数时才启用该静态分支,无统计场景下完全零开销。

6.4 跳转栈 jumpstack

nft_do_chain 维护一个最大深度为 16(NFT_JUMP_STACK_SIZEnf_tables.h 第 21 行)的静态调用栈,支持链的嵌套跳转(NFT_JUMP):

struct nft_jumpstack {
    const struct nft_rule_dp *rule;   // 跳转前的返回规则指针
};

NFT_JUMP 跳转时保存 nft_rule_next(rule) 为返回地址;NFT_RETURN 时弹栈恢复。NFT_GOTO 不保存栈帧,是尾调用语义(不能通过 RETURN 返回)。

当栈满时(stackptr >= NFT_JUMP_STACK_SIZE),nft_do_chain 触发 WARN_ON_ONCE 并返回 NF_DROPnf_tables_core.c 第 317-318 行),防止无限递归导致栈溢出。


7. 集合(set)子系统

7.1 nft_set 与 nft_set_ops

集合是 nftables 相对 iptables 最大的功能扩展,允许规则匹配任意大小的地址/端口集合而只使用一条规则。

nft_set 结构(nf_tables.h 第 587 行)关键字段:

struct nft_set {
    struct list_head    list;         // 链入 table->sets
    struct list_head    bindings;     // 绑定此集合的规则列表
    refcount_t          refs;         // 异步 GC 引用计数
    struct nft_table    *table;
    char                *name;
    u64                  handle;
    u32                  ktype;       // 键类型(NFTA_DATA_VALUE)
    u32                  dtype;       // 值类型(verdict 或数据)
    u32                  size;        // 最大元素数(0 = 无限制)
    u8 field_len[NFT_REG32_COUNT];   // 拼接键各字段长度
    u8 field_count;                   // 拼接键字段数
    u32                  use;         // 被多少规则引用
    atomic_t             nelems;      // 当前元素数(原子操作)
    u64                  timeout;     // 默认超时(0 = 永不过期)
    u32                  gc_int;      // GC 间隔(毫秒)
    const struct nft_set_ops *ops ____cacheline_aligned; // 操作表(对齐到缓存行)
    u16 flags:13, dead:1, genmask:2;
    u8  klen;    // 键长度(字节)
    u8  dlen;    // 值长度
    u8  num_exprs;                        // 集合级表达式数(最多 NFT_SET_EXPR_MAX=2)
    struct nft_expr *exprs[NFT_SET_EXPR_MAX]; // 集合自身的动态表达式
    struct list_head  catchall_list;      // catch-all 元素
    unsigned char     data[];            // 后端私有数据(紧随结构体)
};

nft_set_ops 接口(nf_tables.h 第 467 行)明确区分了热路径操作控制平面操作

struct nft_set_ops {
    // 热路径(数据平面)
    const struct nft_set_ext *(*lookup)(...);  // O(1)/O(logN) 查找
    const struct nft_set_ext *(*update)(...);  // dynset 动态更新
    bool                      (*delete)(...);

    // 控制平面(慢路径)
    int     (*insert)(...);
    void    (*activate)(...);
    void    (*flush)(...);
    void    (*remove)(...);
    void    (*walk)(...);
    bool    (*estimate)(...);  // 估算内存和性能
    int     (*init)(...);
    void    (*destroy)(...);
    void    (*gc_init)(...);
    // ...
};

7.2 哈希实现:nft_rhash

net/netfilter/nft_set_hash.c 实现了基于内核 rhashtable(可扩容哈希表)的集合后端。

核心数据结构(第 24-43 行):

struct nft_rhash {
    struct rhashtable   ht;         // 内核 rhashtable 实例
    struct delayed_work gc_work;    // 定期 GC 工作队列
    u32                 wq_gc_seq;  // GC 序列号
};

struct nft_rhash_elem {
    struct nft_elem_priv  priv;     // 必须是第一个字段(类型擦除接口)
    struct rhash_head     node;     // rhashtable 内部链表节点
    struct llist_node     walk_node;// GC 扫描链表节点
    u32                   wq_gc_seq;
    struct nft_set_ext    ext;      // 元素扩展(键/值/超时等)
};

哈希函数(第 45-56 行)使用 jhash 对键做 Jenkins 哈希,查找复杂度 O(1) 均摊,适合精确匹配场景。

nft_rhash_lookup() 实现(第 84-103 行),在 memcmp 键相等后还需检查三个条件:元素未被标记为 dead(GC 中)、未超时、在当前代可见:

const struct nft_set_ext *
nft_rhash_lookup(const struct net *net, const struct nft_set *set, const u32 *key)
{
    struct nft_rhash *priv = nft_set_priv(set);
    struct nft_rhash_cmp_arg arg = {
        .genmask = nft_genmask_cur(net),  // 当前代掩码
        .set     = set,
        .key     = key,
        .tstamp  = get_jiffies_64(),      // 用于超时检查
    };
    const struct nft_rhash_elem *he =
        rhashtable_lookup(&priv->ht, &arg, nft_rhash_params);
    return he ? &he->ext : NULL;
}

比较函数 nft_rhash_cmp()(第 59-74 行)同时过滤 dead、expired 和 genmask 三个条件:

static inline int nft_rhash_cmp(struct rhashtable_compare_arg *arg,
                                 const void *ptr)
{
    const struct nft_rhash_cmp_arg *x = arg->key;
    const struct nft_rhash_elem *he = ptr;

    if (memcmp(nft_set_ext_key(&he->ext), x->key, x->set->klen))
        return 1;
    if (nft_set_elem_is_dead(&he->ext))
        return 1;
    if (__nft_set_elem_expired(&he->ext, x->tstamp))
        return 1;
    if (!nft_set_elem_active(&he->ext, x->genmask))
        return 1;
    return 0;
}

7.3 红黑树实现:nft_rbtree

net/netfilter/nft_set_rbtree.c 实现了支持区间匹配的红黑树后端,用于 IP 地址前缀/端口区间等范围查找。

struct nft_rbtree {
    struct rb_root   root;           // 标准内核红黑树根
    rwlock_t         lock;           // 读写锁(GC 写,查找读)
    struct nft_array __rcu *array;   // 加速查找的数组视图(RCU 保护)
    struct nft_array *array_next;    // 下一代数组(提交用)
    unsigned long    start_rbe_cookie;
    unsigned long    last_gc;
    struct list_head expired;        // 已超时等待 GC 的元素
    u64              last_tstamp;
};

struct nft_rbtree_elem {
    struct nft_elem_priv  priv;
    union {
        struct rb_node node;         // 红黑树节点
        struct list_head list;       // 超时过期链表节点(GC 阶段使用)
    };
    struct nft_set_ext ext;
};

区间匹配使用"起始点"和"终止点"两个元素配对的方式表示 [start, end] 区间。nft_rbtree_interval_end() 通过 NFT_SET_ELEM_INTERVAL_END 标志区分(第 51-55 行):

static bool nft_rbtree_interval_end(const struct nft_rbtree_elem *rbe)
{
    return nft_set_ext_exists(&rbe->ext, NFT_SET_EXT_FLAGS) &&
           (*nft_set_ext_flags(&rbe->ext) & NFT_SET_ELEM_INTERVAL_END);
}

查找优化:nft_array 数组视图

nft_rbtree 维护了一个 nft_array(排序的 nft_array_interval 结构数组,每个 interval 包含 from/to 指针),通过二分搜索加速区间查找(nft_set_rbtree.c 第 107-129 行):

const struct nft_set_ext *
nft_rbtree_lookup(const struct net *net, const struct nft_set *set,
                  const u32 *key)
{
    struct nft_rbtree *priv = nft_set_priv(set);
    struct nft_array *array = rcu_dereference(priv->array);
    struct nft_array_lookup_ctx ctx = { .key = key, .klen = set->klen };

    interval = bsearch(&ctx, array->intervals, array->num_intervals,
                       sizeof(struct nft_array_interval),
                       nft_array_lookup_cmp);
    if (!interval || nft_set_elem_expired(interval->from))
        return NULL;
    return interval->from;
}
nft_rbtree 区间查找示意图:

  输入:IP 地址 192.168.1.100

  nft_array(排序数组,二分搜索):
  +------------+------------+
  | from       | to         |
  +------------+------------+
  | 10.0.0.0   | 10.255.255.255 |  -> bsearch 快速排除
  | 192.168.0.0 | 192.168.1.255 |  -> 命中!O(log N)
  | 172.16.0.0 | 172.31.255.255 |
  +------------+------------+

  若 bsearch 未命中(超时/dead 元素):
  -> 回退到红黑树精确查找 O(log N)

7.4 pipapo 区间/多字段集合

net/netfilter/nft_set_pipapo.c 实现了"Piece-wise Parallel Population count"(pipapo)算法,专为多字段拼接(concatenation)集合设计,支持如 {src_ip, dst_ip, protocol, dport} 的四元组匹配。

pipapo 将每个字段分解为若干 4 位组(nibble),用位图并联操作实现快速筛选,最终结果是多个字段匹配结果的"位图交集"。net/netfilter/nft_set_pipapo_avx2.c 提供了 AVX2 SIMD 加速版本。

详细分析见第 16 节。

7.5 集合元素扩展 nft_set_ext

集合元素的键、值、超时等可选字段通过 nft_set_ext 扩展机制统一管理(nf_tables.h 第 752 行):

struct nft_set_ext {
    u8   genmask;           // 元素的双代可见掩码
    u8   offset[NFT_SET_EXT_NUM];  // 各扩展字段相对于 data[] 起始的偏移
    char data[];
} __aligned(BITS_PER_LONG / 8);

enum nft_set_extensions {
    NFT_SET_EXT_KEY,         // 键(必须)
    NFT_SET_EXT_KEY_END,     // 区间终止键(可选)
    NFT_SET_EXT_DATA,        // 映射值(可选,用于 map 类型集合)
    NFT_SET_EXT_FLAGS,       // 元素标志
    NFT_SET_EXT_TIMEOUT,     // 超时时间戳
    NFT_SET_EXT_USERDATA,    // 用户数据
    NFT_SET_EXT_EXPRESSIONS, // 元素级表达式(如 counter)
    NFT_SET_EXT_OBJREF,      // 有状态对象引用
    NFT_SET_EXT_NUM
};

扩展字段通过 nft_set_ext_add() 在创建时注册到模板(nft_set_ext_tmpl),按需分配紧凑布局,避免了固定大小结构体浪费内存的问题。nft_set_ext_key()nft_set_ext_data()nft_set_ext_timeout() 等内联函数提供类型安全的访问接口。

7.6 动态集合(dynset)

nft_dynset 表达式支持在数据包处理路径上动态添加/更新集合元素,常用于实现连接速率限制。

nft_rhash_update() 实现了并发安全的"查找不存在则插入"原子操作(nft_set_hash.c 第 126-169 行),使用 rhashtable_lookup_get_insert_key() 处理并发竞争:

static const struct nft_set_ext *
nft_rhash_update(struct nft_set *set, const u32 *key, ...)
{
    he = rhashtable_lookup(&priv->ht, &arg, nft_rhash_params);
    if (he != NULL)
        goto out;  // 已存在,直接返回

    // 分配新元素
    elem_priv = nft_dynset_new(set, expr, regs);
    // 原子插入(并发安全)
    prev = rhashtable_lookup_get_insert_key(&priv->ht, &arg, &he->node, ...);
    if (prev) {
        // 另一 CPU 率先插入,使用其结果
        nft_set_elem_destroy(set, &he->priv, true);
        atomic_dec(&set->nelems);
        he = prev;
    }
    return &he->ext;
}

8. Netlink 事务接口

8.1 消息类型枚举

nftables 通过 Netlink 子系统 NFNL_SUBSYS_NFTABLES 通信,所有操作消息类型定义在 include/uapi/linux/netfilter/nf_tables.h 第 110 行,由 nf_tables_api.c 第 83-110 行的审计映射表完整列出:

NFT_MSG_NEWTABLE / GETTABLE / DELTABLE         表管理
NFT_MSG_NEWCHAIN / GETCHAIN / DELCHAIN         链管理
NFT_MSG_NEWRULE  / GETRULE  / DELRULE          规则管理
NFT_MSG_NEWSET   / GETSET   / DELSET           集合管理
NFT_MSG_NEWSETELEM / GETSETELEM / DELSETELEM   集合元素管理
NFT_MSG_NEWOBJ   / GETOBJ   / DELOBJ           有状态对象管理
NFT_MSG_NEWFLOWTABLE / GETFLOWTABLE / DELFLOWTABLE  flowtable 管理
NFT_MSG_NEWGEN                                 新代通知(提交完成事件)
NFT_MSG_TRACE                                  调试追踪事件

nfnetlink 批处理机制允许在单个 Netlink 消息中包含多个操作,nf_tables_subsyscommit/abort 回调实现事务语义(nf_tables_api.c 第 11436-11445 行):

static const struct nfnetlink_subsystem nf_tables_subsys = {
    .name      = "nf_tables",
    .subsys_id = NFNL_SUBSYS_NFTABLES,
    .cb_count  = NFT_MSG_MAX,
    .cb        = nf_tables_cb,
    .commit    = nf_tables_commit,   // 批处理提交
    .abort     = nf_tables_abort,    // 批处理回滚
    .valid_genid = nf_tables_valid_genid,
};

8.2 事务结构 nft_trans

每个 Netlink 操作在内核事务链表中对应一个 nft_trans 节点:

struct nft_trans {
    struct list_head  list;       // 链入 nft_net->commit_list
    int               msg_type;  // NFT_MSG_NEWRULE 等
    struct net        *net;
    struct nft_table  *table;
    u32               seq;       // Netlink 序列号
    u16               flags;
    u8                report;
    bool              put_net;
};

不同类型的操作通过特化结构扩展基类(以规则为例):

struct nft_trans_rule {
    struct nft_trans  nft_trans;
    struct nft_rule   *rule;
    struct nft_chain  *chain;
    bool              bound;     // 是否已绑定(防止重复提交)
};

nft_net->commit_list 在批处理期间积累所有待处理事务,提交时统一执行,回滚时全部撤销。

8.3 原子提交:nf_tables_commit

nf_tables_commit()nf_tables_api.c 第 10791 行)分为清晰的六个阶段:

阶段 0:验证规则集(nf_tables_validate)
         检查 JUMP 目标存在性、循环引用等
         失败则返回 -EAGAIN(让用户空间重试)

阶段 1:为每条受影响链准备新代规则 blob
         nf_tables_commit_chain_prepare()
         将 nft_rule 链表编译为连续的 nft_rule_blob 内存块
         执行 reduce 优化(寄存器冗余消除)

阶段 2:将新规则 blob 设为可见
         nf_tables_commit_chain()
         将 blob_gen_X 原子切换到新 blob

阶段 3:递增 base_seq(generation counter)
         smp_store_release(&net->nft.base_seq, base_seq)
         此后所有新连接进入新代规则

阶段 4:遍历 commit_list,执行实际对象激活
         case NFT_MSG_NEWRULE: nft_clear() 激活规则
         case NFT_MSG_DELRULE: nft_set_elem_change_active()

阶段 5:发送 Netlink 事件通知(NFT_MSG_NEWGEN 等)
         释放旧规则 blob(call_rcu 延迟,等待 RCU 宽限期)

关键代码(nf_tables_api.c 第 10882-10887 行):

base_seq = nft_base_seq(net);
while (++base_seq == 0);  // 跳过 0 值

// 使用 store_release 保证内存可见性(配合 smp_load_acquire)
smp_store_release(&net->nft.base_seq, base_seq);

8.4 双代(generation)机制

nftables 使用"双代"(two-generation)技术实现规则的 RCU 安全原子切换:

Generation 机制示意图:

    控制平面              数据平面
    =========             =========
                          正在使用 gen_0 规则
                          (genbit=0, blob_gen_0)

    添加新规则
    写入 gen_1 blob
    (blob_gen_1 = new_blob)

    gencursor 切换:
    genbit: 0 -> 1
                          新数据包:使用 gen_1 规则
                          老数据包:继续使用 gen_0

    等待 RCU 宽限期
    (call_rcu)
                          确保无数据包在访问 gen_0
    释放 gen_0 旧 blob

每条规则的 genmask 字段(2 位)用于标记该规则在哪个代可见:

  • bit 0:generation 0 可见性
  • bit 1:generation 1 可见性

新添加的规则设置 genmask 使其在旧代不可见,提交后切换代,自然激活。删除规则时清除对应代的可见位,旧代包仍能看到该规则直到 RCU 宽限期结束。

集合元素同样有 ext.genmasknft_rhash_cmp() 等查找函数通过当前代掩码进行可见性过滤(nft_set_hash.c 第 59-74 行)。


9. Hooks 注册与 Netfilter 集成

9.1 nft_base_chain 与 nf_hook_ops

只有"基础链"(base chain)才能挂载到 Netfilter Hook。nft_base_chain 内嵌 nf_hook_ops 结构(nf_tables.h 第 1246 行),ops.hook 回调对于 filter 类型就是 nft_do_chain

9.2 链类型:filter/route/nat

三种内建链类型(enum nft_chain_typesnf_tables.h 第 1101 行):

NFT_CHAIN_T_DEFAULT (0)  filter 类型
    支持 PREROUTING/INPUT/FORWARD/OUTPUT/POSTROUTING
    hook 函数:nft_do_chain_ipv4/ipv6

NFT_CHAIN_T_ROUTE (1)    route 类型(标记路由)
    hook 函数:nft_route_eval

NFT_CHAIN_T_NAT (2)      nat 类型
    需要连接跟踪支持
    hook 函数:nft_nat_do_chain

nft_chain_type 结构(nf_tables.h 第 1185 行)定义了支持的 Hook 掩码和每个 Hook 的处理函数:

struct nft_chain_type {
    const char          *name;
    enum nft_chain_types type;
    int                  family;
    struct module        *owner;
    unsigned int         hook_mask;    // 允许挂载的 hook 点位掩码
    nf_hookfn           *hooks[NFT_MAX_HOOKS]; // 各 hook 点的处理函数
    int  (*ops_register)(struct net *net, const struct nf_hook_ops *ops);
    void (*ops_unregister)(struct net *net, const struct nf_hook_ops *ops);
};

9.3 Hook 注册流程

nf_tables_register_hook()nf_tables_api.c 第 394 行)处理链到 Netfilter Hook 的注册:

static int nf_tables_register_hook(struct net *net,
                                   const struct nft_table *table,
                                   struct nft_chain *chain)
{
    // Dormant 表或非基础链:不注册 hook
    if (table->flags & NFT_TABLE_F_DORMANT || !nft_is_base_chain(chain))
        return 0;

    basechain = nft_base_chain(chain);
    ops = &basechain->ops;

    // 如果链类型有自定义注册函数(如 NAT)
    if (basechain->type->ops_register)
        return basechain->type->ops_register(net, ops);

    // NETDEV 族(支持多网络接口)
    if (nft_base_chain_netdev(table->family, basechain->ops.hooknum))
        return nft_netdev_register_hooks(net, &basechain->hook_list);

    // 标准单 hook 注册
    return nf_register_net_hook(net, &basechain->ops);
}

对于 NFPROTO_NETDEV 族和 NFPROTO_INET + NF_INET_INGRESS hook,nftables 支持绑定到多个具体网络设备(nft_hook 结构,nf_tables.h 第 1221 行),每个设备有独立的 nf_hook_ops 列表。


10. 连接跟踪(ct)表达式深入分析

ct(connection tracking)表达式实现在 net/netfilter/nft_ct.c,允许规则匹配连接状态、标记、NAT 映射等信息。

10.1 核心实现

nft_ct_get_eval()nft_ct.c 第 51 行)是 ct 表达式的执行函数,从 skb 获取连接跟踪上下文:

static void nft_ct_get_eval(const struct nft_expr *expr,
                            struct nft_regs *regs,
                            const struct nft_pktinfo *pkt)
{
    const struct nft_ct *priv = nft_expr_priv(expr);
    u32 *dest = &regs->data[priv->dreg];
    enum ip_conntrack_info ctinfo;
    const struct nf_conn *ct;

    ct = nf_ct_get(pkt->skb, &ctinfo);  // 从 skb->_nfct 获取连接

    switch (priv->key) {
    case NFT_CT_STATE:    // 连接状态
        if (ct)
            state = NF_CT_STATE_BIT(ctinfo);
        else if (ctinfo == IP_CT_UNTRACKED)
            state = NF_CT_STATE_UNTRACKED_BIT;
        else
            state = NF_CT_STATE_INVALID_BIT;
        *dest = state;
        return;
    case NFT_CT_DIRECTION:  // 连接方向(ORIGINAL/REPLY)
        nft_reg_store8(dest, CTINFO2DIR(ctinfo));
        return;
    case NFT_CT_STATUS:   // 连接状态位
        *dest = ct->status;
        return;
    case NFT_CT_MARK:     // 连接标记(跨包状态传递)
        *dest = READ_ONCE(ct->mark);
        return;
    // 还支持:EXPIRATION/HELPER/LABELS/BYTES/PKTS/AVGPKT/ZONE 等
    }
}

10.2 支持的 ct 键值

键(nft_ct_keys) 说明
NFT_CT_STATE 连接状态(NEW/ESTABLISHED/RELATED/INVALID/UNTRACKED)
NFT_CT_DIRECTION 方向(ORIGINAL=0, REPLY=1)
NFT_CT_STATUS 连接状态位(IPS_CONFIRMED/NAT/HELPER 等)
NFT_CT_MARK 32 位连接标记
NFT_CT_SECMARK SELinux 安全标记
NFT_CT_EXPIRATION 距过期时间(毫秒)
NFT_CT_HELPER 关联的 ALG helper 名称
NFT_CT_LABELS 128 位连接标签(需 CONFIG_NF_CONNTRACK_LABELS)
NFT_CT_BYTES/PKTS 连接字节/包数(需 acct 模块)
NFT_CT_AVGPKT 平均每包字节数
NFT_CT_ZONE 连接跟踪 zone ID(多租户隔离)
NFT_CT_PROTO_SRC/DST 元组中的源/目的端口
NFT_CT_SRC/DST 元组中的源/目的地址

10.3 ct 写操作

ct 表达式不仅支持读取,还支持写入(nft_ct_set_eval()),例如:

  • 设置 NFT_CT_MARKct->mark = regs->data[priv->sreg]
  • 设置 NFT_CT_LABELS:修改 nf_conn_labels 扩展
  • 设置 NFT_CT_ZONE:在 PREROUTING 阶段将包放入特定连接跟踪 zone

这使得 nftables 的 ct 表达式可以完整替代 iptables 的 CONNMARK/CONNLABEL 模块。


11. flowtable 软件加速

flowtable 是 nftables 提供的"软件加速"(fast path)机制,允许已建立的 TCP/UDP 连接绕过完整的 Netfilter/nf_tables 处理路径,直接在 PREROUTING hook 完成转发。

11.1 nf_flowtable 结构

定义在 include/net/netfilter/nf_flow_table.h 第 76 行:

struct nf_flowtable {
    unsigned int             flags;       // NF_FLOWTABLE_HW_OFFLOAD 等
    int                      priority;    // hook 优先级
    struct rhashtable        rhashtable;  // 流表哈希表(数据平面核心)
    struct list_head         list;        // 全局 flowtables 列表
    const struct nf_flowtable_type *type; // ipv4/ipv6/inet 类型
    struct delayed_work      gc_work;     // 定期 GC
    struct flow_block        flow_block;  // TC/硬件 offload 流块
    struct rw_semaphore      flow_block_lock;
    possible_net_t           net;
};

rhashtable 字段位于结构体开头(紧接 flagspriority 只读字段),将哈希表控制块尽量靠前以改善 NUMA 局部性。

11.2 flow_offload 流表项

flow_offload 描述一条已卸载到 flowtable 的连接(nf_flow_table_core.c 中管理):

struct flow_offload {
    struct flow_offload_tuple_rhash tuplehash[FLOW_OFFLOAD_DIR_MAX]; // 双向元组哈希节点
    struct nf_conn   *ct;           // 关联的连接跟踪条目
    unsigned long     flags;        // NF_FLOW_SNAT/DNAT/CLOSING/HW 等
    u32               timeout;      // 绝对过期时间戳
    enum nf_flow_offload_type type; // ROUTE/XFRM/TC 等
    struct rcu_head   rcu_head;     // RCU 释放
};

流表哈希 key 为 5 元组(src/dst IP、src/dst port、L4 协议),使用 jhash 哈希(nf_flow_table_core.c 第 270-294 行)。

flow_offload_alloc()(第 53-76 行)从连接跟踪条目创建 flow_offload:

struct flow_offload *flow_offload_alloc(struct nf_conn *ct)
{
    flow = kmem_cache_zalloc(flow_offload_cachep, GFP_ATOMIC);
    refcount_inc(&ct->ct_general.use);
    flow->ct = ct;
    flow_offload_fill_dir(flow, FLOW_OFFLOAD_DIR_ORIGINAL);
    flow_offload_fill_dir(flow, FLOW_OFFLOAD_DIR_REPLY);
    // 从 ct->status 继承 SNAT/DNAT 标志
    if (ct->status & IPS_SRC_NAT)
        __set_bit(NF_FLOW_SNAT, &flow->flags);
    if (ct->status & IPS_DST_NAT)
        __set_bit(NF_FLOW_DNAT, &flow->flags);
    return flow;
}

11.3 bypass 路径原理

正常路径(无 flowtable):
  NIC -> PREROUTING -> ip_forward() -> POSTROUTING -> NIC

flowtable bypass 路径:
  NIC -> PREROUTING hook(nf_flow_table_ipv4)
           |
           v
      flow_offload_lookup()
           |
      +----+----+
      |         |
     HIT       MISS
      |         |
      v         v
  直接转发   继续正常路径
  (修改TTL,  (若连接 ESTABLISHED,
   MAC,跳过  nft 规则调用
   路由/FWD/  flow_offload_add()
   POSTROUTING) 加入 flowtable)

激活过程nf_flow_table_core.c 第 323 行):

int flow_offload_add(struct nf_flowtable *flow_table, struct flow_offload *flow)
{
    flow->timeout = nf_flowtable_time_stamp + flow_offload_get_timeout(flow);

    // 插入双向哈希(ORIGINAL 和 REPLY 方向各一条)
    rhashtable_insert_fast(&flow_table->rhashtable,
                           &flow->tuplehash[0].node, ...);
    rhashtable_insert_fast(&flow_table->rhashtable,
                           &flow->tuplehash[1].node, ...);

    // 刷新 conntrack 超时(流表接管后 conntrack 不会看到每个包)
    nf_ct_refresh(flow->ct, NF_CT_DAY);

    // 若配置了硬件卸载,通知驱动
    if (nf_flowtable_hw_offload(flow_table))
        nf_flow_offload_add(flow_table, flow);
    return 0;
}

超时与拆卸flow_offload_fixup_ct()(第 196 行)在 flowtable 条目过期或 TCP FIN/RST 时将 conntrack 的超时拉回到正常值,保证连接最终能被 conntrack 正确跟踪和释放。TCP 连接的关闭状态由 NF_FLOW_CLOSING 标志触发 flow_offload_fixup_tcp() 将 conntrack TCP 状态机拉回 CLOSE 状态。

11.4 硬件卸载

当设置 NF_FLOWTABLE_HW_OFFLOAD 标志时,flowtable 通过 flow_block(TC block 机制)将流表规则推送到支持的网卡驱动,实现真正的硬件转发卸载,CPU 完全不参与数据包处理。

nf_flowtable_type 接口的 action() 回调负责生成驱动可理解的 nf_flow_rule 描述,setup() 回调管理驱动的块绑定/解绑。


12. 安全性与性能设计

12.1 并发安全

nftables 的并发安全通过以下机制保证:

控制平面

  • nft_net->commit_mutex:事务提交互斥锁,保证同时只有一个 Netlink 批处理在提交
  • nf_tables_destroy_list_lock(spinlock,nf_tables_api.c 第 39 行):保护对象销毁链表
  • nf_tables_gc_list_lock(spinlock,第 40 行):保护 GC 链表

数据平面

  • RCU:规则 blob(chain->blob_gen_X)用 rcu_dereference()/rcu_assign_pointer() 保护,读侧完全无锁
  • per-CPU 统计nft_stats __percpu 避免了计数器的跨 CPU 竞争
  • 原子操作atomic_t nelems(集合元素计数)用于 dynset 的无锁更新

RCU 宽限期管理:旧的规则 blob 通过 call_rcu() + nft_rule_dp_last.rcu_head 延迟释放,保证所有正在执行的 nft_do_chain 实例都退出后才真正 kvfree()nf_tables_api.c 第 10289-10304 行)。

12.2 Retpoline 缓解

在启用 Retpoline 的内核中(CONFIG_MITIGATION_RETPOLINE),nf_tables_core.c 第 24-39 行使用 static_key 机制:当 CPU 不需要 Retpoline(X86_FEATURE_RETPOLINE 不存在)时,expr_call_ops_eval() 展开为一系列直接函数调用,完全避免了间接跳转的推测执行漏洞缓解开销。

12.3 用户绑定与 Owner 机制

nftables 表支持 NFT_TABLE_F_OWNER 标志,将表绑定到特定 Netlink socket(table->nlpid)。当 socket 关闭时,内核自动删除该表(防止孤儿规则集长期存在)。NFT_TABLE_F_PERSIST 标志则允许规则集在 socket 关闭后继续存在,适合守护进程场景(nf_tables.h 第 1333-1342 行):

static inline bool nft_table_has_owner(const struct nft_table *table)
{
    return table->flags & NFT_TABLE_F_OWNER;
}

static inline bool nft_table_is_orphan(const struct nft_table *table)
{
    return (table->flags & (NFT_TABLE_F_OWNER | NFT_TABLE_F_PERSIST)) ==
            NFT_TABLE_F_PERSIST;
}

12.4 规则集验证

提交前的 nf_tables_validate() 执行:

  1. 循环检测:通过 nft_chain_validate_state.depth 跟踪调用深度(最大 16),检测 JUMP/GOTO 循环
  2. 目标存在性:验证所有 JUMP/GOTO 目标链存在且在同一表中
  3. 绑定检查:匿名集合和 binding chain 必须被至少一条规则引用(防止悬空对象),违规时输出警告(nf_tables_api.c 第 10820-10821 行):pr_warn_once("nftables ruleset with unbound set\n")

12.5 性能总结

nftables 相对 iptables 的性能优势来源于多个层次的优化:

性能优化层次对比:

规则存储:
  iptables: struct list_head 链表(随机内存访问)
  nftables: nft_rule_blob 连续内存块(顺序访问,缓存友好)

规则执行:
  iptables: match/target 函数指针间接调用
  nftables: 快速路径内联 + Retpoline 规避 + 寄存器冗余消除

多值匹配:
  iptables: N 条规则 = O(N) 遍历
  nftables: 哈希集合 = O(1),红黑树 = O(log N)

转发加速:
  iptables: 每包完整 Netfilter 路径
  nftables: flowtable bypass(软件 + 硬件卸载)

计数器:
  iptables: 全局锁保护的 64 位计数
  nftables: per-CPU 无锁计数(u64_stats_sync)

13. nft_compat:iptables 兼容层深度分析

net/netfilter/nft_compat.c 实现了 nftables 对 iptables/ip6tables/ebtables/arptables 扩展模块的兼容适配层,允许现有的 xtables match/target 模块在 nftables 框架下工作,无需重写。

13.1 核心思想

nft_compat 将每个 xtables match 或 target 包装为一个 nft_expr。关键的 eval 函数调用原始的 xtables 模块回调,并将其裁决码转换为 nftables 裁决码。

13.2 nft_target_eval_xt 实现

对于普通 IPv4/IPv6 协议族,xt target 的执行函数(nft_compat.c 第 73-98 行):

static void nft_target_eval_xt(const struct nft_expr *expr,
                               struct nft_regs *regs,
                               const struct nft_pktinfo *pkt)
{
    void *info = nft_expr_priv(expr);
    struct xt_target *target = expr->ops->data;  // xtables target 指针
    struct sk_buff *skb = pkt->skb;
    struct xt_action_param xt;
    int ret;

    // 填充 xt_action_param,桥接 nft_pktinfo 到 xtables 接口
    nft_compat_set_par(&xt, pkt, target, info);

    ret = target->target(skb, &xt);  // 调用原始 xtables target

    if (xt.hotdrop)
        ret = NF_DROP;  // hotdrop 语义转换

    switch (ret) {
    case XT_CONTINUE:
        regs->verdict.code = NFT_CONTINUE;  // xtables 继续 -> nft 继续
        break;
    default:
        regs->verdict.code = ret;  // NF_ACCEPT/NF_DROP 等直接映射
        break;
    }
}

对于 ebtables bridge 协议族,则使用 nft_target_eval_bridge() 做额外的 EBT_ACCEPT/EBT_DROP 到 NF_ACCEPT/NF_DROP 的转换(第 100-134 行)。

13.3 nft_match_eval 实现

对于 xtables match 模块(nft_compat.c 第 392 行),__nft_match_eval() 先调用 match->match(),再根据返回值决定是否设置 NFT_BREAK(不匹配):

static void __nft_match_eval(const struct nft_expr *expr,
                             struct nft_regs *regs,
                             const struct nft_pktinfo *pkt,
                             void *info)
{
    struct xt_match *match = expr->ops->data;
    struct xt_action_param xt;
    bool ret;

    nft_compat_set_par(&xt, pkt, match, info);

    ret = match->match(pkt->skb, &xt);  // 调用原始 xtables match

    if (xt.hotdrop) {
        regs->verdict.code = NF_DROP;
        return;
    }

    // match 返回 false(不匹配)-> NFT_BREAK(本规则不匹配)
    // match 返回 true(匹配)  -> NFT_CONTINUE(继续执行下一个 expr)
    regs->verdict.code = ret ? NFT_CONTINUE : NFT_BREAK;
}

13.4 xtables 模块的初始化与验证

nft_target_init() 在规则创建时调用 xtables target 的 checkentry 函数(第 243-291 行),通过 nft_target_set_tgchk_param() 填充 xt_tgchk_param,模拟 iptables 的初始化流程:

static int
nft_target_init(const struct nft_ctx *ctx, const struct nft_expr *expr,
                const struct nlattr * const tb[])
{
    struct xt_target *target = expr->ops->data;
    struct xt_tgchk_param par;

    // 等待之前的 destroy 工作队列完成(避免 /proc 文件竞争)
    nft_compat_wait_for_destructors(ctx->net);

    ret = xt_check_target(&par, size, proto, inv);
    // ...
}

注意 nft_compat_wait_for_destructors() 调用 nf_tables_trans_destroy_flush_work() 确保之前的 xtables destroy 回调(在工作队列中异步执行)都已完成,防止 /proc 文件创建/销毁的竞态。

13.5 hook 验证

nft_target_validate() 验证 xtable target 的 hook 约束(第 353-390 行)。若 target 定义了 .hooks 位掩码,则检查所在 nft 链的 hooknum 是否在允许范围内,并调用 nft_compat_chain_validate_dependency() 验证链类型(如 NAT target 必须在 nat 类型链上):

static int nft_target_validate(const struct nft_ctx *ctx,
                               const struct nft_expr *expr)
{
    // ...
    hook_mask = 1 << ops->hooknum;
    if (target->hooks && !(hook_mask & target->hooks))
        return -EINVAL;  // target 不支持此 hook 点

    ret = nft_compat_chain_validate_dependency(ctx, target->table);
    // ...
}

13.6 模块动态加载

nft_compat 使用 nft_request_module() 按需加载 xtables 扩展模块。若 xt_check_target() 返回 -ENOENT,对于已知模块(如 LOG -> nf_log_syslog,NFLOG -> nfnetlink_log)自动触发 request_module(),返回 -EAGAIN 让用户空间重试(第 270-283 行)。


14. nftables 集合动态更新与超时机制

14.1 集合元素超时

集合元素的超时通过 NFT_SET_EXT_TIMEOUT 扩展字段实现,存储绝对过期时间(jiffies):

// 检查元素是否过期(nf_tables.h 中的内联函数)
static inline bool __nft_set_elem_expired(const struct nft_set_ext *ext,
                                           u64 tstamp)
{
    return nft_set_ext_exists(ext, NFT_SET_EXT_TIMEOUT) &&
           nft_set_elem_expired_jiffies(nft_set_ext_timeout(ext), tstamp);
}

超时值是绝对 jiffies 时间戳(创建时间 + 超时间隔),通过 get_jiffies_64()tstamp 比较确定是否过期。这避免了每次比较都需要读取当前时间,只在元素创建时计算一次。

14.2 GC(垃圾回收)机制

哈希集合 GCnft_set_hash.c 中的 nft_rhash_gc_work):使用延迟工作队列(delayed_work),定期(gc_int 毫秒)扫描哈希表中的元素,对超时元素:

  1. 调用 nft_setelem_data_deactivate() 在当前代停用元素
  2. 通过 Netlink 发送 NFT_MSG_DELSETELEM 事件(若有监听者)
  3. 调用 nft_set_elem_destroy() + RCU 延迟释放内存

红黑树集合 GCnft_set_rbtree.c):通过读写锁保护,nft_rbtree_gc_elem_move() 将超时元素从红黑树移动到 priv->expired 链表(nft_set_rbtree.c 第 195-200 行),再在 GC 定时器中批量处理:

static void nft_rbtree_gc_elem_move(struct net *net, struct nft_set *set,
                                    struct nft_rbtree *priv,
                                    struct nft_rbtree_elem *rbe)
{
    lockdep_assert_held_write(&priv->lock);
    nft_setelem_data_deactivate(net, set, &rbe->priv);
    // 将节点从 rb_node 联合体切换到 list 用于 expired 链表
    rb_erase(&rbe->node, &priv->root);
    list_add(&rbe->list, &priv->expired);
}

14.3 dynset 超时元素

通过 nft_dynset 表达式动态创建的元素默认继承集合的 timeout 值。若规则中指定了 NFT_DYNSET_OP_ADD + NFT_SET_EXT_TIMEOUT,则使用规则指定的超时覆盖默认值。动态元素在创建时通过 nft_dynset_new() 分配并初始化,超时字段写入当前时间 + 超时间隔。

14.4 集合后端选择算法

nft_select_set_ops() 在创建集合时根据特征自动选择最优后端(nf_tables_api.c 中):

集合后端选择逻辑:

  输入:set->flags, klen, dtype, size

  if (flags & NFT_SET_INTERVAL)      -> nft_rbtree(区间匹配)
    if (field_count > 1)             -> nft_pipapo(多字段区间)

  if (klen <= 2 && !timeout)         -> nft_bitmap(小键精确匹配)

  if (dtype == NFT_DATA_VERDICT)     -> nft_rhash(映射集合)
    if (size == 0)                   -> nft_rhash(无大小限制哈希)

  估算:each backend->estimate() 返回内存/性能分数
  选择分数最优的后端

每个后端通过 nft_set_ops->estimate() 报告自己对给定参数的适用性(内存占用估算和查找时间复杂度权重),内核综合这些信息选出最优后端。


15. nftables netdev 表与 ingress/egress hook

15.1 NFPROTO_NETDEV 协议族

NFPROTO_NETDEV 是 nftables 专为网络设备层引入的协议族,支持在 TC 层之前的 NF_NETDEV_INGRESSNF_NETDEV_EGRESS hook 点处理数据包,实现极早期的包过滤和流量整形。

链类型定义在 net/netfilter/nft_chain_filter.c 第 309-319 行:

static const struct nft_chain_type nft_chain_filter_netdev = {
    .name      = "filter",
    .type      = NFT_CHAIN_T_DEFAULT,
    .family    = NFPROTO_NETDEV,
    .hook_mask = (1 << NF_NETDEV_INGRESS) |
                 (1 << NF_NETDEV_EGRESS),
    .hooks     = {
        [NF_NETDEV_INGRESS] = nft_do_chain_netdev,
        [NF_NETDEV_EGRESS]  = nft_do_chain_netdev,
    },
};

15.2 nft_do_chain_netdev 包路径感知

nft_do_chain_netdev() 能够感知 skb 的以太网协议类型,并相应初始化 nft_pktinfonft_chain_filter.c 第 287-307 行):

static unsigned int nft_do_chain_netdev(void *priv, struct sk_buff *skb,
                                         const struct nf_hook_state *state)
{
    struct nft_pktinfo pkt;
    nft_set_pktinfo(&pkt, skb, state);

    switch (skb->protocol) {
    case htons(ETH_P_IP):
        nft_set_pktinfo_ipv4_validate(&pkt);   // 验证 IPv4 头并设置 thoff
        break;
    case htons(ETH_P_IPV6):
        nft_set_pktinfo_ipv6_validate(&pkt);   // 验证 IPv6 头
        break;
    default:
        nft_set_pktinfo_unspec(&pkt);          // 非 IP 包(ARP 等)
        break;
    }

    return nft_do_chain(&pkt, priv);
}

这意味着在 netdev 链中,payload 表达式可以同时处理 L2(以太网头)和 L3/L4 层字段,而无需协议族感知,体现了 nftables 统一框架的优势。

15.3 多设备绑定机制

netdev 基础链通过 nft_base_chain->hook_listnft_hook 结构列表)绑定到一个或多个网络设备(nf_tables.h 第 1221 行):

struct nft_hook {
    struct list_head    list;      // 链入 basechain->hook_list
    struct list_head    ops_list;  // 此 hook 在各设备上的 nf_hook_ops 列表
    char                ifname[IFNAMSIZ]; // 绑定的设备名(支持通配符)
    unsigned int        ifnamelen;
    struct nf_hook_ops  ops;       // 基础 nf_hook_ops 模板
};

每个 nft_hook 对应一个设备名模式,当系统中有设备注册/注销/改名时,nf_tables_netdev_notifiernft_chain_filter.c 第 431-433 行)会自动更新 hook 注册状态:

static struct notifier_block nf_tables_netdev_notifier = {
    .notifier_call = nf_tables_netdev_event,
};

nf_tables_netdev_event() 处理 NETDEV_REGISTERNETDEV_UNREGISTERNETDEV_CHANGENAME 三种事件(第 402-429 行),保证设备生命周期与 hook 注册同步:

设备注册事件流:

  NETDEV_REGISTER(eth0)
       |
       v
  __nf_tables_netdev_event()
       |
       v
  遍历所有 NFPROTO_NETDEV 表中的基础链
       |
  对每个 nft_hook:
       |
       +-> hook->ifname 与 eth0 匹配?
       |      |
       |      v
       |   kmemdup(&basechain->ops)  分配新 nf_hook_ops
       |   ops->dev = eth0
       |   nf_register_net_hook(net, ops)
       |   list_add_tail_rcu(&ops->list, &hook->ops_list)

15.4 ingress hook 的包处理时机

NF_NETDEV_INGRESS hook 在 netif_receive_skb_core() 中非常早期被调用,此时数据包刚从驱动接收,尚未进入协议栈:

网卡驱动 -> NAPI poll -> __netif_receive_skb()
                              |
                              v
                    NF_NETDEV_INGRESS hook  <- nftables netdev ingress
                              |
                              v
                    ip_rcv() / ip6_rcv() (协议栈入口)
                              |
                              v
                    NF_INET_PRE_ROUTING   <- 标准 nftables 处理

这使得 netdev ingress 链可以:

  • 在协议栈解析之前丢弃无效包(减少 conntrack/NAT 开销)
  • 在 VLAN/PPPoE 解封装之前匹配原始以太网帧
  • 实现极低延迟的早期防火墙策略

15.5 NFPROTO_INET ingress hook

除了纯 netdev 协议族,NFPROTO_INET 还支持 NF_INET_INGRESS hook(nft_chain_filter.c 第 164-207 行),该 hook 在 netdev ingress 之后、ip_rcv 之前触发,能区分 IPv4 和 IPv6 并正确初始化 nft_pktinfo

static unsigned int nft_do_chain_inet_ingress(void *priv, struct sk_buff *skb,
                                               const struct nf_hook_state *state)
{
    struct nf_hook_state ingress_state = *state;

    switch (skb->protocol) {
    case htons(ETH_P_IP):
        ingress_state.pf   = NFPROTO_IPV4;
        ingress_state.hook = NF_INET_INGRESS;
        nft_set_pktinfo(&pkt, skb, &ingress_state);
        if (nft_set_pktinfo_ipv4_ingress(&pkt) < 0)
            return NF_DROP;
        break;
    case htons(ETH_P_IPV6):
        ingress_state.pf   = NFPROTO_IPV6;
        ingress_state.hook = NF_INET_INGRESS;
        // ...
    }
    return nft_do_chain(&pkt, priv);
}

16. pipapo 算法深度解析

net/netfilter/nft_set_pipapo.c 实现的 pipapo 算法是 nftables 集合子系统中最复杂的后端,专为多字段范围匹配(如防火墙策略中的四元组匹配)设计。

16.1 算法思路

pipapo 的核心思想是将多字段的范围匹配问题转化为多阶段的位图操作:

问题模型:
  entries:
    [src_net: 10.0.0.0/8,  dst_port: 80-443]  -> accept
    [src_net: 192.168.0.0/16, dst_port: 8080]  -> drop

匹配输入:
    src_ip = 10.1.2.3, dst_port = 443

pipapo 分解:
  字段 0(src_ip, 32位)-> 分为 8 组,每组 4 位
    查找表 T0[8 groups][16 buckets] -> 结果位图 R0

  字段 1(dst_port, 16位)-> 分为 4 组,每组 4 位
    查找表 T1[4 groups][16 buckets] -> 结果位图 R1

  最终:R0 AND R1 -> 匹配的规则集合

16.2 数据结构

// 每个字段对应一个 pipapo field
struct nft_pipapo_field {
    unsigned int    rules;        // 此字段的规则数(range 展开后可能是原始条目的数倍)
    unsigned int    rules_alloc;  // 已分配的规则槽位
    size_t          groups;       // 分组数(= 字段位数 / NFT_PIPAPO_GROUP_BITS)
    unsigned long   *lt;          // 查找表(lookup table),按 group 排列
    union nft_pipapo_map_bucket *mt; // 映射表(规则 -> 下一字段规则或元素)
    u8              bb;           // 每组位数(NFT_PIPAPO_GROUP_BITS = 4)
};

struct nft_pipapo_match {
    int             field_count;  // 字段数
    unsigned long   scratch_aligned; // 对齐的暂存位图
    size_t          bsize_max;    // 最大位图 word 大小
    struct nft_pipapo_field f[];  // 字段数组(变长)
};

16.3 查找过程

查找函数 pipapo_lookup() 的核心步骤(源码位于 nft_set_pipapo.c):

对于每个字段 f[i]:
  1. 初始化结果位图 res = ALL_ONES(全 1 位图)
  2. 对字段的每个 4-bit 分组 g:
       bucket = 从数据包字段提取第 g 组的 4 位值
       res = res AND T[g][bucket]    // 位图 AND 操作
  3. 若 res 全零:无匹配,返回 NULL
  4. 若有下一个字段:
       fill_map = {} (全零)
       对 res 中每个置位的 bit b:
           fill_map |= mt[b]  // 用映射表扩展到下一字段的规则集合
       res = fill_map(准备下一字段的初始位图)
  5. 若是最后一个字段:
       对 res 中置位的 bit b:
           return mt[b].e  // 返回对应元素指针

16.4 范围到网掩码的展开

范围 [a, b]pipapo_expand() 分解为若干个网掩码(依据 Rottenstreich 2010 定理),每个网掩码对应一条"规则"(rule)。这使得任意范围都可以被位图操作精确匹配。

例如端口范围 1000-1023(二进制 0111110100001111111111)被分解为两个网掩码:

  • 01111101xxx(1000-1007)
  • 0111111xxxx(1008-1023,实际上这里需更细分,仅示意)

分解后,每个网掩码的"wildcard bit"对应的所有 bucket 都会在查找表中被标记,从而覆盖整个范围。

16.5 AVX2 优化

net/netfilter/nft_set_pipapo_avx2.c 实现了 pipapo_avx2_lookup() 函数,使用 256-bit AVX2 指令一次处理 256 位的位图操作:

标准实现:  每次处理 64 位(unsigned long)
AVX2 实现:每次处理 256 位(_mm256_and_si256 等)

对于有 N 个规则的集合:
  标准:需要 ceil(N/64) 次 AND 操作
  AVX2:需要 ceil(N/256) 次 AND 操作

提速比:约 4x(在规则数足够多时)

启用条件:static_branch_unlikely(&pipapo_avx2_available) 检查 CPU 是否支持 AVX2,若支持则自动使用(避免在虚拟机或老旧 CPU 上非法指令错误)。


17. bitmap 集合实现

net/netfilter/nft_set_bitmap.c 实现了专为小型整数键集合优化的位图后端,适用于端口号(16 位)或协议号(8 位)等场景。

17.1 核心设计

位图集合用一个 u8 数组表示集合,每个元素用 2 位表示(用于双代机制),数组大小 = 2 * 2^klen bits / 8 字节:

struct nft_bitmap {
    struct list_head list;
    u16              bitmap_size;  // 字节数 = 2 * (1 << klen) / 8
    u8               bitmap[];    // 位图数组
};

每个键值 k 对应 bit 位 2*k(当前代)和 2*k+1(下一代),其 2-bit 组合编码了元素的双代可见状态(nft_set_bitmap.c 第 21-45 行):

元素状态(2-bit 编码,^ 表示当前代游标):
  11  = 在当前代和下一代都可见(稳定激活)  ^
  00  = 在当前代和下一代都不可见(稳定停用)^
  01  = 在当前代不可见,下一代可见(pending add)^
  10  = 在当前代可见,下一代不可见(pending del)^

17.2 查找实现

位图查找是 O(1) 操作,仅需计算 bit 位置并检查 2 位状态(nft_set_bitmap.c 第 77-93 行):

const struct nft_set_ext *
nft_bitmap_lookup(const struct net *net, const struct nft_set *set,
                  const u32 *key)
{
    const struct nft_bitmap *priv = nft_set_priv(set);
    static const struct nft_set_ext found;  // 静态伪 ext(bitmap 不存储扩展数据)
    u8 genmask = nft_genmask_cur(net);
    u32 idx, off;

    nft_bitmap_location(set, key, &idx, &off);  // 计算 bit 位置

    if (nft_bitmap_active(priv->bitmap, idx, off, genmask))
        return &found;  // 命中:返回静态 ext(外部只关心非 NULL)

    return NULL;
}

nft_bitmap_active() 是一个简单的位操作(第 71-75 行):

static inline bool nft_bitmap_active(const u8 *bitmap, u32 idx, u32 off,
                                      u8 genmask)
{
    // 提取 2-bit 组合,与 genmask 做 AND 判断当前代可见性
    return (bitmap[idx] & (0x3 << off)) & (genmask << off);
}

这使得端口集合的匹配性能极高:一次位操作完成,无哈希计算,无内存分配,完全 cache-line 局部。

17.3 性能与限制

  • 查找:O(1),单次内存访问(小位图可完整在 L1 cache)
  • 键长度限制:最多 16 位(65536 个元素,位图 16 KB)
  • 不支持超时和元素级表达式(简化设计)
  • 双代机制通过每元素 2 位内嵌实现,无需额外 genmask 字段

18. nftables 与 conntrack 深度集成

nftables 与连接跟踪(conntrack)的集成远超简单的状态匹配,两者在多个层次深度协作。

18.1 conntrack 启动时机

conntrack 的创建由 nf_conntrack_in() 在 PREROUTING 和 OUTPUT hook 触发,nftables 规则可以通过以下方式影响 conntrack 行为:

ct expression 与 conntrack 交互层次:

Layer 1: 读取 ct 状态(NFT_CT_STATE)
  -> nf_ct_get(skb, &ctinfo) 读取已建立的连接状态

Layer 2: 修改 ct 标记(NFT_CT_MARK set)
  -> WRITE_ONCE(ct->mark, val)
  -> 替代 iptables CONNMARK --set-mark

Layer 3: 连接标签(NFT_CT_LABELS set)
  -> nf_conn_labels 扩展 128 位标签
  -> 替代 iptables CONNLABEL

Layer 4: 连接 zone(NFT_CT_ZONE set)
  -> 在 PREROUTING 阶段设置 zone ID
  -> 实现多租户 conntrack 隔离

Layer 5: dynset + ct(连接速率追踪)
  -> nft_dynset 动态插入集合元素
  -> 元素级 counter/limit 表达式统计每连接流量

18.2 ct zone 与多租户隔离

NFT_CT_ZONE 的写操作使用 per-CPU 模板连接(nft_ct.c 第 33-37 行中的 nft_ct_pcpu_template),在 PREROUTING 阶段为包分配带有特定 zone ID 的连接,实现不同租户的流量在不同 conntrack zone 中追踪(避免不同 VPN 用户的连接表互相干扰)。

18.3 conntrack 超时与 flowtable 协作

当连接被卸载到 flowtable 后,conntrack 不再接收该连接的数据包,若不处理则 conntrack 条目会因超时被删除(导致 flowtable 条目孤立)。解决方案(nf_flow_table_core.c 第 505-556 行):

static void nf_flow_table_extend_ct_timeout(struct nf_conn *ct)
{
    // 若 ct 超时时间 < min_timeout(5分钟),且 ct 仍被 flowtable 持有
    if (nf_ct_is_confirmed(ct) &&
        test_bit(IPS_OFFLOAD_BIT, &ct->status)) {
        // TCP 已建立:延长到 NF_CT_DAY(86400 秒)
        // UDP 已回复:延长到 NF_CT_DAY
        cmpxchg(&ct->timeout, expires, new_timeout);
    }
}

使用 cmpxchg 原子操作避免与 conntrack 数据路径的竞态:若 conntrack 数据路径同时更新 ct->timeout,以数据路径为准("数据路径是权威的")。

18.4 ct fast_eval 优化

对于最常见的 NFT_CT_STATENFT_CT_MARK 读操作,内核提供了 nft_ct_get_fast_eval() 快速路径(通过 Retpoline 规避机制直接调用),绕过完整的 switch 语句(nft_ct.c 中通过宏生成)。

18.5 ct helper 对象

nftables 支持通过 nft_ct_helper_obj 有状态对象(nft_ct.c 第 27-31 行)在数据平面动态设置 conntrack helper:

struct nft_ct_helper_obj {
    struct nf_conntrack_helper *helper4;  // IPv4 helper
    struct nf_conntrack_helper *helper6;  // IPv6 helper
    u8 l4proto;  // IPPROTO_TCP 或 IPPROTO_UDP
};

这允许在 nft 规则中用 ct helper set "ftp" 替代 iptables 的 -j CT --helper ftp,在运行时为连接关联 ALG helper(FTP/RTSP/SIP 等)。


19. flowtable 快速路径完整流程

本节从源码层面完整梳理 flowtable 数据包处理流程。

19.1 流表项的生命周期

生命周期状态机:

  [NOT_IN_FLOWTABLE]
      |
      | nft 规则触发:add @flowtable { ... }
      | -> flow_offload_alloc() 分配 flow
      | -> flow_offload_route_init() 填充路由信息
      | -> flow_offload_add() 插入 rhashtable
      v
  [ACTIVE]   <--------- 定期超时刷新 flow_offload_refresh()
      |
      |  TCP FIN/RST 到达
      +-> set_bit(NF_FLOW_CLOSING, &flow->flags)
      |   (nf_flow_state_check, nf_flow_table_ip.c:34-43)
      |
      |  超时 或 ct 即将过期
      +-> flow_offload_teardown()
              |
              v
          [TEARDOWN]
              |
              | flow_offload_fixup_ct() 恢复 conntrack 超时
              |
              v
          nf_flow_offload_gc_step() -> flow_offload_del()
              |
              v
          [DELETED]   flow_offload_free() -> kfree_rcu()

19.2 快速路径包处理详解

IPv4 flowtable 快速路径(nf_flow_table_ip.c 第 497-545 行)的 nf_flow_offload_forward() 函数:

static int nf_flow_offload_forward(struct nf_flowtable_ctx *ctx,
                                    struct nf_flowtable *flow_table,
                                    struct flow_offload_tuple_rhash *tuplehash,
                                    struct sk_buff *skb)
{
    dir = tuplehash->tuple.dir;
    flow = container_of(tuplehash, struct flow_offload, tuplehash[dir]);

    // 1. MTU 检查(考虑封装层偏移)
    mtu = flow->tuplehash[dir].tuple.mtu + ctx->offset;
    if (unlikely(nf_flow_exceeds_mtu(skb, mtu)))
        return 0;  // 回退到慢路径(触发分片)

    // 2. TCP 状态检查(FIN/RST/SYN 处理)
    iph = (struct iphdr *)(skb_network_header(skb) + ctx->offset);
    thoff = (iph->ihl * 4) + ctx->offset;
    if (nf_flow_state_check(flow, iph->protocol, skb, thoff))
        return 0;

    // 3. 目标路由有效性检查
    if (!nf_flow_dst_check(&tuplehash->tuple)) {
        flow_offload_teardown(flow);  // 路由变化,拆卸流表项
        return 0;
    }

    // 4. 确保 skb 可写(COW)
    if (skb_try_make_writable(skb, thoff + ctx->hdrsize))
        return -1;

    // 5. 刷新超时
    flow_offload_refresh(flow_table, flow, false);

    // 6. 剥离封装层(VLAN/PPPoE/隧道)
    nf_flow_encap_pop(ctx, skb, tuplehash);
    thoff -= ctx->offset;

    // 7. NAT 处理(SNAT/DNAT IP 和端口)
    iph = ip_hdr(skb);
    nf_flow_nat_ip(flow, skb, thoff, dir, iph);

    // 8. TTL 递减
    ip_decrease_ttl(iph);
    skb_clear_tstamp(skb);

    // 9. 可选:更新 conntrack 计数
    if (flow_table->flags & NF_FLOWTABLE_COUNTER)
        nf_ct_acct_update(flow->ct, tuplehash->tuple.dir, skb->len);

    return 1;  // 成功,由调用者完成实际发送
}

19.3 NAT 处理在快速路径中的实现

flow->flags 包含 NF_FLOW_SNATNF_FLOW_DNAT 时,快速路径直接修改 IP 和 TCP/UDP 头(nf_flow_table_ip.c 第 83-141 行):

static void nf_flow_snat_ip(const struct flow_offload *flow,
                             struct sk_buff *skb, struct iphdr *iph,
                             unsigned int thoff, enum flow_offload_tuple_dir dir)
{
    switch (dir) {
    case FLOW_OFFLOAD_DIR_ORIGINAL:
        // 正向包:源地址替换为 REPLY 方向的目的地址
        addr = iph->saddr;
        new_addr = flow->tuplehash[FLOW_OFFLOAD_DIR_REPLY].tuple.dst_v4.s_addr;
        iph->saddr = new_addr;
        break;
    case FLOW_OFFLOAD_DIR_REPLY:
        // 反向包:目的地址恢复为 ORIGINAL 方向的源地址
        addr = iph->daddr;
        new_addr = flow->tuplehash[FLOW_OFFLOAD_DIR_ORIGINAL].tuple.src_v4.s_addr;
        iph->daddr = new_addr;
        break;
    }
    csum_replace4(&iph->check, addr, new_addr);  // IP checksum 增量更新
    nf_flow_nat_ip_l4proto(skb, iph, thoff, addr, new_addr);  // L4 checksum 更新
}

NAT 的 IP/TCP/UDP checksum 全部通过增量更新算法(csum_replace4/inet_proto_csum_replace4)高效完成,无需重新计算整个包的校验和。

19.4 VLAN/PPPoE/隧道封装处理

flowtable 支持穿越 VLAN 和 PPPoE 封装的转发加速。nf_flow_encap_pop() 在快速路径中剥离封装层(nf_flow_table_ip.c 第 433-463 行):

static void nf_flow_encap_pop(struct nf_flowtable_ctx *ctx,
                               struct sk_buff *skb,
                               struct flow_offload_tuple_rhash *tuplehash)
{
    for (i = 0; i < tuplehash->tuple.encap_num; i++) {
        if (skb_vlan_tag_present(skb)) {
            __vlan_hwaccel_clear_tag(skb);
            continue;
        }
        switch (skb->protocol) {
        case htons(ETH_P_8021Q):
            vlan_hdr = (struct vlan_hdr *)skb->data;
            __skb_pull(skb, VLAN_HLEN);
            vlan_set_encap_proto(skb, vlan_hdr);
            break;
        case htons(ETH_P_PPP_SES):
            skb->protocol = __nf_flow_pppoe_proto(skb);
            skb_pull(skb, PPPOE_SES_HLEN);
            break;
        }
    }
    // 处理 IP-in-IP 隧道
    nf_flow_ip_tunnel_pop(ctx, skb);
}

相应地,在出方向需要重新添加封装(nf_flow_pppoe_push() 等),确保转发后的包格式与原始路径一致。


20. nftables 事务机制完整分析

20.1 Netlink 批处理与事务边界

用户空间通过 NFNL_MSG_BATCH_BEGIN / NFNL_MSG_BATCH_END 标记事务边界,内核在收到 BATCH_END 时调用 nf_tables_commit() 提交整个批次(nf_tables_api.c):

用户空间(nft 命令)         内核(nfnetlink)
==================          ===================
send BATCH_BEGIN
send NFT_MSG_NEWTABLE       -> nf_tables_newtable()
                               nft_trans_table_add() 入 commit_list
send NFT_MSG_NEWCHAIN       -> nf_tables_newchain()
                               nft_trans_chain_add() 入 commit_list
send NFT_MSG_NEWRULE[0]     -> nf_tables_newrule()
                               nft_trans_rule_add() 入 commit_list
send NFT_MSG_NEWRULE[1]
...
send BATCH_END              -> nf_tables_commit()
                               [原子提交所有操作]

若中途任何操作失败,所有已入队的事务将在 nf_tables_abort() 中回滚,内核状态不受影响。

20.2 事务的原子性保证

原子性由以下机制共同保证:

  1. commit_mutex 互斥:同时只有一个批处理在进行,其他批处理排队等待
  2. 双代机制:提交前新对象在旧代不可见,提交瞬间切换代,无中间状态
  3. genmask 字段:每个可见性相关对象(表/链/规则/集合元素)都有 2-bit genmask,在提交前控制其在新旧代的可见性

20.3 回滚机制

nf_tables_abort()nf_tables_api.c)逆序遍历 commit_list,对每种类型的事务执行相应的撤销操作:

NFT_MSG_NEWRULE:
    nft_rule_del() 从链表删除规则
    call_rcu() 延迟释放规则内存

NFT_MSG_DELRULE:
    恢复规则的 genmask(使其在当前代重新可见)

NFT_MSG_NEWSET:
    nft_set_destroy() 销毁集合及所有元素

NFT_MSG_NEWTABLE:
    nft_table_destroy()

NFT_MSG_NEWSETELEM:
    nft_set_elem_destroy() 删除已添加的元素

20.4 提交阶段的 blob 编译

nf_tables_commit_chain_prepare() 中,nft_rule 链表被"编译"为 nft_rule_blob,这是提交阶段最复杂的操作:

编译流程:

1. 统计所有规则的总大小
   total_size = sum(sizeof(nft_rule_dp) + rule->dlen for rule in rules)

2. kvmalloc 分配连续内存块

3. 遍历规则链表:
   for each rule:
     a. 复制 nft_rule_dp 头(is_last=0, dlen=rule->dlen, handle=rule->handle)
     b. 复制表达式字节码(rule->data[0..dlen])
     c. 对每个表达式尝试 reduce 优化(消除冗余寄存器加载)
     d. 对某些表达式选择 fast_ops 变体(nft_cmp_select_ops 等)

4. 写入终止哨兵(is_last=1)

5. RCU 安全地替换 blob_gen_X

20.5 审计日志

每次成功提交后,nftables 通过 audit_log_nfcfg() 向内核审计子系统记录配置变更(nf_tables_api.c 中的审计映射表,第 83-110 行),包含操作类型、表名、规则句柄等信息,满足安全审计要求(CONFIG_AUDIT 开启时)。


21. nftables 调试与追踪

21.1 nft trace 机制

nftables 内置了强大的规则追踪(trace)功能,允许在不重新加载规则的情况下观察数据包的规则匹配过程。

启用追踪的方式:

# 用户空间设置 skb->nf_trace 标志
nft add rule ip filter input meta nftrace set 1
# 监听追踪事件
nft monitor trace

内核实现(nf_tables_core.c 第 41-72 行):

// 追踪功能通过静态键控制开销
DEFINE_STATIC_KEY_FALSE(nft_trace_enabled);

// 每个规则执行后,若 nf_trace=1 则发送 Netlink 事件
static inline void nft_trace_packet(...)
{
    if (static_branch_unlikely(&nft_trace_enabled)) {
        info->nf_trace = pkt->skb->nf_trace;
        __nft_trace_packet(pkt, verdict, rule, info, type);
    }
}

追踪信息通过 nft_trace_notify() 发送 NFT_MSG_TRACE Netlink 消息,包含:

  • 当前正在执行的表名、链名、规则句柄
  • 裁决类型(RULE/RETURN/POLICY)
  • 如果有终止裁决,包含其值

21.2 traceinfo 结构

struct nft_traceinfo {
    bool                 trace;     // 是否启用追踪
    bool                 nf_trace;  // skb->nf_trace 缓存值
    bool                 packet_dumped; // 是否已发送包内容
    enum nft_trace_types type;      // RULE/RETURN/POLICY
    const struct nft_base_chain *basechain;
};

nft_trace_init()nft_do_chain 入口处初始化该结构,nft_trace_verdict() 在每条规则产生终止裁决时发送事件(nf_tables_core.c 第 135-142 行)。

21.3 chain 级别统计

nft_base_chain->stats 是 per-CPU 的 nft_stats 计数器,在链执行完毕(默认策略应用前)更新(nf_tables_core.c 第 178-197 行):

static noinline void nft_update_chain_stats(const struct nft_chain *chain,
                                             const struct nft_pktinfo *pkt)
{
    base_chain = nft_base_chain(chain);
    pstats = READ_ONCE(base_chain->stats);
    if (pstats) {
        local_bh_disable();
        stats = this_cpu_ptr(pstats);
        u64_stats_update_begin(&stats->syncp);
        stats->pkts++;
        stats->bytes += pkt->skb->len;
        u64_stats_update_end(&stats->syncp);
        local_bh_enable();
    }
}

使用 u64_stats_sync 保证 32 位平台上的 64 位计数器读取一致性(避免高低 32 位字撕裂)。


22. nftables 网络命名空间支持

22.1 每命名空间独立状态

nftables 完整支持 Linux 网络命名空间(network namespace)隔离。每个 net 结构通过 nftables_pernet 私有数据维护独立的表/链/规则集,不同命名空间中的规则集完全隔离、互不干扰。

nf_tables_net_id 是通过 register_pernet_subsys() 注册的 pernet ID,nft_pernet(net) 返回该命名空间的 nftables 状态:

// nf_tables_api.c 中
static struct pernet_operations nf_tables_net_ops = {
    .init    = nf_tables_init_net,
    .pre_exit = nf_tables_pre_exit_net,
    .exit    = nf_tables_exit_net,
    .id      = &nf_tables_net_id,
    .size    = sizeof(struct nftables_pernet),
};

22.2 命名空间销毁流程

当网络命名空间销毁时,nf_tables_exit_net() 清理所有规则:

nf_tables_pre_exit_net():
  - 遍历所有表,注销其基础链的 hook(通过 nf_tables_unregister_hook)
  - 此时 hook 已注销,数据平面不再调用 nft_do_chain

nf_tables_exit_net():
  - 遍历所有表中的链/规则/集合/对象,逐一销毁
  - 释放事务相关内存(commit_list/destroy_list)
  - 确保所有 RCU 延迟释放完成(synchronize_rcu_expedited)

22.3 netns 与 flowtable

flowtable 同样按命名空间隔离。nf_flow_table_core.c 中的全局 flowtables 链表(第 18 行 LIST_HEAD(flowtables))在历史版本中曾是全局共享的,现已改为通过 net 字段(nf_flowtable->net)关联到各自命名空间,确保跨 netns 的 flowtable 不共享任何状态。


23. nftables 有状态对象系统

23.1 nft_object 抽象

nftables 的"有状态对象"(stateful objects)允许多条规则共享同一个状态实例(如共享一个计数器或速率限制器)。对象通过 nft_object 抽象(nf_tables.h 第 1073 行):

struct nft_object {
    struct list_head    list;       // 链入 table->objects
    struct rhlist_head  rhlhead;    // 对象名哈希
    struct nft_table   *table;
    char               *name;
    u64                 handle;
    u32                 use;        // 被多少 objref 表达式引用
    u16                 udlen;
    u8                 *udata;
    const struct nft_object_type *ops;  // 操作表(eval/init/destroy/dump)
    unsigned char       data[]         // 对象私有数据
        __attribute__((aligned(__alignof__(u64))));
};

23.2 内置对象类型

nf_tables_core_module_init() 中注册的基础对象类型(nf_tables_core.c 第 368-373 行):

static struct nft_object_type *nft_basic_objects[] = {
#ifdef CONFIG_NETWORK_SECMARK
    &nft_secmark_obj_type,   // SELinux secmark 标记
#endif
    &nft_counter_obj_type,   // 计数器(共享状态)
};

外部模块还可注册:

  • nft_quota_obj_type:流量配额对象
  • nft_limit_obj_type:速率限制对象(令牌桶)
  • nft_synproxy_obj_type:TCP SYN proxy 对象
  • nft_ct_helper_obj_type:conntrack helper 绑定
  • nft_ct_timeout_obj_type:conntrack 超时策略

23.3 objref 表达式

nft_objref 表达式(net/netfilter/nft_objref.c)通过名称查找有状态对象并调用其 eval 函数,实现规则与对象的解耦:

规则 1: payload load IP src -> reg1
        cmp reg1 == 192.168.1.1
        objref "counter_web"  <- 引用共享计数器

规则 2: payload load IP src -> reg1
        cmp reg1 == 10.0.0.1
        objref "counter_web"  <- 同一个计数器,两条规则共享计数

对象:
  nft add counter ip filter counter_web

对象在提交时通过 nft_tableobjects 链表管理,nft_trans_obj 事务节点保证对象创建/删除的原子性。


24. nftables 内核模块初始化流程

24.1 模块加载顺序

nftables 相关模块的初始化依赖顺序(通过 module_init / module_exit 管理):

1. nf_tables(核心框架)
   nf_tables_module_init()
     -> nf_tables_core_module_init()    注册基础 expr 类型和对象类型
     -> register_pernet_subsys()        注册命名空间子系统
     -> nfnetlink_subsys_register()     注册 Netlink 子系统

2. nft_chain_filter(链类型)
   nft_chain_filter_init()
     -> nft_chain_filter_ipv4_init()    注册 IPv4 filter 链类型
     -> nft_chain_filter_ipv6_init()    注册 IPv6 filter 链类型
     -> nft_chain_filter_arp_init()     注册 ARP filter 链类型
     -> nft_chain_filter_bridge_init()  注册 bridge filter 链类型
     -> nft_chain_filter_inet_init()    注册 inet (dual-stack) filter 链类型
     -> nft_chain_filter_netdev_init()  注册 netdev filter 链类型 + 设备事件

3. nf_flow_table(flowtable 核心)
   nf_flow_table_module_init()
     -> flow_offload_cachep 初始化(kmem_cache_create)
     -> nf_flow_table_inet_module_init()  注册 inet flowtable 类型

4. 可选外部模块
   nft_compat.ko                         xtables 兼容层
   nft_nat.ko                            NAT 表达式
   nft_ct.ko                             conntrack 表达式
   nft_set_rbtree.ko                     红黑树集合后端
   nft_set_hash.ko                       哈希集合后端
   nft_set_pipapo.ko                     pipapo 集合后端

24.2 nf_tables_core_module_init 详解

nf_tables_core_module_init()nf_tables_core.c 第 375-405 行)是最核心的初始化函数:

int __init nf_tables_core_module_init(void)
{
    int err, i, j = 0;

    nft_counter_init_seqcount();  // 初始化 per-CPU seqcount(counter 并发保护)

    // 注册基础对象类型(counter、secmark)
    for (i = 0; i < ARRAY_SIZE(nft_basic_objects); i++) {
        err = nft_register_obj(nft_basic_objects[i]);
        if (err) goto err;
    }

    // 注册基础表达式类型(cmp/payload/meta/lookup/...)
    for (j = 0; j < ARRAY_SIZE(nft_basic_types); j++) {
        err = nft_register_expr(nft_basic_types[j]);
        if (err) goto err;
    }

    // Retpoline 优化:若 CPU 不需要 Retpoline,启用直接调用优化
    nf_skip_indirect_calls_enable();

    return 0;
    // ...
}

nf_skip_indirect_calls_enable()(第 32-36 行)检查 cpu_feature_enabled(X86_FEATURE_RETPOLINE),若 CPU 本身已修复间接跳转预测漏洞,则开启 nf_tables_skip_direct_calls 静态键,使 expr_call_ops_eval() 展开为直接调用序列。


25. 总结

核心设计哲学

nftables 的设计体现了 Linux 内核网络栈的几个核心原则:

  1. 数据/控制平面分离:规则存储(nft_rule 链表)与执行(nft_rule_dp blob)完全分离,控制平面的变更不影响数据平面的执行,两者通过 RCU 双代机制解耦

  2. 可扩展性优于内置功能:表达式系统(nft_expr_ops)和集合后端(nft_set_ops)都是插件化接口,新功能通过注册新模块实现,内核核心代码无需修改

  3. 性能工程:从规则 blob 的连续内存布局、fast ops 内联、per-CPU 统计,到 flowtable bypass 路径和硬件卸载,每个层次都有针对性的性能优化

  4. 正确性优先:事务机制确保配置原子性;RCU 保证数据平面无锁;严格的提交前验证防止无效规则集进入数据路径

关键源码位置速查

功能 文件 关键符号
执行核心 net/netfilter/nf_tables_core.c nft_do_chain() 第 250 行
核心数据结构 include/net/netfilter/nf_tables.h nft_table(1313),nft_chain(1144),nft_rule(1004),nft_expr(413),nft_set(587)
Netlink API net/netfilter/nf_tables_api.c nf_tables_commit() 第 10791 行
payload 表达式 net/netfilter/nft_payload.c nft_payload_eval() 第 159 行
payload 快速路径 net/netfilter/nf_tables_core.c nft_payload_fast_eval() 第 144 行
ct 表达式 net/netfilter/nft_ct.c nft_ct_get_eval() 第 51 行
xtables 兼容 net/netfilter/nft_compat.c nft_target_eval_xt() 第 73 行
哈希集合 net/netfilter/nft_set_hash.c nft_rhash_lookup() 第 84 行,nft_rhash_update() 第 126 行
红黑树集合 net/netfilter/nft_set_rbtree.c nft_rbtree_lookup() 第 107 行,nft_array 第 24 行
bitmap 集合 net/netfilter/nft_set_bitmap.c nft_bitmap_lookup() 第 77 行,双代 2-bit 编码
pipapo 集合 net/netfilter/nft_set_pipapo.c DOC 注释第 10 行,pipapo 算法详解
flowtable 核心 net/netfilter/nf_flow_table_core.c flow_offload_add() 第 323 行,nf_flow_offload_gc_step() 第 558 行
flowtable IP 快路 net/netfilter/nf_flow_table_ip.c nf_flow_offload_forward() 第 497 行
netdev 链类型 net/netfilter/nft_chain_filter.c nft_chain_filter_netdev 第 309 行,nf_tables_netdev_notifier 第 431 行
UAPI 定义 include/uapi/linux/netfilter/nf_tables.h nft_registers(第 22 行),nft_verdicts(第 64 行)
flowtable 数据结构 include/net/netfilter/nf_flow_table.h nf_flowtable(第 76 行)

数据结构层次速查

nftables_pernet (per-netns)
  |
  +-- tables (list)
  |     |
  |     +-- nft_table (family/name/flags)
  |           |
  |           +-- chains (list + chains_ht)
  |           |     |
  |           |     +-- nft_chain (blob_gen_0/1, rules)
  |           |     |     |
  |           |     |     +-- nft_rule (genmask, dlen, data[])
  |           |     |           |
  |           |     |           +-- nft_expr (ops, data[])
  |           |     |                 |
  |           |     |                 +-- nft_expr_ops (eval/init/dump/...)
  |           |     |
  |           |     +-- nft_base_chain (nf_hook_ops, policy, stats, hook_list)
  |           |
  |           +-- sets (list)
  |           |     |
  |           |     +-- nft_set (ops, klen, dlen, timeout, nelems)
  |           |           |
  |           |           +-- backend: nft_rhash / nft_rbtree / nft_bitmap / nft_pipapo
  |           |           |
  |           |           +-- elements: nft_set_ext (key, data, timeout, flags)
  |           |
  |           +-- objects (list)
  |           |     |
  |           |     +-- nft_object (counter / limit / quota / ct_helper / ...)
  |           |
  |           +-- flowtables (list)
  |                 |
  |                 +-- nf_flowtable (rhashtable, gc_work, flow_block)
  |                       |
  |                       +-- flow_offload (tuplehash[2], ct, flags, timeout)
  |
  +-- commit_list (pending transactions)
        |
        +-- nft_trans (msg_type, table, seq)
              +-- nft_trans_rule / nft_trans_chain / nft_trans_set / ...

由 Claude Code 分析生成