James Yu


写了那么多的if else 依然未能参透其精髓。。。


利用ICMP协议实现Traceroute

最近工作中遇到一个需求,就是需要知道我们发出去的请求经过的所有路由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

comments powered by Disqus