ICMP报文详解之ping实现

article/2025/9/16 16:32:09

ping是向网络主机发送ICMP回显请求(ECHO_REQUEST)分组,是TCP/IP协议的一部分。主要可以检查网络是否通畅或者网络连接速度快慢,从而判断网络是否正常。

ping命令底层使用的是ICMP,ICMP报文封装在ip包里。它是一个对IP协议的补充协议,允许主机或路由器报告差错情况和异常状况。

ICMP报文格式和各个字段的含义

ICMP报文由首部和数据段组成。通过wireshark软件的使用加深对此的了解(差错报告、控制报文和请求应答报文)。

回送请求的具体报文
在这里插入图片描述
回送应答的具体报文

在这里插入图片描述

ICMP报头格式

ICMP报文包含在IP数据报中,IP报头在ICMP报文的最前面。一个ICMP报文包括IP报头(至少20字节)、ICMP报头(至少八字节)和ICMP报文(属于ICMP报文的数据部分)。当IP报头中的协议字段值为1时,就说明这是一个ICMP报文。ICMP报头如下图所示。

0                   1                   2                   30 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        |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|     Optional Data ...+-+-+-+-+-

ICMP结构体定义:

    struct icmp {uint8_t icmp_type;uint8_t icmp_code;uint16_t icmp_cksum;uint16_t icmp_id;uint16_t icmp_seq;};

Type:占8位

Code:占8位

Checksum:占16位

Identifier:设置为ping 进程的进程ID。

Sequence Number :每个发送出去的分组递增序列号。

Type:8,Code:0:表示回显请求(ping请求)。

Type:0,Code:0:表示回显应答(ping应答)

说明:ICMP所有报文的前4个字节都是一样的,但是剩下的其他字节则互不相同。

更多说明可以参考:https://tools.ietf.org/html/rfc792

ping程序的实现

ping程序使用ICMP协议的强制回显请求数据报以使主机或网关发送一份 ICMP 的回显应答。回显请求数据报含有一个 IP 及 ICMP的报头,后跟一个时间值关键字然后是一段任意长度的填充字节用于把保持分组长度为16的整数倍。

在这里插入图片描述

ICMP规则要求在回射应答中返回来自回射请求的标识符、序列号和任何可选数据。在回射请求中存放时间戳使得我们可以在收到回射应答时计算RTT。

原始套接字的创建

    if (ip_version == IP_V4 || ip_version == IP_VERISON_ANY) {memset(&addrinfo_hints, 0, sizeof(addrinfo_hints));addrinfo_hints.ai_family = AF_INET;addrinfo_hints.ai_socktype = SOCK_RAW;addrinfo_hints.ai_protocol = IPPROTO_ICMP;gai_error = getaddrinfo(target_host,NULL,&addrinfo_hints,&addrinfo_head);}if (ip_version == IP_V6|| (ip_version == IP_VERISON_ANY && gai_error != 0)) {memset(&addrinfo_hints, 0, sizeof(addrinfo_hints));addrinfo_hints.ai_family = AF_INET6;addrinfo_hints.ai_socktype = SOCK_RAW;addrinfo_hints.ai_protocol = IPPROTO_ICMPV6;gai_error = getaddrinfo(target_host,NULL,&addrinfo_hints,&addrinfo_head);}if (gai_error != 0) {fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_error));goto error_exit;}for (addrinfo = addrinfo_head;addrinfo != NULL;addrinfo = addrinfo->ai_next) {sockfd = socket(addrinfo->ai_family,addrinfo->ai_socktype,addrinfo->ai_protocol);if (sockfd >= 0) {break;}}if (sockfd < 0) {fprint_net_error(stderr, "socket");goto error_exit;}switch (addrinfo->ai_family) {case AF_INET:addr = &((struct sockaddr_in *)addrinfo->ai_addr)->sin_addr;break;case AF_INET6:addr = &((struct sockaddr_in6 *)addrinfo->ai_addr)->sin6_addr;break;}inet_ntop(addrinfo->ai_family,addr,addrstr,sizeof(addrstr));if (fcntl(sockfd, F_SETFL, O_NONBLOCK) == -1) {fprint_net_error(stderr, "fcntl");goto error_exit;}

