基于 Linux kernel master 分支源码分析 主要参考文件:
net/netfilter/nf_tables_core.cinclude/net/netfilter/nf_tables.hnet/netfilter/nf_tables_api.cnet/netfilter/nft_payload.cnet/netfilter/nft_set_hash.cnet/netfilter/nft_set_rbtree.cnet/netfilter/nft_set_bitmap.cnet/netfilter/nft_set_pipapo.cnet/netfilter/nft_ct.cnet/netfilter/nft_compat.cnet/netfilter/nft_chain_filter.cnet/netfilter/nf_flow_table_core.cnet/netfilter/nf_flow_table_ip.cinclude/uapi/linux/netfilter/nf_tables.hinclude/net/netfilter/nf_flow_table.h
- nftables 概述与历史背景
- nftables 与 iptables 的对比
- 整体架构图
- 核心数据结构
- 表达式(expr)系统详解
- nft 虚拟机执行引擎
- 集合(set)子系统
- Netlink 事务接口
- Hooks 注册与 Netfilter 集成
- 连接跟踪(ct)表达式深入分析
- flowtable 软件加速
- 安全性与性能设计
- nft_compat:iptables 兼容层深度分析
- nftables 集合动态更新与超时机制
- nftables netdev 表与 ingress/egress hook
- pipapo 算法深度解析
- bitmap 集合实现
- nftables 与 conntrack 深度集成
- flowtable 快速路径完整流程
- nftables 事务机制完整分析
- nftables 调试与追踪
- nftables 网络命名空间支持
- nftables 有状态对象系统
- 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)系统和虚拟机消除了这种重复。
iptables 架构(旧) nftables 架构(新)
+---------------------------+ +-------------------------------+
| ip_tables (IPv4) | | |
| ip6_tables (IPv6) | | nf_tables (统一框架) |
| arp_tables (ARP) | | |
| eb_tables (bridge) | | 表达式虚拟机执行引擎 |
| | | |
| 各自的 match/target 模块 | | pluggable expr 模块 |
+---------------------------+ +-------------------------------+
| |
+---------------+ +---------------------+
| 内核固化的 | | 通用 Netlink 事务 |
| iptables 命令 | | 接口(nfnetlink) |
+---------------+ +---------------------+
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() 回调,操作共享寄存器组
| 特性 | iptables | nftables |
|---|---|---|
| 协议族支持 | 各协议族独立实现 | 统一框架,family 参数区分 |
| 规则格式 | 固定结构体 | 虚拟机字节码(表达式链) |
| 多值匹配 | 需要多条规则或 ipset | 内置 set 子系统 |
| 配置原子性 | 非原子(iptables-restore 近似) | 完全原子(Netlink 事务) |
| 内核 API | setsockopt(IP_SET_REPLACE) | nfnetlink(Netlink 子系统) |
| 计数器统计 | 每条规则固定计数 | 可选的 counter 表达式,或有状态对象 |
| 动态规则更新 | 重加载整个表 | 单条规则增删,原子提交 |
| 并发安全 | 整表锁 | RCU + 双代(generation)机制 |
用户空间 内核空间
========== ==========================================
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 (位并联) |
+------------------------+
定义在 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 -> ...
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_0 和 blob_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() 辅助函数)。
控制平面规则 nft_rule(nf_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_dp(nf_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 预取友好,无链表指针跳转
表达式是 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))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) // = 16nft_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,驱动执行引擎的下一步动作
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 行)。
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);
}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 指针和私有数据)。
每种表达式(如 payload、meta、ct)对应一个 nft_expr_type(nf_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(隧道内层报文处理)
};实现在 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 = ®s->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 = ®s->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 属性区分读/写语义。
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->skb 或 pkt->state 中读取字段,写入目标寄存器,开销极低。
详见第 10 节和第 18 节。
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 保护更新。
NAT 表达式(net/netfilter/nft_nat.c)工作在 NFT_CHAIN_T_NAT 类型的链上,通过调用 nf_nat_setup_info() 修改连接跟踪的 NAT 映射,并根据寄存器中的地址/端口值完成动态 SNAT/DNAT。
与 iptables 的 MASQUERADE/DNAT target 不同,nftables 的 nat 表达式通过寄存器传递目标地址/端口,允许使用 map 集合实现地址映射表,写法更加灵活。
cmp 表达式(net/netfilter/nft_cmp.c)是执行寄存器与常量比较的基础表达式。内核实现了三种变体以适应不同场景:
通用 cmp(nft_cmp_eval):支持任意长度的 memcmp 比较,支持 == 和 != 操作符,适合 IPv6 地址(16 字节)等大型键的比较。
fast_ops cmp(nft_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_ops(nft_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 *)®s->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() 在初始化时根据比较长度和对齐要求自动选择最优变体。
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, ®s);
else if (expr->ops == &nft_cmp16_fast_ops)
nft_cmp16_fast_eval(expr, ®s);
else if (expr->ops == &nft_bitwise_fast_ops)
nft_bitwise_fast_eval(expr, ®s);
else if (expr->ops != &nft_payload_fast_ops ||
!nft_payload_fast_eval(expr, ®s, pkt))
expr_call_ops_eval(expr, ®s, 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)
裁决码定义在 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=1、NF_DROP=0 等)不重叠,nft_do_chain 通过 NF_VERDICT_MASK(0x000000ff)区分二者。
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
执行子链 -> 子链策略 -> 直接返回(不回到原链)
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 计数时才启用该静态分支,无统计场景下完全零开销。
nft_do_chain 维护一个最大深度为 16(NFT_JUMP_STACK_SIZE,nf_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_DROP(nf_tables_core.c 第 317-318 行),防止无限递归导致栈溢出。
集合是 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)(...);
// ...
};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;
}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)
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 节。
集合元素的键、值、超时等可选字段通过 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() 等内联函数提供类型安全的访问接口。
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;
}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_subsys 的 commit/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,
};每个 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 在批处理期间积累所有待处理事务,提交时统一执行,回滚时全部撤销。
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);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.genmask,nft_rhash_cmp() 等查找函数通过当前代掩码进行可见性过滤(nft_set_hash.c 第 59-74 行)。
只有"基础链"(base chain)才能挂载到 Netfilter Hook。nft_base_chain 内嵌 nf_hook_ops 结构(nf_tables.h 第 1246 行),ops.hook 回调对于 filter 类型就是 nft_do_chain。
三种内建链类型(enum nft_chain_types,nf_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);
};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 列表。
ct(connection tracking)表达式实现在 net/netfilter/nft_ct.c,允许规则匹配连接状态、标记、NAT 映射等信息。
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 = ®s->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 等
}
}| 键(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 | 元组中的源/目的地址 |
ct 表达式不仅支持读取,还支持写入(nft_ct_set_eval()),例如:
- 设置
NFT_CT_MARK:ct->mark = regs->data[priv->sreg] - 设置
NFT_CT_LABELS:修改nf_conn_labels扩展 - 设置
NFT_CT_ZONE:在 PREROUTING 阶段将包放入特定连接跟踪 zone
这使得 nftables 的 ct 表达式可以完整替代 iptables 的 CONNMARK/CONNLABEL 模块。
flowtable 是 nftables 提供的"软件加速"(fast path)机制,允许已建立的 TCP/UDP 连接绕过完整的 Netfilter/nf_tables 处理路径,直接在 PREROUTING hook 完成转发。
定义在 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 字段位于结构体开头(紧接 flags 和 priority 只读字段),将哈希表控制块尽量靠前以改善 NUMA 局部性。
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;
}正常路径(无 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 状态。
当设置 NF_FLOWTABLE_HW_OFFLOAD 标志时,flowtable 通过 flow_block(TC block 机制)将流表规则推送到支持的网卡驱动,实现真正的硬件转发卸载,CPU 完全不参与数据包处理。
nf_flowtable_type 接口的 action() 回调负责生成驱动可理解的 nf_flow_rule 描述,setup() 回调管理驱动的块绑定/解绑。
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 行)。
在启用 Retpoline 的内核中(CONFIG_MITIGATION_RETPOLINE),nf_tables_core.c 第 24-39 行使用 static_key 机制:当 CPU 不需要 Retpoline(X86_FEATURE_RETPOLINE 不存在)时,expr_call_ops_eval() 展开为一系列直接函数调用,完全避免了间接跳转的推测执行漏洞缓解开销。
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;
}提交前的 nf_tables_validate() 执行:
- 循环检测:通过
nft_chain_validate_state.depth跟踪调用深度(最大 16),检测 JUMP/GOTO 循环 - 目标存在性:验证所有 JUMP/GOTO 目标链存在且在同一表中
- 绑定检查:匿名集合和 binding chain 必须被至少一条规则引用(防止悬空对象),违规时输出警告(
nf_tables_api.c第 10820-10821 行):pr_warn_once("nftables ruleset with unbound set\n")
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)
net/netfilter/nft_compat.c 实现了 nftables 对 iptables/ip6tables/ebtables/arptables 扩展模块的兼容适配层,允许现有的 xtables match/target 模块在 nftables 框架下工作,无需重写。
nft_compat 将每个 xtables match 或 target 包装为一个 nft_expr。关键的 eval 函数调用原始的 xtables 模块回调,并将其裁决码转换为 nftables 裁决码。
对于普通 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 行)。
对于 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;
}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 文件创建/销毁的竞态。
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);
// ...
}nft_compat 使用 nft_request_module() 按需加载 xtables 扩展模块。若 xt_check_target() 返回 -ENOENT,对于已知模块(如 LOG -> nf_log_syslog,NFLOG -> nfnetlink_log)自动触发 request_module(),返回 -EAGAIN 让用户空间重试(第 270-283 行)。
集合元素的超时通过 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 比较确定是否过期。这避免了每次比较都需要读取当前时间,只在元素创建时计算一次。
哈希集合 GC(nft_set_hash.c 中的 nft_rhash_gc_work):使用延迟工作队列(delayed_work),定期(gc_int 毫秒)扫描哈希表中的元素,对超时元素:
- 调用
nft_setelem_data_deactivate()在当前代停用元素 - 通过 Netlink 发送
NFT_MSG_DELSETELEM事件(若有监听者) - 调用
nft_set_elem_destroy()+ RCU 延迟释放内存
红黑树集合 GC(nft_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);
}通过 nft_dynset 表达式动态创建的元素默认继承集合的 timeout 值。若规则中指定了 NFT_DYNSET_OP_ADD + NFT_SET_EXT_TIMEOUT,则使用规则指定的超时覆盖默认值。动态元素在创建时通过 nft_dynset_new() 分配并初始化,超时字段写入当前时间 + 超时间隔。
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() 报告自己对给定参数的适用性(内存占用估算和查找时间复杂度权重),内核综合这些信息选出最优后端。
NFPROTO_NETDEV 是 nftables 专为网络设备层引入的协议族,支持在 TC 层之前的 NF_NETDEV_INGRESS 和 NF_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,
},
};nft_do_chain_netdev() 能够感知 skb 的以太网协议类型,并相应初始化 nft_pktinfo(nft_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 统一框架的优势。
netdev 基础链通过 nft_base_chain->hook_list(nft_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_notifier(nft_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_REGISTER、NETDEV_UNREGISTER、NETDEV_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)
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 解封装之前匹配原始以太网帧
- 实现极低延迟的早期防火墙策略
除了纯 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);
}net/netfilter/nft_set_pipapo.c 实现的 pipapo 算法是 nftables 集合子系统中最复杂的后端,专为多字段范围匹配(如防火墙策略中的四元组匹配)设计。
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 -> 匹配的规则集合
// 每个字段对应一个 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[]; // 字段数组(变长)
};查找函数 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 // 返回对应元素指针
范围 [a, b] 被 pipapo_expand() 分解为若干个网掩码(依据 Rottenstreich 2010 定理),每个网掩码对应一条"规则"(rule)。这使得任意范围都可以被位图操作精确匹配。
例如端口范围 1000-1023(二进制 01111101000 到 01111111111)被分解为两个网掩码:
01111101xxx(1000-1007)0111111xxxx(1008-1023,实际上这里需更细分,仅示意)
分解后,每个网掩码的"wildcard bit"对应的所有 bucket 都会在查找表中被标记,从而覆盖整个范围。
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 上非法指令错误)。
net/netfilter/nft_set_bitmap.c 实现了专为小型整数键集合优化的位图后端,适用于端口号(16 位)或协议号(8 位)等场景。
位图集合用一个 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)^
位图查找是 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 局部。
- 查找:O(1),单次内存访问(小位图可完整在 L1 cache)
- 键长度限制:最多 16 位(65536 个元素,位图 16 KB)
- 不支持超时和元素级表达式(简化设计)
- 双代机制通过每元素 2 位内嵌实现,无需额外 genmask 字段
nftables 与连接跟踪(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 表达式统计每连接流量
NFT_CT_ZONE 的写操作使用 per-CPU 模板连接(nft_ct.c 第 33-37 行中的 nft_ct_pcpu_template),在 PREROUTING 阶段为包分配带有特定 zone ID 的连接,实现不同租户的流量在不同 conntrack zone 中追踪(避免不同 VPN 用户的连接表互相干扰)。
当连接被卸载到 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,以数据路径为准("数据路径是权威的")。
对于最常见的 NFT_CT_STATE 和 NFT_CT_MARK 读操作,内核提供了 nft_ct_get_fast_eval() 快速路径(通过 Retpoline 规避机制直接调用),绕过完整的 switch 语句(nft_ct.c 中通过宏生成)。
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 等)。
本节从源码层面完整梳理 flowtable 数据包处理流程。
生命周期状态机:
[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()
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; // 成功,由调用者完成实际发送
}当 flow->flags 包含 NF_FLOW_SNAT 或 NF_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)高效完成,无需重新计算整个包的校验和。
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() 等),确保转发后的包格式与原始路径一致。
用户空间通过 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() 中回滚,内核状态不受影响。
原子性由以下机制共同保证:
- commit_mutex 互斥:同时只有一个批处理在进行,其他批处理排队等待
- 双代机制:提交前新对象在旧代不可见,提交瞬间切换代,无中间状态
- genmask 字段:每个可见性相关对象(表/链/规则/集合元素)都有 2-bit genmask,在提交前控制其在新旧代的可见性
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() 删除已添加的元素
在 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
每次成功提交后,nftables 通过 audit_log_nfcfg() 向内核审计子系统记录配置变更(nf_tables_api.c 中的审计映射表,第 83-110 行),包含操作类型、表名、规则句柄等信息,满足安全审计要求(CONFIG_AUDIT 开启时)。
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)
- 如果有终止裁决,包含其值
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 行)。
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 位字撕裂)。
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),
};当网络命名空间销毁时,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)
flowtable 同样按命名空间隔离。nf_flow_table_core.c 中的全局 flowtables 链表(第 18 行 LIST_HEAD(flowtables))在历史版本中曾是全局共享的,现已改为通过 net 字段(nf_flowtable->net)关联到各自命名空间,确保跨 netns 的 flowtable 不共享任何状态。
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))));
};在 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 超时策略
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_table 的 objects 链表管理,nft_trans_obj 事务节点保证对象创建/删除的原子性。
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 集合后端
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() 展开为直接调用序列。
nftables 的设计体现了 Linux 内核网络栈的几个核心原则:
-
数据/控制平面分离:规则存储(
nft_rule链表)与执行(nft_rule_dpblob)完全分离,控制平面的变更不影响数据平面的执行,两者通过 RCU 双代机制解耦 -
可扩展性优于内置功能:表达式系统(
nft_expr_ops)和集合后端(nft_set_ops)都是插件化接口,新功能通过注册新模块实现,内核核心代码无需修改 -
性能工程:从规则 blob 的连续内存布局、fast ops 内联、per-CPU 统计,到 flowtable bypass 路径和硬件卸载,每个层次都有针对性的性能优化
-
正确性优先:事务机制确保配置原子性;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 分析生成