背景
IP协议并不完美,在传递数据时提供的是一种无连接的不可靠数据报交付,协议本身不提供任何错误检验和恢复机制。为弥补这个缺陷,于是引入网际控制报文协议ICMP。
ICMP作用于IP主机与路由器之间,控制的消息很多种:数据错误信息、网络状态信息、主机状况信息等,这些控制消息虽不传输用户信息,但对用户传输信息有重要影响。从协议栈分层上观察,ICMP属于网络层,配合IP完成数据传输,提高数据传输的有效性,虽ICMP有自己报文结构,但是被封装在IP报文中发送。
相关概念
一方面,IP本身不提供差错控制来保证数据传输的有效性。在路由器无法传输一个数据报或数据报生存时间为0时,路由器会直接丢弃这个数据报。很多情况下,源主机希望数据报传输出现异常时能得到相关失败信息,以便重传或其他处理。另一方面,IP缺少一个辅助机制,即主机的管理类和查询机制。在一些情况下,源主机要确定另一主机或路由器是否活跃,对于不活跃的主机没必要再发送数据报,因为发了也没用。还有一些情况,主机管理员希望获取另一个主机或路由的信息,根据这些信息进行自身的配置、数据报发送控制等。
报文类型
ICMP报文分为两大类:差错报告报文和查询报文。
ICMP报文封装如下:
差错报告报文主要向IP数据报源主机返回一个差错报告信息,这个信息产生的原因是路由器或主机不能对当前数据报进行正常的处理,例如数据无法传给上层协议、生存时间TTL为0而被删除等。
查询报文用于一台主机向另一台主机查询特定的信息,通常查询报文都是成对出现的,即源主机发起一个查询报文,目的主机会按照约定的格式向源主机返回一个应答报文。
常见类型如下表:
报文格式
ICMP报文由8字节首部和可变长度的数据部分组成,如下图所示:
不同类型的ICMP报文,首部格式有一定差异,但前4个字节是相同的。代码部分是标识该种类型ICMP报文的具体原因。这里校验和包括了整个ICMP报文。
差错报文
LWIP协议栈可根据数据报的处理异常情况发送目的站不可达报文和数据报超时报文,当收到任何类型差错报文时,直接丢弃报文,不做任何处理。
-
目的站不可达
当路由器不能找到合适路由路径或主机不能向上层协议传递数据时,相应IP数据报被丢弃,然后一个目的站不可达差错报文被返回给源主机。代码段记录了差错原因,常见值如下表:
目的站不可达报文如下:
-
数据报超时
数据报超时用来防止数据报再网络中被循环的路由,IP首部有个生存计数器TTL,数据报每转发一次,TTL减1,当TTL被减为0时,数据报被网络丢弃,同时ICMP超时报文返回给源主机。常见值如下表:
数据报超时报文如下:
-
源站抑制
各个主机间采用稳定的流量控制机制提高数据交互的有效性。当路由器或主机因拥塞而丢弃数据报时,她可向源站发送源站抑制报文。 -
重定向
当一台主句刚运行时,会将所有数据报发送给网络中一个默认路由器。若默认路由器收到数据报后,它就发送一个ICMP重定位报文告诉主机改变它的路由表了。 -
数据报参数错误
数据报再网络传输时,首部出现的任何二义性都会产生严重问题,如果主机或路由发现这种二义性或数据报某个字段丢失,路由会直接丢弃数据报并返回一个数据报参数错误。
查询报文
常见ICMP查询报文有以下几种:回送请求、路由器查询和通告、时间戳请求、信息请求、地址掩码请求。使用较多的时回送请求和时间戳请求,而其他三个使用很少,他们三种主要主机启动时使用,现在DHCP协议已经完全实现这些功能。
LWIP协议栈能接收主机外部的回送请求,并根据报文返回一个ICMP回答报文。回送请求和回送回答时为诊断目的设计的,网络管理员和用户都可以使用这对报文来发现网络问题,两者组合起来可以确定两个网络设备之间彼此是否能够通信。
回送请求和回答报文格式如下:
类型字段的8表示请求报文,0表示回答报文。
代码实现
数据结构
//常见报文类型
#define ICMP_ER 0 /* echo reply */
#define ICMP_DUR 3 /* destination unreachable */
#define ICMP_SQ 4 /* source quench */
#define ICMP_RD 5 /* redirect */
#define ICMP_ECHO 8 /* echo */
#define ICMP_TE 11 /* time exceeded */
#define ICMP_PP 12 /* parameter problem */
#define ICMP_TS 13 /* timestamp */
#define ICMP_TSR 14 /* timestamp reply */
#define ICMP_IRQ 15 /* information request */
#define ICMP_IR 16 /* information reply */
//目的站不可达报文中代码字段常用取值
enum icmp_dur_type {
ICMP_DUR_NET = 0, /* net unreachable */
ICMP_DUR_HOST = 1, /* host unreachable */
ICMP_DUR_PROTO = 2, /* protocol unreachable */
ICMP_DUR_PORT = 3, /* port unreachable */
ICMP_DUR_FRAG = 4, /* fragmentation needed and DF set */
ICMP_DUR_SR = 5 /* source route failed */
};
//数据报超时报文中的代码字段取值
enum icmp_te_type {
ICMP_TE_TTL = 0, /* time to live exceeded in transit */
ICMP_TE_FRAG = 1 /* fragment reassembly time exceeded */
};
//回送请求报文首部结构
struct icmp_echo_hdr {
PACK_STRUCT_FIELD(u8_t type);
PACK_STRUCT_FIELD(u8_t code);
PACK_STRUCT_FIELD(u16_t chksum);
PACK_STRUCT_FIELD(u16_t id);
PACK_STRUCT_FIELD(u16_t seqno);
} PACK_STRUCT_STRUCT;
//用于读取首部字段
#define ICMPH_TYPE(hdr) ((hdr)->type)
#define ICMPH_CODE(hdr) ((hdr)->code)
//用于首部字段写入相关值
/** Combines type and code to an u16_t */
#define ICMPH_TYPE_SET(hdr, t) ((hdr)->type = (t))
#define ICMPH_CODE_SET(hdr, c) ((hdr)->code = (c))
源码中只定义了目的不可达报文、数据超时报文、回送请求报文相关数据结构。
发送差错报文代码实现
数据报不能递交任何一个上层协议时,函数icmp_dest_unreach会被调用,然后发送一个目的不可达差错报文给源主机。
发送超时报文的函数时icmp_time_exceeded,数据报转发和分片重装过程中,都可能调用这个函数,原因可能是:1.TTL为0;2.分片重装时间超时。
void icmp_dest_unreach(struct pbuf *p, enum icmp_dur_type t)
{
icmp_send_response(p, ICMP_DUR, t);
}
void icmp_time_exceeded(struct pbuf *p, enum icmp_te_type t)
{
icmp_send_response(p, ICMP_TE, t);
}
static void icmp_send_response(struct pbuf *p, u8_t type, u8_t code)
{
struct pbuf *q;
struct ip_hdr *iphdr;
/* we can use the echo header here */
struct icmp_echo_hdr *icmphdr;
/* ICMP header + IP header + 8 bytes of data */
q = pbuf_alloc(PBUF_IP, sizeof(struct icmp_echo_hdr) + IP_HLEN + ICMP_DEST_UNREACH_DATASIZE,
PBUF_RAM);
if (q == NULL) {
return;
}
iphdr = (struct ip_hdr *)p->payload;
icmphdr = (struct icmp_echo_hdr *)q->payload;
icmphdr->type = type;
icmphdr->code = code;
icmphdr->id = 0;
icmphdr->seqno = 0;
/* copy fields from original packet */
SMEMCPY((u8_t *)q->payload + sizeof(struct icmp_echo_hdr), (u8_t *)p->payload,
IP_HLEN + ICMP_DEST_UNREACH_DATASIZE);
/* calculate checksum */
icmphdr->chksum = 0;
icmphdr->chksum = inet_chksum(icmphdr, q->len);
ip_output(q, NULL, &iphdr_src, ICMP_TTL, 0, IP_PROTO_ICMP);
pbuf_free(q);
}
代码逻辑很简单,就是给报文申请空间,然后根据类型和代码字段填写数据,然后计算校验和,最后发送。
回送报文请求
目前LWIP只支持回送请求报文处理,其他类型的报文会直接丢弃,不做任何响应,这对于嵌入式产品已经够用了。回送请求中,icmp_input需要生成一个回送回答报文并返回给源主机。
void icmp_input(struct pbuf *p, struct netif *inp)
{
u8_t type;
struct icmp_echo_hdr *iecho;
struct ip_hdr *iphdr;
s16_t hlen;
iphdr = (struct ip_hdr *)p->payload;
hlen = IPH_HL(iphdr) * 4;
if (pbuf_header(p, -hlen) || (p->tot_len < sizeof(u16_t)*2)) {
goto lenerr;
}
type = *((u8_t *)p->payload);
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). */
break;
case ICMP_ECHO:
{
int accepted = 1;
/* multicast destination address? */
if (ip_addr_ismulticast(¤t_iphdr_dest)) {
accepted = 0;
}
/* broadcast destination address? */
if (ip_addr_isbroadcast(¤t_iphdr_dest, inp)) {
accepted = 0;
}
/* broadcast or multicast destination address not acceptd? */
if (!accepted) {
pbuf_free(p);
return;
}
}
if (p->tot_len < sizeof(struct icmp_echo_hdr)) {
goto lenerr;
}
if (inet_chksum_pbuf(p) != 0) {
pbuf_free(p);
return;
}
/* At this point, all checks are OK. */
/* We generate an answer by switching the dest and src ip addresses,
* setting the icmp type to ECHO_RESPONSE and updating the checksum. */
iecho = (struct icmp_echo_hdr *)p->payload;
ip_addr_copy(iphdr->src, *ip_current_dest_addr());
ip_addr_copy(iphdr->dest, *ip_current_src_addr());
ICMPH_TYPE_SET(iecho, ICMP_ER);
/* adjust the checksum */
if (iecho->chksum >= PP_HTONS(0xffffU - (ICMP_ECHO << 8))) {
iecho->chksum += PP_HTONS(ICMP_ECHO << 8) + 1;
} else {
iecho->chksum += PP_HTONS(ICMP_ECHO << 8);
}
/* Set the correct TTL and recalculate the header checksum. */
IPH_TTL_SET(iphdr, ICMP_TTL);
IPH_CHKSUM_SET(iphdr, 0);
IPH_CHKSUM_SET(iphdr, inet_chksum(iphdr, IP_HLEN));
if(pbuf_header(p, hlen)) {
} else {
err_t ret;
/* send an ICMP packet, src addr is the dest addr of the curren packet */
ret = ip_output_if(p, ip_current_dest_addr(), IP_HDRINCL,
ICMP_TTL, 0, IP_PROTO_ICMP, inp);
}
break;
default:
pbuf_free(p);
return;
lenerr:
pbuf_free(p);
return;
}
上面源码首先一上来,就开始做一系列的校验工作,判断ICMP头部长度是否小于4个字节,若是就丢弃,否则继续根据判断头部类型,分情况干活;若是回送请求,则检查报文目的地址是否合法,如果是多播地址和广播地址,不回应;再检查报文长度是否合法;然后判断校验和是否正确。
校验完后,直接调整回送请求报文的相关字段。生成回送回答报文:交换数据报中的源IP和目的IP,填写报文类型字段,重新计算ICMP报文校验和。