土法炼钢兴趣小组的算法知识备份

【Linux 网络子系统深度拆解】Netfilter 内核实现:钩子、conntrack 与 NAT

文章导航

分类入口
linuxnetworking
标签入口
#netfilter#conntrack#nat#iptables#nftables#nf-hook#nf-conn#snat#dnat#bpftrace

目录

路由子系统 决定了包往哪走,但”这个包能不能走”的决策权在 Netfilter。iptables、nftables、conntrack、NAT——这些耳熟能详的工具,底层都建立在同一个内核框架上。

Netfilter 的设计哲学是钩子(hook):在收发包路径的五个关键位置插入回调链,每个包依次经过所有注册的回调,由它们决定放行、丢弃还是修改。如果你读过 k8s-network 系列的 Netfilter 与 iptables,那篇文章从用户态视角讲了五链四表的用法。本文深入内核,拆解三个核心实现:钩子注册与遍历、连接跟踪(conntrack)、NAT 转换路径。

一、五个钩子点

Netfilter 在 IPv4 收发路径中插入五个钩子点(include/uapi/linux/netfilter.h):

                          ┌────────────────┐
                          │   NF_INET_     │
                          │  PRE_ROUTING   │ ← 进入本机的包(路由前)
                          │   (hook 0)     │
                          └───────┬────────┘
                                  │
                           路由决策
                          ╱            ╲
               本机接收 ╱                ╲ 转发
        ┌──────────────┐          ┌──────────────┐
        │  NF_INET_    │          │  NF_INET_    │
        │  LOCAL_IN    │          │  FORWARD     │
        │  (hook 1)    │          │  (hook 2)    │
        └──────┬───────┘          └──────┬───────┘
               │                         │
         本地进程                         │
               │                         │
        ┌──────┴───────┐                 │
        │  NF_INET_    │                 │
        │  LOCAL_OUT   │                 │
        │  (hook 3)    │                 │
        └──────┬───────┘                 │
               │                         │
               └──────────┬──────────────┘
                          │
                  ┌───────┴────────┐
                  │  NF_INET_      │
                  │  POST_ROUTING  │ ← 离开本机的包(路由后)
                  │  (hook 4)      │
                  └────────────────┘

每个钩子点可以注册多个回调,按优先级排序执行。IPv4 的标准优先级定义在 include/uapi/linux/netfilter_ipv4.h:30

优先级 常量 用途
-450 NF_IP_PRI_RAW_BEFORE_DEFRAG 分片重组前的 raw 规则
-400 NF_IP_PRI_CONNTRACK_DEFRAG 分片重组(conntrack 依赖)
-300 NF_IP_PRI_RAW raw 表(NOTRACK 在这里生效)
-200 NF_IP_PRI_CONNTRACK 连接跟踪查找/创建
-150 NF_IP_PRI_MANGLE mangle 表(修改包头)
-100 NF_IP_PRI_NAT_DST DNAT(目的地址转换)
0 NF_IP_PRI_FILTER filter 表(放行/丢弃)
50 NF_IP_PRI_SECURITY SELinux 安全策略
100 NF_IP_PRI_NAT_SRC SNAT(源地址转换)
300 NF_IP_PRI_CONNTRACK_HELPER 应用层协议助手
INT_MAX NF_IP_PRI_CONNTRACK_CONFIRM 连接确认

关键顺序:raw(-300)在 conntrack(-200)之前——NOTRACK 规则可以在连接跟踪之前标记包,让它完全跳过 conntrack。DNAT(-100)在路由之前,SNAT(100)在路由之后——因为 DNAT 改变目的地址会影响路由决策。

二、钩子注册与遍历

nf_hook_ops:注册结构

每个模块通过 struct nf_hook_opsinclude/linux/netfilter.h:97)注册钩子:

struct nf_hook_ops {
    nf_hookfn   *hook;        /* 回调函数 */
    struct net_device *dev;   /* 绑定设备(可选) */
    void        *priv;        /* 私有数据 */
    u8           pf;          /* 协议族:NFPROTO_IPV4/IPV6 */
    unsigned int hooknum;     /* 钩子点编号(0-4) */
    int          priority;    /* 优先级(小优先) */
};

回调函数签名:

typedef unsigned int nf_hookfn(void *priv,
                               struct sk_buff *skb,
                               const struct nf_hook_state *state);

返回值(verdict)决定包的命运:

Verdict 含义
NF_DROP 0 丢弃,释放 skb
NF_ACCEPT 1 继续下一个钩子
NF_STOLEN 2 钩子接管 skb,框架不再处理
NF_QUEUE 3 排入用户态队列(NFQUEUE)
NF_REPEAT 4 再次调用当前钩子

