📚 第四章 IP协议
📄 一句话概括

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 秒实验

  1. 算一下:首部长度字段是 0x45 时,首部有多少字节?
  2. 想一下:为什么校验和不覆盖数据部分?
  3. 想一下:TTL 设成 1 会发生什么?

常见误区

  1. 以为「校验和能查出数据错误」。 只覆盖首部,数据错了要靠 TCP 或 UDP 的校验和。
  2. 以为「首部长度固定 20 字节」。 有选项时可以到 60,所以要靠字段算。
  3. 以为「总长度只算数据」。 含首部,这一点和 IPv6 正好相反。
自测三问(点开对答案) 1. IPv4 首部最短多少字节?最长多少? —— **最短 20 字节,最长 60 字节**(选项最多 40 字节)。 2. 首部校验和覆盖哪些内容? —— **只覆盖首部**,不含数据部分。 3. 协议号 6 和 17 分别代表什么? —— **6 是 TCP,17 是 UDP**,1 是 ICMP。

下一节看 IPv6 首部:同样是首部,被砍成了什么样。

相关笔记