Linux 网络内核协议栈深度剖析(三):IP 层路由查找与报文转发

IP 层(网络层)是 Linux 协议栈的核心,负责报文的寻址、路由、转发与分片重组。本文基于 Linux 6.4-rc1 源码,从 ip_rcv 入口函数出发,逐层拆解接收路径、FIB 路由查找、发送路径、分片重组和 netfilter 钩子的完整实现,并给出实用的内核诊断技巧。文中所有代码均来自 net/ipv4/ip_input.cnet/ipv4/ip_output.cnet/ipv4/fib_trie.cnet/ipv4/ip_fragment.cnet/ipv4/ip_forward.cnet/netfilter/core.c 等实际文件。

1. IP 接收路径

1.1 L3 入口:ip_rcv 与校验和检查

网络设备驱动通过 netif_receive_skbsk_buff 交给协议栈。内核在 ptype_base 哈希表中依据以太网帧的 EtherTypeETH_P_IP = 0x0800)找到注册的 packet_type 结构,调用其 func 指针,即 ip_rcv

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* net/ipv4/ip_input.c */
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt,
struct net_device *orig_dev)
{
struct net *net = dev_net(dev);

skb = ip_rcv_core(skb, net);
if (skb == NULL)
return NET_RX_DROP;

return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
net, NULL, skb, dev, NULL,
ip_rcv_finish);
}

实际的合法性验证集中在 ip_rcv_core 中,该函数逐一检查以下条件:

a) 混杂模式过滤:若 skb->pkt_type == PACKET_OTHERHOST,表示网卡在混杂模式下收到了非本机的帧,直接丢弃,统计 rx_otherhost_dropped

b) IP 头最小长度pskb_may_pull(skb, sizeof(struct iphdr)) 确保 IP 头的 20 字节已经在线性区(linear area)中可以直接访问,否则触发 pskb_may_pull 内部的拷贝操作。

c) 版本和 IHL 字段iph->ihl < 5 || iph->version != 4 时进入 inhdr_error 路径,统计 IPSTATS_MIB_INHDRERRORS

d) 校验和验证ip_fast_csum((u8 *)iph, iph->ihl) 对整个 IP 头做一次补码求和,结果非零即为错误,统计 IPSTATS_MIB_CSUMERRORS。对于本地回环接口,驱动会设置 CHECKSUM_UNNECESSARY,跳过此步骤。

e) 长度一致性:比较 skb->leniph->tot_len,若实际长度不足则触发 IPSTATS_MIB_INTRUNCATEDPKTS;若有填充字节(以太网最小帧填充),调用 pskb_trim_rcsum 裁剪至 IP 包声明的长度。

f) 控制块初始化memset(IPCB(skb), 0, sizeof(struct inet_skb_parm)) 清零 IP 控制块,并记录入接口索引 IPCB(skb)->iif,供后续路由和 netfilter 使用。

验证通过后,ip_rcv 调用 NF_HOOK 触发 PREROUTING 钩子,这是 netfilter 对接收方向的第一个拦截点。iptables -t nat -A PREROUTING(DNAT)、conntrack 初始化(优先级 -200)均在此处完成。

1.2 ip_rcv_finish 与 Early Demux

PREROUTING hook 返回 NF_ACCEPT 后,回调 ip_rcv_finish,它调用 ip_rcv_finish_core

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
/* net/ipv4/ip_input.c */
static int ip_rcv_finish_core(struct net *net, struct sock *sk,
struct sk_buff *skb, struct net_device *dev,
const struct sk_buff *hint)
{
const struct iphdr *iph = ip_hdr(skb);
int err, drop_reason;
struct rtable *rt;

/* 路由提示:批量处理同一流的报文时复用上一个报文的路由结果 */
if (ip_can_use_hint(skb, iph, hint)) {
err = ip_route_use_hint(skb, iph->daddr, iph->saddr, iph->tos, dev, hint);
if (unlikely(err))
goto drop_error;
}

/* Early demux:对 TCP/UDP 跳过完整 FIB 查找 */
if (READ_ONCE(net->ipv4.sysctl_ip_early_demux) &&
!skb_dst(skb) && !skb->sk && !ip_is_fragment(iph)) {
switch (iph->protocol) {
case IPPROTO_TCP:
if (READ_ONCE(net->ipv4.sysctl_tcp_early_demux))
tcp_v4_early_demux(skb);
break;
case IPPROTO_UDP:
if (READ_ONCE(net->ipv4.sysctl_udp_early_demux)) {
err = udp_v4_early_demux(skb);
if (unlikely(err))
goto drop_error;
}
break;
}
}

/* 路由查找(若 early demux 未绑定 dst)*/
if (!skb_valid_dst(skb)) {
err = ip_route_input_noref(skb, iph->daddr, iph->saddr, iph->tos, dev);
if (unlikely(err))
goto drop_error;
}
/* ... */
return NET_RX_SUCCESS;
}

Early demux 是 Linux 3.7 引入的优化:对于已建立的 TCP/UDP 连接,通过反查 socket 哈希表(__inet_lookup_established)直接定位到 socket,并从 socket 缓存的 sk->sk_dst_cache 获取路由结果,完全绕过 fib_lookup。在 10Gbps 以上的高速网络场景中,此优化可以减少约 10-15% 的 CPU 开销。

路由查找完成后,dst_entry 被绑定到 skb(skb->_skb_refdst)。ip_rcv_finish 调用 dst_input(skb),实际执行的是 skb_dst(skb)->input(skb)——这是一个函数指针,指向 ip_local_deliver(本机)或 ip_forward(转发)。

