Internet Control Message Protocol, ICMP(RFC-792) 基于 IP 协议,工作在七层协议的第三层,主要用来解析网络路由情况,通过返回错误信息进行分析定位。
这里详细介绍其使用方式。
简介
ICMP 报文封装在 IP 报文内部,通过 RAW_SOCK
接收到的报文同时会包含 IP 头部信息,这也就意味着在使用时,需要将 IP 的头先拆掉,然后再获取 ICMP 报文。
IP 报文头为 20 字节,详细的内容这里就不介绍了。
ICMP 报文
ICMP 报文头根据 type
和 code
的不同,其对应的大小也有所区别,而 Ping 使用的就是 ICMP 报文,其报头为 8 字节,数据报长度最大为 64K 字节。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Code | Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Identifier | Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data ...
+-+-+-+-+-+-+-
Ping 使用的是发送和接受格式,其中 Type(8)
是请求报文,而 Type(0)
是应答报文。
注意,如果需要扫描多个个主机,那么返回的报文中只有 ID
以及 SEQ
,所以,可以将相关的信息添加到 Data 字段中,这个字段是二进制的,可以以 \0'
终止。
代码实现
在接收时通过如下方式实现,常用于 socket 的发送接收消息,函数声明如下。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
#include <sys/socket.h>
struct iovec { /* Scatter/gather array items */
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};
struct msghdr {
void *msg_name; /* optional address */
socklen_t msg_namelen; /* size of address */
struct iovec *msg_iov; /* scatter/gather array */
size_t msg_iovlen; /* # elements in msg_iov */
void *msg_control; /* ancillary data, see below */
size_t msg_controllen; /* ancillary data buffer len */
int msg_flags; /* flags on received message */
};
看起来比较复杂,实际上结构成员可以分为四组:
- 套接口地址成员
msg_name
与msg_namelen
; - IO 向量引用
msg_iov
与msg_iovlen
; - 附属数据缓冲区成员
msg_control
与msg_controllen
; - 接收信息标记位
msg_flags
,详细可以查看man
手册。
其中 msg_control
指向的是一个 struct cmsghdr
结构体。
其它
ICMP 报文在 Linux C 中通过 struct icmp[netinet/ip_icmp.h]
定义,而报文中的消息体,则根据不同 TYPE
、CODE
会有所区别。
在 C 中,通过 union
进行适配,为了方便使用,同时又定义了很多辅助的宏定义。
Socket 选项
这也是比较常规的设置,这里简单介绍下,可以通过 setsockopt()
设置相关的参数,常用的参数如下。
SOL_SOCKET
SO_BINDTODEVICE
绑定到某个网络设备上,也就是从某个指定的网卡发送数据。SOL_SOCKET
SO_MARK
用来添加标记,然后作策略路由之类的动作。SOL_SOCKET
SO_TIMESTAMP
让协议栈接受到一个网络帧时为其打上时间戳,并将此时间戳作为一笔附加数据,与网络帧数据一起递交到上层协议。
Mark 标记
用于将特定的数据包打上标签,供 iptables
配合 TC
做 QOS
流量限制、应用策略路由等。
系统可用模块,对 CentOS 而言可通过 ls /usr/lib64/xtables/ | grep -i mark
查看,其中大写的为标记模块,小写的为匹配模块。
----- 查看关于Mark标记的帮助信息
# iptables -j MARK --help
# iptables -m mark --help
----- 将所有TCP数据标记1
# iptables -t mangle -A PREROUTING -p tcp -j MARK --set-mark 1
----- 匹配标记1的数据并保存数据包中的MARK到连接中
# iptables -t mangle -A PREROUTING -p tcp -m mark --mark 1 -j CONNMARK --save-mark
策略路由
标签并不是设置在数据包内容中,而是在内核中数据包的载体上,如果需要在数据包内容中设置标签,可以使用 TOS 规则目标,也就是修改 IP 数据包头的 TOS 值。
----- 将从网络接口tun0进入的、目标端口为5222的TCP数据包设置mark值为1
# iptables -t mangle -A PREROUTING -j MARK --set-mark 1 -i tun0 -p tcp --dport 5222
接着,根据设置的 mark 值可用来设定策略路由,比如,把 mark 值为 1 的数据包交由网关 192.168.0.1 转发。
----- 1. 确定一张空路由表,这里选定300
# ip route show table 300
----- 2. 在表中添加路由条目
# ip route add default via 192.168.0.1 table 300
----- 3. 查看当前路由规则
# ip rule list
----- 4. 为mark值为1的数据包指定路由表策略
# ip rule add fwmark 0x1 table 300
通过这种方法,可以使用 iptables 根据匹配规则设置 mark,再由路由模块根据 mark 值进行路由决策,从而实现复杂的策略路由。
TIMESTAMP
也就是在内核的协议栈中,在接收到报文之后以及交接给上层协议栈处理之前,添加时间戳,这样计算的时间戳就不再依赖应用层的响应速度,尤其是当采用了类似 cgroup 的资源限制。
Socket 缓冲区
使用 RAW_SOCKET
时,例如发送 ICMP 报文,因为不存在 TCP 中的滑动窗口、限流等机制,在流量过大时极易引起报文在缓冲区的静默丢失。此时可以通过 tcpdump 获取,但是无法通过 read()
、recvmsg()
等接口无法接收到数据。
接收缓冲区的大小可以使用 setsockopt()
设置 SO_RCVBUF
选项,其默认和最大值可以通过如下命令查看。
$ cat /proc/sys/net/core/rmem_default
$ cat /proc/sys/net/core/rmem_max
$ cat /proc/sys/net/core/wmem_default
$ cat /proc/sys/net/core/wmem_max
在设置 Socket 的时候,如果期望设置的缓存大于上述的 max ,实际上会得到两倍于 max 值。
TTL
Time To Live, TTL 存活时间,表示这个 PING 数据包能在网络上存活多少时间,实际上就是用来表示可以被路由转发多少次,在 IP 头中通过 8bits 表示。
执行 Ping 操作时,会在本机发送一个数据包,正常来说,数据包会经过一定的路由到达目的主机。不过,由于很多原因可能会导致一些数据包不能正常传送到目的主句,为了防止由于数据包一直传输导致网络负载增加,增加的 TTL 作为包的存活时间。
当数据包传送到一个路由器之后,TTL 就自动减 1,如果减到 0 了还是没有传送到目的主机,那么就自动丢弃,此时一般是 Request timed out 了。
64 bytes from 192.168.9.1: icmp_seq=1 ttl=60 time=0.909 ms
目前测试来看,当 ICMP 报文发送之后,其返回的报文实际上会重新设置(依赖目标机器的设置),也即是说 TTL 的值是单程的。
在 Linux 命令行中,可以通过 -t
参数进行设置。例如,返回的 TTL 为 60,那么你可以初步猜测对端设置的初始 TTL 为 64,那么通过 -t 2
设置时候,就会出现 Time to live exceeded
的报错。
系统设置
Windows 一般在 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters
注册表中设置。
在 Linux 中,可以通过如下方式修改默认的 TTL 。
----- 临时修改
# sysctl net.ipv4.ip_default_ttl=129
# echo 129 > /proc/sys/net/ipv4/ip_default_ttl
----- 保存到配置文件中
$ cat /etc/sysctl.conf
net.ipv4.ip_default_ttl=129
各个系统的默认值如下。
Linux 64/255
Windows NT/2000/XP 128
Windows 98 32
UNIX 255
QoS
Quality of Service, QoS 服务质量,提供的质量越好,表示有越低的延迟、丢包、抖动等,同时其吞吐量和可靠性要更高。简单来说,就是利用包的特定标志位,告诉路由器如何处理包,是先还是后。
在第二层 (Link Layer) 和第三层 (IP) 都有标示位,其中 IP 层采用的是 8Bits,包括 IPv4 和 IPv6,只是其位置不同。
其它
非 root 套接字
新版本的内核中提供了低权限的 Socket 访问方式,不过内核会自动过滤报文,所以,不太适合大批量的监控,这里仅简单介绍下。
ICMP 套接字的目的是允许在不设置 SUID 或者 CAP_NET_RAW 权限的时候允许 ping 程序的使用,详细的实现可以查看内核的邮件列表 add IPPROTO_ICMP socket kind 。
是否支持是通过内核的 net.ipv4.ping_group_range
指定,这是一对整数,指定了允许使用 ICMP 套接字的组 ID 的范围,默认为 1 0
也就意味着没有人能够使用这个特性。
可以通过如下命令修改。
# sysctl -w net.ipv4.ping_group_range='0 10'
然后可以通过如下方式创建 ICMP 的套接字。
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_ICMP)
如果系统不支持这个特性,在创建套接字的时候会得到 Protocol not supported
的错误,而如果没有权限,则会得到 Permission denied
的错误。
它的类型和 UDP 套接字一样,是 SOCK_DGRAM
而非 SOCK_RAW
,这也就意味着你不会收到 20 字节的 IP 头,而且内核会计算校验和,并且填充 ICMP ID,在接收到响应后,内核会只把相应 ID 的 ICMP 响应返回给程序,不需要自己或者要求内核过滤了。
抓包
可以通过如下命令获取包,如果使用的 IPv6 需要将 icmp 替换为 icmp6 。
----- ICMP echo request
# tcpdump -nni eth0 -e icmp[icmptype] == 8
----- ICMP echo reply
# tcpdump -nni eth0 -e icmp[icmptype] == 0
----- 保存到文件中
# tcpdump -nni eth0 -w /tmp/icmp.pcap -e icmp[icmptype] == 0
----- 读取文件
# tcpdump -nn -r icmp.pcap
---- 只过滤icmp报文
# tcpdump -nni eth0 icmp
---- 针对某台主机的报文
# tcpdump -nni eth0 icmp and host 10.80.97.129
# nohup tcpdump -w /var/log/icmp-10.80.97.129.cap -nni eth0 icmp and host 10.80.97.129 &
参考
- 可以参考 Github liboping、Raw Sockets and ICMP 。
- 关于各个操作系统的默认 TTL 可以参考 Default TTL (Time To Live) Values of Different OS 。