从字节流构建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个字段,所以只要按字段固定长度及对应顺序填充即可。
- type, 根据上文协议类型,echo 请求包 type 字段为 8, code 字段为 0
- code, 0
- checksum,初始为 0, 最终值需要根据另外5个字段进行计算,计算方式在 [RFC 1071] 有描述, 下文代码中有相关实现。
- id, 可随机,范围: 0 ~ 65535
- sequence, 可随机,范围: 0 ~ 65535
- 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 均与之前发送的相同,可以说明发送成功。