最近工作中遇到一個需求,就是需要知道我們發(fā)出去的請求經(jīng)過的所有路由IP地址杠愧。查了些資料,主要是用ICMP(Internet控制報文協(xié)議)。
ICMP
ICMP是IP層的一個組成部分纹安,用來傳遞錯誤報文信息的,這個東西運維用得比較多。下圖是ICMP在TCP/IP中的位置厢岂。
ICMP報文是在數(shù)據(jù)報內部被傳輸?shù)墓舛剑袷饺缦聢D:
ICMP報文格式會根據(jù)不同的錯誤類型有不同的格式,但是8位類型塔粒,8位代碼结借,16位校驗和是必不可少的,如下圖:
ICMP報文類型:
ICMP有18種報文類型卒茬,每個類型里面又分不同的code船老。
下面看看幾個常見的報文出錯類型格式。
ICMP地址掩碼請求與應答
ICMP時間戳請求與應答
ICMP不可到達報文
這個報文格式也是我們下面程序實現(xiàn)解析的依據(jù)扬虚。
Ping跟蹤路由的原理
Ping主要用來測試某臺主機能否到達努隙,使用的是ICMP請求回顯報文,但是同樣也提供了IP路由記錄選項功能辜昵。只需要在ping的時候加上參數(shù)-R即可荸镊,如:
當開啟這個RR選項后,IP數(shù)據(jù)報在經(jīng)過路由器的時候堪置,會將IP地址放置IP首部中的選項字段躬存。當數(shù)據(jù)報到達目的端時,IP地址清單復制到ICMP回顯應答中舀锨,當ping收到回顯應答時岭洲,控制臺打印出所有的IP地址。
過程很容易理解坎匿,但是有兩個缺點盾剩。第一,ping的RR選項不是所有系統(tǒng)都支持的替蔬。第二告私、保存的IP地址數(shù)目是有限的。
為什么說保存的IP地址數(shù)目是有限的呢承桥?首先看下IP首部格式:
IP首部的長度有4位首部長度決定驻粟,因此IP首部最大長度為15*32bit,也就是60個字節(jié)凶异。IP首都固定長度為20個字節(jié)蜀撑,所以選項字段的最大長度也只有40個字節(jié)能夠用來保存IP地址。
IP地址在IP首部選項中保存的格式:
開啟RR選項用去3個字節(jié)剩彬,剩下也只有37個字節(jié)可以使用酷麦,每個IP地址占用4個字節(jié),所以最多也就只能保存9個IP地址喉恋。如果我們的數(shù)據(jù)報經(jīng)過的路由器比較多時贴铜,就不準確了粪摘。
Traceroute路由跟蹤
Traceroute也是用來跟蹤IP路由選項的,但是它沒有ping的那些限制绍坝。Traceroute跟蹤路由的原理是通過設置IP數(shù)據(jù)報的TTL(生存周期)。IP數(shù)據(jù)報每經(jīng)過一個路由器的時候苔悦,就將TTL減1轩褐,如果發(fā)現(xiàn)TTL等于0,那么將不會進行再次轉發(fā)玖详,并將數(shù)據(jù)報丟棄把介,并給源地址發(fā)送一個ICMP不可到達報文。而這份ICMP報文中包含了該路由器的信息蟋座。
所以拗踢,Traceroute跟蹤路由的大致流程是先發(fā)送一個TTL為1的數(shù)據(jù)報,當?shù)谝粋€路由器處理時向臀,將TTL值減1巢墅,然后丟棄該數(shù)據(jù)報,并返回一個超時ICMP報文券膀,得到第一個IP地址君纫。然后再發(fā)送一個TTL為2的數(shù)據(jù)報,當?shù)降诙€路由器的時候芹彬,又返回一個IP地址蓄髓。重復以上步驟,我們會不斷得到超時ICMP報文舒帮。那我們如何知道我們的數(shù)據(jù)報何時到達目的主機呢会喝?
Traceroute通過發(fā)送一個UDP包,并且端口號是大于30000的玩郊。如果目的主機沒有任何程序使用該端口肢执,那么主機會產(chǎn)生一份"端口不可到達錯誤"。所以瓦宜,我們程序要做的就是解析兩種情況下的ICMP報文蔚万,一種是超時報文,還有一個是端口不可到達報文临庇。
看下系統(tǒng)的Traceroute運行過程:
終端輸入traceroute 115.239.210.27
這個是Wireshark抓包反璃,看到Traceroute運行的過程:
我們可以看到系統(tǒng)的traceroute命令實現(xiàn)是使用采用的UDP,并且發(fā)送的端口是大于30000的假夺,并且每次都是端口加1淮蜈,用來防止端口被目的主機占用的可能,返回的是ICMP報文已卷。
traceroute不能保證每次路由都是一致的梧田,可能會因為路由的選擇,結果可能不一定一致,但是大致是相似的裁眯。
程序實現(xiàn)
首先看下UDP不可到達格式鹉梨,下面的代碼解析也是根據(jù)這個來的:
可以看到IP數(shù)據(jù)報格式,由20字節(jié)IP首部+ICMP首部+產(chǎn)生差錯的數(shù)據(jù)報IP首部+UDP首部8字節(jié)穿稳。
struct hostent *_host = gethostbyname([host UTF8String]);
if (_host == NULL) {
//域名解析失敶嬖怼!
return;
}
struct in_addr *addr = (struct in_addr *)_host->h_addr_list[0];
char *ip_addr = inet_ntoa(*addr);
struct sockaddr_in destAddr, fromAddr;
memset(&destAddr, 0, sizeof(destAddr));
destAddr.sin_family = AF_INET;
destAddr.sin_addr.s_addr = inet_addr(ip_addr);
destAddr.sin_port = htons(_sourePort);
//發(fā)送采用UDP
if ((send_sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
NSLog(@"fail to create send_socket:%s", strerror(errno));
return;
}
//接受ICMP
if ((recv_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP)) < 0) {
NSLog(@"fail to create recv_socket:%s", strerror(errno));
return;
}
struct timeval timeout;
memset(&timeout, 0, sizeof(timeout));
timeout.tv_sec = 0;
timeout.tv_usec = _timeout;
//設置超時時間
if (setsockopt(send_sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(timeout)) < 0) {
NSLog(@"fail to set socket option:%s", strerror(errno));
return;
}
//設置超時時間
if (setsockopt(recv_sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(timeout)) < 0) {
NSLog(@"fail to set socket option:%s", strerror(errno));
return;
}
char recvBuf[1024];
int ttl = 1;
char sendBuf[100];
memset(sendBuf, 0, sizeof(sendBuf));
while (ttl < _maxTTL) {
//設置TTL
if (setsockopt(send_sock, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl)) < 0) {
NSLog(@"fail to set socket option:%s", strerror(errno));
return;
}
//開始發(fā)送
for (int i = 0; i < _maxAttempts; i++) {
destAddr.sin_port = htons(_sourePort++);
if (sendto(send_sock, sendBuf, 0, 0, (struct sockaddr *) &destAddr, sizeof(destAddr)) < 0) {
NSLog(@"fail to send data:%s", strerror(errno));
continue;
}
ssize_t recv;
memset(&fromAddr, 0, sizeof(fromAddr));
memset(&recvBuf, 0, sizeof(recvBuf));
socklen_t len = sizeof(fromAddr);
if ((recv = recvfrom(recv_sock, recvBuf, sizeof(recvBuf), 0, (struct sockaddr *)&fromAddr, &len)) < 0) {
NSLog(@"fail to recv data:code:%d %s", errno,strerror(errno));
if (i == _maxAttempts - 1) {
//超過最大嘗試次數(shù)后就不再發(fā)送了
break;
}
continue;
}
else {
//以下只是數(shù)據(jù)報的解析了
struct ip *ip = (struct ip*)recvBuf;
int ipLen = ip->ip_hl<<2;
struct icmp *icmp = (struct icmp*)(recvBuf + ipLen);
//整個ICMP報文長度:ICMP首部 + 產(chǎn)生出錯的ip首部 + UDP首部8字節(jié)
int icmpLen = recv - ipLen;
if (icmpLen < 8) {
continue;
}
if (icmp->icmp_type == ICMP_TIMXCEED
&& icmp->icmp_code == ICMP_TIMXCEED_INTRANS) {
//獲取產(chǎn)生出錯的ip首部 + UDP首部8字節(jié)
if (icmpLen < 8 + sizeof(struct ip)) {
continue;
}
struct ip *errorIP = (struct ip *)(recvBuf + ipLen + 8);
int errorIPLength = errorIP->ip_hl<<2;
if (icmpLen < 8 + errorIPLength + 8) {
continue;
}
struct udphdr *udp = (struct udphdr *)(recvBuf + ipLen + 8 + errorIPLength);
// u_short port = htons(_sourePort);
// u_short po = htons(_sourePort);
// u_char ip_p = errorIP->ip_p;
// errorIP->ip_p == IPPROTO_UDP
char address[16];
memset(&address, 0, sizeof(address));
inet_ntop(AF_INET, &fromAddr.sin_addr.s_addr, address, sizeof (address));
NSString *hostAddress = [NSString stringWithFormat:@"%s",address];
//打印IP地址
NSLog(@"====address:%@", hostAddress);
break;
}
else if (icmp->icmp_type == ICMP_UNREACH
&& icmp->icmp_code == ICMP_UNREACH_PORT) {
//發(fā)生端口不可到達
break;
}
else {
NSLog(@"====%d===%d", icmp->icmp_type, icmp->icmp_code);
}
}
}
ttl++;
}
以上代碼在真機上是跑不了的逢艘,只能在模擬器上旦袋。因為iPhone的sdk里面把解析數(shù)據(jù)報的幾個頭文件給去掉了。它改。不過不影響我們對IP獲取的需求疤孕。實際運行發(fā)現(xiàn),端口不可到達這個報文央拖,不是立馬就能得到的祭阀,包括系統(tǒng)的traceroute命令也是,系統(tǒng)會不斷的發(fā)送UDP包爬泥,過了好久有可能收到柬讨。。袍啡。
參考:
TCP/IP協(xié)議詳解
http://www.cnblogs.com/aLittleBitCool/archive/2011/09/20/2182760.html