基于ICMP原始套接字实现Ping与Tracert(Windows)

一、简介Ping与Tracert命令

1.1 Ping

  Ping命令是用于测试网络连接状态的工具,其核心功能是通过发送和接收特定数据包来验证目标主机的可达性并评估网络性能。
  若收到应答,表明当前主机与目标主机之间存在连通的物理路径。但需注意:Ping成功仅保证基础网络通畅,不代表应用层服务正常。

在命令行以管理员身份执行ping指令:

ping 127.0.0.1

运行示例
  TTL是IP数据包头部的一个8位字段,表示数据包在网络中的最大存活时间或允许经过的最大路由器跳数。每经过一个路由器(或三层设备),TTL值减1。当TTL降为0时,数据包被丢弃,并向发送端返回​​ICMP超时消息。

  通过TTL值可推断数据包经过的路由器数量(例如TTL=119表示数据包通过了约9个路由节点,Windows默认初始TTL = 128)

1.2 Tracert

   Tracert(Traceroute)是网络诊断中用于追踪数据包传输路径的核心工具,其通过分析网络路径中的节点信息,帮助定位延迟、故障或路由异常。
Tracert通过发送​​递增TTL值的数据包​​(初始TTL=1,逐渐加1)实现路径追踪。

交互流程:

  1. 源端发送TTL=1的探测包,第一跳路由器返回超时信息。
  2. 源端逐步增加TTL值,逐跳捕获中间节点的IP地址和响应时间。
  3. 最终探测到目标主机后,输出完整路径及延迟数据

在命令行以管理员身份执行Tracert指令:

tracert www.baidu.com

在这里插入图片描述

二、ICMP协议简介

由于IP协议是不可靠,无连接、无状态的协议,为了解决IP协议这些缺点带来的问题,设计了ICMP协议。
​​ICMP协议​​作为IP协议的附属协议,专门用于报告网络层中的错误(如目标不可达、超时等)和传递控制信息(如路由优化、网络诊断等),弥补了IP协议缺乏反馈机制的缺陷。

ICMP是承载在IP内部的,如图所示:
在这里插入图片描述

ICMP协议的报文格式

在这里插入图片描述
ICMP报文通过Type与Code定义ICMP报文类型
例如
Type:8, Code:0 为ping请求,Type:0, Code:0为ping应答

ICMP的报文分为差错报文与查询报文两类

基于ICMP主要提供两种命令ping与tracert,通过ping工具发送ICMP回显请求消息探测网络的连通性(对端回应ICMP回显应答请求),通过tracert工具发送ICMP回显请求来探测去往某目的网路的路径信息(修改ICMP回显请请求的TTL的值,通过对端回应的Type为11的ICMP差错报文来得到路径信息)

三、C++实现Ping与Tracert

3.1 引入相关库与头文件

#include <winsock2.h>
#include <ws2tcpip.h>
#include <iphlpapi.h>
#include <iostream>
#include <chrono>
#include <thread>
#pragma comment(lib, "ws2_32.lib")
#pragma comment(lib, "iphlpapi.lib")

   winsock2.h、ws2tcpip.h等,用于网络编程。同时,通过 #pragma comment链接了ws2_32.lib和iphlpapi.lib库,这些库提供了Socket编程和IP帮助函数的功能。这部分是初始化Winsock环境所必须的,确保程序可以使用网络功能。

3.2 定义相关宏与结构体

#define ICMP_ECHO_REQUEST 8  // ICMP回显请求(Ping请求)
#define ICMP_ECHO_REPLY 0    // ICMP回显应答(Ping响应)
#define ICMP_TIMEOUT 11		// ICMP超时报文(TTL耗尽或重组超时)
#define DEFAULT_PACKET_SIZE 32  // ICMP数据部分默认长度(字节)
#define MAX_HOPS 30             // Tracert最大跳数限制
#define TIMEOUT_MS 2000         // 接收响应的超时时间(2秒)
#define MAX_ATTEMPTS 3          // 每个TTL值的探测尝试次数