1.3 ip_local_deliver 与分片重组

对于目的地址属于本机的报文,dst->input 被设置为 ip_local_deliver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* net/ipv4/ip_input.c */
int ip_local_deliver(struct sk_buff *skb)
{
struct net *net = dev_net(skb->dev);

if (ip_is_fragment(ip_hdr(skb))) {
if (ip_defrag(net, skb, IP_DEFRAG_LOCAL_DELIVER))
return 0; /* 分片已入队,等待重组 */
}

return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN,
net, NULL, skb, skb->dev, NULL,
ip_local_deliver_finish);
}

ip_is_fragment 检查 frag_off 字段:若 IP_MF(More Fragments)位置位,或 frag_off 偏移量非零,则认为是分片报文。完整报文触发 NF_INET_LOCAL_IN(INPUT hook),iptables -A INPUT 规则在此生效。

1.4 ip_local_deliver_finish:协议分发表

INPUT hook 通过后,进入 ip_local_deliver_finiship_protocol_deliver_rcu

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
/* net/ipv4/ip_input.c */
void ip_protocol_deliver_rcu(struct net *net, struct sk_buff *skb, int protocol)
{
const struct net_protocol *ipprot;
int raw, ret;

resubmit:
raw = raw_local_deliver(skb, protocol); /* RAW socket 副本分发 */

ipprot = rcu_dereference(inet_protos[protocol]);
if (ipprot) {
if (!ipprot->no_policy) {
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
kfree_skb_reason(skb, SKB_DROP_REASON_XFRM_POLICY);
return;
}
nf_reset_ct(skb);
}
ret = INDIRECT_CALL_2(ipprot->handler, tcp_v4_rcv, udp_rcv, skb);
if (ret < 0) {
protocol = -ret; /* 协议要求以新 protocol 值重新分发 */
goto resubmit;
}
__IP_INC_STATS(net, IPSTATS_MIB_INDELIVERS);
} else {
if (!raw) {
if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
__IP_INC_STATS(net, IPSTATS_MIB_INUNKNOWNPROTOS);
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PROT_UNREACH, 0);
}
kfree_skb_reason(skb, SKB_DROP_REASON_IP_NOPROTO);
} else {
__IP_INC_STATS(net, IPSTATS_MIB_INDELIVERS);
consume_skb(skb);
}
}
}

inet_protos[] 是一个以 IPPROTO_* 为索引的全局指针数组,在系统初始化时由各协议模块调用 inet_add_protocol 注册。例如,TCP 注册时将 handler 设为 tcp_v4_rcv,UDP 设为 udp_rcv,ICMP 设为 icmp_rcv,GRE 设为 gre_rcv

INDIRECT_CALL_2 是针对 Spectre v2 漏洞的性能优化宏:对 tcp_v4_rcvudp_rcv 展开为直接 CALL 指令,避免 retpoline(返回蹦床)开销。对于其他协议,仍通过函数指针间接调用。

若没有任何处理器注册且也不是 RAW socket,内核回复 ICMP DEST_UNREACH / PROT_UNREACH(类型 3,代码 2),这是常见的”端口不可达”的协议层变体。

1.5 ip_forward:转发路径

当路由查找判定报文需要转发(fib_result.type == RTN_UNICAST 且目的不是本机),dst->input 被设置为 ip_forward(位于 net/ipv4/ip_forward.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
/* net/ipv4/ip_forward.c */
int ip_forward(struct sk_buff *skb)
{
u32 mtu;
struct iphdr *iph;
struct rtable *rt;
struct ip_options *opt = &(IPCB(skb)->opt);
struct net *net;

if (skb->pkt_type != PACKET_HOST)
goto drop;

skb_forward_csum(skb); /* 更新校验和偏移量,便于硬件 offload */
net = dev_net(skb->dev);

/* TTL 检查:到达 0 或 1 时停止转发 */
if (ip_hdr(skb)->ttl <= 1)
goto too_many_hops;

if (!xfrm4_route_forward(skb)) /* IPsec 策略检查 */
goto drop;

rt = skb_rtable(skb);

/* 设置转发标志位,后续 netfilter 可识别 */
IPCB(skb)->flags |= IPSKB_FORWARDED;

mtu = ip_dst_mtu_maybe_forward(&rt->dst, true);
if (ip_exceeds_mtu(skb, mtu)) {
IP_INC_STATS(net, IPSTATS_MIB_FRAGFAILS);
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED, htonl(mtu));
goto drop;
}

/* 写时复制(Copy-on-Write),准备修改 TTL */
if (skb_cow(skb, LL_RESERVED_SPACE(rt->dst.dev) + rt->dst.header_len))
goto drop;
iph = ip_hdr(skb);

ip_decrease_ttl(iph); /* TTL - 1,并增量更新 IP 头校验和 */

if (IPCB(skb)->flags & IPSKB_DOREDIRECT && !opt->srr && !skb_sec_path(skb))
ip_rt_send_redirect(skb); /* 可能发送 ICMP Redirect */

return NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD,
net, NULL, skb, skb->dev, rt->dst.dev,
ip_forward_finish);

too_many_hops:
__IP_INC_STATS(net, IPSTATS_MIB_INHDRERRORS);
icmp_send(skb, ICMP_TIME_EXCEEDED, ICMP_EXC_TTL, 0);
/* 落到 drop */
}

转发路径的几个设计要点值得深入理解:

