IPv4 协议

本页目录

一个 4000 B IPv4 数据报遇到 1500 B 出口 MTU,可能被拆成三片,也可能被丢弃并触发错误反馈,关键在 DF、路径和端点处理。IPv4 提供尽力而为的数据报承载,地址、长度、TTL 与分片字段共同决定当前这一包怎样转发;可靠、有序和拥塞恢复则需要相应上层机制。

查阅问题本篇入口最容易算错的部分
头长和包长怎样区分逐字段与校验IHL × 4、Total Length 含头
多条路由都匹配选哪条CIDR 与下一跳最长前缀、策略与邻居
分片偏移为何是 185完整分片算例IP 数据偏移以 8 B 为单位
收到最后一片能否交付重组条件总长度、缺口、重叠与超时
小包通、大包不通PMTUD大小相关现象与因果证据
DSCP/ECN 怎样共存服务与拥塞标记六位 DSCP 和两位 ECN 独立

地址与路由:32 bit 地址不是直接的物理位置

CIDR 前缀将地址分为路由聚合所用的高位与剩余部分。192.0.2.130/26 所属网络为 192.0.2.128/26,掩码 255.255.255.192;在常规广播子网语义下地址范围到 192.0.2.191,其中网络/广播地址用途与普通主机不同。不能把“每子网一定减二”推广到 /31 点到点或 /32 主机路由。

在一张普通目的路由表中,匹配 198.51.100.0/24 的包优先使用它,而不是同样匹配的 0.0.0.0/0。策略路由、VRF 和多路径可以增加选择上下文,但不会让 ARP 代替路由决策:路由选出出口/下一跳后,才处理该链路的邻居映射。

地址不提供发送者认证;NAT 可改写地址及相关字段,也不会把 IPv4 变为可靠传输协议。32 bit 编号空间不等于全部可公开分配,历史分配池耗尽也不是所有区域、机构和用途在同一天失去任何地址。

基本头逐字段理解

IPv4 基本头:每行 32 bit,共 20 B;字段宽度按位比例 V=4 IHL DSCP ECN Total Length Identification Flags Fragment Offset TTL Protocol Header Checksum Source Address Destination Address IHL 以 4 B 为单位;IHL > 5 时后面还有选项和必要填充 图按网络位序从左向右排列;代码解析 Flags/Offset 应使用 0x4000、0x2000 与 0x1fff 等掩码。
字段语义阅读边界
Version / IHLIPv4;头长为 IHL×4 B最小 20 B,最大 60 B;不能小于 5
Total Length当前这一 IP 包的头加数据最大 65535 B,不含以太网填充/FCS
Identification为需要重组的数据报提供标识不是所有 IPv4 包全球唯一序号
Flags / Fragment Offset禁分片、更多分片和数据位置偏移不从以太网头或 IP 头开始
TTL限制转发生命周期路由转发至少递减,不能靠初始值精确确定系统
Protocol指示上层,如 TCP 6、UDP 17不是端口号
Header Checksum当前 IPv4 头的校验不覆盖 IP 载荷,也不认证来源

解析时先确认实际可用字节足够,再校验版本、IHL 和 Total Length 之间的关系;从以太网收到额外填充不能把它算进 IP 数据。IP Options 有各自格式与复制属性,不能假定每片都简单复制一份完全相同的头。RFC 791:IPv4 格式与分片

TTL 与头校验为什么每跳相关

路由器转发时更新 TTL,头部发生变化,校验和也要更新;源/目的在普通非 NAT 转发中保持不变,不意味着整个 IP 头不变。TTL 耗尽时丢弃,并在协议允许和资源/策略条件下生成 ICMP Time Exceeded;错误反馈有例外与限速,不能要求每次丢弃必定返回可见 ICMP。

IPv4 头校验和对 16 bit 字进行一补和,再取反,进位要折回低 16 bit。验证完整头时,一补和应为全 1,等价的“取反后结果”为零。它只保护 IPv4 头;TCP/UDP 校验另有伪首部与载荷覆盖规则,IPv4 UDP 还存在零校验和的特定语义,不能说所有上层都必定提供同样保障。

末尾代码会构造完整头,验证校验和,再把 TTL 从 64 改为 63,确认旧校验不再成立并重算。这比只记“路由器每跳减一”更能说明字段之间的依赖。

TTL 只减一,为什么旧校验就失效?

