Linux内核分析之网络协议栈-03
47.1 Netfilter 钩子与 hook 点
Netfilter 框架本体极小:五个钩子点 + 一个注册接口 + 一个执行器,其余一切(NAT、过滤、跟踪)都是钩子消费者。框架的工程精华在于无规则时近乎零成本(static_branch 短路)与同点多钩子的优先级协议。本节解剖框架与裁决协议。
47.1.1 五个钩子点:位置即语义
// include/uapi/linux/netfilter.h:43-47
NF_INET_PRE_ROUTING, /* 路由决策前 */
NF_INET_LOCAL_IN, /* 交给本机 sock 前 */
NF_INET_FORWARD, /* 转发路径中 */
NF_INET_LOCAL_OUT, /* 本机产生, 路由决策后 */
NF_INET_POST_ROUTING, /* 出设备前 */
五点在协议链上的精确位置 (46 章流程的插入点):
收包: ip_rcv (46.1)
└─▶ [PREROUTING] ← DNAT 唯一合理处!
(改目的地址必须在路由决策之前,
否则路由查的是旧地址)
路由决策 ─┬─ 本地 ─▶ [INPUT] ─▶ sock (45.2 队列)
└─ 转发 ─▶ [FORWARD] ─┐
(TTL 递减/分片检查) │
发包: sock (45.1) ─▶ 路由 ─▶ [OUTPUT] ──┴─▶ [POSTROUTING] ─▶ 设备
(改源地址前路由已定) ← SNAT/伪装处
(下一跳确定后改源)
每点语义由"位置"唯一决定 — 这就是为什么
DNAT 只在 PREROUTING、SNAT 只在 POSTROUTING/OUTPUT:
地址改变与路由决策的先后关系不可颠倒
47.1.2 nf_hook_ops:注册协议
// include/linux/netfilter.h:98
struct nf_hook_ops {
nf_hookfn *hook; /* 钩子函数 */
struct net_device *dev; /* 可绑指定设备(NULL=全部) */
void *priv;
u8 pf; /* 协议族 (NFPROTO_IPV4/IPV6/BRIDGE...) */
enum nf_hook_ops_type hook_ops_type:8;
unsigned int hooknum; /* 挂五个点中的哪个 */
int priority; /* 同点执行序 (数字小先跑) */
};
// net/netfilter/core.c:554
int nf_register_net_hook(struct net *net, const struct nf_hook_ops *reg)
优先级协议让多消费者在同一点有序协作:conntrack(priority ~-200)先于 mangle(-150)先于 filter(0)先于 NAT 源转换(100)——"跟踪建条目 → 过滤匹配 → 出口改源地址"的顺序不是约定而是优先级强制。dev 字段支持"只在 eth0 上过滤";pf 支持协议族隔离(IPv4/IPv6/桥各一套五点)。
per-net namespace 隔离(9 章):nf_register_net_hook 的 net 参数让容器内 nftables 规则与宿主互不可见——同一物理网卡的包按所属 netns 走各自的钩子链。
47.1.3 执行器:nf_hook_slow 与零成本短路
// net/netfilter/core.c:616
int nf_hook_slow(struct sk_buff *skb, struct nf_hook_state *state,
const struct nf_hook_entries *e, unsigned int i)
// :35
struct static_key nf_hooks_needed[NFPROTO_NUMPROTO][NF_MAX_HOOKS];
执行器的裁决协议:
按 priority 升序逐个调钩子, 每个返回:
NF_ACCEPT 继续 (下一个钩子)
NF_DROP 丢弃 (立即终结, 原因随 state)
NF_STOLEN skb 所有权转移 (异步处理, 不再回栈)
NF_QUEUE 排队给用户态 (NFQUEUE 号 —
用户态防病毒/深度检测的场景,
nfnetlink_queue 接收)
NF_REPEAT 重跑本钩子 (少见: 钩子内改了
需要自己重验的东西)
零成本短路 (static_key, :35):
nf_hooks_needed[pf][hooknum] 是静态键:
该点从未注册钩子 → 协议栈代码里的
nf_hook() 调用点被编译成"跳过"(nop) —
无规则的机器五个点穿越成本 ≈ 一次分支预测
注册第一个钩子时 text patch 打开该键
(与 21 章 static_branch 家族同源)
钩子链的存储: nf_hook_entries 紧凑数组
(注册/注销时重建 + RCU 替换 — 19.5/47.2 节
"双代切换"思想的又一次出现)
47.1.4 钩子消费者全景
| 消费者 | 挂点 | 优先级 | 职责 |
|---|---|---|---|
| conntrack | PREROUTING/OUTPUT(+其余刷新) | ~-200 | 47.3 节状态跟踪 |
| mangle | 全部五点 | -150 | 报文头改写(TOS/TTL/mark) |
| NAT DNAT | PREROUTING/OUTPUT | -100 | 目的转换 |
| filter | INPUT/FORWARD/OUTPUT | 0 | 过滤(nftables filter 链) |
| NAT SNAT | POSTROUTING/INPUT | 100 | 源转换/伪装 |
观察面:/proc/net/netfilter/nf_log、nft list ruleset(规则即钩子链的内容化视图)、perf 中 nf_hook_slow 的占比(防火墙 CPU 成本)。
小结
Netfilter 框架以"五点(位置即语义:DNAT 前于路由、SNAT 后于路由)+ 优先级链(conntrack→mangle→filter→NAT 强制序)+ 四态裁决(ACCEPT/DROP/STOLEN/QUEUE)"把过滤/NAT/跟踪标准化;nf_hooks_needed 静态键让无规则机器的穿越成本近乎零,per-net 注册让容器防火墙隔离。它是"内核留白"设计的典范——框架只定位置与协议,语义全部下沉消费者。下一节看最大的消费者 nftables。
47.2 nftables 子系统
nftables 把防火墙规则统一为"表达式虚拟机":规则 = 表达式的线性序列(在通用寄存器上逐个求值),表/链/集合全部经 netlink(48 章)原子配置——取代 iptables 的 filter/nat/mangle/raw 多套重复实现。本节解剖其对象模型、求值引擎(nft_do_chain 的源码级走读)、双代原子替换与集合后端。
47.2.1 对象模型:table → chain → rule → expr
// include/net/netfilter/nf_tables.h:1314/1142/1002/409(节选要点)
struct nft_table { ... /* 链/集合/对象的容器, per 协议族 */ };
struct nft_chain {
...
struct nft_rule_blob __rcu *blob_gen_0; /* 双代规则 blob */
struct nft_rule_blob __rcu *blob_gen_1; /* (47.2.3 节) */
...
u8 flags; /* filter/nat/route 型 */
};
struct nft_rule {
struct list_head list;
unsigned char data[] /* 表达式序列 */
__attribute__((aligned(__alignof__(struct nft_expr))));
};
struct nft_expr {
const struct nft_expr_ops *ops; /* eval/克隆/销毁 */
unsigned char data[] /* 表达式私有数据 */
__attribute__((aligned(__alignof__(struct nft_expr))));
};
对象与命令的对应:
nft add table inet mytab → struct nft_table
nft add chain mytab input { type filter hook input priority 0 ; }
→ nft_chain (挂 47.1 节钩子点)
nft add rule mytab input ip saddr @bad drop
→ nft_rule:
[payload: 载 ip saddr → reg1]
[lookup: reg1 ∈ set @bad?]
[immediate: verdict drop]
─ 表达式序列内联在规则的 data[] 尾部 (同块内存,
每包遍历零间接分配)
47.2.2 求值引擎:nft_do_chain 走读
// net/netfilter/nf_tables_core.c:250(节选走读)
nft_do_chain(struct nft_pktinfo *pkt, void *priv)
{
const struct nft_chain *chain = priv;
struct nft_regs regs; /* 通用寄存器组 */
unsigned int stackptr = 0;
struct nft_jumpstack jumpstack[NFT_JUMP_STACK_SIZE]; /* jump/return 栈 */
bool genbit = READ_ONCE(net->nft.gencursor); /* 读哪一代 */
struct nft_rule_blob *blob;
...
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;
}
/* jump/return/goto/drop/accept ... */
}
...
}
虚拟机的三件机关:
[1] 寄存器数据流: 表达式读写统一的 nft_regs —
payload 把报文字段载入 reg, cmp/lookup 消费 reg,
任意协议字段 = 一个 payload 表达式 (无需为
每种协议写专门匹配代码)
[2] 快慢路径: cmp_fast/bitwise_fast/payload_fast
是高频表达式的内联特化 (常量偏移直接读);
其余走 expr_call_ops_eval 通用分发 —
"90% 的规则是简单比较"的针对性优化
[3] 双代 blob + gencursor: 读侧一次 READ_ONCE
选定代, 整包查找期间代不切换 (规则变更在
另一代进行) — 查找无锁的根基 (47.2.3)
47.2.3 原子替换与集合后端
双代机制 (与 19.5/44 章 RCU 思想同族):
规则变更 (netlink, 48 章):
在非活动代构造新 blob → netlink 提交点
切 gencursor → 后续包读新代 → 旧代等 RCU
宽限释放
→ 防火墙规则"整批原子生效"; 查找全程无锁
(对照 iptables: 整表复制替换, 成本高)
set (集合) 与后端选择:
nft add set @bad { type ipv4_addr; flags timeout; }
hash 型: O(1) 精确匹配 (大量 IP)
rbtree 型: 区间/前缀 (CIDR 黑名单)
bitmap 型: 小范围枚举
动态元素: timeout/计数器/限速附着在元素上
(动态黑名单: 规则不动, 集合自更新)
flowtable: 已建立连接卸载到快速路径
(软件流表或网卡流表 — 转发类负载跳过协议栈)
与 iptables 的关系: iptables-nft 把老语法翻译到
nft 内核设施 — 内核只保留一套引擎
47.2.4 与 tc 的分界
| 维度 | nftables (netfilter) | tc (traffic control) |
|---|---|---|
| 主职 | 过滤/NAT/状态匹配 | 带宽整形/调度 |
| 位置 | 五钩子点(47.1 节) | qdisc(dev_queue_xmit,44.2 节) |
| 编程模型 | 表达式规则 + verdict | 类别树 + 队列算法 |
| 状态依赖 | ct(47.3) | 无连接概念 |
| 协同 | tc 过滤器可调 nft | flowtable 转发卸载 |
观测:nft list ruleset(规则的内核态视图,含计数器实值)、nft monitor(变更事件流,走 48 章 netlink 多播)、每规则的 counter 表达式是策略命中的直接证据。
小结
nftables 以"table/chain/rule/expr 四层对象 + 寄存器式表达式虚拟机"统一防火墙:nft_do_chain 的源码展示快慢路径分派(cmp_fast 内联特化 + 通用 eval)与双代 blob 的无锁查找;jumpstack 支撑 jump/return 的链间跳转;集合按特性选哈希/树/位图后端并支持动态超时元素。它是最重的钩子消费者,也是 47.1 节"框架留白"价值的最佳证明。下一节看有状态过滤的前提——连接跟踪。
47.3 连接跟踪 (conntrack)
conntrack 把无连接的 IP 流提升为"有状态的连接":每个流(方向化的四元组)一个 nf_conn,记录双向元组、状态与 NAT 变换,供有状态过滤(ct state established,related accept)、NAT、超时回收消费。它是现代 Linux 防火墙与云网络(K8s Service/NAT 网关)的基石。本节解剖跟踪入口、双向元组哈希、状态机与 NAT 耦合。
47.3.1 nf_conn 与双向元组
// include/net/netfilter/nf_conntrack.h:74(节选要点)
struct nf_conn {
struct nf_conntrack ct_general; /* 引用计数 */
struct nf_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];
/* 两个方向的哈希节点 — 双向查找的根基:
[0]=原始方向 (客户端→服务)
[1]=期望的回复方向 (服务→客户端) */
...
unsigned long status; /* IPS_SEEN_REPLY/ASSURED/DYING... */
u32 timeout; /* 超时到期(秒) */
...
/* NAT 变换信息 (可选, per 方向) */
};
// net/netfilter/nf_conntrack_core.c:2011
unsigned int nf_conntrack_in(struct sk_buff *skb, const struct nf_hook_state *state)
tuple (元组) 的构成:
原始方向: {src_ip, src_port, dst_ip, dst_port, proto}
回复方向: {dst_ip, dst_port, src_ip, src_port, proto}
(方向翻转 — 记录"回包该长什么样")
同一 nf_conn 的两个 tuple_hash 都入全局哈希表
→ 任意方向的包一次哈希查到同一连接
(协议相关: TCP 按端口, ICMP 按 id/类型/码 —
ICMP 也有"连接"概念: ping 的 request/reply)
47.3.2 跟踪入口的裁决
// net/netfilter/nf_conntrack_core.c:2011(签名)
unsigned int nf_conntrack_in(struct sk_buff *skb,
const struct nf_hook_state *state)
nf_conntrack_in 的流程 (挂在 PREROUTING/OUTPUT,
优先级 -200 — 早于 filter, 47.1.4 节序表):
[1] 解析五元组 → 构造原始方向 tuple
[2] 双向查哈希:
命中原始方向 → 既有流: 刷新超时
(若此前未见过回复 → 置 IPS_SEEN_REPLY
= 状态升级为 ESTABLISHED)
命中回复方向 → 同一连接的回包
未命中 → 新建 nf_conn (双向 tuple 入哈希)
首包标 IP_CT_NEW
[3] 校验: TCP 序号窗口/校验和 → 不合法标 INVALID
[4] 资源尽: 哈希满/内存不足 → NF_DROP
(unconfirmed 链限制 — "table full, dropping
packet" 日志的出处)
状态标签 (供 nft ct state / iptables -m conntrack):
NEW 首包 (未见过回复)
ESTABLISHED 双向都有包
RELATED 关联连接 (FTP 数据口/ICMP 错误
— helper 协议钩子负责"预期"登记)
INVALID 无法归属 (乱序/残包)
UNTRACKED 显式不跟踪 (NOTRACK)
47.3.3 NAT:conntrack 的附加工种
NAT 的实现哲学: "变换记录在 nf_conn, 回包自动还原"
DNAT (PREROUTING, 47.1.1 位置语义):
目的改写 (10.0.0.5:80 → 192.168.1.10:8080)
nf_conn 的回复 tuple 同步记录"回包源要改回"
SNAT/伪装 (POSTROUTING):
源改写 (伪装 = 取出口设备 IP, 端口耗尽时改
源端口保唯一性 — masquerade 的端口分配器)
回复包经回复 tuple 反向翻译 — 内网客户端无感
流程图 (一个 DNAT+SNAT 网关):
客户端 ──{C→S: cip:cport → vip:vport}──▶ [PREROUTING]
conntrack 新建 + DNAT 改目的 │
路由 → [POSTROUTING]
SNAT 改源 (cip→gw_ip)
服务 ◀──{gw_ip:gwport → sip:sport}──────────┘
服务回包 ──{sip:sport → gw_ip:gwport}──▶ 网关 [PREROUTING]
命中回复 tuple → 自动反译两处地址
──{vip:vport → cip:cport}──▶ 客户端 (无感)
47.3.4 超时状态机与容量治理
超时表 (per 协议, /proc/sys/net/netfilter/ 下可调):
TCP: SYN_SENT(2m) → ESTABLISHED(5 天!) →
TIME_WAIT(2m) / CLOSE(10s)...
UDP: unreplied(30s) → assured(已双向, 180s)
ICMP: 30s
机制: 定时器 + 垃圾回收器双驱动; 有包刷新
容量治理 (云主机的第一调参面):
nf_conntrack_max 条目上限 (内存按此预估)
nf_conntrack_buckets 哈希桶数 (建议=max/4)
hashsize 启动参数 桶的实际粒度
打满的表现: "nf_conntrack: table full, dropping
packet" + 随机丢包 — 大规模 K8s NAT 网关的
经典事故
排查: conntrack -S (统计) / -L (全表, 走 ctnetlink
即 48 章 netlink 的 nf 子协议)
47.3.5 与本卷的耦合图
conntrack 的消费面全景:
47.1 钩子框架: 跟踪挂在 PREROUTING/OUTPUT (建/查)
+ FORWARD/IN/POST (刷新) — 优先级最先
47.2 nftables: ct state / ct helper / ct count
表达式消费状态 (有状态防火墙的一行规则
= "回复包全部放行" 的语义)
NAT: 伪装网关/K8s Service/出向代理的地址变换
K8s/kube-proxy: Service 负载均衡 = 一条 DNAT 规则
+ conntrack 保会话粘性
VM/云网络: 虚拟路由器的 NAT 同跑此设施
小结
conntrack 以"双向 tuple 哈希 + 状态标签 + per 协议超时"把 IP 流升为可匹配的"连接":nf_conntrack_in 在首包建双向条目、回复包置 SEEN_REPLY 升级 ESTABLISHED、helper 机制派生 RELATED;NAT 是附加工种——变换记在 nf_conn 内由回复方向自动反译(DNAT 前于路由、SNAT 后于路由的位置语义在 47.1 节);容量上限与超时表是云环境的常规巡检面。Netfilter 三章至此完成;下一章进入 Netlink——本卷三次引用的内核/用户态配置总线。