消息格式 (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 消息类型。常见值:
| 值 | 消息类型 |
|---|---|
| 0 | Echo Reply (回显应答) |
| 3 | Destination Unreachable (目的地不可达) |
| 4 | Source Quench (源抑制) |
| 5 | Redirect (重定向) |
| 8 | Echo Request (回显请求) |
| 11 | Time Exceeded (超时) |
| 12 | Parameter Problem (参数问题) |
| 13 | Timestamp Request (时间戳请求) |
| 14 | Timestamp Reply (时间戳应答) |
| 15 | Information Request (信息请求) |
| 16 | Information 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 消息类型使用额外的字段。详见各消息类型规范。
处理指南
发送方职责
-
构造正确的 IP 头部:
- 设置 Protocol = 1 (ICMP)
- 设置适当的 TTL
- 计算 IP 头部校验和
-
构造 ICMP 消息:
- 设置正确的 Type 和 Code
- 填写消息特定字段
- 将未使用字段置零
- 计算 ICMP 校验和
-
为错误包含上下文:
- 包含原始 IP 头部
- 包含原始数据的前 64 位
接收方职责
-
验证 IP 头部:
- 检查 IP 头部校验和
- 验证目的地址
-
验证 ICMP 消息:
- 验证 ICMP 校验和
- 检查 Type 和 Code 值
-
处理消息:
- 提取相关信息
- 多路分解到适当的处理程序
- 采取适当的行动
-
错误处理:
- 静默丢弃无效消息
- 不要发送关于 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 消息类型共享这一公共头部结构。校验和之后字段的解释因消息类型而异。实现必须处理所有已定义的消息类型, 并静默丢弃无法识别的类型。