利用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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容

  • 8.1 引言 由Van Jacobson编写的Traceroute程序是一个能更深入探索TCP/IP协议的方便可用...
    张芳涛阅读 1,646评论 0 3
  • 个人认为,Goodboy1881先生的TCP /IP 协议详解学习博客系列博客是一部非常精彩的学习笔记,这虽然只是...
    贰零壹柒_fc10阅读 5,051评论 0 8
  • 1.这篇文章不是本人原创的,只是个人为了对这部分知识做一个整理和系统的输出而编辑成的,在此郑重地向本文所引用文章的...
    SOMCENT阅读 13,051评论 6 174
  • 11.1 引言 UDP是一个简单的面向数据报的运输层协议:进程的每个输出操作都正好产生一个UDP数据报,并组装成一...
    张芳涛阅读 2,795评论 1 6
  • 接了新的班级,又要认识一批新的一年级的孩子,以往是贴桌签,一目了然一周的时间保证认识一班的孩子。此次,放慢认识...
    心童夕惕阅读 330评论 0 1