Linux 网络内核协议栈深度剖析(七):Netfilter、nftables 与 eBPF 网络程序

在前六篇中,我们依次拆解了 sk_buff 生命周期、网卡驱动收发路径、TCP/IP 协议栈、路由子系统、套接字层以及 NAPI/GRO 机制。本篇聚焦数据包过滤与处理的核心框架:Netfilter hook 体系、nftables 规则执行引擎、conntrack 连接追踪,以及近年来高速演进的 eBPF 网络程序(XDP、tc BPF、Socket Filter)。所有代码片段均源自 Linux 6.4-rc1。

一、Netfilter 框架:五个 Hook 点与 Hook 链遍历

1.1 核心数据结构

Netfilter 的基石是 struct nf_hook_ops,定义在 include/linux/netfilter.h

1
2
3
4
5
6
7
8
9
10
11
12
/* include/linux/netfilter.h  (Linux 6.4-rc1) */
struct nf_hook_ops {
/* User fills in from here down. */
nf_hookfn *hook;
struct net_device *dev;
void *priv;
u8 pf;
enum nf_hook_ops_type hook_ops_type:8;
unsigned int hooknum;
/* Hooks are ordered in ascending priority. */
int priority;
};

各字段含义:

  • hook:实际的回调函数,类型为 unsigned int (*)(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
  • pf:协议族,如 NFPROTO_IPV4NFPROTO_IPV6
  • hooknum:挂载点编号,IPv4/IPv6 共享 NF_INET_PRE_ROUTING(0)、NF_INET_LOCAL_IN(1)、NF_INET_FORWARD(2)、NF_INET_LOCAL_OUT(3)、NF_INET_POST_ROUTING(4)五个点。
  • priority:优先级,数值越小越先执行;conntrack 为 -200,NAT 为 -100,iptables filter 为 0
  • hook_ops_type:区分普通 hook 与 nftables hook(NF_HOOK_OP_NF_TABLES)、BPF hook(NF_HOOK_OP_BPF)。BPF hook 要求 priority 唯一,防止两个 BPF 程序以相同优先级并存造成顺序不确定。

struct nf_hook_state 在每次调用 hook 链时由调用方构造并传入:

1
2
3
4
5
6
7
8
9
struct nf_hook_state {
u8 hook; /* hooknum */
u8 pf; /* 协议族 */
struct net_device *in;
struct net_device *out;
struct sock *sk;
struct net *net;
int (*okfn)(struct net *, struct sock *, struct sk_buff *);
};

hook 返回 NF_ACCEPT 后,框架最终会调用 okfn 把包交回内核继续处理。

1.2 五个 Hook 点在 IP 层的调用位置

Netfilter 提供的 NF_HOOK() 宏在 IP 收发路径中有以下实际调用点:

PRE_ROUTING — 进入 IP 层后、路由查找前(net/ipv4/ip_input.c):

1
2
3
4
/* net/ipv4/ip_input.c  ip_rcv() */
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
net, NULL, skb, dev, NULL,
ip_rcv_finish);

LOCAL_IN — 路由判定为本机后(net/ipv4/ip_input.c):

1
2
3
4
/* net/ipv4/ip_input.c  ip_local_deliver() */
return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN,
net, NULL, skb, skb->dev, NULL,
ip_local_deliver_finish);

FORWARD — 路由判定为转发后(net/ipv4/ip_forward.c):

1
2
/* net/ipv4/ip_forward.c */
return NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD, ...);

LOCAL_OUT — 本地生成的包刚进入 IP 层时(net/ipv4/ip_output.c):

1
2
3
4
/* net/ipv4/ip_output.c  __ip_local_out() */
return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT,
net, sk, skb, NULL, skb_dst(skb)->dev,
dst_output);

POST_ROUTING — 包即将离开本机时(net/ipv4/ip_output.c):

1
2
/* net/ipv4/ip_output.c  ip_output() */
return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING, ...);

1.3 Hook 注册与注销

nf_register_net_hook / nf_unregister_net_hook 是模块注册 hook 的公共 API(声明于 include/linux/netfilter.h)。内部流程是:在 net->nf.hooks_ipv4[hooknum](或对应的 IPv6/ARP 数组)所指向的 struct nf_hook_entries 中按 priority 插入新条目,通过 RCU 发布新的 entries 指针,并等待读者侧宽限期后释放旧数组。

最多允许 1024 个 hook(MAX_HOOK_COUNT)。BPF hook 类型额外要求 priority 唯一:

