跳到主要内容

消息格式 (Message Formats)

ICMP 消息使用基本的 IP 头部发送。数据报数据部分的第一个字节是 ICMP 类型字段; 该字段的值决定了其余数据的格式。任何标记为 "unused" (未使用) 的字段保留供将来扩展使用, 发送时必须置零, 但接收方不应使用这些字段 (除了将其包含在校验和计算中)。除非另有说明, 互联网头部字段的值如下所述:

ICMP 的 IP 头部字段

Version (版本)

4

IHL (互联网头部长度)

以 32 位字为单位的互联网头部长度。

Type of Service (服务类型)

0

Total Length (总长度)

互联网头部和数据的总字节长度。

Identification, Flags, Fragment Offset (标识, 标志, 分片偏移)

用于分片, 参见 [1]。

Time to Live (生存时间)

以秒为单位的生存时间; 由于该字段在每台处理数据报的机器上递减, 该字段的值应至少与数据报将经过的网关数量一样大。

Protocol (协议)

ICMP = 1

Header Checksum (头部校验和)

头部中所有 16 位字的反码和的 16 位反码。计算校验和时, 校验和字段应置零。此校验和将来可能被替换。

Source Address (源地址)

组成 ICMP 消息的网关或主机的地址。除非另有说明, 这可以是网关的任意地址。

Destination Address (目的地址)

消息应发送到的网关或主机的地址。

ICMP 消息格式

 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Code | Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Message Body |
| (Format varies by Type) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Type 字段 (8 位)

标识 ICMP 消息类型。常见值:

消息类型
0Echo Reply (回显应答)
3Destination Unreachable (目的地不可达)
4Source Quench (源抑制)
5Redirect (重定向)
8Echo Request (回显请求)
11Time Exceeded (超时)
12Parameter Problem (参数问题)
13Timestamp Request (时间戳请求)
14Timestamp Reply (时间戳应答)
15Information Request (信息请求)
16Information Reply (信息应答)

Code 字段 (8 位)

为消息类型提供附加上下文。其含义取决于 Type 字段。

示例 - Destination Unreachable (Type 3) 的 Code 值:

  • 0 = 网络不可达
  • 1 = 主机不可达
  • 2 = 协议不可达
  • 3 = 端口不可达
  • 4 = 需要分片但设置了 DF 标志
  • 5 = 源路由失败

Checksum 字段 (16 位)

校验和是从 ICMP Type 字段开始的 ICMP 消息的反码和的 16 位反码。计算校验和时, 校验和字段应置零。

校验和计算算法:

// ICMP 校验和计算的伪代码
uint16_t calculate_icmp_checksum(uint8_t *data, int length) {
uint32_t sum = 0;

// 对所有 16 位字求和
for (int i = 0; i < length; i += 2) {
uint16_t word = (data[i] << 8) + data[i+1];
sum += word;
}

// 如果长度为奇数, 添加最后一个字节
if (length % 2 == 1) {
sum += (data[length-1] << 8);
}

// 将 32 位和折叠为 16 位
while (sum >> 16) {
sum = (sum & 0xFFFF) + (sum >> 16);
}

// 返回反码
return ~sum;
}

验证:

// 接收方通过对整个消息 (包括校验和字段) 计算校验和来验证
// 结果应为 0xFFFF
bool verify_icmp_checksum(uint8_t *message, int length) {
uint16_t result = calculate_icmp_checksum(message, length);
return (result == 0xFFFF);
}

完整的 ICMP 数据包结构

以太网帧
┌──────────────────────────────────────────┐
│ 以太网头部 │
├──────────────────────────────────────────┤
│ IP 头部 │
│ - Version: 4 │
│ - IHL: 5 (20 字节) │
│ - Protocol: 1 (ICMP) │
│ - Source IP: 192.168.1.1 │
│ - Dest IP: 10.0.0.1 │
├──────────────────────────────────────────┤
│ ICMP 消息 │
│ ┌────────────────────────────────────┐ │
│ │ Type (8 位) │ │
│ ├────────────────────────────────────┤ │
│ │ Code (8 位) │ │
│ ├────────────────────────────────────┤ │
│ │ Checksum (16 位) │ │
│ ├────────────────────────────────────┤ │
│ │ 消息特定数据 │ │
│ │ (根据 Type 和 Code 而变化) │ │
│ └────────────────────────────────────┘ │
└──────────────────────────────────────────┘

错误消息格式

对于错误消息 (Type 3, 4, 5, 11, 12), 消息体包含:

 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Code | Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Unused = 0 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Internet Header + 64 bits of Original Data Datagram |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

为什么要包含原始数据?

  • 允许接收方识别是哪次传输导致了错误
  • 包含用于多路分解的源/目的端口
  • 为错误处理提供上下文

示例:

原始 TCP 段:
┌─────────────────────────────────────┐
│ IP Header (20 字节) │
│ - Src: 192.168.1.10 │
│ - Dst: 203.0.113.50 │
│ - Protocol: 6 (TCP) │
├─────────────────────────────────────┤
│ TCP Header (20 字节) │
│ - Src Port: 54321 │
│ - Dst Port: 80 │
│ - Seq: 1000 │
├─────────────────────────────────────┤
│ TCP 数据 │
└─────────────────────────────────────┘

ICMP 错误消息包含:
┌─────────────────────────────────────┐
│ ICMP Header (8 字节) │
│ - Type: 3 │
│ - Code: 1 (Host Unreachable) │
├─────────────────────────────────────┤
│ 原始 IP Header (20 字节) │ ← 完整 IP 头部
├─────────────────────────────────────┤
│ TCP Header 的前 8 字节 │ ← 包含端口号
│ - Src Port: 54321 │
│ - Dst Port: 80 │
└─────────────────────────────────────┘

8 字节的 TCP 头部足以识别:
- 源端口和目的端口
- 允许将错误多路分解到正确的套接字

查询消息格式

对于查询/应答消息 (Type 0, 8, 13, 14, 15, 16):

 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Code | Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Identifier | Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Identifier (标识符) 和 Sequence Number (序列号):

  • 用于将请求与应答匹配
  • Identifier: 通常是发送方的进程 ID
  • Sequence Number: 随每条消息递增

示例 - Ping 会话:

请求 1:  ID=12345, Seq=1, Type=8
应答 1: ID=12345, Seq=1, Type=0

请求 2: ID=12345, Seq=2, Type=8
应答 2: ID=12345, Seq=2, Type=0

请求 3: ID=12345, Seq=3, Type=8
应答 3: ID=12345, Seq=3, Type=0

匹配: 当 (ID, Seq) 相同时, 应答与请求匹配

字段说明

未使用字段

任何标记为 "unused" (未使用) 或 "reserved" (保留) 的字段:

  • 发送方: 必须置零
  • 接收方:
    • 必须将其包含在校验和计算中
    • 不应出于任何目的解释或使用这些字段
    • 为将来的协议扩展预留空间

消息特定字段

不同的 ICMP 消息类型使用额外的字段。详见各消息类型规范。

处理指南

发送方职责

  1. 构造正确的 IP 头部:

    • 设置 Protocol = 1 (ICMP)
    • 设置适当的 TTL
    • 计算 IP 头部校验和
  2. 构造 ICMP 消息:

    • 设置正确的 Type 和 Code
    • 填写消息特定字段
    • 将未使用字段置零
    • 计算 ICMP 校验和
  3. 为错误包含上下文:

    • 包含原始 IP 头部
    • 包含原始数据的前 64 位

接收方职责

  1. 验证 IP 头部:

    • 检查 IP 头部校验和
    • 验证目的地址
  2. 验证 ICMP 消息:

    • 验证 ICMP 校验和
    • 检查 Type 和 Code 值
  3. 处理消息:

    • 提取相关信息
    • 多路分解到适当的处理程序
    • 采取适当的行动
  4. 错误处理:

    • 静默丢弃无效消息
    • 不要发送关于 ICMP 错误的 ICMP 错误

字节序

所有多字节字段以网络字节序 (大端序, big-endian) 传输:

  • 最高有效字节在前
  • 与 IP 头部约定一致

示例:

// 校验和值: 0x1234
// 线路格式: [0x12] [0x34]

uint16_t checksum = 0x1234;
uint8_t byte1 = (checksum >> 8) & 0xFF; // 0x12
uint8_t byte2 = checksum & 0xFF; // 0x34

// 在线路上: 先传输 byte1, 再传输 byte2

实现说明: 所有 ICMP 消息类型共享这一公共头部结构。校验和之后字段的解释因消息类型而异。实现必须处理所有已定义的消息类型, 并静默丢弃无法识别的类型。