本文转载自微信公众号「飞天小牛肉」,作者飞天小牛肉。转载本文请联系飞天小牛肉公众号。
什么是 ICMP 协议
关于这点我们在 IP 协议那篇文章中提过一嘴,IP 协议作为一种提供不可靠数据交付的网络层协议,在传输的过程中,其 IP 数据报可能会发生丢失、重复、延迟和乱序等各种情况, 但是 IP 协议对这些糟糕的情况并不拥有有效的检测和弥补措施,当然更不会将这些结果通知收发双方。
为此,鉴于上述原因,我们在构建 IP 网络时,就需要特别注意两点:
- 确认网络是否能够正常工作
- 即使诊断出现异常时的原因所在
于是,网际控制报文协议(Internet Control Message Protocol,ICMP)出现了。
形象来说,IP 协议就好像一个将军,而 ICMP 协议就是他手下的情报员。将军运筹帷幄于千里之外,而在前线浴血奋战的士卒们伤亡当然也在所难免。
那无法亲临前线的将军最起码要知道两件事情:第一点,我的士卒们是不是按照我指引的方向在前进着,别一阵猛冲发现冲错了地方;第二点,我的士卒们伤亡多少,被什么所伤,了解了己方伤亡的原因才好做下一步的战略部署,总不能死了个不明白。不必多说,这就是情报员 ICMP 该做的事情了。
当然了,上述只是打个比方,可能不是很严谨,各位知道什么意思就行,不必过于吹毛求疵。
这里我们再用学术点的语言来总结下,ICMP 的主要功能有如下两点:
1)确认 IP 数据报是否成功送达目标地址
2)如果某个 IP 数据报因为某种原因未能正常到达目的地,则由 ICMP 负责通知具体的原因
ICMP 报文初探
具体的出错原因是 ICMP 协议负责通知的,这个通知的学名就是 ICMP 报文,那么 ICMP 报文是由发送方发送方发出的还是由接收方发出的呢?
都不是。
ICMP 报文是由路由器发出来的。
举个例子:主机 A 在不知情的情况下向主机 B 发送了数据包,而主机 B 正在呼呼大睡。主机 A 和主机 B 不在同一个局部网内,假设它俩之间会经过两个路由器,看下图:
众所周知,除了 IP 地址我们还需要 MAC 地址才能确保数据包精准的找到传送方向,因此,路由器 2 为了知道主机 B 的 MAC 地址,它会广播一个 ARP 请求报文,希望获取到主机 B 的 MAC 地址,而主机 B 都关机了自然也就无法应答这个请求报文了。
为此,路由器 2 会一遍又一遍的重新发送着 ARP 请求报文,在多次无果后,路由器 2 就会返回一个 ICMP Destination Unreachable 的包给主机 A(关于 ICMP 报文类型下文会讲),通知主机 A,非常遗憾,您发往主机 B 的包未能成功抵达。
那么,ICMP 报文具体是怎么传输给主机 A 的呢?
这个很简单,TCP/UDP 报文是怎么传输的,ICMP 报文就怎么传输。
也就是说,真正的数据首先会被加上 ICMP 首部,封装成 ICMP 报文,然后被 IP 协议封装成 IP 数据报进行明文传输,由 IP 协议指定源 IP 地址和目的地址。主机 A 收到数据后会一层一层解封装,从而获得真正的数据得知发生异常的原因,遂大怒一声:蠢货主机 B。
ICMP 报文格式
至此,各位已经知道了,ICMP 报文是被封装在 IP 数据报里面的,我们来看看下图:
额,这里好像没啥好说的,上图画的很 Nice ,是我之前考研的时候看 B 站上的王道视频截下来的,各位看明白上图,了解 ICMP 报头有哪些东西,知道类型和代码这两个字段很重要就好了,尤其是类型这个,接下来我们先重点讲它。
ICMP 报文类型
上文提到了 ICMP Destination Unreachable,也就是目标不可达的 ICMP 报文。
ICMP 报文类型大体上可以分为两种,差错报文和询问报文,解释一下:
所谓查询报文就是,用于主机进行诊断的查询消息。
这么学术性的文字可能不是很好理解,这样,咱形象来说,查询报文其实和通信异常没啥关系,查询报文就好比将军率领着千军万马来到了一片寂静的峡谷,正是一个容易被埋伏的地方,将军不敢贸然前进,于是派遣几个情报员前去探明敌情,一有动静立马回报。
常见的 ICMP 查询报文类型有以下几种:
- 回送应答(Echo Reply),对应 ICMP 报文首部类型字段的值:0
- 回送请求(Echo Request),对应 ICMP 报文首部类型字段的值:8
而差错报文就是,用于通知主机出错的原因。显然,ICMP 差错报告报文是伴随着出错数据产生的。一旦 IP 协议发现某个 IP 数据报出错了,首先就会毅然地丢弃出错的这个 IP 数据报,然后发送 ICMP 差错报文。
常见的 ICMP 差错报文类型有以下几种:
- 目标不可达(Destination Unreachable),对应 ICMP 报文首部类型字段的值:3
- 原点抑制(Source Quench),对应 ICMP 报文首部类型字段的值:4
- 重定向或改变路由(Redirect),对应 ICMP 报文首部类型字段的值:5
- 超时(Time Exceeded),对应 ICMP 报文首部类型字段的值:11
下面详细解释一下这几个常见的 ICMP 报文类型。
ICMP 回送消息(类型 0、8)
用于进行通信的主机或路由器之间,判断所发送的数据包是否已经成功到达对端的一种消息。
可以向对端主机发送 ICMP 回送请求的消息(Echo Request,类型 8),也可以接收对端主机发回来的 ICMP 回送应答消息(Echo Reply,类型 0)
图片
我们常用的 ping 命令就是基于 ICMP 回送消息实现的。
ping 这个单词源自声纳定位,而这个命令的作用也确实如此,它发送类型为 0 的 ICMP Echo Request 消息,收到请求的主机则用类型为 8 的 ICMP Echo Reply 消息进行回应。ping 就会计算发送 Requenst 和接收到 Reply 的消息间隔时间,并计算有多少个包被送达,丢失了多少个包等。用户就可以据此判断网络大致的情况。
如下图我们来 ping 一下 Github:
ping 也并不是啥事也没做,它在 ICMP 报文格式中又添加了两个字段:标识符和序号。这俩其实很好理解:
1)标识符用来区分是哪个应用程序发 ICMP 包。
形象来说,将军派出了两个情报员,一个用来是了解战况的,一个是用来搬救兵的,那总得有个标识区分这俩情报员吧。标识符就是干这事的。最容易想到的能作为标识符的东西,想来也不用我多嘴吧,就是进程的 PID。
2)序号用来确认网络包是否有丢失。
形象来说,将军派出了 10 个情报员,给每个情报员都编个号。这样,如果派出去 10 个,回来 10 个,就说明前方战况不错;如果派出去 10 个,一个也没回来或者就回来 1 个,说明情况不妙啊。
ICMP 目标不可达消息(类型 3)
路由器无法将 IP 数据报发送给目标地址时,会给发送端主机返回一个目标不可达(Destination Unreachable Message)的 ICMP 消息。
那目标不可达有多种可能的原因,比如说网络问题、目标主机问题等等,所以这个目标不可达消息还需要指明不可达的具体原因,这个具体原因就记录在 ICMP 报头的代码字段。
那么这里我们仍然以行军打战的例子来看看常见的目标不可达类型的代码有哪些:
1)前方战事吃紧,将军(主机 A)派了一队士兵回京城找皇上(主机 B)搬救兵,中途情报员快马加鞭赶到汇报:将军,我们在途中迷失了方向,找不到京城在哪。这就是网络不可达,其代码为 0
2)假设士兵们成功回到京城,但是皇上出城了,不在京城,朝廷百官也不敢私自同意出兵救援。这就是主机不可达,到了地方却没找到人,其代码为 1
3)假设士兵成功找到了京城,但是由于将士们常年在外征战,守城的年轻护卫们已经不认识这些威名赫赫的将士们了,所以需要进城口令证明身份,但是久经沙场的将士们一时半会想不起来这些东西,遂无法进城。这就是协议不可达,其代码为 2
4)假设士兵们成功进了城,也成功面见了圣上,但是皇上却说密侦司告诉他的情报和你们说的不一样,你们说你们需要的是救兵,而我得到的消息是你们只需要粮草。这就是端口不可达,其代码为 3
5)假设士兵们成功求得了救兵,并且获得了火器十余箱,但是中途山路狭窄,装火器的马车太大过不去,为此需要换小一点的马车,每个马车装一点,但是由于火器技术尚不成熟,考虑安全问题,将军早就严令禁止把火器分装。于是乎,浩浩荡荡的援兵阻塞在了狭窄的山路。这就是需要进行分片但设置了不分片位,其代码为 4
ICMP 重定向消息(类型 5)
说到这个,我们先要明白 IP 协议或者网络层的职责是什么,就是选择合适的网间路由和交换结点, 确保数据的及时传送。
为此,我们总是倾向于基于最短最优的路径进行传输。
那么如果路由器发现发送端主机使用了某个不是最优的路径发送数据,他就会返回一个 ICMP 重定向消息(ICMP Redirect Message)给这个主机,并且,在这个消息中包含了最优的路由信息和源数据。
写上瘾了哈哈,举个例子:将军得知 ICMP 的情报后震怒,派出去搬救兵的领队竟然带着十万救兵在绕弯子,乱臣贼子,将军赶紧下令诛杀这个领队并立即走最近的路赶回来。
ICMP 超时消息(类型 11)
IP 包中有一个字段叫做 TTL (Time To Live,生存周期),它的值随着每经过一次路由器就会减 1,直到减到 0 时该 IP 包会被丢弃。
此时,IP 路由器将会发送一个 ICMP 超时消息(ICMP Time Exceeded Message)给发送端主机,并通知该包已被丢弃。
形象来说,就是将军派出去的搬救兵的那队人马苦于找不到京城的方向,路途开始带上的只够三天的粮草断尽,全队饮恨而死。
设置 IP 包生存周期的主要目的,是为了在路由控制遇到问题发生循环状况时,避免 IP 包无休止地在网络上被转发。
ICMP 的应用
其中一个应用 ping 命令我们已经说过了,它是基于 ICMP 查询报文的。
还有一个命令,是基于 ICMP 差错报文的,在 Linux 下这条命令是 traceroute,在 Windows 是 tracert。
大家可能会觉得 ICMP 差错报文是只有在通信异常的时候才会生成,其实不然,traceroute 命令就是一个例外,它会使用 ICMP 的规则,故意制造一些能够产生异常的场景。
traceroute 命令有两大作用:
1)故意设置特殊的 TTL,来追踪去往目的主机上沿途经过的路由器。
具体来说,就是发送端主机会不断的向接收端主机发送 UDP 报文,UDP 报文被封装成 IP 数据报,同时将 TTL 从 1 开始按照顺序递增。
比如说,将 TTL 设置 为 1,那么遇到第一个路由器的时候,这个 IP 数据报就会被丢弃,接着返回 ICMP 差错报文,类型是 ICMP 超时消息。
接下来将 TTL 设置为 2,第一个路由器过了,遇到第二个路由器时这个 IP 数据报就会被丢弃,接着返回ICMP 差错报文。
......
这样,traceroute 就拿到了所有路由器 IP。
那到这里其实还有一个问题,怎么知道数据到底有没有到达目的主机呢?
traceroute 是基于 UDP 传输的,那自然是需要指定一个端口号的,traceroute 会选择一个不可能的值作为 UDP 的端口号。
这样,当数据到达目的主机时,就会发现端口对不上,于是路由器会产生一份 ICMP 目标不可达消息,其代码是 3,即端口不可达。
当发送端主机接收这份端口不可达的 ICMP 报文时,就知道目的主机成功收到了数据。
2)故意设置不分片,从而确定路径的最大传输单元 MTU。
某些情况下我们并不知道路径的 MTU 大小,所以我们需要某种手段去获取 MTU,才能控制发送的数据包的大小。
发送端主机要做的工作很简单,就是像往常一样发送 IP 数据报,但是将 IP 首部的分片禁止标志位置为 1。
这样,如果 IP 数据报的长度超过了 MTU,该数据报会被路由器直接丢弃,并且给发送端主机发送 ICMP 目标不可达消息,其代码为 4,即需要进行分片但设置了不分片位
这样,发送端主机每次收到 ICMP 需要进行分片但设置了不分片位消息时就减小 IP 数据报的长度,直到顺利到达目标主机。