typedef struct {
    BYTE type;       // ICMP类型(如8=请求,0=应答)
    BYTE code;       // 子类型代码(如网络不可达Code=0)
    USHORT checksum; // 校验和(反码求和算法)
    USHORT id;       // 进程标识符(用于匹配请求响应)
    USHORT seq;      // 序列号(追踪报文顺序)
    ULONG timestamp; // 时间戳(计算往返时延RTT)
} ICMP_HEADER;

typedef struct {
    BYTE hdr_len :4;    // IP头长度(以4字节为单位)
    BYTE version :4;   // IP版本(4=IPv4)
    BYTE tos;          // 服务类型(QoS优先级标记)
    USHORT total_len;   // 总长度(IP头+数据)
    USHORT identifier;  // 数据包标识(分片重组用)
    USHORT frag_flags;  // 分片标志(DF/MF)和偏移量
    BYTE ttl;          // 生存时间(TTL跳数限制)
    BYTE protocol;     // 上层协议类型(1=ICMP)
    USHORT checksum;    // IP头校验和
    ULONG src_addr;     // 源IP地址(网络字节序)
    ULONG dst_addr;     // 目标IP地址(网络字节序)
} IP_HEADER;

定义ICMP头和IP头结构体以及他们一些相关的宏定义

3.3 校验和计算函数

用于计算ICMP头部的校验和的函数,这是ICMP协议要求的步骤(见ICMP协议格式),确保数据完整性。ICMP报文需要计算校验和,该函数的实现符合标准方法,即对16位字进行累加并取反。

USHORT CalculateChecksum(USHORT* buffer, int size) {
    ULONG cksum = 0;
    while(size > 1) {
        cksum += *buffer++;
        size -= sizeof(USHORT);
    }
    if(size) cksum += *(UCHAR*)buffer;
    cksum = (cksum >> 16) + (cksum & 0xffff);
    return (USHORT)(~cksum);
}

3.4 基于原始套接字实现Ping命令

3.4.1 创建原始套接字+目标地址配置​

创建原始套接字:
AF_INET:IPV4族,
SOCK_RAW:原始套接字(可以直接修改ICMP头部)
IPPROTO_ICMP:使用ICMP协议
并检验是否出错(INVALID_SOCKET)

  SOCKET sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    if(sock == INVALID_SOCKET) {
        std::cerr << "Socket error: " << WSAGetLastError() << std::endl;
        return;
    }

解析传入目标地址(target)
即:将目标地址解析成二进制格式(dest.sin_adder)

 sockaddr_in dest = {0};
    dest.sin_family = AF_INET;
    InetPtonA(AF_INET, target, &dest.sin_addr);

3.4.2 构造ICMP报文

发送缓冲区包含ICMP头+32字节数据(默认长度)
type = 8,标志回显请求

char sendBuf[sizeof(ICMP_HEADER) + DEFAULT_PACKET_SIZE] = {0};
    ICMP_HEADER* icmp = (ICMP_HEADER*)sendBuf;
    icmp->type = ICMP_ECHO_REQUEST; //标志回显请求
    icmp->id = GetCurrentProcessId();

设置接收超时
设置接收超时为2000ms(避免无限等待响应)#define TIMEOUT_MS 2000

DWORD timeout = TIMEOUT_MS;
    setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(timeout));

3.4.3 发送ICMP回显请求+ 响应接收与解析​​

ICMP协议默认一次发送4个回显请求

for(int i=0; i<4; ++i) {
.......
}

首先完整补充ICMP头部,sendto()发送到目标地址

  icmp->seq = i+1;
        icmp->timestamp = GetTickCount();
        icmp->checksum = 0;
        icmp->checksum = CalculateChecksum((USHORT*)icmp, sizeof(ICMP_HEADER));

        auto start = std::chrono::steady_clock::now();
        sendto(sock, sendBuf, sizeof(sendBuf), 0, (sockaddr*)&dest, sizeof(dest));