1
2
3
4
/* net/netfilter/core.c */
if (reg->priority == orig_ops[i]->priority &&
reg->hook_ops_type == NF_HOOK_OP_BPF)
return ERR_PTR(-EBUSY);

1.4 nf_hook_slow:Hook 链遍历

nf_hook_slow 是 hook 链遍历的核心(net/netfilter/core.c):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/* net/netfilter/core.c
* Returns 1 if okfn() needs to be executed by the caller,
* -EPERM for NF_DROP, 0 otherwise. Caller must hold rcu_read_lock. */
int nf_hook_slow(struct sk_buff *skb, struct nf_hook_state *state,
const struct nf_hook_entries *e, unsigned int s)
{
unsigned int verdict;
int ret;

for (; s < e->num_hook_entries; s++) {
verdict = nf_hook_entry_hookfn(&e->hooks[s], skb, state);
switch (verdict & NF_VERDICT_MASK) {
case NF_ACCEPT:
break;
case NF_DROP:
kfree_skb_reason(skb, SKB_DROP_REASON_NETFILTER_DROP);
ret = NF_DROP_GETERR(verdict);
if (ret == 0)
ret = -EPERM;
return ret;
case NF_QUEUE:
ret = nf_queue(skb, state, s, verdict);
if (ret == 1)
continue;
return ret;
default:
/* Implicit handling for NF_STOLEN, as well as any other
* non conventional verdicts.
*/
return 0;
}
}

return 1;
}

返回值语义:

  • 返回 1:所有 hook 通过,调用方需执行 okfn
  • 返回 0:包被 hook 消耗(NF_STOLEN),调用方不应再碰 skb。
  • 返回负值(如 -EPERM):包被 NF_DROP,已释放 skb。
  • NF_QUEUE:包入用户态队列(NFQUEUE),当前返回 0 或重新入链继续。

二、nf_tables(nftables)核心:字节码执行引擎

iptables 时代每条规则都是内核模块注册的 match/target 函数调用链,扩展性差且每新增规则都需遍历整个链表。nftables 借鉴 BPF 思路,将规则编译为”字节码”式的表达式序列,在一个统一的执行引擎中运行。

2.1 核心数据结构层次

1
2
3
4
5
nft_table
└─ nft_chain(多个)
└─ nft_rule(多个,存于 blob)
└─ nft_expr(多个,连续排列)
└─ nft_expr_ops(ops->eval 回调)

**struct nft_table**(include/net/netfilter/nf_tables.h):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct nft_table {
struct list_head list;
struct rhltable chains_ht;
struct list_head chains;
struct list_head sets;
struct list_head objects;
struct list_head flowtables;
u64 hgenerator;
u64 handle;
u32 use;
u16 family:6,
flags:8,
genmask:2;
u32 nlpid;
char *name;
u16 udlen;
u8 *udata;
u8 validate_state;
};

表(table)代表一个命名空间,如 inet filterip nat,包含多条链(chain)。family 对应 NFPROTO_*genmask 是双缓冲生效位(generation mask),用于原子提交规则变更。

**struct nft_chain**:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct nft_chain {
struct nft_rule_blob __rcu *blob_gen_0;
struct nft_rule_blob __rcu *blob_gen_1;
struct list_head rules;
struct list_head list;
struct rhlist_head rhlhead;
struct nft_table *table;
u64 handle;
u32 use;
u8 flags:5,
bound:1,
genmask:2;
char *name;
u16 udlen;
u8 *udata;
struct nft_rule_blob *blob_next;
};

blob_gen_0blob_gen_1 是两个交替使用的规则 blob,由 gencursor 决定当前生效哪一个,实现规则集的无锁原子切换。

**struct nft_rule**:

1
2
3
4
5
6
7
8
9
struct nft_rule {
struct list_head list;
u64 handle:42,
genmask:2,
dlen:12,
udata:1;
unsigned char data[]
__attribute__((aligned(__alignof__(struct nft_expr))));
};

规则本身只是元数据头部,真正的表达式序列内联在 data[] 中,dlendata 的字节长度。

struct nft_expr 与 **struct nft_expr_ops**:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct nft_expr {
const struct nft_expr_ops *ops;
unsigned char data[]
__attribute__((aligned(__alignof__(u64))));
};

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);
unsigned int size;
int (*init)(const struct nft_ctx *ctx,
const struct nft_expr *expr,
const struct nlattr * const tb[]);
/* ... activate, deactivate, destroy, dump, validate, offload ... */
const struct nft_expr_type *type;
void *data;
};