nf_hook_entries:运行时数据结构

注册后的钩子被编译为 struct nf_hook_entriesnetfilter.h:119)——一个紧凑的数组,按优先级排序:

struct nf_hook_entries {
    u16  num_hook_entries;        /* 钩子数量 */
    struct nf_hook_entry hooks[]; /* 钩子数组 */
    /* 尾部隐含:
     * const struct nf_hook_ops *orig_ops[];
     * struct nf_hook_entries_rcu_head head;
     */
};

struct nf_hook_entry {
    nf_hookfn  *hook;             /* 函数指针 */
    void       *priv;             /* 私有数据 */
};

为什么不直接用链表? 因为数组的 cache locality 远好于链表——每个包都要遍历所有钩子,紧凑数组可以预取到 L1 cache。nf_hook_entry 只有两个指针(16 字节),一条 cache line 可以容纳 4 个钩子。

NF_HOOK 宏:遍历入口

NF_HOOK()netfilter.h)是协议栈调用 Netfilter 的入口:

static inline int NF_HOOK(uint8_t pf, unsigned int hook,
                          struct net *net, struct sock *sk,
                          struct sk_buff *skb,
                          struct net_device *in, struct net_device *out,
                          int (*okfn)(struct net *, struct sock *,
                                      struct sk_buff *))

Jump Label 快路径:当某个钩子点没有注册任何回调时,static_key 机制让 NF_HOOK 几乎零开销——编译器把检查替换为 NOP 指令,只在注册/注销时修改:

#ifdef CONFIG_JUMP_LABEL
extern struct static_key nf_hooks_needed[NFPROTO_NUMPROTO][NF_MAX_HOOKS];
/* 如果 static_key 为 false,直接调用 okfn(),1-2 个 CPU 周期 */
if (!static_key_false(&nf_hooks_needed[pf][hook]))
    return okfn(net, sk, skb);
#endif

遍历时,nf_hook_slow() 逐个调用 hooks[i].hook(),直到所有钩子返回 NF_ACCEPT 或某个钩子返回 NF_DROP。整个遍历在 rcu_read_lock() 保护下进行——允许并发注册/注销。

三、连接跟踪(conntrack)

nf_conn:连接对象

连接跟踪是 Netfilter 最重的子系统——它为每条连接维护一个 struct nf_conninclude/net/netfilter/nf_conntrack.h:75)对象:

struct nf_conn {
    struct nf_conntrack     ct_general;    /* 引用计数 */
    spinlock_t              lock;          /* 每连接锁 */
    u32                     timeout;       /* 超时(jiffies32) */

    /* 双向元组哈希 */
    struct nf_conntrack_tuple_hash
        tuplehash[IP_CT_DIR_MAX];          /* [0]=原方向, [1]=回复方向 */

    unsigned long           status;        /* 连接状态位图 */
    possible_net_t          ct_net;        /* 网络命名空间 */
    struct nf_conn          *master;       /* 父连接(用于 expected) */

    u_int32_t               mark;          /* 连接标记(iptables -j CONNMARK) */
    u_int32_t               secmark;       /* 安全标记 */

    struct nf_ct_ext        *ext;          /* 扩展(NAT、helper 等) */
    union nf_conntrack_proto proto;        /* L4 协议特定数据 */
};

连接元组

struct nf_conntrack_tuplenf_conntrack.h:37)唯一标识一条流:

struct nf_conntrack_tuple {
    struct nf_conntrack_man src;       /* 源:IP + 端口/类型 + L3 协议 */
    struct {
        union nf_inet_addr u3;         /* 目的 IP */
        union {
            __be16 all;
            struct { __be16 port; } tcp, udp, sctp;
            struct { u_int8_t type, code; } icmp;
        } u;
        u_int8_t protonum;            /* L4 协议号 */
        u_int8_t dir;                 /* 方向 */
    } dst;
};

每个 nf_conn 存储两个方向的元组: - tuplehash[IP_CT_DIR_ORIGINAL]:客户端 → 服务器 - tuplehash[IP_CT_DIR_REPLY]:服务器 → 客户端

两个方向的元组都插入同一个全局哈希表——查找时,不管包从哪个方向来,都能找到对应的连接。

哈希表实现

conntrack 哈希表是一个开链哈希表,大小由 nf_conntrack_hashsize 控制(默认约 65536):