把 TTL 与 Protocol 组成的16位字标成金色,把校验字段标成蓝色。两者的变化在同一份头部中发生;下方保留旧值,便于看见重算修复了什么。载荷开关刻意不改变头,展示校验覆盖范围的边界。

正在呈现知识画面
TTL 只减一,为什么旧校验就失效?

IPv4 头校验覆盖头字段。TTL 改变会破坏旧校验,载荷改变却不会;通过头校验不能证明数据或发送者可信。

分片:按数据长度对齐,而非按整片长度对齐

设原数据报 Total Length=4000、IHL=5,无选项,则 IP 数据为 3980 B。出口 MTU=1500,每片至少保留 20 B 头;非末片的数据长度需为 8 B 整数倍,最大取 floor((1500−20)/8)×8=1480 B。

4000 B IP 数据报经过 MTU 1500:分的是 3980 B 数据 原包:20 B IP 头 + 3980 B IP 数据 数据下标 0 … 3979,假设无 IP 选项,DF=0 片 1:1480 B 数据 offset=0,MF=1 片 2:1480 B 数据 offset=185,MF=1 片 3:1020 B 数据 offset=370,MF=0 Total Length = 1500 Total Length = 1500 Total Length = 1040 偏移从原始 IP 数据部分起算,单位 8 B;每片另带自己的 IP 头,只有首片包含传输层起始头。
分片原数据范围数据长度Offset 字段MF本片 Total Length
10~14791480011500
21480~2959148018511500
32960~3979102037001040

1480/8=185、2960/8=370。非末片是数据长度按 8 对齐,1500 B 的整片总长本身并非 8 的整数倍。每片都有自己的 Total Length、偏移、MF 和校验和,Identification 则用于把属于同一数据报的片联系起来;TCP/UDP 起始头只位于原始数据开头,不会在每个分片复制一次。

DF 是 16 bit Flags/Offset 字的 0x4000,MF 是 0x2000,偏移掩码为 0x1fff;这避免把图上从左数的标志位编号误当作最低位编号。设置 DF 不会让数据自动变小,出口放不下时通常需要丢弃并给出相应反馈。

Identification 的约束需结合源、目的、协议和重组时间窗。RFC 6864 对不分片的 atomic datagram 放宽了 ID 使用要求,因此“每个 IP 包必须有唯一 ID”不成立;高包速率还会让有限 ID 空间与重组更值得关注。RFC 6864:IPv4 ID 语义

重组要凑齐区间,而非只等最后一片

收到末片 MF=0 后,可由 8×Offset + 本片数据长度 得到原始 IP 数据总长度:本例为 370×8+1020=3980 B,再加原始首片头长得到 4000 B。不能把 Offset 原值直接加到末片 Total Length,更不能把末片的 IP 头再次计入数据范围。

知道总长度还不够,前面的区间必须没有缺口,并通过相应重叠、边界和资源检查;片可乱序到达。普通 IP 转发路径不需要每跳重组,但防火墙、NAT 等中间处理可能为了其功能重组。超时或资源限制导致重组失败后,IP 层本身不会请求补发某一片,上层是否以及怎样重传取决于协议。

重组超时由实现及配置决定,不是全网统一常数。Linux 7.2 的 IP_FRAG_TIME 为 30×HZ,并用于初始化可配置的 ipfrag_time;这仍只是该实现的基准,不代表实际网络所有设备。碎片化也使中间设备处理更复杂,现代应用通常更希望通过合理分包避免它。

末片到了,为什么仍不能交付?

先递送最后一片,再补中间分片,观察“知道多长”与“已经凑齐”的差别。每个按钮上的 Offset 是报文字段,区间图上的位置已经乘过8;重复同一分片只复用已有覆盖,不能让已收字节数虚增。

正在呈现知识画面
末片到了,为什么仍不能交付?

MF=0 只揭示终点;重组还要求从零到终点没有缺口。Offset 的单位是8字节,重复递送不会补上别处的洞。

PMTUD:错误反馈帮助端点调整包大小

经典 IPv4 PMTUD 让端点发送 DF 包,当中间出口 MTU 不够时,路由器丢弃并按条件返回 ICMP Destination Unreachable Type 3 Code 4,携带 MTU 等信息。端点据此更新路径估计,传输/应用层调整后续分包;不是把原包的 DF 自动清零再继续发送。RFC 1191

如果必要 ICMP 被过滤,端点可能持续重发过大的包,形成黑洞。但“小 ping 通、大 ping 不通”只提示大小相关问题,还可能是过滤、限速或不同路径。验证需要控制探测大小、协议和方向,并观察出入口或 ICMP 证据,不能单凭一次探测下结论。

TCP MSS 通告约束对端发送的 TCP 数据规模,并不等于整包 MTU,也不覆盖 UDP。MSS clamping 可缓解某些 TCP 路径问题,但所依据 PMTU 可能不完整,路径还可能变化,不能作为所有 ICMP 被屏蔽场景的必然修复。PLPMTUD/DPLPMTUD 利用分包层的探测与确认机制帮助发现可用大小,其状态机仍要正确实现。RFC 8899:数据报传输的 DPLPMTUD

DSCP:标记需由网络策略兑现

IPv4 第二个字节高六位为 DSCP、低两位为 ECN,ECN 不是“DSCP 的低两位”。DSCP 选择逐跳转发行为,设备是否信任、重标记、整形或映射到哪个队列,由网络策略决定。RFC 2474:DS 字段

CS 类、AF 类与 EF 表达不同服务设计。AF 的不同丢弃优先级是在相应类中的处理维度,不能把全部代码点按数字大小排成统一高低;EF 常用代码点为十进制 46,但并不等于任意设备上的“无限最高优先级”。低延迟行为需要配置服务速率、准入和调度约束。RFC 3246:EF PHB

例如 DSCP=46、ECN=ECT(0)=2,完整字节为 (46<<2)|2=0xBA。修改 DSCP 时应保留所需 ECN 位,不能随手覆盖整个字节破坏拥塞信号。

ECN:先有协商,再解释拥塞反馈

ECN 两位含义
00Not-ECT
10ECT(0)
01ECT(1)
11CE,经历拥塞

以 RFC 3168 的经典 TCP ECN 为例,SYN 用 TCP ECE/CWR 标志表达协商,SYN-ACK 用相应响应,而不是通过给初始 SYN 的 IP 头写 ECT 来完成协商。之后合适数据包可标 ECT,支持 ECN 的网络节点可在相应拥塞条件下标 CE,接收方经 ACK 的 ECE 回报,发送方执行拥塞响应并用 CWR 表示已响应。RFC 3168:经典 ECN

CE 不保证从此绝不丢包,队列、严重拥塞和不支持路径仍可丢弃。发送方的窗口响应取决于所用拥塞算法,不能统一称为每次减半;DCTCP 等有不同反馈处理。后续规范也放宽了部分实验限制,因此应把上述过程明确视为经典基线,不拿它覆盖所有现代 ECN 变体。RFC 8311:ECN 实验边界更新

本地构造、分片与校验

import struct
from ipaddress import IPv4Address, IPv4Network

def checksum(data):
    assert len(data)%2 == 0
    total = sum(struct.unpack('!%dH' % (len(data)//2), data))
    while total >> 16:
        total = (total&0xffff)+(total>>16)
    return (~total)&0xffff

header = bytearray(struct.pack('!BBHHHBBH4s4s',
    0x45, (46<<2)|2, 4000, 0x1234, 0, 64, 17, 0,
    IPv4Address('192.0.2.10').packed, IPv4Address('198.51.100.20').packed))
header[10:12] = struct.pack('!H', checksum(header))
assert checksum(header) == 0
assert header[1] == 0xBA
header[8] = 63
assert checksum(header) != 0
header[10:12] = bytes(2)
header[10:12] = struct.pack('!H', checksum(header))
assert checksum(header) == 0

payload = bytes(i%251 for i in range(3980))
chunk = ((1500-20)//8)*8
fragments = []
for start in range(0,len(payload),chunk):
    data = payload[start:start+chunk]
    fragments.append((start//8, int(start+len(data)<len(payload)), len(data)+20, data))
assert [f[:3] for f in fragments] == [(0,1,1500),(185,1,1500),(370,0,1040)]
assert b''.join(f[3] for f in fragments) == payload
assert fragments[-1][0]*8 + len(fragments[-1][3]) + 20 == 4000
assert str(IPv4Network('192.0.2.130/26', strict=False)) == '192.0.2.128/26'
print('IPv4 头校验、TTL 更新、DS 字节、分片区间与 CIDR 检查通过')

这是无选项教学报文的本地验证,未发送探测流量、修改 MTU/MSS 或调整拥塞控制配置。

继续查阅