每个表达式的私有数据跟在 ops 指针之后(nft_expr_priv(expr) 返回 expr->data)。表达式通过 ops->eval 读写 struct nft_regs 寄存器组——这正是”字节码虚拟机”的概念。

2.2 nft_do_chain:规则执行引擎

nft_do_chain 是整个 nftables 的执行心脏(net/netfilter/nf_tables_core.c):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
unsigned int
nft_do_chain(struct nft_pktinfo *pkt, void *priv)
{
const struct nft_chain *chain = priv, *basechain = chain;
const struct net *net = nft_net(pkt);
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];
bool genbit = READ_ONCE(net->nft.gencursor);
struct nft_rule_blob *blob;
struct nft_traceinfo info;

info.trace = false;
if (static_branch_unlikely(&nft_trace_enabled))
nft_trace_init(&info, pkt, basechain);
do_chain:
if (genbit)
blob = rcu_dereference(chain->blob_gen_1);
else
blob = rcu_dereference(chain->blob_gen_0);

rule = (struct nft_rule_dp *)blob->data;
next_rule:
regs.verdict.code = NFT_CONTINUE;
for (; !rule->is_last ; rule = nft_rule_next(rule)) {
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;
}

switch (regs.verdict.code) {
case NFT_BREAK:
regs.verdict.code = NFT_CONTINUE;
continue;
case NFT_CONTINUE:
continue;
}
break;
}

switch (regs.verdict.code & NF_VERDICT_MASK) {
case NF_ACCEPT:
case NF_DROP:
case NF_QUEUE:
case NF_STOLEN:
return regs.verdict.code;
}

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_CONTINUE:
case NFT_RETURN:
break;
}

if (stackptr > 0) {
rule = jumpstack[--stackptr].rule;
goto next_rule;
}

return nft_base_chain(basechain)->policy;
}

关键设计要点:

  1. 双缓冲读取genbitnet->nft.gencursor 原子读取,决定使用 blob_gen_0 还是 blob_gen_1,配合 RCU 实现规则集热更新。

  2. 快速路径优化nft_cmp_fast_opsnft_bitwise_fast_opsnft_payload_fast_ops 等内联快速版本绕过间接调用(Retpoline 背景下间接调用代价高),直接内联执行。

  3. JUMP/GOTO 语义NFT_JUMP 把返回点压栈(类似 iptables 的 --goto/-j),NFT_GOTO 不压栈(不会返回)。最大跳转深度为 NFT_JUMP_STACK_SIZE(16)。

  4. 寄存器结果传递:每个 expr 通过写 regs.verdict.code 表达继续(NFT_CONTINUE)、中断当前规则(NFT_BREAK)或最终裁决(NF_DROP 等)。

2.3 与 iptables 的对比

维度 iptables nftables
规则存储 内核模块注册的 match/target 链表 线性 blob,内联表达式序列
执行方式 函数指针链式调用 统一执行引擎,寄存器模型
协议族 分开(iptables/ip6tables/arptables) 统一(inet 族可同时处理 IPv4/IPv6)
原子更新 替换整个 table(需持锁) 双缓冲 + RCU,gencursor 原子切换
集合支持 ipset(外部模块) 原生 set,支持区间/字典/位图
用户空间工具 iptables/ip6tables/… nft

三、conntrack(连接追踪):状态机与哈希表

3.1 struct nf_conn

连接追踪的核心对象是 struct nf_conninclude/net/netfilter/nf_conntrack.h):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
struct nf_conn {
/* 引用计数:哈希表持有 1,每个 skb 持有 1,master 连接持有 1 */
struct nf_conntrack ct_general;

spinlock_t lock;
/* jiffies32 when this ct is considered dead */
u32 timeout;

#ifdef CONFIG_NF_CONNTRACK_ZONES
struct nf_conntrack_zone zone;
#endif
/* 原始方向和应答方向的元组哈希 */
struct nf_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];

/* 连接状态位集合 */
unsigned long status;

possible_net_t ct_net;

#if IS_ENABLED(CONFIG_NF_NAT)
struct hlist_node nat_bysource;
#endif
struct { } __nfct_init_offset;

/* 若被 expectation 期待创建,则指向 master */
struct nf_conn *master;

/* mark、secmark、扩展(acct、timestamp、labels 等)*/
struct nf_ct_ext *ext;

