最近工作中遇到一个需求,就是需要知道我们发出去的请求经过的所有路由IP地址。查了些资料,主要是用ICMP(Internet控制报文协议)。
ICMP
ICMP是IP层的一个组成部分,用来传递错误报文信息的,这个东西运维用得比较多。下图是ICMP在TCP/IP中的位置。
ICMP报文是在数据报内部被传输的,格式如下图:
ICMP报文格式会根据不同的错误类型有不同的格式,但是8位类型,8位代码,16位校验和是必不可少的,如下图:
ICMP报文类型:
ICMP有18种报文类型,每个类型里面又分不同的code。
下面看看几个常见的报文出错类型格式。
ICMP地址掩码请求与应答
ICMP时间戳请求与应答
ICMP不可到达报文
这个报文格式也是我们下面程序实现解析的依据。
Ping跟踪路由的原理
Ping主要用来测试某台主机能否到达,使用的是ICMP请求回显报文,但是同样也提供了IP路由记录选项功能。只需要在ping的时候加上参数-R即可,如:
当开启这个RR选项后,IP数据报在经过路由器的时候,会将IP地址放置IP首部中的选项字段。当数据报到达目的端时,IP地址清单复制到ICMP回显应答中,当ping收到回显应答时,控制台打印出所有的IP地址。
过程很容易理解,但是有两个缺点。第一,ping的RR选项不是所有系统都支持的。第二、保存的IP地址数目是有限的。
为什么说保存的IP地址数目是有限的呢?首先看下IP首部格式:
IP首部的长度有4位首部长度决定,因此IP首部最大长度为15*32bit,也就是60个字节。IP首都固定长度为20个字节,所以选项字段的最大长度也只有40个字节能够用来保存IP地址。
IP地址在IP首部选项中保存的格式:
开启RR选项用去3个字节,剩下也只有37个字节可以使用,每个IP地址占用4个字节,所以最多也就只能保存9个IP地址。如果我们的数据报经过的路由器比较多时,就不准确了。
Traceroute路由跟踪
Traceroute也是用来跟踪IP路由选项的,但是它没有ping的那些限制。Traceroute跟踪路由的原理是通过设置IP数据报的TTL(生存周期)。IP数据报每经过一个路由器的时候,就将TTL减1,如果发现TTL等于0,那么将不会进行再次转发,并将数据报丢弃,并给源地址发送一个ICMP不可到达报文。而这份ICMP报文中包含了该路由器的信息。
所以,Traceroute跟踪路由的大致流程是先发送一个TTL为1的数据报,当第一个路由器处理时,将TTL值减1,然后丢弃该数据报,并返回一个超时ICMP报文,得到第一个IP地址。然后再发送一个TTL为2的数据报,当到第二个路由器的时候,又返回一个IP地址。重复以上步骤,我们会不断得到超时ICMP报文。那我们如何知道我们的数据报何时到达目的主机呢?
Traceroute通过发送一个UDP包,并且端口号是大于30000的。如果目的主机没有任何程序使用该端口,那么主机会产生一份"端口不可到达错误"。所以,我们程序要做的就是解析两种情况下的ICMP报文,一种是超时报文,还有一个是端口不可到达报文。
看下系统的Traceroute运行过程:
终端输入traceroute 115.239.210.27
这个是Wireshark抓包,看到Traceroute运行的过程:
我们可以看到系统的traceroute命令实现是使用采用的UDP,并且发送的端口是大于30000的,并且每次都是端口加1,用来防止端口被目的主机占用的可能,返回的是ICMP报文。
traceroute不能保证每次路由都是一致的,可能会因为路由的选择,结果可能不一定一致,但是大致是相似的。
程序实现
首先看下UDP不可到达格式,下面的代码解析也是根据这个来的:
可以看到IP数据报格式,由20字节IP首部+ICMP首部+产生差错的数据报IP首部+UDP首部8字节。
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);
//发送采用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;
}
//开始发送
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) {
//超过最大尝试次数后就不再发送了
break;
}
continue;
}
else {
//以下只是数据报的解析了
struct ip *ip = (struct ip*)recvBuf;
int ipLen = ip->ip_hl<<2;
struct icmp *icmp = (struct icmp*)(recvBuf + ipLen);
//整个ICMP报文长度:ICMP首部 + 产生出错的ip首部 + UDP首部8字节
int icmpLen = recv - ipLen;
if (icmpLen < 8) {
continue;
}
if (icmp->icmp_type == ICMP_TIMXCEED
&& icmp->icmp_code == ICMP_TIMXCEED_INTRANS) {
//获取产生出错的ip首部 + UDP首部8字节
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) {
//发生端口不可到达
break;
}
else {
NSLog(@"====%d===%d", icmp->icmp_type, icmp->icmp_code);
}
}
}
ttl++;
}
以上代码在真机上是跑不了的,只能在模拟器上。因为iPhone的sdk里面把解析数据报的几个头文件给去掉了。。不过不影响我们对IP获取的需求。实际运行发现,端口不可到达这个报文,不是立马就能得到的,包括系统的traceroute命令也是,系统会不断的发送UDP包,过了好久有可能收到。。。
参考:
TCP/IP协议详解
http://www.cnblogs.com/aLittleBitCool/archive/2011/09/20/2182760.html