在前六篇中,我们依次拆解了 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 struct nf_hook_ops { nf_hookfn *hook; struct net_device *dev ; void *priv; u8 pf; enum nf_hook_ops_type hook_ops_type :8 ; unsigned int hooknum; int priority; };
各字段含义:
hook:实际的回调函数,类型为 unsigned int (*)(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)。
pf:协议族,如 NFPROTO_IPV4、NFPROTO_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; 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 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 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 return NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD, ...);
LOCAL_OUT — 本地生成的包刚进入 IP 层时(net/ipv4/ip_output.c):
1 2 3 4 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 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 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 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 : 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 filter、ip 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_0 和 blob_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[] 中,dlen 是 data 的字节长度。
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[]); 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, ®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 ; } 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; }
关键设计要点:
双缓冲读取 :genbit 从 net->nft.gencursor 原子读取,决定使用 blob_gen_0 还是 blob_gen_1,配合 RCU 实现规则集热更新。
快速路径优化 :nft_cmp_fast_ops、nft_bitwise_fast_ops、nft_payload_fast_ops 等内联快速版本绕过间接调用(Retpoline 背景下间接调用代价高),直接内联执行。
JUMP/GOTO 语义 :NFT_JUMP 把返回点压栈(类似 iptables 的 --goto/-j),NFT_GOTO 不压栈(不会返回)。最大跳转深度为 NFT_JUMP_STACK_SIZE(16)。
寄存器结果传递 :每个 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_conn(include/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 { struct nf_conntrack ct_general ; spinlock_t lock; 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; struct nf_conn *master ; 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 视角看到的连接信息编码在 ctinfo(enum 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_ROUTING 和 LOCAL_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_conn;nf_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; 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 , XDP_MODE_DRV = 1 , XDP_MODE_HW = 2 , __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_buff(include/net/xdp.h)描述接收到的帧:
1 2 3 4 5 6 7 8 9 10 struct xdp_buff { void *data; void *data_end; void *data_meta; void *data_hard_start; 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; __u32 data_end; __u32 data_meta; __u32 ingress_ifindex; __u32 rx_queue_index; __u32 egress_ifindex; };
BPF 验证器会把对 xdp_md 字段的访问翻译成对内核 xdp_buff 的实际访问,保证安全边界检查。
4.3 返回值语义 1 2 3 4 5 6 7 8 enum xdp_action { XDP_ABORTED = 0 , XDP_DROP, XDP_PASS, XDP_TX, XDP_REDIRECT, };
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 类型不同,行为各异:
DEVMAP (BPF_MAP_TYPE_DEVMAP):将包转发到另一个网口(ifindex),实现高性能软件交换。
CPUMAP (BPF_MAP_TYPE_CPUMAP):将包重定向到另一个 CPU 的 XDP 处理队列,实现多核负载均衡。仅 Native XDP 支持。
XSKMAP (BPF_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 struct tc_cls_bpf_offload cls_bpf = {};
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 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 nft list ruleset iptables -L -n -v --line-numbers ip6tables -L -n -v 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 conntrack -L -p tcp --state ESTABLISHED | wc -l conntrack -S cat /proc/sys/net/netfilter/nf_conntrack_countcat /proc/sys/net/netfilter/nf_conntrack_maxecho 2000000 > /proc/sys/net/netfilter/nf_conntrack_max
conntrack 表满导致丢包的排查 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 cat /proc/net/stat/nf_conntrackdmesg | grep "nf_conntrack: table full" 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 bpftool prog list bpftool prog dump xlated id <prog_id> bpftool prog dump jited id <prog_id> ip link set eth0 xdp obj xdp_prog.o sec xdp ip link set eth0 xdp off ip link show eth0 | grep xdp tc qdisc add dev eth0 clsact tc filter add dev eth0 ingress bpf obj tc_prog.o sec classifier direct-action 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 bpftrace -e 'kprobe:nft_do_chain { @[str(args->chain->name)] = count(); }' bpftrace -e ' kprobe:nf_hook_slow { @[args->state->hook] = count(); }' bpftrace -e 'kprobe:nf_ct_alloc_hashtable { @alloc = count(); } kretprobe:nf_ct_alloc_hashtable / retval == 0 / { @fail = count(); }' bpftrace -e 'tracepoint:xdp:xdp_exception { @[args->act] = count(); }' bpftrace -e 'tracepoint:xdp:xdp_redirect_err { @err = count(); }' bpftrace -e 'kprobe:nf_conntrack_in { @[nsecs/1000000000] = count(); }'
7.5 性能热点分析 1 2 3 4 5 6 7 8 9 10 11 12 perf stat -e 'net:netif_receive_skb,net:net_dev_xmit' -a sleep 5 cat /proc/net/stat/nf_conntrack | awk '{print $1, $2}' | head -20ethtool -S eth0 | grep -i xdp cat /proc/sys/net/core/bpf_jit_enable
八、整体架构总结 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)的内核实现。