Traceroute(路由追蹤)的原理及實現(xiàn)

現(xiàn)實世界中的網(wǎng)絡(luò)是由無數(shù)的計算機和路由器組成的一張的大網(wǎng),應(yīng)用的數(shù)據(jù)包在發(fā)送到服務(wù)器之前都要經(jīng)過層層的路由轉(zhuǎn)發(fā)。而Traceroute是一種常規(guī)的網(wǎng)絡(luò)分析工具,用來定位到目標(biāo)主機之間的所有路由器

原理

在介紹Traceroute的原理之前,需要了解幾個技術(shù)名詞:

  • IP協(xié)議

    IP協(xié)議是TCP/IP協(xié)議族中最核心的部分,它的作用是在兩臺主機之間傳輸數(shù)據(jù),所有上層協(xié)議的數(shù)據(jù)(HTTP、TCP、UDP等)都會被封裝在一個個的IP數(shù)據(jù)包中被發(fā)送到網(wǎng)絡(luò)上。

  • ICMP
    ICMP全稱為互聯(lián)網(wǎng)控制報文協(xié)議,它常用于傳遞錯誤信息,ICMP協(xié)議是IP層的一部分,它的報文也是通過IP數(shù)據(jù)包來傳輸?shù)摹?/p>

  • TTL
    TTL(time-to-live)是IP數(shù)據(jù)包中的一個字段,它指定了數(shù)據(jù)包最多能經(jīng)過幾次路由器。從我們源主機發(fā)出去的數(shù)據(jù)包在到達目的主機的路上要經(jīng)過許多個路由器的轉(zhuǎn)發(fā),在發(fā)送數(shù)據(jù)包的時候源主機會設(shè)置一個TTL的值,每經(jīng)過一個路由器TTL就會被減去一,當(dāng)TTL為0的時候該數(shù)據(jù)包會被直接丟棄(不再繼續(xù)轉(zhuǎn)發(fā)),并發(fā)送一個超時ICMP報文給源主機。

具體到traceroute的實現(xiàn)細節(jié)上,有兩種不同的方案:

基于UDP實現(xiàn)

在基于UDP的實現(xiàn)中,客戶端發(fā)送的數(shù)據(jù)包是通過UDP協(xié)議來傳輸?shù)模褂昧艘粋€大于30000的端口號,服務(wù)器在收到這個數(shù)據(jù)包的時候會返回一個端口不可達的ICMP錯誤信息,客戶端通過判斷收到的錯誤信息是TTL超時還是端口不可達來判斷數(shù)據(jù)包是否到達目標(biāo)主機,具體的流程如圖:

基于UDP實現(xiàn)的traceroute
  1. 客戶端發(fā)送一個TTL為1,端口號大于30000的UDP數(shù)據(jù)包,到達第一站路由器之后TTL被減去1,返回了一個超時的ICMP數(shù)據(jù)包,客戶端得到第一跳路由器的地址。
  2. 客戶端發(fā)送一個TTL為2的數(shù)據(jù)包,在第二跳的路由器節(jié)點處超時,得到第二跳路由器的地址。
  3. 客戶端發(fā)送一個TTL為3的數(shù)據(jù)包,數(shù)據(jù)包成功到達目標(biāo)主機,返回一個端口不可達錯誤,traceroute結(jié)束。

Linux和macOS系統(tǒng)自帶了一個traceroute指令,可以結(jié)合Wireshark抓包來看看它的實現(xiàn)原理。首先對百度的域名進行traceroute:traceroute www.baidu.com,每一跳默認發(fā)送三個數(shù)據(jù)包,我們會看到下面這樣的輸出:

traceroute.png

對該域名的IP:115.239.210.27進行traceroute,此時Wireshark抓包的結(jié)果如下:

抓包結(jié)果

注意看紅框處的內(nèi)容,跟第一張圖對比,可以看到traceroute程序首先通過UDP協(xié)議向目標(biāo)地址115.239.210.27發(fā)送了一個TTL為1的數(shù)據(jù)包,然后在第一個路由器中TTL超時,返回一個錯誤類型為Time-to-live exceeded的ICMP數(shù)據(jù)包,此時我們通過該數(shù)據(jù)包的源地址可知第一站路由器的地址為10.242.0.1。之后只需要不停增加TTL的值就能得到每一跳的地址了。

然而一直跑下去會發(fā)現(xiàn),traceroute并不能到達目的地,當(dāng)TTL增加到一定大小之后就一直拿不到返回的數(shù)據(jù)包了:

結(jié)果全是丟失

其實這個時候數(shù)據(jù)包已經(jīng)到達目標(biāo)服務(wù)器了,但是因為安全問題大部分的應(yīng)用服務(wù)器都不提供UDP服務(wù)(或者被防火墻擋掉),所以我們拿不到服務(wù)器的任何返回,程序就理所當(dāng)然的認為還沒有結(jié)束,一直嘗試增加數(shù)據(jù)包的TTL。

目前在網(wǎng)上找到許多開源iOS traceroute實現(xiàn)大多都是基于UDP的方案,實際用起來并不能達到想要的效果,所以我們需要采用另一種方案來實現(xiàn)。

基于ICMP實現(xiàn)

上述方案失敗的原因是由于服務(wù)器對于UDP數(shù)據(jù)包的處理,所以在這一種實現(xiàn)中我們不使用UDP協(xié)議,而是直接發(fā)送一個ICMP回顯請求(echo request)數(shù)據(jù)包,服務(wù)器在收到回顯請求的時候會向客戶端發(fā)送一個ICMP回顯應(yīng)答(echo reply)數(shù)據(jù)包,在這之后的流程還是跟第一種方案一樣。這樣就避免了我們的traceroute數(shù)據(jù)包被服務(wù)器的防火墻策略墻掉。

采用這種方案的實現(xiàn)流程如下:

基于ICMP實現(xiàn)的traceroute
  1. 客戶端發(fā)送一個TTL為1的ICMP請求回顯數(shù)據(jù)包,在第一跳的時候超時并返回一個ICMP超時數(shù)據(jù)包,得到第一跳的地址。
  2. 客戶端發(fā)送一個TTL為2的ICMP請求回顯數(shù)據(jù)包,得到第二跳的地址。
  3. 客戶端發(fā)送一個TTL為3的ICMP請求回顯數(shù)據(jù)包,到達目標(biāo)主機,目標(biāo)主機返回一個ICMP回顯應(yīng)答,traceroute結(jié)束。

可以看出與第一種實現(xiàn)相比,區(qū)別主要在發(fā)送的數(shù)據(jù)包類型以及對于結(jié)束的判斷上,大體的流程還是一致的。

值得一提的是在Windows系統(tǒng)中也有traceroute程序,它的名字叫做tracert,tracert就是用采用這種方法來實現(xiàn)的,感興趣的話可以自行嘗試一下,這里就不再演示了。

實現(xiàn)

這里我們主要討論基于ICMP的實現(xiàn),相關(guān)的Demo已經(jīng)上傳至github:https://github.com/L-Zephyr/TracerouteDemo.git

采用這種方案時,ICMP數(shù)據(jù)包的創(chuàng)建、解析、校驗都需要我們自己進行,ICMP是封裝在IP數(shù)據(jù)包的數(shù)據(jù)段中傳輸?shù)模躁P(guān)鍵在于如何創(chuàng)建和發(fā)送ICMP數(shù)據(jù),以及接收到返回的數(shù)據(jù)時如何從IP數(shù)據(jù)包中將ICMP解析出來:

創(chuàng)建ICMP數(shù)據(jù)

ICMP數(shù)據(jù)包頭部的格式如下:

ICMP數(shù)據(jù)結(jié)構(gòu)

其中的類型字段用來表示消息的類型,在Wiki上可以看到所有類型代表的含義。報文中的標(biāo)識符和序列號由發(fā)送端指定,如果這個ICMP報文是一個請求回顯的報文(類型為8,代碼為0),這兩個字段會被原封不動的返回。

根據(jù)上圖中各個字段的大小可以定義如下類型:

typedef struct ICMPPacket {
    uint8_t     type; // 類型
    uint8_t     code; // 類型代碼
    uint16_t    checksum; // 校驗碼
    uint16_t    identifier; // ID
    uint16_t    sequenceNumber; // 序列號
    // data...
} ICMPPacket;

其中的type字段指定了這個ICMP數(shù)據(jù)包的類型,是需要重點關(guān)注的對象,為此定義一個報文類型的枚舉:

// ICMPv4報文類型
typedef enum ICMPv4Type {
    kICMPv4TypeEchoReply = 0, // 回顯應(yīng)答
    kICMPv4TypeEchoRequest = 8, // 回顯請求
    kICMPv4TypeTimeOut = 11, // 超時
}ICMPv4Type;

比較麻煩的是校驗的計算,這一部分直接使用了蘋果官方示例SimplePing中的代碼,所涉及到的幾個工具方法封裝在類型TracerouteCommon中。

在發(fā)送數(shù)據(jù)的時系統(tǒng)會自動加上IP頭部不需要自己處理,如此一來我們只需要創(chuàng)建一個ICMPPacket數(shù)據(jù)包并通過socket發(fā)送到目標(biāo)服務(wù)器就可以了。

解析ICMP數(shù)據(jù)

接下來就是要接收服務(wù)器向我們返回的ICMP數(shù)據(jù)了,我們接收到的是帶有IP頭部的原始數(shù)據(jù),所以必須先進行一些處理將ICMP從IP數(shù)據(jù)包中提取出來,IP數(shù)據(jù)包由兩部分組成:數(shù)據(jù)包頭部信息部分以及實際的數(shù)據(jù)部分。下圖是IPv4數(shù)據(jù)包的結(jié)構(gòu):

IPv4數(shù)據(jù)包格式

一眼看上去是不是感覺很混亂,其實這里面只有用紅框圈出來的這這三個字段需要我們關(guān)心:版本表示該數(shù)據(jù)包是IPv4還是IPv6;之前說過ICMP協(xié)議是通過IP協(xié)議來傳輸?shù)模绻摂?shù)據(jù)包傳輸?shù)氖荌CMP協(xié)議則協(xié)議字段會被設(shè)置為1;由于IPv4數(shù)據(jù)包帶有可選的選項字段,所以其頭部的長度是可變的,此時需要根據(jù)首部長度字段來獲取具體的數(shù)據(jù)。

根據(jù)上面的結(jié)構(gòu)可以定義類型:

typedef struct IPv4Header {
    uint8_t versionAndHeaderLength; // 版本和首部長度
    uint8_t serviceType;
    uint16_t totalLength; 
    uint16_t identifier;
    uint16_t flagsAndFragmentOffset;
    uint8_t timeToLive;
    uint8_t protocol; // 協(xié)議類型,1表示ICMP
    uint16_t checksum;
    uint8_t sourceAddress[4];
    uint8_t destAddress[4];
    // options...
    // data...
} IPv4Header;

提取ICMP數(shù)據(jù)包的方法如下:

+ (ICMPPacket *)unpackICMPv4Packet:(char *)packet len:(int)len {
    if (len < (sizeof(IPv4Header) + sizeof(ICMPPacket))) {
        return NULL;
    }
    
    const struct IPv4Header *ipPtr = (const IPv4Header *)packet;
    if ((ipPtr->versionAndHeaderLength & 0xF0) != 0x40 || // IPv4
        ipPtr->protocol != 1) { // ICMP
        return NULL;
    }
    
    // 獲取IP頭部長度
    size_t ipHeaderLength = (ipPtr->versionAndHeaderLength & 0x0F) * sizeof(uint32_t); 
    if (len < ipHeaderLength + sizeof(ICMPPacket)) {
        return NULL;
    }
    
    // 返回數(shù)據(jù)部分的ICMP
    return (ICMPPacket *)((char *)packet + ipHeaderLength);
}

其中出現(xiàn)的如ipPtr->versionAndHeaderLength & 0xF0的判斷是因為版本號和首部長度各自只占4個bit,在結(jié)構(gòu)中直接定義了一個1字節(jié)的uint8_t類型來表示,所以只能通過位運算符&來獲取各自的值。

整體流程

有了上面的兩步,剩下的事情就很簡單了,下面是整體流程的偽代碼:

// 1. 創(chuàng)建一個套接字
int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);

// 2. 最多嘗試30跳
int ttl = 1;
for (0...30) {
    // 3. 設(shè)置TTL,發(fā)送3個ICMP數(shù)據(jù)包,每一跳都將遞增TTL
    setsockopt(sock, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl));
    ++ttl;
    for (0...3) {
        // 4. 發(fā)送并等待返回的數(shù)據(jù)包
        sendto(...);
        recvfrom(...);
        
        // 5. 解析數(shù)據(jù)包,記錄數(shù)據(jù),成功條件判斷
        ICMPPacket *packet = unpack(...);
    }
}

socket的類型采用了SOCK_DGRAM,有些小伙伴可能會感到疑惑:用SOCK_DGRAM創(chuàng)建套接字不還是發(fā)送UDP數(shù)據(jù)么?

確實在許多系統(tǒng)的實現(xiàn)中要直接發(fā)送ICMP的話需要使用原始套接字(類型為SOCK_RAW),這在iOS系統(tǒng)中是不被允許使用的,但是查閱資料中了解到macOS支持一種使用參數(shù)SOCK_DGRAMIPPROTO_ICMP來直接創(chuàng)建ICMP套接字方式,嘗試之下果然iOS也支持這種用法。不過在使用中發(fā)現(xiàn)了一個問題:使用IPv4套接字的時候接收到的數(shù)據(jù)包是帶有原始IP頭部的,而使用IPv6套接字的時候收到的數(shù)據(jù)包卻沒有IP頭部,這個問題讓我比較疑惑,各位大佬如果有對這一塊了解的話還望賜教。

總結(jié)

Demo中的示例程序已經(jīng)在模擬器和真機環(huán)境經(jīng)過測試,可以看到,現(xiàn)在Traceroute已經(jīng)能夠正常的工作了:

Traceroute Demo

有些路由器會隱藏的自己的位置,不讓ICMP Timeout的消息通過,結(jié)果就是在那一跳上始終會顯示星號,此外服務(wù)器也可以偽造traceroute路徑的,不過一般應(yīng)用服務(wù)器也沒有理由這么做,所以Traceroute的結(jié)果還是能夠為網(wǎng)絡(luò)分析提供一些參考的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1.這篇文章不是本人原創(chuàng)的,只是個人為了對這部分知識做一個整理和系統(tǒng)的輸出而編輯成的,在此鄭重地向本文所引用文章的...
    SOMCENT閱讀 13,340評論 6 174
  • 個人認為,Goodboy1881先生的TCP /IP 協(xié)議詳解學(xué)習(xí)博客系列博客是一部非常精彩的學(xué)習(xí)筆記,這雖然只是...
    貳零壹柒_fc10閱讀 5,180評論 0 8
  • 8.1 引言 由Van Jacobson編寫的Traceroute程序是一個能更深入探索TCP/IP協(xié)議的方便可用...
    張芳濤閱讀 1,797評論 0 3
  • Traceroute是一個非常便利的網(wǎng)絡(luò)診斷工具。它可以輸出以下三個內(nèi)容: 1 網(wǎng)絡(luò)數(shù)據(jù)包的從源地址到目的地址的整...
    蝎子看互聯(lián)網(wǎng)閱讀 1,036評論 0 51
  • 11.1 引言 UDP是一個簡單的面向數(shù)據(jù)報的運輸層協(xié)議:進程的每個輸出操作都正好產(chǎn)生一個UDP數(shù)據(jù)報,并組裝成一...
    張芳濤閱讀 2,963評論 1 6

友情鏈接更多精彩內(nèi)容