MTU 与 DF 位ip_exceeds_mtu 会检查 IP_DF 标志。若报文大于出口 MTU 且 DF 位为 1,必须丢包并发送 ICMP_FRAG_NEEDED(携带 MTU 信息),这是 PMTUD(Path MTU Discovery)的基础机制。

写时复制skb_cow 检查 skb 的引用计数,若有多方持有则克隆一份私有副本,再执行 ip_decrease_ttlip_decrease_ttl 使用增量更新公式 check = ~(~check + ~old + new) 避免重新计算整个校验和。

FORWARD hook:触发 NF_INET_FORWARDip_forward_finish 调用 dst_output 进入发送路径。


2. FIB 路由表:LC-Trie 实现

2.1 整体架构

Linux IPv4 路由子系统由三层抽象组成:

  1. 路由策略(Policy Routing Rules)ip rule 管理的策略表,根据源地址、防火墙标记(fwmark)等选择查找哪张路由表;
  2. 路由表(FIB Table)struct fib_table,每个命名空间默认有 local(255)和 main(254)两张表;
  3. FIB Trie:每张路由表内部用 LC-Trie 索引前缀。

2.2 核心数据结构

LC-Trie 的节点由 key_vector 描述(net/ipv4/fib_trie.c):

1
2
3
4
5
6
7
8
9
10
struct key_vector {
t_key key; /* 该节点表示的前缀 key */
unsigned char pos; /* 节点在 32-bit key 中的起始位位置 */
unsigned char bits; /* 本节点管辖的位数;0 表示叶子节点 */
unsigned char slen; /* 该节点下所有子树中最长的前缀长度 */
union {
struct hlist_head leaf; /* 叶子:路由别名(fib_alias)链表 */
DECLARE_FLEX_ARRAY(struct key_vector __rcu *, tnode); /* 内部节点子指针数组 */
};
};

三种节点类型的判断:

1
2
3
#define IS_TRIE(n)  ((n)->pos >= KEYLENGTH)  /* pos == 32:根节点哨兵 */
#define IS_TNODE(n) ((n)->bits) /* bits > 0:内部分支节点 */
#define IS_LEAF(n) (!(n)->bits) /* bits == 0:叶子节点 */

叶子节点通过 fib_alias 链表关联具体路由:

1
2
3
4
5
6
7
8
9
10
11
12
/* net/ipv4/fib_lookup.h */
struct fib_alias {
struct hlist_node fa_list;
struct fib_info *fa_info; /* 路由的详细信息:nexthop、metrics 等 */
dscp_t fa_dscp; /* DSCP/TOS 精确匹配值 */
u8 fa_type; /* RTN_UNICAST、RTN_BLACKHOLE、RTN_LOCAL 等 */
u8 fa_state; /* FA_S_ACCESSED 标志 */
u8 fa_slen; /* 前缀长度(从高位算起的比特数) */
u32 tb_id; /* 所属路由表 ID */
s16 fa_default; /* ECMP 默认 nexthop 索引 */
/* ... */
};

fib_table 是路由表的顶层容器:

1
2
3
4
5
6
7
8
9
/* include/net/ip_fib.h */
struct fib_table {
struct hlist_node tb_hlist; /* 挂入 net->ipv4.fib_table_hash */
u32 tb_id; /* 表 ID:RT_TABLE_MAIN = 254 */
int tb_num_default;
struct rcu_head rcu;
unsigned long *tb_data; /* 实际指向 struct trie */
unsigned long __data[];
};

路由查找结果填入 fib_result,包含选定的 nexthop 信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* include/net/ip_fib.h */
struct fib_result {
__be32 prefix;
unsigned char prefixlen;
unsigned char nh_sel;
unsigned char type; /* RTN_UNICAST 等 */
unsigned char scope;
u32 tclassid;
struct fib_nh_common *nhc; /* 下一跳公共字段(设备、gateway)*/
struct fib_info *fi; /* 路由完整信息 */
struct fib_table *table;
struct hlist_head *fa_head;
};

2.3 LC-Trie 查找算法

LC-Trie(Level Compressed Trie,也称 LPC-Trie)是传统 Patricia Trie 的改进:内部节点可以跨越多个比特位(bits 字段),将树的高度从 O(W)(W 为 key 宽度 = 32)压缩到 O(log W)。在包含 50 万条路由的实际测试中,平均查找步数不超过 6 步。

