4. 网络层核心-IPv4:IP头部结构、路由子系统核心数据结构、IP分片与重组

网络层,说白了就是整个网络协议栈的“交通枢纽”。我做了这么多年内核网络,最深的体会就是:搞懂了网络层,你才算真正入了内核网络的门。今天咱们就聊聊IPv4这一块的核心内容——IP头部结构、路由子系统的数据结构,还有那个让人又爱又恨的IP分片与重组。

4.1 IP头部结构:每一比特都有故事

先看IP头部。嗯,这个结构你可能在教科书上见过无数次了,但内核里是怎么处理的?我建议你把它刻在脑子里。

struct iphdr {
    __u8    ihl:4,      // 头部长度,单位4字节
            version:4;  // 版本号,IPv4就是4
    __u8    tos;        // 服务类型
    __be16  tot_len;    // 总长度,包含头部
    __be16  id;         // 标识符,分片用
    __be16  frag_off;   // 分片偏移 + 标志位
    __u8    ttl;        // 生存时间
    __u8    protocol;   // 上层协议号
    __sum16 check;      // 头部校验和
    __be32  saddr;      // 源IP
    __be32  daddr;      // 目的IP
    /* 选项字段可选 */
};

这里有个坑,我当年踩过。ihl字段的单位是4字节,不是字节。如果你直接拿它当头部长度用,那校验和算出来全是错的。我曾经在一个项目里排查了整整两天,最后发现是这里搞错了——嗯,从那以后我再也不敢小看这个位域了。

核心要点:IP头部最小20字节,最大60字节。选项字段现在基本不用了,但内核里还得支持,毕竟兼容性是个大问题。

再看frag_off字段,它其实包含了三个东西:

  • 高3位:标志位(DF、MF、保留位)
  • 低13位:分片偏移量,单位8字节

你想想看,为什么偏移量单位是8字节?因为13位最多表示8192,而IP数据报最大65535字节,除以8正好对上。这就是设计上的精妙之处。

4.2 路由子系统核心数据结构

路由子系统,说白了就是内核里的一张“地图”。数据包来了,查一下该往哪走。我个人的习惯是,先看数据结构,再看算法逻辑。

4.2.1 fib_table:路由表的“容器”

fib_table是路由表的抽象。每个路由表对应一个fib_table实例。

struct fib_table {
    struct hlist_node tb_hlist;   // 哈希链表节点
    u32         tb_id;            // 表ID,比如254是主路由表
    int         tb_num_default;   // 默认路由数量
    struct fib_alias *tb_aliases; // 路由别名链表
    struct fib_result *tb_result; // 查询结果缓存
    // ... 其他字段
};

我记得在调试一个多路由表场景时,发现tb_num_default这个字段特别有用。它告诉你当前表里有多少条默认路由。如果大于1,那路由选择就有歧义了——内核会怎么处理?答案是:选第一条匹配的。

4.2.2 fib_info:路由的“详细信息”

fib_info存储的是路由的具体信息,比如下一跳、出接口等。

struct fib_info {
    struct hlist_node fib_hash;   // 哈希节点
    struct hlist_node fib_lhash;  // 本地哈希节点
    int         fib_treeref;      // 引用计数
    u32         fib_prefsrc;      // 首选源地址
    u32         fib_priority;     // 路由优先级
    struct fib_nh  fib_nh[0];     // 下一跳数组,柔性数组
};

小技巧:fib_nh[0]是柔性数组,这意味着fib_info的大小是动态的。多路径路由(ECMP)就是靠这个实现的。我在做负载均衡优化时,就是通过修改这里的fib_nh数组来调整多路径权重的。

4.2.3 路由查找流程

路由查找的核心逻辑在fib_lookup函数里。大致流程是这样的:

  1. 先查fib_table的哈希表,找到匹配的表
  2. 再查表里的路由条目,匹配目的IP
  3. 如果有多条匹配,选优先级最高的
  4. 如果没找到,走默认路由

这里有个性能关键点:哈希冲突。当路由条目特别多时,哈希冲突会导致查找变慢。我曾经在一个核心路由器项目里,把哈希表大小从256改成4096,路由查找性能直接提升了3倍。

