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)


相关推荐

  • 501,502,503,504的区别_412状态码

    501,502,503,504的区别_412状态码502badgateway顾名思义网关错误后端服务器tomcat没有起来,应用服务的问题(前提是接入层7层正常的情况下)。应用服务问题一种是应用本身问题;另一种是因为依赖服务问题比如依赖服务RT高,依赖的服务有大的读取(mysql慢查,http等),以至于调用方超过超时read时间;服务集群压力大时,也会出现502超时(502理解为不可响应或响应不过来,其实还是不可响应)。504…

  • 一致性哈希算法的问题

    一致性哈希算法的问题本文将从如下三个方面探探一致性哈希算法一致性哈希算法经典实用场景 一致性哈希算法通常不适合用于服务类负载均衡 面试应对之策1、一致性哈希算法经典使用场景在数据库存储领域如果单表数据量很大,通常会采用分库分表,同样在缓存领域同样需要分库,下面以一个非常常见的Redis分库架构为例进行阐述。将上述3个Redis节点称之为分片,每一个节点存储部分数据,期间需要使用负载均衡算法,将数据尽量分摊到各个节点,充分发挥分布式的优势,提升系统缓存访问的性能。在分布缓存领域,对数据存在新增与..

  • SpringBoot + Vue 开发前后端分离的旅游管理系统

    SpringBoot + Vue 开发前后端分离的旅游管理系统旅游管理系统项目简介需求分析数据库建表环境搭建引入依赖(pom.xml)配置文件(application.properties)前端页面注册功能验证码工具类项目简介所需技术栈:后端技术栈:springboot+mybatis前后端分离:axios、json前端技术栈、技术架构:Vue、node.js要求:了解Vue组件之前的知识对springboot+mybatis较熟悉开发流程:需求分析库表设计编码(项目环境搭建+编码)项目调试项目部署上线需求分析

  • ASP.NET MVC (五、HttpClient接口解析)

    ASP.NET MVC (五、HttpClient接口解析)前言:MVC对于已经跨域的接口进行解析是个比较容易的事情。况且在第四章节的时候已经通过Ajax进行了页面的解析测试,效果也比较明显。所以本章节从容应对。这个世界上只有一种真正的英雄主义:认清生活的真相,并且仍然热爱它。难道向上攀爬的那条路,不是比站在顶峰更让人心潮澎湃吗?1、MVC项目创建在解决方案上点击【鼠标右键】,依次选择【添加】【新建项目】选择【ASP.NETWeb应用程序(.NETFramework)】项目,点击【下一步】输入项目名称,这里是【M…

  • 在c语言中提供的条件运算符是什么_c语言条件运算符有哪些

    在c语言中提供的条件运算符是什么_c语言条件运算符有哪些Date:2020-02-01Author:SolerHOBook:CPrimerPlusDescription:条件运算符:?:C提供了条件表达式(conditionalexpression)作为表达ifelse语句的一种便捷方式,表达式使用:?:条件运算符。运算符分为两部分,需要3个运算对象。也就是所谓的三元运算符,也是C语言中的唯一的三元运算符。例如:x…

  • 设计模式 | 适配器模式及典型应用

    设计模式 | 适配器模式及典型应用适配器模式适配器模式(AdapterPattern):将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。在适配器模式中,我们通过增加一个新的适配器类来解决接口不兼容的问题,使得原本没有任何关系的类可以协同工作。根据适配器类与适配者类的关系不同,适配器模式可分为对象适配器和类适…

发表回复

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

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