union nf_conntrack_proto proto;
};

tuplehash[IP_CT_DIR_ORIGINAL] 存储首包方向的五元组哈希节点,tuplehash[IP_CT_DIR_REPLY] 存储应答方向。两者都挂在全局 nf_conntrack_hash 哈希表中,这样无论哪个方向的包都能快速查到同一个 nf_conn

status 是连接状态位,常见位:

含义
IPS_EXPECTED 由 helper 期待创建(如 FTP 数据连接)
IPS_SEEN_REPLY 已见到应答方向的包(对应 ESTABLISHED)
IPS_ASSURED 已确认,不会被 early drop
IPS_CONFIRMED 已加入哈希表
IPS_NAT_MASK 经过 NAT

3.2 连接状态与 ip_conntrack_info

从 skb 视角看到的连接信息编码在 ctinfoenum ip_conntrack_info):

  • IP_CT_NEW:属于新连接的首包。
  • IP_CT_ESTABLISHED:属于已建立连接的原始方向包。
  • IP_CT_ESTABLISHED_REPLY:属于已建立连接的应答方向包(此时 IPS_SEEN_REPLY 位被置位)。
  • IP_CT_RELATED:相关连接的包(如 ICMP 错误、FTP 数据连接)。
  • IP_CT_UNTRACKED:被明确标记为不追踪的包(通过 CT --notrack 规则)。

3.3 nf_conntrack_in:追踪入口

nf_conntrack_in 挂载在 PRE_ROUTINGLOCAL_OUT hook 上,是连接追踪的主入口(net/netfilter/nf_conntrack_core.c):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
nf_conntrack_in(struct sk_buff *skb, const struct nf_hook_state *state)
{
enum ip_conntrack_info ctinfo;
struct nf_conn *ct, *tmpl;
u_int8_t protonum;
int dataoff, ret;

tmpl = nf_ct_get(skb, &ctinfo);
if (tmpl || ctinfo == IP_CT_UNTRACKED) {
if ((tmpl && !nf_ct_is_template(tmpl)) ||
ctinfo == IP_CT_UNTRACKED)
return NF_ACCEPT;
skb->_nfct = 0;
}

dataoff = get_l4proto(skb, skb_network_offset(skb), state->pf, &protonum);
if (dataoff <= 0) {
NF_CT_STAT_INC_ATOMIC(state->net, invalid);
ret = NF_ACCEPT;
goto out;
}
repeat:
ret = resolve_normal_ct(tmpl, skb, dataoff, protonum, state);
if (ret < 0) {
NF_CT_STAT_INC_ATOMIC(state->net, drop);
ret = NF_DROP;
goto out;
}

ct = nf_ct_get(skb, &ctinfo);
if (!ct) {
NF_CT_STAT_INC_ATOMIC(state->net, invalid);
ret = NF_ACCEPT;
goto out;
}

ret = nf_conntrack_handle_packet(ct, skb, dataoff, ctinfo, state);
if (ret <= 0) {
nf_ct_put(ct);
skb->_nfct = 0;
if (ret == -NF_REPEAT)
goto repeat;
ret = -ret;
goto out;
}

if (ctinfo == IP_CT_ESTABLISHED_REPLY &&
!test_and_set_bit(IPS_SEEN_REPLY_BIT, &ct->status))
nf_conntrack_event_cache(IPCT_REPLY, ct);
out:
if (tmpl)
nf_ct_put(tmpl);
return ret;
}

核心流程:resolve_normal_ct 内部调用 __nf_conntrack_find_get 做哈希查找,若未命中则分配新 nf_connnf_conntrack_handle_packet 调用各协议(TCP/UDP/ICMP)的 packet 回调更新协议状态机和超时。

3.4 __nf_conntrack_find_get:哈希查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
__nf_conntrack_find_get(struct net *net, const struct nf_conntrack_zone *zone,
const struct nf_conntrack_tuple *tuple, u32 hash)
{
struct nf_conntrack_tuple_hash *h;
struct nf_conn *ct;

h = ____nf_conntrack_find(net, zone, tuple, hash);
if (h) {
ct = nf_ct_tuplehash_to_ctrack(h);
if (likely(refcount_inc_not_zero(&ct->ct_general.use))) {
smp_acquire__after_ctrl_dep();
if (likely(nf_ct_key_equal(h, tuple, zone, net)))
return h;
/* TYPESAFE_BY_RCU 被回收,重新查找 */
nf_ct_put(ct);
}
h = NULL;
}
return h;
}

