Traceroute(路由追踪)的原理及实现
现实世界中的网络是由无数的计算机和路由器组成的一张的大网,应用的数据包在发送到服务器之前都要经过层层的路由转发。而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超时还是端口不可达来判断数据包是否到达目标主机,具体的流程如图:

- 客户端发送一个TTL为1,端口号大于30000的UDP数据包,到达第一站路由器之后TTL被减去1,返回了一个超时的ICMP数据包,客户端得到第一跳路由器的地址。
- 客户端发送一个TTL为2的数据包,在第二跳的路由器节点处超时,得到第二跳路由器的地址。
- 客户端发送一个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数据包被服务器的防火墙策略墙掉。
采用这种方案的实现流程如下:

- 客户端发送一个TTL为1的ICMP请求回显数据包,在第一跳的时候超时并返回一个ICMP超时数据包,得到第一跳的地址。
- 客户端发送一个TTL为2的ICMP请求回显数据包,得到第二跳的地址。
- 客户端发送一个TTL为3的ICMP请求回显数据包,到达目标主机,目标主机返回一个ICMP回显应答,traceroute结束。
可以看出与第一种实现相比,区别主要在发送的数据包类型以及对于结束的判断上,大体的流程还是一致的。
值得一提的是在Windows系统中也有traceroute程序,它的名字叫做tracert
,tracert
就是用采用这种方法来实现的,感兴趣的话可以自行尝试一下,这里就不再演示了。
实现
这里我们主要讨论基于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; // ID
uint16_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表示ICMP
uint16_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 || // IPv4
ipPtr->protocol != 1) { // ICMP
return NULL;
}
// 获取IP头部长度
size_t ipHeaderLength = (ipPtr->versionAndHeaderLength & 0x0F) * sizeof(uint32_t);
if (len < ipHeaderLength + sizeof(ICMPPacket)) {
return NULL;
}
// 返回数据部分的ICMP
return (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数据包,每一跳都将递增TTL
setsockopt(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_DGRAM
和IPPROTO_ICMP
来直接创建ICMP套接字方式,尝试之下果然iOS也支持这种用法。不过在使用中发现了一个问题:使用IPv4套接字的时候接收到的数据包是带有原始IP头部的,而使用IPv6套接字的时候收到的数据包却没有IP头部,这个问题让我比较疑惑,各位大佬如果有对这一块了解的话还望赐教。
总结
Demo中的示例程序已经在模拟器和真机环境经过测试,可以看到,现在Traceroute已经能够正常的工作了:

有些路由器会隐藏的自己的位置,不让ICMP Timeout的消息通过,结果就是在那一跳上始终会显示星号,此外服务器也可以伪造traceroute路径的,不过一般应用服务器也没有理由这么做,所以Traceroute的结果还是能够为网络分析提供一些参考的。
Traceroute(路由追踪)的原理及实现的更多相关文章
- Traceroute(路由追踪)
Traceroute的实现有两种: 基于UDP实现 traceroute向目的主机发送一系列普通IP数据报,但每个数据报携带一个不可达UDP端口号的UDP报文,TTL分别为1,2,3...TTL过期时 ...
- 10.10 traceroute:追踪数据传输路由状况
traceroute命令 用于显示网络数据包传输到指定主机的路径信息,追踪数据传输路由状况.默认数据包大小是60字节(IPv4)或80字节(IPv6),用户可另行设置.它与Windows下的trace ...
- NET/ASP.NET Routing路由(深入解析路由系统架构原理)(转载)
NET/ASP.NET Routing路由(深入解析路由系统架构原理) 阅读目录: 1.开篇介绍 2.ASP.NET Routing 路由对象模型的位置 3.ASP.NET Routing 路由对象模 ...
- HA主备路由模式的原理 + HA和负载均衡的区别
HA主备路由模式的原理 HA是High Availability缩写,即高可用性 ,可防止网络中由于单个防火墙的设备故障或网络故障导致网络中断,保证网络服务的连续性和安全强度.目前,ha功能已经 ...
- PHP路由技术的原理与实践
0x00 路由实现原理 用户通过指定的URL范式对后台进行訪问.URL路由处理类进行处理后.转发到逻辑处理类,逻辑处理类将请求结果返回给用户. 约定URL范式和规则 约定一套自己喜欢的,对搜索引擎友好 ...
- 一文详解|Go 分布式链路追踪实现原理
在分布式.微服务架构下,应用一个请求往往贯穿多个分布式服务,这给应用的故障排查.性能优化带来新的挑战.分布式链路追踪作为解决分布式应用可观测问题的重要技术,愈发成为分布式应用不可缺少的基础设施.本文将 ...
- 解读Go分布式链路追踪实现原理
摘要:本文将详细介绍分布式链路的核心概念.架构原理和相关开源标准协议,并分享我们在实现无侵入 Go 采集 Sdk 方面的一些实践. 本文分享自华为云社区<一文详解|Go 分布式链路追踪实现原理& ...
- 路由追踪:traceroute/tcptraceroute
一.工作原理 traceroute:IP路由过程中对数据包TTL(Time to Live,存活时间)进行处理.当路由器收到一个IP包时,会修改IP包的TTL(及由此造成的头部检验和checksum变 ...
- tracteroute路由追踪
traceroute 跟踪数据包到达网络主机所经过的路由工具: 是用来发出数据包的主机到目标主机之间所经过的网关的工具.traceroute 的原理是试图以最小的TTL发出探测包来跟踪数据包到达目标主 ...
随机推荐
- Android 常用动画之RotateAnimation
前两天接到任务做一个UI,有用到动画,于是抽空看了下Android动画相关知识. Android Animation共有四大类型,分别是 Alpha 透明度动画 Scale 大小伸 ...
- 排序算法的JS实现
排序算法是基础算法,虽然关键在于算法的思想而不是语言,但还是决定借助算法可视化工具结合自己常用的语言实现一下 1.冒泡排序 基本思路:依次比较两两相邻的两个数,前面数比后面数小,不变.前面数比后面数大 ...
- poj 2236 加点 然后判断某两点是否连通
题目大意:给你N台电脑,从1-N.一个数字,表示两台计算机的最大通信距离,超过这个距离就无法进行通信.然后分别告诉这些电脑的坐标,接下来有两种操作,第一种O表示这点电脑修好,第二种S,表示测试这两台电 ...
- 【LOJ】#2533. 「CQOI2018」交错序列
题解 有毒吧 这题\(O(n)\)过不去 非得写\(O((a + b)^3\log n)\)的矩乘,同样很卡常 把\(x\)换成\(n - y\) 我们拆完式子发现是这样的 \(\sum_{i = 0 ...
- 【AtCoder】AGC026 题解
A - Colorful Slimes 2 找相同颜色的一段,然后答案加上段长除2下取整 代码 #include <iostream> #include <cstdio> us ...
- 最受欢迎编程语言又是谁?C语言居首,大数据赢了
C语言占据榜首,但大数据类是最大赢家. IEEE Spectrum的第三次“最受欢迎编程语言”交互式排行榜新鲜出炉.因为不可能顾及到每一个程序员的想法,Spectrum使用多样化.可交互的的指标权重来 ...
- 关于函数getline()(简单注意事项,不懂你怼我!!!)
关于getline()函数好使但是有毒: 有两种操作需要进行特殊处理: First: #include <iostream>#include <cstring>#include ...
- java实现最小堆
1.堆:通常通过二叉堆,实为二叉树的一种,分为最小堆和最大堆,具有以下性质: 任意节点小于它的所有后裔,最小元在堆的根上. 堆总是一棵完全树 将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小 ...
- mongodb spring 配置文件
在使用spring-data-mongodb中,需要配置spring文件,如下: mongodb.xml <?xml version="1.0" encoding=" ...
- Kali Linux 2017.3发布了
Kali Linux 2017.3发布了 Kali Linux官方在11月21日发布Kali Linux 2017的第三个版本2017.3.这次发布变化相对不大,主要是设置面板风格发生改变,增加少量 ...