首页 > 编程笔记 > Go语言笔记

Go语言ICMP协议:向主机发送消息

ICMP 是用来对网络状况进行反馈的协议,可以用来侦测网络状态或检测网络错误。

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消息类型

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 的报文格式

ICMP 报文包含在 IP 数据报中,IP 报头在 ICMP 报文的最前面。一个 ICMP 报文包括 IP 报头(至少 20 字节)、ICMP 报头(至少八字节)和 ICMP 报文(属于 ICMP 报文的数据部分)。当 IP 报头中的协议字段值为 1 时,就说明这是一个 ICMP 报文。

ICMP 报头如下图所示:

ICMP 报头
图: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 的报文格式返回,发送方计算这个时间差。有些系统不响应这种报文。

ICMP 的应用 - ping

ping 可以说是 ICMP 的最著名的应用,当我们某一个网站上不去的时候,通常会 ping 一下这个网站。ping 会回显出一些有用的信息,一般的信息如下:

Reply from 10.4.24.1: bytes=32 time<1ms TTL=255
Reply from 10.4.24.1: bytes=32 time<1ms TTL=255
Reply from 10.4.24.1: bytes=32 time<1ms TTL=255
Reply from 10.4.24.1: bytes=32 time<1ms TTL=255

Ping statistics for 10.4.24.1:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 0ms, Maximum = 0ms, Average = 0ms

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)
}
运行结果如下:

go run main.go www.xinbaoku.com
[8 0 148 205 0 13 0 37 99]
[69 0 0 29 14 113 0 0 46 1 225 215 61 240 154 116 192 168 3 139 0 0 156 205 0 13 0 37 99]
Got response
Identifier matches
Sequence matches
Custom data matches

但是,无论如何 ip 头所能纪录的路由列表是非常的有限。如果要观察路由,我们还是需要使用更好的工具,就是要讲到的 Traceroute(windows 下面的名字叫做 tracert)。

ICMP 的应用 - Traceroute

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 数据报给主机。

所有教程

优秀文章