哈希表大小:nf_conntrack_hashsize(可调)
最大连接数:nf_conntrack_max = hashsize × 2(默认)
桶锁数量:CONNTRACK_LOCKS = 1024(减少锁竞争)

查找路径:

nf_conntrack_find_get(net, zone, &tuple)
    → hash = hash_conntrack_raw(&tuple, zone, net)
    → bucket = hash % hashsize
    → 在 bucket 链中比较 tuple
    → 找到 → 增加引用计数,返回
    → 未找到 → 返回 NULL(需要创建新连接)

1024 个桶锁(不是每桶一锁)——锁粒度足够细以支持高并发,但不会浪费过多内存。

conntrack 在收发包路径中的位置

收包路径(PRE_ROUTING):
ip_rcv() → NF_HOOK(NF_INET_PRE_ROUTING, ...)
    ├── priority -400: conntrack_defrag → 重组 IP 分片
    ├── priority -200: nf_conntrack_in()
    │   ├── 提取五元组 → nf_conntrack_tuple
    │   ├── 哈希查找已有连接
    │   │   ├── 找到 → skb->_nfct = ct(关联连接)
    │   │   └── 未找到 → 创建 unconfirmed 连接
    │   └── 更新 L4 状态(TCP 状态跟踪)
    └── priority INT_MAX: nf_conntrack_confirm()
        └── 将 unconfirmed 连接插入哈希表

发包路径(LOCAL_OUT + POST_ROUTING):
类似,但方向为 ORIGINAL

Unconfirmed 连接:新创建的连接不立即插入哈希表——等包完整通过所有钩子(没被 DROP)后,在 POST_ROUTINGnf_conntrack_confirm() 才确认。这避免了 DROP 包留下垃圾条目。

连接状态

状态 含义 典型场景
NEW 首包,连接刚创建 SYN 包
ESTABLISHED 双向都有包 SYN+ACK 后
RELATED 与已有连接关联的新连接 FTP 数据连接
INVALID 不属于任何已知连接 状态不一致的包

GC 与超时

conntrack 条目通过超时自动过期。不同协议有不同的默认超时:

协议/状态 默认超时 sysctl
TCP ESTABLISHED 5 天 nf_conntrack_tcp_timeout_established
TCP TIME_WAIT 120 秒 nf_conntrack_tcp_timeout_time_wait
UDP 30 秒 nf_conntrack_udp_timeout
UDP stream 120 秒 nf_conntrack_udp_timeout_stream
ICMP 30 秒 nf_conntrack_icmp_timeout

GC 通过工作队列定期扫描过期条目。当表接近 nf_conntrack_max 时,GC 变得更激进——优先淘汰最近未使用的连接。

四、NAT 转换路径

NAT 类型

类型 Hook 点 操作
DNAT PRE_ROUTING / LOCAL_OUT 修改目的 IP/端口
SNAT POST_ROUTING / LOCAL_IN 修改源 IP/端口
MASQUERADE POST_ROUTING SNAT + 自动选择出接口 IP
REDIRECT PRE_ROUTING DNAT 到本机端口

NAT 与 conntrack 的关系

NAT 依赖 conntrack——只有首包执行 NAT 规则查找和转换,后续包根据 conntrack 条目自动转换:

首包(NEW 状态):
nf_nat_setup_info(ct, range, maniptype)
    ├── 选择新的 IP/端口
    │   └── 避免与已有 NAT 映射冲突
    ├── 修改 ct->tuplehash[REPLY] 的元组
    │   └── 回复方向的源 → 成为 DNAT 的原始目的
    └── 标记 ct->status |= IPS_NAT_DONE_MASK

后续包(ESTABLISHED 状态):
nf_nat_packet(ct, ctinfo, hooknum, skb)
    ├── 根据方向和类型确定修改哪些字段
    └── nf_nat_manip_pkt(skb, ct, mtype, dir)
        └── 直接修改包头(IP + L4 checksum)

HOOK2MANIP 宏

#define HOOK2MANIP(hooknum) \
    ((hooknum) != NF_INET_POST_ROUTING && \
     (hooknum) != NF_INET_LOCAL_IN)
hooknum HOOK2MANIP 含义
PRE_ROUTING 1 (DST) DNAT:修改目的
LOCAL_IN 0 (SRC) SNAT 回程:修改源
FORWARD 1 (DST)
LOCAL_OUT 1 (DST) DNAT:本机重定向
POST_ROUTING 0 (SRC) SNAT:修改源

MASQUERADE 的特殊性

MASQUERADE 是 SNAT 的动态版本——源 IP 自动选择出接口的当前地址。当接口 IP 变化(DHCP 重新分配)时,MASQUERADE 自动失效旧连接。nf_conn_nat->masq_index 记录绑定的接口索引。