设置接收与接收方地址
用recvfrom()函数读取接收方响应内容,解析获取往返时延(RTT)
解析接收到的IP头获得生存时间(TTL)以及ip头长度从而得到ICMP头,确认ICMP头中的type为ICMP_ECHO_REPLY,即为回显应答。输出信息。

 char recvBuf[1024];
        sockaddr_in from = {0};
        int fromLen = sizeof(from);
        
        if(recvfrom(sock, recvBuf, sizeof(recvBuf), 0, (sockaddr*)&from, &fromLen) != SOCKET_ERROR) {
            auto end = std::chrono::steady_clock::now();
            auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);

            IP_HEADER* ip = (IP_HEADER*)recvBuf;
            int ipHeaderLen = (ip->hdr_len & 0x0F) * 4;
            ICMP_HEADER* recvIcmp = (ICMP_HEADER*)(recvBuf + ipHeaderLen);

            if(recvIcmp->type == ICMP_ECHO_REPLY) {
                std::cout << "Reply from " << inet_ntoa(from.sin_addr)
                          << " bytes=" << sizeof(sendBuf)
                          << " time=" << duration.count() << "ms"
                          << " TTL=" << (int)ip->ttl << std::endl;

结束关闭套接字

 closesocket(sock);

3.4.5 Ping函数完整代码如下:

void Ping(const char* target) {
    SOCKET sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    if(sock == INVALID_SOCKET) {
        std::cerr << "Socket error: " << WSAGetLastError() << std::endl;
        return;
    }

    sockaddr_in dest = {0};
    dest.sin_family = AF_INET;
    InetPtonA(AF_INET, target, &dest.sin_addr);

    char sendBuf[sizeof(ICMP_HEADER) + DEFAULT_PACKET_SIZE] = {0};
    ICMP_HEADER* icmp = (ICMP_HEADER*)sendBuf;
    icmp->type = ICMP_ECHO_REQUEST;
    icmp->id = GetCurrentProcessId();

    // 设置接收超时
    DWORD timeout = TIMEOUT_MS;
    setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(timeout));

    for(int i=0; i<4; ++i) {
        icmp->seq = i+1;
        icmp->timestamp = GetTickCount();
        icmp->checksum = 0;
        icmp->checksum = CalculateChecksum((USHORT*)icmp, sizeof(ICMP_HEADER));

        auto start = std::chrono::steady_clock::now();
        sendto(sock, sendBuf, sizeof(sendBuf), 0, (sockaddr*)&dest, sizeof(dest));

        char recvBuf[1024];
        sockaddr_in from = {0};
        int fromLen = sizeof(from);
        
        if(recvfrom(sock, recvBuf, sizeof(recvBuf), 0, (sockaddr*)&from, &fromLen) != SOCKET_ERROR) {
            auto end = std::chrono::steady_clock::now();
            auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);

            IP_HEADER* ip = (IP_HEADER*)recvBuf;
            int ipHeaderLen = (ip->hdr_len & 0x0F) * 4;
            ICMP_HEADER* recvIcmp = (ICMP_HEADER*)(recvBuf + ipHeaderLen);

            if(recvIcmp->type == ICMP_ECHO_REPLY) {
                std::cout << "Reply from " << inet_ntoa(from.sin_addr)
                          << " bytes=" << sizeof(sendBuf)
                          << " time=" << duration.count() << "ms"
                          << " TTL=" << (int)ip->ttl << std::endl;
            }
        }
    }
    closesocket(sock);
}

3.5 基于原始套接字实现Tracert命令

Tracert命令原理:
从1开始逐步递增TTL, 直到MAX_HOPS(MAX_HOPS = 30),每个TTL发3次回显请求。

3.5.1 创建原始套接字+目标地址配置

略,同3.4.1

3.5.2 构造ICMP报文

略,同3.4.2

3.5.3 发送TTL递增探测报文 + 响应接受解析

整体代码框架:两层for循环

	sockaddr_in from = {0};		//接收响应方地址
    int fromLen = sizeof(from);

    for(int ttl=1; ttl<=MAX_HOPS; ++ttl) {
     for(int attempt=0; attempt<MAX_ATTEMPTS; ++attempt) {
       //  发送TTL报文
		......
	  //   接受分析响应
		}
    
    }
for(int ttl=1; ttl<=MAX_HOPS; ++ttl) 外循环内容:

