raceroute命令的原理是利用IP数据报的生存时间(TTL)字段和因特网控制报文协议(ICMP)。
TTL字段是一个8位的整数,表示数据报在网络中可以经过的最大跳数。每当数据报经过一个路由器,TTL字段就会减一,直到减到零为止。此时,路由器会丢弃数据报,并向源主机发送一个ICMP超时错误报文。
traceroute命令通过发送一系列TTL值逐渐增加的IP数据报,来探测从源主机到目的主机之间的所有路由器。例如,首先发送一个TTL为1的数据报,该数据报会被第一个路由器丢弃,并返回一个ICMP超时错误报文。然后发送一个TTL为2的数据报,该数据报会被第二个路由器丢弃,并返回一个ICMP超时错误报文。依次类推,直到发送一个TTL值足够大的数据报,该数据报能够到达目的主机,并得到一个ICMP端口不可达错误报文。
traceroute命令根据收到的ICMP错误报文,记录下每个路由器的IP地址和往返时间,并显示出来。通常每个路由器会测量三次,以得到更准确和稳定的结果。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/ip.h>
#include <netinet/icmp.h>
// 定义最大尝试次数和最大跳数
#define MAX_ATTEMPTS 3
#define MAX_HOP 30
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Usage: %s <target>\n", argv[0]);
exit(EXIT_FAILURE);
}
// 获取目标主机信息
struct hostent *host;
if ((host = gethostbyname(argv[1])) == NULL) {
fprintf(stderr, "Could not find host %s\n", argv[1]);
exit(EXIT_FAILURE);
}
// 创建原始套接字并绑定到任意端口
int sockfd;
if ((sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW)) < 0) {
perror("socket");
exit(EXIT_FAILURE);
}
// 设置目标主机的IP地址和端口号为0(系统会自动选择合适的端口号)
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(inet_ntoa(*(struct in_addr*)*host->h_addr_list));
addr.sin_port = htons(0); // 使用系统自动选择的端口号
// 发送ICMP回显请求报文,并记录每个跳点的往返时间
for (int i = 1; i <= MAX_HOP; i++) {
char buf[1024]; // 存储ICMP报文和IP报文头部的缓冲区
struct ip *iph = (struct ip*)buf; // IP报文头部指针
struct icmp *icmp = (struct icmp*)((char*)iph + sizeof(struct ip)); // ICMP报文指针
icmp->icmp_type = ICMP_ECHO; // 设置ICMP类型为回显请求(ping)
icmp->icmp_code = 0; // 设置ICMP代码为0(表示回显请求)
icmp->icmp_cksum = 0; // 设置ICMP校验和为0(将由系统计算)
icmp->icmp_seq = htons(i); // 设置序列号(每个跳点递增)
strcpy(icmp->icmp_data, "Traceroute"); // 设置ICMP数据字段(可以是任意数据)
iph->ip_v = IPVERSION; // 设置IP版本为4(IPv4)和头部长度为5(以32位为单位)
iph->ip_hl = sizeof(struct ip) / sizeof(unsigned char); // 设置IP头部长度(以字节为单位)
iph->ip_tos = 0; // 设置服务类型字段(可以是任意值)
iph->ip_off = 0; // 设置标志和分片偏移量字段(不进行分片)
iph->ip_ttl = i; // 设置生存时间字段(每跳递增)
iph->ip_p = IPPROTO_ICMP; // 设置下一个协议字段为ICMP(对于原始套接字来说是必要的)
iph->ip_src.s_addr = inet_addr("127.0.0.1"); // 设置源IP地址为本机环回地址(用于测试)
iph->ip_dst.s_addr = inet_addr(inet_ntoa(*(struct in_addr*)*host->h_addr_list)); // 设置目标IP地址为目标主机的IP地址
iph->ip_len = htons(sizeof(struct icmp) + strlen("Traceroute")); // 设置IP报文长度(包括ICMP报文和数据字段的长度)
iph->ip_sum = 0; // 设置IP校验和为0(将由系统计算)
// 发送ICMP报文并记录往返时间
if (sendto(sockfd, buf, sizeof(struct icmp) + strlen("Traceroute"), 0, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("sendto");
exit(EXIT_FAILURE);
}
// 等待接收ICMP回显应答
char recvbuf[1024];
struct sockaddr_in recvaddr;
socklen_t recvlen = sizeof(recvaddr);
int recvfd = recvfrom(sockfd, recvbuf, sizeof(recvbuf), MSG_WAITALL, (struct sockaddr*)&recvaddr, &recvlen);
if (recvfd < 0) {
perror("recvfrom");
exit(EXIT_FAILURE);
}
// 解析接收到的ICMP回显应答
struct ip *recviph = (struct ip*)recvbuf;
struct icmp *recvicmp = (struct icmp*)((char*)recviph + sizeof(struct ip));
if (recviph->ip_p != IPPROTO_ICMP || recvicmp->icmp_type != ICMP_ECHOREPLY) {
printf("Not a ICMP echo reply\n");
continue;
}
// 计算往返时间并输出结果
int rtt = (int)(recvicmp->icmp_cksum + recviph->ip_len - ntohs(recviph->ip_len));
printf("RTT: %d ms\n", rtt);
}
// 关闭原始套接字
close(sockfd);
return 0;
}