五、nf_tables 架构

与 iptables 的核心差异

iptables 的规则存储为链表,每条规则是一个 struct ipt_entry 加 match/target 数组。问题: 1. 规则更新需要整表替换(iptables-restore),O(n) 复杂度 2. 内存布局不紧凑,cache 命中率低 3. match/target 是独立内核模块,函数调用开销大

nf_tables 的架构改进:

表和链

struct nft_tableinclude/net/netfilter/nf_tables.h:1283):

struct nft_table {
    struct rhltable chains_ht;     /* 链的哈希表——O(1) 查找 */
    struct list_head chains;       /* 链列表 */
    struct list_head sets;         /* 集合列表 */
    u64  hgenerator;               /* 句柄自增器 */
    u16  family:6, flags:8, genmask:2;  /* 协议族和代际掩码 */
};

struct nft_chainnf_tables.h:1113):

struct nft_chain {
    struct nft_rule_blob __rcu *blob_gen_0;  /* 第 0 代规则 blob */
    struct nft_rule_blob __rcu *blob_gen_1;  /* 第 1 代规则 blob */
    struct list_head rules;                   /* 控制面规则列表 */
    u64  handle:42, genmask:2;
};

Generation-based 无锁更新

nftables 的核心创新:双代(generation)更新

         数据面(RCU 读)              控制面(更新)
         ──────────────              ──────────────
当前代=0  读 blob_gen_0               修改 blob_gen_1
         │                           │
         切换代: 当前代=1             │
         │                           │
         读 blob_gen_1               修改 blob_gen_0(旧代回收)

规则更新时: 1. 在非当前代的 blob 上修改规则 2. 原子切换当前代(genmask 翻转) 3. 等待旧代的所有 RCU 读者退出 4. 回收旧代内存

数据面始终读当前代——无锁、无等待。对比 iptables 的全表替换,这是质的飞跃。

Blob 化规则存储

struct nft_rule_blobnf_tables.h:1088)把一条链的所有规则紧凑排列在连续内存中:

struct nft_rule_blob {
    unsigned long size;        /* 总大小 */
    unsigned char data[];      /* 连续的 nft_rule_dp 数组 */
};

struct nft_rule_dp {
    u64  is_last:1, dlen:12, handle:42;  /* 紧凑位域 */
    unsigned char data[];                 /* 表达式字节码 */
};

所有规则在一块连续内存中——遍历时 CPU prefetcher 可以高效预取,cache 命中率远高于链表。

表达式求值

nftables 的规则由表达式(expression)序列组成:

struct nft_expr {
    const struct nft_expr_ops *ops;  /* 表达式类型 */
    unsigned char data[];            /* 类型特定数据 */
};

表达式在寄存器struct nft_regs,20 个 u32 寄存器)上操作:

规则求值示例:ip daddr 10.0.0.0/24 tcp dport 80 accept

nft_payload_eval()     → 从包头加载 dst IP 到 reg[0]
nft_cmp_eval()         → 比较 reg[0] 与 10.0.0.0/24
nft_payload_eval()     → 从包头加载 dst port 到 reg[0]
nft_cmp_eval()         → 比较 reg[0] 与 80
nft_immediate_eval()   → 设置 verdict = NF_ACCEPT

集合(Set)

nftables 的 set 支持多种后端实现——根据元素数量和类型自动选择:

后端 用途 复杂度
hash 精确匹配大集合 O(1)
rbtree 区间匹配(端口范围、IP 范围) O(log n)
bitmap 小范围连续值(0-65535 端口) O(1)
pipapo 多维匹配(IP + 端口组合) O(n × fields)

六、性能调优要点

conntrack 表大小

# 查看当前连接数
conntrack -C
cat /proc/sys/net/netfilter/nf_conntrack_count

# 查看最大值
cat /proc/sys/net/netfilter/nf_conntrack_max

# 调整(每条约 288 字节)
sysctl -w net.netfilter.nf_conntrack_max=262144
sysctl -w net.netfilter.nf_conntrack_hashsize=65536

经验法则:nf_conntrack_max = hashsize × 2,hashsize 取 2 的幂。

NOTRACK 旁路

对于不需要连接跟踪的高吞吐流量(如 DNS 转发、负载均衡器直连),用 raw 表跳过 conntrack:

# iptables
iptables -t raw -A PREROUTING -d 10.0.0.0/8 -p udp --dport 53 -j NOTRACK

