完整代码如何实现
时间: 2025-08-04 20:59:34 浏览: 3
<think>我们计划使用原始套接字(rawsocket)来构建ICMP包,并设置IP头部选项来记录路由(RecordRoute)。注意:由于IP头部选项的空间限制(最多只能记录9个IP地址),而且现代路由器很多都不支持记录路由,所以实际效果可能有限。步骤:1.创建原始套接字(需要root权限)。2.构建IP头部,并在IP选项中设置RecordRoute选项(选项码为7)。3.构建ICMPEchoRequest报文。4.发送报文并接收回复。5.解析回复报文中的IP选项,提取记录的路由。代码结构:-包含必要的头文件-定义IP和ICMP头结构(注意字节对齐)-计算校验和的函数-主函数:解析参数,创建套接字,构建报文,发送和接收,解析结果注意:IP头部加上选项后,头长度会超过20字节(通常为60字节),因此我们需要构建一个完整的IP数据包(包括选项)和ICMP数据部分。以下是一个简化的实现:</think>
# 在Linux系统中实现`ping -R`路由记录功能的完整C代码
以下是用C语言实现`ping -R`功能的完整代码示例,该程序创建一个原始套接字,发送带有"Record Route"选项的ICMP请求包,并解析返回的路由信息。
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <errno.h>
#include <net/if.h>
#include <linux/sockios.h>
// ICMP包校验和计算函数
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;
}
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("使用方法: %s <目标主机>\n", argv[0]);
return 1;
}
// 1. 创建原始套接字
int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (sockfd < 0) {
perror("创建套接字失败");
return 1;
}
// 设置套接字选项以接收IP头部
int on = 1;
if (setsockopt(sockfd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on)) < 0) {
perror("设置套接字选项失败");
close(sockfd);
return 1;
}
// 2. 构建IP头部和RR选项
struct {
struct iphdr ip;
unsigned char options[40]; // 路由记录选项空间
struct icmphdr icmp;
char data[64]; // ICMP负载数据
} packet;
memset(&packet, 0, sizeof(packet));
// IP头部配置
packet.ip.version = 4; // IPv4
packet.ip.ihl = 15; // 15 * 32位 = 60字节(含选项)
packet.ip.tos = 0; // 服务类型
packet.ip.tot_len = htons(sizeof(packet));
packet.ip.id = htons(getpid()); // 标识符
packet.ip.frag_off = 0; // 不分片
packet.ip.ttl = 64; // TTL
packet.ip.protocol = IPPROTO_ICMP;
packet.ip.check = 0; // 校验和稍后计算
// 目标主机地址解析
struct hostent *he;
struct sockaddr_in target_addr;
if ((he = gethostbyname(argv[1])) == NULL) {
herror("主机解析失败");
close(sockfd);
return 1;
}
memcpy(&target_addr.sin_addr, he->h_addr, he->h_length);
target_addr.sin_family = AF_INET;
// 设置源地址(获取本地地址)
struct sockaddr_in my_addr;
socklen_t addr_len = sizeof(my_addr);
getsockname(sockfd, (struct sockaddr *)&my_addr, &addr_len);
packet.ip.saddr = my_addr.sin_addr.s_addr;
packet.ip.daddr = target_addr.sin_addr.s_addr;
// 路由记录选项(RFC791)
memset(packet.options, 0, sizeof(packet.options));
packet.options[0] = 0x07; // RR选项码
packet.options[1] = 39; // 选项长度(39字节)
packet.options[2] = 4; // 指针位置(从选项开始+4字节)
// 3. ICMP报文配置
packet.icmp.type = ICMP_ECHO;
packet.icmp.code = 0;
packet.icmp.un.echo.id = htons(getpid());
packet.icmp.un.echo.sequence = htons(1);
memset(packet.data, 'A', sizeof(packet.data)); // 填充数据
// 计算ICMP校验和
packet.icmp.checksum = 0;
packet.icmp.checksum = checksum(&packet.icmp, sizeof(packet.icmp) + sizeof(packet.data));
// 4. 发送ICMP请求包
struct sockaddr_in dest;
memcpy(&dest, &target_addr, sizeof(target_addr));
if (sendto(sockfd, &packet, sizeof(packet), 0,
(struct sockaddr *)&dest, sizeof(dest)) < 0) {
perror("发送失败");
close(sockfd);
return 1;
}
printf("已发送 %ld 字节的ICMP请求到 %s\n", sizeof(packet), inet_ntoa(dest.sin_addr));
// 5. 接收ICMP回复
char recv_buf[1500];
struct sockaddr_in from_addr;
socklen_t from_len = sizeof(from_addr);
struct timeval tv = {2, 0}; // 设置2秒超时
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
ssize_t recv_len = recvfrom(sockfd, recv_buf, sizeof(recv_buf), 0,
(struct sockaddr *)&from_addr, &from_len);
if (recv_len < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
printf("未收到响应,超时\n");
} else {
perror("接收失败");
}
close(sockfd);
return 1;
}
printf("从 %s 收到 %zd 字节的回复\n", inet_ntoa(from_addr.sin_addr), recv_len);
// 6. 解析路由记录
struct iphdr *ip_recv = (struct iphdr *)recv_buf;
if (ip_recv->ihl <= 5) {
printf("响应中不包含路由记录选项\n");
close(sockfd);
return 0;
}
// 提取IP选项部分
unsigned char *options = (unsigned char *)(recv_buf + sizeof(struct iphdr));
// 查找Record Route选项(代码为0x07)
int option_found = 0;
int i = 0;
while (i < (ip_recv->ihl - 5) * 4) {
if (options[i] == IPOPT_END) break;
if (options[i] == IPOPT_NOOP) {
i++;
continue;
}
if (options[i] == 0x07) { // RR选项
printf("路由记录路径: \n");
printf("1. 源地址: %s\n", inet_ntoa(my_addr.sin_addr));
int option_length = options[i+1];
int pointer = options[i+2];
int address_count = (pointer - 4) / 4; // 每个IP地址占4字节
for (int j = 0; j < address_count; j++) {
struct in_addr addr;
memcpy(&addr, options + i + 3 + j*4, sizeof(addr));
printf("%d. 路由器: %s\n", j+2, inet_ntoa(addr));
}
printf("%d. 目标地址: %s\n", address_count+2, argv[1]);
option_found = 1;
break;
}
i += (options[i+1] > 0) ? options[i+1] : 1; // 下一个选项
}
if (!option_found) {
printf("响应中未找到有效的路由记录\n");
}
close(sockfd);
return 0;
}
```
## 编译与运行说明
1. **编译方法**:
```bash
gcc ping_r.c -o ping_r
```
2. **运行要求**:
- 需要root权限运行(原始套接字需要特权)
- 大部分路由器默认禁止RR功能,可能无法返回路由信息
3. **执行示例**:
```bash
sudo ./ping_r example.com
```
## 代码工作原理
1. **套接字创建**:创建原始套接字(SOCK_RAW)用于发送/接收自定义IP包
2. **IP头部配置**:
- 设置IP头部长度(ihl)为15(即60字节,包含选项空间)
- 添加Record Route选项(代码0x07):
$$ \text{选项格式} = \begin{bmatrix} \text{类型} & \text{长度} & \text{指针} & \text{地址空间} \end{bmatrix} $$
3. **ICMP报文构造**:
- 类型设置为ICMP_ECHO(8)
- 计算校验和:$ \text{checksum} = \sum(\text{反码}) $
4. **路由记录解析**:
- 查找IP选项中的RR记录(类型0x07)
- 解析记录的路由IP地址序列:$ \text{跳数} = \frac{\text{指针位置}-4}{4} $
## 技术限制说明
1. **IP选项空间限制**:
- 最大路由记录数:$ \frac{40-3}{4} \approx 9.25 \Rightarrow 9 $
- 实际可用空间:39字节-头部=9个IPv4地址
2. **路由器支持**:
```mermaid
graph LR
A[ping -R请求] --> B{路由器支持RR?}
B -- 是 --> C[记录自身IP]
B -- 否 --> D[跳过记录]
```
3. **单程路径记录**:
- 只记录去程路由,不记录返程路径
- 实际路径可能不对称
## 相关扩展问题
1. 如何检测中间路由器是否丢弃了RR选项?
2. 能否实现带时间戳的路由跟踪?
3. 如何处理IPv6环境下的路由记录?
4. 原始套接字编程中如何避免安全风险?
[^1]: Linux系统中ping命令的基本用法和参数含义
[^2]: ICMP协议在ping命令中的工作原理和应用
阅读全文
相关推荐
