fib_table_lookup 的三阶段实现:

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
/* net/ipv4/fib_trie.c */
int fib_table_lookup(struct fib_table *tb, const struct flowi4 *flp,
struct fib_result *res, int fib_flags)
{
struct trie *t = (struct trie *) tb->tb_data;
const t_key key = ntohl(flp->daddr); /* 目的 IP → 主机字节序 */
struct key_vector *n, *pn;
struct fib_alias *fa;
unsigned long index;
t_key cindex;

pn = t->kv; /* 从根节点出发 */
cindex = 0;
n = get_child_rcu(pn, cindex);

/* === 阶段 1:沿最长路径向叶子行进 === */
for (;;) {
index = get_cindex(key, n); /* 提取节点管辖的位段 */
if (index >= (1ul << n->bits))
break; /* 当前节点 key 与目标不匹配 */
if (IS_LEAF(n))
goto found;
if (n->slen > n->pos) { /* 此节点下有比当前深度更长的前缀,记录为回溯点 */
pn = n;
cindex = index;
}
n = get_child_rcu(n, index);
if (unlikely(!n))
goto backtrace;
}

/* === 阶段 2:回溯,寻找实际可匹配的最长前缀 === */
for (;;) {
struct key_vector __rcu **cptr = n->tnode;
if (unlikely(prefix_mismatch(key, n)) || (n->slen == n->pos))
goto backtrace;
if (unlikely(IS_LEAF(n)))
break;
while ((n = rcu_dereference(*cptr)) == NULL) {
backtrace:
while (!cindex) {
t_key pkey = pn->key;
if (IS_TRIE(pn)) {
return -EAGAIN; /* 整棵树都没有匹配 */
}
pn = node_parent_rcu(pn);
cindex = get_index(pkey, pn);
}
cindex &= cindex - 1; /* 清除最低有效位,找下一个兄弟子树 */
cptr = &pn->tnode[cindex];
}
}

found:
/* === 阶段 3:叶子节点语义匹配 === */
hlist_for_each_entry_rcu(fa, &n->leaf, fa_list) {
struct fib_info *fi = fa->fa_info;
struct fib_nh_common *nhc;
int nhsel, err;

/* 前缀精确匹配:验证 key 与 n->key 在前缀 slen 位内完全一致 */
if ((BITS_PER_LONG > KEYLENGTH) || (fa->fa_slen < KEYLENGTH)) {
if (index >= (1ul << fa->fa_slen))
continue;
}
if (fa->fa_dscp &&
inet_dscp_to_dsfield(fa->fa_dscp) != flp->flowi4_tos)
continue;
if (fi->fib_dead)
continue;
if (fa->fa_info->fib_scope < flp->flowi4_scope)
continue;
fib_alias_accessed(fa); /* 标记 FA_S_ACCESSED */
err = fib_props[fa->fa_type].error;
if (unlikely(err < 0))
return err; /* RTN_BLACKHOLE / RTN_PROHIBIT 等返回错误 */
/* ... 选择 nexthop,填充 res,返回 0 ... */
}
}

整个查找过程在 RCU 读锁下完成,允许多 CPU 并发查找而无须任何写锁,这是 Linux 路由子系统能够扩展到多核场景的关键。

2.4 dst_entry 与路由结果缓存

路由查找成功后,内核将结果封装为 rtabledst_entry 的 IPv4 子类),绑定到 skb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* include/net/route.h */
struct rtable {
struct dst_entry dst; /* 必须是第一个字段 */
int rt_genid;
unsigned int rt_flags; /* RTCF_LOCAL、RTCF_BROADCAST 等 */
__u16 rt_type; /* RTN_UNICAST 等 */
__u8 rt_is_input; /* 1 = 接收方向,0 = 发送方向 */
__u8 rt_uses_gateway;
int rt_iif;
u8 rt_gw_family;
union {
__be32 rt_gw4; /* IPv4 网关地址 */
struct in6_addr rt_gw6;
};
u32 rt_mtu_locked:1,
rt_pmtu:31; /* Path MTU 缓存 */
};

dst_entry 结构本身包含两个关键函数指针:

1
2
3
4
5
6
7
8
9
10
/* include/net/dst.h */
struct dst_entry {
struct net_device *dev;
struct dst_ops *ops;
unsigned long _metrics; /* 路由度量(RTT、MTU 等)*/
unsigned long expires;
int (*input)(struct sk_buff *); /* ip_local_deliver 或 ip_forward */
int (*output)(struct net *net, struct sock *sk, struct sk_buff *skb); /* ip_output */
/* ... */
};

inputoutput 函数指针在 ip_route_input_slow__mkroute_output 中被设置,这使得 dst_input/dst_output 的调用成为多态分派,支持本机收发、转发、IPsec 隧道等不同场景,同一套代码框架无需大量 if-else。

对于 socket 绑定的发送路由,socket 缓存一份 dst_entrysk->sk_dst_cache),通过 __sk_dst_checkrt_genid 验证有效性,避免重复查找。当路由表发生变化时,rt_genid 全局递增,所有缓存的 dst_entry 自动失效。


3. IP 发送路径

3.1 ip_queue_xmit 与路由查找

传输层完成报文构造后,通过 ip_queue_xmit(TCP/SCTP)或 ip_send_skb(UDP Raw)将 skb 交给 IP 层,核心实现在 __ip_queue_xmitnet/ipv4/ip_output.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
/* net/ipv4/ip_output.c */
int __ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl, __u8 tos)
{
struct inet_sock *inet = inet_sk(sk);
struct net *net = sock_net(sk);
struct rtable *rt;

rcu_read_lock();
rt = skb_rtable(skb);
if (rt)
goto packet_routed; /* skb 已有路由(如 SCTP 预先设置)*/

/* 尝试复用 socket 缓存路由 */
rt = (struct rtable *)__sk_dst_check(sk, 0);
if (!rt) {
/* 执行输出路由查找,结果缓存到 socket */
rt = ip_route_output_ports(net, fl4, sk,
daddr, inet->inet_saddr,
inet->inet_dport, inet->inet_sport,
sk->sk_protocol,
RT_CONN_FLAGS_TOS(sk, tos),
sk->sk_bound_dev_if);
if (IS_ERR(rt))
goto no_route;
sk_setup_caps(sk, &rt->dst);
}
skb_dst_set_noref(skb, &rt->dst);

packet_routed:
/* 构建 IP 头:填充 version、ihl、tos、ttl、protocol、id、frag_off、saddr、daddr */
skb_push(skb, sizeof(struct iphdr) + (inet_opt ? inet_opt->opt.optlen : 0));
skb_reset_network_header(skb);
iph = ip_hdr(skb);
/* ... 填充各字段 ... */

return ip_local_out(net, sk, skb);
}

