[TCP/IP] 发送 ICMP echo 包

4,513 阅读5分钟

从字节流构建ICMP echo包,并发送至指定IP。

从行为上来说,实现了跟 ping 命令发出请求类似的功能。

读完下文,可以初步了解以下问题:

  • 一个TCP/IP协议中的包是如何被构建的?
  • 其中的协议又发挥了什么作用?
  • 比特流如何构成一个包,包又怎样转换为比特流或字节流发送出去?

1. ICMP 报文结构

报文结构这部分来自 维基百科 并做了适应性修改和错误修复。

报头

ICMP报头从IP报头的第160位开始(IP首部20字节)(除非使用了IP报头的可选部分)。

Bits 160-167 168-175 176-183 184-191
160 Type Code 校验码(checksum)
192 ID 序号(sequence)
  • Type - ICMP的类型,标识生成的错误报文。
  • Code - 进一步划分ICMP的类型,该字段用来查找产生错误的原因。例如,ICMP的目标不可达类型可以把这个位设为1至15等来表示不同的意思。
  • Checksum - 校验码部分,这个字段包含有从ICMP报头和数据部分计算得来的,用于检查错误的数据,其中此校验码字段的值视为0。
  • ID - 这个字段包含了ID值,在Echo Reply类型的消息中要返回这个字段。
  • Sequence - 这个字段包含一个序号,同样要在Echo Reply类型的消息中要返回这个字段。

填充数据(data字段)

填充的数据紧接在ICMP报头的后面(以8位为一组):

  • Linux的"ping"工具填充的ICMP除了8个8 bit 的报头以外,默认情况下还另外填充数据使得总大小为64字节。
  • Windows的"ping.exe"填充的ICMP除了8个8 bit 的报头以外,默认情况下还另外填充数据使得总大小为40字节。

报文类型

Type Code 描述
0 - Echo Reply 0 echo响应 (程序ping使用)
8 - Echo Request 0 echo请求

更多Type与Code请点击 互联网控制消息协议 - 维基百科

2. Wireshark捕获

通过Wireshark添加过滤器 icmp 即可捕获到ICMP协议相关的包。

echo 请求

▲ 一个ICMP echo请求,可以观察到协议中的各个字段。

echo 回复

▲ 一个ICMP echo回复,可以观察到除了Type不同,其余均与echo请求相同。

3. 构造ICMP Echo request packet

现在来手动构建一个ICMP echo 请求包。

协议中共有6个字段,所以只要按字段固定长度及对应顺序填充即可。

  1. type, 根据上文协议类型,echo 请求包 type 字段为 8, code 字段为 0
  2. code, 0
  3. checksum,初始为 0, 最终值需要根据另外5个字段进行计算,计算方式在 [RFC 1071] 有描述, 下文代码中有相关实现。
  4. id, 可随机,范围: 0 ~ 65535
  5. sequence, 可随机,范围: 0 ~ 65535
  6. data, 格式为字符串,可为空。

▼ ICMP echo请求包 数据结构

{
    "type": 8,
    "code": 0,
    "checksum": 0,
    "id": 10086,
    "seq": 1,
    "data": "hello world"
}

▼ 构建ICMP echo包 ( Python 实现)

In [3]: echo = Echo(10086, seq=1, data='hello world')

In [4]: echo
Out[4]: Echo(id=10086, seq=1, data="hello world", checksum=51774)

In [6]: echo.raw
Out[6]: '''0000100000000000001111101100101
           0001001110110011000000000000000
           0101101000011001010110110001101
           1000110111100100000011101110110
           1111011100100110110001100100'''

In [8]: print(echo.pretty)
# ICMP echo 的可视化描述
+-----------------------------------+
|          ICMP - Echo              |
+--------+--------+-----------------+
|  Type  |  Code  |    chechsum     |
+--------+--------+-----------------+
|00001000|00000000|00111110 11001010|
+-----------------+-----------------+
|       ID        |    sequence     |
+-----------------+-----------------+
|00100111 01100110|00000000 00000001|
+-----------------+-----------------+
|                Data               |
+-----------------------------------+
|01101000 01100101 01101100 01101100|
+-----------------------------------+
|01101111 00100000 01110111 01101111|
+-----------------------------------+
|01110010 01101100 01100100         |
+-----------------------------------+

▼ 发送ICMP echo请求包

In [9]: import socket
In [10]: s = socket.socket(socket.AF_INET,
    ...:                   socket.SOCK_RAW,
    ...:                   socket.IPPROTO_ICMP)
In [11]: s.connect(('10.10.10.10', 1))
In [13]: s.send(str(echo))
Out[13]: 19     # 发送ICMP包长度为 19 bytes

这里需要注意,因为使用了 socket.SOCK_RAW 来发送IP包,需要root权限才能运行。

▶ 点击查看Echo类源码: Github: icmp_echo.py

4. 从terminal监听echo包

使用Wireshark可以方便的监听ICMP包,如果需要在terminal中监听,可以使用 tcpdump 命令。

$ sudo tcpdump -i eth0 icmp and icmp[icmptype]=icmp-echo

08:26:32.397922 IP 10.10.10.9 > 10.10.10.10: ICMP echo request, id 10086, seq 1, length 19

注意到这个包 id, seq, length 均与之前发送的相同,可以说明发送成功。

5. 参考

  1. RFC 792 - Internet Control Message Protocol - IETF Tools
  2. RFC 1071 - Computing the Internet checksum - IETF Tools
  3. 互联网控制消息协议 - 维基百科
  4. TCP/IP Illustrated, Volume 1: The Protocols
  5. dpkt Tutorial #1: ICMP Echo
  6. stackoverflow.com/questions/2…