UNIX下Raw Socket实现Traceroute异常:无法追踪外网且IP重复
问题描述
我尝试在UNIX环境下使用Raw Socket开发Traceroute程序,作为新手整合了相关代码,但程序运行不符合预期:无法追踪本地网络外的地址,输出中同一IP重复显示30次。更换网络后问题仍存在,求解决方法。
原始代码
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <netinet/ip_icmp.h> #include <arpa/inet.h> #include <sys/time.h> #include <errno.h> unsigned short checksum(void *b, int len) { unsigned short *buf = b; unsigned int sum = 0; unsigned short result; for (sum = 0; len > 1; len -= 2) sum += *buf++; if (len == 1) sum += *(unsigned char *)buf; sum = (sum >> 16) + (sum & 0xFFFF); sum += (sum >> 16); result = ~sum; return result; } void trace_route(int client_socket, const char *ip_address) { int sock; struct sockaddr_in dest_addr; struct icmp icmp_pkt; struct timeval start, end; unsigned int ttl; socklen_t len = sizeof(dest_addr); sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); if (sock < 0) { perror("Socket creation failed"); return; } memset(&dest_addr, 0, sizeof(dest_addr)); dest_addr.sin_family = AF_INET; dest_addr.sin_addr.s_addr = inet_addr(ip_address); for (ttl = 1; ttl <= 30; ttl++) { // Max 30 hops // Set TTL for each packet setsockopt(sock, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl)); // Create ICMP packet (ping) memset(&icmp_pkt, 0, sizeof(icmp_pkt)); icmp_pkt.icmp_type = ICMP_ECHO; // ICMP Echo Request type icmp_pkt.icmp_code = 0; icmp_pkt.icmp_id = getpid(); icmp_pkt.icmp_seq = ttl; icmp_pkt.icmp_cksum = checksum(&icmp_pkt, sizeof(icmp_pkt)); gettimeofday(&start, NULL); // Send packet if (sendto(sock, &icmp_pkt, sizeof(icmp_pkt), 0, (struct sockaddr*)&dest_addr, sizeof(dest_addr)) < 0) { perror("Send failed"); continue; } // Wait for response memset(&icmp_pkt, 0, sizeof(icmp_pkt)); int response = recvfrom(sock, &icmp_pkt, sizeof(icmp_pkt), 0, (struct sockaddr*)&dest_addr, &len); gettimeofday(&end, NULL); // Calculate RTT long rtt = (end.tv_sec - start.tv_sec) * 1000 + (end.tv_usec - start.tv_usec) / 1000; if (response < 0) { printf("Hop %d: Request Timed Out\n", ttl); } else { printf("Hop %d: %s (RTT: %ld ms)\n", ttl, inet_ntoa(dest_addr.sin_addr), rtt); } if (icmp_pkt.icmp_type == ICMP_ECHOREPLY) { printf("Trace completed.\n"); break; } } close(sock); }
问题分析与修复方案
核心问题1:未解析IP头部,错误处理响应数据
使用SOCK_RAW + IPPROTO_ICMP创建的socket,recvfrom接收的是包含IP头部的完整数据包,而非单纯的ICMP包。直接将数据写入struct icmp会导致无法正确解析中间路由器返回的ICMP超时包(类型11),只能错误识别目标IP,且永远触发不了终止条件,最终循环跑满30次。
修复:
定义IP头部结构体,跳过IP头部后再解析ICMP数据:
struct iphdr *ip_hdr; struct icmp *icmp_hdr; char buf[1024]; // 用足够大的缓冲区接收完整数据包 // 替换原recvfrom逻辑 int response = recvfrom(sock, buf, sizeof(buf), 0, (struct sockaddr*)&dest_addr, &len); if (response > 0) { ip_hdr = (struct iphdr*)buf; // 跳过IP头部,IP头部长度由ihl字段决定(单位:4字节) icmp_hdr = (struct icmp*)(buf + ip_hdr->ihl * 4); // 后续用icmp_hdr替代原icmp_pkt处理 }
核心问题2:未设置接收超时
原代码中recvfrom会无限阻塞,即使中间路由器无响应也不会显示超时,导致程序卡住。
修复:
在创建socket后添加超时设置:
struct timeval timeout; timeout.tv_sec = 1; timeout.tv_usec = 0; if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) { perror("Failed to set timeout"); close(sock); return; }
核心问题3:终止条件判断逻辑错误
原代码仅在收到ICMP_ECHOREPLY(目标主机回复)时终止,但追踪过程中大部分响应是中间路由器返回的ICMP_TIME_EXCEEDED(TTL超时),需要正确识别这两种情况:
- 收到
ICMP_ECHOREPLY:到达目标,终止循环 - 收到
ICMP_TIME_EXCEEDED:记录当前路由器IP,继续下一跳
修复后的完整代码
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <netinet/ip_icmp.h> #include <arpa/inet.h> #include <sys/time.h> #include <errno.h> unsigned short checksum(void *b, int len) { unsigned short *buf = b; unsigned int sum = 0; unsigned short result; for (sum = 0; len > 1; len -= 2) sum += *buf++; if (len == 1) sum += *(unsigned char *)buf; sum = (sum >> 16) + (sum & 0xFFFF); sum += (sum >> 16); result = ~sum; return result; } void trace_route(const char *ip_address) { int sock; struct sockaddr_in dest_addr, recv_addr; struct icmp icmp_pkt; struct timeval start, end, timeout; unsigned int ttl; socklen_t len = sizeof(recv_addr); char buf[1024]; struct iphdr *ip_hdr; struct icmp *icmp_hdr; sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); if (sock < 0) { perror("Socket creation failed"); return; } // 设置接收超时1秒 timeout.tv_sec = 1; timeout.tv_usec = 0; if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) { perror("Failed to set receive timeout"); close(sock); return; } memset(&dest_addr, 0, sizeof(dest_addr)); dest_addr.sin_family = AF_INET; if (inet_pton(AF_INET, ip_address, &dest_addr.sin_addr) <= 0) { perror("Invalid IP address"); close(sock); return; } for (ttl = 1; ttl <= 30; ttl++) { // 设置TTL if (setsockopt(sock, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl)) < 0) { perror("Failed to set TTL"); continue; } // 构造ICMP Echo Request包 memset(&icmp_pkt, 0, sizeof(icmp_pkt)); icmp_pkt.icmp_type = ICMP_ECHO; icmp_pkt.icmp_code = 0; icmp_pkt.icmp_id = getpid(); icmp_pkt.icmp_seq = ttl; icmp_pkt.icmp_cksum = checksum(&icmp_pkt, sizeof(icmp_pkt)); gettimeofday(&start, NULL); // 发送数据包 if (sendto(sock, &icmp_pkt, sizeof(icmp_pkt), 0, (struct sockaddr*)&dest_addr, sizeof(dest_addr)) < 0) { perror("Send failed"); continue; } // 接收响应 memset(buf, 0, sizeof(buf)); int response_len = recvfrom(sock, buf, sizeof(buf), 0, (struct sockaddr*)&recv_addr, &len); gettimeofday(&end, NULL); long rtt = (end.tv_sec - start.tv_sec) * 1000 + (end.tv_usec - start.tv_usec) / 1000; if (response_len < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { printf("Hop %2d: *\n", ttl); } else { perror("Receive failed"); } continue; } // 解析IP头部和ICMP数据 ip_hdr = (struct iphdr*)buf; icmp_hdr = (struct icmp*)(buf + ip_hdr->ihl * 4); // 输出当前跳信息 printf("Hop %2d: %s (RTT: %ld ms)\n", ttl, inet_ntoa(recv_addr.sin_addr), rtt); // 判断是否到达目标 if (icmp_hdr->icmp_type == ICMP_ECHOREPLY) { printf("Trace completed.\n"); break; } } close(sock); } int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "Usage: %s <target_ip>\n", argv[0]); return 1; } // Raw Socket需要root权限 if (getuid() != 0) { fprintf(stderr, "Error: Must run as root\n"); return 1; } trace_route(argv[1]); return 0; }
额外注意事项
- 运行程序需要root权限,因为Raw Socket的创建需要特权。
- 部分网络环境可能拦截ICMP包,导致某些跳显示超时(*),这是正常现象。
- 替换了
inet_addr为inet_pton,后者更安全,支持IPv6(当前代码仍为IPv4)。
内容的提问来源于stack exchange,提问作者Balan Constantin-Cosmin
相关产品推荐
相关产品推荐

