#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 中可以看到,物理机和虚拟机之间可以互相访问,且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 // Opcode:ARP请求包 #define XARP_REPLY 0x2 // Opcode:ARP响应包 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 中可以看到,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 中可以看到,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")