# nftables
nft add rule ip raw prerouting ip daddr 10.0.0.0/8 udp dport 53 notrack

raw 表在优先级 -300 执行,conntrack 在 -200——NOTRACK 标记的包完全跳过连接跟踪和 NAT,大幅降低 CPU 开销。

Jump Label 零开销

如果某个钩子点完全没有注册任何回调(比如没有 Netfilter 规则的纯路由器),static_keyNF_HOOK 编译为一条 NOP + 直接调用 okfn()——1-2 个 CPU 周期,几乎不存在。

七、可观测性实战

conntrack 事件监控

# 实时监控连接创建/销毁
conntrack -E

# 按协议统计
conntrack -L -p tcp | wc -l
conntrack -L -p udp | wc -l

conntrack 表满检测

# 追踪 nf_conntrack_max 限制导致的丢包
bpftrace -e '
tracepoint:net:nf_conntrack_drop {
    printf("conntrack DROP! reason=%d\n", args->reason);
    @drops = count();
}
interval:s:10 { print(@drops); clear(@drops); }'

# 或监控内核日志
dmesg | grep "nf_conntrack: table full"

Netfilter 钩子延迟

# 测量 PRE_ROUTING 钩子链的总耗时
bpftrace -e '
kprobe:nf_hook_slow {
    @start[tid] = nsecs;
}
kretprobe:nf_hook_slow /@start[tid]/ {
    @hook_ns = hist(nsecs - @start[tid]);
    delete(@start[tid]);
}'

NAT 映射统计

# 查看 NAT 映射
conntrack -L --src-nat
conntrack -L --dst-nat

# 统计 NAT 端口分配失败
bpftrace -e '
kretprobe:nf_nat_setup_info /retval != 0/ {
    printf("NAT setup failed: %d\n", retval);
    @nat_failures = count();
}'

nftables 规则命中统计

# 查看每条规则的计数器
nft list ruleset -a
nft list chain ip filter input

# 追踪规则匹配
nft add rule ip filter input counter

八、关键参数速查

参数 默认值 含义 调优建议
nf_conntrack_max ~65536 最大连接跟踪数 高并发增大到 262144+
nf_conntrack_hashsize ~16384 哈希桶数 max / 2,取 2 的幂
nf_conntrack_tcp_timeout_established 432000 (5天) TCP 连接超时 短连接场景可减小
nf_conntrack_udp_timeout 30 UDP 首包超时 DNS 场景可减小
nf_conntrack_udp_timeout_stream 120 UDP 双向超时 按需调整
nf_conntrack_tcp_timeout_time_wait 120 TIME_WAIT 超时 通常不需调整
nf_conntrack_expect_max 256 最大 expect 条目 FTP/SIP 场景增大
nf_conntrack_tcp_loose 1 宽松 TCP 跟踪 严格环境设为 0
nf_conntrack_acct 0 启用流量计账 按需开启

参考文献

  1. Linux 内核源码,include/linux/netfilter.h,6.8(nf_hook_state 于第 78 行,nf_hook_ops 于第 97 行,nf_hook_entries 于第 119 行)
  2. Linux 内核源码,include/net/netfilter/nf_conntrack.h,6.8(nf_conn 于第 75 行)
  3. Linux 内核源码,include/net/netfilter/nf_nat.h,6.8(HOOK2MANIP 于第 18 行)
  4. Linux 内核源码,include/net/netfilter/nf_tables.h,6.8(nft_table 于第 1283 行,nft_chain 于第 1113 行)
  5. Linux 内核源码,include/uapi/linux/netfilter_ipv4.h,6.8(优先级常量于第 30 行)
  6. Pablo Neira Ayuso, “nftables - the new packet classification framework”, Netfilter Workshop, 2014
  7. Linux 内核文档,Documentation/networking/nf_conntrack-sysctl.rst

上一篇路由子系统深度拆解:FIB、策略路由与路由缓存

下一篇Traffic Control 深度拆解:qdisc、class 与 filter

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-20 · linux / networking

【Linux 网络子系统深度拆解】IP 层内核实现:路由查找、分片与转发

IP 层是 Linux 网络栈的中枢——收包时决定本地投递还是转发,发包时查路由、过 Netfilter、做分片。本文从 Linux 6.6 内核源码出发,拆解 ip_rcv → 路由决策 → ip_local_deliver / ip_forward 的完整路径,深入 FIB 表的 LC-trie 实现、策略路由 ip rule 选表机制、IP 分片/重组状态机、PMTU 发现与 FNHE 缓存,以及 Netfilter 五个钩子点的实际调用时机。


By .