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

2022-08-24 15:53:48 浏览数 (1)

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

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报头如下图所示。

代码语言:javascript复制
    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结构体定义:

代码语言:javascript复制
    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。

原始套接字的创建

代码语言:javascript复制
    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: %sn", 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 的结构体(列表)指针而不是一个地址清单。

构造并发送回射请求:

代码语言:javascript复制
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 %sn", 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

代码语言:javascript复制
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;
}

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

计算时间戳:

代码语言:javascript复制
static uint64_t get_time(void) { 
   

struct timeval now;
return gettimeofday(&now, NULL) != 0
	? 0
	: now.tv_sec * 1000000   now.tv_usec;

}

处理所接收的ICMP消息:

代码语言:javascript复制
  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 closedn");
                break;
            }
            if (recv_result < 0) { 
   

                if (errno == EAGAIN) { 
   

                    if (delay > REQUEST_TIMEOUT) { 
   
                        printf("Request timed outn");
                        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

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

0 人点赞