Files
NE_YuR/network/arpicmplab/arp/main.txt
2025-12-25 14:33:29 +08:00

524 lines
23 KiB
Plaintext
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#set page(header: [
#set par(spacing: 6pt)
#align(center)[#text(size: 11pt)[《计算机网络》实验报告]]
#v(-0.3em)
#line(length: 100%, stroke: (thickness: 1pt))
],)
#show heading: it => box(width: 100%)[
#v(0.50em)
#set text(font: hei)
#it.body
]
// #outline(title: "目录",depth: 3, indent: 1em)
// #outline(
// title: [图目录],
// target: figure.where(kind: image),
// )
#show heading: it => box(width: 100%)[
#v(0.50em)
#set text(font: hei)
#counter(heading).display()
// #h(0.5em)
#it.body
]
#set enum(indent: 0.5em,body-indent: 0.5em,)
#pagebreak()
// Display inline code in a small box
// that retains the correct baseline.
#show raw.where(block: false): box.with(
fill: luma(240),
inset: (x: 3pt, y: 0pt),
outset: (y: 3pt),
radius: 2pt,
)
// Display block code in a larger block
// with more padding.
#show raw.where(block: true): it => block(
text(font: ("Consolas","FangSong_GB2312"), it),
fill: luma(240),
inset: 10pt,
radius: 4pt,
width: 100%,
)
= 实验目的与要求
== 实验目的
== 实验要求
= 实验内容
= 实验原理
= 实验环境
== 实验背景
== 实验设备
#para[
#align(center)[#table(
columns: (auto, auto,auto),
rows:(2em,2em,3em),
inset: 10pt,
align: horizon+center,
table.header(
[*设备名称*], [*设备型号*], [*设备数量*]
),
"交换机", "华为S5735", "2",
"PC", "联想启天M410
Windows 10", "4",
)]
另有网线若干控制线2条。
]
= 实验步骤
== 环境配置
=== 虚拟机网络配置
#para[
安装Windows 10虚拟机并配置物理机和虚拟机的IP地址使其能够互相访问
- 物理机网卡IP地址配置为`192.168.254.1/24`
- 虚拟机IP地址配置为`192.168.254.3/24`。
#figure(image("物理机虚拟机IP配置.png",format: "png",width: 90%,fit: "stretch"),caption: "物理机与虚拟机IP配置")
配置好之后,在两边的命令行中分别使用`ping`命令测试是否能够互相访问。同时在物理机上开启Wireshark以过滤条件`icmp`进行抓包查看IP地址是否正确
#figure(image("环境配置ping测试.png",format: "png",width: 100%,fit: "stretch"),caption: "环境配置ping通测试")<figure2>
从@figure2 中可以看到物理机和虚拟机之间可以互相访问且Wireshark抓包显示IP地址正确。
]
=== 使用CMake运行项目
#para[
CMake配置较为简单。首先在开发工具中安装对应版本CMake插件。其次在终端中进入项目根目录在此使用```shell mkdir build```命令新建`build`文件夹并进入该文件夹。接下来使用CMake工具生成对应的Makefile文件```shell cmake -G"MinGW Makefiles" ..```。
然后再运行```shell make```命令编译项目,最后使用```shell xnet.exe```命令即可运行项目:
#figure(image("cmake编译运行.png",format: "png",width: 70%,fit: "stretch"),caption: "CMake配置")
其中MinGW是一个Windows下的GNU编译器套件可以在Windows下编译出Linux下的可执行文件。```shell cmake -G"MinGW Makefiles" ..```命令的作用是配置使用MinGW编译器。
至此,环境配置结束。
]
== 实现ARP协议
#para[
代码已经实现了最基础的以太网协议实现了以太网帧的封装和解封装。接下来在此基础上继续实现ARP协议。
]
=== 定义相关数据结构
#para[
在`xnet_tiny.h`中定义IP地址长度以及数据结构
```c
#define XNET_IPV4_ADDR_SIZE 4 // IP地址长度
// IP地址
typedef union _xipaddr_t {
uint8_t array[XNET_IPV4_ADDR_SIZE]; // 以数据形式存储的ip
uint32_t addr; // 32位的ip地址
}xipaddr_t;
```
该数据结构定义了IP地址的数据结构包括了IP地址的数组形式和32位的IP地址。
然后定义MAC地址的长度以及ARP表项的结构体
```c
#define XNET_MAC_ADDR_SIZE 6 // MAC地址长度
// ARP表项
typedef struct _xarp_entry_t {
xipaddr_t ipaddr; // ip地址
uint8_t macaddr[XNET_MAC_ADDR_SIZE]; // mac地址
uint8_t state; // 状态位
uint16_t tmo; // 当前超时
uint8_t retry_cnt; // 当前重试次数
}xarp_entry_t;
```
该结构体定义了ARP表项的数据结构包括了IP地址、MAC地址、状态位、超时时间和重试次数。
定义ARP表项的最大数量
```c
#define XARP_CFG_ENTRY_SIZE 6 // ARP表大小
```
随后,在`xnet_tiny.c`中将ARP表定义为全局变量并定义一个表项指针方便后续代码编写
```c
static xarp_entry_t arp_table[XARP_CFG_ENTRY_SIZE]; // ARP表
static xarp_entry_t* arp_entry; // ARP表项指针
```
]
=== ARP表初始化
#para[
接下来编写ARP表的初始化函数。首先在`xnet_tiny.h`中定义ARP表项的第一个状态
```c
#define XARP_ENTRY_FREE 0 // ARP表项空闲
```
然后在`xnet_tiny.c`中定义初始化函数```c void xarp_init(void)```
```c
// ARP初始化
void xarp_init(void) {
for (arp_entry = arp_table;
arp_entry < XARP_CFG_ENTRY_SIZE * sizeof(xarp_entry_t) + arp_table;
arp_entry = arp_entry + sizeof(xarp_entry_t))
{
arp_entry->state = XARP_ENTRY_FREE; // 此处用到了上面定义的状态
}
arp_entry = arp_table;
}
```
初始化函数```c void xarp_init(void)```是一个循环。首先将前面定义的全局表项指针指向ARP表的第一个表项循环结束条件为指针指向的表项的地址超过ARP表的最后一个表项的地址。循环会遍历ARP表中的所有表项将表项状态初始化为`XARP_STATE_FREE`。最后,函数会将表项指针指向第一个表项,避免其他初始化过程中可能的指针越界问题。
最后,在协议栈的初始化函数中添加```c xarp_init()```
```c
void xnet_init (void) {
ethernet_init(); // 初始化以太网
xarp_init(); // *初始化ARP
}
```
]
=== 定义ARP报文
#para[
接下来编写无回报ARP报文的相关函数所以需要先定义ARP报文结构以及它所用到的相关结构。
首先在`xnet_tiny.h`中定义ARP报文中的几个字段。可以靠Wireshark抓包分析来获取这些字段的值下面是一个示例展示通过抓包来获取```c XNET_PROTOCOL_IP = 0x0800```
#figure(table(
columns: (auto),
rows:(auto,auto),
inset: 10pt,
align: horizon+center,
figure(image("抓一个arp分析结构.png",format: "png",fit:"stretch",width: 100%),),
figure(image("分析结构2.png",format: "png",fit:"stretch",width: 100%),),
stroke: none,
),caption: "通过抓包分析来获取字段值",kind: image)
代码编写如下:
```c
#define XARP_HW_ETHER 0x1 // 硬件类型:以太网
#define XARP_REQUEST 0x1 // OpcodeARP请求包
#define XARP_REPLY 0x2 // OpcodeARP响应包
typedef enum _xnet_protocol_t {
XNET_PROTOCOL_ARP = 0x0806, // ARP协议
XNET_PROTOCOL_IP = 0x0800, // IPv4协议
XNET_PROTOCOL_ICMP = 1, // ICMP协议
}xnet_protocol_t;
```
然后定义ARP报文的数据结构
```c
typedef struct _xarp_packet_t {
uint16_t hw_type, pro_type; // 硬件类型和协议类型
uint8_t hw_len, pro_len; // 硬件地址长 + 协议地址长
uint16_t opcode; // 请求/响应
uint8_t sender_mac[XNET_MAC_ADDR_SIZE]; // 发送包硬件地址
uint8_t sender_ip[XNET_IPV4_ADDR_SIZE]; // 发送包协议地址
uint8_t target_mac[XNET_MAC_ADDR_SIZE]; // 接收方硬件地址
uint8_t target_ip[XNET_IPV4_ADDR_SIZE]; // 接收方协议地址
}xarp_packet_t;
```
然后,使用前面定义的```c union xipaddr_t```结构,在`xnet_tiny.h`和`xnet_tiny.c`中定义ARP报文发送函数需要用到的IP地址、组播MAC地址
```c
// xnet_tiny.h
#define XNET_CFG_NETIF_IP {192, 168, 254, 2} // 本项目模拟出的网卡的IP
// xnet_tiny.c
static const xipaddr_t netif_ipaddr = XNET_CFG_NETIF_IP;
static const uint8_t ether_broadcast[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
```
至此定义ARP报文的数据结构结束。
]
=== ARP报文发送函数
#para[
下面编写ARP报文发送函数```c xarp_make_request(const xipaddr_t * ipaddr)```。
```c
/**
* 产生一个ARP请求请求网络指定ip地址的机器发回一个ARP响应
* @param ipaddr 请求的IP地址
* @return 请求结果
*/
xnet_err_t xarp_make_request(const xipaddr_t * ipaddr) {
xarp_packet_t* arp_packet;
xnet_packet_t * packet = xnet_alloc_for_send(sizeof(xarp_packet_t));
arp_packet = (xarp_packet_t *)packet->data;
arp_packet->hw_type = swap_order16(XARP_HW_ETHER); // 设置硬件类型为以太网
arp_packet->pro_type = swap_order16(XNET_PROTOCOL_IP); // 设置协议类型为IP
arp_packet->hw_len = XNET_MAC_ADDR_SIZE; // 设置硬件地址长度
arp_packet->pro_len = XNET_IPV4_ADDR_SIZE; // 设置协议地址长度
arp_packet->opcode = swap_order16(XARP_REQUEST); // 设置操作码为ARP请求
// 复制发送方MAC地址
memcpy(arp_packet->sender_mac, netif_mac, XNET_MAC_ADDR_SIZE);
// 复制发送方IP地址
memcpy(arp_packet->sender_ip, netif_ipaddr.array, XNET_IPV4_ADDR_SIZE);
// 目标MAC地址清零
memset(arp_packet->target_mac, 0, XNET_MAC_ADDR_SIZE);
// 复制目标IP地址
memcpy(arp_packet->target_ip, ipaddr->array, XNET_IPV4_ADDR_SIZE);
// 通过以太网发送ARP请求
return ethernet_out_to(XNET_PROTOCOL_ARP, ether_broadcast, packet);
}
```
这个函数的主要功能是生成并发送一个ARP请求报文以请求指定IP地址即函数的输入```c const xipaddr_t * ipaddr```的机器返回其MAC地址。函数的具体步骤如下
1. 分配一个用于发送的ARP数据包```c arp_packet```并将其数据段设置为ARP报文结构。
2. 设置ARP报文的各个字段包括硬件类型`hw_type`、协议类型`pro_type`、硬件地址长度`hw_len`、协议地址长度`pro_len`、操作码`opcode`(设置为`XARP_REQUEST`)等。
3. 复制发送方即本项目模拟出的网卡的MAC地址和IP地址到ARP报文中。
4. 将目标MAC地址字段清零并复制目标IP地址到ARP报文中。
5. 最后通过以太网发送该ARP请求报文返回发送结果状态码
]
=== 启动时的ARP请求
#para[
在以太网协议的初始化函数```c static xnet_err_t ethernet_init(void)```中添加一个ARP请求
```c
/**
* 以太网初始化
* @return 初始化结果
*/
static xnet_err_t ethernet_init (void) {
xnet_err_t err = xnet_driver_open(netif_mac);
if (err < 0) return err;
return xarp_make_request(&netif_ipaddr); // 发送ARP请求
}
```
这样当协议栈初始化时会发送一个ARP请求。
下面用Wireshark抓包来验证ARP请求是否发送成功。首先重新编译项目其次开启Wireshark抓包最后启动程序
#figure(image("启动ARP2.png",format: "png",width: 100%,fit: "stretch"),caption: "启动时的ARP请求")<figure3>
从@figure3 中可以看到ARP请求发送成功说明编写至此的代码没有问题。
]
=== ARP报文接收函数
#para[
ARP报文接收函数主要功能是处理接收到的ARP报文包括解析报文、更新ARP表、发送ARP响应等。下面根据这些需求编写ARP报文接收函数```c void xarp_in(xnet_packet_t * packet)```
```c
/**
* 处理接收到的ARP包
* @param packet 输入的ARP包
*/
void xarp_in(xnet_packet_t * packet) {
// 检查包的大小是否符合ARP包的最小长度要求
if (packet->size >= sizeof(xarp_packet_t)) {
xarp_packet_t * arp_packet = (xarp_packet_t *) packet->data;
uint16_t opcode = swap_order16(arp_packet->opcode);
// 检查包的合法性,包括硬件类型、硬件地址长度、协议类型、协议地址长度和操作码
if ((swap_order16(arp_packet->hw_type) != XARP_HW_ETHER) ||
(arp_packet->hw_len != XNET_MAC_ADDR_SIZE) ||
(swap_order16(arp_packet->pro_type) != XNET_PROTOCOL_IP) ||
(arp_packet->pro_len != XNET_IPV4_ADDR_SIZE)
|| ((opcode != XARP_REQUEST) && (opcode != XARP_REPLY))) {
return;
}
// 只处理目标IP地址为自己的ARP请求或响应包
if (!xipaddr_is_equal_buf(&netif_ipaddr, arp_packet->target_ip)) {
return;
}
// 根据操作码进行处理
switch (swap_order16(arp_packet->opcode)) {
case XARP_REQUEST: // 处理ARP请求发送ARP响应并更新ARP表项
xarp_make_response(arp_packet);
update_arp_entry(arp_packet->sender_ip, arp_packet->sender_mac);
break;
case XARP_REPLY: // 处理ARP响应更新ARP表项
update_arp_entry(arp_packet->sender_ip, arp_packet->sender_mac);
break;
}
}
}
```
该函数主要功能是处理接收到的ARP包。首先进行简单的长度判断避免后续字段读取失败造成内存错误。随后检查包的合法性包括硬件类型、硬件地址长度、协议类型、协议地址长度和操作码。APR响应只要求机器处理目标IP地址为自己的ARP请求或响应包所以使用```c if (!xipaddr_is_equal_buf(&netif_ipaddr, arp_packet->target_ip))```来判断。最后根据操作码进行处理分别处理ARP请求和ARP响应
- ARP请求发送ARP响应```c xarp_make_response(...)```并更新ARP表项```c update_arp_entry(...)```
- ARP响应只需要更新ARP表项。
其中,用到的宏```c xipaddr_is_equal_buf()```函数用于比较两个IP地址是否相等实现如下
```c
// 比较IP地址是否相等
#define xipaddr_is_equal_buf(addr, buf) (memcmp(
(addr)->array,
(buf),
XNET_IPV4_ADDR_SIZE
)
== 0
)
```
然后,需要编写上面函数中调用的两个函数:```c xarp_make_response()```和```c update_arp_entry()```。
```c xarp_make_response()```函数主要功能是输入一个ARP请求包通过此包内的源信息生成对应的ARP响应并发送出去。具体代码如下
```c
/**
* 生成一个ARP响应
* @param arp_packet 接收到的ARP请求包
* @return 生成结果
*/
xnet_err_t xarp_make_response(xarp_packet_t * arp_packet) {
xarp_packet_t* response_packet;
xnet_packet_t * packet = xnet_alloc_for_send(sizeof(xarp_packet_t));
response_packet = (xarp_packet_t *)packet->data;
response_packet->hw_type = swap_order16(XARP_HW_ETHER); // 设置硬件类型为以太网
response_packet->pro_type = swap_order16(XNET_PROTOCOL_IP); // 设置协议类型为IP
response_packet->hw_len = XNET_MAC_ADDR_SIZE; // 设置硬件地址长度
response_packet->pro_len = XNET_IPV4_ADDR_SIZE; // 设置协议地址长度
response_packet->opcode = swap_order16(XARP_REPLY); // 设置操作码为ARP响应
// 复制目标MAC地址
memcpy(response_packet->target_mac, arp_packet->sender_mac, XNET_MAC_ADDR_SIZE);
// 复制目标IP地址
memcpy(response_packet->target_ip, arp_packet->sender_ip, XNET_IPV4_ADDR_SIZE);
// 复制发送方MAC地址
memcpy(response_packet->sender_mac, netif_mac, XNET_MAC_ADDR_SIZE);
// 复制发送方IP地址
memcpy(response_packet->sender_ip, netif_ipaddr.array, XNET_IPV4_ADDR_SIZE);
// 通过以太网发送ARP响应
return ethernet_out_to(XNET_PROTOCOL_ARP, ether_broadcast, packet);
}
```
可以发现此函数与前面的ARP请求函数```c xarp_make_request()```非常相似,只是操作码不同,此处为`XARP_REPLY`其他字段均从源ARP请求报文中获取并填入对应区域。
```c update_arp_entry()```函数主要功能是更新所有ARP表项附带一定的可视化功能。具体代码如下
```c
/**
* 更新ARP表项
* @param src_ip 源IP地址
* @param mac_addr 对应的mac地址
*/
static void update_arp_entry(uint8_t* src_ip, uint8_t* mac_addr) {
for (arp_entry = arp_table;
arp_entry < XARP_CFG_ENTRY_SIZE * sizeof(xarp_entry_t) + arp_table;
arp_entry = arp_entry + sizeof(xarp_entry_t))
{
// 检查ARP表项是否为空或者是否与给定的源IP地址匹配且状态不是有效的
if (arp_entry->state == XARP_ENTRY_FREE ||
( arp_entry->state == XARP_ENTRY_OK
&& xipaddr_is_equal_buf(&arp_entry->ipaddr, src_ip)
))
{
// 更新ARP表项中的IP地址和MAC地址
memcpy(arp_entry->ipaddr.array, src_ip, XNET_IPV4_ADDR_SIZE);
memcpy(arp_entry->macaddr, mac_addr, 6);
printf("learned☝🤓mac addr\n");
for (
int i = 0;
i < sizeof(mac_addr) / sizeof(mac_addr[0]);
++i)
{
printf("%02X%c",
mac_addr[i],
i < sizeof(mac_addr) / sizeof(mac_addr[0]) - 1 ? ':' : '\n'
);
}
// 设置ARP表项状态为有效
arp_entry->state = XARP_ENTRY_OK;
// 设置ARP表项的超时时间
arp_entry->tmo = XARP_CFG_ENTRY_OK_TMO;
// 设置ARP表项的重试次数
arp_entry->retry_cnt = XARP_CFG_MAX_RETRIES;
print_arp_table(); // 打印完整的ARP表
return; // 更新后退出函数
}
}
// 如果ARP表已满采用LRU策略替换最老的表项
arp_entry = arp_table; // 重置arp_entry指向表头
xarp_entry_t* oldest_entry = NULL;
uint32_t oldest_tmo = 0xFFFFFFFF;
for (arp_entry = arp_table;
arp_entry < XARP_CFG_ENTRY_SIZE * sizeof(xarp_entry_t) + arp_table;
arp_entry = arp_entry + sizeof(xarp_entry_t))
{
if (arp_entry->tmo < oldest_tmo) {
oldest_tmo = arp_entry->tmo;
oldest_entry = arp_entry;
}
}
if (oldest_entry != NULL) {
// 更新最老的ARP表项
memcpy(oldest_entry->ipaddr.array, src_ip, XNET_IPV4_ADDR_SIZE);
memcpy(oldest_entry->macaddr, mac_addr, 6);
printf("learned☝🤓mac addr\n");
for (int i = 0; i < sizeof(mac_addr) / sizeof(mac_addr[0]); ++i) {
printf("%02X%c", mac_addr[i], i < sizeof(mac_addr) / sizeof(mac_addr[0]) - 1 ? ':' : '\n');
}
// 设置ARP表项状态为有效
oldest_entry->state = XARP_ENTRY_OK;
// 设置ARP表项的超时时间
oldest_entry->tmo = XARP_CFG_ENTRY_OK_TMO;
// 设置ARP表项的重试次数
oldest_entry->retry_cnt = XARP_CFG_MAX_RETRIES;
print_arp_table(); // 打印完整的ARP表
}
}
```
这个函数很长。它主要功能是更新ARP表项。更新分为两种情况
- ARP表还有空闲表项
- ARP表已满采用LRU策略替换最老的表项
首先函数通过遍历ARP表中的所有表项检查表项是否为空或者是否与给定的源IP地址匹配且状态不是有效的。如果满足条件则更新ARP表项中的IP地址和MAC地址并设置表项状态为有效设置超时时间和重试次数最后会打印完整的ARP表。如果ARP表已满则采用LRU策略替换最老的表项。函数会遍历ARP表找到超时时间最小的表项并更新该表项的IP地址和MAC地址设置表项状态为有效设置超时时间和重试次数最后打印完整的ARP表。
用到的打印函数实现如下:
```c
/**
* 打印完整的ARP表
*/
void print_arp_table() {
printf("\n----ARP Table----\n");
for (arp_entry = arp_table; arp_entry < XARP_CFG_ENTRY_SIZE * sizeof(xarp_entry_t) + arp_table; arp_entry = arp_entry + sizeof(xarp_entry_t)) {
if (arp_entry->state != XARP_ENTRY_FREE) {
printf("IP: ");
for (int i = 0; i < XNET_IPV4_ADDR_SIZE; ++i) {
printf("%d%c",
arp_entry->ipaddr.array[i],
i < XNET_IPV4_ADDR_SIZE - 1 ? '.' : '\n'
);
}
printf("MAC: ");
for (int i = 0; i < 6; ++i) {
printf("%02X%c", arp_entry->macaddr[i], i < 5 ? ':' : '\n');
}
printf(
"State: %s\n",
arp_entry->state == XARP_ENTRY_FREE ? "FREE" :
arp_entry->state == XARP_ENTRY_RESOLVING ? "RESOLVING" : "OK"
);
}
}
printf("\n-----------------\n");
}
```
最后需要在以太网帧接收函数中添加ARP报文的处理
```c
/**
* 以太网数据帧输入输出
* @param packet 待处理的包
*/
static void ethernet_in (xnet_packet_t * packet) {
// 至少要比头部数据大
if (packet->size <= sizeof(xether_hdr_t)) {
return;
}
// 根据协议类型分发到不同的处理函数
xether_hdr_t* hdr = (xether_hdr_t*)packet->data;
switch (swap_order16(hdr->protocol)) {
case XNET_PROTOCOL_ARP:
// 移除以太网头部处理ARP协议
remove_header(packet, sizeof(xether_hdr_t));
xarp_in(packet);
break;
case XNET_PROTOCOL_IP: {
break;
}
}
}
```
其中,主要在```c case XNET_PROTOCOL_ARP```中添加了对ARP报文的处理。
在继续之前再次使用Wireshark检验这部分代码编写。重新编译后按照以下流程进行检验
- 开启Wireshark抓包
- 运行本程序;
- 在虚拟机上ping本程序以触发ARP请求
- 查看Wireshark抓包结果和程序输出。
#figure(image("ARP响应.png",format: "png",width: 100%,fit: "stretch"),caption: "ARP请求响应")<figure4>
从@figure4 中可以看到ARP响应都发送成功程序输出中也表明学习到了虚拟机的MAC地址说明代码编写正确。
]
=== ARP超时重传
= 实验总结
== 内容总结
== 心得感悟
#show heading: it => box(width: 100%)[
#v(0.50em)
#set text(font: hei)
// #counter(heading).display()
// #h(0.5em)
#it.body
]
#pagebreak()
#bibliography("ref.yml",full: true,title: "参考文献",style:"gb-7714-2015-numeric")