目录
1.ICMP协议简介
2.ICMP报文格式
2.1 ICMP报文以太网数据帧格式
2.2 ICMP首部格式
2.3 ICMP报文类型列表
3.ICMP故障排查工具
3.1 ping工具
3.2 traceroute工具
4.常见ICMP报文
4.1 ICMP请求和应答
4.2 ICMP差错报告报文
4.3 目标主机不可达
5.ICMP校验和计算
5.1 ICMP校验和计算
5.2 ICMP校验和验证
6.ICMP编程示例
6.1 发送回显请求
6.2 发送回显应答
1.ICMP协议简介
ICMP(Internet Control Message Protocol)是一种网络协议,它用于在IP网络中传递控制信息和错误消息。它通常与IP协议一起使用,IP协议负责发送和路由数据包,而ICMP协议负责检查网络是否可达、路由是否正确、主机是否可达等网络状态的反馈信息。
ICMP协议的主要功能如下:
发现网络错误:当一个数据包在传输过程中出现错误时,ICMP协议通过向发送方发送错误通知来发现网络错误。
检查网络是否可达:通过发送ICMP ECHO请求并接收ICMP ECHO回复消息,可以确定目标主机是否可达。
发现主机错误:当一个主机无法正常工作时,ICMP协议通过向发送方发送错误通知来发现主机错误。
发送路由信息:ICMP协议可以向其他主机发送路由信息,以帮助它们在网络中找到合适的路由。
2.ICMP报文格式
2.1 ICMP报文以太网数据帧格式
图 1 ICMP以太网数据帧
ICMP报文属于IP子协议,协议号为1。
2.2 ICMP首部格式
图 2 ICMP首部格式
其中各字段的含义如下:
类型(Type):指定 ICMP 报文的类型,占 1 个字节。常见类型有:回显应答(Echo Reply:0)、回显请求(Echo Request:8)等。
代码(Code):指定 ICMP 报文的代码,占 1 个字节。用于进一步描述 ICMP 报文,与 Type 字段组合使用。
校验和(Checksum):校验和,用于检查 ICMP 报文是否有损坏,占 2 个字节。
由类型决定的4字节:根据类型不一样,4字节表达的意思不一样。
数据(Data):数据,可变长度。可以是任意数据,长度由具体的 ICMP 报文类型和代码决定。
2.3 ICMP报文类型列表
表 1 ICMP报文类型表
常见的ICMP报文类型:
Echo Reply(回显应答):用于回复Echo Request(回显请求)报文,通常用于测试网络连接是否正常。
Destination Unreachable(目的地不可达):用于指示主机或路由器无法到达目的地或某个网络服务不可用。
Source Quench(源站抑制):当接收方无法处理所有传入的数据报时,源站抑制报文会发送到发送方,以通知其减慢数据传输速度。
Redirect(重定向):用于通知发送方,其正在使用的路由不再是最佳路由,建议使用另一条路由。
Echo Request(回显请求):用于测试测试网络连接是否正常。
Time Exceeded(时间超时):用于指示一个数据包在传输过程中被丢弃,原因是数据包在经过路由器时超过了其生存时间。
Parameter Problem(参数问题):用于指示数据包头部中存在错误的参数或选项,导致数据包无法被识别或处理。
Timestamp Request/Reply(时间戳请求/应答):用于向另一个主机请求当前时间戳,并将其返回给请求方。
Information Request/Reply(信息请求/应答):用于向另一个主机请求特定信息,并将其返回给请求方。
Address Mask Request/Reply(地址掩码请求/应答):用于请求另一个主机的网络掩码,并将其返回给请求方。
3.ICMP故障排查工具
3.1 ping工具
Ping命令是一种常用的网络诊断工具,用于测试网络连接性和响应时间。它发送一个ICMP数据包(Internet控制消息协议),并在目标主机收到数据包后返回一个响应,以确定目标主机是否可达,以及响应时间。
Ping命令的语法如下:
ping [-t] [-a] [-n count] [-l size] [-f] [-i TTL] [-v TOS] [-r count] [-s count] [[-j host-list] | [-k host-list]] [-w timeout] destination-list
其中,常用的参数包括:
-t:持续发送数据包,直到手动停止
-a:解析IP地址为主机名
-n count:指定要发送的数据包数
-l size:指定要发送的数据包大小
-f:在数据包中设置“不分片”标志
-i TTL:设置数据包的存活时间
-w timeout:指定等待响应的最大时间
3.2 traceroute工具
traceroute命令用于检测网络连接的路径和延迟时间,以及确定网络上的故障点。它通过向目标主机发送一系列的数据包,并记录每个包从源主机到目标主机的路由路径上所经过的中间节点(路由器)。
traceroute命令会输出每个中间节点的IP地址、主机名(如果可用)、延迟时间和TTL值。TTL(Time to Live)值是每个数据包的生命周期,当数据包经过一个路由器时,TTL值就会减少1,如果TTL值降到0,则该数据包就会被丢弃并返回一个ICMP超时消息,这样我们就可以知道数据包到达了哪个中间节点。
例如,我们可以使用以下命令来traceroute到百度的IP地址(202.108.22.5):
traceroute 202.108.22.5
输出结果可能类似于以下内容:
traceroute to 202.108.22.5 (202.108.22.5), 30 hops max, 60 byte packets1 router (192.168.1.1) 2.025 ms 1.326 ms 1.115 ms2 100.64.0.1 (100.64.0.1) 4.505 ms 4.591 ms 4.659 ms3 218.240.40.121 (218.240.40.121) 7.131 ms 7.217 ms 7.291 ms4 218.240.40.146 (218.240.40.146) 25.398 ms 25.397 ms 25.393 ms5 202.96.12.26 (202.96.12.26) 25.373 ms 202.96.12.34 (202.96.12.34) 25.357 ms 25.344 ms6 202.96.12.110 (202.96.12.110) 25.314 ms 25.301 ms 25.289 ms7 202.97.94.118 (202.97.94.118) 25.492 ms 202.97.94.114 (202.97.94.114) 25.478 ms 25.463 ms8 202.97.58.237 (202.97.58.237) 25.434 ms 25.409 ms 25.394 ms9 202.97.58.233 (202.97.58.233) 25.372 ms 25.363 ms 25.349 ms
10 * * *
11 * * *
12 202.108.22.5 (202.108.22.5) 25.633 ms 25.618 ms 25.603 ms
从输出结果中可以看到,traceroute命令首先会输出目标主机的IP地址和最大跳数(30),然后每一行显示一个中间节点的信息。例如,第一行显示第一个中间节点的IP地址(192.168.1.1)、主机名(如果可用)、三次ping的延迟时间。最后一行显示目标主机的IP地址和延迟时间。在第10和11行中,我们看到了两个星号,这表示该数据包在到达该中间节点时已经超时并被丢弃了,因此我们无法确定该节点的IP地址。
4.常见ICMP报文
4.1 ICMP请求和应答
执行ping命令,ping一个可以通信的IP地址,如下命令:
ping 223.5.5.5
ping通后会收到对端发来的ICMP应答报文。
ICMP回显请求报文
图 3 ICMP回显请求报文
ICMP回显响应报文
图 4 ICMP回显响应报文
4.2 ICMP差错报告报文
TTL过期差错报告报文
图 5 TTL过期原理
ping命令通过-i指定TTL值,如下命令:
ping 223.5.5.5 -i 5 -t
命令指定TTL值为5,也就是通过5个路由器后,TTL会变成0,数据包丢弃,路由器发送ICMP TTL过期报文给源主机。
ICMP TTL过期报文
图 6 TTL过期差错报告报文
4.3 目标主机不可达
当我们使用ping命令向一个主机发送ICMP(Internet控制消息协议)数据包时,如果目标主机无法到达,我们将会得到“目标主机不可达”的错误提示。
这个错误通常是由以下几种原因引起的:
-
目标主机已经关闭或没有连接到网络。这种情况下,我们无法通过网络与目标主机通信。
-
网络连接故障。如果网络连接故障,例如连接断开或路由器故障,那么我们无法到达目标主机。
-
防火墙阻止了ping请求。如果目标主机上的防火墙设置了规则以阻止ping请求,那么我们无法与目标主机进行通信。
-
ICMP协议被禁用。有些主机可能会禁用ICMP协议,这意味着它们不会回应ping请求。
ICMP目标主机不可达报文
ICMP type:3,code:1
5.ICMP校验和计算
ICMP校验和计算的校验数据为整个ICMP数据包。
5.1 ICMP校验和计算
a.校验数据以16bit为单位进行累加求和,校验数据需为偶数字节,奇数字节末尾填充0变为偶数字节。
b.如果累加和超过16bit,产生了进位,需将高16bit和低16bit累加求和。
c.循环步骤2,直至未产生进位为止。
d.累加和取反得到校验和。
5.2 ICMP校验和验证
a.校验数据16bit为单位进行累加求和,校验数据需为偶数字节,奇数字节末尾填充0变为偶数字节。
b.如果累加和超过16bit,产生了进位,需将高16bit和低16bit累加求和。
c.循环步骤2,直至未产生进位为止。
d.累加和和校验和相加得到0xffff,校验成功,否则失败。
6.ICMP编程示例
6.1 发送回显请求
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdint.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <arpa/inet.h>#define PACKET_SIZE 4096
#define ICMP_PACKET_SIZE 28uint16_t checksum(uint16_t *buf, int len)
{unsigned long sum = 0;while (len > 1) {sum += *buf++;len -= 2;}if (len == 1) {sum += *(unsigned char *)buf;}sum = (sum >> 16) + (sum & 0xffff);sum += (sum >> 16);return ~sum;
}int main(int argc, char *argv[])
{if (argc != 2) {printf("Usage: %s <destination_ip>\n", argv[0]);return -1;}char buf[PACKET_SIZE] = {0};memset(buf, 0, sizeof(buf));int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);if (sockfd < 0) {perror("socket error");return -1;}struct sockaddr_in dest_addr;memset(&dest_addr, 0, sizeof(dest_addr));dest_addr.sin_family = AF_INET;dest_addr.sin_addr.s_addr = inet_addr(argv[1]);uint16_t seq = 0;while(1) {memset(buf, 0, PACKET_SIZE);struct icmp *icmp_packet = (struct icmp *)buf;icmp_packet->icmp_type = ICMP_ECHO;icmp_packet->icmp_code = 0;icmp_packet->icmp_id = 0;icmp_packet->icmp_seq = seq++;memset(icmp_packet->icmp_data, 0, ICMP_PACKET_SIZE);icmp_packet->icmp_cksum = 0;icmp_packet->icmp_cksum = checksum((uint16_t *)icmp_packet, ICMP_PACKET_SIZE);printf("icmp_packet size:%lu\n", sizeof(struct icmp));int sent_bytes = sendto(sockfd, buf, sizeof(struct icmp), 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr));if (sent_bytes <= 0) {perror("sendto error");break;}printf("sent icmp request:%d bytes to:%s\n", sent_bytes, argv[1]);int recv_bytes = recv(sockfd, buf, PACKET_SIZE, 0);if (recv_bytes <= 0) {perror("recv");break;}struct iphdr *ip_packet = (struct iphdr *)buf;struct icmp *icmp_reply = (struct icmp *)(buf + (ip_packet->ihl << 2));printf("recv icmp reply:%d from:%s\n", recv_bytes, inet_ntoa(dest_addr.sin_addr));printf("icmp type:%d,code:%d\n", icmp_reply->icmp_type, icmp_reply->icmp_code);sleep(1);}close(sockfd);return 0;
}
6.2 发送回显应答
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <linux/in.h>
#include <arpa/inet.h>#define IP_HDRLEN (20)
#define PACKET_SIZE (4096)uint16_t checksum(uint16_t *buf, int len)
{unsigned long sum = 0;while (len > 1) {sum += *buf++;len -= 2;}if (len == 1) {sum += *(unsigned char *)buf;}sum = (sum >> 16) + (sum & 0xffff);sum += (sum >> 16);return ~sum;
}uint16_t checksum_nofold(uint16_t *buf, int len)
{unsigned long sum = 0;while (len > 1) {sum += *buf++;len -= 2;}if (len == 1) {sum += *(unsigned char *)buf;}sum = (sum >> 16) + (sum & 0xffff);sum += (sum >> 16);return sum;
}bool parse_pack(char *buf, uint32_t len) {struct icmp *icmp_packet = (struct icmp *)buf;uint16_t csum = checksum_nofold((uint16_t *)buf, len);printf("icmp csum:0x%04x\n", csum);return csum == 0xffff;
}int main(int argc , char *argv[]) {int sockfd;int ret;char send_buf[PACKET_SIZE] = {0};char recv_buf[PACKET_SIZE] = {0};sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);if (sockfd == -1) {perror("socket error");return -1;}while(1) {struct sockaddr_in peer;socklen_t peerlen = sizeof(peer);memset(recv_buf, 0, PACKET_SIZE);ret = recvfrom(sockfd, recv_buf, PACKET_SIZE, 0, (struct sockaddr *)&peer, &peerlen);if (ret <= 0) {printf("ret:%d, errno:%d(%s)\n", ret, errno, strerror(errno));} else {printf("recv len:%d, peer src:port->%s:%d\n", ret, inet_ntoa(peer.sin_addr), ntohs(peer.sin_port));bool bret = parse_pack(recv_buf, ret);if (bret) {struct icmp *recv_icmp = (struct icmp *)(recv_buf + sizeof(struct iphdr));memset(send_buf, 0, PACKET_SIZE);struct icmp *icmp_packet = (struct icmp *)send_buf;icmp_packet->icmp_type = ICMP_ECHOREPLY;icmp_packet->icmp_code = 0;icmp_packet->icmp_id = 0;icmp_packet->icmp_seq = recv_icmp->icmp_seq;memset(icmp_packet->icmp_data, 0, sizeof(struct icmp));icmp_packet->icmp_cksum = 0;icmp_packet->icmp_cksum = checksum((uint16_t *)icmp_packet, sizeof(struct icmp));int sent_bytes = sendto(sockfd, send_buf, sizeof(struct icmp), 0, (struct sockaddr *)&peer, sizeof(peer));if (sent_bytes <= 0) {perror("sendto error");break;}}}}close(sockfd);return 0;
}