哈希表在 RCU 读锁下查找,找到候选后先增引用计数,再二次校验 key(防止 TYPESAFE_BY_RCU 复用场景下的 ABA 问题)。

3.5 超时机制

nf_conn.timeout 存储到期 jiffies32 值。每个协议对应不同默认超时,例如 TCP ESTABLISHED 为 5 天(nf_ct_tcp_timeout_established),UDP 为 180 秒,UDP 应答后为 180 秒。conntrack GC 工作队列(conntrack_gc_work)周期性扫描哈希桶,清理超时条目。GC 间隔在 1 秒到 60 秒之间动态调整,GC_SCAN_INTERVAL_CLAMP(300 秒)用于钳制 TCP 未应答连接的超时不被过度推迟。


四、XDP(eXpress Data Path):内核最快数据平面

4.1 三种挂载模式

XDP 程序挂载点由 enum bpf_xdp_mode 定义(include/linux/netdevice.h):

1
2
3
4
5
6
enum bpf_xdp_mode {
XDP_MODE_SKB = 0, /* GENERIC:软件模拟,在 skb 分配之后运行 */
XDP_MODE_DRV = 1, /* NATIVE/DRV:驱动原生支持,在 skb 分配之前运行 */
XDP_MODE_HW = 2, /* OFFLOAD:卸载到 SmartNIC,完全绕过主机 CPU */
__MAX_XDP_MODE
};
  • GENERIC(SKB 模式):无需驱动支持,XDP 程序运行在 skb 已分配之后,性能提升有限,但兼容所有网卡。通过 ip link set dev eth0 xdp 在不支持 Native 时自动降级。
  • NATIVE(DRV 模式):驱动在 NAPI poll 中收到 DMA 数据后、构造 skb 之前调用 XDP 程序,节省了 skb 分配和初始化的开销,典型吞吐提升数倍。主流驱动(mlx5、i40e、ixgbe 等)均支持。
  • OFFLOAD(HW 模式):BPF 字节码被编译下推到 SmartNIC(如 Netronome),完全在网卡硬件上执行,CPU 零参与。

4.2 struct xdp_buff 与 struct xdp_md

驱动侧使用 struct xdp_buffinclude/net/xdp.h)描述接收到的帧:

1
2
3
4
5
6
7
8
9
10
struct xdp_buff {
void *data; /* 指向帧数据起点(L2 头部)*/
void *data_end; /* 指向帧数据终点 */
void *data_meta; /* 元数据区域(data_meta <= data)*/
void *data_hard_start; /* DMA 缓冲区起点,供 bpf_xdp_adjust_head 使用 */
struct xdp_rxq_info *rxq;
struct xdp_txq_info *txq;
u32 frame_sz;
u32 flags;
};

BPF 程序中看到的上下文是 struct xdp_md(UAPI,include/uapi/linux/bpf.h):

1
2
3
4
5
6
7
8
struct xdp_md {
__u32 data; /* 等价于 xdp_buff->data 的偏移 */
__u32 data_end;
__u32 data_meta;
__u32 ingress_ifindex; /* rxq->dev->ifindex */
__u32 rx_queue_index; /* rxq->queue_index */
__u32 egress_ifindex; /* txq->dev->ifindex */
};

BPF 验证器会把对 xdp_md 字段的访问翻译成对内核 xdp_buff 的实际访问,保证安全边界检查。

4.3 返回值语义

1
2
3
4
5
6
7
8
/* include/uapi/linux/bpf.h */
enum xdp_action {
XDP_ABORTED = 0, /* 程序出错,包被丢弃,触发 xdp_exception tracepoint */
XDP_DROP, /* 丢弃包,不产生 skb */
XDP_PASS, /* 继续送往内核网络栈(构造 skb) */
XDP_TX, /* 原路发回(从同一网口发出) */
XDP_REDIRECT, /* 转发到另一网口/CPU/socket */
};

XDP_DROP 是最高效的丢包方式,在 DMA 层就回收 buffer,不产生任何 skb,适合 DDoS 防护。XDP_REDIRECT 配合 bpf_redirect_map() 实现零拷贝包转发。

4.4 bpf_redirect_map

1
long bpf_redirect_map(struct bpf_map *map, u64 key, u64 flags)