发送路由查找使用 flowi4 描述五元组(源/目的 IP、源/目的端口、协议、TOS),通过 fib_lookup 查找主路由表,选择出口设备和网关。

3.2 ip_local_out 与 OUTPUT hook

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
/* net/ipv4/ip_output.c */
int __ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
{
struct iphdr *iph = ip_hdr(skb);

iph_set_totlen(iph, skb->len); /* 填写 IP 总长度 */
ip_send_check(iph); /* 计算并填写 IP 头校验和 */

skb = l3mdev_ip_out(sk, skb); /* VRF 设备处理 */
if (unlikely(!skb))
return 0;

skb->protocol = htons(ETH_P_IP);

return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT,
net, sk, skb, NULL, skb_dst(skb)->dev,
dst_output);
}

int ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
{
int err;
err = __ip_local_out(net, sk, skb);
if (likely(err == 1))
err = dst_output(net, sk, skb); /* 所有 hook 通过,继续发送 */
return err;
}

OUTPUT hook(NF_INET_LOCAL_OUT)是本机发出报文的第一个 netfilter 检查点,iptables -A OUTPUT 规则、本机发出流量的 DNAT(如 -t nat -A OUTPUT -p tcp --dport 80 -j REDIRECT --to-port 8080)均在此处执行。dst_output 指向的是 ip_output

3.3 ip_output 与 POSTROUTING hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* net/ipv4/ip_output.c */
int ip_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
struct net_device *dev = skb_dst(skb)->dev, *indev = skb->dev;

IP_UPD_PO_STATS(net, IPSTATS_MIB_OUT, skb->len);

skb->dev = dev;
skb->protocol = htons(ETH_P_IP);

return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING,
net, sk, skb, indev, dev,
ip_finish_output,
!(IPCB(skb)->flags & IPSKB_REROUTED));
}

POSTROUTING hook(NF_INET_POST_ROUTING)是所有出方向报文(本机发出 + 转发)在离开内核前的最后一个 netfilter 点,iptables MASQUERADE(SNAT)、MARK 等在此执行。NF_HOOK_COND 的第五个参数是条件值,当 IPSKB_REROUTED 标志位为 1 时跳过此 hook(避免重路由后重复执行 NAT)。

3.4 ip_finish_output:分片决策

1
2
3
4
5
6
7
8
9
10
11
12
13
/* net/ipv4/ip_output.c */
static int __ip_finish_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
unsigned int mtu = ip_skb_dst_mtu(sk, skb);

if (skb_is_gso(skb))
return ip_finish_output_gso(net, sk, skb, mtu); /* GSO 分段 */

if (skb->len > mtu || IPCB(skb)->frag_max_size)
return ip_fragment(net, sk, skb, mtu, ip_finish_output2); /* IP 分片 */

return ip_finish_output2(net, sk, skb); /* 直接发送 */
}

ip_skb_dst_mtu 会优先使用 PMTU 缓存值(rt->rt_pmtu),如果没有则返回出口设备的 dev->mtu。GSO(Generic Segmentation Offload)路径允许上层传递超大 skb(TSO/UFO),在此处再分拆为实际片段,或将分段任务卸载给支持 TSO 的网卡。

3.5 ip_finish_output2:邻居子系统与 ARP 解析

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
/* net/ipv4/ip_output.c */
static int ip_finish_output2(struct net *net, struct sock *sk, struct sk_buff *skb)
{
struct dst_entry *dst = skb_dst(skb);
struct rtable *rt = (struct rtable *)dst;
struct net_device *dev = dst->dev;
unsigned int hh_len = LL_RESERVED_SPACE(dev);
struct neighbour *neigh;
bool is_v6gw = false;

/* 为链路层头部预留 headroom */
if (unlikely(skb_headroom(skb) < hh_len && dev->header_ops)) {
skb = skb_expand_head(skb, hh_len);
if (!skb)
return -ENOMEM;
}

rcu_read_lock();
neigh = ip_neigh_for_gw(rt, skb, &is_v6gw);
if (!IS_ERR(neigh)) {
int res;
sock_confirm_neigh(skb, neigh);
res = neigh_output(neigh, skb, is_v6gw); /* 发往链路层 */
rcu_read_unlock();
return res;
}
rcu_read_unlock();

kfree_skb_reason(skb, SKB_DROP_REASON_NEIGH_CREATEFAIL);
return -EINVAL;
}

ip_neigh_for_gw 根据 rt->rt_gw4(路由网关 IP)或直接目的 IP 在邻居表中查找或创建 neighbour 结构体。邻居表维护一个 NUD(Neighbor Unreachability Detection)状态机:

  • NUD_NONE:未初始化
  • NUD_INCOMPLETE:ARP 请求已发出,等待应答
  • NUD_REACHABLE:有效,可直接使用缓存 MAC
  • NUD_STALE:超时,下次使用时会触发探测
  • NUD_FAILED:ARP 多次失败,不可达

当邻居状态为 NUD_REACHABLE 时,neigh_output 直接调用 neigh_hh_output 将缓存的硬件头(neigh->hh)前置到 skb 并调用 dev_queue_xmit 发送,是最快路径。若邻居未解析,则触发 ARP 请求(arp_solicit)并将 skb 加入邻居的发送等待队列。


4. IP 分片与重组

4.1 分片重组的整体框架

