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

在这里插入图片描述

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

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

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • shell脚本常用命令及操作_shell脚本执行命令

    shell脚本常用命令及操作_shell脚本执行命令Linux常用命令ls常用命令ls-a列举出当前目录所有文件,包括隐藏文件ls-r正常列举顺序反序列化输出ls-t按照文件修改时间排序输出ls-S按照文件大小排序输出ls-l列举出文件名、文件的权限、所有者、文件大小等信息cd常用命令cd/usr/local/src切换到指定路径(使用绝对路径方式)cd~进入当前用户的家目录cd-进入上次目录cd..进入上一级目录cd.进入当前目录rm常用命令rm…

    2022年10月9日
    0
  • 深度学习中的9种归一化方法概述

    深度学习中的9种归一化方法概述9种归一化(Normalization)方法概述

    2022年10月28日
    0
  • 模型评估

    模型评估

    2021年5月19日
    184
  • 浅谈WeakHashMap

    浅谈WeakHashMapJavaWeakHashMap到底Weak在哪里,它真的很弱吗?WeakHashMap的适用场景是什么,使用时需要注意些什么?弱引用和强引用对JavaGC有什么不同影响?本文将给出清晰而简洁

    2022年7月2日
    24
  • 实验室仪器管理系统_实验室设备管理系统代码

    实验室仪器管理系统_实验室设备管理系统代码实验室设备管理系统主要包括:实验室设备信息的管理模块,实验室设备信息的浏览查询模块,设备事故记录模块,设备资料管理模块设备的损坏管理模块,设备损坏信息浏览查询,设备类别设置,系统用户的管理。通过本系统,可以更加有效的管理学生实验室设备信息开发技术:php,mysql,apache课题名称:实验室设备管理系统1)系统简介每学年要对实验室设备使用情况进行统计、更新。其中:(1)对于已彻底损坏的做报废处理,同时详细记录有关信息。(2)对于由严重问题(故障)的要及时修理,并记录修理日期、设备名、编号

    2022年10月13日
    0
  • Springboot面试问题总结

    Springboot面试问题总结Q:什么是springboot?A:多年来,随着新功能的增加,spring变得越来越复杂。只需访问页面https://spring.io/projects,我们将看到所有在应用程序中使用的不同功能的spring项目。如果必须启动一个新的spring项目,我们必须添加构建路径或maven依赖项,配置applicationserver,添加spring配置。因此,启动一个新的spring项…

    2022年6月6日
    26

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

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