ICMP协议是一个网络层协议。 一个新搭建好的网络,往往需要先进行一个简单的测试,来验证网络是否畅通;但是IP协议并不提供可靠传输。如果丢包了,IP协议并不能通知传输层是否丢包以及丢包的原因。因此我们需要ICMP协议来完成这样的功能。
总结来说:为了更有效地转发IP数据报和提高交付成功机会
对于ICMP协议中的差错报告报文,在lwIP中实现的是目的不可达以及超时的报文;对于超时报文又分为两种,一个是生存时间TTL(在IP首部中),另一个是分片传输中,接收到一个分片后的超时等待时间超时;
ICMP协议中的询问报文,lwIP实现的则是回送请求/应答报文。
无论是差错还是询问报文,前4个字节是一样的:第一个是类型,第二个是代码,例如超时就是0/1,0代表生存时间为0、1则是超时等待时间为0;后两个是校验和;
之后的4个字节则是取决于ICMP报文的类型;整个ICMP的数据部分,长度取决于类型;
整个ICMP报文是在网络层,可以说IP数据包包含了IP首部以及ICMP报文。
用于检测IP数据报在传输过程的异常信息(目的不可达、源站抑制、重定向、超时、参数错误)
ICMP类型为3,则代表了是目的不可达;lwIP实现了代码值2、3、4的差错;ICMP类型为11则代表了是超时错误;代码值0代表传输期间生存时间为0,1代表数据报组装期间生存时间为0.
用于诊断两个网络设备之间彼此是否能够通信
lwIP只处理ICMP类型0/8,代表了回显请求/应答;目的主机收到 ICMP 回送请求报文后立刻回送应答报文,若源主机能收到 ICMP 回送应答报文,则说明到达该主机的网络正常(PING)。
以上结构体位于icmp.h中;包括有ICMP的类型、代码、校验和、标志符以及序号五个变量。
差错报文中,前4个字节是类型、代码和校验后;后4个字节全为0;然后传输的数据就是因其差错的IP首部以及他的pbuf的前8个字节的数据;
查询报文的前4个字节与差错报文一样;后4个字节中,2格式标识符,2个事序号;数据部分则是请求报文发送和应答报文重复(就是类型为8,就是回送请求,直接把类型改为0,变成回送应答)。
lwIP只实现目的不可达、超时差错报文,它们分别为icmp_dest_unreach和icmp_time_exceeded函数;
这两种差错报文都是调用icmp_send_response发送;其源码和注释如下:
static void
icmp_send_response(struct pbuf *p, u8_t type, u8_t code)
{
struct pbuf *q;
struct ip_hdr *iphdr;
struct icmp_echo_hdr *icmphdr;
ip4_addr_t iphdr_src;
struct netif *netif;
u16_t response_pkt_len;
MIB2_STATS_INC(mib2.icmpoutmsgs);
/* response_pkt_len = 20 + 8 = 28*/
response_pkt_len = IP_HLEN + ICMP_DEST_UNREACH_DATASIZE;
if (p->tot_len < response_pkt_len) {
response_pkt_len = p->tot_len;
}
/* 申请pbuf内存 */
q = pbuf_alloc(PBUF_IP, sizeof(struct icmp_echo_hdr) + response_pkt_len, PBUF_RAM);
if (q == NULL) {
MIB2_STATS_INC(mib2.icmpouterrors);
return;
}
/* 获取对方主机数据报的IP首部 */
iphdr = (struct ip_hdr *)p->payload;
/* 在q->payload地址附加icmp_echo_hdr结构体 */
icmphdr = (struct icmp_echo_hdr *)q->payload;
/* 设置ICMP首部信息 */
icmphdr->type = type;
icmphdr->code = code;
icmphdr->id = 0;
icmphdr->seqno = 0;
/* 把对方主机的数据复制IP首部和前8字节数据到新申请的pbuf当中 */
SMEMCPY((u8_t *)q->payload + sizeof(struct icmp_echo_hdr), (u8_t *)p->payload,
response_pkt_len);
/* 复制目标地址 */
ip4_addr_copy(iphdr_src, iphdr->src);
/* 判断是否同一网段 */
netif = ip4_route(&iphdr_src);
if (netif != NULL) {
icmphdr->chksum = 0;
ICMP_STATS_INC(icmp.xmit);
/* 发送ICMP差错报文 */
ip4_output_if(q, NULL, &iphdr_src, ICMP_TTL, 0, IP_PROTO_ICMP, netif);
}
pbuf_free(q);
}
以上源码的逻辑,就是根据当前的type和code判断处理方式,判断得到是差错报文,就把被丢弃数据包的pbuf中的IP首部和前8个字节数据拷贝到差错报文中(同样也是一个pbuf)。
请求报文发送,应答报文重复。简单来讲,应答包是在请求包的基础上修改得来;查询报文的源码和注释如下:
void
icmp_input(struct pbuf *p, struct netif *inp)
{
u8_t type;
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);
if (hlen < IP_HLEN) {
goto lenerr;
}
if (p->len < sizeof(u16_t) * 2) {
goto lenerr;
}
/* 获取 ICMP 的类型字段 */
type = *((u8_t *)p->payload);
switch (type) {
case ICMP_ER:/* 回送应答 */
MIB2_STATS_INC(mib2.icmpinechoreps);
break;
case ICMP_ECHO:/* 回送请求 */
MIB2_STATS_INC(mib2.icmpinechos);
/* 获取IP地址 */
src = ip4_current_dest_addr();
/* 判断是否为多播地址 */
if (ip4_addr_ismulticast(ip4_current_dest_addr())) {
goto icmperr;
}
/* 判断是否为广播地址 */
if (ip4_addr_isbroadcast(ip4_current_dest_addr(), ip_current_netif())) {
goto icmperr;
}
/* 判断这个pbuf的总长度是否小于icmp首部 */
if (p->tot_len < sizeof(struct icmp_echo_hdr)) {
goto lenerr;
}
#if LWIP_ICMP_ECHO_CHECK_INPUT_PBUF_LEN
/* 偏移paylosd指针 = IP首部(20) + 14 + 0 = 34 */
if (pbuf_add_header(p, hlen + PBUF_LINK_HLEN + PBUF_LINK_ENCAPSULATION_HLEN)) {
struct pbuf *r;
/* 申请大小 = p->tot_len + hlen */
u16_t alloc_len = (u16_t)(p->tot_len + hlen);
if (alloc_len < p->tot_len) {
goto icmperr;
}
/* 申请pbuf */
r = pbuf_alloc(PBUF_LINK, alloc_len, PBUF_RAM);
if (r == NULL) {
goto icmperr;
}
if (r->len < hlen + sizeof(struct icmp_echo_hdr)) {
pbuf_free(r);
goto icmperr;
}
/* 拷贝IP首部到新申请的pbuf当中 */
MEMCPY(r->payload, iphdr_in, hlen);
/* 偏移paylosd指针 */
if (pbuf_remove_header(r, hlen)) {
pbuf_free(r);
goto icmperr;
}
/* 复制没有ip首部的其余数据包 */
if (pbuf_copy(r, p) != ERR_OK) {
pbuf_free(r);
goto icmperr;
}
/* 释放pbuf(p) */
pbuf_free(p);
/* p -> r */
p = r;
} else {
/* 偏移paylosd指针 IP首部(20) + 14 + 0 = 34 */
if (pbuf_remove_header(p, hlen + PBUF_LINK_HLEN + PBUF_LINK_ENCAPSULATION_HLEN)) {
goto icmperr;
}
}
#endif
/* p->payload地址上附加icmp_echo_hdr结构体 */
iecho = (struct icmp_echo_hdr *)p->payload;
/* 添加IP首部 */
if (pbuf_add_header(p, hlen)) {
} else {
err_t ret;
/* 设置IP首部信息 */
struct ip_hdr *iphdr = (struct ip_hdr *)p->payload;
ip4_addr_copy(iphdr->src, *src);
ip4_addr_copy(iphdr->dest, *ip4_current_src_addr());
/* 设置ICMP首部信息 */
ICMPH_TYPE_SET(iecho, ICMP_ER);
iecho->chksum = 0;
/* 设置生存时间 */
IPH_TTL_SET(iphdr, ICMP_TTL);
IPH_CHKSUM_SET(iphdr, 0);
ICMP_STATS_INC(icmp.xmit);
MIB2_STATS_INC(mib2.icmpoutmsgs);
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) {
}
}
break;
default: /* lwIP不作处理 */
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);
}
}
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
}
总结来说,ICMP的回送请求,把ICMP结构体的type从8改成0,然后把pbuf的payload上移20个字节,添加IP首部,就变成了回送应答包。
这一篇的源码还是比较简单易懂的,没有太多要F12跳转的内容,总的原理也比较清晰。