bigywater.tech
网络协议 · 12

IP 分片与重组:超过 MTU 的包是怎么拆开的

IP 包比链路的 MTU 大怎么办?拆成分片,到目的地再拼回去。但分片到底怎么标记、偏移怎么算、为什么说“丢一个全丢”、为什么 TCP 总是带 DF 位?这篇在真实网口上抓一个 4000 字节的 UDP 数据报,逐个分片看 id、MF、片偏移,再用实验和计算看分片的代价。

3 个分片
4000 字节 UDP 数据报在 MTU 1500 的 en0 上:偏移 0 / 1480 / 2960,MF = 1 / 1 / 0
12.2%
计算值:5% 丢包下,60000 字节数据报(41 片)整个送达的概率,1000 字节的是 95.0%
302 / 303
TCP 下载 1 MiB 时抓到的 IP 包里带 DF 位的个数,没有任何分片

分片怎么标记

实测 · 本机执行输出
  4000 字节的 UDP 数据(+ 8 字节 UDP 头 = 4008 字节的 IP 载荷)抓到 3 个 IP 分片:
    序号  id      MF  DF  片偏移(字节)  IP 总长  IP 载荷  以太网帧长
    1     843     1   0   0            1500     1480     1514
    2     843     1   0   1480         1500     1480     1514
    3     843     0   0   2960         1068     1048     1082
  规律:每个分片的 IP 载荷是 8 的倍数(最后一个除外),这里 1480 = 1500 − 20;最后一个分片 MF=0。偏移字段 13 位、单位 8 字节,所以抓到的“片偏移”是字节数 / 8 × 8。
  只有第一个分片带 UDP 头(源 / 目的端口);后面的分片只有 IP 头 + 数据,单看它们不知道是哪个端口——所以带端口过滤的防火墙遇到分片很头疼
  netstat -s -p ip 的计数这次:发出的分片 +3(与抓到的 3 个分片一致)

同一个数据报的所有分片共用一个 id;MF(更多分片)位除最后一个外都置 1;片偏移字段 13 位、单位是 8 字节,所以每个分片的载荷是 8 的倍数。实测 4000 字节 UDP 加 8 字节头,在 MTU 1500 上得到载荷 1480 / 1480 / 1048 的三个分片,只有第一个带 UDP 头。

环回不分片

实测 · 本机执行输出
  先看系统限制:net.inet.udp.maxdgram: 9216——默认情况下超过 9216 字节的 UDP 数据报 sendto 会报 Message too long,要把发送缓冲区调大(SO_SNDBUF)才能发更大的
  不调大 SO_SNDBUF 直接发 20000 字节:[Errno 40] Message too long
  应用调用一次 sendto(40000 字节),一次 recvfrom 拿到 40000 字节;lo0 上看到 1 个 IP 包(IP 总长远大于 MTU 16384,却没有分片——macOS 的 lo0 不对超过 MTU 的包分片,直接整包交给本机的接收方):
    分片 1: id=4415 MF=0 偏移=0 IP 载荷=40008
  内核计数: 被分片的包 +0,发出的分片 +0,收到的分片 +0,成功重组 +0

macOS 的 lo0 MTU 是 16384,但 40000 字节的 UDP 数据报在 lo0 上仍是一个 IP 包,内核分片计数不动。想在本机看真分片,要用真实网口(如 en0)。

丢一个全丢

实测 · 本机执行输出
   1000 字节的数据报:送达 953 / 1000 = 95.3%
  20000 字节的数据报:送达 949 / 1000 = 94.9%
  60000 字节的数据报:送达 950 / 1000 = 95.0%
  一个 60000 字节的数据报在 lo0 上是 1 个 IP 包——lo0 不分片,所以 3 档大小的送达率都约等于 95%,看不出“丢一个分片全丢”的放大效应
  要看放大效应得有真的分片(MTU 1500 的以太网路径),而丢包 / 延迟只能在 lo0 上注入;下面用公式和随机模拟补上(这是计算,不是实测)
    数据报大小    分片数(MTU 1500)  理论送达率 0.95^n  模拟 10 万次
      1000 字节         1            95.0%            95.1%
      4000 字节         3            85.7%            85.6%
     20000 字节        14            48.8%            48.8%
     60000 字节        41            12.2%            12.2%

任何一个分片丢失,目的地就无法重组整个数据报。分片越多,送达概率按 0.95^n 下降。lo0 上的实测因为没有真分片,三档大小都约 95%;放大效应这部分是公式和模拟,不是实测。

TCP 的做法

实测 · 本机执行输出
  抓到 303 个 IP 包:MF 置位 0 个,片偏移非 0 0 个,DF 置位 302 个——TCP 的包全部带 DF,从不依赖分片;超过路径 MTU 时靠 ICMP“需要分片”来调整(见 PMTUD 一篇)

TCP 的包带 DF 位,并用 MSS 限制段长,发送方根据 ICMP 的反馈调整,基本不依赖分片;实测 303 个包里 302 个带 DF,没有 MF 或非零偏移。

重组的限制

实测 · 本机执行输出
  net.inet.ip.maxfragpackets: 8192
  net.inet.ip.maxfragsperpacket: 128
  netstat -s -p ip 当前计数:收到的分片 1716;成功重组的包 0;发出的分片 12;被分片的包 0;重组超时丢弃的分片 0;因其它原因丢弃的分片 0
  这些计数多半是 0 或很小:现代网络里 TCP 为主、有 PMTUD,真正的 IP 分片已经很少见;UDP 大数据报(DNS 大响应、QUIC 之前的一些协议)仍可能触发

系统限制同时重组的包数和每个包的分片数(maxfragpackets、maxfragsperpacket),超时的分片会被丢弃。netstat -s -p ip 可以看到各项计数。

分步演示

IP 分片
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。第一节在 en0 上抓包(用 tcpdump 的 pcap 文件交给 dpkt 解析),网关地址与 MAC 做了遮盖;第三节的丢包由 netlab 在 lo0 上注入,以太网分片数下的送达率是公式与随机模拟的结果,不是实测;第四节只测了一次 1 MiB 下载
    • 没有验证:IPv6(中间路由器不分片,只有源端分片)的行为;重组超时的具体时长与触发;分片重叠 / 微小分片攻击;分片对防火墙、NAT 的影响;真实以太网路径上的丢包下分片数据报的送达率;netstat 里“收到的分片 1716 而成功重组 0”的原因
    京ICP备2026067502号-1京公网安备11010502063487号