ICMP报文格式,代码实现,及应用

一、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;
}

注意要点

  1. 权限要求:创建原始套接字需要 root 权限,因此运行程序时可能需要使用 sudo。
  2. 字节序处理:在网络传输中,多字节字段要使用网络字节序(大端序),可以借助 htons ()、htonl () 等函数进行转换。
  3. 接收处理:实际应用中,还需要接收并解析 ICMP 应答,这涉及到对 IP 头部和 ICMP 头部的处理。
  4. 超时设置:发送请求后,需要设置超时机制来处理可能出现的无响应情况。

五、一个简单的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;
}

无他多读源代码,多分析优秀源代码,多写高质量代码!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值