根据 map 类型不同,行为各异:

  • DEVMAPBPF_MAP_TYPE_DEVMAP):将包转发到另一个网口(ifindex),实现高性能软件交换。
  • CPUMAPBPF_MAP_TYPE_CPUMAP):将包重定向到另一个 CPU 的 XDP 处理队列,实现多核负载均衡。仅 Native XDP 支持。
  • XSKMAPBPF_MAP_TYPE_XSKMAP):转发到 AF_XDP socket(零拷贝用户态收包)。

flags 的低两位用作 map 查找失败时的默认返回码(通常为 XDP_DROP);BPF_F_BROADCAST 可广播给 DEVMAP 中所有接口。

4.5 XDP 与 tc BPF 的区别

维度 XDP tc BPF
运行时机 驱动收包时(skb 分配前,NATIVE 模式) 流量控制层(skb 已分配)
上下文 struct xdp_md struct __sk_buff
方向 仅 ingress(TX 方向有 XDP_TX/redirect) ingress 和 egress 均支持
包修改能力 有限(需 adjust_head/tail) 丰富(可访问 skb 元数据)
性能 极高(zero-copy,无 skb 开销) 高(skb 层,有一定开销)
典型用途 DDoS 防护、负载均衡、XSK 收包 带宽限速、策略路由、服务网格 sidecar

五、tc BPF(Traffic Control eBPF):流量分类与策略

tc(Traffic Control)子系统历史悠久,eBPF 的引入让它焕发新生。tc BPF 程序挂在 cls_bpf 分类器上,可以做任意包匹配和修改。

5.1 cls_bpf_classify

cls_bpf_classify 是 tc BPF 分类器的核心(net/sched/cls_bpf.c):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
TC_INDIRECT_SCOPE int cls_bpf_classify(struct sk_buff *skb,
const struct tcf_proto *tp,
struct tcf_result *res)
{
struct cls_bpf_head *head = rcu_dereference_bh(tp->root);
bool at_ingress = skb_at_tc_ingress(skb);
struct cls_bpf_prog *prog;
int ret = -1;

list_for_each_entry_rcu(prog, &head->plist, link) {
int filter_res;

qdisc_skb_cb(skb)->tc_classid = prog->res.classid;

if (tc_skip_sw(prog->gen_flags)) {
filter_res = prog->exts_integrated ? TC_ACT_UNSPEC : 0;
} else if (at_ingress) {
__skb_push(skb, skb->mac_len);
bpf_compute_data_pointers(skb);
filter_res = bpf_prog_run(prog->filter, skb);
__skb_pull(skb, skb->mac_len);
} else {
bpf_compute_data_pointers(skb);
filter_res = bpf_prog_run(prog->filter, skb);
}

if (prog->exts_integrated) {
res->classid = TC_H_MAJ(prog->res.classid) |
qdisc_skb_cb(skb)->tc_classid;
ret = cls_bpf_exec_opcode(filter_res);
if (ret == TC_ACT_UNSPEC)
continue;
break;
}
/* ... */
}
return ret;
}

ingress 方向需先 push MAC 头(__skb_push(skb, skb->mac_len))使 data 指针指向二层头部,BPF 程序运行后再 pull 回去。bpf_prog_run 执行 BPF 字节码,返回 TC_ACT_OK(放行)、TC_ACT_SHOT(丢弃)、TC_ACT_REDIRECT 等动作码。

5.2 direct-action 模式

当 tc filter 使用 direct-action(da)标志时,BPF 程序的返回值直接作为 tc action 结果,无需再查 action 链表,大幅降低开销。现代 Cilium、Istio ambient 等服务网格均使用该模式。

5.3 硬件卸载

struct tc_cls_bpf_offload 用于将 tc BPF 程序卸载到支持的 SmartNIC:

1
2
3
/* net/sched/cls_bpf.c */
struct tc_cls_bpf_offload cls_bpf = {};
/* ... 填充 cls_bpf,通过 tcf_block_offload_cmd 下发给驱动 */

5.4 与 iptables 的性能对比

在典型的 10GbE 环境下:

  • iptables:规则数量线性增加时,匹配时间 O(n);100 万条规则时单核处理能力降至数十万 pps 量级。
  • nftables(使用 set):哈希 set 查找 O(1),百万 IP 规则下延迟保持稳定。
  • XDP(Native):单核可达 14Mpps(10G 线速),完全不受规则数量影响(逻辑硬编码在 BPF 程序中)。
  • tc BPF:介于两者之间,单核 2~5Mpps,支持更复杂的上下文操作。

