前言
参考:RFC 792
原文:李柱明博客:https://www.cnblogs.com/lizhuming/p/16861945.html
10.1 ICMP简介
IP 协议是一种不可靠、无连接的协议,只在各个主机间交付数据,但是对于数据的到达与否,IP 协议并不关心。
但是有些源主机希望能得到当数据没能发送到目标的时候有个回应,不然目标主机都不知道发的数据到了哪里。
所以 IP 协议并不完美,这就出现的 ICMP。
ICMP 是“Internet Control Message Protocol”(网际报文控制协议)的缩写。
ICMP协议用于在IP主机、路由器之间传递控制消息,包括数据包错误信息、网络状况信息和主机状况信息等。
ICMP属于网络层。
虽然从报文上看ICMP报文是基于IP报文的,但是ICMP从协议和功能上看是属于网络层的,因为ICMP报文的目的不是目的主机的某个应用程序,不为应用程序提供传输服务,而是是IP协议的辅助协议。
所以其报文主要分两大类:ICMP差错报告报文和ICMP查询报文。
10.2 ICMP报文
10.2.1 ICMP报文格式
IPCM报文封装在IP数据区中,如图:
其ICMP报文格式如下图:
ICMP报文由8字节首部和可变长度的数据部分组成。
不同类型的ICMP报文,ICMP 报文首部的格式也会有点差异,但是首部的前 4 个字节都是通用的:
- 类型(type)字段:占用1字节。表示产生这种类型 ICMP 报文的原因。
- 代码(code)字段:占用1字节。进一步描述了产生这种类型 ICMP 报文的具体原因。
- 校验和字段:占用2字节。记录包括 ICMP 报文数据部分在内的整个 ICMP 数据报的校验和。其计算方法和IP首部校验和一样。
参考RFC 792
:
- 根据各种ICMP报文类型来定义剩余非固定的报文格式。
- 也可以参考该协议找到各个字段的意义。
10.2.2 ICMP报文类型
ICMP 报文有两大类型:ICMP差错报告报文和ICMP查询报文。
ICMP差错报告报文主要是用来向 IP 数据报源主机返回一个差错报告信息,而这个差错报告信息产生的原因是路由器或者主机不能对当前数据报进行正常的处理。
简单来说就是源主机发送的数据报没法到目标主机中,或者到达了目标主机而无法递交给上层协议。便会产生ICMP差错报告报文返回给源主机。
ICMP查询报文用于一台主机向另一台主机发起一个请求,如果目标主机收到这个查询的请求后,就会按照查询报文的格式向源主机做出应答。如ping。
结合ICMP报文类型字段的表格:
ICMP报文类型 | 具体类型 | 描述 |
---|---|---|
差错报告报文 |
3 | 目的不可达 |
4 | 源站抑制 | |
5 | 重定向 | |
11 | 超时 | |
12 | 参数错误报文 | |
查询报文 |
0或8 | 回显请求或回显应答 |
9或10 | 路由器询问或通告 | |
13或14 | 时间戳请求或应答 | |
15或16 | 信息请求或信息应答 | |
17或18 | 掩码请求或应答 |
10.2.3 ICMP报文固定首部字段意义
参考RFC 792
。
类型 | 代码 | 描述 | 查询 | 差错 |
---|---|---|---|---|
0 | 0 | 回显应答(如ping应答) | √ | |
3 |
目的不可达: | |||
0 | 网络不可达 | √ | ||
1 | 主机不可达 | √ | ||
2 | 协议不可达 | √ | ||
3 | 端口不可达 | √ | ||
4 | 需要进行分片,但设置不了分片比特 | √ | ||
5 | 源站选路失败 | √ | ||
6 | 目的网络不认识 | √ | ||
7 | 目的主机不认识 | √ | ||
8 | 源主机被隔离(作废不用) | √ | ||
9 | 目的网络被强制禁止 | √ | ||
10 | 目的主机被强制禁止 | √ | ||
11 | 由于服务类型TOS,网络不可达 | √ | ||
12 | 由于服务类型TOS,主机不可达 | √ | ||
13 | 由于过滤,通信被强制禁止 | √ | ||
14 | 主机越权 | √ | ||
15 | 优先权中止生效 | √ | ||
4 | 0 | 源站抑制 | √ | |
5 |
重定向: | |||
0 | 对网络重定向 | √ | ||
1 | 对主机重定向 | √ | ||
2 | 对服务类型和网络重定向 | √ | ||
3 | 对服务类型和主机重定向 | √ | ||
8 | 0 | 请求回显(如ping请求) | √ | |
9 | 0 | 路由器通告 | √ | |
10 | 0 | 路由器请求 | √ | |
11 |
超时: | |||
0 | 传输期间TTL为0 | √ | ||
1 | 分片数据报重装超时 | √ | ||
12 |
参数问题: | |||
0 | 坏IP首部 | √ | ||
1 | 缺少必须的选项 | √ | ||
13 | 0 | 时间戳请求 | √ | |
14 | 0 | 时间戳应答 | √ | |
15 | 0 | 信息请求 | √ | |
16 | 0 | 信息应答 | √ | |
17 | 0 | 地址掩码请求 | √ | |
18 | 0 | 地址掩码应答 | √ |
10.3 ICMP差错报告报文
注意,有几种数据报出现错误是不会产生对应的差错报文的:
- 携带ICMP差错报文的数据报。
- 不是某数据报第一个分片的分片数据报。
- 具有多播地址的数据报。
- 具有其它特殊目的地址的数据报。(如环回、多播、广播地址等等)
10.3.1 目的不可达
类型 | 代码 | 描述 | 查询 | 差错 |
---|---|---|---|---|
3 |
目的不可达: | |||
0 | 网络不可达 | √ | ||
1 | 主机不可达 | √ | ||
2 | 协议不可达 | √ | ||
3 | 端口不可达 | √ | ||
4 | 需要进行分片,但设置不了分片比特 | √ | ||
5 | 源站选路失败 | √ | ||
6 | 目的网络不认识 | √ | ||
7 | 目的主机不认识 | √ | ||
8 | 源主机被隔离(作废不用) | √ | ||
9 | 目的网络被强制禁止 | √ | ||
10 | 目的主机被强制禁止 | √ | ||
11 | 由于服务类型TOS,网络不可达 | √ | ||
12 | 由于服务类型TOS,主机不可达 | √ | ||
13 | 由于过滤,通信被强制禁止 | √ | ||
14 | 主机越权 | √ | ||
15 | 优先权中止生效 | √ |
当路由器不能给数据报找到合适的路由路径,或者主机不能将数据报递交给上层协议时,相应的IP数据报就会被丢弃,然后返回给源主机一个目的站不可达的ICMP差错控制报文。
ICMP 目的不可达报文首部剩下的 4 字节全部未用:目的不可达的ICMP报文格式:
10.3.2 源站抑制
类型 | 代码 | 描述 | 查询 | 差错 |
---|---|---|---|---|
4 | 0 | 源站抑制 | √ |
ICMP源站抑制报文格式参考 目的不可达的ICMP报文格式。
ICMP源站抑制报文的目的就是告诉源主机,数据降速,这边快处理不过来了。
源站抑制详细描述:
如果网关没有必要的缓冲区空间,则可以丢弃Internet数据报,以便将数据报排队输出到目的地网络路由上的下一个网络。
如果网关丢弃一个数据报,它可以向该数据报的Internet源主机发送一个源站抑制消息。
如果数据报到达得太快而无法处理,目标主机也可以发送源抑制消息。
源站抑制消息是向主机发出的请求,要求它降低向Internet目的地发送通信的速度。
网关可以为它丢弃的每一个消息发送一个源站抑制消息。
在接收到源站抑制消息时,源主机应该降低它向指定目的地发送通信的速率,直到它没有收到源站抑制消息为止。
然后,源主机可以逐渐增加它向目标发送流量的速度,直到它再次接收到源站抑制消息为止。
网关或主机可以在接近其容量限制时发送源站抑制消息,而不是等待直到容量超过再发。
因为这样可以触发源站抑制的数据报可以被交付。
代码0可以从网关或主机接收。
10.3.3 重定向(改变路由)
类型 | 代码 | 描述 | 查询 | 差错 |
---|---|---|---|---|
5 |
重定向: | |||
0 | 对网络重定向 | √ | ||
1 | 对主机重定向 | √ | ||
2 | 对服务类型和网络重定向 | √ | ||
3 | 对服务类型和主机重定向 | √ |
数据包格式:
在以下情况下,网关会向主机发送重定向消息。
网关Gl从网关所连接的网络上的主机接收互联网数据报,然后检查自己的路由表,得到数据报internet目的网络X的路由上的下一个网关G2的地址。
如果G2与数据报internet源地址标识的主机在同一网络上,则向该主机发送重定向消息。
重定向消息建议主机将其网络X的流量直接发送到网关G2,因为这是到达目的地的较短路径。
网关将原始数据报的数据转发到其internet目的地。
对于带有IP源路由选项和目的地址字段中网关地址的数据报,即使到达最终目的地的路由比源路由中的下一个地址有更好的路由,也不发送重定向消息。
可以从网关接收编码0、1、2和3。
10.3.4 超时
类型 | 代码 | 描述 | 查询 | 差错 |
---|---|---|---|---|
11 |
超时: | |||
0 | 传输期间TTL为0 | √ | ||
1 | 分片数据报重装超时 | √ |
10.3.5 参数错误
类型 | 代码 | 描述 | 查询 | 差错 |
---|---|---|---|---|
12 |
参数问题: | |||
0 | IP首部异常 | √ | ||
1 | 缺少必须的选项 | √ |
IP 数据报在网络中传输的时候,都是根据其首部进行识别的,如果首部出现错误,那么就会产生严重的问题,因此如果 IP 数据报首部出现错误就会丢弃数据报,并且向源主机返回一个 ICMP参数错误报文。
10.4 ICMP查询报文
常见的ICMP查询报文:
- 回显请求或回答。
- 路由器查询和通告。
- 时间戳请求或回答。
- 信息请求或回答。
- 地址掩码请求或回答。
10.4.1 回显
这是当前lwip唯一实现的ICMP查询报文。
类型 | 代码 | 描述 | 查询 | 差错 |
---|---|---|---|---|
0 | 0 | 回显应答(如ping应答) | √ | |
8 | 0 | 请求回显(如ping请求) | √ |
报文格式:
标识符和序列号没有具体范围要求,符合字节限制即可。发送方可以*使用这两个字段。
如该标识符可能被用作TCP或UDP中的端口来标识会话,而序列号可能会在每次发送回显请求时递增。
数据字段可选,但是在请求回显消息中接收到的数据必须在回显应答消息中返回。(即是同一对回显的数据必须一致)
10.5 ICMP数据结构
lwip支持的icmp数据结构在icmp.h
文件中。
10.5.1 ICMP数据报数据结构
数据结构:
/** This is the standard ICMP header only that the u32_t data
* is split to two u16_t like ICMP echo needs it.
* This header is also used for other ICMP types that do not
* use the data part.
*/
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); // 标识符字段
PACK_STRUCT_FIELD(u16_t seqno); // 序号字段
} PACK_STRUCT_STRUCT;
PACK_STRUCT_END
操作:
#define ICMPH_TYPE(hdr) ((hdr)->type) // 读取类型字段
#define ICMPH_CODE(hdr) ((hdr)->code) // 读取代码字段
#define ICMPH_TYPE_SET(hdr, t) ((hdr)->type = (t)) // 设置类型字段
#define ICMPH_CODE_SET(hdr, c) ((hdr)->code = (c)) // 设置代码字段
10.5.2 ICMP类型字段
#define ICMP_ER 0 /* 回显回答 */
#define ICMP_DUR 3 /* 目的不可达 */
#define ICMP_SQ 4 /* 源站抑制 */
#define ICMP_RD 5 /* 重定向 */
#define ICMP_ECHO 8 /* 回显请求 */
#define ICMP_TE 11 /* 数据报超时 */
#define ICMP_PP 12 /* 数据报参数错误 */
#define ICMP_TS 13 /* 时间戳请求 */
#define ICMP_TSR 14 /* 时间戳回答 */
#define ICMP_IRQ 15 /* 信息请求 */
#define ICMP_IR 16 /* 信息回答 */
#define ICMP_AM 17 /* 地址掩码请求 */
#define ICMP_AMR 18 /* 地址掩码回答 */
10.5.3 目的不可达代码字段
/** ICMP目的不可达代码字段 */
enum icmp_dur_type {
/** 网络不可达 */
ICMP_DUR_NET = 0,
/** 主机不可达 */
ICMP_DUR_HOST = 1,
/** 协议不可达 */
ICMP_DUR_PROTO = 2,
/** 端口不可达 */
ICMP_DUR_PORT = 3,
/** 需要分片但不分片置位 */
ICMP_DUR_FRAG = 4,
/** 源路由失败 */
ICMP_DUR_SR = 5
};
10.5.4 超时代码字段
/** ICMP超时代码字段 */
enum icmp_te_type {
/** 生存时间超时值TTL为0 */
ICMP_TE_TTL = 0,
/** 分片数据报重装超时 */
ICMP_TE_FRAG = 1
};
10.6 发送ICMP差错报告
10.6.1 发送ICMP差错报文基函数
icmp_send_response()
函数就是发送ICMP差错报文的基函数,可以封装这个函数实现各种ICMP差错报文。
该函数主要逻辑:
-
检查触发ICMP差错报告的IP报文pbuf中的数据size是否符合要求。
- 目的不可达的ICMP差错报文一般需要IP首部+8字节数据。
-
申请ICMP差错报文pbuf资源。
-
组装ICMP数据报。
-
路由匹配网卡。
-
通过IP层输出函数发送ICMP报文。
/**
* Send an icmp packet in response to an incoming packet.
*
* @param p the input packet for which the 'unreachable' should be sent,
* p->payload pointing to the IP header
* @param type Type of the ICMP header
* @param code Code of the ICMP header
*/
static void
icmp_send_response(struct pbuf *p, u8_t type, u8_t code)
{
struct pbuf *q; /* ICMP差错报文pbuf指针 */
struct ip_hdr *iphdr; /* 触发ICMP差错报告的IP头指针 */
struct icmp_echo_hdr *icmphdr; /* ICMP报文首部指针 */
ip4_addr_t iphdr_src; /* 触发ICMP差错报告的IP源地址 */
struct netif *netif; /* 发送ICMP差错报告的网卡 */
u16_t response_pkt_len; /* ICMP数据区长度 */
/* 增加试图发送的消息数 */
MIB2_STATS_INC(mib2.icmpoutmsgs);
/* IP头+8 */
response_pkt_len = IP_HLEN + ICMP_DEST_UNREACH_DATASIZE;
if (p->tot_len < response_pkt_len) { /* 如果触发ICMP差错报告的IP报文pbuf数据区不够8,就全部上传即可。 */
response_pkt_len = p->tot_len;
}
/* 申请ICMP差错报告的pbuf资源,长度为ICMP差错报告首部+数据区*/
q = pbuf_alloc(PBUF_IP, sizeof(struct icmp_echo_hdr) + response_pkt_len, PBUF_RAM);
if (q == NULL) {
LWIP_DEBUGF(ICMP_DEBUG, ("icmp_time_exceeded: failed to allocate pbuf for ICMP packet.\n"));
MIB2_STATS_INC(mib2.icmpouterrors);
return;
}
LWIP_ASSERT("check that first pbuf can hold icmp message",
(q->len >= (sizeof(struct icmp_echo_hdr) + response_pkt_len)));
iphdr = (struct ip_hdr *)p->payload; /* 提取IP报文首部 */
LWIP_DEBUGF(ICMP_DEBUG, ("icmp_time_exceeded from "));
ip4_addr_debug_print_val(ICMP_DEBUG, iphdr->src);
LWIP_DEBUGF(ICMP_DEBUG, (" to "));
ip4_addr_debug_print_val(ICMP_DEBUG, iphdr->dest);
LWIP_DEBUGF(ICMP_DEBUG, ("\n"));
icmphdr = (struct icmp_echo_hdr *)q->payload;
icmphdr->type = type; /* 设置ICMP类型字段 */
icmphdr->code = code; /* 设置ICMP代码字段 */
icmphdr->id = 0; /* 设置ICMP标识字段 */
icmphdr->seqno = 0; /* 设置ICMP序号字段 */
/* 拷贝触发ICMP差错的IP报文首部+(<=8)字节的原数据到ICMP差错报告数据区 */
SMEMCPY((u8_t *)q->payload + sizeof(struct icmp_echo_hdr), (u8_t *)p->payload,
response_pkt_len);
ip4_addr_copy(iphdr_src, iphdr->src); /* 提取IP源地址 */
#ifdef LWIP_HOOK_IP4_ROUTE_SRC
{
ip4_addr_t iphdr_dst;
ip4_addr_copy(iphdr_dst, iphdr->dest); /* 提取IP目的地址 */
netif = ip4_route_src(&iphdr_dst, &iphdr_src); /* 路由匹配网卡 */
}
#else
netif = ip4_route(&iphdr_src); /* 路由匹配网卡 */
#endif
if (netif != NULL) { /* 匹配网卡成功 */
icmphdr->chksum = 0; /* ICMP校验和字段 */
#if CHECKSUM_GEN_ICMP
IF__NETIF_CHECKSUM_ENABLED(netif, NETIF_CHECKSUM_GEN_ICMP) {
icmphdr->chksum = inet_chksum(icmphdr, q->len); /* 计算ICMP校验和 */
}
#endif
ICMP_STATS_INC(icmp.xmit);
/* 通过指定网卡把ICMP报文转交到IP层发送出去 */
ip4_output_if(q, NULL, &iphdr_src, ICMP_TTL, 0, IP_PROTO_ICMP, netif);
}
/* 释放ICMP报文资源 */
pbuf_free(q);
}
10.6.2 icmp_dest_unreach()目的不可达差错报告
icmp_dest_unreach()
:
-
struct pbuf *p
:目的不可达的pbuf包。 -
enum icmp_dur_type t
:目的不可达的原因。
/**
* Send an icmp 'destination unreachable' packet, called from ip_input() if
* the transport layer protocol is unknown and from udp_input() if the local
* port is not bound.
*
* @param p the input packet for which the 'unreachable' should be sent,
* p->payload pointing to the IP header
* @param t type of the 'unreachable' packet
*/
void
icmp_dest_unreach(struct pbuf *p, enum icmp_dur_type t)
{
MIB2_STATS_INC(mib2.icmpoutdestunreachs);
icmp_send_response(p, ICMP_DUR, t);
}
10.6.3 icmp_time_exceeded()超时差错报告
icmp_time_exceeded()
:
-
struct pbuf *p
:目的不可达的pbuf包。 -
enum icmp_te_type t
:超时原因。
/**
* Send a 'time exceeded' packet, called from ip_forward() if TTL is 0.
*
* @param p the input packet for which the 'time exceeded' should be sent,
* p->payload pointing to the IP header
* @param t type of the 'time exceeded' packet
*/
void
icmp_time_exceeded(struct pbuf *p, enum icmp_te_type t)
{
MIB2_STATS_INC(mib2.icmpouttimeexcds);
icmp_send_response(p, ICMP_TE, t);
}
10.7 接收ICMP报文处理
IP层接收到ICMP报文,会调用icmp_input函数处理。
目前LWIP只支持ICMP回显请求的报文处理,其它ICMP报文直接丢弃。
而对于ICMP回显请求,LWIP组装回显回答报文即可。
icmp_input()
:
-
获取IP报文首部。判断是否有效:首部长度
-
判断pbuf是否有效:ICMP报文长度不能少于ICMP首部长度4Byte。
-
提取ICMP报文类型字段:
-
只处理回显请求报文,组装回显回答ICMP差错控制报文,用于ping。
- 开启了PING功能才支持多播和广播地址的回显请求。
-
其它ICMP报文,只记录,不处理。
-
-
LWIP_ICMP_ECHO_CHECK_INPUT_PBUF_LEN
表示是否检查IP报文对应的pbuf包是否有足够的空间扩充首部。- 如果够空间,那就利用这个pbuf的空间资源作为回显回答ICMP报文。
- 如果不够空间,那就重新申请一个pbuf。
-
组装回显回答ICMP报文。
-
调用
ip4_output_if()
通过IP层发送出去。
/**
* Processes ICMP input packets, called from ip_input().
*
* Currently only processes icmp echo requests and sends
* out the echo response.
*
* @param p the icmp echo request packet, p->payload pointing to the icmp header
* @param inp the netif on which this packet was received
*/
void
icmp_input(struct pbuf *p, struct netif *inp)
{
u8_t type;
#ifdef LWIP_DEBUG
u8_t code;
#endif /* LWIP_DEBUG */
struct icmp_echo_hdr *iecho;
const struct ip_hdr *iphdr_in;
u16_t hlen;
const ip4_addr_t *src;
ICMP_STATS_INC(icmp.recv);
MIB2_STATS_INC(mib2.icmpinmsgs);
iphdr_in = ip4_current_header(); /* 获取IP报文首部 */
hlen = IPH_HL_BYTES(iphdr_in); /* 获取IP报文首部长度 */
if (hlen < IP_HLEN) { /* IP报文首部长度异常,丢弃 */
LWIP_DEBUGF(ICMP_DEBUG, ("icmp_input: short IP header (%"S16_F" bytes) received\n", hlen));
goto lenerr;
}
if (p->len < sizeof(u16_t) * 2) { /* ICMP报文长度异常,丢弃 */
LWIP_DEBUGF(ICMP_DEBUG, ("icmp_input: short ICMP (%"U16_F" bytes) received\n", p->tot_len));
goto lenerr;
}
type = *((u8_t *)p->payload); /* 获取ICMP类型字段 */
#ifdef LWIP_DEBUG
code = *(((u8_t *)p->payload) + 1); /* 获取ICMP代码字段 */
/* if debug is enabled but debug statement below is somehow disabled: */
LWIP_UNUSED_ARG(code);
#endif /* LWIP_DEBUG */
switch (type) {
case ICMP_ER:
/* This is OK, echo reply might have been parsed by a raw PCB
(as obviously, an echo request has been sent, too). */
MIB2_STATS_INC(mib2.icmpinechoreps);
break;
case ICMP_ECHO: /* 只处理ICMP差错控制中的回显请求 */
MIB2_STATS_INC(mib2.icmpinechos);
src = ip4_current_dest_addr(); /* 获取IP报文的目的地址作为ICMP回显回答的原IP地址 */
if (ip4_addr_ismulticast(ip4_current_dest_addr())) { /* 回显请求的买的地址为多播地址 */
#if LWIP_MULTICAST_PING
/* 对于组播,使用接收接口的地址作为源地址 */
src = netif_ip4_addr(inp);
#else /* LWIP_MULTICAST_PING */
LWIP_DEBUGF(ICMP_DEBUG, ("icmp_input: Not echoing to multicast pings\n"));
goto icmperr;
#endif /* LWIP_MULTICAST_PING */
}
if (ip4_addr_isbroadcast(ip4_current_dest_addr(), ip_current_netif())) { /* 回显请求的买的地址为多播地址 */
#if LWIP_BROADCAST_PING
/* 对于广播,使用接收接口的地址作为源地址 */
src = netif_ip4_addr(inp);
#else /* LWIP_BROADCAST_PING */
LWIP_DEBUGF(ICMP_DEBUG, ("icmp_input: Not echoing to broadcast pings\n"));
goto icmperr;
#endif /* LWIP_BROADCAST_PING */
}
LWIP_DEBUGF(ICMP_DEBUG, ("icmp_input: ping\n"));
if (p->tot_len < sizeof(struct icmp_echo_hdr)) { /* 长度校验 */
LWIP_DEBUGF(ICMP_DEBUG, ("icmp_input: bad ICMP echo received\n"));
goto lenerr;
}
#if CHECKSUM_CHECK_ICMP
IF__NETIF_CHECKSUM_ENABLED(inp, NETIF_CHECKSUM_CHECK_ICMP) {
if (inet_chksum_pbuf(p) != 0) { /* ICMP校验和校验失败 */
LWIP_DEBUGF(ICMP_DEBUG, ("icmp_input: checksum failed for received ICMP echo\n"));
pbuf_free(p);
ICMP_STATS_INC(icmp.chkerr);
MIB2_STATS_INC(mib2.icmpinerrors);
return;
}
}
#endif
#if LWIP_ICMP_ECHO_CHECK_INPUT_PBUF_LEN /* 支持检查pbuf复用 */
if (pbuf_add_header(p, hlen + PBUF_LINK_HLEN + PBUF_LINK_ENCAPSULATION_HLEN)) { /* IP报文的pbuf长度不够,不能复用 */
/* 长度不够就重新申请pbuf */
struct pbuf *r;
u16_t alloc_len = (u16_t)(p->tot_len + hlen); /* 新pbuf的长度:IP数据区长度+IP首部长度 */
if (alloc_len < p->tot_len) { /* 溢出,丢弃 */
LWIP_DEBUGF(ICMP_DEBUG, ("icmp_input: allocating new pbuf failed (tot_len overflow)\n"));
goto icmperr;
}
/* 为链路头分配空间的新数据包缓冲区 */
r = pbuf_alloc(PBUF_LINK, alloc_len, PBUF_RAM);
if (r == NULL) {
LWIP_DEBUGF(ICMP_DEBUG, ("icmp_input: allocating new pbuf failed\n"));
goto icmperr;
}
if (r->len < hlen + sizeof(struct icmp_echo_hdr)) { /* 检查空间是否足够 */
LWIP_DEBUGF(ICMP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("first pbuf cannot hold the ICMP header"));
pbuf_free(r);
goto icmperr;
}
/* 复制IP头 */
MEMCPY(r->payload, iphdr_in, hlen);
/* 有效负载移到IP数据区 */
if (pbuf_remove_header(r, hlen)) {
LWIP_ASSERT("icmp_input: moving r->payload to icmp header failed\n", 0);
pbuf_free(r);
goto icmperr;
}
/* 拷贝原IP报文数据区到新的pbuf */
if (pbuf_copy(r, p) != ERR_OK) {
LWIP_DEBUGF(ICMP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("icmp_input: copying to new pbuf failed"));
pbuf_free(r);
goto icmperr;
}
/* 释放就pbuf */
pbuf_free(p);
/* 现在,我们有了一个相同的p副本,其中有链路头的空间 */
p = r;
} else { /* 如果空间足够,就把有效负载恢复到IP数据区。因为这里只需要检查并报文pbuf有足够空间即可 */
/* restore p->payload to point to icmp header (cannot fail) */
if (pbuf_remove_header(p, hlen + PBUF_LINK_HLEN + PBUF_LINK_ENCAPSULATION_HLEN)) {
LWIP_ASSERT("icmp_input: restoring original p->payload failed\n", 0);
goto icmperr;
}
}
#endif /* LWIP_ICMP_ECHO_CHECK_INPUT_PBUF_LEN */
/* 到这里,所有的检查都是OK的 */
/* 通过切换dest和src的ip地址,将icmp类型设置为ECHO_RESPONSE并更新校验和来生成回显回答的ICMP差错控制报文 */
iecho = (struct icmp_echo_hdr *)p->payload; /* 获取ICMP报文地址 */
/* 扩展报文首部,成为IP报文 */
if (pbuf_add_header(p, hlen)) {
LWIP_DEBUGF(ICMP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("Can't move over header in packet"));
} else {
err_t ret;
struct ip_hdr *iphdr = (struct ip_hdr *)p->payload;
ip4_addr_copy(iphdr->src, *src); /* 设置IP报文的原IP地址 */
ip4_addr_copy(iphdr->dest, *ip4_current_src_addr()); /* 设置IP报文的目的IP地址 */
ICMPH_TYPE_SET(iecho, ICMP_ER); /* 设置ICMP报文为回显回答类型 */
#if CHECKSUM_GEN_ICMP
IF__NETIF_CHECKSUM_ENABLED(inp, NETIF_CHECKSUM_GEN_ICMP) { /* 设置ICMP校验和 */
/* adjust the checksum */
if (iecho->chksum > PP_HTONS(0xffffU - (ICMP_ECHO << 8))) {
iecho->chksum = (u16_t)(iecho->chksum + PP_HTONS((u16_t)(ICMP_ECHO << 8)) + 1);
} else {
iecho->chksum = (u16_t)(iecho->chksum + PP_HTONS(ICMP_ECHO << 8));
}
}
#if LWIP_CHECKSUM_CTRL_PER_NETIF
else {
iecho->chksum = 0;
}
#endif /* LWIP_CHECKSUM_CTRL_PER_NETIF */
#else /* CHECKSUM_GEN_ICMP */
iecho->chksum = 0;
#endif /* CHECKSUM_GEN_ICMP */
/* S设置正确的TTL并重新计算头部校验和 */
IPH_TTL_SET(iphdr, ICMP_TTL);
IPH_CHKSUM_SET(iphdr, 0);
#if CHECKSUM_GEN_IP
IF__NETIF_CHECKSUM_ENABLED(inp, NETIF_CHECKSUM_GEN_IP) {
IPH_CHKSUM_SET(iphdr, inet_chksum(iphdr, hlen)); /* 设置IP首部校验和 */
}
#endif /* CHECKSUM_GEN_IP */
ICMP_STATS_INC(icmp.xmit);
/* increase number of messages attempted to send */
MIB2_STATS_INC(mib2.icmpoutmsgs);
/* increase number of echo replies attempted to send */
MIB2_STATS_INC(mib2.icmpoutechoreps);
/* 发送ICMP报文 */
ret = ip4_output_if(p, src, LWIP_IP_HDRINCL,
ICMP_TTL, 0, IP_PROTO_ICMP, inp);
if (ret != ERR_OK) {
LWIP_DEBUGF(ICMP_DEBUG, ("icmp_input: ip_output_if returned an error: %s\n", lwip_strerr(ret)));
}
}
break;
default:
if (type == ICMP_DUR) {
MIB2_STATS_INC(mib2.icmpindestunreachs);
} else if (type == ICMP_TE) {
MIB2_STATS_INC(mib2.icmpintimeexcds);
} else if (type == ICMP_PP) {
MIB2_STATS_INC(mib2.icmpinparmprobs);
} else if (type == ICMP_SQ) {
MIB2_STATS_INC(mib2.icmpinsrcquenchs);
} else if (type == ICMP_RD) {
MIB2_STATS_INC(mib2.icmpinredirects);
} else if (type == ICMP_TS) {
MIB2_STATS_INC(mib2.icmpintimestamps);
} else if (type == ICMP_TSR) {
MIB2_STATS_INC(mib2.icmpintimestampreps);
} else if (type == ICMP_AM) {
MIB2_STATS_INC(mib2.icmpinaddrmasks);
} else if (type == ICMP_AMR) {
MIB2_STATS_INC(mib2.icmpinaddrmaskreps);
}
LWIP_DEBUGF(ICMP_DEBUG, ("icmp_input: ICMP type %"S16_F" code %"S16_F" not supported.\n",
(s16_t)type, (s16_t)code));
ICMP_STATS_INC(icmp.proterr);
ICMP_STATS_INC(icmp.drop);
}
pbuf_free(p);
return;
lenerr:
pbuf_free(p);
ICMP_STATS_INC(icmp.lenerr);
MIB2_STATS_INC(mib2.icmpinerrors);
return;
#if LWIP_ICMP_ECHO_CHECK_INPUT_PBUF_LEN || !LWIP_MULTICAST_PING || !LWIP_BROADCAST_PING
icmperr:
pbuf_free(p);
ICMP_STATS_INC(icmp.err);
MIB2_STATS_INC(mib2.icmpinerrors);
return;
#endif /* LWIP_ICMP_ECHO_CHECK_INPUT_PBUF_LEN || !LWIP_MULTICAST_PING || !LWIP_BROADCAST_PING */
}