目录
一、概述
二、ICMP报文格式详解
2.1 什么是ICMP
2.2 ICMP报文格式
2.3 ICMP报文类型
2.4 实际报文举例
三、使用go实现icmp请求以及接收响应内容
一、概述
本文主要旨在学习icmp报文格式,以及通过go语言来实现ICMP发包。
二、ICMP报文格式详解
2.1 什么是ICMP
因特网控制报文协议ICMP(Internet Control Message Protocol)是一个差错报告机制,是TCP/IP协议簇中的一个重要子协议,通常被IP层或更高层协议(TCP或UDP)使用,属于网络层协议,主要用于在IP主机和路由器之间传递控制消息,用于报告主机是否可达、路由是否可用等。这些控制消息虽然并不传输用户数据,但是对于收集各种网络信息、诊断和排除各种网络故障以及用户数据的传递具有至关重要的作用。ICMP的功能是检错而不是纠错。
2.2 ICMP报文格式
CMP报文包含在IP数据报中,属于IP的一个用户,IP头部就在ICMP报文的前面,所以一个ICMP报文包括IP头部、ICMP头部和ICMP报文,IP头部的Protocol值为1就说明这是一个ICMP报文,ICMP头部中的类型(Type)域用于说明ICMP报文的作用及格式,此外还有一个代码(Code)域用于详细说明某种ICMP报文的类型,所有数据都在ICMP头部后面
- type:类型,1字节,报文类型,用来标识报文
- code:代码,1字节,提供报文类型的进一步信息
- checksum:校验和,2字节,使用和IP相同的加法校验和算法,但是icmp校验仅覆盖ICMP报文
- Message body:数据部分,长度可变,字段的长度的和内容,取决于消息的类型和代码
2.3 ICMP报文类型
1、类型比对表
type | code | 描述 | 查询/差错 |
0--Echo(应答)响应 | 0 | Echo Reply -- 回显应答(Ping应答) | 查询 |
3--目的不可达 | 0 | Network Unreachable -- 网络不可达 | 差错 |
1 | Host Unreachable -- 主机不可达 | 差错 | |
2 | Protocol Unreachable --协议不可达 | 差错 | |
3 | Port Unreachable --端口不可达 | 差错 | |
4 | Fragmentation needed but no frag. bit set--要求分段并设置DF flag标志报文 | 差错 | |
5 | Source routing failed --源路由失败报文 | 差错 | |
6 | Destination network unknown --目的网络未知 | 差错 | |
7 | Destination host unknown --目的主机未知 | 差错 | |
8 | Source host isolated(obsolete)--源主机被隔离(作废不用) | 差错 | |
9 | Destination network administratively prohibited -- 目的网络被强制禁止 | 差错 | |
10 | Destination host administratively prohibited --目的主机被强制禁止 | 差错 | |
11 | Network unreachable for TOS --对特定的TOS网络不可达报文 | 差错 | |
12 | Host unreachable for TOS --对特定的TOS主机不可达报文 | 差错 | |
13 | Communiation administratively prohibited by filtering --由于过滤 网络流量被禁止报文 | 差错 | |
14 | Host precedence violation --主机越权报文 | 差错 | |
15 | Precedence cutoff ineffect --优先权终止生效报文 | 差错 | |
4--流量控制 | 0 | Source quench --源端被关闭(基本流控制) | 差错 |
5--重定向 | 0 | Redirect for network --对网络重定向 | 差错 |
1 | Redirect for host --主机重定向 | 差错 | |
2 | Redirect for TOS and network --对服务类型和网络重定向 | 差错 | |
3 | Redirect for TOS and host --对服务类型和主机重定向 | 差错 | |
8--Echo请求 | 0 | Echo request -- 回显请求(ping请求) | 查询 |
9-路由器通告 | 0 | Router advertisement --路由器通告 | 查询 |
10--路由器请求 | 0 | Route solicitation --路由器的发现/选择/请求报文 | 查询 |
11--ICMP超时 | 0 | TTL equals 0 during transit --传输期间生存时间为0 | 差错 |
1 | TTL equals 0 during reassembly --在数据报组装期间生存时间为0 | 差错 | |
12--参数问题 | 0 | IP header bad(catchall error) --坏的IP首部(包括各种差错) | 差错 |
1 | Required options missing --缺少必须的选项 | 差错 | |
2 | 不支持的长度报文 | 差错 | |
13--时间戳请求 | 0 | Timestamp request(obsolete) --时间戳请求(作废不用) | 查询 |
14--时间戳应答 | Timestamp reply(obsolete) --时间戳应答(作废不用) | 查询 | |
15--信息请求 | Information request(obsolete) --信息请求(作废不用) | 查询 | |
16--信息应答 | 0 | Information reply(obsolete) --信息应答(作废不用) | 查询 |
17--掩码请求 | 0 | Address mask request --地址掩码请求 | 查询 |
18--掩码应答 | 0 | Address mask reply --地址掩码应答 | 查询 |
2、ICMP分类
- 差错报文
①目的不可达
目的不可达的类型字段值为3,代码字段有为0-15,也就是说若将目的不可达的ICMP报文再做一个细分,会将目的不可达的原因分为16种,并用不同ICMP差错报文进行表示。
- code=0;代表着网络不可达,出现这个ICMP差错报文,就代表着报文在路由过程的时候出现了问题,比如报文的目的网络在路由器上没有相应的条目,于是该路由器就回送网络不可达的报文
- code=1;代表主机不可达,这个报文的来源一般是目的主机所处的网关发送的,因为目的主机所处的网关没有找到对应的目的主机的IP地址,而无法转交该数据报文,所以将数据报文丢弃并回送该ICMP差错报文。
- code=2;代表着协议不可达,这就说明数据交互的双方在协议上的出现了问题。
- code=3;代表着端口不可达,这就说明数据包上指定的目的端口在目的主机上可能没有监听
- code=4;代表一个原本需要分片的数据包,但是IP头部上的表示是不进行分片,由此就出现了错误。比如我们可以设置自己的网卡的MTU大小比网关的MTU大,那么我们发送过去的数据在被网关接收后可能会出现错误,因为网关网卡的最大接收MTU数比发送过来的数据包小,而且这个数据包还标识不进行分片,这就会出现错误。
②参数问题
参数问题的类型字段值为12,它主要是因为对IP头部中的字段值出现了问题,从而导致收到这些问题报文的主机返送一个参数问题的ICMP差错报文
- ICMP控制报文
- ①源站抑制
type=4,code=0
源站抑制是拥塞控制的一种方式,虽然TCP在端到端上使用了窗口机制和慢开始,拥塞避免和快重传对流量进行了控制,网关通过对链路上的链路情况进行监控,对信源发送源站抑制里面包含着目的网络的信息,当接收方接收该信息后根据目的网络信息知道去往该网络的链路发生拥塞,于是减少信息的发送。
- ②路由重定向
type=5;code=0-3
路由重定向是指当主机发送给某个路由器的时候,这个路由器会判断自己是否是最佳的转发设备,如果根据它的路由信息发现其他的转发设备对于该主机来说最好,也就是能够更快的将数转发到目的对象,那么它就将发送路由重定向给这个主机让它将路由修改为更佳的路由。更佳路由的信息存储在ICMP的后4个字节上,
- ICMP查询报文
- ①请求和回应报文
type=8,code=0
需要注意的是请求和回应的ICMP报文使用到了ICMP头部的后4个字节,分为两个字段,即标识(和序列号,标识一般是发送该报文的进程号,标识和序列号是标识一对请求和回应报文,只有与某请求报文对应的回应报文,它们的标识与序列号才是相同的。
需要注意的是,请求和回应的ICMP数据包中的数据部分都是相同的。
- ②路由询问或通告
路由询问的类型字段值为10,通告的类型字段值为9,只有一个代码0
该类型报文主要用于无盘工作站,没有办法保存网关的情况,它就只能靠发送路由询问,来询问网关信息。路由询问报文只用了ICMP头部的前面4个字节,但是路由通告使用了全部的8个字节。、后4个字节有三个字段,分别为“地址数,地址项长度,生存时间”,它们占用的长度是1B,1B,2B这三个字段记载着数据部分包含的路由条目数量,路由条目的长度(即IP地址的长度),以及路由条目在路由器上面的有效生存时间。
在该ICMP报文中,每个路由信息分为路由地址和优先级,各自占用4个字节,优先级越高越有可能成为该主机的默认网关。
- ③时间戳请求与应答
时间戳的请求的类型字段为13,应答为14,只有一个代码0
它的头部与请求与回应的ICMP报文一致,但是数据部分它使用了12个字节,每4个字节记录一段时间信息,总共有三段,分别是“发送时间戳 ,接收时间戳,回送时间戳”,发送时间戳的信息由时间戳请求者记录,后面两个字段由回送者记录。字段里面记录的是有关当前时间的毫秒数的表示,发送者只要根据回送者发送的时间信息就可以很容易的求出往返时长。
- ④地址掩码请求和应答
请求的类型字段值为17,应答的类型字段为18,只有一个代码0
它的ICMP头部与请求的ICMP包的头部相同,数据字段存储的是请求的子网掩码
PS:
Identifier(标识符):Identifier 是一个16位的字段,通常用于标识 ICMP Echo 请求和响应之间的匹配。当发送 ICMP Echo 请求时,Identifier 字段的值会被设置为一个特定的标识符(通常是随机生成的),然后在接收到 ICMP Echo 响应时,接收端会将相同的标识符字段包含在响应中,以便发送端能够识别与响应相关联的请求。
Sequence Number(序列号):Sequence Number 是一个16位的字段,它通常用于按顺序对 ICMP Echo 请求和响应进行排序。每个 ICMP Echo 请求都会包含一个唯一的序列号,然后在接收到 ICMP Echo 响应时,接收端会将相同的序列号字段包含在响应中,以便发送端能够识别响应与哪个请求相对应。
这两个字段的组合(标识符和序列号)允许发送端将 ICMP Echo 请求与响应正确匹配,从而可以测量网络的延迟和连通性。当发送多个 ICMP Echo 请求时,这些字段的组合确保了每个响应都与特定的请求关联,并且可以按顺序排列。
需要注意的是,Identifier 和 Sequence Number 的确切含义可能因 ICMP 报文的类型和用途而有所不同。上述解释是针对 ICMP Echo 请求和响应的常见用法。其他类型的 ICMP 报文可能会使用这些字段以不同的方式
2.4 实际报文举例
1、请求响应
请求包
响应包
2、网络、主机、协议、端口不可达
三、使用go实现icmp请求以及接收响应内容
package main
import (
"bytes"
"container/list"
"encoding/binary"
"fmt"
"net"
"os"
"time"
)
type ICMP struct {
Type uint8
Code uint8
Checksum uint16
Identifier uint16
SequenceNum uint16
}
func main() {
var (
icmp ICMP
laddr = net.IPAddr{IP: net.ParseIP("0.0.0.0")}
raddr, _ = net.ResolveIPAddr("ip", os.Args[1])
)
conn, err := net.DialIP("ip4:icmp", &laddr, raddr)
if err != nil {
fmt.Println(err.Error())
return
}
defer conn.Close()
icmp.Type = 8
icmp.Checksum = 0
icmp.Code = 0
icmp.Identifier = 0
icmp.SequenceNum = 0
var buffer bytes.Buffer
binary.Write(&buffer, binary.BigEndian, icmp)
data := []byte("This is a custom ICMP payload.")
// Append the custom data to the buffer
buffer.Write(data)
icmp.Checksum = CheckSum(buffer.Bytes())
buffer.Reset()
binary.Write(&buffer, binary.BigEndian, icmp)
buffer.Write(data)
fmt.Printf("\nPing %s with 0 bytes of data:\n", raddr.String())
recv := make([]byte, 1024)
statistic := list.New()
sended_packets := 0
for i := 4; i > 0; i-- {
if _, err := conn.Write(buffer.Bytes()); err != nil {
fmt.Println(err.Error())
return
}
sended_packets++
t_start := time.Now()
conn.SetReadDeadline((time.Now().Add(time.Second * 5)))
_, err := conn.Read(recv)
if err != nil {
fmt.Println("Request timeout")
continue
}
t_end := time.Now()
dur := t_end.Sub(t_start).Nanoseconds() / 1e6
fmt.Printf("Reply from %s: time = %dms\n", raddr.String(), dur)
statistic.PushBack(dur)
}
defer func() {
fmt.Println("")
var min, max, sum int64
if statistic.Len() == 0 {
min, max, sum = 0, 0, 0
} else {
min, max, sum = statistic.Front().Value.(int64), statistic.Front().Value.(int64), int64(0)
}
for v := statistic.Front(); v != nil; v = v.Next() {
val := v.Value.(int64)
switch {
case val < min:
min = val
case val > max:
max = val
}
sum = sum + val
}
recved, losted := statistic.Len(), sended_packets-statistic.Len()
fmt.Printf("Ping statistics for %s:\n Packets: Sent = %d, Received = %d, Lost = %d (%.1f%% loss),\nRound-Trip Time (ms):\n Min = %dms, Max = %dms, Avg = %.0fms\n",
raddr.String(),
sended_packets, recved, losted, float32(losted)/float32(sended_packets)*100,
min, max, float32(sum)/float32(recved),
)
}()
}
func CheckSum(data []byte) uint16 {
var (
sum uint32
length int = len(data)
index int
)
for length > 1 {
sum += uint32(data[index])<<8 + uint32(data[index+1])
index += 2
length -= 2
}
if length > 0 {
sum += uint32(data[index])
}
sum += (sum >> 16)
return uint16(^sum)
}
或者
package main
import (
"bytes"
"encoding/binary"
"flag"
"fmt"
"log"
"net"
"os"
"time"
)
var (
timeout int64
size int
count int
typ uint8 = 8
code uint8 = 0
)
type ICMP struct {
Type uint8
Code uint8
CheckSum uint16
ID uint16
SequnceNum uint16
}
func main() {
getCommandArgs()
desIP := os.Args[len(os.Args)-1]
conn, err := net.DialTimeout("ip:icmp", desIP, time.Duration(timeout)*time.Millisecond)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
fmt.Printf("正在ping %s [%s] 具有 %d 字节的数据:", desIP, conn.RemoteAddr(), size)
for i := 0; i < count; i++ {
t1 := time.Now()
icmp := *&ICMP{
Type: typ,
Code: code,
CheckSum: 0,
ID: 1,
SequnceNum: 1,
}
data := make([]byte, size)
var buffer bytes.Buffer
binary.Write(&buffer, binary.BigEndian, icmp)
buffer.Write(data)
data = buffer.Bytes()
checkSum := checkSum(data)
data[2] = byte(checkSum >> 8)
data[3] = byte(checkSum)
conn.SetDeadline(time.Now().Add(time.Duration(timeout) * time.Millisecond))
n, err := conn.Write(data)
if err != nil {
log.Fatalln(err)
continue
}
buf := make([]byte, 65535)
n, err = conn.Read(buf)
if err != nil {
log.Println(err)
continue
}
ts := time.Since(t1).Milliseconds()
fmt.Printf("来自 %d.%d.%d.%d 的回复: 字节= %d 时间= %dms TTL=%d\n", buf[12], buf[13], buf[14], buf[15], n-28, ts, buf[8])
}
}
func getCommandArgs() {
flag.Int64Var(&timeout, "w", 1000, "请求超时时长,单位毫秒")
flag.IntVar(&size, "l", 32, "请求发送缓冲区大小,单位字节")
flag.IntVar(&count, "n", 4, "发送请求数")
flag.Parse()
}
func checkSum(data []byte) uint16 {
length := len(data)
index := 0
var sum uint32 = 0
for length > 1 {
sum += uint32(data[index])<<8 + uint32(data[index+1])
length -= 2
index += 2
}
if length != 0 {
sum += uint32(data[index])
}
hi16 := sum >> 16
for hi16 != 0 {
sum = hi16 + uint32(uint16(sum))
hi16 = sum >> 16
}
return uint16(^sum)
}