六、Socket Filter 与 sk_msg:Socket 层 BPF

6.1 SO_ATTACH_FILTER / SO_ATTACH_BPF

经典 Socket Filter 通过 setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &prog, sizeof(prog)) 附加,现代 eBPF 程序使用 SO_ATTACH_BPF(传入 BPF fd)。

struct sk_filter 定义于 include/linux/filter.h

1
2
3
4
5
struct sk_filter {
refcount_t refcnt;
struct rcu_head rcu;
struct bpf_prog *prog;
};

结构极简:一个引用计数、一个 RCU 头(用于延迟释放)、一个 BPF 程序指针。每个 sock 结构中有 sk->sk_filter 字段指向该过滤器。

6.2 sk_filter_trim_cap:Socket 收包过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/* net/core/filter.c */
int sk_filter_trim_cap(struct sock *sk, struct sk_buff *skb, unsigned int cap)
{
int err;
struct sk_filter *filter;

if (skb_pfmemalloc(skb) && !sock_flag(sk, SOCK_MEMALLOC)) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_PFMEMALLOCDROP);
return -ENOMEM;
}
err = BPF_CGROUP_RUN_PROG_INET_INGRESS(sk, skb);
if (err)
return err;

err = security_sock_rcv_skb(sk, skb);
if (err)
return err;

rcu_read_lock();
filter = rcu_dereference(sk->sk_filter);
if (filter) {
struct sock *save_sk = skb->sk;
unsigned int pkt_len;

skb->sk = sk;
pkt_len = bpf_prog_run_save_cb(filter->prog, skb);
skb->sk = save_sk;
err = pkt_len ? pskb_trim(skb, max(cap, pkt_len)) : -EPERM;
}
rcu_read_unlock();

return err;
}

BPF 程序返回 0 时包被丢弃(返回 -EPERM);返回非零值为保留的字节数,pskb_trim 按此截断包(tcpdump 抓包时可利用此机制截断到固定长度)。

6.3 sockmap 与 sk_msg:高性能 Socket 代理

sockmap(BPF_MAP_TYPE_SOCKMAP/BPF_MAP_TYPE_SOCKHASH)允许 BPF 程序在 socket 之间直接转发数据,跳过用户态 proxy 的 read()/write() 往返。

主要程序类型:

  • BPF_PROG_TYPE_SK_SKB:在 skb 级别拦截 socket 接收路径,可修改包后重定向到另一个 socket(bpf_sk_redirect_map)。
  • BPF_PROG_TYPE_SK_MSG:在 sendmsg() 路径上拦截,通过 struct sk_msg 操作数据流,适合 L7 代理场景(如 Cilium 的 sockops 加速)。

sk_msg 操作 helper 包括:

  • bpf_msg_apply_bytes:对后续 N 字节应用当前 verdict。
  • bpf_msg_cork_bytes:积累 N 字节后再触发 verdict(消息聚合)。
  • bpf_msg_pull_data:将分散 sg 数据拉入线性区,方便访问。
  • bpf_msg_push_data / bpf_msg_pop_data:在消息中插入或删除数据(协议封装/解封)。

典型用途:Kubernetes 集群内 Pod 间通信,通过 sockmap 完全绕过 iptables/conntrack,延迟降低 30%~50%。


七、诊断方法

7.1 规则集查看

1
2
3
4
5
6
7
8
9
10
# nftables:查看完整规则集
nft list ruleset

# iptables:带包计数器详细输出
iptables -L -n -v --line-numbers
ip6tables -L -n -v

# 对比差异:iptables 按链遍历,nft 支持按表/链过滤
nft list table inet filter
nft list chain inet filter input

7.2 conntrack 连接追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 查看当前连接追踪表
conntrack -L

# 按协议过滤,查看 TCP ESTABLISHED 连接数
conntrack -L -p tcp --state ESTABLISHED | wc -l

# 实时统计
conntrack -S

# 查看当前追踪条目数和最大值
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max

# 快速扩容(临时)
echo 2000000 > /proc/sys/net/netfilter/nf_conntrack_max

conntrack 表满导致丢包的排查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 检查 conntrack 相关计数器
cat /proc/net/stat/nf_conntrack

# 典型症状:drop 列持续增长
# insert_failed: 哈希插入失败(通常是表满或哈希冲突链过长)
# drop: 因无法创建新连接而丢包

# 内核日志中会出现
dmesg | grep "nf_conntrack: table full"

