You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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抓包显示目标数据包已被接收,对比发现代码和标准命令的数据包存在差异。


问题原因及修复说明

  1. 接收缓冲区过小:原代码的recvbuf仅64字节,而TTL超时返回的ICMP响应会包含原始IP+ICMP头部,实际大小超过64字节,导致recvfrom无法完整接收数据包,返回错误。修复后将recvbuf增大到1500字节(MTU标准大小)。
  2. ICMP数据包不完整:原代码仅发送ICMP头部,未填充数据部分,且校验和仅计算头部。标准traceroute -I会发送包含数据的完整64字节数据包,修复后生成完整数据包并计算整个包的校验和。
  3. 响应匹配缺失:原代码未校验响应包的ID和Sequence,可能接收其他进程的ICMP包。修复后增加ID匹配判断,确保只处理当前进程的响应。
  4. 重复输出问题:原代码对同一响应重复打印信息,修复后统一输出格式。

内容的提问来源于stack exchange,提问作者yrrr

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 22:19:52