现实世界中的网络是由无数的计算机和路由器组成的一张的大网,应用的数据包在发送到服务器之前都要经过层层的路由转发。而Traceroute是一种常规的网络分析工具,用来定位到目标主机之间的所有路由器

原理

在介绍Traceroute的原理之前,需要了解几个技术名词:

  • IP协议

    IP协议是TCP/IP协议族中最核心的部分,它的作用是在两台主机之间传输数据,所有上层协议的数据(HTTP、TCP、UDP等)都会被封装在一个个的IP数据包中被发送到网络上。

  • ICMP
    ICMP全称为互联网控制报文协议,它常用于传递错误信息,ICMP协议是IP层的一部分,它的报文也是通过IP数据包来传输的。

  • TTL
    TTL(time-to-live)是IP数据包中的一个字段,它指定了数据包最多能经过几次路由器。从我们源主机发出去的数据包在到达目的主机的路上要经过许多个路由器的转发,在发送数据包的时候源主机会设置一个TTL的值,每经过一个路由器TTL就会被减去一,当TTL为0的时候该数据包会被直接丢弃(不再继续转发),并发送一个超时ICMP报文给源主机。

具体到traceroute的实现细节上,有两种不同的方案:

基于UDP实现

在基于UDP的实现中,客户端发送的数据包是通过UDP协议来传输的,使用了一个大于30000的端口号,服务器在收到这个数据包的时候会返回一个端口不可达的ICMP错误信息,客户端通过判断收到的错误信息是TTL超时还是端口不可达来判断数据包是否到达目标主机,具体的流程如图:

基于UDP实现的traceroute

  1. 客户端发送一个TTL为1,端口号大于30000的UDP数据包,到达第一站路由器之后TTL被减去1,返回了一个超时的ICMP数据包,客户端得到第一跳路由器的地址。
  2. 客户端发送一个TTL为2的数据包,在第二跳的路由器节点处超时,得到第二跳路由器的地址。
  3. 客户端发送一个TTL为3的数据包,数据包成功到达目标主机,返回一个端口不可达错误,traceroute结束。

Linux和macOS系统自带了一个traceroute指令,可以结合Wireshark抓包来看看它的实现原理。首先对百度的域名进行traceroute:traceroute www.baidu.com,每一跳默认发送三个数据包,我们会看到下面这样的输出:

对该域名的IP:115.239.210.27进行traceroute,此时Wireshark抓包的结果如下:

注意看红框处的内容,跟第一张图对比,可以看到traceroute程序首先通过UDP协议向目标地址115.239.210.27发送了一个TTL为1的数据包,然后在第一个路由器中TTL超时,返回一个错误类型为Time-to-live exceeded的ICMP数据包,此时我们通过该数据包的源地址可知第一站路由器的地址为10.242.0.1。之后只需要不停增加TTL的值就能得到每一跳的地址了。

然而一直跑下去会发现,traceroute并不能到达目的地,当TTL增加到一定大小之后就一直拿不到返回的数据包了:

其实这个时候数据包已经到达目标服务器了,但是因为安全问题大部分的应用服务器都不提供UDP服务(或者被防火墙挡掉),所以我们拿不到服务器的任何返回,程序就理所当然的认为还没有结束,一直尝试增加数据包的TTL。

目前在网上找到许多开源iOS traceroute实现大多都是基于UDP的方案,实际用起来并不能达到想要的效果,所以我们需要采用另一种方案来实现。

基于ICMP实现

上述方案失败的原因是由于服务器对于UDP数据包的处理,所以在这一种实现中我们不使用UDP协议,而是直接发送一个ICMP回显请求(echo request)数据包,服务器在收到回显请求的时候会向客户端发送一个ICMP回显应答(echo reply)数据包,在这之后的流程还是跟第一种方案一样。这样就避免了我们的traceroute数据包被服务器的防火墙策略墙掉。

采用这种方案的实现流程如下:

  1. 客户端发送一个TTL为1的ICMP请求回显数据包,在第一跳的时候超时并返回一个ICMP超时数据包,得到第一跳的地址。
  2. 客户端发送一个TTL为2的ICMP请求回显数据包,得到第二跳的地址。
  3. 客户端发送一个TTL为3的ICMP请求回显数据包,到达目标主机,目标主机返回一个ICMP回显应答,traceroute结束。

可以看出与第一种实现相比,区别主要在发送的数据包类型以及对于结束的判断上,大体的流程还是一致的。

值得一提的是在Windows系统中也有traceroute程序,它的名字叫做tracerttracert就是用采用这种方法来实现的,感兴趣的话可以自行尝试一下,这里就不再演示了。

实现

这里我们主要讨论基于ICMP的实现,相关的Demo已经上传至github:https://github.com/L-Zephyr/TracerouteDemo.git

采用这种方案时,ICMP数据包的创建、解析、校验都需要我们自己进行,ICMP是封装在IP数据包的数据段中传输的,所以关键在于如何创建和发送ICMP数据,以及接收到返回的数据时如何从IP数据包中将ICMP解析出来:

创建ICMP数据

ICMP数据包头部的格式如下:

其中的类型字段用来表示消息的类型,在Wiki上可以看到所有类型代表的含义。报文中的标识符和序列号由发送端指定,如果这个ICMP报文是一个请求回显的报文(类型为8,代码为0),这两个字段会被原封不动的返回。

根据上图中各个字段的大小可以定义如下类型:

typedef struct ICMPPacket {uint8_t     type; // 类型uint8_t     code; // 类型代码uint16_t    checksum; // 校验码uint16_t    identifier; // IDuint16_t    sequenceNumber; // 序列号// data...
} ICMPPacket;

其中的type字段指定了这个ICMP数据包的类型,是需要重点关注的对象,为此定义一个报文类型的枚举:

// ICMPv4报文类型
typedef enum ICMPv4Type {kICMPv4TypeEchoReply = 0, // 回显应答kICMPv4TypeEchoRequest = 8, // 回显请求kICMPv4TypeTimeOut = 11, // 超时
}ICMPv4Type;

比较麻烦的是校验的计算,这一部分直接使用了苹果官方示例SimplePing中的代码,所涉及到的几个工具方法封装在类型TracerouteCommon中。

在发送数据的时系统会自动加上IP头部不需要自己处理,如此一来我们只需要创建一个ICMPPacket数据包并通过socket发送到目标服务器就可以了。

解析ICMP数据

接下来就是要接收服务器向我们返回的ICMP数据了,我们接收到的是带有IP头部的原始数据,所以必须先进行一些处理将ICMP从IP数据包中提取出来,IP数据包由两部分组成:数据包头部信息部分以及实际的数据部分。下图是IPv4数据包的结构:

一眼看上去是不是感觉很混乱,其实这里面只有用红框圈出来的这这三个字段需要我们关心:版本表示该数据包是IPv4还是IPv6;之前说过ICMP协议是通过IP协议来传输的,如果该数据包传输的是ICMP协议则协议字段会被设置为1;由于IPv4数据包带有可选的选项字段,所以其头部的长度是可变的,此时需要根据首部长度字段来获取具体的数据。

根据上面的结构可以定义类型:

typedef struct IPv4Header {uint8_t versionAndHeaderLength; // 版本和首部长度uint8_t serviceType;uint16_t totalLength; uint16_t identifier;uint16_t flagsAndFragmentOffset;uint8_t timeToLive;uint8_t protocol; // 协议类型,1表示ICMPuint16_t checksum;uint8_t sourceAddress[4];uint8_t destAddress[4];// options...// data...
} IPv4Header;

提取ICMP数据包的方法如下:

+ (ICMPPacket *)unpackICMPv4Packet:(char *)packet len:(int)len {if (len < (sizeof(IPv4Header) + sizeof(ICMPPacket))) {return NULL;}const struct IPv4Header *ipPtr = (const IPv4Header *)packet;if ((ipPtr->versionAndHeaderLength & 0xF0) != 0x40 || // IPv4ipPtr->protocol != 1) { // ICMPreturn NULL;}// 获取IP头部长度size_t ipHeaderLength = (ipPtr->versionAndHeaderLength & 0x0F) * sizeof(uint32_t); if (len < ipHeaderLength + sizeof(ICMPPacket)) {return NULL;}// 返回数据部分的ICMPreturn (ICMPPacket *)((char *)packet + ipHeaderLength);
}

其中出现的如ipPtr->versionAndHeaderLength & 0xF0的判断是因为版本号和首部长度各自只占4个bit,在结构中直接定义了一个1字节的uint8_t类型来表示,所以只能通过位运算符&来获取各自的值。

整体流程

有了上面的两步,剩下的事情就很简单了,下面是整体流程的伪代码:

// 1. 创建一个套接字
int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);// 2. 最多尝试30跳
int ttl = 1;
for (0...30) {// 3. 设置TTL,发送3个ICMP数据包,每一跳都将递增TTLsetsockopt(sock, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl));++ttl;for (0...3) {// 4. 发送并等待返回的数据包sendto(...);recvfrom(...);// 5. 解析数据包,记录数据,成功条件判断ICMPPacket *packet = unpack(...);}
}

socket的类型采用了SOCK_DGRAM,有些小伙伴可能会感到疑惑:用SOCK_DGRAM创建套接字不还是发送UDP数据么?

确实在许多系统的实现中要直接发送ICMP的话需要使用原始套接字(类型为SOCK_RAW),这在iOS系统中是不被允许使用的,但是查阅资料中了解到macOS支持一种使用参数SOCK_DGRAMIPPROTO_ICMP来直接创建ICMP套接字方式,尝试之下果然iOS也支持这种用法。不过在使用中发现了一个问题:使用IPv4套接字的时候接收到的数据包是带有原始IP头部的,而使用IPv6套接字的时候收到的数据包却没有IP头部,这个问题让我比较疑惑,各位大佬如果有对这一块了解的话还望赐教。

总结

