ICMP 协议分析
主要内容参照12. 网际控制报文协议ICMP — [野火]LwIP应用开发实战指南—基于野火STM32 文档,整理出来自用。
在 TCP/IP 协议栈中,ICMP 是保障网络通信可感知、可诊断的关键协议。它弥补了 IP 协议 “无连接、不可靠” 的缺陷,成为网络层故障排查与状态交互的核心工具。
一、ICMP 功能
IP 协议仅负责数据交付,不关心数据是否到达、为何失败。ICMP 的核心作用就是为 IP 协议补充反馈机制,主要体现在两点:
- 差错报告:当 IP 数据报因网络不可达、TTL 耗尽、端口未开放等原因无法交付时,路由器 / 目标主机会通过 ICMP 向源主机返回差错报文(如 “目的不可达”“超时”),避免数据 “石沉大海”。
- 网络查询:支持主机间主动交互网络状态,最典型的就是
ping
命令 —— 通过 “回显请求 / 应答” 报文验证目标主机是否可达,是网络调试的常用工具。
需注意:ICMP 本身不传输用户数据,也不纠正错误,仅负责 “报告问题”;且 ICMP 报文需封装在 IP 数据报中传输,因此同样受 IP 协议 “不可靠” 特性影响,可能被丢弃。
二、ICMP 报文结构:8 字节首部 + 可变数据
ICMP 报文通过 “两次封装” 传输(以太网帧→IP 数据报→ICMP 报文),其结构分为首部(固定 8 字节) 和数据区域(可变长度) :
字段 | 字节数 | 功能说明 |
---|---|---|
类型(Type) | 1 | 标识 ICMP 报文用途(如 3 = 目的不可达、8 = 回显请求、0 = 回显应答) |
代码(Code) | 1 | 进一步细化原因(如类型 3 “目的不可达” 中,代码 3 = 端口不可达、代码 4 = 需要分片) |
校验和 | 2 | 验证整个 ICMP 报文(含数据区)的传输完整性,计算方式与 IP 首部一致 |
可选字段 | 4 | 随报文类型变化(如回显报文用 “标识符 + 序列号” 匹配请求与应答) |
数据区域 | 可变 | 差错报文需携带 “IP 首部 + IP 数据前 8 字节”(用于源主机定位故障数据包);查询报文则携带自定义数据 |
三、ICMP 报文类型
ICMP 报文按功能分为差错报告报文和查询报文,实际开发中需重点关注以下常用类型:
报文分类 | 类型值 | 名称 | 核心用途 |
---|---|---|---|
差错报告 | 3 | 目的不可达 | 路由器 / 主机无法转发数据(如网络不可达、端口不可达) |
差错报告 | 11 | 超时 | TTL 耗尽(转发超时)或 IP 分片未按时重组(重组超时) |
差错报告 | 12 | 参数错误 | IP 首部格式错误(如字段非法) |
查询报文 | 8/0 | 回显请求 / 应答 | ping 命令核心:请求端发 8,应答端回 0,验证主机可达性 |
查询报文 | 9/10 | 路由器询问 / 通告 | 主机获取默认路由(现多被 DHCP 替代) |
四、LwIP 中的 ICMP 实现
LwIP 作为嵌入式常用的轻量级 TCP/IP 协议栈,仅实现 ICMP 核心功能(聚焦差错报告与ping
支持),以下是关键实现细节。
1. 核心数据结构
LwIP 用icmp_echo_hdr
结构体描述 ICMP 首部(复用为所有类型报文的首部模板),并通过宏定义简化字段操作:
// ICMP首部结构体(以回显报文为模板)
PACK_STRUCT_BEGIN
struct icmp_echo_hdr {PACK_STRUCT_FLD_8(u8_t type); // 类型PACK_STRUCT_FLD_8(u8_t code); // 代码PACK_STRUCT_FIELD(u16_t chksum); // 校验和PACK_STRUCT_FIELD(u16_t id); // 标识符(匹配ping请求/应答)PACK_STRUCT_FIELD(u16_t seqno); // 序列号(记录ping包顺序)
} PACK_STRUCT_STRUCT;
PACK_STRUCT_END// 常用类型宏定义
#define ICMP_ER 0 // 回显应答
#define ICMP_DUR 3 // 目的不可达
#define ICMP_ECHO 8 // 回显请求
#define ICMP_TE 11 // 超时
2. 差错报文发送
当 IP 数据报处理失败时,LwIP 通过专用函数发送差错报文,核心逻辑是 “封装 ICMP 首部 + 复制故障 IP 数据报关键信息”:
// 发送“目的不可达”报文
void icmp_dest_unreach(struct pbuf *p, enum icmp_dur_type t) {MIB2_STATS_INC(mib2.icmpoutdestunreachs);icmp_send_response(p, ICMP_DUR, t); // 复用发送逻辑
}// 核心发送函数:申请缓冲区→填写首部→拷贝IP关键数据→发送
static void icmp_send_response(struct pbuf *p, u8_t type, u8_t code) {struct pbuf *q = pbuf_alloc(PBUF_IP, sizeof(struct icmp_echo_hdr) + IP_HLEN + 8, PBUF_RAM);if (q == NULL) return; // 内存申请失败struct icmp_echo_hdr *icmphdr = (struct icmp_echo_hdr *)q->payload;icmphdr->type = type; // 填写差错类型icmphdr->code = code; // 填写具体原因icmphdr->chksum = 0; // 校验和后续计算// 拷贝故障IP数据报的“首部+前8字节数据”(用于源主机定位问题)SMEMCPY((u8_t *)q->payload + sizeof(struct icmp_echo_hdr), (u8_t *)p->payload, IP_HLEN + 8);ip4_output_if(q, NULL, &iphdr_src, ICMP_TTL, 0, IP_PROTO_ICMP, netif); // 发送pbuf_free(q);
}
3. 报文处理
LwIP 的icmp_input()
函数是 ICMP 报文的入口,仅处理 “回显请求”(支持ping
),其他类型直接丢弃:
void icmp_input(struct pbuf *p, struct netif *inp) {u8_t type = *((u8_t *)p->payload); // 读取报文类型switch (type) {case ICMP_ECHO: // 处理ping请求struct icmp_echo_hdr *iecho = (struct icmp_echo_hdr *)p->payload;// 1. 调整报文:类型改为“回显应答(0)”,交换源/目的IPICMPH_TYPE_SET(iecho, ICMP_ER);ip4_addr_swap(&iphdr->src, &iphdr->dest);// 2. 计算校验和,发送应答iecho->chksum = 0;ip4_output_if(p, src, LWIP_IP_HDRINCL, ICMP_TTL, 0, IP_PROTO_ICMP, inp);break;default: // 其他类型(如重定向、时间戳)直接丢弃ICMP_STATS_INC(icmp.drop);break;}pbuf_free(p);
}