引言
在IP网络环境中,我们已经知道:IP数据包的路由并不是完全可靠的,只是尽力而为。IP协议只负责尽可能将你的数据包送达对端,至于能否正确送达并不能完全保证。IP协议自身也并不提供任何的差错报告机制来保证数据递交的有效性,这就会出现一种情况:“我本机发出的数据包,对端是否正确收到?上层是否需要重新发一个过去?许久没和对端通信之后,对端是否还活着并正常提供服务?甚至我自己的网络环境怎么样,能不能和外界正常通信?”这些都是IP协议自身无法解决的问题。
因此我们就需要一个协议来传递整个通信过程中出现的异常行为,以及监控通信是否正常。这个协议就是:ICMP(Internet Control Message Protocol)。
什么是 ICMP?
ICMP 是网络层的一个重要的协议,负责传递网络层的差错和控制信息,不携带用户数据。虽然与IP、ARP协议同属于网络层,但是ICMP其实是依附在IP协议之上的一个更上层的协议。这一点就注定了ICMP的报文形式与ARP的报文形式有本质区别,下文将详细讲述。
这里要注意:ICMP不属于传输层,因此不涉及任何的端口概念。
ICMP有三大功能:IP重定向、差错信息传递(差错报文)、查询控制(查询报文)。
ICMP 报文格式
ICMP报文是基于IP协议为基础构建的,也就是说其实ICMP本质也是一个IP包,ICMP的报文部分就是这个IP包的数据段。只是协议栈内部设计了单独处理这种类型报文的逻辑,这也就是我们说的ICMP协议了。
ICMP和ARP虽然共处于网络层,但是在报文形式上的本质区别是:ARP报文直接封装在以太网帧中,不需要有IP头部,ARP在这种前提下可以认为是完全脱离IP协议而独立的;而ICMP报文封装在IP中,与IP协议是一个上下的依赖关系。
ICMP报文控制头部固定8字节,这8个字节的布局如下表所示:
| 字段 | 长度 | 说明 |
|---|---|---|
| 类型 | 1 字节 | 标识 ICMP 报文的大类 |
| 代码 | 1 字节 | 类型的子分类 |
| 校验和 | 2 字节 | 覆盖整个 ICMP 报文 |
| 其余字段 | 4字节 | 含义取决于类型和代码 |
这里讲讲数据部分取决于类型是什么意思:ICMP这8个字节的头部是固定不可变的。前四个字节(类型、代码、校验和)是完全通用的。后4个字节负载根据不同的ICMP报文类型,会采取不同的措施。
就以我们常见的Ping举例:对于Ping的Echo Request和Reply,ICMP数据负载那4个字节被划分为了——标识符(2字节)+序列号(2字节);而对于重定向功能,后4个字节则被定义为新网关IP地址;而对于一些其它类型,则选择保留(全0)。所以总结就是:这4个字节始终存在于固定头部中,只是具体含义随报文类型而变化。
这里我还需要再说明一个地方:
先来看个示例,我们亲自抓一个Ping包看看。我们以Ping网关为例
1 | |
发现了吗?ping输出了一个56(84)大小的数据,而我们tshark抓到的包却显示98字节捕获。这里讲讲这几个数据为什么会出现差异:
- 56(84):Ping中输出的56,指的是ICMP数据载荷长度,也就是我们上图中数据负载那一部分。而84指的是整个IP数据包的大小。此例中——我们 IP标准头(20字节)+ICMP控制头部(8字节)+ICMP数据负载部分(56字节) = 84字节。
- 98:tshark在抓包时抓的是整个以太网帧的长度,是货真价实跑在物理传输线路上的,因此要算上MAC头部(14字节)。即:84 + 14 = 98字节。这里要注意:FCS校验和在抓包前通常已经被底层网卡硬件给校验完毕后去除了,所以抓不到也不显示。
讲这个示例目的是为了分清ICMP控制数据部分和ICMP数据负载部分。ICMP控制头8字节,不代表整个ICMP报文就只能是8个字节,控制头后面有时还跟得有数据负载的!你比如我们可以以下列命令构造一个1400字节负载的Ping包
1 | |
一定要正确区分控制头的数据部分和控制头之外的ICMP数据负载!
最后这里给出一个类型与代码对应表,后续将以该表为依据讲几个常用的类型:
| 类型 | 名称 | 代码 | 说明 |
|---|---|---|---|
| 0 | Echo Reply | 0 | ping 应答 |
| 3 | Destination Unreachable | 0~15 | 目标不可达 |
| 5 | Redirect | 0~3 | 重定向 |
| 8 | Echo Request | 0 | ping 请求 |
| 11 | Time Exceeded | 0/1 | TTL 超时 |
| 12 | Parameter Problem | 0~2 | 参数错误 |
| 13/14 | Timestamp | 0 | 时间戳(很少使用) |
ICMP 查询报文
ICMP查询机制里面,有多对查询类型报文:比如ping报文(类型8/0)、时间戳(13/14)这些都是ICMP查询机制里面包含的,但是其中很多均已废弃,并不常用。当前ICMP整个查询报文里面最常用的就是ping。
ping报文的作用是检测网络环境的连通性。其请求报文的类型(Type)为8,代码(Code)也为0;ping应答报文的类型为0,代码为0。ping报文ICMP控制头的低4字节数据部分是有使用的,这4个字节被拆分为了标识符(2字节)+序列号(2字节)。
- 标识符:用于区分由哪个进程发起的本次ping通信。
- 序列号:区分这是同一个进程的第几个ping包。
如下图就是一个ping请求与应答的ICMP报文部分
可以看到,对于ping的回复基本上是将请求数据包原封不动送回去。由于类型改变了,因此CheckSum要重算,其它地方全部不动。
再看一个实际的抓包示例:
1 | |
该示例中,标识符就是13872,序列号为1表示是本次ping通信的第一个包。
一次完整的ping通信流程如下:
- 上层触发ping通信。
- 协议栈内部构造ICMP Echo Request报文。
- 将ICMP信息报装入IP数据包中。查路由表决定下一跳,确定IP数据包的发送方向。
- 若目标设备处于同一网段,则对目标IP发起ARP解析以获取其MAC(若ARP表中无该条目或条目已过期,会先挂起发送动作并广播ARP。详见ARP地址解析协议);若处于不同网段,则解析本网段网关的MAC,发给网关。
- 目标设备收到后,构造ICMP Reply并回送。
- 本机收到对方的回复后,计算往返时间。
ICMP 差错报文
差错信息上报机制是ICMP协议的三大功能之一,差错报文专门用来通告网络通信中出现的各种问题。与查询报文不同,差错报文是单向通信!差错报文不能主动请求发送,只能在网络出问题时协议栈内部自行发送。
差错报文主要有以下几个类型:
类型3
| Code | 含义 | 典型场景 |
|---|---|---|
| 0 | 网络不可达 | 没有路由 |
| 1 | 主机不可达 | 主机不存在/关机 |
| 2 | 协议不可达 | 目标不支持该协议 |
| 3 | 端口不可达 | 目标端口没人监听 |
| 4 | 需要分片但 DF 置位 | MTU 问题 |
| 5 | 源路由失败 | — |
| 6 | 目的网络未知 | — |
| 7 | 目的主机未知 | — |
| 9/10 | 网络/主机被管理员禁止 | 防火墙 |
| 13 | 通信被管理员禁止 | 防火墙 |
类型3是平时见得最多的错误类型,例如错误访问了一个未被监听的端口,对端就会发来该类型的报文。这里以一个实际抓包为例:我们在Windows侧的UDP调试助手上发送一个UDP数据报给WSL2中Linux本机的未监听端口8081,同时抓取eth0网卡上的ICMP流量。抓到的数据如下:
1 | |
这里重点来看ICMP部分。我们的UDP数据报从Windows虚拟网卡地址 172.18.224.1 发出,目标为 Linux 本机 172.18.228.235 的 8081 端口。由于Linux本机并未监听该端口,内核直接封装了一个 ICMP Type 3 Code 3(端口不可达) 报文,发送给发送方 172.18.224.1。注意,172.18.224.1 既是WSL的网关,也是Windows宿主机上虚拟网卡的IP,因此该ICMP报文直接到达宿主机,由宿主机的网络栈通知对应的UDP套接字,而无需经过额外的网关转发过程。
类型11:Time Exceed超时
| Code | 含义 |
|---|---|
| 0 | TTL 耗尽(传输中) |
| 1 | 分片重组超时 |
类型11在平时也十分常见,当我们去尝试连接一个并不存在的地址、或在traceroute场景下经常会与该类型的包打交道。这里同样来看一个实际的抓包示例:这里我们尝试去连接一个不同网段的地址172.16.224.1看看会发生什么。
1 | |
执行后,两个终端分别得到如下输出:
1 | |
TTL 0 during transit from ip=10.3.255.38 name=UNKNOWN这里表示我们的包在传输途中TTL已经减为0了,被路由器丢弃。而丢弃该包的路由器正是 10.3.255.38,我们抓到的该包ICMP报文也是该路由器送出给我们的差错报文。
在捕获的原始报文中,我们可以按照格式解析出以下数据
1 | |
这就是一个货真价实的类型11超时ICMP错误报文,由中间某段路由器发来告诉我们本地包已经被丢了,通信失败。
这里简单讲讲为什么会被丢。首先我们在WSL虚拟环境中的本机地址为 172.18.228.235,网关是172.18.224.1,本地网段的网络地址为172.18.224.0/20(子网掩码:255.255.240.0)。我们请求连接的对方是 172.16.224.1,我们用子网掩码去&一下对方地址,得到 172.16.224.0 != 172.18.224.0。
因此我们的SYN请求包先被发给了我们的网关,然后由网关来到我们宿主机Windows本地,Windows本机查自己的ARP表也找不到该地址,只能继续发给Windows的对外网关。公网路由器并不知道172.16.224.1在哪,只能按默认网关路由,于是该包在网络环境中被反复路由,直到TTL耗尽。
路径大致是这样的: WSL –> Windows –> 校园网/企业网 –> 外部网络。
这里需要再多说明一点:看抓包的源码你会发现ICMP控制头之后,还带有不少负载数据。这是因为差错报文不仅需要指明是何种错误,还需要携带原始IP包的IP头部以及原始包的IP数据段至少前8字节。为什么至少8字节?因为在传输层TCP/UDP头部中,前8字节刚好能够拿到源端口、目标端口以及序列号。如下所示:
1 | |
这里数据段多从TCP头部中截取了12个字节。也就是说我们抓的该包中 原始IP头部(20字节) + 原始IP数据段(也就是TCP头)(20字节) = 40字节。多截取数据部分并没有问题,因为协议只规定了至少要8字节。
验算一下:
MAC头14字节 + IP头20字节 + ICMP控制头8字节 + 原始IP头20字节 + 截取的原始IP数据段20字节 = 82字节。正确
ICMP差错报文是有硬性约束的,目的是为了防止差错报文引发互联网环境灾难。
ICMP差错报文如果本身也出错,那么将直接丢弃,不允许再次发回ICMP报文描述这次差错。
这个约束性质在很多网络资料中都会被简化为为了防止出现无限差错风暴而设计的,但是你实际去想一下拓扑会发现无限循环这种网络拓扑实际很难出现,这个特性更多是防止差错链变长占用带宽。试想一下:假如允许使用差错回复差错,那么一个差错报文在长达数十跳的路由环境中假如频繁出现差错,那么这一整条线上会多出数个因此而诞生的ICMP报文占用带宽。
不为广播/多播发ICMP报文。
目标地址是多播或广播的包,出错时不允许发送ICMP报文。源地址是广播/多播地址的包,也不会回复ICMP。
比如我们向本地广播地址发一个ping包,从底层视角来看这个ping包实际是被该网段下每个设备的以太网路硬件都收到了的,但是包被移交上层时协议栈发现该ICMP报文目标竟然是广播,直接就静默丢弃了。
这么做是为了防止攻击者利用广播进行攻击,假如攻击者伪造受害者不断ping广播,如果允许回复的话,那么受害者将瞬间被大量ICMP直接打瘫痪。
不为非分片首片的包发差错。
如果一个包被分片了,只有第一个分片(offset = 0)出错才回差错,后续分片不回。因为分片包只有首片携带了传输层的头部(TCP/UDP),其它分片只携带数据,就算回了差错报文,发送方拿到后也获取不了有效信息反而多个差错报文会干扰发送方,还会多余占用带宽。
重定向 Redirect
除了类型3和类型11这种纯粹的差错上报,ICMP还有一类特殊的报文——重定向(类型5),它不仅能报告异常,还能主动优化主机的路由表。
类型5
| Code | 含义 |
|---|---|
| 0 | 重定向网络 |
| 1 | 重定向主机 |
| 2 | 重定向服务类型 + 网络 |
| 3 | 重定向服务类型 + 主机 |
重定向是ICMP报文的又一大应用之一,独特的点在于能够改变主机的路由表,影响主机的路由选择。
其原理是:当中间路由器发现主机使用的发送路径并不是最优且主机并未开启IP源路径选项时,路由器在向外递送报文的同时会向主机回复一个ICMP重定向报文,这个报文中涵盖了一个新的网关IP。主机收到该报文后,将自己的路由表的网关改为新网关IP,后续流量直接走新网关,不必再经过旧网关多绕一遍,提高通信效率。
例如下图所示:
示例中,我们本机默认网关为R1,当我们本机需要与公网10.12.4.4进行通信时,将数据包发给网关R1(图中蓝色箭头),由R1代发给公网。R1收到后查路由表发现去往10.0.0.0/8更优的下一跳是R2,而且R1与R2与本机都在同一个网段。于是R1在将本次收到的数据包递交给R2的同时也回复一个ICMP重定向给本机,本机收到后将网关改为重定向的IP。后续到10.12.4.4的通信全部直接走R2,就不需要再经过R1绕一遍了。
这里讲讲ICMP的重定向触发条件:
- 首先肯定是主机和路由器双方都必须允许ICMP重定向,这是根本条件。假如双端有任何一端不允许重定向,那么整个动作都无法完成,无论是否满足剩余条件。
- 入接口必须等于出接口:一个包从路由器进来,如果进来与出去的流量都是走同一个接口,那么说明发送方和路由器在同一个网段,且下一跳也在同一个网段,这才可能触发重定向。例如上述示例,假设一个网络包从R1(eth0)进来(主机发给R1),R1查路由表发现这个包得给R2,而R2刚好处于同一网段,eth0可以直达,于是包就从eth0进入,eth0发出。满足条件。
- 下一跳IP和包的源IP处于同一网段:这是肯定的,否则就算你R1给推荐了一个路由,但是与本机不处于同一网段,本机够不到,也是白搭。
- 包没有被源路由:如果发送方IP选项内部已经指明了该包必须按照指定的路径发,那么路由器就不得擅自修改。
- 源包不是路由器自己发的:路由器自己发的包,谈不上告诉别人改路由这个情况。
在实验环境中,重定向很难出现,得在多路由环境下才能观测得到,所以此处不给出示例了。
当然,重定向和ARP的学习行为都是一把双刃剑。既然能改路由表,那就避免不了攻击。常见的重定向攻击便是攻击者伪造一个ICMP重定向报文,使得目标主机将路由改为攻击者,后续发给公网的数据全部被攻击者劫持和窃听。
所以当今互联网环境中,重定向功能基本都被主机默认关闭。关于优化问题现在也有不少可靠的动态路由协议,重定向的使用面已经比较窄了。
ICMP 其它应用
Traceroute
traceroute算是ICMP报文一个十分经典的应用了,主要用于跟踪数据包被路由的每一跳节点。其核心原理就是:靠ICMP类型11的超时报文,让包故意在路径上失活,然后依次暴露路径上的每一跳直到包到达目的端。
这里需要知道,IP头部有一个TTL字段,含义是包的生存时间。TTL每经过一跳就会被中间路由器减1,当TTL减到0时,路由器便会丢弃该包,并向源IP回复一个ICMP 类型11的差错报文(超时)。
因此,我们就可以故意发 TTL = 1、TTL = 2、TTL = 3….的包,每一跳路由器都会向我们回复一个ICMP报文,我们通过解析该报文即可获得对应的中间路由器的地址。每收到一个ICMP就暴露一跳,直到包到达目标。
来看下面一个推演流程,例如我们有以下网络拓扑:
1 | |
- 第一跳:TTL = 1,由本机H->R1。R1收到包后TTL为0,向本机回复ICMP,本机收到ICMP后记录第一跳。
- 第二跳:TTL = 2,本机H->R2。R2收到后TTL为0回ICMP。本机记录R2。
- 第三跳:TTL = 3,同理本机收到ICMP后记录R3。
- 第四跳:TTL = 4,成功到达目标,目标T回复ICMP。本机收到后记录,本轮traceroute探测终止。
关于traceroute的方法,可以自行构造ping包挨个递增TTL或自行构造UDP包访问对端未监听高端口,并自行递增TTL,这是手动的方法。目前也有许多自动化工具用于追踪,比如Linux上的traceroute工具。该工具并不预装,需要使用以下命令进行安装:
1 | |
接下来就以该工具演示trace百度的一个实例:
1 | |
结果如下所示:
1 | |
这里可以看到我们的traceroute默认发的是ICMP。整个过程你会发现虽然确实暴露了不少中间路由节点,但是还有不少的***,这通常是正常现象,表示该跳中间节点并没有响应我们。
上述我们说过,每一跳会暴露对应中间路由,这是在理论推演的情况下进行的。实际上每个中间路由节点的配置不同,并不是每一个中间节点都会回复。这其中原因很多:有的服务器防火墙会拦截ICMP,导致探测包被挡、有的服务器默认不响应。你也可以采用UDP或TCP的形式进行追踪,不过此处不展开详细讨论。
关于Traceroute最后补充一点就是:
不只有ICMP可以进行探测,UDP/TCP都可以。
UDP模式的原理是:本机发UDP包访问对端高端口(这些端口往往不被监听),当UDP包处于中间路由器时,中间路由器回复ICMP超时。当包到达对端时触发ICMP:端口不可达,完成探测。
TCP模式是类似的:本机发起TCP握手连接,指定一个目标端口。当TCP包处于中间路由器时,中间路由器依旧回复ICMP超时。当包到达对端后,如果端口被监听,则对面会回复ACK;若端口未被监听,对面会回复RST。本机收到ACK/RST后即可判断成功抵达对端,本次trace结束。
TCP优点在于能够直连对方的服务端口,防火墙往往会拦截ICMP和UDP高端口访问,但是不会拦截TCP的正常端口访问,因此防火墙穿透能力很高。
PMTUD (路径MTU发现)
这也是一个很经典的应用:网络环境中,有时候底层链路的MTU往往是不同的,你比如以太网最大MTU为1500,假如我们为了效率都按照这个MTU来发包,看似没问题。但是数据包在网络中路由时会经过很多跳,有的跳底层MTU并没有1500,那么这个时候就会发生分片行为,降低效率。甚至当我们设置了不分片时,包会一直被丢弃,导致本机通信失败。
所以向对端发包时,我们需要知道整个路由上最小的一个MTU是多少,并以此为依据进行发送。这个时候就要用到PMTUD技术了。
其原理很简单:我们一开始先发一个大包,并设置不允许分片。大包发出去后若直达对端,说明是理想情况,整条路由路径上没有MTU瓶颈。若在中间某一跳卡住了,被卡住的那台中间路由器会回复ICMP类型3代码4(需要分片但不允许分片),并会携带一个MTU值,我们收到后就把包大小调整成新MTU的大小,继续发送。
循环上述操作,最终包会抵达对端,我们也成功探出了整条路径上最小的MTU大小。当然,这种方法肯定是有局限的,因为十分依赖ICMP报文。假如本地防火墙卡了ICMP、或中间路由器不响应ICMP、再或者ICMP路由时出错了,这些都是有可能的。
如果上层没有防御手段,那么表现就是通信挂掉了。本文只讲MTU的探测原理,所以这里不详细展开。