ICMP(網際控制報文協議),ICMP數據包是封裝在IP數據包中的,由於IP不是爲可靠傳輸服務設計的,ICMP的目的主要是用於在TCP/IP網絡中發送和控制消息。主要應用有Ping、Traceroute和MTU測試。
ICMP報文的種類有三大種類,即ICMP差錯報文、控制報文、請求/應答報文,各大類型報文又分爲多種類型報文。
差錯報文:
(1) 特點:
1.ICMP差錯報文都是有路由器發送到源主機的。
2.ICMP報文只提供IP數據報的差錯報告,並不採取處理措施,差錯處理由應用程序處理。
3.傳輸過程中可能丟失、損壞,甚至被拋棄。
4.ICMP差錯報文是伴隨着拋棄出錯的IP數據報而產生的。
5.爲了防止廣播風暴,以下情況不會產生ICMP差錯報文。
1)ICMP差錯報文 2)目的地址是廣播或多播 3)鏈路層廣播的數據報 4)不是IP分片的第一片 5)源地址是零地址、回送地址、廣播地址或多播地址
(2)信息不可達報文:
1.目的機硬件故障或關機
2.目標地址不存在
3.網關不知道去往目的機的路徑
(3)超時報文:
1.爲了避免無限制的在網中循環,IP協議採用
1)在數據報頭設置TTL域 2)對分片數據報採用定時器技術
2.當報文超時出現時,路由器或目的機立即丟棄該數據報,並向信源機發送超時報文
控制報文:
(1)擁塞控制與源站控制報文:
1.當路由器接收比處理快或者傳入比傳出快時就會產生擁塞
2.路由器通過發送源站抑制報文來抑制主機發送速率
3.源主機一段時間內沒有收到抑制報文,便認爲抑制解除,逐漸恢復原來的數據流量
(2)路由控制與重定向報文:
1.當源主機要向目標主機發送IP數據報時,則把IP數據報發送給默認路器1,再由路由器1經過選路發送給路由器2,路由器2發送給目標主機
2.如果路由器1和路由器2在同一網絡,路由器1發現源主機可直接發送IP數據報給路由器2,就會向源主機發送重定向報文,以後源主機將直接發送IP報文給路由器2
請求/應答報文:
(1)回送請求與應答報文:
1.測試目標主機和路由器是否可以到達
(2)時戳請求與應答報文:
1.同步互聯網中各個主機的時鐘
(3)地址掩碼請求和應答報文
1.用於無盤系統在引導過程中獲取子網掩碼。啓動時廣播地址掩碼請求,路由器收到請求後回送一個包含32位地址掩碼應答報文
報文格式,ICMP報文由8字節首部和可變長數據部分組成。不同類型的ICMP報文,首部格式有一定差異,但是首部前4字節的字段對所有類型通用。
類型:標識了ICMP具體類型
代碼:進一步指出產生該類型ICMP的原因
校驗和:整個ICMP的校驗和
LWIP實現了ICMP的部分功能:目的不可達報文、超時報文、回送查詢報文。
報文格式分別如下所示:
ICMP頭部基本類似,信息不可達和超時報文的頭部,相當於回送查詢報文頭部中標識符和序號設置爲0的情況。因此,LwIP只以回送查詢報文爲基礎提供了一種ICMP頭部結構體
/* 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 /* 信息應答 */
/* 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 /* 源路由失敗 */
};
/* ICMP超時代碼 */
enum icmp_te_type {
ICMP_TE_TTL = 0, /* 生存時間計時器超時 */
ICMP_TE_FRAG = 1 /* 分片重裝超時 */
};
/* ICMP回送查詢報文頭部 */
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;
PACK_STRUCT_END
在前面分析ip_input函數時,當報文類型爲ICMP報文時,調用icmp_input函數處理ICMP報文。當遇到不支持的協議時調用icmp_dest_unreach函數,返回目的不可達報文。
/* IP數據包輸入處理 */
err_t ip_input(struct pbuf *p, struct netif *inp)
{
struct ip_hdr *iphdr;
......
/* IP頭部指針 */
iphdr = p->payload;
......
/* 判斷協議類型 */
switch (IPH_PROTO(iphdr)) {
/* UDP協議 */
case IP_PROTO_UDP:
udp_input(p, inp);
break;
/* TCP協議 */
case IP_PROTO_TCP:
tcp_input(p, inp);
break;
/* ICMP協議 */
case IP_PROTO_ICMP:
icmp_input(p, inp);
break;
/* 不支持的協議 */
default:
/* 目的IP不是廣播地址且不是組播地址 */
if (!ip_addr_isbroadcast(&(iphdr->dest), inp) && !ip_addr_ismulticast(&(iphdr->dest))) {
p->payload = iphdr;
/* 發送ICMP目的不可達報文(協議不可達) */
icmp_dest_unreach(p, ICMP_DUR_PROTO);
}
/* 釋放數據包 */
pbuf_free(p);
}
}
......
return ERR_OK;
}
下面先來看目的不可達報文的實現方式
/* 發送ICMP目的不可達報文 */
void icmp_dest_unreach(struct pbuf *p, enum icmp_dur_type t)
{
icmp_send_response(p, ICMP_DUR, t);
}
/* 發送ICMP響應報文 */
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;
/* 爲ICMP報文申請內存空間 */
q = pbuf_alloc(PBUF_IP, sizeof(struct icmp_echo_hdr) + IP_HLEN + ICMP_DEST_UNREACH_DATASIZE, PBUF_RAM);
if (q == NULL) {
LWIP_DEBUGF(ICMP_DEBUG, ("icmp_time_exceeded: failed to allocate pbuf for ICMP packet.\n"));
return;
}
LWIP_ASSERT("check that first pbuf can hold icmp message", (q->len >= (sizeof(struct icmp_echo_hdr) + IP_HLEN + ICMP_DEST_UNREACH_DATASIZE)));
/* IP頭部指針 */
iphdr = p->payload;
LWIP_DEBUGF(ICMP_DEBUG, ("icmp_time_exceeded from "));
ip_addr_debug_print(ICMP_DEBUG, &(iphdr->src));
LWIP_DEBUGF(ICMP_DEBUG, (" to "));
ip_addr_debug_print(ICMP_DEBUG, &(iphdr->dest));
LWIP_DEBUGF(ICMP_DEBUG, ("\n"));
/* ICMP頭部指針 */
icmphdr = q->payload;
/* ICMP頭部類型 */
icmphdr->type = type;
/* ICMP頭部代碼 */
icmphdr->code = code;
/* ICMP頭部剩餘字節(標識符) */
icmphdr->id = 0;
/* ICMP頭部剩餘字節(序號) */
icmphdr->seqno = 0;
/* 將源數據報中的8字節 */
SMEMCPY((u8_t *)q->payload + sizeof(struct icmp_echo_hdr), (u8_t *)p->payload, IP_HLEN + ICMP_DEST_UNREACH_DATASIZE);
/* ICMP頭部檢驗和 */
icmphdr->chksum = 0;
icmphdr->chksum = inet_chksum(icmphdr, q->len);
ICMP_STATS_INC(icmp.xmit);
snmp_inc_icmpoutmsgs();
snmp_inc_icmpouttimeexcds();
/* 調用IP輸出函數,發送ICMP數據包 */
ip_output(q, NULL, &(iphdr->src), ICMP_TTL, 0, IP_PROTO_ICMP);
/* 釋放數據包 */
pbuf_free(q);
}
接下來再看 icmp_input函數
/* ICMP報文接收處理 */
void icmp_input(struct pbuf *p, struct netif *inp)
{
u8_t type;
struct icmp_echo_hdr *iecho;
struct ip_hdr *iphdr;
struct ip_addr tmpaddr;
s16_t hlen;
/* IP頭部指針 */
iphdr = p->payload;
/* IP頭部長度 */
hlen = IPH_HL(iphdr) * 4;
/* 向後調整剝離IP頭部,保證ICMP頭部不小於4字節 */
if (pbuf_header(p, -hlen) || (p->tot_len < sizeof(u16_t) * 2)) {
goto lenerr;
}
/* ICMP類型 */
type = *((u8_t *)p->payload);
/* 判斷ICMP類型 */
switch (type) {
/* 回送查詢報文 */
case ICMP_ECHO:
{
int accepted = 1;
/* ICMP目的IP爲組播地址 */
if (ip_addr_ismulticast(&iphdr->dest)) {
accepted = 0; //不做處理
}
/* ICMP目的IP爲廣播地址 */
if (ip_addr_isbroadcast(&iphdr->dest, inp)) {
accepted = 0; //不做處理
}
/* 不做處理,直接釋放ICMP數據包 */
if (!accepted) {
pbuf_free(p);
return;
}
}
/* 檢查ICMP數據包長度合不合理 */
if (p->tot_len < sizeof(struct icmp_echo_hdr)) {
goto lenerr;
}
/* 檢查檢驗和是否正確 */
if (inet_chksum_pbuf(p) != 0) {
pbuf_free(p);
return;
}
/* ICMP頭部指針 */
iecho = p->payload;
/* 將IP頭部中源地址和目的地址交換 */
tmpaddr.addr = iphdr->src.addr;
iphdr->src.addr = iphdr->dest.addr;
iphdr->dest.addr = tmpaddr.addr;
/* 設置ICMP報文類型爲回送查詢應答 */
ICMPH_TYPE_SET(iecho, ICMP_ER);
/* 重新設置ICMP頭部校驗和 */
if (iecho->chksum >= htons(0xffff - (ICMP_ECHO << 8))) {
iecho->chksum += htons(ICMP_ECHO << 8) + 1;
} else {
iecho->chksum += htons(ICMP_ECHO << 8);
}
/* 設置IP頭部TTL字段 */
IPH_TTL_SET(iphdr, ICMP_TTL);
/* 計算IP頭部校驗和字段 */
IPH_CHKSUM_SET(iphdr, 0);
IPH_CHKSUM_SET(iphdr, inet_chksum(iphdr, IP_HLEN));
/* 向前調整出IP頭部空間 */
if(pbuf_header(p, hlen)) {
}
else {
/* 通過IP發送函數,發送回送查詢應答報文 */
ip_output_if(p, &(iphdr->src), IP_HDRINCL, ICMP_TTL, 0, IP_PROTO_ICMP, inp);
}
break;
/* 不處理其它類型的ICMP報文 */
default:
break;
}
pbuf_free(p);
return;
lenerr:
pbuf_free(p);
return;
}
在前面分析IP數據報重組的時候, 一旦重組超時協議棧就會刪除重組並向源主機返回ICMP分片重組超時報文
/* 重組IP數據報定時器回調函數(週期1秒) */
void ip_reass_tmr(void)
{
struct ip_reassdata *r, *prev = NULL;
/* 遍歷所有重組IP數據報 */
r = reassdatagrams;
while (r != NULL) {
/* 沒有超時,將生存週期減一 */
if (r->timer > 0) {
r->timer--;
prev = r;
r = r->next;
}
/* 超時 */
else {
struct ip_reassdata *tmp;
tmp = r;
r = r->next;
/* 釋放重組IP數據報的所有分片,並將重組IP數據報從鏈表中移除 */
ip_reass_free_complete_datagram(tmp, prev);
}
}
}
/* 釋放重組IP數據報的所有分片,並將重組IP數據報從鏈表中移除 */
static int ip_reass_free_complete_datagram(struct ip_reassdata *ipr, struct ip_reassdata *prev)
{
......
struct ip_reass_helper *iprh;
/* 向源主機發送ICMP分片重組超時報文 */
iprh = (struct ip_reass_helper *)ipr->p->payload;
if (iprh->start == 0) {
p = ipr->p;
ipr->p = iprh->next_pbuf;
SMEMCPY(p->payload, &ipr->iphdr, IP_HLEN);
icmp_time_exceeded(p, ICMP_TE_FRAG);
pbufs_freed += pbuf_clen(p);
pbuf_free(p);
}
......
}
下面就來看一下ICMP超時報文發送函數
/* 發送ICMP超時報文 */
void icmp_time_exceeded(struct pbuf *p, enum icmp_te_type t)
{
icmp_send_response(p, ICMP_TE, t);
}