C语言实现traceroute -I时recvfrom从TTL=2起返回-1的问题
实现traceroute -I选项时的接收数据包问题
我编写了如下C代码实现traceroute的-I选项(基于ICMP ping的路由追踪功能):
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <netinet/ip_icmp.h> #include <netdb.h> #include <sys/socket.h> #include <sys/types.h> #include <errno.h> #define PACKET_SIZE 64 #define MAX_HOPS 30 #define TIMEOUT 3 // 超时设置为3秒 // 生成ICMP数据包 void create_icmp_packet(char *packet, int seq) { memset(packet, 0, PACKET_SIZE); struct icmphdr *icmp_hdr = (struct icmphdr *)packet; icmp_hdr->type = ICMP_ECHO; icmp_hdr->un.echo.id = getpid(); icmp_hdr->un.echo.sequence = seq; icmp_hdr->checksum = 0; // 填充数据部分(标准实现会包含) char *data = packet + sizeof(struct icmphdr); for (int i = 0; i < PACKET_SIZE - sizeof(struct icmphdr); i++) { data[i] = i; } // 计算整个数据包的校验和 unsigned short *buf = (unsigned short *)packet; unsigned int checksum = 0; int packet_len = PACKET_SIZE; while (packet_len > 1) { checksum += *buf++; packet_len -= 2; } if (packet_len == 1) { checksum += *(unsigned char *)buf; } checksum = (checksum >> 16) + (checksum & 0xFFFF); checksum += (checksum >> 16); icmp_hdr->checksum = ~checksum; } // 解析主机名到IP地址 int resolve_hostname(const char *hostname, struct sockaddr_in *dest) { struct hostent *host_entity; if ((host_entity = gethostbyname(hostname)) == NULL) { perror("gethostbyname"); return -1; } dest->sin_family = AF_INET; dest->sin_port = htons(0); dest->sin_addr.s_addr = *(long *)host_entity->h_addr; return 0; } int main(int argc, char *argv[]) { if (argc != 2) { printf("Usage: %s <hostname>\n", argv[0]); return -1; } struct sockaddr_in dest; if (resolve_hostname(argv[1], &dest) < 0) { return -1; } int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); if (sockfd < 0) { perror("socket"); return -1; } struct timeval timeout; timeout.tv_sec = TIMEOUT; timeout.tv_usec = 0; if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char *)&timeout, sizeof(timeout)) < 0) { perror("setsockopt SO_RCVTIMEO"); close(sockfd); return -1; } int recv_buffer_size = 1024 * 1024; // 1MB if (setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &recv_buffer_size, sizeof(recv_buffer_size)) < 0) { perror("setsockopt SO_RCVBUF"); close(sockfd); return -1; } char sendbuf[PACKET_SIZE]; char recvbuf[1500]; // 增大接收缓冲区到MTU大小 struct sockaddr_in recv_addr; socklen_t addrlen = sizeof(recv_addr); for (int ttl = 1; ttl <= MAX_HOPS; ttl++) { // 设置TTL if (setsockopt(sockfd, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl)) < 0) { perror("setsockopt IP_TTL"); close(sockfd); return 1; } // 生成并发送ICMP数据包 create_icmp_packet(sendbuf, ttl); if (sendto(sockfd, sendbuf, PACKET_SIZE, 0, (struct sockaddr *)&dest, sizeof(dest)) < 0) { perror("sendto"); continue; // 发送失败继续下一跳 } ssize_t recv_bytes = recvfrom(sockfd, recvbuf, sizeof(recvbuf), 0, (struct sockaddr *)&recv_addr, &addrlen); if (recv_bytes < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { printf("%d * * * (Timeout)\n", ttl); // 超时 } else { perror("recvfrom"); } continue; } char ip_str[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &recv_addr.sin_addr, ip_str, sizeof(ip_str)); struct iphdr *ip_hdr = (struct iphdr *)recvbuf; struct icmphdr *recv_icmp_hdr = (struct icmphdr *)(recvbuf + (ip_hdr->ihl << 2)); // 校验响应的ICMP包是否匹配当前进程 if (recv_icmp_hdr->un.echo.id == getpid()) { if (recv_icmp_hdr->type == ICMP_TIME_EXCEEDED) { printf("%d %s (TTL exceeded)\n", ttl, ip_str); } else if (recv_icmp_hdr->type == ICMP_ECHOREPLY) { printf("%d %s (Trace complete)\n", ttl, ip_str); break; } else { printf("%d %s (Unexpected ICMP type: %d)\n", ttl, ip_str, recv_icmp_hdr->type); } } else { // 忽略不属于当前进程的ICMP响应 continue; } } close(sockfd); return 0; }
但和直接执行traceroute -I不同,代码在TTL=2及之后的recvfrom调用返回-1,无法接收数据包。同一终端下traceroute -I运行正常,排除网络问题;Wireshark抓包显示目标数据包已被接收,对比发现代码和标准命令的数据包存在差异。
问题原因及修复说明
- 接收缓冲区过小:原代码的
recvbuf仅64字节,而TTL超时返回的ICMP响应会包含原始IP+ICMP头部,实际大小超过64字节,导致recvfrom无法完整接收数据包,返回错误。修复后将recvbuf增大到1500字节(MTU标准大小)。 - ICMP数据包不完整:原代码仅发送ICMP头部,未填充数据部分,且校验和仅计算头部。标准
traceroute -I会发送包含数据的完整64字节数据包,修复后生成完整数据包并计算整个包的校验和。 - 响应匹配缺失:原代码未校验响应包的ID和Sequence,可能接收其他进程的ICMP包。修复后增加ID匹配判断,确保只处理当前进程的响应。
- 重复输出问题:原代码对同一响应重复打印信息,修复后统一输出格式。
内容的提问来源于stack exchange,提问作者yrrr
相关产品推荐
相关产品推荐

