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


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

 

現實世界中的網絡是由無數的計算機和路由器組成的一張的大網,應用的數據包在發送到服務器之前都要經過層層的路由轉發。而Traceroute是一種常規的網絡分析工具,用來定位到目標主機之間的所有路由器

原理

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

  • IP協議

    IP協議是TCP/IP協議族中最核心的部分,它的作用是在兩台主機之間傳輸數據,所有上層協議的數據(HTTP、TCP、UDP等)都會被封裝在一個個的IP數據包中被發送到網絡上。

  • ICMP
    ICMP全稱為互聯網控制報文協議,它常用於傳遞錯誤信息,ICMP協議是IP層的一部分,它的報文也是通過IP數據包來傳輸的。

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

具體到traceroute的實現細節上,有兩種不同的方案:

基於UDP實現

在基於UDP的實現中,客戶端發送的數據包是通過UDP協議來傳輸的,使用了一個大於30000的端口號,服務器在收到這個數據包的時候會返回一個端口不可達的ICMP錯誤信息,客戶端通過判斷收到的錯誤信息是TTL超時還是端口不可達來判斷數據包是否到達目標主機,具體的流程如圖:

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

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

traceroute.png

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

抓包結果

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

然而一直跑下去會發現,traceroute並不能到達目的地,當TTL增加到一定大小之后就一直拿不到返回的數據包了:

結果全是丟失

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

目前在網上找到許多開源iOS traceroute實現大多都是基於UDP的方案,實際用起來並不能達到想要的效果,所以我們需要采用另一種方案來實現。

基於ICMP實現

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

采用這種方案的實現流程如下:

基於ICMP實現的traceroute
  1. 客戶端發送一個TTL為1的ICMP請求回顯數據包,在第一跳的時候超時並返回一個ICMP超時數據包,得到第一跳的地址。
  2. 客戶端發送一個TTL為2的ICMP請求回顯數據包,得到第二跳的地址。
  3. 客戶端發送一個TTL為3的ICMP請求回顯數據包,到達目標主機,目標主機返回一個ICMP回顯應答,traceroute結束。

可以看出與第一種實現相比,區別主要在發送的數據包類型以及對於結束的判斷上,大體的流程還是一致的。

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

實現

這里我們主要討論基於ICMP的實現,相關的Demo已經上傳至github:https://github.com/L-Zephyr/TracerouteDemo.git

采用這種方案時,ICMP數據包的創建、解析、校驗都需要我們自己進行,ICMP是封裝在IP數據包的數據段中傳輸的,所以關鍵在於如何創建和發送ICMP數據,以及接收到返回的數據時如何從IP數據包中將ICMP解析出來:

創建ICMP數據

ICMP數據包頭部的格式如下:

ICMP數據結構

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

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

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

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

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

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

在發送數據的時系統會自動加上IP頭部不需要自己處理,如此一來我們只需要創建一個ICMPPacket數據包並通過socket發送到目標服務器就可以了。

解析ICMP數據

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

IPv4數據包格式

一眼看上去是不是感覺很混亂,其實這里面只有用紅框圈出來的這這三個字段需要我們關心:版本表示該數據包是IPv4還是IPv6;之前說過ICMP協議是通過IP協議來傳輸的,如果該數據包傳輸的是ICMP協議則協議字段會被設置為1;由於IPv4數據包帶有可選的選項字段,所以其頭部的長度是可變的,此時需要根據首部長度字段來獲取具體的數據。

根據上面的結構可以定義類型:

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

提取ICMP數據包的方法如下:

+ (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; } // 返回數據部分的ICMP return (ICMPPacket *)((char *)packet + ipHeaderLength); } 

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

整體流程

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

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

socket的類型采用了SOCK_DGRAM,有些小伙伴可能會感到疑惑:用SOCK_DGRAM創建套接字不還是發送UDP數據么?

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

總結

Demo中的示例程序已經在模擬器和真機環境經過測試,可以看到,現在Traceroute已經能夠正常的工作了:

Traceroute Demo

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM