ICMP报文详解之ping实现「建议收藏」

ICMP报文详解之ping实现「建议收藏」ping是向网络主机发送ICMP回显请求(ECHO_REQUEST)分组,是TCP/IP协议的一部分。主要可以检查网络是否通畅或者网络连接速度快慢,从而判断网络是否正常。ping命令底层使用的是ICMP,ICMP报文封装在ip包里。它是一个对IP协议的补充协议,允许主机或路由器报告差错情况和异常状况。ICMP报文格式和各个字段的含义…

大家好,又见面了,我是你们的朋友全栈君。

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                   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        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     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),回复:领取学习资料。或者回复:进入技术交流群。网盘资料有如下:

在这里插入图片描述

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/140796.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • 如何理解95%置信区间_95的置信区间和90的置信区间

    如何理解95%置信区间_95的置信区间和90的置信区间1.点估计与区间估计首先我们看看点估计的含义:是用样本统计量来估计总体参数,因为样本统计量为数轴上某一点值,估计的结果也以一个点的数值表示,所以称为点估计。点估计虽然给出了未知参数的估计值,但是未给出估计值的可靠程度,即估计值偏离未知参数真实值的程度。接下来看下区间估计:给定置信水平,根据估计值确定真实值可能出现的区间范围,该区间通常以估计值为中心,该区间则为置信区间。2.中心…

  • idea企业开发之插件推荐

    idea企业开发之插件推荐推荐在企业开发中,使用IntelliJIDEA时常使用到的插件。

  • visdom 使用教程

    visdom 使用教程visdom教程visdom安装与启动服务visdom常用功能image窗口:图像显示与更新窗口显示images窗口:多个图像显示与更新窗口显示text窗口:显示文本与更新文本line窗口:绘制折线图与更新折线图scatter窗口:绘制散点图与更新散点图visdom安装与启动服务安装visdompipinstallvisdom打开服务python-mvisdom.server…

  • Android opencv人脸识别

    Android opencv人脸识别opencv人脸识别Androidopencv人脸识别图片:![在这里插入图片描述](https://img-blog.csdnimg.cn/2019012214185895.png//开始人脸检测publicvoidstart(){n_Start();}//停止人脸检测publicvoidstop(){n_Stop();}//设…

  • RBF神经网络理论与实现「建议收藏」

    RBF神经网络理论与实现「建议收藏」前言最近发现有挺多人喜欢径向基函数(RadialBasisFunction,RBF)神经网络,其实它就是将RBF作为神经网络层间的一种连接方式而已。这里做一个简单的描述和找了个代码解读。之前也写过一篇,不过排版不好看,可以戳这里跳转国际惯例,参考博客:维基百科径向基函数《模式识别与智能计算——matlab技术实现第三版》第6.3章节《matlab神经网络43个案例分析》第7章节tensorflow2.0实现RBF理论基本思想用RBF作为隐单元的“基”构成隐藏层空间

    2022年10月24日
  • 京东云服务器使用教程视频_京东通信app下载

    京东云服务器使用教程视频_京东通信app下载形势分析对公司而言,服务器并不是大事。互联网公司都有服务器和机房。但对个人开发者而言,服务器长久以来确是一大难题。但近年,国外亚马逊牵头开始做AWS云服务,并迅速获得极大成功。国内阿里巴巴及时跟进,推出阿里云平台。服务器对个人开发者而言不再是遥不可及,反而变得触手可及。甚至很多企业不再自己搭建服务器,转而使用云服务平台以节省成本。目前国内云服务平台已是百家争鸣,比起早年互联网环境已好很多。除阿里云外

    2022年10月14日

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号