Linux 的分片重组使用 inet_frags 框架(net/ipv4/inet_fragment.c),IPv4 和 IPv6 共享同一套代码骨架。每个命名空间有一个 fqdir(Fragment Queue Directory),管理该命名空间下的所有分片队列:

  • fqdir->high_thresh(默认 4MB):内存上限,超过则触发驱逐;
  • fqdir->low_thresh(默认 3MB):驱逐目标水位线;
  • fqdir->timeout(默认 30 秒,IP_FRAG_TIME):单个队列超时;
  • fqdir->max_dist(默认 64):单个源的最大”距离”,防止资源耗尽攻击。

4.2 ip_defrag 与 ipq 队列

ip_defragnet/ipv4/ip_fragment.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
/* net/ipv4/ip_fragment.c */
int ip_defrag(struct net *net, struct sk_buff *skb, u32 user)
{
struct net_device *dev = skb->dev ? : skb_dst(skb)->dev;
int vif = l3mdev_master_ifindex_rcu(dev);
struct ipq *qp;

__IP_INC_STATS(net, IPSTATS_MIB_REASMREQDS);
skb_orphan(skb); /* 解除与 socket 的关联,防止内存统计混乱 */

qp = ip_find(net, ip_hdr(skb), user, vif);
if (qp) {
int ret;
spin_lock(&qp->q.lock);
ret = ip_frag_queue(qp, skb);
spin_unlock(&qp->q.lock);
ipq_put(qp);
return ret;
}

__IP_INC_STATS(net, IPSTATS_MIB_REASMFAILS);
kfree_skb(skb);
return -ENOMEM;
}

ip_find 以四元组(源 IP、目的 IP、报文 ID、协议号)为键,通过 rhashtable(一种可自动收缩的并发哈希表)在 fqdir 中查找或创建 ipq 队列,操作全程 RCU 保护。

ipq 结构体嵌套了通用的 inet_frag_queue

1
2
3
4
5
6
7
8
9
/* net/ipv4/ip_fragment.c */
struct ipq {
struct inet_frag_queue q; /* 通用分片队列(rb_fragments 红黑树、定时器等)*/
u8 ecn; /* 各分片的 ECN 标志按位或运算结果 */
u16 max_df_size; /* 带 DF 标志的最大分片大小(用于重组后重分片决策)*/
int iif; /* 最后到达分片的入接口索引 */
unsigned int rid; /* 防重放计数器(与 inet_peer 配合)*/
struct inet_peer *peer; /* 源 IP 对应的 inet_peer,用于 max_dist 判断 */
};

分片入队时,ip_frag_queue 将 skb 插入 qp->q.rb_fragments 红黑树(按字节偏移排序),同时记录 INET_FRAG_FIRST_IN(offset == 0 的首片已到)和 INET_FRAG_LAST_INIP_MF == 0 的末片已到)标志。当两个标志都置位且 meat(已到字节数)== len(期望总长度) 时,立即调用 ip_frag_reasm 完成重组。

4.3 分片超时与 ICMP 回复

每个 ipq 创建时启动一个内核软定时器,超时回调为 ip_expire

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* net/ipv4/ip_fragment.c(关键逻辑)*/
static void ip_expire(struct timer_list *t)
{
struct inet_frag_queue *frag = from_timer(frag, t, timer);
struct ipq *qp = container_of(frag, struct ipq, q);
struct net *net = qp->q.fqdir->net;

/* ... */
qp->q.flags |= INET_FRAG_DROP;
ipq_kill(qp);
__IP_INC_STATS(net, IPSTATS_MIB_REASMFAILS);
__IP_INC_STATS(net, IPSTATS_MIB_REASMTIMEOUT);

/* RFC 792:仅当首片已到时,才向发送方报告超时 */
if (!(qp->q.flags & INET_FRAG_FIRST_IN))
goto out;

/* 重新路由首片并发送 ICMP Time Exceeded(类型 11,代码 1)*/
err = ip_route_input_noref(head, iph->daddr, iph->saddr, iph->tos, head->dev);
if (!err)
icmp_send(head, ICMP_TIME_EXCEEDED, ICMP_EXC_FRAGTIME, 0);
}

重组超时后,内核发送 ICMP TIME_EXCEEDED / Fragment Reassembly Time Exceeded,告知发送方本数据报的某个分片未能在规定时间内到达,需要重传整个数据报。这一机制对排查网络路径中的分片丢失问题非常有用。


5. Netfilter 钩子机制

5.1 五个 Hook 点与优先级

IPv4 netfilter 定义五个挂载点,由 uapi/linux/netfilter_ipv4.h 给出:

1
2
3
4
5
#define NF_IP_PRE_ROUTING   0   /* 收包后路由前(DNAT、conntrack 入方向)*/
#define NF_IP_LOCAL_IN 1 /* 路由判定为本机,递交上层前(filter INPUT)*/
#define NF_IP_FORWARD 2 /* 路由判定需转发(filter FORWARD)*/
#define NF_IP_LOCAL_OUT 3 /* 本机产生报文,路由后(filter OUTPUT、DNAT)*/
#define NF_IP_POST_ROUTING 4 /* 所有出方向,发网卡前(SNAT、MASQUERADE)*/

每个 hook 点按优先级排列 hook 函数。常见模块的注册优先级:

优先级 注册模块
NF_IP_PRI_CONNTRACK_DEFRAG -400 nf_defrag_ipv4
NF_IP_PRI_RAW -300 iptables raw 表
NF_IP_PRI_CONNTRACK -200 nf_conntrack(连接跟踪)
NF_IP_PRI_MANGLE -150 iptables mangle 表
NF_IP_PRI_NAT_DST -100 iptables nat 表 DNAT
NF_IP_PRI_FILTER 0 iptables filter 表
NF_IP_PRI_SECURITY 50 SELinux
NF_IP_PRI_NAT_SRC 100 iptables nat 表 SNAT