设置发送报文的TTL,设置一些标志
gotResponse:TTL为某值的报文是否得到回答,初始值为
totalTime:从发送请求到收到响应的总时间
recieveCount:TTL为某值的报文在3次发送中共收到几次

  setsockopt(sock, IPPROTO_IP, IP_TTL, (char*)&ttl, sizeof(ttl));
   bool gotResponse = false;
   long totalTime = 0;
   int receivedCount = 0;
for(int attempt=0; attempt<MAX_ATTEMPTS; ++attempt)内循环:

1> 填充ICMP头部信息并发送回显请求,start记录起始时间

 icmp->seq = ttl*10 + attempt;
            icmp->timestamp = GetTickCount();
            icmp->checksum = 0;
            icmp->checksum = CalculateChecksum((USHORT*)icmp, sizeof(ICMP_HEADER));

            auto start = std::chrono::steady_clock::now();
            sendto(sock, sendBuf, sizeof(sendBuf), 0, (sockaddr*)&dest, sizeof(dest));

2> 接收分析响应
recvBuf为接受缓冲区,接受到回答后,修改相应标志并判断是何种类型报文,是回显响应报文就结束函数
否则,如果是超时,则继续执行未完成的内循环。

   char recvBuf[1024];
            // 重置from结构体
            memset(&from, 0, sizeof(from));
            fromLen = sizeof(from);
  if(recvfrom(sock, recvBuf, sizeof(recvBuf), 0, 
               (sockaddr*)&from, &fromLen) != SOCKET_ERROR) 
            {
                auto end = std::chrono::steady_clock::now();
                auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
                
                IP_HEADER* ip = (IP_HEADER*)recvBuf;
                int ipHeaderLen = (ip->hdr_len & 0x0F) * 4;
                ICMP_HEADER* recvIcmp = (ICMP_HEADER*)(recvBuf + ipHeaderLen);

                if(recvIcmp->type == ICMP_TIMEOUT || recvIcmp->type == ICMP_ECHO_REPLY) {
                    totalTime += duration.count();
                    receivedCount++;
                    gotResponse = true;

                    if(recvIcmp->type == ICMP_ECHO_REPLY) {
                        // 使用inet_ntoa前需验证地址有效性
                        std::cout << ttl << "\t" << inet_ntoa(from.sin_addr) 
                                  << "\t" << duration.count() << " ms *\n";
                        closesocket(sock);
                        return;
                    }
                }
            }

如果改TTL未能到达目标主机,则待3次相同TTL报文发送完毕之后,结束内循环

结束内循环后检查各个标志,查看是否得到响应,得到响应则输出信息,到达主机ip、往返时延等。若未得到响应则输出请求超时。

if(gotResponse) {
            // from在此作用域已有效
            std::cout << ttl << "\t" << inet_ntoa(from.sin_addr) << "\t"
                      << static_cast<float>(totalTime)/receivedCount << " ms\n";
        } else {
            std::cout << ttl << "\t*\t\tRequest timed out\n";
        }

重复执行,直到到达目标主机或者达到Tracert最大跳数限制。

完整Tracert函数:

