Linux内核分析之网络协议栈-03

This language version is unavailable; showing the other language.

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, &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;
        }
        /* 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——本卷三次引用的内核/用户态配置总线。