优先级数值越小,越先执行。同一 hook 点的多个 hook 以数组形式存储,遍历时天然按升序排列。

5.2 nf_hook_slow:遍历 Hook 链

当内核代码调用 NF_HOOK 宏时,最终到达 nf_hook_slownet/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
/* net/netfilter/core.c */
/* 返回 1:所有 hook 通过,调用方执行 okfn
* 返回 -EPERM:NF_DROP
* 返回 0:NF_STOLEN 或已排队 */
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; /* 放行,继续下一个 hook */
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:
/* NF_STOLEN:hook 函数已接管 skb 所有权 */
return 0;
}
}

return 1; /* 全部通过 */
}

nf_hook_entries 是一个连续数组,比老版本的链表在 CPU 缓存利用率上更优。nf_hook_entry_hookfn 是一个内联函数,直接访问 e->hooks[s].hook(priv, skb, state) 而不走额外的虚函数表。

nf_hook_state 结构携带本次调用的上下文(协议族、hook 号、输入/输出设备、socket、网络命名空间),每个 hook 函数都可以读取这些信息做出判断。

5.3 nf_register_net_hook:注册钩子

模块通过 nf_register_net_hook 注册 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
/* net/netfilter/core.c */
int nf_register_net_hook(struct net *net, const struct nf_hook_ops *reg)
{
int err;

if (reg->pf == NFPROTO_INET) {
/* NFPROTO_INET 同时注册到 IPv4 和 IPv6 */
err = __nf_register_net_hook(net, NFPROTO_IPV4, reg);
if (err < 0)
return err;
err = __nf_register_net_hook(net, NFPROTO_IPV6, reg);
if (err < 0) {
__nf_unregister_net_hook(net, NFPROTO_IPV4, reg);
return err;
}
} else {
err = __nf_register_net_hook(net, reg->pf, reg);
if (err < 0)
return err;
}
return 0;
}
EXPORT_SYMBOL(nf_register_net_hook);

__nf_register_net_hook 的工作流程:

  1. 调用 nf_hook_entries_grow 分配新的 nf_hook_entries 数组(旧条目 + 新条目,按优先级插入);
  2. 使用 rcu_assign_pointer 原子替换 net->nf.hooks[pf][hooknum] 指针;
  3. 旧数组通过 call_rcu 延迟释放——等待所有 RCU 读侧临界区退出后再释放内存,保证无锁并发安全。

这套机制使得模块的动态加载/卸载(如 modprobe ip_tables)完全无需停止报文处理,对转发性能的影响极小。


6. 路由诊断方法

6.1 ip route 与内核路由查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 查看路由表(主表 main)
ip route show table main

# 查看本地路由表(接口地址、广播等)
ip route show table local

# 模拟路由查找:对 8.8.8.8 执行完整的 fib_lookup
ip route get 8.8.8.8
# 输出:8.8.8.8 via 192.168.1.1 dev eth0 src 192.168.1.100 uid 1000
# cache

# 带源地址的路由查找(策略路由场景)
ip route get 10.0.0.1 from 192.168.2.1

# 查看策略路由规则列表
ip rule show

ip route get 在内核中触发 RTM_GETROUTE netlink 请求,由 inet_rtm_getroute 处理,调用 ip_route_input_norefip_route_output_key,并将 rtable 的内容序列化返回给用户空间。

6.2 查看 FIB Trie 结构与统计

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
# FIB Trie 统计:节点数、叶子数、最大深度、平均查找步数
cat /proc/net/fib_triestat

# 示例输出:
# Basic info: size of leaf: 48 bytes, size of tnode: 40 bytes.
# Main:
# Aver depth: 2.67
# Max depth: 6
# Leaves: 12
# Prefixes: 13
# Internal nodes: 7
# 2: 3 3: 2 4: 1 5: 1

# 查看完整 FIB Trie(路由表很大时慎用,会产生大量输出)
cat /proc/net/fib_trie

# fib_trie 输出片段:
# Main:
# +-- 0.0.0.0/0 3 0 5
# +-- 0.0.0.0/1 2 0 2
# +-- 0.0.0.0/8 2 0 2
# |-- 0.0.0.0
# /0 universe INDIR
# |-- 10.0.0.0
# /8 universe INDIR

6.3 /proc/net/snmp 中的 IP 统计字段

1
2
3
4
5
cat /proc/net/snmp | grep -A1 "^Ip"
# Ip: Forwarding DefaultTTL InReceives InHdrErrors InAddrErrors
# ForwDatagrams InUnknownProtos InDiscards InDelivers OutRequests
# OutDiscards OutNoRoutes ReasmTimeout ReasmReqds ReasmOKs ReasmFails
# FragOKs FragFails FragCreates

关键字段含义:

字段 对应 MIB 常量 说明
InReceives IPSTATS_MIB_IN L3 层接收总包数
InHdrErrors IPSTATS_MIB_INHDRERRORS IP 头校验和错误、版本错误等
ForwDatagrams IPSTATS_MIB_OUTFORWDATAGRAMS 成功转发的包数
InDelivers IPSTATS_MIB_INDELIVERS 成功递交上层协议的包数
OutNoRoutes IPSTATS_MIB_OUTNOROUTES 因无路由被丢弃的发送包数
ReasmTimeout IPSTATS_MIB_REASMTIMEOUT 重组超时次数
ReasmOKs IPSTATS_MIB_REASMOKS 重组成功次数
ReasmFails IPSTATS_MIB_REASMFAILS 重组失败次数
FragFails IPSTATS_MIB_FRAGFAILS 因 DF 位无法分片的次数