void Tracert(const char* target) {
    SOCKET sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    if(sock == INVALID_SOCKET) {
        std::cerr << "Socket error: " << WSAGetLastError() << std::endl;
        return;
    }

    sockaddr_in dest = {0};
    dest.sin_family = AF_INET;
    InetPtonA(AF_INET, target, &dest.sin_addr);

    char sendBuf[sizeof(ICMP_HEADER) + DEFAULT_PACKET_SIZE] = {0};
    ICMP_HEADER* icmp = (ICMP_HEADER*)sendBuf;
    icmp->type = ICMP_ECHO_REQUEST;
    icmp->id = GetCurrentProcessId();

    // 设置接收超时
    DWORD timeout = TIMEOUT_MS;
    setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(timeout));

    std::cout << "\nTracing route to " << target << std::endl;
    std::cout << "Hop\tIP Address\t\tTime\n";
    std::cout << "----------------------------------------\n";

    sockaddr_in from = {0};
    int fromLen = sizeof(from);

    for(int ttl=1; ttl<=MAX_HOPS; ++ttl) {
        setsockopt(sock, IPPROTO_IP, IP_TTL, (char*)&ttl, sizeof(ttl));
        
        bool gotResponse = false;
        long totalTime = 0;
        int receivedCount = 0;

        for(int attempt=0; attempt<MAX_ATTEMPTS; ++attempt) {
            icmp->seq = ttl*10 + attempt;
            icmp->timestamp = GetTickCount();
            icmp->checksum = 0;
            icmp->checksum = CalculateChecksum((USHORT*)icmp, sizeof(ICMP_HEADER));

            auto start = std::chrono::steady_clock::now();
            sendto(sock, sendBuf, sizeof(sendBuf), 0, (sockaddr*)&dest, sizeof(dest));

            char recvBuf[1024];
            // 重置from结构体
            memset(&from, 0, sizeof(from));
            fromLen = sizeof(from);

            if(recvfrom(sock, recvBuf, sizeof(recvBuf), 0, 
               (sockaddr*)&from, &fromLen) != SOCKET_ERROR) 
            {
                auto end = std::chrono::steady_clock::now();
                auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
                
                IP_HEADER* ip = (IP_HEADER*)recvBuf;
                int ipHeaderLen = (ip->hdr_len & 0x0F) * 4;
                ICMP_HEADER* recvIcmp = (ICMP_HEADER*)(recvBuf + ipHeaderLen);

                if(recvIcmp->type == ICMP_TIMEOUT || recvIcmp->type == ICMP_ECHO_REPLY) {
                    totalTime += duration.count();
                    receivedCount++;
                    gotResponse = true;

                    if(recvIcmp->type == ICMP_ECHO_REPLY) {
                        // 使用inet_ntoa前需验证地址有效性
                        std::cout << ttl << "\t" << inet_ntoa(from.sin_addr) 
                                  << "\t" << duration.count() << " ms *\n";
                        closesocket(sock);
                        return;
                    }
                }
            }
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
        }

        if(gotResponse) {
            // from在此作用域已有效[3,5](@ref)
            std::cout << ttl << "\t" << inet_ntoa(from.sin_addr) << "\t"
                      << static_cast<float>(totalTime)/receivedCount << " ms\n";
        } else {
            std::cout << ttl << "\t*\t\tRequest timed out\n";
        }
    }
    closesocket(sock);
}

3.6 编写main()测试Ping()与Tracert()

int main(){
 WSADATA wsa;
    if(WSAStartup(MAKEWORD(2,2), &wsa) != 0) {
        std::cerr << "WSAStartup failed: " << WSAGetLastError() << std::endl;
        return 1;
    }

    std::cout << "=== Ping Test ===" << std::endl;
    Ping("8.8.8.8");

    std::cout << "\n=== Tracert Test ===" << std::endl;
    Tracert("8.8.8.8");

    WSACleanup();
    return 0;
}

四、测试函数

4.1 编译文件

由于该程序链接了ws2_32.lib和iphlpapi.lib库,因此调试代码如下:

# 编译命令(需管理员权限)
g++ test.cpp -o test.exe -lws2_32 -liphlpapi

4.2 调试程序

原始套接字权限​​:Windows要求管理员权限运行程序(未以管理员启动VSCode或CMD)

# 运行测试
.\tracert.exe 8.8.8.8

​​协议栈过滤​​:系统禁用ICMPv4入站规则(默认开启但可能被安全软件覆盖)
注意!!!在调试之前临时允许ICMPv4入站

#临时允许ICMPv4入站(管理员权限)
netsh advfirewall firewall add rule name="ICMP Allow" protocol=icmpv4:any,any dir=in action=allow

但是开启允许ICMPv4入站规则可能会造成安全问题,应及时执行下面代码关闭:

# 立即删除规则
netsh advfirewall firewall delete rule name="ICMP Allow"

# 验证规则状态
netsh advfirewall show rule name="ICMP Allow"

调试结果:
在这里插入图片描述

对比系统ping指令
在这里插入图片描述
系统tracert指令
在这里插入图片描述
测试成功!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值