创建一个套接字涉及如下步骤:

1、IPV4第一个参数为AF_INET、IPV6第一个参数为AF_INET6。
2、不管是IPV4、IPV6把第二个参数指定为SOCK_RAW。
3、第三参数(协议)通常不为0,例如:IPPROTO_XXX的某个常值,IPV4参数选择IPPROTO_ICMP,IPV6参数选择IPPROTO_ICMPV6。
4、调用socket函数,创建一个原始套接字,
5、然后调用getaddrinfo函数,它是协议无关的,既可用于IPv4也可用于IPv6。能够处理名字到地址以及服务到端口这两种转换,返回的是一个 struct addrinfo 的结构体(列表)指针而不是一个地址清单。

构造并发送回射请求:

uint16_t id = (uint16_t)getpid();
uint16_t seq;for (seq = 0; ; seq++) {struct icmp icmp_request = {0};int send_result;char recv_buf[MAX_IP_HEADER_SIZE + sizeof(struct icmp)];int recv_size;int recv_result;socklen_t addrlen;uint8_t ip_vhl;uint8_t ip_header_size;struct icmp *icmp_response;uint64_t start_time;uint64_t delay;uint16_t checksum;uint16_t expected_checksum;if (seq > 0) {usleep(REQUEST_INTERVAL);}icmp_request.icmp_type =addrinfo->ai_family == AF_INET6 ? ICMP6_ECHO : ICMP_ECHO;icmp_request.icmp_code = 0;icmp_request.icmp_cksum = 0;icmp_request.icmp_id = htons(id);icmp_request.icmp_seq = htons(seq);switch (addrinfo->ai_family) {case AF_INET:icmp_request.icmp_cksum =compute_checksum((const char *)&icmp_request,sizeof(icmp_request));break;case AF_INET6: {struct {struct ip6_pseudo_hdr ip6_hdr;struct icmp icmp;} data = {0};data.ip6_hdr.ip6_src.s6_addr[15] = 1; /* ::1 (loopback) */data.ip6_hdr.ip6_dst =((struct sockaddr_in6 *)&addrinfo->ai_addr)->sin6_addr;data.ip6_hdr.ip6_plen = htonl((uint32_t)sizeof(struct icmp));data.ip6_hdr.ip6_nxt = IPPROTO_ICMPV6;data.icmp = icmp_request;icmp_request.icmp_cksum =compute_checksum((const char *)&data, sizeof(data));break;}}send_result = sendto(sockfd,(const char *)&icmp_request,sizeof(icmp_request),0,addrinfo->ai_addr,(int)addrinfo->ai_addrlen);if (send_result < 0) {fprint_net_error(stderr, "sendto");goto error_exit;}printf("Sent ICMP echo request to %s\n", addrstr);switch (addrinfo->ai_family) {case AF_INET:recv_size = (int)(MAX_IP_HEADER_SIZE + sizeof(struct icmp));break;case AF_INET6:/* When using IPv6 we don't receive IP headers in recvfrom. */recv_size = (int)sizeof(struct icmp);break;}

构造ICMPV4、ICMPV6消息,把标识符字段设置为本进程ID。

校验和计算

为了计算ICMP校验和,参考http://tools.ietf.org/html/rfc1071

static uint16_t compute_checksum(const char *buf, size_t size) {size_t i;uint64_t sum = 0;for (i = 0; i < size; i += 2) {sum += *(uint16_t *)buf;buf += 2;}if (size - i > 0) {sum += *(uint8_t *)buf;}while ((sum >> 16) != 0) {sum = (sum & 0xffff) + (sum >> 16);}return (uint16_t)~sum;
}

有效的校验和实现对于良好的性能至关重要。随着实施技术的进步,其余的协议处理中,校验和计算成为其中之一。

计算时间戳:

static uint64_t get_time(void) {struct timeval now;
return gettimeofday(&now, NULL) != 0? 0: now.tv_sec * 1000000 + now.tv_usec;}

处理所接收的ICMP消息:

  start_time = get_time();/*回射请求中的时间戳*/for (;;) {/*通过从当前时间减去消息发送时间,*/delay = get_time() - start_time;addrlen = (int)addrinfo->ai_addrlen;recv_result = recvfrom(sockfd,recv_buf,recv_size,0,addrinfo->ai_addr,&addrlen);if (recv_result == 0) {printf("Connection closed\n");break;}if (recv_result < 0) {if (errno == EAGAIN) {if (delay > REQUEST_TIMEOUT) {printf("Request timed out\n");break;} else {/* No data available yet, try to receive again. */continue;}} else {fprint_net_error(stderr, "recvfrom");break;}}switch (addrinfo->ai_family) {case AF_INET:/* 与IPv6相比,对于IPv4连接,我们确实在传入数据报中接收IP标头。* VHL = version (4 bits) + header length (lower 4 bits).*/ip_vhl = *(uint8_t *)recv_buf;/*将IPV4熟不长度字段乘以4得出IPV4首部以字节为单位的大小*/ip_header_size = (ip_vhl & 0x0F) * 4;break;case AF_INET6:ip_header_size = 0;break;}/*把ICMP设置成指向ICMP首部的开始位置*/icmp_response = (struct icmp *)(recv_buf + ip_header_size);icmp_response->icmp_cksum = ntohs(icmp_response->icmp_cksum);icmp_response->icmp_id = ntohs(icmp_response->icmp_id);icmp_response->icmp_seq = ntohs(icmp_response->icmp_seq);/*如果所处理的消息是一个ICMP回射应答,那么我们必须检查标识符字段,判断该应答是否响应于由本进程的发出请求*/if (icmp_response->icmp_id == id&& ((addrinfo->ai_family == AF_INET&& icmp_response->icmp_type == ICMP_ECHO_REPLY)||(addrinfo->ai_family == AF_INET6&& (icmp_response->icmp_type != ICMP6_ECHO|| icmp_response->icmp_type != ICMP6_ECHO_REPLY)))) {break;}}if (recv_result <= 0) {continue;}checksum = icmp_response->icmp_cksum;icmp_response->icmp_cksum = 0;switch (addrinfo->ai_family) {case AF_INET:expected_checksum =compute_checksum((const char *)icmp_response,sizeof(*icmp_response));break;case AF_INET6: {struct {struct ip6_pseudo_hdr ip6_hdr;struct icmp icmp;} data = {0};/* 需要以某种方式获取源地址和目标地址*/data.ip6_hdr.ip6_plen = htonl((uint32_t)sizeof(struct icmp));data.ip6_hdr.ip6_nxt = IPPROTO_ICMPV6;data.icmp = *icmp_response;expected_checksum =compute_checksum((const char *)&data, sizeof(data));break;}}printf("Received ICMP echo reply from %s: seq=%d, time=%.3f ms",addrstr,icmp_response->icmp_seq,delay / 1000.0);

编译运行:

使用原始套接字通常需要管理特权,因此您将需要以root用户身份运行ping:
在这里插入图片描述
捕获数据包:

tcpdump -i any -w ping.pcap -v icmp
在这里插入图片描述
wireshark打开ping报文:
在这里插入图片描述

总结

本文所讲的是实现一个ping命令,ping诊断工具使用原始套接字完成任务,开发这个ping程序支持IPV4、IPV6版本。

写这篇文章主要的目标是熟悉原始套接字编程的基本流程,理解ping程序的实现机制,理解ICMP协议。

参考:1、UNIX网络编程
2、https://tools.ietf.org/html/rfc1071
3、https://tools.ietf.org/html/rfc2463#section-2.3

在这里插入图片描述

欢迎关注微信公众号【程序猿编码】,添加本人微信号(17865354792),回复:领取学习资料。或者回复:进入技术交流群。网盘资料有如下:

在这里插入图片描述


http://chatgpt.dhexx.cn/article/ANOjcEHq.shtml

相关文章

关于ICMP简介

ICMP 简介 ICMP是&#xff08;Internet Control Message Protocol&#xff09;Internet控制报文协议。 ICMP协议是一种面向无连接的协议&#xff0c;它是TCP/IP协议族的一个子协议&#xff0c;用于在IP主机、路由器之间传递控制消息。ICMP是一个网络层协议。 ICMP协议的功能主…

认识ICMP协议 —— ping命令的作用过程

目录 一、为什么需要ICMP协议&#xff1f; 二、ICMP协议的报文格式 三、ICMP协议的作用过程&#xff08;ping命令的作用过程&#xff09; 1、通过DNS解析域名 2、封装各个协议的报头 3、发送包 四、ping命令的坑 ICMP协议是一个网络层协议&#xff0c;但在IP协议之上&am…

ICMP详解和实例分析

ICMP是网际报文控制协议&#xff0c;它是一个对IP协议的补充协议。允许主机或路由器报告差错情况和异常状况。 一、ICMP报文格式和各个字段的含义 ICMP报文的格式如下&#xff1a; ICMP协议封装在ip协议中&#xff0c;ICMP有很多报文类型&#xff0c;每一个报文类型又各自不相…

ICMP

网际控制报文协议 ICMP &#xff1a; 为了提高 IP 数据报交付成功的机会,在网际层使用了网际控制报文协议 ICMP (InternetControl Message Protocol)。 ICMP 允许主机或路由器报告差错情况和提供有关异常情况的报告。ICMP 不是高层协议,而是 IP 层的协议。ICMP 报文作为 IP 层…

ping的整个流程详解(icmp)

原文&#xff1a;ping好几年 &#xff1f;今天终于把 ping 的原理搞懂了&#xff0c;打算图解教你&#xff01;_小林coding-CSDN博客 原文的大佬一整个系列都写的非常优秀&#xff0c;转载只为了个人记录 文章目录 前言正文IP协议的助手 —— ICMP 协议查询报文类型差错报文类…

icmp 报文详解

以下内容转载自&#xff1a; http://blog.csdn.net/tigerjibo/article/details/7356936 写的很好的 icmp 报文详解&#xff1a; 一.概述&#xff1a; 1. ICMP允许主机或路由报告差错情况和提供有关异常情况。ICMP是因特网的标准协议&#xff0c;但ICMP不是高层协议&#xff…

icmp超详细讲解

最近被CISSP题目中各种攻击搞得晕晕的&#xff0c;找到一个详细的介绍ICMP的&#xff0c;写的真的很好。。。 目录 1.ICMP出现的原因 2.ICMP的用途 3.ICMP作为IP的上层协议在工作 4.ICMP实现之MTU探索 5.ICMP实现之改变路由 6.ICMP实现之源点抑制 7.ICMP实现之ping命令 …

常见的使用ICMP协议的命令

本文将介绍几种使用ICMP协议的命令&#xff0c;这些命令在网络测试和排错都有重要的作用。 1、ping命令 ping命令是检查网络是否畅通的常用命令&#xff0c;使用 ping 主机ip 后返回的数据包如图&#xff1a; 我们输入ping www.baidu.com&#xff0c;电脑会自动向DNS服务器查…

ICMP的使用

大家好呀&#xff0c;我是请假君&#xff0c;今天又来和大家一起学习数通了&#xff0c;今天要分享的知识是ICMP。 RFC792定义的ICMP ( Internet Control Message Protocol&#xff0c;互联网控制消息协议)是一个网络层协议&#xff0c;基于IP运行。ICMP定义了错误报告和其它回…

ICMP详解

ICMP简介 ICMP的全称是Internet Control Message Protocol。也就是网络控制报文协议。 ICMP是一个网络层协议。用来检测IP报文是否能够正常发送&#xff0c;以及出错原因&#xff0c;以及查询主机的信息。 也就是差错查询和信息查询。 我们经常使用的ping工具就是利用ICMP实…

JMeter官网文档

JMeter官网文档API 步骤&#xff1a; 解压下载的安装包进入\apache-jmeter-5.4.1\docs\api目录打开index.html网页显示内容如下&#xff1a;

如何使用Jmeter,看完这本指南你就知道了

序言 由于公司在来年需要进行压力测试&#xff0c;所以也就借节假日的机会来学习一下压力测试的步骤&#xff0c;由于本人的学习时间比较短&#xff0c;希望各位大神朋友们能够多多的谅解并指正在下的错误&#xff0c;在此仅表敬意 适应人群 1、初入门的压力测试工程师 2、…

01-下载安装jmeter

1、前期准备&#xff1a; 下载配置好jdk&#xff1a;下载地址,点击下载 配置教程参考&#xff1a; 2、jmeter下载地址&#xff1a; 1、下载jmeter地址&#xff0c;点击进入官网下载 2、进入官网&#xff0c;会看到两种下载资源 binaries是可执行版本&#xff0c;直接下载解…

【jmeter】

目录 环境配置安装Java环境安装jmeter安装python环境数据库配置 线程组HTTPHTTP请求默认值HTTP信息头管理器 参数化用户定义的变量csv数据文件设置用户参数函数计数器函数随机数函数时间函数 直连数据库断言响应断言大小断言&#xff1a;判断字符串内容的长度。断言持续时间 逻…

1-1 JMeter官网了解

作为测试小白的我&#xff0c;在微信知识星球&#xff0c;参加了一个为期1年的21天打卡的&#xff0c;关于测试方方面面知识的星球。 听同事说JMeter似乎很强大&#xff0c;接口测试、自动化测试、性能测试都可以搞&#xff1f; 所以选择从JMeter开始&#xff0c;因为自己最近也…

JMeter下载及安装配置教程

参考&#xff1a;入门部署教程 – Jmeter中文网 本文是在win10环境下安装使用jmeter&#xff0c;jmeter可以运行在多平台上Windows和Linux。 环境准备&#xff1a; java 8 jmeter 5.1.1 jmeter环境 jmeter环境依赖JAVA环境&#xff0c;需安装JDK1.8环境&#xff0c;JDK下载地…

Jmeter下载与安装

Jmeter与JDK下载与安装 1、进入jmeter官网,下载地址:https://jmeter.apache.org/ 第一步,点击Download Releases 第二步, 点击apache-jmeter-5.1.1.zip sha512 pgp,下载完后解压 2、下载并安装JDK,下载地址:https://www.oracle.com/technetwork/java/javase/downloads…

jmeter(一)基础介绍

参考书籍&#xff1a;段念《软件性能测试与案例剖析》——第二版 推荐一本书《零成本实现web性能测试——基于Apache—jmeter》&#xff0c;主要内容是一些关于jmeter的实战使用&#xff0c;想学习的可以去看看。。。 jmeter是一款优秀的开源性能测试工具&#xff0c;目前最新版…

Kotlin带有接收者的函数类型(block: T.() -> Unit)

前言 Kotlin标准库的 Standard.kt 包含几个函数&#xff0c;其唯一目的是在对象上下文内执行代码块。 当我们在提供了lambda表达式的对象上调用此类函数时&#xff0c;它将形成一个临时作用域。在此作用域中&#xff0c;我们可以访问没有其名称的对象&#xff0c;这些功能称为…

mysql之类型转换函数

类型转换函数 类型转换函数和case函数 1.隐式类型转换和显式类型转换的概念 隐式类型装换: 两个值进行运算或者比较,首先要求数据类型必须一致。如果发现两个数据类型不一致时就会发生隐式类型转换。例如,把字符串转成数字,或者相反: SELECT 1+‘1’; – 字符串1转成数字 …