Ping
ping 是一个非常常用的网络工具,常常被用来测试服务器的连通性,例如我们测试 www.google.com
:
|
|
ping 的工作原理如同声纳,通过发送 ICMP 数据包给目标主机并接收目标主机的应答来计算往返时间差,从而衡量本机到目标主机的网络质量,听起来听简单的对吧,那么第一个写出这个在当今如此通用的工具的人是谁呢?就是下面这位老哥,看着就很有精神(大声)!
迈克·缪斯(Mike Muuss)于 1983 年在美国陆军弹道研究实验室工作时编写了这个工具,当时他编写这个工具只是为了解决实验室的网络问题,他一定怎么也想不到如今几乎世界上所有计算机都有这个工具,不过不幸的是他在 2000 年因车祸去世了。
介绍完背景,是时候开始动手实现了,这次的目标是实现一个最为简单的 ping,测试本机到目标主机的网络延时。
实现过程
ICMP
ping 通过收发 ICMP 数据包来计算网络延时,因此我们首先要知道 ICMP 数据包的构成,它的结构如下图所示:
Type 和 Code 用于控制 ICMP 的行为,具体可参考 Control messages,在 ping 的工作流程中,常用的 Type 有 0, 3, 4, 5, 8, 11,当我们发起 echo 请求时用的就是 8 – Echo Request
,而在正常情况下目标主机返回的 ICMP 类型为 0 – Echo Reply
,其他类型就属于异常处理的范畴,在代码中我们可以根据接收的 ICMP 数据包中的 Type 判断目标主机是否正确响应了我们的请求。
Checksum 顾名思义是消息校验和,它用于校验消息的完整性,它的值通过 RFC 1071 - Computing the Internet Checksum 中定义的计算方法得到,对于 ICMP 而言,我们需要将整个 ICMP 数据包纳入计算。
首先将数据按照 2 字节(16 位)进行分割并累加,保留进位,因此在代码实现上我们需要 32 位的变量去存放累加的结果,接下来我们处理数据是奇数字节的情况,我们将最后的 8 位(1 字节)后补 0 到 16 位,再进行(高位)累加,此时我们得到了累加和,之后我们将累加和的进位分离,并将其作为低位加到分离后的后 16 位累加和上,例如将 2479c
的进位分离并相加得到 2 + 479c = 479e
,对最后的结果取反,即可得到 Checksum,代码实现如下:
|
|
由于 Checksum 的计算需包含整个 ICMP 数据包,因此我们在计算前需要将 ICMP 包的其他部分全部填写完毕,Type 我们填写 ICMP_ECHO
,Code 我们填写 0
,Checksum 我们也填写 0
,除了这 3 个字段之外,对于 ping 来说我们还需要填写 id 与 seq,id 通常为进程的 pid,用于区分 ICMP 数据包的身份,seq 为数据包的序号,用于判断是否丢包,在连续 ping 的情况下 seq 在每发送一次请求后都会加 1,我们可将初始值设定为 0。构造 ping ICMP 数据包的代码如下:
|
|
构造好数据包后我们就可以准备发送了。
Raw socket
想要发送 ICMP 数据包,我们首先需要创建 socket,在系统版本为 Ubuntu 23.10,内核版本为 Linux 6.6.20 的环境中,我们需要使用 Raw socket 才能创建 ICMP 协议的 socket,而创建 Raw socket 需要当前进程拥有 cap_net_raw 的能力,一种方法是使用 sudo 权限,另一种方法是使用 libcap 库来让我们的程序在创建 socket 的过程中暂时拥有 cap_net_raw 权限,但是在当前环境下,即使是使用 libcap 库也还是需要通过 sudo setcap cap_net_raw+ep ./ping_demo
的命令赋予进程这个能力。如果查看 ping 的源码,可以看到在创建 socket 之前使用 libcap 库中的 cap_set_flag()
函数赋予了进程 cap_net_raw 的能力,而在 socket 创建完成后又移除了该能力,这就像是一个安全开关一样,是一种 最小权限原则 的体现。既然都看过 ping 的源码了我们本次实践中也尝试一下使用 libcap 库吧。
在使用 Raw socket 的过程中有个点需要注意,调用 recvfrom
函数时我们收取的是 整个 IP 数据包,在 raw 的 man page 中有这么一段话,对于本次实现来说非常重要:
The IPv4 layer generates an IP header when sending a packet unless the IP_HDRINCL socket option is enabled on the socket. When it is enabled, the packet must contain an IP header. For receiving, the IP header is always included in the packet.
因此获取 Echo reply 时我们需要跳过 20 字节的 IP Header,否则就会发现收取的 ICMP 数据包中的 Type 是 69,而在 ICMP 的定义中根本没有这个 Type,这实际上的 IP Header 的数据。
RTT
往返时延即成功发送请求到成功接收请求的时间间隔,我们可以直接获取这两个时间进行计算,也可以在发送数据包前将当前时间放入数据包中,由于对端回复时会将信息原封不动地发回来,因此我们可以在接收数据时获取到我们发送数据的时间,从而计算时延。本次我们使用第一种方法。
完整实现
总结一下流程,首先我们从参数中获取目标 IP,之后创建 Socket,设置超时时间,构造数据包,发送数据包,获取发送时间,接收数据包,获取接收时间,计算时间差。
|
|
编译运行并与系统的 ping 进行对比,符合预期:
|
|
小结
本文记录了一次实现简单 ping 的过程,通过 IP 地址对主机进行连通性测试,并不包含其他功能。重点在于数据包的构造与 Raw socket 的使用。