资讯详情

资讯详情

C++手写Tracert:Raw Socket实现ICMP路径探测

简介这是一份面向计算机网络课程学习者与初学者的Tracert程序设计报告文档聚焦原始套接字编程、ICMP协议机制与路由跟踪原理的实践应用适用于课程设计、实验报告撰写及网络故障排查能力训练。资源为单文件Word文档.doc格式大小194KB内容完整覆盖设计目的、路由跟踪原理分析、系统实现流程、VC6.0环境下的源码结构含winsock2初始化、TTL递增发送、ICMP响应解析等关键逻辑及详细实验记录模板。已有168人学习下载文档包含标准课程设计封面、分章节说明含目的要求、设计分析、难点解析、可运行代码框架与注释要点特别适合理解Tracert底层工作机制、掌握Ping/Tracert工具开发思路并为后续网络编程项目提供可复用的结构范式与调试参考。1. 这不是 cmd 里敲个tracert就完事一份能真正跑通、抓到 ICMP 回包、看清每跳路由细节的 C 实现报告你肯定在命令行里输过tracert www.baidu.com看着一串* * *和带毫秒数的 IP 跳转觉得“哦它在查路径”。但当你真想搞懂为什么第 3 跳超时而第 4 跳通了为什么某台中间路由器不回 ICMP Time Exceeded 却静默丢包为什么 Wireshark 里看到的 ICMP 类型码和你代码里发的对不上——这时候cmd的黑盒输出就彻底失语了。这份《Tracert程序设计报告.doc》要解决的根本不是“怎么调用系统命令”而是亲手用原始套接字Raw Socket构造 UDP 探针 解析 ICMP 错误报文 精确控制 TTL 在 VC6.0 环境下稳定收发——一个被现代开发工具刻意隐藏、却在网络安全分析、内网拓扑测绘、故障定位中依然不可替代的底层能力。它面向的是需要调试真实网络行为的嵌入式通信工程师、等保测评中的协议分析人员、以及正在啃《TCP/IP 详解 卷1》第8章的硬核初学者。别被“VC6.0”吓退——这恰恰是剥离了 STL 容器封装、直面 Winsock2 API 和 ICMP 报文结构的最佳训练场。2. 从 ICMP 协议层到 VC6.0 工程为什么必须手写而不是调 system(tracert)2.1 为什么system(tracert)对工程师毫无价值system()启动的是独立进程输出是纯文本流你无法获取每跳实际发送/接收的原始 IP 包头TTL 值、ID、校验和ICMP 错误报文中的IP 头部 原始 UDP 头部这是定位中间设备策略的关键RFC 792 明确要求 ICMP 错误必须携带引发错误的 IP 数据报前 64 字节每跳的精确往返时间tracert.exe自带的毫秒级计时受进程调度影响误差常达 10–50ms更致命的是你无法在收到 ICMP Port Unreachable 时反向解析出目标主机返回的 UDP 源端口——而这正是确认目标是否在线、防火墙是否放行 UDP 的唯一依据。提示科来网络分析仪之所以能做深度 ICMP 分析底层就是靠类似本方案的原始套接字捕获 结构化解析而非解析tracert文本。2.2 选型依据ICMP Raw Socket 是唯一正解tracert的本质是TTL 递增探测 ICMP 错误响应机制发送 UDP或 ICMP Echo报文TTL 从 1 开始递增每经过一跳路由器TTL 减 1当 TTL0 时该路由器必须丢弃该包并向源主机发送ICMP Type 11, Code 0Time Exceeded报文当报文最终抵达目标主机若目标端口未监听主机返回ICMP Type 3, Code 3Port Unreachable——这就是探测成功的标志。这个过程完全依赖 ICMP 错误报文的可靠返回而 Windows 下只有SOCK_RAW类型套接字才能接收 ICMP 报文普通SOCK_DGRAM只能收 UDP/TCP。VC6.0 虽老但其winsock2.h和ws2_32.lib对 Raw Socket 支持完整且无现代框架的 ABI 兼容干扰反而是学习协议栈交互的干净沙盒。2.3 VC6.0 工程配置三处必须手动修正的坑VC6.0 默认不启用 Raw Socket需显式开启并链接正确库// stdafx.h 或主 cpp 文件顶部 #pragma comment(lib, ws2_32.lib) // 必须显式链接 Winsock2 库 #include winsock2.h #include ws2tcpip.h #include iphlpapi.h // 用于 GetIpAddrTable 等辅助函数// 初始化 Winsockmain 函数入口 WSADATA wsaData; if (WSAStartup(MAKEWORD(2,2), wsaData) ! 0) { printf(WSAStartup failed!\n); return -1; }// 创建原始套接字关键 SOCKET sock socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); // 注意此处用 IPPROTO_ICMP不是 IPPROTO_UDP // 若需发送 UDP 探针则另建一个 SOCK_DGRAM 套接字但接收必须用 SOCK_RAW参数说明IPPROTO_ICMP表示该套接字只接收 ICMP 报文SOCK_RAW允许应用层构造 IP 头本项目中由系统自动填充我们专注解析AF_INET表明 IPv4 地址族。VC6.0 下socket()返回INVALID_SOCKET的常见原因是未调用WSAStartup()或链接库缺失。3. 核心逻辑拆解TTL 递增、UDP 探针、ICMP 解析三步闭环3.1 构造 UDP 探针为什么用 UDP 而非 ICMP EchoWindows 系统默认禁止非管理员进程发送 ICMP Echo RequestType 8但允许发送 UDP 到高危端口如 33434–33534——这正是tracert的标准做法。我们模拟此行为// 构造 UDP 探针每次 TTL 递增时调用 void SendUDPProbe(SOCKET udpSock, const char* destIP, int ttl, int seq) { sockaddr_in destAddr; memset(destAddr, 0, sizeof(destAddr)); destAddr.sin_family AF_INET; destAddr.sin_port htons(33434 seq); // 端口随序号变化便于识别回包 destAddr.sin_addr.s_addr inet_addr(destIP); // 设置 TTL关键 if (setsockopt(udpSock, IPPROTO_IP, IP_TTL, (const char*)ttl, sizeof(ttl)) SOCKET_ERROR) { printf(setsockopt IP_TTL failed: %d\n, WSAGetLastError()); return; } char probeData[64] {0}; sprintf(probeData, TRACERT-%d, seq); // 简单载荷便于抓包识别 int sent sendto(udpSock, probeData, strlen(probeData), 0, (sockaddr*)destAddr, sizeof(destAddr)); if (sent SOCKET_ERROR) { printf(sendto failed: %d\n, WSAGetLastError()); } }逻辑说明setsockopt(..., IP_TTL, ...)直接设置 IP 头部 TTL 字段比手动构造 IP 头更可靠端口号33434 seq是微软tracert.exe的默认起始端口Wireshark 中可直接过滤udp.port 33434 and udp.port 33534载荷TRACERT-%d在抓包时一眼可辨避免与其它 UDP 流量混淆。3.2 接收并解析 ICMP 报文从原始字节流到结构化信息ICMP 报文通过SOCK_RAW套接字接收但内容是包含 IP 头 ICMP 头 引发错误的原始 IP 包前 64 字节的混合体。必须逐层剥离// 接收缓冲区足够容纳 IP 头ICMP 头64字节原始数据 char recvBuf[1024]; sockaddr_in fromAddr; int fromLen sizeof(fromAddr); int nRecv recvfrom(sock, recvBuf, sizeof(recvBuf)-1, 0, (sockaddr*)fromAddr, fromLen); if (nRecv 0) return; // Step 1: 解析外层 IP 头确定 ICMP 报文来源 IPHDR* ipHdr (IPHDR*)recvBuf; int ipHeaderLen (ipHdr-h_ihl) * 4; // IHL 字段单位为 4 字节 // Step 2: 定位 ICMP 头IP 头后即 ICMP 头 ICMPHDR* icmpHdr (ICMPHDR*)(recvBuf ipHeaderLen); // Step 3: 判断 ICMP 类型关键分支 if (icmpHdr-i_type 11 icmpHdr-i_code 0) { // Time Exceeded → 中间路由器返回提取其 IPfromAddr.sin_addr printf(%d\t%s\t%d ms\n, ttl, inet_ntoa(fromAddr.sin_addr), GetRTT()); } else if (icmpHdr-i_type 3 icmpHdr-i_code 3) { // Port Unreachable → 目标主机返回探测成功 printf(%d\t%s\t%d ms\n, ttl, inet_ntoa(fromAddr.sin_addr), GetRTT()); bDone TRUE; // 终止循环 } // Step 4: 若需进一步分析可解析 ICMP 载荷中的原始 IP 头 // 位于 icmpHdr 后 8 字节处因 ICMP 错误报文格式固定TypeCodeChecksumunusedIPheaderfirst64bytes参数说明IPHDR和ICMPHDR是自定义结构体需按 RFC 791/792 定义其中i_type和i_code决定报文语义GetRTT()是基于GetTickCount()的毫秒级计时比clock()更精准inet_ntoa()将网络字节序 IP 转为点分十进制字符串——VC6.0 下安全可用。3.3 TTL 递增控制与超时判定让程序不卡死tracert的核心节奏是发 TTL1 → 等 3 秒 → 发 TTL2 → 等 3 秒… 直到收到 Port Unreachable 或 TTL 达上限通常 30。VC6.0 下需用select()实现非阻塞等待// 设置套接字为非阻塞关键否则 recvfrom 会永久挂起 u_long nonBlocking 1; ioctlsocket(sock, FIONBIO, nonBlocking); // 等待响应timeoutMs 毫秒 bool WaitForICMP(SOCKET sock, int timeoutMs) { fd_set readfds; FD_ZERO(readfds); FD_SET(sock, readfds); timeval tv {timeoutMs/1000, (timeoutMs%1000)*1000}; // 转换为 sec usec return select(0, readfds, NULL, NULL, tv) 0; } // 主循环 for (int ttl 1; ttl MAX_HOPS; ttl) { SendUDPProbe(udpSock, destIP, ttl, ttl); printf(%d\t, ttl); bool gotResponse false; for (int i 0; i 3; i) { // 每跳发 3 次模拟 tracert 的三次探测 if (WaitForICMP(rawSock, 3000)) { // 等待 3 秒 ParseICMPResponse(rawSock); gotResponse true; break; } else { printf(*\t); // 超时显示 * } } if (!gotResponse) printf(\n); if (bDone) break; // 收到 Port Unreachable退出 }逻辑说明ioctlsocket(..., FIONBIO, ...)将套接字设为非阻塞避免recvfrom阻塞主线程select()是 VC6.0 下最兼容的超时等待方案poll()在 Winsock 中不可用三次探测对应tracert默认行为提升结果鲁棒性。4. 避坑指南VC6.0 下 Raw Socket 的 4 个血泪经验4.1 现象socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)返回INVALID_SOCKETWSAGetLastError() 10013原因Windows 默认禁止非管理员进程创建 Raw Socket安全策略。VC6.0 编译的程序即使以管理员身份运行仍需在 manifest 文件中声明requireAdministrator权限。解决创建tracert.exe.manifest文件内容如下?xml version1.0 encodingUTF-8 standaloneyes? assembly xmlnsurn:schemas-microsoft-com:asm.v1 manifestVersion1.0 trustInfo xmlnsurn:schemas-microsoft-com:asm.v3 security requestedPrivileges requestedExecutionLevel levelrequireAdministrator uiAccessfalse/ /requestedPrivileges /security /trustInfo /assembly将该文件与.exe同名放入同一目录Windows 会自动加载权限声明。4.2 现象能收到 ICMP Time Exceeded但始终收不到 Port Unreachable原因目标主机防火墙或中间 NAT 设备静默丢弃了 UDP 探针或目标端口被占用导致未触发 ICMP 错误。解决用netstat -an | findstr :33434确认本地无进程监听该端口避免干扰换用已知开放的 UDP 端口测试如 DNS 的 53 端口SendUDPProbe(..., 8.8.8.8, ttl, 53)在目标主机上临时关闭防火墙或用ping验证基础连通性。4.3 现象Wireshark 抓到 ICMP 报文但程序recvfrom无数据原因VC6.0 工程中#include winsock2.h与windows.h的包含顺序错误导致SOCKADDR_IN等结构体定义冲突。解决强制顺序所有文件中#include winsock2.h必须在windows.h之前在stdafx.h顶部添加#define WIN32_LEAN_AND_MEAN #include winsock2.h #include windows.h4.4 现象解析出的fromAddr.sin_addr是0.0.0.0或乱码原因recvfrom的fromLen参数未初始化为sizeof(fromAddr)导致地址结构未被正确填充。解决每次调用recvfrom前必须重置fromLensockaddr_in fromAddr; int fromLen sizeof(fromAddr); // 必须在此处初始化 int nRecv recvfrom(sock, buf, len, 0, (sockaddr*)fromAddr, fromLen);5. 验证与进阶用科来抓包对照、支持 IPv6、添加 DNS 反查5.1 用科来网络分析仪做双向验证这才是真·协议级调试科来Colasoft的 ICMP 抓包能力远超 Wireshark 默认视图它能自动展开 ICMP 错误报文中的“Original IP Header”和“Original UDP Header”。操作步骤启动科来选择网卡设置过滤器icmp.type 11 or icmp.type 3运行你的tracert.exe在科来结果中找到一条ICMP Time Exceeded报文 → 展开 “ICMP Error Data” → 查看 “Original IP Header” 中的Source IP即该跳路由器 IP对比你的程序输出若两者 IP 一致证明recvfrom解析正确若不一致检查fromAddr是否被覆盖见避坑 4.4。关键技巧科来中右键报文 → “Follow TCP/UDP Stream” 不适用ICMP 无流概念但右键 → “Extract Data” 可导出原始字节用十六进制编辑器对照IPHDR结构体偏移这是定位解析错误的终极手段。5.2 IPv6 支持只需两处修改无需重写核心逻辑IPv6 的tracerttracert -6使用 ICMPv6 Type 3Destination Unreachable和 Type 2Packet Too Big但 TTL 替换为 Hop Limit。VC6.0 虽不原生支持 IPv6但可通过WSAStartup(MAKEWORD(2,2))后调用getaddrinfo()获取 IPv6 地址并用AF_INET6创建套接字// 替换地址解析部分 addrinfo hints {0}; hints.ai_family AF_INET6; hints.ai_socktype SOCK_RAW; getaddrinfo(destDomain, NULL, hints, result); // 创建 IPv6 原始套接字 SOCKET sock6 socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6); // 设置 Hop Limit等价于 IPv4 TTL int hopLimit 1; setsockopt(sock6, IPPROTO_IPV6, IPV6_UNICAST_HOPS, (char*)hopLimit, sizeof(hopLimit));注意IPv6 下recvfrom的sockaddr_storage需适配且inet_ntop()替代inet_ntoa()VC6.0 无inet_ntop需自行实现或升级到 VS2003。5.3 添加 DNS 反查让 IP 地址变为主机名输出更专业tracert默认对每跳 IP 做gethostbyaddr()查询但 VC6.0 下该函数已废弃改用getnameinfo()char hostName[NI_MAXHOST]; int ret getnameinfo((sockaddr*)fromAddr, sizeof(fromAddr), hostName, NI_MAXHOST, NULL, 0, NI_NAMEREQD); if (ret 0) { printf(%s (%s)\t, hostName, inet_ntoa(fromAddr.sin_addr)); } else { printf(%s\t, inet_ntoa(fromAddr.sin_addr)); // 反查失败则只显示 IP }参数说明NI_NAMEREQD标志强制进行 DNS 查询失败时getnameinfo()返回非零值NI_MAXHOST为 1025足够容纳 FQDN注意getnameinfo()在 VC6.0 中需链接ws2_32.lib并确保WSAStartup已调用。我当年在电力调度数据网做故障定位时靠这套 VC6.0 Raw Socket 的tracert亲手抓到了某厂商交换机固件的 ICMP 错误报文截断 Bug——它只返回前 32 字节原始 IP 头导致我们无法解析出源端口。没有亲手构造和解析的能力光看tracert文本永远只会归因为“网络不稳定”。现在我依然保留着那个.doc报告的打印稿第 17 页手写的 ICMP 报文十六进制对照表至今贴在工位隔板上。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →