現(xiàn)實(shí)世界中的網(wǎng)絡(luò)是由無(wú)數(shù)的計(jì)算機(jī)和路由器組成的一張的大網(wǎng)榆纽,應(yīng)用的數(shù)據(jù)包在發(fā)送到服務(wù)器之前都要經(jīng)過(guò)層層的路由轉(zhuǎn)發(fā)铅祸。而Traceroute是一種常規(guī)的網(wǎng)絡(luò)分析工具眼耀,用來(lái)定位到目標(biāo)主機(jī)之間的所有路由器
原理
在介紹Traceroute的原理之前固蛾,需要了解幾個(gè)技術(shù)名詞:
-
IP協(xié)議
IP協(xié)議是TCP/IP協(xié)議族中最核心的部分碉哑,它的作用是在兩臺(tái)主機(jī)之間傳輸數(shù)據(jù),所有上層協(xié)議的數(shù)據(jù)(HTTP鸵熟、TCP副编、UDP等)都會(huì)被封裝在一個(gè)個(gè)的IP數(shù)據(jù)包中被發(fā)送到網(wǎng)絡(luò)上。
ICMP
ICMP全稱為互聯(lián)網(wǎng)控制報(bào)文協(xié)議流强,它常用于傳遞錯(cuò)誤信息痹届,ICMP協(xié)議是IP層的一部分,它的報(bào)文也是通過(guò)IP數(shù)據(jù)包來(lái)傳輸?shù)摹?/p>TTL
TTL(time-to-live)是IP數(shù)據(jù)包中的一個(gè)字段打月,它指定了數(shù)據(jù)包最多能經(jīng)過(guò)幾次路由器队腐。從我們?cè)粗鳈C(jī)發(fā)出去的數(shù)據(jù)包在到達(dá)目的主機(jī)的路上要經(jīng)過(guò)許多個(gè)路由器的轉(zhuǎn)發(fā),在發(fā)送數(shù)據(jù)包的時(shí)候源主機(jī)會(huì)設(shè)置一個(gè)TTL的值奏篙,每經(jīng)過(guò)一個(gè)路由器TTL就會(huì)被減去一柴淘,當(dāng)TTL為0的時(shí)候該數(shù)據(jù)包會(huì)被直接丟棄(不再繼續(xù)轉(zhuǎn)發(fā)),并發(fā)送一個(gè)超時(shí)ICMP報(bào)文給源主機(jī)。
具體到traceroute的實(shí)現(xiàn)細(xì)節(jié)上为严,有兩種不同的方案:
基于UDP實(shí)現(xiàn)
在基于UDP的實(shí)現(xiàn)中敛熬,客戶端發(fā)送的數(shù)據(jù)包是通過(guò)UDP協(xié)議來(lái)傳輸?shù)模褂昧艘粋€(gè)大于30000的端口號(hào)第股,服務(wù)器在收到這個(gè)數(shù)據(jù)包的時(shí)候會(huì)返回一個(gè)端口不可達(dá)的ICMP錯(cuò)誤信息荸型,客戶端通過(guò)判斷收到的錯(cuò)誤信息是TTL超時(shí)還是端口不可達(dá)來(lái)判斷數(shù)據(jù)包是否到達(dá)目標(biāo)主機(jī),具體的流程如圖:
- 客戶端發(fā)送一個(gè)TTL為1炸茧,端口號(hào)大于30000的UDP數(shù)據(jù)包瑞妇,到達(dá)第一站路由器之后TTL被減去1,返回了一個(gè)超時(shí)的ICMP數(shù)據(jù)包梭冠,客戶端得到第一跳路由器的地址辕狰。
- 客戶端發(fā)送一個(gè)TTL為2的數(shù)據(jù)包,在第二跳的路由器節(jié)點(diǎn)處超時(shí)控漠,得到第二跳路由器的地址蔓倍。
- 客戶端發(fā)送一個(gè)TTL為3的數(shù)據(jù)包,數(shù)據(jù)包成功到達(dá)目標(biāo)主機(jī)盐捷,返回一個(gè)端口不可達(dá)錯(cuò)誤偶翅,traceroute結(jié)束。
Linux和macOS系統(tǒng)自帶了一個(gè)traceroute
指令碉渡,可以結(jié)合Wireshark抓包來(lái)看看它的實(shí)現(xiàn)原理聚谁。首先對(duì)百度的域名進(jìn)行traceroute:traceroute www.baidu.com
,每一跳默認(rèn)發(fā)送三個(gè)數(shù)據(jù)包滞诺,我們會(huì)看到下面這樣的輸出:
對(duì)該域名的IP:115.239.210.27
進(jìn)行traceroute形导,此時(shí)Wireshark抓包的結(jié)果如下:
注意看紅框處的內(nèi)容,跟第一張圖對(duì)比习霹,可以看到traceroute
程序首先通過(guò)UDP協(xié)議向目標(biāo)地址115.239.210.27發(fā)送了一個(gè)TTL為1的數(shù)據(jù)包朵耕,然后在第一個(gè)路由器中TTL超時(shí),返回一個(gè)錯(cuò)誤類型為Time-to-live exceeded
的ICMP數(shù)據(jù)包淋叶,此時(shí)我們通過(guò)該數(shù)據(jù)包的源地址可知第一站路由器的地址為10.242.0.1
阎曹。之后只需要不停增加TTL的值就能得到每一跳的地址了。
然而一直跑下去會(huì)發(fā)現(xiàn)煞檩,traceroute并不能到達(dá)目的地处嫌,當(dāng)TTL增加到一定大小之后就一直拿不到返回的數(shù)據(jù)包了:
其實(shí)這個(gè)時(shí)候數(shù)據(jù)包已經(jīng)到達(dá)目標(biāo)服務(wù)器了,但是因?yàn)榘踩珕?wèn)題大部分的應(yīng)用服務(wù)器都不提供UDP服務(wù)(或者被防火墻擋掉)形娇,所以我們拿不到服務(wù)器的任何返回锰霜,程序就理所當(dāng)然的認(rèn)為還沒(méi)有結(jié)束,一直嘗試增加數(shù)據(jù)包的TTL桐早。
目前在網(wǎng)上找到許多開(kāi)源iOS traceroute實(shí)現(xiàn)大多都是基于UDP的方案,實(shí)際用起來(lái)并不能達(dá)到想要的效果,所以我們需要采用另一種方案來(lái)實(shí)現(xiàn)哄酝。
基于ICMP實(shí)現(xiàn)
上述方案失敗的原因是由于服務(wù)器對(duì)于UDP數(shù)據(jù)包的處理友存,所以在這一種實(shí)現(xiàn)中我們不使用UDP協(xié)議,而是直接發(fā)送一個(gè)ICMP回顯請(qǐng)求(echo request)數(shù)據(jù)包陶衅,服務(wù)器在收到回顯請(qǐng)求的時(shí)候會(huì)向客戶端發(fā)送一個(gè)ICMP回顯應(yīng)答(echo reply)數(shù)據(jù)包屡立,在這之后的流程還是跟第一種方案一樣。這樣就避免了我們的traceroute數(shù)據(jù)包被服務(wù)器的防火墻策略墻掉搀军。
采用這種方案的實(shí)現(xiàn)流程如下:
- 客戶端發(fā)送一個(gè)TTL為1的ICMP請(qǐng)求回顯數(shù)據(jù)包膨俐,在第一跳的時(shí)候超時(shí)并返回一個(gè)ICMP超時(shí)數(shù)據(jù)包,得到第一跳的地址罩句。
- 客戶端發(fā)送一個(gè)TTL為2的ICMP請(qǐng)求回顯數(shù)據(jù)包焚刺,得到第二跳的地址。
- 客戶端發(fā)送一個(gè)TTL為3的ICMP請(qǐng)求回顯數(shù)據(jù)包门烂,到達(dá)目標(biāo)主機(jī)乳愉,目標(biāo)主機(jī)返回一個(gè)ICMP回顯應(yīng)答,traceroute結(jié)束屯远。
可以看出與第一種實(shí)現(xiàn)相比蔓姚,區(qū)別主要在發(fā)送的數(shù)據(jù)包類型以及對(duì)于結(jié)束的判斷上,大體的流程還是一致的慨丐。
值得一提的是在Windows系統(tǒng)中也有traceroute程序坡脐,它的名字叫做tracert
,tracert
就是用采用這種方法來(lái)實(shí)現(xiàn)的房揭,感興趣的話可以自行嘗試一下挨措,這里就不再演示了。
實(shí)現(xiàn)
這里我們主要討論基于ICMP的實(shí)現(xiàn)崩溪,相關(guān)的Demo已經(jīng)上傳至github:https://github.com/L-Zephyr/TracerouteDemo.git
采用這種方案時(shí)浅役,ICMP數(shù)據(jù)包的創(chuàng)建、解析伶唯、校驗(yàn)都需要我們自己進(jìn)行觉既,ICMP是封裝在IP數(shù)據(jù)包的數(shù)據(jù)段中傳輸?shù)模躁P(guān)鍵在于如何創(chuàng)建和發(fā)送ICMP數(shù)據(jù)乳幸,以及接收到返回的數(shù)據(jù)時(shí)如何從IP數(shù)據(jù)包中將ICMP解析出來(lái):
創(chuàng)建ICMP數(shù)據(jù)
ICMP數(shù)據(jù)包頭部的格式如下:
其中的類型
字段用來(lái)表示消息的類型瞪讼,在Wiki上可以看到所有類型代表的含義。報(bào)文中的標(biāo)識(shí)符和序列號(hào)由發(fā)送端指定粹断,如果這個(gè)ICMP報(bào)文是一個(gè)請(qǐng)求回顯的報(bào)文(類型為8符欠,代碼為0),這兩個(gè)字段會(huì)被原封不動(dòng)的返回瓶埋。
根據(jù)上圖中各個(gè)字段的大小可以定義如下類型:
typedef struct ICMPPacket {
uint8_t type; // 類型
uint8_t code; // 類型代碼
uint16_t checksum; // 校驗(yàn)碼
uint16_t identifier; // ID
uint16_t sequenceNumber; // 序列號(hào)
// data...
} ICMPPacket;
其中的type
字段指定了這個(gè)ICMP數(shù)據(jù)包的類型希柿,是需要重點(diǎn)關(guān)注的對(duì)象诊沪,為此定義一個(gè)報(bào)文類型的枚舉:
// ICMPv4報(bào)文類型
typedef enum ICMPv4Type {
kICMPv4TypeEchoReply = 0, // 回顯應(yīng)答
kICMPv4TypeEchoRequest = 8, // 回顯請(qǐng)求
kICMPv4TypeTimeOut = 11, // 超時(shí)
}ICMPv4Type;
比較麻煩的是校驗(yàn)的計(jì)算,這一部分直接使用了蘋果官方示例SimplePing中的代碼曾撤,所涉及到的幾個(gè)工具方法封裝在類型TracerouteCommon
中端姚。
在發(fā)送數(shù)據(jù)的時(shí)系統(tǒng)會(huì)自動(dòng)加上IP頭部不需要自己處理,如此一來(lái)我們只需要?jiǎng)?chuàng)建一個(gè)ICMPPacket
數(shù)據(jù)包并通過(guò)socket發(fā)送到目標(biāo)服務(wù)器就可以了挤悉。
解析ICMP數(shù)據(jù)
接下來(lái)就是要接收服務(wù)器向我們返回的ICMP數(shù)據(jù)了渐裸,我們接收到的是帶有IP頭部的原始數(shù)據(jù),所以必須先進(jìn)行一些處理將ICMP從IP數(shù)據(jù)包中提取出來(lái)装悲,IP數(shù)據(jù)包由兩部分組成:數(shù)據(jù)包頭部信息部分以及實(shí)際的數(shù)據(jù)部分昏鹃。下圖是IPv4數(shù)據(jù)包的結(jié)構(gòu):
一眼看上去是不是感覺(jué)很混亂,其實(shí)這里面只有用紅框圈出來(lái)的這這三個(gè)字段需要我們關(guān)心:版本
表示該數(shù)據(jù)包是IPv4還是IPv6诀诊;之前說(shuō)過(guò)ICMP協(xié)議是通過(guò)IP協(xié)議來(lái)傳輸?shù)亩床常绻摂?shù)據(jù)包傳輸?shù)氖荌CMP協(xié)議則協(xié)議
字段會(huì)被設(shè)置為1
;由于IPv4數(shù)據(jù)包帶有可選的選項(xiàng)字段畏梆,所以其頭部的長(zhǎng)度是可變的您宪,此時(shí)需要根據(jù)首部長(zhǎng)度
字段來(lái)獲取具體的數(shù)據(jù)。
根據(jù)上面的結(jié)構(gòu)可以定義類型:
typedef struct IPv4Header {
uint8_t versionAndHeaderLength; // 版本和首部長(zhǎng)度
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頭部長(zhǎng)度
size_t ipHeaderLength = (ipPtr->versionAndHeaderLength & 0x0F) * sizeof(uint32_t);
if (len < ipHeaderLength + sizeof(ICMPPacket)) {
return NULL;
}
// 返回?cái)?shù)據(jù)部分的ICMP
return (ICMPPacket *)((char *)packet + ipHeaderLength);
}
其中出現(xiàn)的如ipPtr->versionAndHeaderLength & 0xF0
的判斷是因?yàn)榘姹咎?hào)和首部長(zhǎng)度各自只占4個(gè)bit宪巨,在結(jié)構(gòu)中直接定義了一個(gè)1字節(jié)的uint8_t
類型來(lái)表示,所以只能通過(guò)位運(yùn)算符&
來(lái)獲取各自的值溜畅。
整體流程
有了上面的兩步捏卓,剩下的事情就很簡(jiǎn)單了,下面是整體流程的偽代碼:
// 1. 創(chuàng)建一個(gè)套接字
int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
// 2. 最多嘗試30跳
int ttl = 1;
for (0...30) {
// 3. 設(shè)置TTL慈格,發(fā)送3個(gè)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
蒜田,有些小伙伴可能會(huì)感到疑惑:用SOCK_DGRAM創(chuàng)建套接字不還是發(fā)送UDP數(shù)據(jù)么?
確實(shí)在許多系統(tǒng)的實(shí)現(xiàn)中要直接發(fā)送ICMP的話需要使用原始套接字(類型為SOCK_RAW
)选泻,這在iOS系統(tǒng)中是不被允許使用的冲粤,但是查閱資料中了解到macOS支持一種使用參數(shù)SOCK_DGRAM
和IPPROTO_ICMP
來(lái)直接創(chuàng)建ICMP套接字方式,嘗試之下果然iOS也支持這種用法页眯。不過(guò)在使用中發(fā)現(xiàn)了一個(gè)問(wèn)題:使用IPv4套接字的時(shí)候接收到的數(shù)據(jù)包是帶有原始IP頭部的梯捕,而使用IPv6套接字的時(shí)候收到的數(shù)據(jù)包卻沒(méi)有IP頭部,這個(gè)問(wèn)題讓我比較疑惑窝撵,各位大佬如果有對(duì)這一塊了解的話還望賜教傀顾。
總結(jié)
Demo中的示例程序已經(jīng)在模擬器和真機(jī)環(huán)境經(jīng)過(guò)測(cè)試,可以看到碌奉,現(xiàn)在Traceroute已經(jīng)能夠正常的工作了:
有些路由器會(huì)隱藏的自己的位置短曾,不讓ICMP Timeout的消息通過(guò)寒砖,結(jié)果就是在那一跳上始終會(huì)顯示星號(hào),此外服務(wù)器也可以偽造traceroute路徑的错英,不過(guò)一般應(yīng)用服務(wù)器也沒(méi)有理由這么做入撒,所以Traceroute的結(jié)果還是能夠?yàn)榫W(wǎng)絡(luò)分析提供一些參考的隆豹。