IPv4 首部最短 20 字节,13 个字段,有选项时可以到 60 字节。 每个字段都对应一个实际问题:长度、分片、跳数、校验、寻址。
4.5.2 的分片三个字段就在这里,这一节把它们放回原位。 4.8 会讲 IPv6 首部,两节对照着看最有效。
一、20 字节的固定部分
先记结构:前 20 字节是所有包都有的,后面可以跟 0 到 40 字节的选项。
| 字节 | 内容 | 说明 |
|---|---|---|
| 0 | 版本 + 首部长度 | 各 4 位 |
| 1 | 区分服务 | 优先级/QoS |
| 2~3 | 总长度 | 首部加数据 |
| 4~5 | 标识 | 分片时同组编号 |
| 6~7 | 标志 + 片偏移 | DF/MF 和位置 |
| 8 | 生存时间 | 每跳减一 |
| 9 | 协议 | 上层是什么 |
| 10~11 | 首部校验和 | 只管首部 |
| 12~15 | 源地址 | 4 字节 |
| 16~19 | 目的地址 | 4 字节 |
第一行把两个字段塞进一个字节:高 4 位是版本(4),低 4 位是首部长度(以 32 位字为单位)。 所以 0x45 表示「IPv4,首部 5 个字 = 20 字节」。 第八行「只管首部」很容易误解:校验和不覆盖数据部分,所以数据错了它查不出来, 要靠上层协议。
二、几个字段的实际用途
挑四个最常用的说清:
| 字段 | 典型值 | 实际意义 |
|---|---|---|
| 生存时间 TTL | 64 / 128 / 255 | 还能过几跳 |
| 协议号 | 6=TCP,17=UDP,1=ICMP | 上层是谁 |
| 区分服务 DSCP | 0 是默认 | 要不要优先转发 |
| 标识 | 随机 | 分片归组用 |
第一行 TTL 是最有用的排查工具:每过一台路由器减一,减到 0 就丢弃并回 ICMP 超时。
所以 traceroute 就是「故意设很小的 TTL」,让路径上的设备一个个现身。
第二行「协议号」是「多路复用」的入口:IP 层收到包,看这个数字决定交给 TCP、UDP 还是 ICMP。
IPv6 把它改名成「下一个首部」,作用一样。
三、校验和怎么算
算法三步,简单到有点粗糙:每 16 位当一个大数加起来、进位加回低 16 位、取反。
| 步骤 | 做什么 | 为什么 |
|---|---|---|
| 1 | 16 位一组求和 | 把首部当一个长整数 |
| 2 | 进位回卷 | 防止溢出丢信息 |
| 3 | 取反 | 让接收方算出来是全 1 |
第三步「取反」是为了让验证变简单: 发送方填好校验和,接收方把整个首部(含校验和)再算一遍,结果应该是 0xFFFF。 不需要「减」也不需要「比」,加一遍看结果就行。 这个算法的弱点要清楚:它查不出「两个 16 位字交换位置」,也挡不住有意构造的碰撞。 所以它是「防随机比特错」,不是「防篡改」——防篡改要靠上层的 TLS。
四、选项字段为什么没人用
选项最多 40 字节,能做的事不少(记录路由、时间戳、安全标签)。 但现实中几乎不用,原因有三个:
| 原因 | 后果 | 结果 |
|---|---|---|
| 长度可变 | 路由器要逐字节解析 | 转发变慢 |
| 很多设备直接丢 | 带了也白带 | 不可靠 |
| 有安全风险 | 可被用来探测路径 | 常被禁用 |
第一行是根本原因:「首部长度可变」意味着「找目的地址之前要先算偏移」, 这对硬件转发是负担。 IPv6 干脆把它改成「固定 40 字节 + 扩展首部」,就是为了消除这个负担。 第二行「很多设备直接丢」是实践经验:所以就算你用了选项,也不能指望它一定生效。 这也是 IPv6 把扩展首部设计成「可跳过」的原因。
五、动手 Demo:手工拼一个 IPv4 首部
用 struct 拼出 20 字节,再按字段读回来,最后算一遍校验和。
注意最后「填进去再算一遍」的结果必须是 0xFFFF。
import struct, ipaddress
src = bytes([192, 168, 3, 152])
dst = bytes([8, 8, 8, 8])
hdr = struct.pack('!BBHHHBBH4s4s', 0x45, 0, 1500,
0x1234, 0x4000, 64, 6, 0, src, dst)
w = struct.unpack('!10H', hdr)
s = sum(w)
while s >> 16:
s = (s & 0xFFFF) + (s >> 16)
print(hex(s), hex((~s) & 0xFFFF))
实跑输出:
--------------------------------------------------------------------
手工拼一个 IPv4 首部
--------------------------------------------------------------------
**不看文档,直接用字节拼一遍**:**拼完再按字段读回来,看看对不对。**
**长度 20 字节,先按 4 字节一行打出来**:
**00: 45 00 05 DC**
**04: 12 34 40 00**
**08: 40 06 00 00**
**12: C0 A8 03 98**
**16: 08 08 08 08**
**第一字节 0x45 拆开就是两个 4 位字段**:
**高 4 位 4 是版本,低 4 位 5 是「首部长度 5 个 32 位字」,也就是 20 字节。**
字段 读出来的值 含义
-----------------------------------------------------
版本 4 IPv4
首部长度 5 字 = 20 字节 含选项
区分服务 0 优先级
总长度 1500 首部加数据
标识 0x1234 分片时同一组的编号
标志 0x4000 0x4000 表示 DF
生存时间 64 每过一跳减一
协议 6 6 是 TCP
首部校验和 0x0000 待填
源地址 192.168.3.152
目的地址 8.8.8.8
**校验和是算出来的,算法就三步**:**每 16 位一组求和、进位加回低 16 位、取反。**
**累加得 0xB167,取反得 0x4E98**
**填进去再算一遍:0xFFFF**
**结果必须是 0xFFFF(取反后为 0)**——
**这就是接收方验证的全部动作:把整个首部当数字加起来,看是不是全 1。**
**这个算法很弱**:**它查不出「两个 16 位字交换位置」这种错**,**也挡不住有意构造的碰撞。**
**它的定位只是「防止传输中的随机比特错」,不是「防篡改」。**
!BBHHHBBH4s4s 这个格式串要和字段顺序对上:B 是 1 字节、H 是 2 字节、4s 是 4 字节。
加起来 1+1+2+2+2+1+1+2+4+4 = 20,正好是一行首部。
0x4000 那个位置放的是「标志 + 片偏移」:
高 3 位是标志,0x4000 表示「DF=1、MF=0」——也就是「不许分片」。
所以这个包在 MTU 小于 1500 的链路上会被拒。
校验和那两个数(0xB167 和 0x4E98)可以自己验算:
把 0x4E98 填进第 10、11 字节再算,结果必须是 0xFFFF。
这个「填进去再验一遍」的动作,就是排查「校验和写错」的标准手法。
30 秒实验
- 算一下:首部长度字段是 0x45 时,首部有多少字节?
- 想一下:为什么校验和不覆盖数据部分?
- 想一下:TTL 设成 1 会发生什么?
常见误区
- 以为「校验和能查出数据错误」。 只覆盖首部,数据错了要靠 TCP 或 UDP 的校验和。
- 以为「首部长度固定 20 字节」。 有选项时可以到 60,所以要靠字段算。
- 以为「总长度只算数据」。 含首部,这一点和 IPv6 正好相反。
自测三问(点开对答案)
1. IPv4 首部最短多少字节?最长多少? —— **最短 20 字节,最长 60 字节**(选项最多 40 字节)。 2. 首部校验和覆盖哪些内容? —— **只覆盖首部**,不含数据部分。 3. 协议号 6 和 17 分别代表什么? —— **6 是 TCP,17 是 UDP**,1 是 ICMP。下一节看 IPv6 首部:同样是首部,被砍成了什么样。
- ☐ (本节完,下一节 4.8 IPv6首部格式)