IP 层(网络层)是 Linux 协议栈的核心,负责报文的寻址、路由、转发与分片重组。本文基于 Linux 6.4-rc1 源码,从 ip_rcv 入口函数出发,逐层拆解接收路径、FIB 路由查找、发送路径、分片重组和 netfilter 钩子的完整实现,并给出实用的内核诊断技巧。文中所有代码均来自 net/ipv4/ip_input.c、net/ipv4/ip_output.c、net/ipv4/fib_trie.c、net/ipv4/ip_fragment.c、net/ipv4/ip_forward.c 和 net/netfilter/core.c 等实际文件。
1. IP 接收路径
1.1 L3 入口:ip_rcv 与校验和检查
网络设备驱动通过 netif_receive_skb 将 sk_buff 交给协议栈。内核在 ptype_base 哈希表中依据以太网帧的 EtherType(ETH_P_IP = 0x0800)找到注册的 packet_type 结构,调用其 func 指针,即 ip_rcv:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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->len 与 iph->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
| 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; }
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; } }
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
| 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_finish → ip_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
| 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);
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; 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_rcv 和 udp_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
| 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); net = dev_net(skb->dev);
if (ip_hdr(skb)->ttl <= 1) goto too_many_hops;
if (!xfrm4_route_forward(skb)) goto drop;
rt = skb_rtable(skb);
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; }
if (skb_cow(skb, LL_RESERVED_SPACE(rt->dst.dev) + rt->dst.header_len)) goto drop; iph = ip_hdr(skb);
ip_decrease_ttl(iph);
if (IPCB(skb)->flags & IPSKB_DOREDIRECT && !opt->srr && !skb_sec_path(skb)) ip_rt_send_redirect(skb);
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); }
|
转发路径的几个设计要点值得深入理解:
MTU 与 DF 位:ip_exceeds_mtu 会检查 IP_DF 标志。若报文大于出口 MTU 且 DF 位为 1,必须丢包并发送 ICMP_FRAG_NEEDED(携带 MTU 信息),这是 PMTUD(Path MTU Discovery)的基础机制。
写时复制:skb_cow 检查 skb 的引用计数,若有多方持有则克隆一份私有副本,再执行 ip_decrease_ttl。ip_decrease_ttl 使用增量更新公式 check = ~(~check + ~old + new) 避免重新计算整个校验和。
FORWARD hook:触发 NF_INET_FORWARD,ip_forward_finish 调用 dst_output 进入发送路径。
2. FIB 路由表:LC-Trie 实现
2.1 整体架构
Linux IPv4 路由子系统由三层抽象组成:
- 路由策略(Policy Routing Rules):
ip rule 管理的策略表,根据源地址、防火墙标记(fwmark)等选择查找哪张路由表;
- 路由表(FIB Table):
struct fib_table,每个命名空间默认有 local(255)和 main(254)两张表;
- 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; unsigned char pos; unsigned char bits; unsigned char slen; union { struct hlist_head leaf; DECLARE_FLEX_ARRAY(struct key_vector __rcu *, tnode); }; };
|
三种节点类型的判断:
1 2 3
| #define IS_TRIE(n) ((n)->pos >= KEYLENGTH) #define IS_TNODE(n) ((n)->bits) #define IS_LEAF(n) (!(n)->bits)
|
叶子节点通过 fib_alias 链表关联具体路由:
1 2 3 4 5 6 7 8 9 10 11 12
| struct fib_alias { struct hlist_node fa_list; struct fib_info *fa_info; dscp_t fa_dscp; u8 fa_type; u8 fa_state; u8 fa_slen; u32 tb_id; s16 fa_default; };
|
fib_table 是路由表的顶层容器:
1 2 3 4 5 6 7 8 9
| struct fib_table { struct hlist_node tb_hlist; u32 tb_id; int tb_num_default; struct rcu_head rcu; unsigned long *tb_data; unsigned long __data[]; };
|
路由查找结果填入 fib_result,包含选定的 nexthop 信息:
1 2 3 4 5 6 7 8 9 10 11 12 13
| struct fib_result { __be32 prefix; unsigned char prefixlen; unsigned char nh_sel; unsigned char type; unsigned char scope; u32 tclassid; struct fib_nh_common *nhc; 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
| 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); 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);
for (;;) { index = get_cindex(key, n); if (index >= (1ul << n->bits)) break; 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; }
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: 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;
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); err = fib_props[fa->fa_type].error; if (unlikely(err < 0)) return err; } }
|
整个查找过程在 RCU 读锁下完成,允许多 CPU 并发查找而无须任何写锁,这是 Linux 路由子系统能够扩展到多核场景的关键。
2.4 dst_entry 与路由结果缓存
路由查找成功后,内核将结果封装为 rtable(dst_entry 的 IPv4 子类),绑定到 skb:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| struct rtable { struct dst_entry dst; int rt_genid; unsigned int rt_flags; __u16 rt_type; __u8 rt_is_input; __u8 rt_uses_gateway; int rt_iif; u8 rt_gw_family; union { __be32 rt_gw4; struct in6_addr rt_gw6; }; u32 rt_mtu_locked:1, rt_pmtu:31; };
|
dst_entry 结构本身包含两个关键函数指针:
1 2 3 4 5 6 7 8 9 10
| struct dst_entry { struct net_device *dev; struct dst_ops *ops; unsigned long _metrics; unsigned long expires; int (*input)(struct sk_buff *); int (*output)(struct net *net, struct sock *sk, struct sk_buff *skb); };
|
input 和 output 函数指针在 ip_route_input_slow 或 __mkroute_output 中被设置,这使得 dst_input/dst_output 的调用成为多态分派,支持本机收发、转发、IPsec 隧道等不同场景,同一套代码框架无需大量 if-else。
对于 socket 绑定的发送路由,socket 缓存一份 dst_entry(sk->sk_dst_cache),通过 __sk_dst_check 按 rt_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_xmit(net/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
| 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;
rt = (struct rtable *)__sk_dst_check(sk, 0); if (!rt) { 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: 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
| 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_send_check(iph);
skb = l3mdev_ip_out(sk, skb); 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); 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
| 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
| 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);
if (skb->len > mtu || IPCB(skb)->frag_max_size) return ip_fragment(net, sk, skb, mtu, ip_finish_output2);
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
| 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;
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_defrag(net/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
| 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);
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
| struct ipq { struct inet_frag_queue q; u8 ecn; u16 max_df_size; int iif; unsigned int rid; struct inet_peer *peer; };
|
分片入队时,ip_frag_queue 将 skb 插入 qp->q.rb_fragments 红黑树(按字节偏移排序),同时记录 INET_FRAG_FIRST_IN(offset == 0 的首片已到)和 INET_FRAG_LAST_IN(IP_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
| 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);
if (!(qp->q.flags & INET_FRAG_FIRST_IN)) goto out;
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 #define NF_IP_LOCAL_IN 1 #define NF_IP_FORWARD 2 #define NF_IP_LOCAL_OUT 3 #define NF_IP_POST_ROUTING 4
|
每个 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_slow(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
|
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; }
|
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
| int nf_register_net_hook(struct net *net, const struct nf_hook_ops *reg) { int err;
if (reg->pf == NFPROTO_INET) { 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 的工作流程:
- 调用
nf_hook_entries_grow 分配新的 nf_hook_entries 数组(旧条目 + 新条目,按优先级插入);
- 使用
rcu_assign_pointer 原子替换 net->nf.hooks[pf][hooknum] 指针;
- 旧数组通过
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
| ip route show table main
ip route show table local
ip route get 8.8.8.8
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_noref 或 ip_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
| cat /proc/net/fib_triestat
cat /proc/net/fib_trie
|
6.3 /proc/net/snmp 中的 IP 统计字段
1 2 3 4 5
| cat /proc/net/snmp | grep -A1 "^Ip"
|
关键字段含义:
| 字段 |
对应 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
nstat -s 2
nstat | grep -iE "^Ip|^IpExt"
nstat -n
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
| 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]); }'
bpftrace -e ' kretprobe:fib_table_lookup { @ret[retval] = count(); }'
bpftrace -e 'kprobe:ip_expire { @timeout_count = count(); printf("frag timeout\n"); }'
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
| perf stat -e probe:ip_route_input_noref -a -- sleep 10
perf record -F 99 -ag -- sleep 30 perf script | /usr/share/perf/stackcollapse-perf.pl | \ /usr/share/perf/flamegraph.pl > ip_forward.svg
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
| ip route get <dst_ip>
sysctl net.ipv4.ip_forward
sysctl net.ipv4.conf.all.rp_filter sysctl net.ipv4.conf.<iface>.rp_filter
iptables -L -n -v --line-numbers iptables -t nat -L -n -v
nstat | grep -E "Drop|Discard|Fail|Error|NoRoute"
sysctl net.ipv4.ipfrag_time sysctl net.ipv4.ipfrag_high_thresh
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_input → ip_local_deliver(本机,INPUT hook,协议分发表 inet_protos[])或 ip_forward(转发,TTL 递减,MTU 检查,FORWARD hook)。
FIB/LC-Trie:fib_table_lookup 三阶段(前向行进 → 回溯 → 叶子语义匹配),全程 RCU 保护;dst_entry 的 input/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_triestat、nstat、bpftrace 和 perf 形成可观测性完整闭环。
理解这些路径的源码实现,是优化容器网络(CNI、eBPF XDP 绕过)、调试 NAT/netfilter 异常、分析高速转发性能瓶颈的坚实基础。下一篇将深入 TCP 层,分析三次握手、拥塞控制与零拷贝发送的内核实现。