# ARP及ICMP协议实现报告 本文档详细记录了在一个C语言网络协议栈项目中,从零开始实现地址解析协议(ARP)和Internet控制报文协议(ICMP)的完整过程。项目基于一个为Windows环境设计的初始模板,整个过程不仅包括了协议逻辑的实现,还涉及了代码的Linux兼容性改造和整体架构的模块化重构。 ## 1. 初始代码分析 项目开始于一个名为`start`的目录,其中包含一个简化的网络协议栈框架。 ### 1.1. 项目结构勘探 首先,我们分析了`start`目录下的源代码,特别是`xnet_tiny/src`子目录。我们识别出以下几个核心文件: - `xnet_tiny.h`: 定义了协议栈的基本数据结构,如以太网帧头`xether_hdr_t`和网络数据包`xnet_packet_t`。 - `xnet_tiny.c`: 包含了协议栈的核心驱动逻辑,如`xnet_init()`(初始化)和`xnet_poll()`(轮询)。 - `app.c`: 项目的主入口,调用`xnet_init()`和`xnet_poll()`来启动和运行协议栈。 - `xnet_app/port_pcap.c`: 负责与`pcap`库交互,进行底层网络数据包的收发。 ### 1.2. 关键实现点定位 在`xnet_tiny.c`中,我们发现了`ethernet_in`函数,它负责处理接收到的以太网数据帧。函数内部的`switch`语句根据以太网帧头的协议类型字段来分发数据包: ```c switch (swap_order16(hdr->protocol)) { case XNET_PROTOCOL_ARP: // 此处为空,是ARP实现入口 break; case XNET_PROTOCOL_IP: { // 此处为空,是IP及ICMP实现入口 break; } } ``` 这两个`case`分支是空的,这正是我们需要插入ARP和IP/ICMP处理逻辑的地方。 ## 2. ARP协议实现 为了保持代码的模块化,我们将ARP相关的逻辑实现在单独的文件中。 ### 2.1. 创建ARP模块 我们创建了以下两个文件: - `xarp.h`: 定义ARP协议包结构`xarp_packet_t`和ARP缓存条目`xarp_entry_t`,并声明了外部接口`xarp_init()`和`xarp_in()`。 - `xarp.c`: 实现ARP的具体逻辑。 ### 2.2. 实现ARP核心逻辑 在`xarp.c`中,我们完成了以下工作: 1. **ARP缓存(ARP Table)**: 定义了一个静态数组`arp_table[]`作为ARP缓存,用于存储IP地址到MAC地址的映射。 2. **`xarp_init()`**: 此函数在协议栈初始化时被调用,负责清空ARP缓存中的所有条目。 3. **`xarp_in()`**: 这是处理ARP报文的核心。 - 当接收到一个ARP请求报文时,检查其请求的目标IP地址是否为本机IP。如果是,则构建一个ARP响应报文,填入本机的MAC地址,然后通过`ethernet_out_to()`函数发送出去。 - 无论是请求还是响应,都会从报文中提取发送方的IP和MAC地址,并更新到ARP缓存中(`update_entry`函数)。 4. **`xarp_resolve()`**: 为了让上层协议(如IP)能够通过IP地址查询MAC地址,我们添加了此函数。它会首先查询ARP缓存。如果找到匹配的条目,则返回对应的MAC地址;如果未找到,则调用`send_arp_request()`广播一个ARP请求,并返回`NULL`,由上层决定是否重试。 ### 2.3. 集成到协议栈 我们将ARP模块集成到主协议栈中: 1. 在`xnet_tiny.c`中包含了`xarp.h`。 2. 在`xnet_init()`函数的末尾调用`xarp_init()`。 3. 在`ethernet_in()`的`switch`语句中,为`XNET_PROTOCOL_ARP`添加了对`xarp_in(packet)`的调用。 为了让`xarp.c`能够调用`xnet_tiny.c`中的`ethernet_out_to`等函数,我们将这些函数的`static`关键字移除,并在`xnet_tiny.h`中添加了它们的声明,使其成为公共接口。 ## 3. ICMP及IP层实现 与ARP类似,我们为ICMP和IP创建了独立的模块。 ### 3.1. 创建ICMP和IP模块 - `xicmp.h`: 定义了IP报头`xip_hdr_t`和ICMP报文结构`xicmp_packet_t`。 - `xicmp.c`: 实现了`xicmp_in()`函数,用于处理传入的ICMP报文。 - `xip.h` / `xip.c`: 创建了一个简单的IP输出模块,核心是`xip_out()`函数,用于发送IP包。 ### 3.2. 实现ICMP Echo Reply `xicmp_in()`是ICMP实现的核心,其工作流程如下: 1. **IP报头解析**: 首先,剥离以太网头部,解析IP报头。验证版本号、头部长度和头部校验和。 2. **目标IP检查**: 确认该IP包的目标地址是本机。 3. **ICMP报文处理**: - 剥离IP头部,解析ICMP报文。 - 检查ICMP类型是否为**Echo Request**(Ping请求)。 - 如果是,则准备一个**Echo Reply**(Ping应答)报文。大部分数据(如ID、序列号和数据负载)可以直接从请求报文中复制。 - 将ICMP类型设置为`ICMP_TYPE_ECHO_REPLY`,然后重新计算ICMP校验和。 4. **构建IP响应包**: - 构建IP头部,源和目的IP地址与收到的请求包相反。 - 重新计算IP头部校验和。 5. **发送响应**: 调用`xip_out()`函数将响应包发送出去。`xip_out`内部会使用`xarp_resolve`来查找下一跳的MAC地址。 ### 3.3. 集成到协议栈 1. 在`xnet_tiny.c`中包含了`xicmp.h`。 2. 在`ethernet_in()`的`switch`语句中,为`XNET_PROTOCOL_IP`添加了对`xicmp_in(packet)`的调用。 3. 在`xicmp.c`中,使用新创建的`xip_out()`函数来发送响应包,而不是直接调用底层的`ethernet_out_to()`,实现了更好的分层。 ## 4. Linux兼容性改造与重构 原始代码是为Windows设计的,为了在Linux上运行,我们进行了以下关键修改: 1. **修正`pcap_device.c`**: - 该文件包含了大量`#if defined(WIN32)`的条件编译块,用于加载`npcap`动态库。 - 我们删除了所有Windows特定的代码(如`#pragma comment`, `GetSystemDirectory`, `MessageBox`等),只保留了`#else`分支下的标准`pcap`实现,使其在Linux下能够直接编译通过。 2. **修正`port_pcap.c`**: - 原始代码中的`my_mac_addr`数组长度为8字节,这是错误的。我们将其修正为标准的6字节以太网MAC地址。 - 将硬编码的pcap设备IP地址从`192.168.254.1`修改为`192.168.254.2`,与我们协议栈配置的IP保持一致。 3. **IP地址集中管理**: - 最初,本机IP地址在`xarp.c`和`xicmp.c`中都有硬编码定义。 - 我们将其移至`xnet_tiny.c`中定义,并在`xnet_tiny.h`中通过`extern`声明,实现了全局统一配置。 ## 5. 编译与测试 完成编码后,即可进行编译测试: 1. **安装依赖**: 确保Linux系统上已安装`libpcap`的开发库(如`sudo apt install libpcap-dev`)。 2. **编译**: - 进入`start/build`目录。 - 运行`cmake ..`来生成Makefile。 - 运行`make`进行编译。 3. **运行与测试**: - 编译成功后,会在`build`目录下生成可执行文件`xnet.exe`。 - 使用`sudo ./xnet.exe`运行(需要root权限以访问网络接口)。 - 在另一台处于同一局域网的主机上,执行`ping 192.168.254.2`,即可测试ICMP Echo功能。同时,可以通过`tcpdump`或`Wireshark`抓包观察ARP请求和响应过程。 ## 6. 总结 通过以上步骤,我们成功地在一个基础网络框架上,完整地实现了ARP和ICMP Echo功能。整个过程不仅加深了对网络协议底层工作原理的理解,也锻炼了在现有代码基础上进行功能扩展、重构和跨平台移植的能力。最终的代码结构清晰,模块化程度高,易于理解和进一步扩展。