Demo中的示例程序已经在模拟器和真机环境经过测试,可以看到,现在Traceroute已经能够正常的工作了:

有些路由器会隐藏的自己的位置,不让ICMP Timeout的消息通过,结果就是在那一跳上始终会显示星号,此外服务器也可以伪造traceroute路径的,不过一般应用服务器也没有理由这么做,所以Traceroute的结果还是能够为网络分析提供一些参考的。

Linux之traceroute命令使用详解—网络故障定位(三)相关推荐

  1. Linux之ping命令使用详解—网络故障定位(六)

    Linux命令有很多,今天跟大家介绍常用的两个命令ping .traceroute命令,按照工具的作用,原理,用法三个维度来理解. 一.ping 1.作用 探测端到端的连通性,包往返时延. 2.原理 ...

  2. Linux之curl命令使用详解—网络故障定位(五)

    前言 该命令设计用于在没有用户交互的情况下工作. curl 是一个工具,用于传输来自服务器或者到服务器的数据.「向服务器传输数据或者获取来自服务器的数据」 可支持的协议有(DICT.FILE.FTP. ...

  3. Linux之telnet命令使用详解—网络故障定位(四)

    前言 什么是Telnet? 对于Telnet的认识,不同的人持有不同的观点,可以把Telnet当成一种通信协议,但是对于入侵者而言,Telnet只是一种远程登录的工具.一旦入侵者与远程主机建立了Tel ...

  4. Linux之dig命令使用详解—网络故障定位(二)

    前言 Linux下解析域名除了使用nslookup之外,开可以使用dig命令来解析域名,dig命令可以得到更多的域名信息.dig 命令主要用来从 DNS 域名服务器查询主机地址信息.dig的全称是 ( ...

  5. linux系统里route -n不起作用,Linux系统中traceroute命令使用详解

    Linux系统中traceroute命令可以追踪到网络数据包的路由途径.下面由学习啦小编为大家整理了linux系统中traceroute命令使用详解,希望对大家有帮助! Linux系统中tracero ...

  6. linux中which命令详解,Linux下which命令使用详解(转)

    我们经常在linux要查找某个文件,但不知道放在哪里了,可以使用下面的一些命令来搜索: which 查看可执行文件的位置. whereis 查看文件的位置. locate 配合数据库查看文件位置. f ...

  7. linux下测试ftp传输,linux下ftp命令使用详解---linux文件传输ftp命令

    linux下ftp命令使用详解---linux文件传输ftp命令 上一篇 / 下一篇  2010-12-18 09:15:35 / 个人分类:Linux ftp(file transfer proto ...

  8. linux bin fuser,Linux中fuser命令用法详解

    描述: fuser可以显示出当前哪个程序在使用磁盘上的某个文件.挂载点.甚至网络端口,并给出程序进程的详细信息. fuser显示使用指定文件或者文件系统的进程ID. 默认情况下每个文件名后面跟一个字母 ...

  9. linux下top命令参数详解

    linux下top命令参数详解 top命令是Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,类似于Windows的任务管理器.下面详细介绍它的使用方法. 内存信息.内容如下: ...

最新文章

  1. iOS标准库中常用数据结构和算法之内存池
  2. python入门经典必备推荐基础教程
  3. 公司网络推广浅析网站想要“久居”首页的方法有哪些?
  4. golang中的文件读写
  5. NOIP2017大爆炸
  6. final阶段团队贡献分分配
  7. 虚拟主机 webdav php,ubuntu 搭建 webdav 文件服务器 及客户端配置 详解
  8. java是一种,java是一种编译程序吗
  9. 在计算机科学中算法指的是,算法 - 为什么斐波纳契数在计算机科学中具有重要意义?...
  10. POJ - 3494
  11. 金山毒霸2011进程合并更新 更顺畅运行电脑
  12. Stemming : one way to normalize 英文单词的标准化
  13. 网络里面如何添加计算机,计算机如何添加网络协议
  14. 在DX12中使用imgui 鼠标响应问题的解决
  15. C++ 几种智能指针的简单实现
  16. VS2015:libcurl静态编译
  17. 华为手机怎么语音服务器,原来华为手机实现文字转语音这么简单!今天才知道,真是绝了...
  18. C++ reverse
  19. 获取文件夹中所有文件清单
  20. 别忘记 DNS 服务器的安全性

热门文章

  1. 建议被降级降薪员工主动辞职?网友炸了!
  2. 皮一皮:直男只想说一句,表白?是表特别白吗?
  3. 支持Dubbo接口文档生成的工具!
  4. 每日一皮:原型还可以啊,怎么上线后就这样了。。。
  5. GitHub被“中介”攻击了?啥是中间人攻击?
  6. 最详细的maven教程,可以收藏!
  7. java 上传视频并播放_java实现视频上传和播放..doc
  8. 自考计算机和行政管理哪个好考,自考行政管理好考吗?自考行政管理都考哪些科目?...
  9. 玩cf出现outofmemory_CF从女主播秀腿到假赛被罚,还能站在电竞上吗?
  10. c++ 动态分配内存