4.3 IP分片与重组:不得不说的痛

IP分片,说白了就是“大包拆小包”。为什么要有这个机制?因为链路层有MTU限制,比如以太网是1500字节。如果IP数据报超过MTU,就得拆。

4.3.1 分片过程

分片是在ip_fragment函数里完成的。核心逻辑:

// 伪代码,展示分片逻辑
int ip_fragment(struct sk_buff *skb, int (*output)(struct sk_buff *)) {
    // 1. 检查是否需要分片
    if (skb->len <= mtu) {
        return output(skb);  // 不用分片,直接发送
    }
    
    // 2. 计算分片数量
    int nfrags = (skb->len - hlen + mtu - 1) / mtu;
    
    // 3. 逐个创建分片
    for (int i = 0; i < nfrags; i++) {
        struct sk_buff *frag = alloc_skb(...);
        // 复制头部,修改frag_off字段
        // 设置MF标志(最后一个分片除外)
        // 计算校验和
        output(frag);
    }
}

注意:分片后的每个包都要重新计算IP头部校验和。而且,分片只发生在源端或中间路由器上。现在很多路由器都设置了DF标志,不允许分片——这时候如果包太大,就直接丢包并返回ICMP错误。

4.3.2 重组过程

重组是在接收端完成的,核心数据结构是ipq(IP分片队列)。

struct ipq {
    struct inet_frag_queue q;    // 分片队列基类
    u32         saddr;           // 源IP
    u32         daddr;           // 目的IP
    __be16      id;              // IP标识符
    u8          protocol;        // 协议号
    int         iif;             // 入接口索引
    struct timer_list timer;     // 重组超时定时器
};

重组的关键是超时机制。如果分片迟迟不来,内核会启动定时器清理。默认超时时间是30秒。我曾经遇到过一个问题:某个设备发送的分片间隔特别大,超过30秒,导致重组一直失败。最后调大了/proc/sys/net/ipv4/ipfrag_time才解决。

4.3.3 分片攻击与防御

分片机制也带来了安全风险。最常见的攻击是:

  • 分片重叠攻击:发送重叠的分片,绕过防火墙检查
  • 分片耗尽攻击:发送大量不完整的分片,耗尽内存

内核里是怎么防御的?ip_frag_queue函数里有个ip_frag_too_far检查,如果分片数量超过ipfrag_max_dist(默认64),就直接丢弃。嗯,这个值我建议别改,除非你很清楚自己在做什么。

4.4 知识体系总览

下面这张图,是我画的一个知识体系总览。你可以把它当作本章的“地图”。

IPv4网络层核心知识体系 IP头部结构 版本号(4bit) 头部长度(4bit) 服务类型(8bit) 总长度(16bit) 标识符 + 分片偏移 TTL + 协议 + 校验和 路由子系统 fib_table └─ tb_id, tb_aliases fib_info └─ fib_nh, fib_priority 路由查找流程 哈希 → 匹配 → 选路 IP分片与重组 分片触发条件 超过MTU 重组核心:ipq队列 超时机制(30s) 安全防御 分片攻击防护 核心要点总结 ① IP头部是网络层的基础,每一比特都有明确含义 ② 路由子系统通过 fib_table + fib_info 实现高效查找 ③ 分片与重组是应对MTU限制的必要机制,但也是攻击入口 ④ 理解这些数据结构,是优化网络性能的前提

这张图把本章的三个核心模块串起来了。你想想看,IP头部是“数据格式”,路由子系统是“控制逻辑”,分片重组是“异常处理”——三者缺一不可。

一句话总结:搞懂IP头部,你就能看懂数据包;搞懂路由数据结构,你就能优化转发性能;搞懂分片重组,你就能避免那些“莫名其妙”的网络问题。

好了,这一章就到这里。下一章咱们聊聊IPv6——嗯,不对,咱们先不急着往下走。先把这些数据结构在代码里跑一遍,你才能真正理解它们是怎么工作的。

实践建议:打开你的Linux机器,执行cat /proc/net/fib_trieip route show table all,看看实际的路由表长什么样。再抓个包,看看IP头部各个字段的值。纸上得来终觉浅,绝知此事要躬行。

专注资料整理