ICMP 是用来对网络状况进行反馈的协议,可以用来侦测网络状态或检测网络错误。
ICMP(Internet Control Message Protocol)因特网控制报文协议。它是 IPv4 协议族中的一个子协议,用于 IP 主机、路由器之间传递控制消息。控制消息是网络是否畅通、主机是否可达、路由是否可用等网络本身的消息。这些控制消息虽然不传输用户数据,但是对于用户数据的传递起着重要的作用。
ICMP 协议是一种面向无连接的协议,用于传输出错报告控制信息,它是一个非常重要的协议,对于网络安全具有极其重要的意义。ICMP 属于网络层协议,主要用于在主机与路由器之间传递控制信息,包括报告错误、交换受限控制和状态信息等。当遇到 IP 数据无法访问目标、IP 路由器无法按当前的传输速率转发数据包等情况时,会自动发送 ICMP 消息。
ICMP 是 TCP/IP 模型中网络层的重要成员,与 IP 协议、ARP 协议、RARP 协议及 IGMP 协议共同构成 TCP/IP 模型中的网络层。ping 和 tracert 是两个常用网络管理命令,ping 用来测试网络可达性,tracert 用来显示到达目的主机的路径。ping 和 tracert 都利用 ICMP 协议来实现网络功能,它们是把网络协议应用到日常网络管理的典型实例。
从技术角度来说,ICMP 就是一个“错误侦测与回报机制”,其目的就是让我们能够检测网络的连线状况﹐也能确保连线的准确性。当路由器在处理一个数据包的过程中发生了意外,可以通过 ICMP 向数据包的源端报告有关事件。
其功能主要有:侦测远端主机是否存在,建立及维护路由资料,重导资料传送路径(ICMP 重定向),资料流量控制。ICMP 在沟通之中,主要是透过不同的类别(Type)与代码(Code)让机器来识别不同的连线状况。
ICMP 协议大致可以分为两类,一种是查询报文,一种是差错报文。其中查询报文有以下几种用途:
而差错报文则产生在数据传送发生错误的时候。
ICMP 报告无法传送数据报的错误,且无法帮助对这些错误进行疑难解答。例如 IPv4 不能将数据报传送到目标主机,路由器或目标主机上的 ICMP 会向主机发送一条“无法到达目标”消息。下表为最常见的 ICMP 消息。
ICMP 消息类型 | 用途说明 |
---|---|
回显请求 | Ping 工具通过发送 ICMP 回显消息检查特定节点的 IPv4 连接以排查网络问题,类型值为 0 |
回显应答 | 节点发送回显答复消息响应 ICMP 回显消息,类型值为 8 |
重定向 | 路由器发送“重定向”消息,告诉发送主机到目标 IPv4 地址更好的路由,类型值为 5 |
源抑制 | 路由器发送“源结束”消息,告诉发送主机它们的 IPv4 数据报将被丢弃,因为路由器上发生了拥塞,于是发送主机将以较低的频度发送数据报,类型值为 4 |
超时 | 这个消息有两种用途。当超过 IP 生存期时向发送系统发出错误信息;如果分段的 IP 数据报没有在某种期限内重新组合,这个消息将通知发送系统,类型值为 11 |
无法到达目标 | 路由器和目标主机发送“无法到达目标”消息,通知发送主机它们的数据无法传送,类型值为 3 |
其中无法到达目标消息中可以细分为一下几项
无法到达目标消息 | 说明 |
---|---|
不能访问主机 | 路由器找不到目标的 IPv4 地址的路由时发送“不能访问主机”消息 |
无法访问协议 | 目标 IPv4 节点无法将 IPv4 报头中的“协议”字段与当前使用的 IPv4 客户端协议相匹配时会发送“无法访问协议”消息 |
无法访问端口 | IPv4 节点在 UDP 报头中的“目标端口”字段与使用该 UDP 端口的应用程序相匹配时发送“无法访问端口”消息 |
需要分段但设置了 DF | 当必须分段但发送节点在 IPv4 报头中设置了“不分段(DF)”标志时,IPv4 路由器会发送“需要分段但设置了 DF”消息 |
ICMP 协议只是试图报告错误,并对特定的情况提供反馈,但最终并没有使 IPv4 成为一个可靠的协议。ICMP 消息是以未确认的 IPv4 数据报传送的,它们自己也不可靠。
ICMP 报文包含在 IP 数据报中,IP 报头在 ICMP 报文的最前面。一个 ICMP 报文包括 IP 报头(至少 20 字节)、ICMP 报头(至少八字节)和 ICMP 报文(属于 ICMP 报文的数据部分)。当 IP 报头中的协议字段值为 1 时,就说明这是一个 ICMP 报文。
ICMP 报头如下图所示:
各字段说明:
我们日常进行的 Ping 操作中就包括了相应请求(类型字段值为 8)和应答(类型字段值为 0)ICMP 报文。一台主机向一个节点发送一个类型字段值为 8 的 ICMP 报文,如果途中没有异常(如果没有被路由丢弃,目标不回应 ICMP 或者传输失败),则目标返回类型字段值为 0 的 ICMP 报文,说明这台主机存在。
这三种报文的格式是一样的。目标不可到达报文(类型值为 3)在路由器或者主机不能传递数据时使用。例如我们要连接对方一个不存在的系统端口(端口号小于 1024)时,将返回类型字段值 3、代码字段值为 3 的 ICMP 报文。
常见的不可到达类型还有网络不可到达(代码字段值为 0)、主机不可达到(代码字段值为 1)、协议不可到达(代码字段值为 2)等等。
源抑制报文(类型字段值为 4,代码字段值为 0)则充当一个控制流量的角色,通知主机减少数据报流量。由于 ICMP 没有回复传输的报文,所以只要停止该报文,主机就会逐渐恢复传输速率。最后,无连接方式网络的问题就是数据报回丢失,或者长时间在网络游荡而找不到目标,或者拥塞导致主机在规定的时间内无法重组数据报分段,这时就要触发 ICMP 超时报文的产生。
超时报文(类型字段值为 11)的代码域有两种取值,代码字段值为 0 表示传输超时;代码字段值为 1 表示分段重组超时。
时间戳请求报文(类型值字段 13)和时间戳应答报文(类型值字段 14)用于测试两台主机之间数据报来回一次的传输时间。传输时,主机填充原始时间戳,接受方收到请求后填充接受时间戳后以类型值字段 14 的报文格式返回,发送方计算这个时间差。有些系统不响应这种报文。
ping 可以说是 ICMP 的最著名的应用,当我们某一个网站上不去的时候,通常会 ping 一下这个网站。ping 会回显出一些有用的信息,一般的信息如下:
ping 这个单词源自声纳定位,而这个程序的作用也确实如此,它利用 ICMP 协议包来侦测另一个主机是否可达。原理是用类型码为 0 的 ICMP 发请求,受到请求的主机则用类型码为 8 的 ICMP 回应。
ping 程序来计算间隔时间,并计算有多少个包被送达。用户就可以判断网络大致的情况。我们可以看到,ping 给出来了传送的时间和 TTL 的数据。
ping 还给我们一个看主机到目的主机的路由的机会。这是因为 ICMP 的 ping 请求数据报在每经过一个路由器的时候,路由器都会把自己的 ip 放到该数据报中。而目的主机则会把这个 ip 列表复制到回应 ICMP 数据包中发回给主机。
package main
import (
"fmt"
"net"
"os"
)
func checkSum(msg []byte) uint16 {
sum := 0
len := len(msg)
for i := 0; i < len-1; i += 2 {
sum += int(msg[i])*256 + int(msg[i+1])
}
if len%2 == 1 {
sum += int(msg[len-1]) * 256 // notice here,why *256?
}
sum = (sum >> 16) + (sum & 0xffff)
sum += (sum >> 16)
var answer uint16 = uint16(^sum)
return answer
}
func checkError(err error) {
if err != nil {
fmt.Fprint(os.Stderr, "Fatal error:", err.Error())
os.Exit(1)
}
}
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "host")
os.Exit(1)
}
service := os.Args[1]
conn, err := net.Dial("ip4:icmp", service)
checkError(err)
var msg [512]byte
msg[0] = 8
msg[1] = 0
msg[2] = 0
msg[3] = 0
msg[4] = 0
msg[5] = 13
msg[6] = 0
msg[7] = 37
msg[8] = 99
len := 9
check := checkSum(msg[0:len])
msg[2] = byte(check >> 8)
msg[3] = byte(check & 0xff)
fmt.Println(msg[0:len])
_, err = conn.Write(msg[0:len])
checkError(err)
_, err = conn.Read(msg[0:])
checkError(err)
fmt.Println(msg[0 : 20+len])
fmt.Println("Got response")
if msg[20+5] == 13 {
fmt.Println("Identifier matches")
}
if msg[20+7] == 37 {
fmt.Println("Sequence matches")
}
if msg[20+8] == 99 {
fmt.Println("Custom data matches")
}
os.Exit(0)
}
运行结果如下:
但是,无论如何 ip 头所能纪录的路由列表是非常的有限。如果要观察路由,我们还是需要使用更好的工具,就是要讲到的 Traceroute(windows 下面的名字叫做 tracert)。
Traceroute 是用来侦测主机到目的主机之间所经路由情况的重要工具,也是最便利的工具。前面说到,尽管 ping 工具也可以进行侦测,但是因为 ip 头的限制,ping 不能完全的记录下所经过的路由器。所以 Traceroute 正好就填补了这个缺憾。
Traceroute 的原理是非常有意思的,它接受到目的主机的 IP 后,首先给目的主机发送一个 TTL=1(还记得 TTL 是什么吗?)的 UDP(后面就知道 UDP 是什么了)数据包,而经过的第一个路由器收到这个数据包以后,就自动把 TTL 减 1,而 TTL 变为 0 以后,路由器就把这个包给抛弃了,并同时产生一个主机不可达的 ICMP 数据报给主机。
主机收到这个数据报以后再发一个 TTL=2 的 UDP 数据报给目的主机,然后刺激第二个路由器给主机发 ICMP 数据报。如此往复直到到达目的主机,这样 traceroute 就拿到了所有的路由器 ip。从而避开了 ip 头只能记录有限路由 IP 的问题。
TCP 和 UDP 协议有一个端口号定义,而普通的网络程序只监控少数的几个号码较小的端口,比如说 80、23 等等。而 traceroute 发送的是端口号 >30000 的 UDP 报,所以到达目的主机的时候,目的主机只能发送一个端口不可达的 ICMP 数据报给主机。