一、ICMP报文格式出现RFC792文件中
二、在 C 语言里,Internet 控制报文协议(ICMP)的数据结构是依据 RFC 792 标准来定义的。
ICMP 报文的开头是一个固定的 8 字节头部,后面可以跟着可变长度的数据部分。其基本格式如下:
struct icmp_hdr {
uint8_t type; // 类型字段,占1字节
uint8_t code; // 代码字段,占1字节
uint16_t checksum; // 校验和,占2字节
union {
// 不同类型的ICMP报文使用不同的字段
uint32_t gateway; // 用于重定向报文
struct {
uint16_t id; // 标识符
uint16_t sequence; // 序列号
} echo; // 用于回显请求和应答
uint16_t mtu; // 用于路径MTU发现
} un;
};
或者:
// ICMP Header
typedef struct __attribute__((__packed__)) {
uint8_t type;
uint8_t code;
uint16_t checksum;
} Icmp_Hdr;
#endif //HDR_H
ICMP相关处理过程
//
// Created by Administrator on 25-6-23.
//
#include <prtc.h>
/**
* 解析ICMP数据包。
*
* 该函数接收原始数据和长度,尝试解析出ICMP头部。
* 首先,它会分配足够的内存来存储整个ICMP数据包。
* 然后,复制数据到新分配的内存中,并验证ICMP数据包的校验和。
* 如果校验和验证失败,表明数据包可能在传输过程中损坏,此时释放内存并返回nullptr。
*
* @param data 指向原始数据包的指针。
* @param len 数据包的长度。
* @return 成功解析则返回ICMP头部的指针,否则返回nullptr。
*/
Icmp_Hdr *icmp_parse(const unsigned char *data, uint16_t len) {
Icmp_Hdr *icmp_hdr = malloc(len);
if (icmp_hdr == NULL) return nullptr;
memcpy(icmp_hdr, data, len);
if (!icmp_checksum(icmp_hdr, len)) {
free(icmp_hdr);
return nullptr;
}
return icmp_hdr;
}
/**
* 计算并验证ICMP数据包的校验和。
*
* 该函数使用通用的校验和计算方法来验证ICMP数据包的完整性。
* 如果计算出的校验和不为0,则认为数据包已损坏,释放内存并返回FALSE。
*
* @param icmp_hdr 指向ICMP头部的指针。
* @param len 数据包的长度。
* @return 校验和验证成功返回TRUE,否则返回FALSE。
*/
BOOL icmp_checksum(Icmp_Hdr *icmp_hdr, uint16_t len) {
if (checksum(icmp_hdr, len) !=0) {
free(icmp_hdr);
return FALSE;
}
return TRUE;
}
/**
* 打印ICMP头部信息。
*
* 根据ICMP头部的类型和代码字段,输出相应的ICMP消息类型。
* 目前只处理两种类型:Echo Reply(类型0,代码0)和Echo Request(类型8,代码0)。
* 其他类型和代码的处理可以类似地添加到这个函数中。
*
* @param icmp_hdr 指向ICMP头部的指针。
*/
void icmp_print(const Icmp_Hdr *icmp_hdr) {
printf("ICMP:\t Type: ");
if (icmp_hdr->type == 0 && icmp_hdr->code == 0) {
printf("Echo Reply\n");
} else if (icmp_hdr->type == 8 && icmp_hdr->code == 0) {
printf("Echo Request\n");
}
}
<prtc.h>内容
//
// Created by Administrator on 25-6-23.
//
#ifndef PRTC_H
#define PRTC_H
#include <nps.h>
#include <hdr.h>
#include <stdio.h>
#include <string.h>
#include <winsock2.h>
/**
* 将MAC地址转换为字符串表示形式
* @param mac MAC地址的二进制数据
* @return 转换后的MAC地址字符串
*/
static char *get_mac_str(const unsigned char *mac) {
char *mac_str = malloc(ETH_II_MAC_LEN + 1);
memset(mac_str, 0, ETH_II_MAC_LEN + 1);
memcpy(mac_str, mac, ETH_II_MAC_LEN);
if (mac_str == NULL) return nullptr;
sprintf(mac_str, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
return mac_str;
}
/**
* 从字符串表示形式转换回MAC地址的二进制数据
* @param mac_str MAC地址的字符串表示
* @return 转换后的MAC地址二进制数据
*/
static uint8_t *from_mac_str(const char *mac_str) {
uint8_t *mac = malloc(ETH_II_MAC_LEN);
memcpy(mac, mac_str, ETH_II_MAC_LEN);
return mac;
}
/**
* 将IP地址转换为字符串表示形式
* @param ip IP地址的二进制数据
* @return 转换后的IP地址字符串
*/
static char *get_ip_str(uint32_t ip) {
struct in_addr addr;
addr.s_addr = htonl(ip);
char *ip_inet = inet_ntoa(addr);
char *ip_str = malloc(strlen(ip_inet) + 1);
strcpy(ip_str, ip_inet);
return ip_str;
}
/**
* 从字符串表示形式转换回IP地址的二进制数据
* @param ip_str IP地址的字符串表示
* @return 转换后的IP地址二进制数据
*/
static uint32_t from_ip_str(char *ip_str) {
struct in_addr ip_addr; // 存储转换后的IP地址
// 使用inet_pton将IP地址字符串转换为网络字节序
if (inet_pton(AF_INET, ip_str, &ip_addr) <= 0) {
perror("inet_pton failed");
return 0;
}
return ip_addr.s_addr;
}
/**
* 获取主机的MAC地址
* @param mac_val 存储主机MAC地址的变量
* @return 成功返回0,失败返回-1
*/
static int host_mac(uint8_t *mac_val) {
const char *mac = getenv("HOST_MAC");
for (int i = 0; i < ETH_II_MAC_LEN; i++) {
if (sscanf(mac + 3 * i, "%2hhx", &mac_val[i]) != 1) {
return -1;
}
}
return 0;
}
/**
* 计算校验和
* @param len 数据的长度
* @return 计算得到的校验和
*/
static uint16_t checksum(void *data, int len) {
uint32_t sum = 0;
uint16_t *ptr = data;
// 遍历数据,按16位单元累加
while (len > 1) {
sum += *ptr++;
if (sum > 0xFFFF) {
sum = (sum & 0xFFFF) + 1; // 如果有进位,将进位加回
}
len -= 2;
}
// 如果长度是奇数,处理最后一个字节
if (len == 1) {
uint8_t last_byte = *(uint8_t *)ptr;
sum += (last_byte << 8); // 高位补齐
if (sum > 0xFFFF) {
sum = (sum & 0xFFFF) + 1;
}
}
// 取反,返回校验和
return ~sum;
}
/**
* 解析以太网II帧
* @param data 待解析的数据包
* @return 解析后的以太网II帧头结构指针
*/
EthII_Hdr *eth_ii_parse(const unsigned char *data);
/**
* 打印以太网II帧信息
* @param eth_ii 以太网II帧头结构指针
*/
void eth_ii_print(const EthII_Hdr *eth_ii);
#define ARP_GRATUITOUS 1
#define ARP_REQUEST 2
/**
* 解析ARP报文
* @param data 待解析的数据包
* @return 解析后的ARP报文头结构指针
*/
Arp_Hdr *arp_parse(const unsigned char *data);
/**
* 打印ARP报文信息
* @param arp ARP报文头结构指针
*/
void arp_print(const Arp_Hdr *arp);
/**
* 发送ARP报文
* @param handle pcap会话句柄
* @param tpa 目标IP地址字符串
* @param type ARP报文类型
* @return 发送成功返回0,否则返回-1
*/
int arp_send(pcap_t *handle, char *tpa, uint8_t type);
#define IPv4_VERSION 4
#define IPv6_VERSION 6
#define IP_TOP_ICMP 1
#define IP_TOP_TCP 6
#define IP_TOP_UDP 17
/**
* 解析IP报文
* @param data 待解析的数据包
* @return 解析后的IP报文头结构指针
*/
Ip_Hdr *ip_parse(const unsigned char *data);
/**
* 校验IP报文的校验和
* @param ip_hdr IP报文头结构指针
* @return 校验和正确返回TRUE,否则返回FALSE
*/
BOOL ip_checksum(Ip_Hdr *ip_hdr);
/**
* 打印IP报文信息
* @param ip_hdr IP报文头结构指针
*/
void ip_print(const Ip_Hdr *ip_hdr);
/**
* 解析ICMP报文
* @param data 待解析的数据包
* @param len 数据包长度
* @return 解析后的ICMP报文头结构指针
*/
Icmp_Hdr *icmp_parse(const unsigned char *data, uint16_t len);
/**
* 校验ICMP报文的校验和
* @param icmp_hdr ICMP报文头结构指针
* @param len 数据包长度
* @return 校验和正确返回TRUE,否则返回FALSE
*/
BOOL icmp_checksum(Icmp_Hdr *icmp_hdr, uint16_t len);
/**
* 打印ICMP报文信息
* @param icmp ICMP报文头结构指针
*/
void icmp_print(const Icmp_Hdr *icmp);
#endif //PRTC_H
三、计算 ICMP 校验和 和上面的校验和代码对照下
计算 ICMP 校验和时,需要把整个 ICMP 报文当作 16 位字的序列来处理:
uint16_t icmp_checksum(const void *data, size_t length) {
uint32_t sum = 0;
const uint16_t *words = data;
// 计算所有16位字的和
while (length > 1) {
sum += *words++;
length -= 2;
}
// 处理剩下的单字节(如果有)
if (length == 1) {
sum += *(const uint8_t *)words;
}
// 把进位加到低16位
while (sum >> 16) {
sum = (sum & 0xffff) + (sum >> 16);
}
// 返回取反后的结果
return ~sum;
}
四、构建和发送 ICMP Echo 请求的示例应用
下面展示了如何构建并发送一个 ICMP Echo 请求:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <netdb.h>
// 计算ICMP校验和的函数(上面已定义)
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "使用方法: %s <目标地址>\n", argv[0]);
return 1;
}
// 创建原始套接字
int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (sockfd < 0) {
perror("socket创建失败");
return 1;
}
// 设置目标地址
struct sockaddr_in dest_addr;
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.sin_family = AF_INET;
// 解析目标地址
if (inet_pton(AF_INET, argv[1], &dest_addr.sin_addr) <= 0) {
struct hostent *host = gethostbyname(argv[1]);
if (!host) {
fprintf(stderr, "无法解析目标地址: %s\n", argv[1]);
close(sockfd);
return 1;
}
memcpy(&dest_addr.sin_addr, host->h_addr_list[0], host->h_length);
}
// 构建ICMP Echo请求
struct icmp_hdr icmp;
memset(&icmp, 0, sizeof(icmp));
icmp.type = ICMP_ECHO; // 8
icmp.code = 0;
icmp.un.echo.id = htons(getpid() & 0xffff); // 进程ID作为标识符
icmp.un.echo.sequence = htons(1); // 序列号
// 计算校验和(先置零)
icmp.checksum = 0;
icmp.checksum = icmp_checksum(&icmp, sizeof(icmp));
// 发送ICMP请求
if (sendto(sockfd, &icmp, sizeof(icmp), 0,
(struct sockaddr *)&dest_addr, sizeof(dest_addr)) < 0) {
perror("sendto失败");
close(sockfd);
return 1;
}
printf("已发送ICMP Echo请求到 %s\n", argv[1]);
close(sockfd);
return 0;
}
注意要点
- 权限要求:创建原始套接字需要 root 权限,因此运行程序时可能需要使用 sudo。
- 字节序处理:在网络传输中,多字节字段要使用网络字节序(大端序),可以借助 htons ()、htonl () 等函数进行转换。
- 接收处理:实际应用中,还需要接收并解析 ICMP 应答,这涉及到对 IP 头部和 ICMP 头部的处理。
- 超时设置:发送请求后,需要设置超时机制来处理可能出现的无响应情况。
五、一个简单的ICMP回显请求程序应用
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <netdb.h>
#include <errno.h>
#define PACKET_SIZE 4096
#define MAX_WAIT_TIME 5
#define MAX_NO_PACKETS 3
char sendpacket[PACKET_SIZE];
char recvpacket[PACKET_SIZE];
int sockfd, datalen = 56;
int nsend = 0, nreceived = 0;
struct sockaddr_in dest_addr;
pid_t pid;
struct timeval tvrecv;
// 校验和函数
unsigned short cal_chksum(unsigned short *addr, int len) {
int nleft = len;
int sum = 0;
unsigned short *w = addr;
unsigned short answer = 0;
// 把ICMP报头二进制数据以2字节为单位累加起来
while (nleft > 1) {
sum += *w++;
nleft -= 2;
}
// 若ICMP报头为奇数个字节,会剩下最后一字节。把最后一个字节视为一个2字节数据的高字节,这个2字节数据的低字节为0,继续累加
if (nleft == 1) {
*(unsigned char *)(&answer) = *(unsigned char *)w;
sum += answer;
}
sum = (sum >> 16) + (sum & 0xffff);
sum += (sum >> 16);
answer = ~sum;
return answer;
}
// 设置ICMP报头
void pack(int pack_no) {
int i, packsize;
struct icmp *icmp;
icmp = (struct icmp *)sendpacket;
icmp->icmp_type = ICMP_ECHO;
icmp->icmp_code = 0;
icmp->icmp_cksum = 0;
icmp->icmp_seq = pack_no;
icmp->icmp_id = pid;
packsize = 8 + datalen;
for (i = 0; i < datalen; i++)
icmp->icmp_data[i] = i;
icmp->icmp_cksum = cal_chksum((unsigned short *)icmp, packsize);
}
// 发送一个ICMP回显请求包
int send_packet() {
int packetsize;
pack(nsend);
packetsize = 8 + datalen;
if (sendto(sockfd, sendpacket, packetsize, 0,
(struct sockaddr *)&dest_addr, sizeof(dest_addr)) < 0) {
perror("sendto error");
return -1;
}
nsend++;
return 0;
}
// 接收一个ICMP回显应答包
int recv_packet() {
int n, fromlen;
extern int errno;
fd_set rfds;
struct timeval tv;
int retval;
fromlen = sizeof(dest_addr);
FD_ZERO(&rfds);
FD_SET(sockfd, &rfds);
tv.tv_sec = MAX_WAIT_TIME;
tv.tv_usec = 0;
// 使用select函数检测套接字是否可读
retval = select(sockfd + 1, &rfds, NULL, NULL, &tv);
if (retval == -1) {
perror("select()");
return -1;
} else if (retval) {
if ((n = recvfrom(sockfd, recvpacket, PACKET_SIZE, 0,
(struct sockaddr *)&dest_addr, &fromlen)) < 0) {
if (errno == EINTR)
return -1;
perror("recvfrom error");
return -1;
}
gettimeofday(&tvrecv, NULL);
return n;
} else {
printf("Request timeout\n");
return -1;
}
}
// 解析接收到的ICMP包
void unpack(int n, struct timeval *tvsend) {
int i, iphdrlen;
struct ip *ip;
struct icmp *icmp;
double rtt;
ip = (struct ip *)recvpacket;
iphdrlen = ip->ip_hl << 2; // 计算IP头部长度
icmp = (struct icmp *)(recvpacket + iphdrlen);
n -= iphdrlen; // 剩下的就是ICMP数据部分的长度
// 检查接收到的是否是我们发送的ICMP_ECHOREPLY包
if (n < 8) {
printf("ICMP packets\'s length is less than 8\n");
return;
}
if ((icmp->icmp_type == ICMP_ECHOREPLY) && (icmp->icmp_id == pid)) {
struct timeval tv;
long long send_t, recv_t;
// 计算往返时间(RTT)
timersub(&tvrecv, tvsend, &tv);
send_t = tvsend->tv_sec * 1000000 + tvsend->tv_usec;
recv_t = tvrecv.tv_sec * 1000000 + tvrecv.tv_usec;
rtt = (double)(recv_t - send_t) / 1000.0;
printf("%d bytes from %s: icmp_seq=%u ttl=%d rtt=%.3f ms\n",
n,
inet_ntoa(dest_addr.sin_addr),
icmp->icmp_seq,
ip->ip_ttl,
rtt);
nreceived++;
} else if (icmp->icmp_type == ICMP_DEST_UNREACH) {
printf("Destination Unreachable\n");
}
}
// 中断处理函数
void statistics(int signo) {
printf("\n----------------PING statistics----------------\n");
printf("%d packets transmitted, %d received, %%%d lost\n",
nsend, nreceived, (nsend - nreceived) * 100 / nsend);
close(sockfd);
exit(1);
}
// 超时处理函数
void timeout(int signo) {
send_packet();
alarm(1);
}
int main(int argc, char *argv[]) {
struct protoent *protocol;
unsigned long inaddr = 0l;
struct hostent *host;
struct timeval tv;
int size = 50 * 1024;
if (argc != 2) {
printf("Usage: %s hostname/IP address\n", argv[0]);
exit(1);
}
// 获取ICMP协议信息
if ((protocol = getprotobyname("icmp")) == NULL) {
perror("getprotobyname");
exit(1);
}
// 创建原始套接字
if ((sockfd = socket(AF_INET, SOCK_RAW, protocol->p_proto)) < 0) {
perror("socket error");
exit(1);
}
// 设置套接字接收缓冲区大小
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));
pid = getpid();
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.sin_family = AF_INET;
// 判断输入是IP地址还是主机名
if (inet_addr(argv[1]) == INADDR_NONE) {
if ((host = gethostbyname(argv[1])) == NULL) {
perror("gethostbyname error");
exit(1);
}
memcpy((char *)&dest_addr.sin_addr, host->h_addr, host->h_length);
} else {
dest_addr.sin_addr.s_addr = inet_addr(argv[1]);
}
printf("PING %s (%s): %d data bytes\n", argv[1],
inet_ntoa(dest_addr.sin_addr), datalen);
// 注册信号处理函数
signal(SIGINT, statistics);
signal(SIGALRM, timeout);
// 开始发送和接收ICMP包
alarm(1);
while (1) {
gettimeofday(&tv, NULL);
if (recv_packet() > 0)
unpack(recv_packet, &tv);
}
return 0;
}
无他多读源代码,多分析优秀源代码,多写高质量代码!!!