6.4 nstat 工具

nstat 直接读取 /proc/net/netstat/proc/net/snmp,以增量差值方式展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 显示自上次执行以来的增量统计(含内核行为解读)
nstat

# 持续监控,每 2 秒刷新
nstat -s 2

# 只关注 IP 层相关计数器
nstat | grep -iE "^Ip|^IpExt"

# 重置统计基线(下次 nstat 从 0 开始计算差值)
nstat -n

# 关注转发丢包:FragFails 或 InNoRoutes 增长表示路由配置问题
nstat | grep -E "FragFails|InNoRoutes|InAddrErrors|InHdrErrors"

6.5 bpftrace 追踪 IP 报文

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
# 统计各进程触发 ip_rcv 的次数(追踪 L3 入口,识别高流量进程)
bpftrace -e 'kprobe:ip_rcv { @[comm] = count(); }'

# 追踪路由查找耗时分布(微秒)
bpftrace -e '
kprobe:ip_route_input_slow { @start[tid] = nsecs; }
kretprobe:ip_route_input_slow /@start[tid]/ {
@latency_us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'

# 追踪 fib_table_lookup 的返回值(-EAGAIN 表示路由未命中)
bpftrace -e '
kretprobe:fib_table_lookup {
@ret[retval] = count();
}'

# 追踪分片重组超时事件
bpftrace -e 'kprobe:ip_expire { @timeout_count = count(); printf("frag timeout\n"); }'

# 追踪因 netfilter 丢包的内核调用栈(定位哪条 iptables 规则丢包)
bpftrace -e '
kprobe:kfree_skb {
if (arg1 == 6) { /* SKB_DROP_REASON_NETFILTER_DROP = 6 */
@[kstack] = count();
}
}'

6.6 perf trace 分析路由查找性能

1
2
3
4
5
6
7
8
9
10
# 记录 ip_route_input_noref 的调用频率和 CPU 开销
perf stat -e probe:ip_route_input_noref -a -- sleep 10

# 对转发路径做 CPU profile,生成火焰图
perf record -F 99 -ag -- sleep 30
perf script | /usr/share/perf/stackcollapse-perf.pl | \
/usr/share/perf/flamegraph.pl > ip_forward.svg

# 追踪 neigh_resolve_output(ARP 解析)调用——发生频繁说明 ARP 缓存频繁失效
perf trace -e net:net_dev_xmit -- sleep 5

6.7 综合诊断流程

遇到路由或转发异常时,建议按以下顺序排查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 1. 确认是否存在目标路由
ip route get <dst_ip>

# 2. 确认 ip_forward 已开启(容器/虚机场景常见问题)
sysctl net.ipv4.ip_forward

# 3. 确认 rp_filter 不会过滤不对称路由
sysctl net.ipv4.conf.all.rp_filter
sysctl net.ipv4.conf.<iface>.rp_filter

# 4. 查看 netfilter 规则是否有意外 DROP
iptables -L -n -v --line-numbers
iptables -t nat -L -n -v

# 5. 查看 MIB 统计中的异常计数器
nstat | grep -E "Drop|Discard|Fail|Error|NoRoute"

# 6. 若有分片丢失,检查重组参数
sysctl net.ipv4.ipfrag_time # 重组超时(默认 30 秒)
sysctl net.ipv4.ipfrag_high_thresh # 内存上限(默认 4MB)

# 7. 高精度定位丢包位置
bpftrace -e 'kprobe:kfree_skb { @[kstack, arg1] = count(); }'

小结

本文沿着 Linux 6.4-rc1 的源码脉络,系统梳理了 IPv4 网络层的完整处理链路:

接收路径ip_rcv(校验和、PREROUTING)→ ip_rcv_finish_core(Early Demux + FIB 查找,绑定 dst_entry)→ dst_inputip_local_deliver(本机,INPUT hook,协议分发表 inet_protos[])或 ip_forward(转发,TTL 递减,MTU 检查,FORWARD hook)。

FIB/LC-Triefib_table_lookup 三阶段(前向行进 → 回溯 → 叶子语义匹配),全程 RCU 保护;dst_entryinput/output 函数指针实现报文处理的多态分派;socket 路由缓存以 rt_genid 实现无效化。

发送路径__ip_queue_xmit(IP 头构建)→ ip_local_out(OUTPUT hook)→ ip_output(POSTROUTING hook)→ ip_finish_output(GSO/分片决策)→ ip_finish_output2(邻居解析,neigh_output 进入 L2)。

分片重组ipq + inet_frag_queue 框架,rhashtable 快速定位队列,红黑树按偏移排序存储分片,定时器超时回收并发送 ICMP。

Netfilter:五个 hook 点按优先级组织成 nf_hook_entries 数组,nf_hook_slow 遍历执行,RCU 保护动态注册/注销。

诊断ip route get/proc/net/fib_triestatnstatbpftraceperf 形成可观测性完整闭环。

理解这些路径的源码实现,是优化容器网络(CNI、eBPF XDP 绕过)、调试 NAT/netfilter 异常、分析高速转发性能瓶颈的坚实基础。下一篇将深入 TCP 层,分析三次握手、拥塞控制与零拷贝发送的内核实现。