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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 16:22:03