利用ICMP实现Traceroute

作者: JamesYu | 来源:发表于2016-01-29 23:56 被阅读2819次

    最近工作中遇到一个需求,就是需要知道我们发出去的请求经过的所有路由IP地址。查了些资料,主要是用ICMP(Internet控制报文协议)。

    ICMP

    ICMP是IP层的一个组成部分,用来传递错误报文信息的,这个东西运维用得比较多。下图是ICMP在TCP/IP中的位置。

    14540545202356

    ICMP报文是在数据报内部被传输的,格式如下图:

    14540496538467

    ICMP报文格式会根据不同的错误类型有不同的格式,但是8位类型,8位代码,16位校验和是必不可少的,如下图:

    14540498013133

    ICMP报文类型:

    14540503478117

    ICMP有18种报文类型,每个类型里面又分不同的code。

    下面看看几个常见的报文出错类型格式。

    ICMP地址掩码请求与应答

    14540513973201

    ICMP时间戳请求与应答

    14540521893396

    ICMP不可到达报文

    14540522236263

    这个报文格式也是我们下面程序实现解析的依据。

    Ping跟踪路由的原理

    Ping主要用来测试某台主机能否到达,使用的是ICMP请求回显报文,但是同样也提供了IP路由记录选项功能。只需要在ping的时候加上参数-R即可,如:

    14540559349951

    当开启这个RR选项后,IP数据报在经过路由器的时候,会将IP地址放置IP首部中的选项字段。当数据报到达目的端时,IP地址清单复制到ICMP回显应答中,当ping收到回显应答时,控制台打印出所有的IP地址。

    过程很容易理解,但是有两个缺点。第一,ping的RR选项不是所有系统都支持的。第二、保存的IP地址数目是有限的。

    为什么说保存的IP地址数目是有限的呢?首先看下IP首部格式:

    14540575128044

    IP首部的长度有4位首部长度决定,因此IP首部最大长度为15*32bit,也就是60个字节。IP首都固定长度为20个字节,所以选项字段的最大长度也只有40个字节能够用来保存IP地址。

    IP地址在IP首部选项中保存的格式:

    14540578013803

    开启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

    14540685145024

    这个是Wireshark抓包,看到Traceroute运行的过程:

    14540684535657

    我们可以看到系统的traceroute命令实现是使用采用的UDP,并且发送的端口是大于30000的,并且每次都是端口加1,用来防止端口被目的主机占用的可能,返回的是ICMP报文。

    traceroute不能保证每次路由都是一致的,可能会因为路由的选择,结果可能不一定一致,但是大致是相似的。

    程序实现

    首先看下UDP不可到达格式,下面的代码解析也是根据这个来的:

    14540695051510

    可以看到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

    相关文章

      网友评论

      • Miss_chalk:弱弱的问下,有在iOS真机上进行traceroute的方法吗
      • pengxiaochao:作为你的ios 客户端,你还知道ICMP ,哈哈,你知道的太多了

      本文标题:利用ICMP实现Traceroute

      本文链接:https://www.haomeiwen.com/subject/zzwjkttx.html