ICMP 协议分析

主要内容参照12. 网际控制报文协议ICMP --- [野火]LwIP应用开发实战指南---基于野火STM32 文档,整理出来自用。

在 TCP/IP 协议栈中,ICMP 是保障网络通信可感知、可诊断的关键协议。它弥补了 IP 协议 "无连接、不可靠" 的缺陷,成为网络层故障排查与状态交互的核心工具。

一、ICMP 功能

IP 协议仅负责数据交付,不关心数据是否到达、为何失败。ICMP 的核心作用就是为 IP 协议补充反馈机制,主要体现在两点:

  1. 差错报告:当 IP 数据报因网络不可达、TTL 耗尽、端口未开放等原因无法交付时,路由器 / 目标主机会通过 ICMP 向源主机返回差错报文(如 "目的不可达""超时"),避免数据 "石沉大海"。
  2. 网络查询 :支持主机间主动交互网络状态,最典型的就是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)",交换源/目的IP
            ICMPH_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);
}