1. 程式人生 > >TCP/IP實現(七) ICMP協議

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錯誤報文。

五.ICMP程式舉例 PING & Traceroute