TCP/IP實現(七) ICMP協議
一.概述
ICMP協議用於在網路間傳遞請求與應答以及差錯訊息。比如Ping程式便是應用了請求與應答,而Traceroute應用的卻是差錯訊息來實現的。
對於ICMP的輸入往往要經過多次處理,首先由icmp_input函式進行處理,進行首部校驗,並根據ICMP型別做進一步處理,對於ICMP型別超過ICMP處理範圍的(即大於icmp_type>ICMP_MAXTYPE,可能是使用者自己定義的某種型別)ICMP報文,會直接交付給應用程序進行處理。接著可能會被上層傳輸協議處理。最後還肯能通過rip_input函式交付給應用程序(比如Ping,Traceroute)。icmp_input函式處理的總流程如下:
void icmp_input(mbuf m, int hlen) { struct ip* ip = mtod(m, struct ip*); // 指向IP首部 int icmplen = ip->ip_len; // ICMP長度 struct icmp *icp = mtod(m, struct icmp*); // 指向ICMP首部 進行ICMP首部校驗 if(icp->icmp_type > ICMP_MAXTYPE) { goto raw; // 直接交付應用程序處理 } switch(icp->icmp_code){ 處理各種型別的icmp報文 default: break; } raw: rip_input(m);// 將ICMP報文交由應用程序進行處理 return; freeit: mfree(m); }
二.ICMP差錯訊息
在博文《TCP/IP實現(五) IP協議》中的TCP/IP錯誤處理流程一節中提到過ICMP錯誤訊息的處理,對於一些錯誤訊息核心會將其轉化為錯誤碼,並賦值給errno,應用程式可以通過檢視errno來得知錯誤型別。
對以下幾種情況都不會產生ICMP差錯報文:1)對一份ICMP差錯報文永遠不會產生另一份ICMP差錯報文。2)目的地址是廣播地址或多播地址的IP資料報。3)作為鏈路層廣播的資料報。4)不是IP分片的第一個分片。5)源地址不是單個主機的資料報(即源地址不能是零地址,環回地址,廣播地址或多播地址)。對於2,3,5條是為了防止對廣播分組進行響應而造成廣播風暴。
1.ICMP差錯處理
ICMP差錯報文一般是在當某個資料報無法到達目的主機或目標埠時,由目的主機或中間路由器生成ICMP差錯報文,並將其返回至源端。接下來我們來思考一下這樣一個問題:ICMP報文是主機間進行交換的,並未指明埠號和協議,那麼核心是如何知道該將錯誤告知哪個上層呢?要想回答這個問題就必須知道ICMP差錯報文的組成。ICMP差錯報文除了ICMP首部外,必須包含生成該差錯的IP資料報首部(包含所有選項),且必須包含跟在IP首部之後的8個位元組(icmp::icmp_dun::id_ip::di_ip即icmp::icmp_ip指向該資料報首部)。而UDP首部總共只有8個位元組(必包含源埠號),TCP首部中的源埠號也在前8個位元組中,這也就解釋了是如何區分將錯誤資訊告知哪個程序了(或說是上層更準確)。
當源端收到ICMP差錯報文後便會在icmp_input函式中將其轉化為一個協議無關的差錯碼。並呼叫運輸層協議的pr_ctlinput函式,該函式根據icmp差錯報文中攜帶的錯誤IP資料包將到達分組分用到正確的協議。其大致虛擬碼如下:
// switch 內部
case ICMP_UNREACH:
switch(code) {
case ICMP_UNREACH_NET:
case xxx:
...
code += PRC_UNREACH_NET;
break;
case xxx:
...
default:
goto badcode;
}
goto deliver;
case ICMP_TIMXCEED:
if(code > 1)
goto badcode;
code = xxx;
goto deliver;
case xxx:
xxx
goto badcode;
goto deliver;
deliver:
// 檢查ICMP報文長度
// ICMP_ADVLENMIN = 8(ICMP首部長度) + sizeof(struct ip) + 8)
// ICMP_ADVLEN(icp): (8 + ((p)->icmp_ip.ip_hl << 2) + 8) 即 8 + IP首部(含選項) + 8
if(icmplen < ICMP_ADVLENMIN || icmplen < ICMP_ADVLEN(icp)) {
icmpstat.icps_badlen++; // 錯誤長度計數
goto freeit;
}
NTOHS(icp->icmp_ip.ip_len);// 將錯誤IP資料中的長度欄位轉為主機位元組序
pr_ctlinput(code, 錯誤資料報的目的IP, icp->icmp_ip);//icp->icmp_ip指向錯誤IP首部
三.ICMP請求與應答處理
ICMP請求報文一般用於在網路中請求獲取其它節點的一下資訊,比如回顯,掩碼詢問等等。
1.回顯請求
所謂回顯請求即接收端將收到的資料重新發回至源端,Ping程式便是通過這個原理實現的,將傳送時的時間記錄於ICMP報文的資料部分,當目的端返回後用當前時間減去傳送事時間便是往返時間rtt。回顯請求的處理很簡單,將ICMP的型別由ICMP_ECHO改為ICMP_ECHOREPLY(icmp_code 總為0)。再由icmp_reflect函式進行應答。其程式碼如下:
case ICMP_ECHO:
icp->icmp_type = ICMP_ECHOREPLY;
goto reflect;
/*其它ICMP請求型別*/
reflect:
icmp.icp_reflect++;
xxx;
icmp_reflect(m);
return; // 注意此處使用了return,因此對TCP/IP中已定義的請求不會由rip_input函式交付給應用,即不會調製第一節中的raw程式碼段
2.時間戳請求
時間戳請求中包含3個時間,傳送請求時間由請求方填寫,其餘兩個:收到請求的時間和發出回答的時間由應答方進行填寫。此處對齊實現不做說明。其仍然交由函式icmp_reflect進行回覆,icmp_reflect函式用於將ICMP回答或差錯發回給請求端或無效資料報的源端。
3.回答處理
核心從不產生任何ICMP請求報文,因此也不處理任何ICMP報文。核心將其收到的所有回答傳給等待ICMP報文的程序,且路由器發現報文也交由其相關的處理程式(路由守護程式)。其虛擬碼如下:
case xxx_REPLY:
case xxx_REPLY:
...
case ICMP_ROUTERSOLICIT:
default:
break;
}//switch
raw:
rip_input(m);
return;
四.ICMP輸出處理
當資料過長時,IP層會對資料進行分片處理,因此當導致錯誤的資料報不是第一個分片時,icmp不會迴應ICMP錯誤報文。