# 永久调整(/etc/sysctl.conf)
net.netfilter.nf_conntrack_max = 2000000
net.netfilter.nf_conntrack_tcp_timeout_established = 300
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30

哈希表大小由 nf_conntrack_htable_size 控制(默认为内存大小 / 16384,最大 65536),大型服务器需显式设置更大的值。

7.3 BPF 程序诊断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 查看已加载的 BPF 程序
bpftool prog list

# 查看特定程序的 BPF 字节码/JIT 汇编
bpftool prog dump xlated id <prog_id>
bpftool prog dump jited id <prog_id>

# 加载 XDP 程序(Native 模式)
ip link set eth0 xdp obj xdp_prog.o sec xdp

# 卸载 XDP 程序
ip link set eth0 xdp off

# 查看网口 XDP 状态
ip link show eth0 | grep xdp

# tc BPF:添加 ingress clsact
tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress bpf obj tc_prog.o sec classifier direct-action

# 查看 tc filter
tc filter show dev eth0 ingress
tc filter show dev eth0 egress

7.4 bpftrace 动态追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 统计 nft_do_chain 调用的链名分布
bpftrace -e 'kprobe:nft_do_chain { @[str(args->chain->name)] = count(); }'

# 追踪 nf_hook_slow 调用频率(按 hook 点)
bpftrace -e '
kprobe:nf_hook_slow {
@[args->state->hook] = count();
}'

# 监控 conntrack 表满事件
bpftrace -e 'kprobe:nf_ct_alloc_hashtable { @alloc = count(); }
kretprobe:nf_ct_alloc_hashtable / retval == 0 / { @fail = count(); }'

# 追踪 XDP 程序返回值分布
bpftrace -e 'tracepoint:xdp:xdp_exception { @[args->act] = count(); }'
bpftrace -e 'tracepoint:xdp:xdp_redirect_err { @err = count(); }'

# 统计 conntrack NEW 连接创建速率
bpftrace -e 'kprobe:nf_conntrack_in { @[nsecs/1000000000] = count(); }'

7.5 性能热点分析

1
2
3
4
5
6
7
8
9
10
11
12
# perf 统计 Netfilter 路径开销
perf stat -e 'net:netif_receive_skb,net:net_dev_xmit' -a sleep 5

# 查看 conntrack 哈希桶使用分布(避免链过长)
cat /proc/net/stat/nf_conntrack | awk '{print $1, $2}' | head -20

# XDP 统计(需驱动支持)
ethtool -S eth0 | grep -i xdp

# 观察 BPF JIT 内存
cat /proc/sys/net/core/bpf_jit_enable
# 0: 关闭 JIT,1: 开启,2: 开启并输出调试信息

八、整体架构总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
物理网卡(DMA 收包)

▼ [XDP_MODE_DRV / NATIVE]
XDP 程序(BPF)
XDP_DROP → 丢弃(最快)
XDP_TX → 原路发回
XDP_REDIRECT → CPUMAP/DEVMAP/XSKMAP
XDP_PASS ↓


构造 sk_buff(NAPI poll)

▼ [XDP_MODE_SKB / GENERIC]
XDP 程序(此时 skb 已存在)


Netfilter NF_INET_PRE_ROUTING(priority 顺序)
├─ conntrack (-200):nf_conntrack_in,分配/查找 nf_conn
├─ ipset/nft (-100~0):nft_do_chain 执行规则
└─ iptables (0):ipt_do_table 匹配规则


路由决策(fib_lookup)
├─ 本机 → NF_INET_LOCAL_IN → socket 收包
│ └─ sk_filter_trim_cap(Socket BPF)
│ └─ sockmap/sk_msg(Socket 代理)
└─ 转发 → NF_INET_FORWARD → NF_INET_POST_ROUTING → 发包


tc qdisc(cls_bpf)
└─ BPF 程序(direct-action)


网卡驱动发包

Netfilter 提供了灵活但相对重量级的 hook 框架,适合有状态防火墙、NAT 等场景。eBPF 以 XDP 和 tc BPF 的形式提供了更高性能的可编程数据平面,两者并非替代关系,而是在不同层次协同工作——连接追踪、NAT 仍依赖 Netfilter,而高性能转发、DDoS 防护、服务网格加速则更多使用 eBPF。

理解这一层次结构,是深入优化生产系统网络性能的前提。下一篇将进入网络命名空间(netns)与容器网络(veth、bridge、overlay)的内核实现。