arplab report almost finished
0
network/arpicmplab/arp/ARP响应.png → network/arpicmplab/ARP响应.png
Executable file → Normal file
|
Before Width: | Height: | Size: 406 KiB After Width: | Height: | Size: 406 KiB |
@@ -1,154 +0,0 @@
|
||||
#let times = "Times LT Pro"
|
||||
#let times = "Times New Roman"
|
||||
#let song = (times, "FZShuSong-Z01")
|
||||
#let hei = (times, "FZHei-B01")
|
||||
#let kai = (times, "FZKai-Z03")
|
||||
#let xbsong = (times, "FZXiaoBiaoSong-B05")
|
||||
#let fsong = (times, "FangSong_GB2312")
|
||||
#let code = (times, "DejaVu Sans Mono")
|
||||
#let nudtlabpaper(title: "",
|
||||
author: "",
|
||||
id: "",
|
||||
training_type:"",
|
||||
grade: "",
|
||||
major: "",
|
||||
department: "",
|
||||
advisor: "",
|
||||
jobtitle: "",
|
||||
lab: "",
|
||||
date: "",
|
||||
header_str: "",
|
||||
body) = {
|
||||
// Set the document's basic properties.
|
||||
set document(author: author, title: title)
|
||||
set page(
|
||||
|
||||
margin: (left: 30mm, right: 30mm, top: 30mm, bottom: 30mm),
|
||||
)
|
||||
|
||||
// Title row.
|
||||
v(158pt)
|
||||
align(center)[
|
||||
#block(text(weight: 700, size: 30pt, font: hei, tracking: 15pt, "计算机网络"))
|
||||
]
|
||||
align(center)[
|
||||
#block(text(weight: 700, size: 30pt, font: song, tracking: 15pt, "本科实验报告"))
|
||||
]
|
||||
|
||||
v(103pt)
|
||||
pad(
|
||||
left: 1em,
|
||||
right: 1em,
|
||||
grid(
|
||||
columns: (80pt, 1fr),
|
||||
rows: (17pt, auto),
|
||||
text(weight: 700, size: 16pt, font: song, "实验名称:"),
|
||||
align(center, text(weight: "regular", size: 16pt, font: song, title)),
|
||||
text(""),
|
||||
line(length: 100%)
|
||||
)
|
||||
// #block(text(weight: 700, 1.75em, title))
|
||||
// underline(text(weight: 700, size: 16pt, font: song, title))
|
||||
)
|
||||
|
||||
// Author information.
|
||||
|
||||
v(82.5pt)
|
||||
|
||||
grid(
|
||||
columns: (0.25fr, 0.25fr, 0.25fr, 0.25fr),
|
||||
rows: (15pt, 8pt, 15pt, 8pt, 15pt, 8pt, 15pt, 8pt, 15pt),
|
||||
text(size: 14pt, font: song, tracking: 10pt, "学员姓名"),
|
||||
align(center, text(size: 14pt, font: song, author)),
|
||||
text(size: 14pt, font: song, tracking: 54pt, "学号"),
|
||||
align(center, text(size: 14pt, font: times, id)),
|
||||
text(""),
|
||||
line(length: 100%),
|
||||
text(""),
|
||||
line(length: 100%),
|
||||
text(size: 14pt, font: song, tracking: 9pt, "培养类型"),
|
||||
align(center, text(size: 14pt, font: song, training_type)),
|
||||
text(size: 14pt, font: song, tracking: 54pt, "年级"),
|
||||
align(center, text(size: 14pt, font: times, grade)),
|
||||
text(""),
|
||||
line(length: 100%),
|
||||
text(""),
|
||||
line(length: 100%),
|
||||
text(size: 14pt, font: song, tracking: 54pt, "专业"),
|
||||
align(center, text(size: 14pt, font: song, major)),
|
||||
text(size: 14pt, font: song, tracking: 9pt, "所属学院"),
|
||||
align(center, text(size: 14pt, font: song, department)),
|
||||
text(""),
|
||||
line(length: 100%),
|
||||
text(""),
|
||||
line(length: 100%),
|
||||
text(size: 14pt, font: song, tracking: 9pt, "指导教员"),
|
||||
align(center, text(size: 14pt, font: song, advisor)),
|
||||
text(size: 14pt, font: song, tracking: 54pt, "职称"),
|
||||
align(center, text(size: 14pt, font: song, jobtitle)),
|
||||
text(""),
|
||||
line(length: 100%),
|
||||
text(""),
|
||||
line(length: 100%),
|
||||
text(size: 14pt, font: song, tracking: 20pt, "实验室"),
|
||||
align(center, text(size: 14pt, font: song, lab)),
|
||||
text(size: 14pt, font: song, tracking: 9pt, "实验时间"),
|
||||
align(center, text(size: 14pt, font: song, date)),
|
||||
text(""),
|
||||
line(length: 100%),
|
||||
text(""),
|
||||
line(length: 100%),
|
||||
)
|
||||
|
||||
v(50.5pt)
|
||||
align(center, text(font: hei, size: 15pt, "国防科技大学教育训练部制"))
|
||||
|
||||
pagebreak()
|
||||
|
||||
set page(
|
||||
margin: (left: 30mm, right: 30mm, top: 30mm, bottom: 30mm),
|
||||
numbering: "i",
|
||||
number-align: center,
|
||||
)
|
||||
|
||||
v(14pt)
|
||||
align(center)[
|
||||
#block(text(font: hei, size: 14pt, "《本科实验报告》填写说明"))
|
||||
]
|
||||
|
||||
v(14pt)
|
||||
text("")
|
||||
par(first-line-indent: 2em, text(font: song, size: 12pt, "实验报告内容编排应符合以下要求:"))
|
||||
|
||||
par(first-line-indent: 2em, text(font: fsong, size: 12pt, "(1)采用A4(21cm×29.7cm)白色复印纸,单面黑字。上下左右各侧的页边距均为3cm;缺省文档网格:字号为小4号,中文为宋体,英文和阿拉伯数字为Times New Roman,每页30行,每行36字;页脚距边界为2.5cm,页码置于页脚、居中,采用小5号阿拉伯数字从1开始连续编排,封面不编页码。"))
|
||||
|
||||
par(first-line-indent: 2em, text(font: fsong, size: 12pt, "(2)报告正文最多可设四级标题,字体均为黑体,第一级标题字号为4号,其余各级标题为小4号;标题序号第一级用“一、”、“二、”……,第二级用“(一)”、“(二)” ……,第三级用“1.”、“2.” ……,第四级用“(1)”、“(2)” ……,分别按序连续编排。"))
|
||||
|
||||
par(first-line-indent: 2em, text(font: fsong, size: 12pt, "(3)正文插图、表格中的文字字号均为5号。"))
|
||||
|
||||
pagebreak()
|
||||
|
||||
set page(
|
||||
margin: (left: 30mm, right: 30mm, top: 30mm, bottom: 30mm),
|
||||
numbering: "1",
|
||||
number-align: center,
|
||||
)
|
||||
|
||||
set heading(numbering: "1.1")
|
||||
// set text(font: hei, lang: "zh")
|
||||
|
||||
show heading: it => box(width: 100%)[
|
||||
#v(0.50em)
|
||||
#set text(font: hei)
|
||||
#counter(heading).display()
|
||||
// #h(0.5em)
|
||||
#it.body
|
||||
]
|
||||
// Main body.
|
||||
set par(justify: true)
|
||||
|
||||
body
|
||||
}
|
||||
|
||||
#let para(t) = par(first-line-indent: 2em, text(font: song, size: 10.5pt, t))
|
||||
|
||||
@@ -1,523 +0,0 @@
|
||||
#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 // 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>
|
||||
从@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")
|
||||
@@ -1,964 +0,0 @@
|
||||
#import "labtemplate.typ": *
|
||||
#show: nudtlabpaper.with(title: "TCP/IP 协议栈 ARP 协议实现实验",
|
||||
author: "王李烜",
|
||||
id: "202202001046",
|
||||
training_type: "无军籍",
|
||||
grade: "2022",
|
||||
major: "网络工程",
|
||||
department: "计算机学院",
|
||||
advisor: "邱振宇",
|
||||
jobtitle: "讲师",
|
||||
lab: "305-505",
|
||||
date: "2024.12.16",
|
||||
header_str: "《计算机网络》实验报告",
|
||||
)
|
||||
|
||||
#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)
|
||||
#pagebreak()
|
||||
#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()
|
||||
|
||||
|
||||
= 实验概要
|
||||
== 实验内容
|
||||
#para[
|
||||
本次实验的主要内容 ARP 协议实现。本次实验包含基础任务和拓展任务两部分,
|
||||
具体任务要求如下:
|
||||
|
||||
- 基础任务:编写程序,完善 TCP/IP 协议栈的 ARP 协议部分。围绕 ARP 的初始化、无回报 ARP 的生成、ARP 的输入处理,以及 ARP 的超时重新请 求几个部分完成。并且保证完成 ARP 协议的完整实现。#box(
|
||||
text(font: ("Times LT Pro","FZXiaoBiaoSong-B05"),fill: luma(240), weight:"bold", "本实验中Completed!"),
|
||||
fill:blue,
|
||||
inset: (x: 3pt, y: 0pt),
|
||||
outset: (y: 3pt),
|
||||
radius: 2pt,)
|
||||
|
||||
- 拓展任务:拓展任务是可选任务,在基础任务实现的 ARP 协议实现基础上, 可选择性的完成如下任务:
|
||||
+ ARP 多个表项的实现;#box(
|
||||
text(font: ("Times LT Pro","FZXiaoBiaoSong-B05"),fill: luma(240), weight:"bold", "本实验中Completed!"),
|
||||
fill:blue,
|
||||
inset: (x: 3pt, y: 0pt),
|
||||
outset: (y: 3pt),
|
||||
radius: 2pt,)
|
||||
+ IP 层的输入输出处理。#box(
|
||||
text(font: ("Times LT Pro","FZXiaoBiaoSong-B05"),fill: luma(240), weight:"bold", "本实验中Completed!"),
|
||||
fill:blue,
|
||||
inset: (x: 3pt, y: 0pt),
|
||||
outset: (y: 3pt),
|
||||
radius: 2pt,)
|
||||
]
|
||||
== 实验要求
|
||||
#para[
|
||||
本实验的具体过程及对应要求如下:
|
||||
- 实验开始前准备工作:在实验开始前,学员需要掌握 C语言 编程基础,理解 TCP/IP 协议栈的工作原理,尤其是 ARP 协议的功能和作用。同时,熟悉 MAC 地址与 IP 地址的转换原理,了解网络设备如何通过 ARP 请求与响应进行地址解析。
|
||||
- 实验过程中:按照实验要求,完成 ARP 协议的实现。具体步骤包括:具体而言,构造 ARP 请求和响应报文,实现报文格式的编码与解析。发送 ARP请求,构建并广播 ARP 请求,获取目标设备的 MAC 地址。处理 ARP 响应,在收到响应后,提取并记录目标 IP 与 MAC 地址的映射。管理 ARP 缓存,设计缓存机制,存储 IP-MAC 映射,并实现超时处理机制。
|
||||
- 实验结束后:总结 ARP 协议的实现过程,详细描述报文格式、缓存管理和通信流程,并根据实验要求撰写实验报告,分析实验结果。
|
||||
]
|
||||
== 实验目的
|
||||
#para[
|
||||
在现代网络环境中, ARP协议广泛应用于各种网络设备和系统,如计算机、路由器和交换机等。深入理解ARP的工作原理,有助于掌握网络设备之间的通信机制,理解数据在网络中的传输过程。特别是对于网络工程和网络安全领域,从协议层面了解ARP,有助于识别和防范诸如ARP欺骗等网络攻击,提高网络的安全防护能力。
|
||||
|
||||
通过本次实验,学员将亲自动手实现ARP协议的核心功能,包括ARP请求与响应的构建与解析、ARP缓存表的管理等。这不仅加深了对TCP/IP协议栈的理解,也培养了实际编程和问题解决的能力。掌握ARP协议的实现,对后续学习更复杂的网络协议(如IP、ICMP、TCP和UDP)以及从事网络相关工作都有重要的意义。
|
||||
]
|
||||
// Display inline code in a small box
|
||||
// that retains the correct baseline.
|
||||
#show raw.where(block: false): it => box(
|
||||
text(font: ("Consolas","FangSong_GB2312"), it),
|
||||
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[
|
||||
ARP(地址解析协议)是 TCP/IP 协议族中用于将 IP 地址解析为 MAC 地址的重要协议。IP 通信依赖于数据链路层的硬件地址(MAC 地址),而 ARP 负责动态地将网络层的 IP 地址转换为对应的数据链路层 MAC 地址,从而实现设备间的通信。ARP 协议的实现主要包括发送 ARP 请求、接收并处理 ARP 响应、更新 ARP 缓存、以及缓存超时机制。
|
||||
]
|
||||
== ARP的初始化
|
||||
#para[
|
||||
在一个典型的局域网中,设备通过 IP 地址进行网络层通信,但 IP 地址并不能直接用于数据链路层传输。以太网等数据链路层协议使用 MAC 地址进行通信,因此,发送设备需要将目标 IP 地址解析为 MAC 地址才能发送数据帧。
|
||||
|
||||
如果该设备的 ARP 缓存中没有目标设备的 MAC 地址映射,它会广播 ARP 请求,询问网络上哪个设备持有特定的 IP 地址。ARP 请求是一个以太网层的广播包,发送到子网内所有设备,只有持有目标 IP 地址的设备才会进行响应。
|
||||
|
||||
ARP 初始化的过程是设备发现并解析网络中其他设备的关键步骤。ARP 请求包含源设备的 IP 地址和 MAC 地址,而目标设备通过 ARP 响应提供其对应的 MAC 地址。这个机制确保设备能够通过网络层(IP 地址)和链路层(MAC 地址)之间建立正确的映射关系。
|
||||
]
|
||||
== 无回报 ARP 的生成
|
||||
#para[
|
||||
无回报 ARP(Gratuitous ARP),又称为“主动 ARP”或“自愿 ARP”,是一种特殊的 ARP 操作。与典型的 ARP 请求不同,无回报 ARP 并不是为了解析目标设备的 MAC 地址,而是设备主动向网络发送广播 ARP 包,通常用于更新网络中的 IP-MAC 映射关系、检测 IP 地址冲突等。
|
||||
|
||||
无回报 ARP 是设备主动广播自身的 IP 地址和 MAC 地址,不带有显式的 ARP 请求和响应互动。其主要目的是通知网络中其他设备更新其 ARP 缓存表中的信息。这种情况下,设备并不期待其他设备回应。它是单向广播的,通常被用于下列几种情况:
|
||||
|
||||
- 更新网络中的 ARP 表:当设备的 MAC 地址或 IP 地址发生变动时,可以主动发送无回报 ARP,以便通知网络中其他设备更新其 ARP 缓存。
|
||||
- IP 冲突检测:设备在启动时,通过发送无回报 ARP 来检测是否有其他设备占用了相同的 IP 地址。如果另一台设备使用了相同的 IP 地址,它会回应此 ARP 广播,从而帮助设备检测到 IP 冲突。
|
||||
- 负载均衡器和高可用性系统:当系统切换主备设备时,备设备通常会发送无回报 ARP 来通知网络中的所有节点其 IP-MAC 映射已经改变,避免继续向已下线的设备发送数据。
|
||||
|
||||
无回报 ARP 的生成过程如下:
|
||||
|
||||
1. 生成 ARP 广播包:设备在确定需要广播自身 IP-MAC 映射时,会生成一个 ARP 广播包。该包包含设备自身的 IP 地址和 MAC 地址,并且目标硬件地址设置为全 0,因为无回报 ARP 并不是请求对方设备的 MAC 地址,而是向网络中的所有设备广播自身的信息。
|
||||
2. 设置操作码为 ARP 请求:尽管无回报 ARP 是主动广播,但它在帧结构中被标记为 ARP 请求(操作码为 1),这使得网络中的其他设备会将其视为一种信息广播,用于更新 ARP 缓存。
|
||||
3. 发送广播:ARP 广播包通过以太网层进行传输,目标 MAC 地址为 FF:FF:FF:FF:FF:FF,即局域网内的所有设备都可以接收到此广播。
|
||||
4. 网络中的设备处理:网络中所有收到此广播的设备会检查 ARP 包中的发送方 IP 地址和 MAC 地址,并将其更新到本地的 ARP 缓存表中。这样,即使该 IP 地址之前未出现在这些设备的 ARP 表中,它们也会记录并更新新的映射。
|
||||
]
|
||||
== ARP 的输入处理
|
||||
#para[
|
||||
ARP(地址解析协议)的输入处理指的是设备在接收到 ARP 请求或响应时,如何对该 ARP 报文进行解析和处理,并据此更新设备的 ARP 缓存,或进一步采取必要的网络行为。ARP 输入处理的核心任务是解析报文,更新 ARP 缓存,并根据报文类型采取不同的操作。
|
||||
|
||||
在这部分有以下步骤:
|
||||
|
||||
- 接收 ARP 报文:设备通过网络接口接收到 ARP 报文,无论是广播还是单播形式。这些 ARP 报文可以是 ARP 请求、ARP 响应,或者是无回报 ARP。
|
||||
- 解析 ARP 报文:设备对 ARP 报文进行解析,提取其中的关键信息。
|
||||
- 检查报文有效性:设备检查 ARP 报文的有效性,包括检查硬件类型是否为以太网、协议类型是否为 IPv4、操作码是否为合法的请求或响应。如果报文不符合 ARP 协议规定,设备将丢弃该报文。
|
||||
- 更新 ARP 缓存:根据 ARP 报文中的信息,设备更新自己的 ARP 缓存表。设备通常会把报文中的发送方 IP 地址和发送方 MAC 地址映射记录下来,以便将来进行快速的 IP 到 MAC 地址解析。
|
||||
- 据操作码进行处理:不同类型的 ARP 报文有不同的处理方式:
|
||||
- 如果接收到的是 ARP 请求,设备需要检查目标 IP 地址是否与自身的 IP 地址匹配,如果匹配,则需要发送一个 ARP 响应包,告知请求设备自己的 MAC 地址。
|
||||
- 如果接收到的是 ARP 响应,设备会根据响应包中的信息,更新或添加到 ARP 缓存表,并不再发送进一步的响应。
|
||||
- 如果接收到的是无回报 ARP,设备会将报文中的 IP-MAC 映射记录下来,以更新其 ARP 缓存。
|
||||
]
|
||||
== ARP 的超时重新请求机制
|
||||
#para[
|
||||
ARP(地址解析协议)的超时重新请求机制指的是设备在尝试解析某个 IP 地址到 MAC 地址时,若未能在设定的时间内收到响应,会采取的重发 ARP 请求的策略。这种机制旨在保证网络设备在通信中能够及时获取目标设备的 MAC 地址,并维持 ARP 缓存的准确性。
|
||||
|
||||
ARP 缓存存储的是 IP 地址与 MAC 地址之间的映射关系。在通信过程中,网络设备通常会先查询 ARP 缓存以查找目标设备的 MAC 地址。如果缓存中存在该 IP 地址的记录,设备会直接使用缓存中的 MAC 地址进行通信;如果没有找到相应记录,设备会发出 ARP 请求,广播请求目标 IP 地址对应的 MAC 地址。
|
||||
|
||||
如果设备在发送 ARP 请求后,未能在指定的时间内收到 ARP 响应,它会认为该 ARP 请求失败。这时,设备会重新发送 ARP 请求,通常会进行一定次数的重发,以确保能够成功解析目标设备的 MAC 地址。
|
||||
]
|
||||
= 实验环境
|
||||
== 实验设备与软件
|
||||
#para[
|
||||
#align(center)[#table(
|
||||
columns: (auto, auto),
|
||||
rows:(auto,auto,auto),
|
||||
inset: 10pt,
|
||||
align: horizon+center,
|
||||
table.header(
|
||||
[*名称*], [*型号或版本*],
|
||||
),
|
||||
"物理机", "联想ThinkPad-Windows 10 22H4",
|
||||
"虚拟机", "Virtual Box-Windows 10 22H4",
|
||||
"Wireshark", "Wireshark 4.4.0",
|
||||
"CMake", "CMake 3.31.3"
|
||||
)]
|
||||
]
|
||||
= 实验步骤
|
||||
== 环境配置
|
||||
=== 虚拟机网络配置
|
||||
#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: 90%,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: 76%,fit: "stretch"),caption: "CMake配置")
|
||||
其中,MinGW是一个Windows下的GNU编译器套件,可以在Windows下编译出Linux下的可执行文件。```shell cmake -G"MinGW Makefiles" ..```命令的作用是配置使用MinGW编译器。
|
||||
|
||||
至此,环境配置结束。
|
||||
]
|
||||
== 实现ARP协议
|
||||
#para[
|
||||
代码已经实现了最基础的以太网协议,实现了以太网帧的封装和解封装。接下来在此基础上继续实现ARP协议。
|
||||
]
|
||||
=== 相关数据结构<multiple_entry>
|
||||
#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 8 // 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表初始化<State1>
|
||||
#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>
|
||||
从@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超时重传
|
||||
#para[
|
||||
首先,需要定义ARP表项的其他两种状态#footnote[第一种状态已经在@State1 中定义过了。]:解析成功、和正在解析(即已发出重传的ARP请求报文,但还未收到响应):
|
||||
```c
|
||||
#define XARP_ENTRY_OK 1 // ARP表项解析成功
|
||||
#define XARP_ENTRY_RESOLVING 2 // ARP表项正在解析
|
||||
#define XARP_TIMER_PERIOD 1 // ARP扫描周期,1s足够
|
||||
```
|
||||
然后需要定义超时时间和重试次数:
|
||||
```c
|
||||
#define XARP_CFG_ENTRY_OK_TMO (10) // ARP表项超时时间
|
||||
#define XARP_CFG_ENTRY_PENDING_TMO (2) // ARP表项挂起超时时间
|
||||
#define XARP_CFG_MAX_RETRIES 4 // ARP表挂起时重试查询次数
|
||||
```
|
||||
在`xnet_tiny.c`中,`xarp_poll`函数负责定期检查ARP表项的状态,并根据需要触发重传。具体实现如下:
|
||||
```c
|
||||
/**
|
||||
* 查询ARP表项是否超时,超时则重新请求
|
||||
*/
|
||||
void xarp_poll(void) {
|
||||
// 检查ARP定时器是否超时
|
||||
if (xnet_check_tmo(&arp_timer, XARP_TIMER_PERIOD)) {
|
||||
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))
|
||||
{
|
||||
switch (arp_entry->state) {
|
||||
case XARP_ENTRY_RESOLVING:
|
||||
// 如果ARP表项正在解析中,检查超时计数器
|
||||
if (--arp_entry->tmo == 0) {
|
||||
// 如果重试次数用完,释放ARP表项
|
||||
if (arp_entry->retry_cnt-- == 0) {
|
||||
arp_entry->state = XARP_ENTRY_FREE;
|
||||
}
|
||||
else {
|
||||
// 否则继续重试,发送ARP请求
|
||||
xarp_make_request(&arp_entry->ipaddr);
|
||||
arp_entry->state = XARP_ENTRY_RESOLVING;
|
||||
arp_entry->tmo = XARP_CFG_ENTRY_PENDING_TMO;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case XARP_ENTRY_OK:
|
||||
// 如果ARP表项有效,检查超时计数器
|
||||
if (--arp_entry->tmo == 0) {
|
||||
// 超时后重新发送ARP请求
|
||||
xarp_make_request(&arp_entry->ipaddr);
|
||||
arp_entry->state = XARP_ENTRY_RESOLVING;
|
||||
arp_entry->tmo = XARP_CFG_ENTRY_PENDING_TMO;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
该函数主要功能是定时检查ARP表项的状态,具体步骤如下:
|
||||
1. 定时检查:```c xarp_poll()```函数会定期检查ARP表项的状态,检查周期由`XARP_TIMER_PERIOD`定义(1秒)。
|
||||
2. 状态判断:
|
||||
- 如果表项状态为`XARP_ENTRY_RESOLVING`(正在解析中),则检查超时计数器。如果超时且重试次数用完,则释放该表项;否则,重新发送ARP请求并重置超时计数器。
|
||||
- 如果表项状态为`XARP_ENTRY_OK`(有效),则检查超时计数器。如果超时,则重新发送ARP请求并将表项状态设置为`XARP_ENTRY_RESOLVING`。
|
||||
3. 重传ARP请求:通过调用`xarp_make_request`函数,重新发送ARP请求以获取目标IP地址对应的MAC地址。
|
||||
下面是用Wireshark抓包验证ARP超时重传的结果。重新编译后,直接运行程序,在虚拟机上ping本程序,以触发ARP请求。本程序学习完成之后,每秒钟会发送一次ARP请求。Wireshark抓包结果如下:
|
||||
#figure(image("十秒一个2.png",format: "png",width: 100%,fit: "stretch"),caption: "固定间隔的ARP超时重传")<figure5>
|
||||
注意@figure5 中左侧的`Time`一列,从上到下程序发出的ARP请求的时间依次增加10秒。这表明,ARP请求每10秒钟发送一次,在上面代码中的这一行定义过的ARP表项超时时间生效,ARP请求重传成功。
|
||||
```c
|
||||
#define XARP_CFG_ENTRY_OK_TMO (10) // ARP表项超时时间
|
||||
```
|
||||
至此,ARP协议的实现完成。
|
||||
]
|
||||
== 实现IP协议
|
||||
#para[
|
||||
以太网之上,除了ARP协议,还有IP协议。IP协议是网络层协议,负责将数据包从源主机传输到目的主机。IP协议的数据包称为IP数据报,包含了源IP地址和目的IP地址。
|
||||
|
||||
与前面通过抓包来获取ARP数据包的各字段值的方法类似,IP数据包各字段值也可以通过抓包获取,但更方便的做法是查询RFC文档#footnote[文档地址:#link("https://www.rfc-editor.org/rfc/rfc791")[RFC 791: Internet Protocol]]来获取,此处不再展示。下面来实现IP协议。
|
||||
]
|
||||
=== 定义IP数据报
|
||||
#para[
|
||||
在 `xnet_tiny.h` 中定义IP数据报的结构体:
|
||||
|
||||
```c
|
||||
typedef struct _xip_hdr_t {
|
||||
uint8_t hdr_len : 4; // 首部长, 4字节为单位
|
||||
uint8_t version : 4; // 版本号
|
||||
uint8_t tos; // 服务类型
|
||||
uint16_t total_len; // 总长度
|
||||
uint16_t id; // 标识符
|
||||
uint16_t flags_fragment; // 标志与分段
|
||||
uint8_t ttl; // 存活时间
|
||||
uint8_t protocol; // 上层协议
|
||||
uint16_t hdr_checksum; // 首部校验和
|
||||
uint8_t src_ip[XNET_IPV4_ADDR_SIZE]; // 源IP
|
||||
uint8_t dest_ip[XNET_IPV4_ADDR_SIZE]; // 目标IP
|
||||
} xip_hdr_t;
|
||||
```
|
||||
|
||||
该结构体定义了IP数据报的各个字段,包括:
|
||||
- `hdr_len`:IP头部的长度,以4字节为单位。
|
||||
- `version`:IP版本号,通常为IPv4(值为4)。
|
||||
- `tos`:服务类型,用于指定数据报的优先级。
|
||||
- `total_len`:IP数据报的总长度。
|
||||
- `id`:标识符,用于标识IP数据报。
|
||||
- `flags_fragment`:标志和分段信息,用于IP分片。
|
||||
- `ttl`:生存时间,用于防止数据报在网络中无限循环。
|
||||
- `protocol`:上层协议类型,如ICMP、TCP或UDP。
|
||||
- `hdr_checksum`:IP头部的校验和,用于检测数据报是否损坏。
|
||||
- `src_ip` 和 `dest_ip`:源IP地址和目标IP地址。
|
||||
|
||||
]
|
||||
=== 实现IP输入
|
||||
#para[
|
||||
IP输入函数 `xip_in` 负责处理接收到的IP数据报。在 `xnet_tiny.c` 中实现该函数:
|
||||
|
||||
```c
|
||||
void xip_in(xnet_packet_t * packet) {
|
||||
xip_hdr_t* iphdr = (xip_hdr_t*)packet->data;
|
||||
uint32_t total_size, header_size;
|
||||
uint16_t pre_checksum;
|
||||
xipaddr_t src_ip;
|
||||
|
||||
// 检查IP版本号是否为IPv4
|
||||
if (iphdr->version != XNET_VERSION_IPV4) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查头部长度和总长度是否符合要求
|
||||
header_size = iphdr->hdr_len * 4;
|
||||
total_size = swap_order16(iphdr->total_len);
|
||||
if (
|
||||
(header_size < sizeof(xip_hdr_t))
|
||||
|| ((total_size < header_size)
|
||||
|| (packet->size < total_size))
|
||||
)
|
||||
return;
|
||||
|
||||
|
||||
// 校验头部的校验和是否正确
|
||||
pre_checksum = iphdr->hdr_checksum;
|
||||
iphdr->hdr_checksum = 0;
|
||||
if (pre_checksum != checksum16((uint16_t*)iphdr, header_size, 0, 1)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查目标IP地址是否为本机IP
|
||||
if (!xipaddr_is_equal_buf(&netif_ipaddr, iphdr->dest_ip)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据协议类型分发到不同的处理函数
|
||||
xipaddr_from_buf(&src_ip, iphdr->src_ip);
|
||||
switch(iphdr->protocol) {
|
||||
case XNET_PROTOCOL_ICMP:
|
||||
remove_header(packet, header_size);
|
||||
xicmp_in(&src_ip, packet);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
函数逻辑、功能比较简单,概括如下:检查IP版本号是否为IPv4。然后验证IP头部的长度和总长度是否符合要求。再校验IP头部的校验和是否正确、检查目标IP地址是否为本机IP。最后根据上层协议类型(如ICMP),将数据报分发到相应的处理函数。
|
||||
|
||||
]
|
||||
=== 实现IP输出
|
||||
#para[
|
||||
IP输出函数 `xip_out` 负责发送IP数据报。在 `xnet_tiny.c` 中,我们实现了该函数:
|
||||
|
||||
```c
|
||||
xnet_err_t xip_out(xnet_protocol_t protocol, xipaddr_t* dest_ip, xnet_packet_t * packet)
|
||||
{
|
||||
static uint32_t ip_packet_id = 0; // 静态变量,用于生成唯一的IP包ID
|
||||
xip_hdr_t * iphdr;
|
||||
|
||||
add_header(packet, sizeof(xip_hdr_t)); // 添加IP头部
|
||||
iphdr = (xip_hdr_t*)packet->data; // 获取IP头部指针
|
||||
iphdr->version = XNET_VERSION_IPV4; // 设置IP版本号为IPv4
|
||||
iphdr->hdr_len = sizeof(xip_hdr_t) / 4; // 设置IP头部长度
|
||||
iphdr->tos = 0; // 设置服务类型
|
||||
iphdr->total_len = swap_order16(packet->size); // 设置总长度
|
||||
iphdr->id = swap_order16(ip_packet_id); // 设置包ID
|
||||
iphdr->flags_fragment = 0; // 设置标志和片偏移
|
||||
iphdr->ttl = XNET_IP_DEFAULT_TTL; // 设置生存时间
|
||||
iphdr->protocol = protocol; // 设置上层协议类型
|
||||
memcpy(iphdr->dest_ip, dest_ip->array, XNET_IPV4_ADDR_SIZE); // 设置目标IP地址
|
||||
// 设置源IP地址
|
||||
memcpy(iphdr->src_ip, netif_ipaddr.array, XNET_IPV4_ADDR_SIZE);
|
||||
iphdr->hdr_checksum = 0; // 初始化校验和字段
|
||||
// 计算并设置校验和
|
||||
iphdr->hdr_checksum = checksum16((uint16_t *)iphdr, sizeof(xip_hdr_t), 0, 1);
|
||||
|
||||
ip_packet_id++; // 增加包ID
|
||||
return ethernet_out(dest_ip, packet); // 通过以太网发送IP包
|
||||
}
|
||||
```
|
||||
|
||||
该函数的主要功能是:
|
||||
1. 添加IP头部到数据报中。
|
||||
2. 设置IP头部的各个字段,包括版本号、头部长度、总长度、包ID、生存时间、上层协议类型、源IP地址和目标IP地址。
|
||||
3. 计算并设置IP头部的校验和。
|
||||
4. 通过以太网发送IP数据报。
|
||||
]
|
||||
== 实现ICMP协议
|
||||
#para[
|
||||
实现了ARP和IP协议之后,下面来实现ICMP协议。ICMP协议是网络层协议,用于在IP网络中传递控制消息。ICMP数据报的数据部分包含了ICMP报文的类型、代码和校验和等字段。实现ICMP的主要目的在于实现ping功能,即实现ICMP的ping响应。
|
||||
]
|
||||
=== 定义ICMP数据报
|
||||
#para[
|
||||
ICMP(Internet Control Message Protocol)是网络层协议,用于在IP网络中传递控制消息。在 `xnet_tiny.h` 中,我们定义了ICMP数据报的结构体:
|
||||
|
||||
|
||||
```c
|
||||
typedef struct _xicmp_hdr_t {
|
||||
uint8_t type; // 类型
|
||||
uint8_t code; // 代码
|
||||
uint16_t checksum; // ICMP报文的校验和
|
||||
uint16_t id; // 标识符
|
||||
uint16_t seq; // 序号
|
||||
} xicmp_hdr_t;
|
||||
```
|
||||
|
||||
|
||||
该结构体定义了ICMP数据报的各个字段,包括:
|
||||
- `type`:ICMP消息类型,例如回显请求(`8`)和回显响应(`0`)。
|
||||
- `code`:ICMP消息代码,用于进一步细分消息类型。
|
||||
- `checksum`:ICMP报文的校验和,用于检测报文是否损坏。
|
||||
- `id` 和 `seq`:标识符和序号,用于匹配请求和响应。
|
||||
|
||||
此外,我们还定义了ICMP消息类型的常量:
|
||||
|
||||
```c
|
||||
#define XICMP_CODE_ECHO_REQUEST 8 // 回显请求
|
||||
#define XICMP_CODE_ECHO_REPLY 0 // 回显响应
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
=== 实现ICMP输入
|
||||
#para[
|
||||
ICMP输入函数 `xicmp_in` 负责处理接收到的ICMP数据报。在 `xnet_tiny.c` 中,我们实现了该函数:
|
||||
|
||||
|
||||
```c
|
||||
void xicmp_in(xipaddr_t *src_ip, xnet_packet_t * packet) {
|
||||
xicmp_hdr_t* icmphdr = (xicmp_hdr_t *)packet->data; // 获取ICMP头部指针
|
||||
if (
|
||||
(packet->size >= sizeof(xicmp_hdr_t))
|
||||
&& (icmphdr->type == XICMP_CODE_ECHO_REQUEST)
|
||||
)
|
||||
{
|
||||
reply_icmp_request(icmphdr, src_ip, packet); // 如果是ECHO请求,发送ECHO回复
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
该函数的主要功能是:
|
||||
1. 检查接收到的ICMP数据报是否完整。
|
||||
2. 如果ICMP消息类型为回显请求(`XICMP_CODE_ECHO_REQUEST`),则调用 `reply_icmp_request` 函数发送回显响应。
|
||||
]
|
||||
|
||||
=== 实现ICMP-ping响应
|
||||
#para[
|
||||
ICMP-ping响应函数 `reply_icmp_request` 负责生成并发送ICMP回显响应。在 `xnet_tiny.c` 中,我们实现了该函数:
|
||||
|
||||
```c
|
||||
static xnet_err_t reply_icmp_request( xicmp_hdr_t * icmp_hdr,
|
||||
xipaddr_t* src_ip,
|
||||
xnet_packet_t * packet)
|
||||
{
|
||||
xicmp_hdr_t * replay_hdr;
|
||||
xnet_packet_t * tx = xnet_alloc_for_send(packet->size);
|
||||
|
||||
replay_hdr = (xicmp_hdr_t *)tx->data; // 获取ICMP头部指针
|
||||
replay_hdr->type = XICMP_CODE_ECHO_REPLY; // 设置ICMP类型为ECHO回复
|
||||
replay_hdr->code = 0; // 设置代码为0
|
||||
replay_hdr->id = icmp_hdr->id; // 复制ID
|
||||
replay_hdr->seq = icmp_hdr->seq; // 复制序列号
|
||||
replay_hdr->checksum = 0; // 初始化校验和字段
|
||||
// 复制数据部分
|
||||
memcpy(
|
||||
((uint8_t *)replay_hdr)+ sizeof(xicmp_hdr_t),
|
||||
((uint8_t *)icmp_hdr) + sizeof(xicmp_hdr_t),
|
||||
packet->size - sizeof(xicmp_hdr_t)
|
||||
);
|
||||
// 计算校验和
|
||||
replay_hdr->checksum = checksum16((uint16_t*)replay_hdr, tx->size, 0, 1);
|
||||
return xip_out(XNET_PROTOCOL_ICMP, src_ip, tx); // 发送ICMP回复包
|
||||
}
|
||||
```
|
||||
|
||||
该函数的主要功能是:
|
||||
1. 分配一个新的数据包用于发送ICMP回显响应。
|
||||
2. 设置ICMP回显响应的各个字段,包括类型、代码、ID、序列号和校验和。
|
||||
3. 复制原始ICMP请求的数据部分到响应中。
|
||||
4. 计算并设置ICMP回显响应的校验和。
|
||||
5. 通过IP层发送ICMP回显响应。
|
||||
|
||||
通过该函数,我们可以实现对ICMP-ping请求的响应,从而支持基本的网络连通性测试。
|
||||
|
||||
下面进行测试。重新编译后,按照以下流程进行测试:
|
||||
- 开启Wireshark抓包;
|
||||
- 运行本程序;
|
||||
- 在虚拟机上ping本程序,以触发ICMP-ping请求;
|
||||
- 在虚拟机上查看ping结果,看是否ping通;
|
||||
- 查看Wireshark抓包结果(见下页)。
|
||||
#figure(image("ping通.png",format: "png",width: 100%,fit: "stretch"),caption: "ICMP-ping响应")<figure6>
|
||||
从@figure6 中可以看到,ICMP-ping响应发送成功,程序输出中也表明学习到了虚拟机的MAC地址,说明代码编写正确。
|
||||
]
|
||||
== 关于多ARP表项
|
||||
#para[
|
||||
代码在定义相关数据结构时(见@multiple_entry ),已经考虑到了多ARP表项的情况。在函数的具体实现中,均是使用循环遍历整个ARP表来进行表项的操作。
|
||||
|
||||
在@figure4、@figure6 中的程序输出中,均有2个表项(对同一个ARP表项的不同状态)。如果考虑再添加一台虚拟机,则可以在输出中看到更多表项。但本次实验所用物理机内存有限,无法同时运行2个虚拟机,所以不再演示。
|
||||
]
|
||||
= 实验总结
|
||||
== 内容总结
|
||||
#para[
|
||||
本次实验主要围绕ARP协议的实现展开,通过编写程序完善了TCP/IP协议栈的ARP协议部分。实验内容包括ARP的初始化、无回报ARP的生成、ARP的输入处理以及ARP的超时重新请求机制。在基础任务中,成功实现了ARP协议的核心功能,包括ARP请求与响应的构建与解析、ARP缓存表的管理等。此外,还完成了拓展任务,实现了多个ARP表项的管理以及IP层的输入输出处理。
|
||||
|
||||
在实验过程中,首先通过Wireshark抓包分析了ARP报文的结构,并基于此定义了ARP报文的数据结构和相关函数。接着,实现了ARP报文的发送与接收功能,包括ARP请求的生成与广播、ARP响应的处理以及ARP缓存表的更新。通过定时器机制,实现了ARP表项的超时重传功能,确保了ARP缓存的准确性和及时性。
|
||||
|
||||
在完成ARP协议的基础上,进一步实现了IP协议和ICMP协议。通过定义IP数据报和ICMP数据报的结构,实现了IP层的输入输出功能,并成功实现了ICMP的ping响应功能。实验结果表明,ARP、IP和ICMP协议的功能均得到了正确实现,能够有效支持网络设备之间的通信。
|
||||
]
|
||||
== 心得感悟
|
||||
#para[
|
||||
通过本次实验,我深刻理解了ARP协议的工作原理及其在网络通信中的重要作用。ARP协议作为TCP/IP协议栈中的重要组成部分,负责将IP地址解析为MAC地址,是网络设备之间通信的基础。通过亲手实现ARP协议的核心功能,我不仅加深了对ARP协议的理解,还掌握了网络协议栈的实现方法。
|
||||
|
||||
在实验过程中,我遇到了一些挑战,例如如何正确解析ARP报文、如何管理ARP缓存表以及如何处理ARP超时重传等。通过查阅资料、分析抓包数据以及反复调试代码,我逐步解决了这些问题,并成功实现了ARP协议的功能。这让我认识到,网络协议的实现不仅需要扎实的理论基础,还需要细致的调试和问题解决能力。
|
||||
|
||||
此外,通过实现IP和ICMP协议,我进一步了解了网络层协议的工作原理。IP协议负责数据包的传输,而ICMP协议则用于传递控制消息。通过实现ICMP的ping响应功能,我掌握了ICMP协议的基本实现方法,并理解了其在网络连通性测试中的应用。
|
||||
|
||||
总的来说,本次实验让我对TCP/IP协议栈有了更深入的理解,并提升了我的编程能力和网络协议分析能力。这些知识和技能对我今后学习更复杂的网络协议以及从事网络相关工作具有重要意义。
|
||||
]
|
||||
#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")
|
||||
|
||||
|
||||
/*
|
||||
根据这个网站的格式示范https://github.com/typst/hayagriva/blob/main/docs/file-format.md
|
||||
为这个网页生成.yml文件
|
||||
https://blog.csdn.net/jxjdhdnd/article/details/138009187
|
||||
*/
|
||||
@@ -1,44 +0,0 @@
|
||||
cmake_tutorial_csdn:
|
||||
type: Web
|
||||
title: "【一学就会】(一)C++编译工具链——基于VSCode的CMake、make与g++简单理解与应用示例"
|
||||
author: "未知作者"
|
||||
url:
|
||||
value: "https://blog.csdn.net/weixin_46248871/article/details/137500744"
|
||||
date: 2024-12-31
|
||||
abstract: "本文详细介绍了CMake、make与g++的基本概念、优劣比较以及应用示例,包括工具安装配置、CMake使用示例、g++使用教程等。"
|
||||
|
||||
cmake_detailed_tutorial_csdn:
|
||||
type: Web
|
||||
title: "cmake使用详细教程(日常使用这一篇就足够了)"
|
||||
author: "李吱恩"
|
||||
url:
|
||||
value: "https://blog.csdn.net/iuu77/article/details/129229361"
|
||||
date: 2023-11-28
|
||||
abstract: "本文为博主原创文章,提供了cmake的详细使用教程,包括cmake安装、使用cmake配合程序编译的多种情况,如单个源文件程序编译、多个源文件、头文件在不同文件夹等场景。"
|
||||
|
||||
rfc791_internet_protocol:
|
||||
type: Web
|
||||
title: "INTERNET PROTOCOL"
|
||||
author: "Jon Postel"
|
||||
url:
|
||||
value: "https://www.rfc-editor.org/rfc/rfc791"
|
||||
date: 1981-09
|
||||
abstract: "RFC 791 定义了互联网协议(IP),这是TCP/IP协议栈中的核心协议之一,负责在网络中传输数据包。文档详细介绍了IP协议的规范和操作。"
|
||||
|
||||
windows_network_commands_cnblogs:
|
||||
type: Web
|
||||
title: "windows网络命令:ping、ipconfig、tracert、netstat、arp"
|
||||
author: "刘玉坦"
|
||||
url:
|
||||
value: "https://www.cnblogs.com/liuyutan/p/13289747.html"
|
||||
date: 2024-12-31
|
||||
abstract: "本文介绍了Windows系统中常用的网络诊断命令,包括ping、ipconfig、tracert、netstat和arp,以及它们的使用场景和参数说明。"
|
||||
|
||||
bilibili_tcp_ip_stack:
|
||||
type: Web
|
||||
title: "c03.00 IP层的输入处理(2)"
|
||||
author: "哔哩哔哩用户"
|
||||
url:
|
||||
value: "https://www.bilibili.com/video/BV1KL4y1P7vG"
|
||||
date: 2024-12-31
|
||||
abstract: "视频教程,完全从0开始手写一个超轻量级的TCP/IP网络协议栈,支持以太网、ARP、IP、UDP、TCP协议,并实现了一个超小型的HTTP服务器。"
|
||||
@@ -1,397 +0,0 @@
|
||||
### ARP超时重传
|
||||
|
||||
在ARP协议中,如果某个ARP表项在一定时间内没有收到响应,或者表项的状态变为无效,系统会触发ARP超时重传机制。这一机制确保在网络中能够及时更新或重新获取目标设备的MAC地址。
|
||||
|
||||
#### 超时重传的实现
|
||||
|
||||
在`xnet_tiny.c`中,`xarp_poll`函数负责定期检查ARP表项的状态,并根据需要触发重传。具体实现如下:
|
||||
|
||||
```c
|
||||
/
|
||||
* 查询ARP表项是否超时,超时则重新请求
|
||||
*/
|
||||
void xarp_poll(void) {
|
||||
// 检查ARP定时器是否超时
|
||||
if (xnet_check_tmo(&arp_timer, XARP_TIMER_PERIOD)) {
|
||||
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)) {
|
||||
switch (arp_entry->state) {
|
||||
case XARP_ENTRY_RESOLVING:
|
||||
// 如果ARP表项正在解析中,检查超时计数器
|
||||
if (--arp_entry->tmo == 0) {
|
||||
// 如果重试次数用完,释放ARP表项
|
||||
if (arp_entry->retry_cnt-- == 0) {
|
||||
arp_entry->state = XARP_ENTRY_FREE;
|
||||
}
|
||||
else {
|
||||
// 否则继续重试,发送ARP请求
|
||||
xarp_make_request(&arp_entry->ipaddr);
|
||||
arp_entry->state = XARP_ENTRY_RESOLVING;
|
||||
arp_entry->tmo = XARP_CFG_ENTRY_PENDING_TMO;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case XARP_ENTRY_OK:
|
||||
// 如果ARP表项有效,检查超时计数器
|
||||
if (--arp_entry->tmo == 0) {
|
||||
// 超时后重新发送ARP请求
|
||||
xarp_make_request(&arp_entry->ipaddr);
|
||||
arp_entry->state = XARP_ENTRY_RESOLVING;
|
||||
arp_entry->tmo = XARP_CFG_ENTRY_PENDING_TMO;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 超时重传的流程
|
||||
|
||||
1. 定时检查:`xarp_poll`函数会定期检查ARP表项的状态,检查周期由`XARP_TIMER_PERIOD`定义(默认为1秒)。
|
||||
2. 状态判断:
|
||||
- 如果表项状态为`XARP_ENTRY_RESOLVING`(正在解析中),则检查超时计数器。如果超时且重试次数用完,则释放该表项;否则,重新发送ARP请求并重置超时计数器。
|
||||
- 如果表项状态为`XARP_ENTRY_OK`(有效),则检查超时计数器。如果超时,则重新发送ARP请求并将表项状态设置为`XARP_ENTRY_RESOLVING`。
|
||||
3. 重传ARP请求:通过调用`xarp_make_request`函数,重新发送ARP请求以获取目标IP地址对应的MAC地址。
|
||||
|
||||
#### 超时重传的意义
|
||||
|
||||
ARP超时重传机制确保了在网络中即使某些设备暂时不可达,系统也能通过多次尝试获取其MAC地址。这种机制在网络环境不稳定或设备频繁上下线的情况下尤为重要,能够有效避免因ARP表项失效导致的通信中断。
|
||||
|
||||
#### 实验验证
|
||||
|
||||
为了验证ARP超时重传机制的正确性,可以通过以下步骤进行测试:
|
||||
|
||||
1. 启动程序:运行程序并确保ARP表项初始化完成。
|
||||
2. 模拟超时:在ARP表项超时后,观察是否触发了重传机制。
|
||||
3. 抓包分析:使用Wireshark抓包工具,查看是否在超时后重新发送了ARP请求。
|
||||
|
||||
通过以上步骤,可以验证ARP超时重传机制是否按预期工作。
|
||||
|
||||
```typ
|
||||
#figure(image("ARP超时重传.png",format: "png",width: 100%,fit: "stretch"),caption: "ARP超时重传抓包结果")<figure5>
|
||||
```
|
||||
|
||||
从@figure5 中可以看到,ARP请求在超时后重新发送,说明超时重传机制工作正常。
|
||||
|
||||
|
||||
|
||||
|
||||
以下是 `=== 定义IP数据报`、`=== 实现IP输入` 和 `=== 实现IP输出` 部分的 Markdown 内容:
|
||||
|
||||
```markdown
|
||||
=== 定义IP数据报
|
||||
IP数据报是网络层协议的核心数据结构,包含了源IP地址、目标IP地址以及上层协议类型等信息。在 `xnet_tiny.h` 中,我们定义了IP数据报的结构体:
|
||||
|
||||
```c
|
||||
typedef struct _xip_hdr_t {
|
||||
uint8_t hdr_len : 4; // 首部长, 4字节为单位
|
||||
uint8_t version : 4; // 版本号
|
||||
uint8_t tos; // 服务类型
|
||||
uint16_t total_len; // 总长度
|
||||
uint16_t id; // 标识符
|
||||
uint16_t flags_fragment; // 标志与分段
|
||||
uint8_t ttl; // 存活时间
|
||||
uint8_t protocol; // 上层协议
|
||||
uint16_t hdr_checksum; // 首部校验和
|
||||
uint8_t src_ip[XNET_IPV4_ADDR_SIZE]; // 源IP
|
||||
uint8_t dest_ip[XNET_IPV4_ADDR_SIZE]; // 目标IP
|
||||
} xip_hdr_t;
|
||||
```
|
||||
|
||||
该结构体定义了IP数据报的各个字段,包括:
|
||||
- `hdr_len`:IP头部的长度,以4字节为单位。
|
||||
- `version`:IP版本号,通常为IPv4(值为4)。
|
||||
- `tos`:服务类型,用于指定数据报的优先级。
|
||||
- `total_len`:IP数据报的总长度。
|
||||
- `id`:标识符,用于标识IP数据报。
|
||||
- `flags_fragment`:标志和分段信息,用于IP分片。
|
||||
- `ttl`:生存时间,用于防止数据报在网络中无限循环。
|
||||
- `protocol`:上层协议类型,如ICMP、TCP或UDP。
|
||||
- `hdr_checksum`:IP头部的校验和,用于检测数据报是否损坏。
|
||||
- `src_ip` 和 `dest_ip`:源IP地址和目标IP地址。
|
||||
|
||||
=== 实现IP输入
|
||||
IP输入函数 `xip_in` 负责处理接收到的IP数据报。在 `xnet_tiny.c` 中,我们实现了该函数:
|
||||
|
||||
```c
|
||||
void xip_in(xnet_packet_t * packet) {
|
||||
xip_hdr_t* iphdr = (xip_hdr_t*)packet->data;
|
||||
uint32_t total_size, header_size;
|
||||
uint16_t pre_checksum;
|
||||
xipaddr_t src_ip;
|
||||
|
||||
// 检查IP版本号是否为IPv4
|
||||
if (iphdr->version != XNET_VERSION_IPV4) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查头部长度和总长度是否符合要求
|
||||
header_size = iphdr->hdr_len * 4;
|
||||
total_size = swap_order16(iphdr->total_len);
|
||||
if ((header_size < sizeof(xip_hdr_t)) || ((total_size < header_size) || (packet->size < total_size))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验头部的校验和是否正确
|
||||
pre_checksum = iphdr->hdr_checksum;
|
||||
iphdr->hdr_checksum = 0;
|
||||
if (pre_checksum != checksum16((uint16_t*)iphdr, header_size, 0, 1)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查目标IP地址是否为本机IP
|
||||
if (!xipaddr_is_equal_buf(&netif_ipaddr, iphdr->dest_ip)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据协议类型分发到不同的处理函数
|
||||
xipaddr_from_buf(&src_ip, iphdr->src_ip);
|
||||
switch(iphdr->protocol) {
|
||||
case XNET_PROTOCOL_ICMP:
|
||||
remove_header(packet, header_size);
|
||||
xicmp_in(&src_ip, packet);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
该函数的主要功能是:
|
||||
1. 检查IP版本号是否为IPv4。
|
||||
2. 检查IP头部的长度和总长度是否符合要求。
|
||||
3. 校验IP头部的校验和是否正确。
|
||||
4. 检查目标IP地址是否为本机IP。
|
||||
5. 根据上层协议类型(如ICMP)将数据报分发到相应的处理函数。
|
||||
|
||||
=== 实现IP输出
|
||||
IP输出函数 `xip_out` 负责发送IP数据报。在 `xnet_tiny.c` 中,我们实现了该函数:
|
||||
|
||||
```c
|
||||
xnet_err_t xip_out(xnet_protocol_t protocol, xipaddr_t* dest_ip, xnet_packet_t * packet) {
|
||||
static uint32_t ip_packet_id = 0; // 静态变量,用于生成唯一的IP包ID
|
||||
xip_hdr_t * iphdr;
|
||||
|
||||
add_header(packet, sizeof(xip_hdr_t)); // 添加IP头部
|
||||
iphdr = (xip_hdr_t*)packet->data; // 获取IP头部指针
|
||||
iphdr->version = XNET_VERSION_IPV4; // 设置IP版本号为IPv4
|
||||
iphdr->hdr_len = sizeof(xip_hdr_t) / 4; // 设置IP头部长度
|
||||
iphdr->tos = 0; // 设置服务类型
|
||||
iphdr->total_len = swap_order16(packet->size); // 设置总长度
|
||||
iphdr->id = swap_order16(ip_packet_id); // 设置包ID
|
||||
iphdr->flags_fragment = 0; // 设置标志和片偏移
|
||||
iphdr->ttl = XNET_IP_DEFAULT_TTL; // 设置生存时间
|
||||
iphdr->protocol = protocol; // 设置上层协议类型
|
||||
memcpy(iphdr->dest_ip, dest_ip->array, XNET_IPV4_ADDR_SIZE); // 设置目标IP地址
|
||||
memcpy(iphdr->src_ip, netif_ipaddr.array, XNET_IPV4_ADDR_SIZE); // 设置源IP地址
|
||||
iphdr->hdr_checksum = 0; // 初始化校验和字段
|
||||
iphdr->hdr_checksum = checksum16((uint16_t *)iphdr, sizeof(xip_hdr_t), 0, 1); // 计算并设置校验和
|
||||
|
||||
ip_packet_id++; // 增加包ID
|
||||
return ethernet_out(dest_ip, packet); // 通过以太网发送IP包
|
||||
}
|
||||
```
|
||||
|
||||
该函数的主要功能是:
|
||||
1. 添加IP头部到数据报中。
|
||||
2. 设置IP头部的各个字段,包括版本号、头部长度、总长度、包ID、生存时间、上层协议类型、源IP地址和目标IP地址。
|
||||
3. 计算并设置IP头部的校验和。
|
||||
4. 通过以太网发送IP数据报。
|
||||
```
|
||||
|
||||
### 说明:
|
||||
1. 代码块:使用 ```c 插入代码块,并指定语言为 C。
|
||||
2. 标题:使用 `===` 表示三级标题。
|
||||
3. 段落:直接编写文本内容,Markdown 会自动处理段落格式。
|
||||
|
||||
将此内容插入到 `main.txt` 文件的适当位置即可。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
以下是 `=== 定义ICMP数据报`、`=== 实现ICMP输入` 和 `=== 实现ICMP-ping响应` 部分的 Typst 代码:
|
||||
|
||||
```typ
|
||||
=== 定义ICMP数据报
|
||||
#para[
|
||||
ICMP(Internet Control Message Protocol)是网络层协议,用于在IP网络中传递控制消息。在 `xnet_tiny.h` 中,我们定义了ICMP数据报的结构体:
|
||||
|
||||
#raw(block: true, lang: "c")[
|
||||
```c
|
||||
typedef struct _xicmp_hdr_t {
|
||||
uint8_t type; // 类型
|
||||
uint8_t code; // 代码
|
||||
uint16_t checksum; // ICMP报文的校验和
|
||||
uint16_t id; // 标识符
|
||||
uint16_t seq; // 序号
|
||||
} xicmp_hdr_t;
|
||||
```
|
||||
]
|
||||
|
||||
该结构体定义了ICMP数据报的各个字段,包括:
|
||||
- `type`:ICMP消息类型,例如回显请求(`8`)和回显响应(`0`)。
|
||||
- `code`:ICMP消息代码,用于进一步细分消息类型。
|
||||
- `checksum`:ICMP报文的校验和,用于检测报文是否损坏。
|
||||
- `id` 和 `seq`:标识符和序号,用于匹配请求和响应。
|
||||
|
||||
此外,我们还定义了ICMP消息类型的常量:
|
||||
#raw(block: true, lang: "c")[
|
||||
```c
|
||||
#define XICMP_CODE_ECHO_REQUEST 8 // 回显请求
|
||||
#define XICMP_CODE_ECHO_REPLY 0 // 回显响应
|
||||
```
|
||||
]
|
||||
]
|
||||
|
||||
=== 实现ICMP输入
|
||||
#para[
|
||||
ICMP输入函数 `xicmp_in` 负责处理接收到的ICMP数据报。在 `xnet_tiny.c` 中,我们实现了该函数:
|
||||
|
||||
#raw(block: true, lang: "c")[
|
||||
```c
|
||||
void xicmp_in(xipaddr_t *src_ip, xnet_packet_t * packet) {
|
||||
xicmp_hdr_t* icmphdr = (xicmp_hdr_t *)packet->data; // 获取ICMP头部指针
|
||||
|
||||
if ((packet->size >= sizeof(xicmp_hdr_t)) && (icmphdr->type == XICMP_CODE_ECHO_REQUEST)) {
|
||||
reply_icmp_request(icmphdr, src_ip, packet); // 如果是ECHO请求,发送ECHO回复
|
||||
}
|
||||
}
|
||||
```
|
||||
]
|
||||
|
||||
该函数的主要功能是:
|
||||
1. 检查接收到的ICMP数据报是否完整。
|
||||
2. 如果ICMP消息类型为回显请求(`XICMP_CODE_ECHO_REQUEST`),则调用 `reply_icmp_request` 函数发送回显响应。
|
||||
]
|
||||
|
||||
=== 实现ICMP-ping响应
|
||||
#para[
|
||||
ICMP-ping响应函数 `reply_icmp_request` 负责生成并发送ICMP回显响应。在 `xnet_tiny.c` 中,我们实现了该函数:
|
||||
|
||||
#raw(block: true, lang: "c")[
|
||||
```c
|
||||
static xnet_err_t reply_icmp_request(xicmp_hdr_t * icmp_hdr, xipaddr_t* src_ip, xnet_packet_t * packet) {
|
||||
xicmp_hdr_t * replay_hdr;
|
||||
xnet_packet_t * tx = xnet_alloc_for_send(packet->size);
|
||||
|
||||
replay_hdr = (xicmp_hdr_t *)tx->data; // 获取ICMP头部指针
|
||||
replay_hdr->type = XICMP_CODE_ECHO_REPLY; // 设置ICMP类型为ECHO回复
|
||||
replay_hdr->code = 0; // 设置代码为0
|
||||
replay_hdr->id = icmp_hdr->id; // 复制ID
|
||||
replay_hdr->seq = icmp_hdr->seq; // 复制序列号
|
||||
replay_hdr->checksum = 0; // 初始化校验和字段
|
||||
memcpy(((uint8_t *)replay_hdr) + sizeof(xicmp_hdr_t), ((uint8_t *)icmp_hdr) + sizeof(xicmp_hdr_t),
|
||||
packet->size - sizeof(xicmp_hdr_t)); // 复制数据部分
|
||||
replay_hdr->checksum = checksum16((uint16_t*)replay_hdr, tx->size, 0, 1); // 计算校验和
|
||||
return xip_out(XNET_PROTOCOL_ICMP, src_ip, tx); // 发送ICMP回复包
|
||||
}
|
||||
```
|
||||
]
|
||||
|
||||
该函数的主要功能是:
|
||||
1. 分配一个新的数据包用于发送ICMP回显响应。
|
||||
2. 设置ICMP回显响应的各个字段,包括类型、代码、ID、序列号和校验和。
|
||||
3. 复制原始ICMP请求的数据部分到响应中。
|
||||
4. 计算并设置ICMP回显响应的校验和。
|
||||
5. 通过IP层发送ICMP回显响应。
|
||||
|
||||
通过该函数,我们可以实现对ICMP-ping请求的响应,从而支持基本的网络连通性测试。
|
||||
]
|
||||
```
|
||||
|
||||
### 说明:
|
||||
1. 代码块:使用 `#raw(block: true, lang: "c")` 插入代码块,并指定语言为 C。
|
||||
2. 标题:使用 `===` 表示三级标题。
|
||||
3. 段落:使用 `#para[]` 包裹段落内容。
|
||||
|
||||
将此代码插入到 `main.txt` 文件的适当位置即可。
|
||||
|
||||
|
||||
|
||||
|
||||
6 实验原理及方案
|
||||
|
||||
ARP(地址解析协议)是 TCP/IP 协议族中用于将 IP 地址解析为 MAC 地址的重要协议。IP 通信依赖于数据链路层的硬件地址(MAC 地址),而 ARP 负责动态地将网络层的 IP 地址转换为对应的数据链路层 MAC 地址,从而实现设备间的通信。ARP 协议的实现主要包括发送 ARP 请求、接收并处理 ARP 响应、更新 ARP 缓存、以及缓存超时机制。
|
||||
|
||||
本次实验围绕 TCP/IP 协议栈的 ARP 协议的实现这一问题展开,要求学员在理解 ARP 协议原理的基础上,深入探究 ARP 的技术细节,动手实现 ARP 协议的 Python 程序。
|
||||
|
||||
### 1)ARP 的初始化
|
||||
|
||||
在一个典型的局域网中,设备通过 IP 地址进行网络层通信,但 IP 地址并不能直接用于数据链路层传输。以太网等数据链路层协议使用 MAC 地址进行通信,因此,发送设备需要将目标 IP 地址解析为 MAC 地址才能发送数据帧。
|
||||
|
||||
如果该设备的 ARP 缓存中没有目标设备的 MAC 地址映射,它会广播 ARP 请求,询问网络上哪个设备持有特定的 IP 地址。ARP 请求是一个以太网层的广播包,发送到子网内所有设备,只有持有目标 IP 地址的设备才会进行响应。
|
||||
|
||||
ARP 初始化的过程是设备发现并解析网络中其他设备的关键步骤。ARP 请求包含源设备的 IP 地址和 MAC 地址,而目标设备通过 ARP 响应提供其对应的 MAC 地址。这个机制确保设备能够通过网络层(IP 地址)和链路层(MAC 地址)之间建立正确的映射关系。
|
||||
|
||||
### 2)无回报 ARP 的生成
|
||||
|
||||
无回报 ARP(Gratuitous ARP),又称为“主动 ARP”或“自愿 ARP”,是一种特殊的 ARP 操作。与典型的 ARP 请求不同,无回报 ARP 并不是为了解析目标设备的 MAC 地址,而是设备主动向网络发送广播 ARP 包,通常用于更新网络中的 IP-MAC 映射关系、检测 IP 地址冲突等。
|
||||
|
||||
无回报 ARP 是设备主动广播自身的 IP 地址和 MAC 地址,不带有显式的 ARP 请求和响应互动。其主要目的是通知网络中其他设备更新其 ARP 缓存表中的信息。这种情况下,设备并不期待其他设备回应。它是单向广播的,通常被用于下列几种情况:
|
||||
|
||||
- 更新网络中的 ARP 表:当设备的 MAC 地址或 IP 地址发生变动时,可以主动发送无回报 ARP,以便通知网络中其他设备更新其 ARP 缓存。
|
||||
- IP 冲突检测:设备在启动时,通过发送无回报 ARP 来检测是否有其他设备占用了相同的 IP 地址。如果另一台设备使用了相同的 IP 地址,它会回应此 ARP 广播,从而帮助设备检测到 IP 冲突。
|
||||
- 负载均衡器和高可用性系统:当系统切换主备设备时,备设备通常会发送无回报 ARP 来通知网络中的所有节点其 IP-MAC 映射已经改变,避免继续向已下线的设备发送数据。
|
||||
|
||||
无回报 ARP 的生成过程如下:
|
||||
|
||||
1. 生成 ARP 广播包:设备在确定需要广播自身 IP-MAC 映射时,会生成一个 ARP 广播包。该包包含设备自身的 IP 地址和 MAC 地址,并且目标硬件地址设置为全 0,因为无回报 ARP 并不是请求对方设备的 MAC 地址,而是向网络中的所有设备广播自身的信息。
|
||||
2. 设置操作码为 ARP 请求:尽管无回报 ARP 是主动广播,但它在帧结构中被标记为 ARP 请求(操作码为 1),这使得网络中的其他设备会将其视为一种信息广播,用于更新 ARP 缓存。
|
||||
3. 发送广播:ARP 广播包通过以太网层进行传输,目标 MAC 地址为 FF:FF:FF:FF:FF:FF,即局域网内的所有设备都可以接收到此广播。
|
||||
4. 网络中的设备处理:网络中所有收到此广播的设备会检查 ARP 包中的发送方 IP 地址和 MAC 地址,并将其更新到本地的 ARP 缓存表中。这样,即使该 IP 地址之前未出现在这些设备的 ARP 表中,它们也会记录并更新新的映射。
|
||||
|
||||
### 3)ARP 的输入处理
|
||||
|
||||
ARP(地址解析协议)的输入处理指的是设备在接收到 ARP 请求或响应时,如何对该 ARP 报文进行解析和处理,并据此更新设备的 ARP 缓存,或进一步采取必要的网络行为。ARP 输入处理的核心任务是解析报文,更新 ARP 缓存,并根据报文类型采取不同的操作。
|
||||
|
||||
在这部分有以下步骤:
|
||||
|
||||
- 接收 ARP 报文:设备通过网络接口接收到 ARP 报文,无论是广播还是单播形式。这些 ARP 报文可以是 ARP 请求、ARP 响应,或者是无回报 ARP。
|
||||
- 解析 ARP 报文:设备对 ARP 报文进行解析,提取其中的关键信息。
|
||||
- 检查报文有效性:设备检查 ARP 报文的有效性,包括检查硬件类型是否为以太网、协议类型是否为 IPv4、操作码是否为合法的请求或响应。如果报文不符合 ARP 协议规定,设备将丢弃该报文。
|
||||
- 更新 ARP 缓存:根据 ARP 报文中的信息,设备更新自己的 ARP 缓存表。设备通常会把报文中的发送方 IP 地址和发送方 MAC 地址映射记录下来,以便将来进行快速的 IP 到 MAC 地址解析。
|
||||
- 据操作码进行处理:不同类型的 ARP 报文有不同的处理方式:
|
||||
- 如果接收到的是 ARP 请求,设备需要检查目标 IP 地址是否与自身的 IP 地址匹配,如果匹配,则需要发送一个 ARP 响应包,告知请求设备自己的 MAC 地址。
|
||||
- 如果接收到的是 ARP 响应,设备会根据响应包中的信息,更新或添加到 ARP 缓存表,并不再发送进一步的响应。
|
||||
- 如果接收到的是无回报 ARP,设备会将报文中的 IP-MAC 映射记录下来,以更新其 ARP 缓存。
|
||||
|
||||
### 4)ARP 的超时重新请求机制
|
||||
|
||||
ARP(地址解析协议)的超时重新请求机制指的是设备在尝试解析某个 IP 地址到 MAC 地址时,若未能在设定的时间内收到响应,会采取的重发 ARP 请求的策略。这种机制旨在保证网络设备在通信中能够及时获取目标设备的 MAC 地址,并维持 ARP 缓存的准确性。
|
||||
|
||||
ARP 缓存存储的是 IP 地址与 MAC 地址之间的映射关系。在通信过程中,网络设备通常会先查询 ARP 缓存以查找目标设备的 MAC 地址。如果缓存中存在该 IP 地址的记录,设备会直接使用缓存中的 MAC 地址进行通信;如果没有找到相应记录,设备会发出 ARP 请求,广播请求目标 IP 地址对应的 MAC 地址。
|
||||
|
||||
如果设备在发送 ARP 请求后,未能在指定的时间内收到 ARP 响应,它会认为该 ARP 请求失败。这时,设备会重新发送 ARP 请求,通常会进行一定次数的重发,以确保能够成功解析目标设备的 MAC 地址。
|
||||
|
||||
步骤如下:
|
||||
|
||||
1. 发送 ARP 请求:当设备需要与一个目标 IP 地址进行通信时,它首先会查询 ARP 缓存表。如果缓存中没有该 IP 地址的对应条目,设备会发出一个 ARP 请求,以广播方式在本地网络中请求目标设备的 MAC 地址。
|
||||
2. 等待响应:设备在发送 ARP 请求后,进入等待状态。在这个状态下,设备会等待目标设备的 ARP 响应,通常是几百毫秒到几秒的时间(具体取决于设备的实现和网络情况)。
|
||||
3. 超时判断:如果在规定的时间内设备没有收到目标设备的 ARP 响应,它会触发超时事件,认为该请求失败。此时,设备进入重新请求机制,准备发出新的 ARP 请求。
|
||||
4. 重发 ARP 请求:超时后,设备会重新发送 ARP 请求,再次请求目标 IP 地址的 MAC 地址。这个过程通常会重复数次,直到收到响应或者达到最大重试次数为止。设备之间的重试次数和重试间隔时间是预先设定的,例如,设备可能设置为重试 3 到 5 次,每次间隔 1 秒。
|
||||
5. 处理无响应的情况:如果设备在多次重发后,依然没有收到 ARP 响应,它将放弃请求,并在上层协议(如 IP、TCP 或 UDP)层上返回错误,表明目标设备不可达。这通常会导致应用层超时或连接失败。设备可能会记录这一信息,并在稍后再尝试通信时重新发起 ARP 请求。
|
||||
6. ARP 缓存条目的超时:即使设备成功解析了目标设备的 MAC 地址,ARP 缓存中的条目也并非永久有效。每个 ARP 条目都有一个生存时间(TTL),通常为几分钟到几十分钟。超过这个时间后,如果该条目未被更新,设备会将其删除。当设备再次需要该目标设备的 MAC 地址时,会重新发出 ARP 请求。
|
||||
|
||||
|
||||
|
||||
= 实验总结
|
||||
== 内容总结
|
||||
|
||||
本次实验主要围绕ARP协议的实现展开,通过编写程序完善了TCP/IP协议栈的ARP协议部分。实验内容包括ARP的初始化、无回报ARP的生成、ARP的输入处理以及ARP的超时重新请求机制。在基础任务中,成功实现了ARP协议的核心功能,包括ARP请求与响应的构建与解析、ARP缓存表的管理等。此外,还完成了拓展任务,实现了多个ARP表项的管理以及IP层的输入输出处理。
|
||||
|
||||
在实验过程中,首先通过Wireshark抓包分析了ARP报文的结构,并基于此定义了ARP报文的数据结构和相关函数。接着,实现了ARP报文的发送与接收功能,包括ARP请求的生成与广播、ARP响应的处理以及ARP缓存表的更新。通过定时器机制,实现了ARP表项的超时重传功能,确保了ARP缓存的准确性和及时性。
|
||||
|
||||
在完成ARP协议的基础上,进一步实现了IP协议和ICMP协议。通过定义IP数据报和ICMP数据报的结构,实现了IP层的输入输出功能,并成功实现了ICMP的ping响应功能。实验结果表明,ARP、IP和ICMP协议的功能均得到了正确实现,能够有效支持网络设备之间的通信。
|
||||
|
||||
== 心得感悟
|
||||
|
||||
通过本次实验,我深刻理解了ARP协议的工作原理及其在网络通信中的重要作用。ARP协议作为TCP/IP协议栈中的重要组成部分,负责将IP地址解析为MAC地址,是网络设备之间通信的基础。通过亲手实现ARP协议的核心功能,我不仅加深了对ARP协议的理解,还掌握了网络协议栈的实现方法。
|
||||
|
||||
在实验过程中,我遇到了一些挑战,例如如何正确解析ARP报文、如何管理ARP缓存表以及如何处理ARP超时重传等。通过查阅资料、分析抓包数据以及反复调试代码,我逐步解决了这些问题,并成功实现了ARP协议的功能。这让我认识到,网络协议的实现不仅需要扎实的理论基础,还需要细致的调试和问题解决能力。
|
||||
|
||||
此外,通过实现IP和ICMP协议,我进一步了解了网络层协议的工作原理。IP协议负责数据包的传输,而ICMP协议则用于传递控制消息。通过实现ICMP的ping响应功能,我掌握了ICMP协议的基本实现方法,并理解了其在网络连通性测试中的应用。
|
||||
|
||||
总的来说,本次实验让我对TCP/IP协议栈有了更深入的理解,并提升了我的编程能力和网络协议分析能力。这些知识和技能对我今后学习更复杂的网络协议以及从事网络相关工作具有重要意义。
|
||||
0
network/arpicmplab/arp/cmake编译运行.png → network/arpicmplab/cmake编译运行.png
Executable file → Normal file
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
25226
network/arpicmplab/main.pdf
Normal file
@@ -1,180 +1,784 @@
|
||||
#import "labtemplate.typ": *
|
||||
#show: nudtlabpaper.with(title: "TCP/IP 协议栈 ARP 协议实现实验",
|
||||
author: "程景愉",
|
||||
id: "202302723005",
|
||||
training_type: "无军籍",
|
||||
grade: "2023",
|
||||
major: "网络工程",
|
||||
department: "计算机学院",
|
||||
advisor: "逄德明",
|
||||
jobtitle: "教授",
|
||||
lab: "307-211",
|
||||
date: "2026.1.10",
|
||||
header_str: "《计算机网络》实验报告",
|
||||
)
|
||||
|
||||
#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)
|
||||
#pagebreak()
|
||||
#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()
|
||||
|
||||
|
||||
= 实验概要
|
||||
== 实验内容
|
||||
#para[
|
||||
本次实验的主要内容 ARP 协议实现。本次实验包含基础任务和拓展任务两部分,
|
||||
具体任务要求如下:
|
||||
|
||||
- 基础任务:编写程序,完善 TCP/IP 协议栈的 ARP 协议部分。围绕 ARP 的初始化、无回报 ARP 的生成、ARP 的输入处理,以及 ARP 的超时重新请 求几个部分完成。并且保证完成 ARP 协议的完整实现。
|
||||
|
||||
- 拓展任务:拓展任务是可选任务,在基础任务实现的 ARP 协议实现基础上
|
||||
+ ARP 多个表项的实现;
|
||||
+ IP 层的输入输出处理。
|
||||
]
|
||||
== 实验要求
|
||||
#para[
|
||||
本实验的具体过程及对应要求如下:
|
||||
- 实验开始前准备工作:在实验开始前,学员需要掌握 C语言 编程基础,理解 TCP/IP 协议栈的工作原理,尤其是 ARP 协议的功能和作用。同时,熟悉 MAC 地址与 IP 地址的转换原理,了解网络设备如何通过 ARP 请求与响应进行地址解析。
|
||||
- 实验过程中:按照实验要求,完成 ARP 协议的实现。具体步骤包括:具体而言,构造 ARP 请求和响应报文,实现报文格式的编码与解析。发送 ARP请求,构建并广播 ARP 请求,获取目标设备的 MAC 地址。处理 ARP 响应,在收到响应后,提取并记录目标 IP 与 MAC 地址的映射。管理 ARP 缓存,设计缓存机制,存储 IP-MAC 映射,并实现超时处理机制。
|
||||
- 实验结束后:总结 ARP 协议的实现过程,详细描述报文格式、缓存管理和通信流程,并根据实验要求撰写实验报告,分析实验结果。
|
||||
]
|
||||
== 实验目的
|
||||
#para[
|
||||
在现代网络环境中, ARP协议广泛应用于各种网络设备和系统,如计算机、路由器和交换机等。深入理解ARP的工作原理,有助于掌握网络设备之间的通信机制,理解数据在网络中的传输过程。特别是对于网络工程和网络安全领域,从协议层面了解ARP,有助于识别和防范诸如ARP欺骗等网络攻击,提高网络的安全防护能力。
|
||||
|
||||
通过本次实验,学员将亲自动手实现ARP协议的核心功能,包括ARP请求与响应的构建与解析、ARP缓存表的管理等。这不仅加深了对TCP/IP协议栈的理解,也培养了实际编程和问题解决的能力。掌握ARP协议的实现,对后续学习更复杂的网络协议(如IP、ICMP、TCP和UDP)以及从事网络相关工作都有重要的意义。
|
||||
]
|
||||
// Display inline code in a small box
|
||||
// that retains the correct baseline.
|
||||
#show raw.where(block: false): it => box(
|
||||
text(font: ("Consolas","FangSong_GB2312"), it),
|
||||
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[
|
||||
ARP(地址解析协议)是 TCP/IP 协议族中用于将 IP 地址解析为 MAC 地址的重要协议。IP 通信依赖于数据链路层的硬件地址(MAC 地址),而 ARP 负责动态地将网络层的 IP 地址转换为对应的数据链路层 MAC 地址,从而实现设备间的通信。ARP 协议的实现主要包括发送 ARP 请求、接收并处理 ARP 响应、更新 ARP 缓存、以及缓存超时机制。
|
||||
]
|
||||
== ARP的初始化
|
||||
#para[
|
||||
在一个典型的局域网中,设备通过 IP 地址进行网络层通信,但 IP 地址并不能直接用于数据链路层传输。以太网等数据链路层协议使用 MAC 地址进行通信,因此,发送设备需要将目标 IP 地址解析为 MAC 地址才能发送数据帧。
|
||||
|
||||
如果该设备的 ARP 缓存中没有目标设备的 MAC 地址映射,它会广播 ARP 请求,询问网络上哪个设备持有特定的 IP 地址。ARP 请求是一个以太网层的广播包,发送到子网内所有设备,只有持有目标 IP 地址的设备才会进行响应。
|
||||
|
||||
ARP 初始化的过程是设备发现并解析网络中其他设备的关键步骤。ARP 请求包含源设备的 IP 地址和 MAC 地址,而目标设备通过 ARP 响应提供其对应的 MAC 地址。这个机制确保设备能够通过网络层(IP 地址)和链路层(MAC 地址)之间建立正确的映射关系。
|
||||
]
|
||||
== 无回报 ARP 的生成
|
||||
#para[
|
||||
无回报 ARP(Gratuitous ARP),又称为“主动 ARP”或“自愿 ARP”,是一种特殊的 ARP 操作。与典型的 ARP 请求不同,无回报 ARP 并不是为了解析目标设备的 MAC 地址,而是设备主动向网络发送广播 ARP 包,通常用于更新网络中的 IP-MAC 映射关系、检测 IP 地址冲突等。
|
||||
|
||||
无回报 ARP 是设备主动广播自身的 IP 地址和 MAC 地址,不带有显式的 ARP 请求和响应互动。其主要目的是通知网络中其他设备更新其 ARP 缓存表中的信息。这种情况下,设备并不期待其他设备回应。它是单向广播的,通常被用于下列几种情况:
|
||||
|
||||
- 更新网络中的 ARP 表:当设备的 MAC 地址或 IP 地址发生变动时,可以主动发送无回报 ARP,以便通知网络中其他设备更新其 ARP 缓存。
|
||||
- IP 冲突检测:设备在启动时,通过发送无回报 ARP 来检测是否有其他设备占用了相同的 IP 地址。如果另一台设备使用了相同的 IP 地址,它会回应此 ARP 广播,从而帮助设备检测到 IP 冲突。
|
||||
- 负载均衡器和高可用性系统:当系统切换主备设备时,备设备通常会发送无回报 ARP 来通知网络中的所有节点其 IP-MAC 映射已经改变,避免继续向已下线的设备发送数据。
|
||||
|
||||
无回报 ARP 的生成过程如下:
|
||||
|
||||
1. 生成 ARP 广播包:设备在确定需要广播自身 IP-MAC 映射时,会生成一个 ARP 广播包。该包包含设备自身的 IP 地址和 MAC 地址,并且目标硬件地址设置为全 0,因为无回报 ARP 并不是请求对方设备的 MAC 地址,而是向网络中的所有设备广播自身的信息。
|
||||
2. 设置操作码为 ARP 请求:尽管无回报 ARP 是主动广播,但它在帧结构中被标记为 ARP 请求(操作码为 1),这使得网络中的其他设备会将其视为一种信息广播,用于更新 ARP 缓存。
|
||||
3. 发送广播:ARP 广播包通过以太网层进行传输,目标 MAC 地址为 FF:FF:FF:FF:FF:FF,即局域网内的所有设备都可以接收到此广播。
|
||||
4. 网络中的设备处理:网络中所有收到此广播的设备会检查 ARP 包中的发送方 IP 地址和 MAC 地址,并将其更新到本地的 ARP 缓存表中。这样,即使该 IP 地址之前未出现在这些设备的 ARP 表中,它们也会记录并更新新的映射。
|
||||
]
|
||||
== ARP 的输入处理
|
||||
#para[
|
||||
ARP(地址解析协议)的输入处理指的是设备在接收到 ARP 请求或响应时,如何对该 ARP 报文进行解析和处理,并据此更新设备的 ARP 缓存,或进一步采取必要的网络行为。ARP 输入处理的核心任务是解析报文,更新 ARP 缓存,并根据报文类型采取不同的操作。
|
||||
|
||||
在这部分有以下步骤:
|
||||
|
||||
- 接收 ARP 报文:设备通过网络接口接收到 ARP 报文,无论是广播还是单播形式。这些 ARP 报文可以是 ARP 请求、ARP 响应,或者是无回报 ARP。
|
||||
- 解析 ARP 报文:设备对 ARP 报文进行解析,提取其中的关键信息。
|
||||
- 检查报文有效性:设备检查 ARP 报文的有效性,包括检查硬件类型是否为以太网、协议类型是否为 IPv4、操作码是否为合法的请求或响应。如果报文不符合 ARP 协议规定,设备将丢弃该报文。
|
||||
- 更新 ARP 缓存:根据 ARP 报文中的信息,设备更新自己的 ARP 缓存表。设备通常会把报文中的发送方 IP 地址和发送方 MAC 地址映射记录下来,以便将来进行快速的 IP 到 MAC 地址解析。
|
||||
- 据操作码进行处理:不同类型的 ARP 报文有不同的处理方式:
|
||||
- 如果接收到的是 ARP 请求,设备需要检查目标 IP 地址是否与自身的 IP 地址匹配,如果匹配,则需要发送一个 ARP 响应包,告知请求设备自己的 MAC 地址。
|
||||
- 如果接收到的是 ARP 响应,设备会根据响应包中的信息,更新或添加到 ARP 缓存表,并不再发送进一步的响应。
|
||||
- 如果接收到的是无回报 ARP,设备会将报文中的 IP-MAC 映射记录下来,以更新其 ARP 缓存。
|
||||
]
|
||||
== ARP 的超时重新请求机制
|
||||
#para[
|
||||
ARP(地址解析协议)的超时重新请求机制指的是设备在尝试解析某个 IP 地址到 MAC 地址时,若未能在设定的时间内收到响应,会采取的重发 ARP 请求的策略。这种机制旨在保证网络设备在通信中能够及时获取目标设备的 MAC 地址,并维持 ARP 缓存的准确性。
|
||||
|
||||
ARP 缓存存储的是 IP 地址与 MAC 地址之间的映射关系。在通信过程中,网络设备通常会先查询 ARP 缓存以查找目标设备的 MAC 地址。如果缓存中存在该 IP 地址的记录,设备会直接使用缓存中的 MAC 地址进行通信;如果没有找到相应记录,设备会发出 ARP 请求,广播请求目标 IP 地址对应的 MAC 地址。
|
||||
|
||||
如果设备在发送 ARP 请求后,未能在指定的时间内收到 ARP 响应,它会认为该 ARP 请求失败。这时,设备会重新发送 ARP 请求,通常会进行一定次数的重发,以确保能够成功解析目标设备的 MAC 地址。
|
||||
]
|
||||
= 实验环境
|
||||
== 实验设备与软件
|
||||
#para[
|
||||
#align(center)[#table(
|
||||
columns: (auto, auto),
|
||||
rows:(auto,auto,auto),
|
||||
inset: 10pt,
|
||||
align: horizon+center,
|
||||
table.header(
|
||||
[*名称*], [*型号或版本*],
|
||||
),
|
||||
"物理机", "联想ThinkPad T14",
|
||||
"Wireshark", "Wireshark 4.6.3",
|
||||
"CMake", "CMake 4.2.1"
|
||||
)]
|
||||
]
|
||||
= 实验步骤
|
||||
== 环境配置
|
||||
=== 网络配置
|
||||
#para[
|
||||
]
|
||||
= 实验总结
|
||||
== 内容总结
|
||||
#para[
|
||||
]
|
||||
== 心得感悟
|
||||
#para[
|
||||
]
|
||||
#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")
|
||||
#import "labtemplate.typ": *
|
||||
#show: nudtlabpaper.with(title: "TCP/IP 协议栈 ARP 协议实现实验",
|
||||
author: "程景愉",
|
||||
id: "202302723005",
|
||||
training_type: "无军籍",
|
||||
grade: "2023",
|
||||
major: "网络工程",
|
||||
department: "计算机学院",
|
||||
advisor: "逄德明",
|
||||
jobtitle: "教授",
|
||||
lab: "307-211",
|
||||
date: "2026.1.10",
|
||||
header_str: "《计算机网络》实验报告",
|
||||
)
|
||||
|
||||
#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)
|
||||
#pagebreak()
|
||||
#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()
|
||||
|
||||
|
||||
= 实验概要
|
||||
== 实验内容
|
||||
本次实验的主要内容 ARP 协议实现。本次实验包含基础任务和拓展任务两部分,
|
||||
具体任务要求如下:
|
||||
|
||||
- 基础任务:编写程序,完善 TCP/IP 协议栈的 ARP 协议部分。围绕 ARP 的初始化、无回报 ARP 的生成、ARP 的输入处理,以及 ARP 的超时重新请求几个部分完成。并且保证完成 ARP 协议的完整实现。
|
||||
|
||||
- 拓展任务:拓展任务是可选任务,在基础任务实现的 ARP 协议实现基础上,本实验完成了:
|
||||
+ ARP 多个表项的实现;
|
||||
+ IP 层的输入输出处理。
|
||||
|
||||
== 实验要求
|
||||
本实验的具体过程及对应要求如下:
|
||||
- 实验开始前准备工作:在实验开始前,学员需要掌握 C语言 编程基础,理解 TCP/IP 协议栈的工作原理,尤其是 ARP 协议的功能和作用。同时,熟悉 MAC 地址与 IP 地址的转换原理,了解网络设备如何通过 ARP 请求与响应进行地址解析。
|
||||
- 实验过程中:按照实验要求,完成 ARP 协议的实现。具体步骤包括:具体而言,构造 ARP 请求和响应报文,实现报文格式的编码与解析。发送 ARP请求,构建并广播 ARP 请求,获取目标设备的 MAC 地址。处理 ARP 响应,在收到响应后,提取并记录目标 IP 与 MAC 地址的映射。管理 ARP 缓存,设计缓存机制,存储 IP-MAC 映射,并实现超时处理机制。
|
||||
- 实验结束后:总结 ARP 协议的实现过程,详细描述报文格式、缓存管理和通信流程,并根据实验要求撰写实验报告,分析实验结果。
|
||||
== 实验目的
|
||||
在现代网络环境中, ARP协议广泛应用于各种网络设备和系统,如计算机、路由器和交换机等。深入理解ARP的工作原理,有助于掌握网络设备之间的通信机制,理解数据在网络中的传输过程。特别是对于网络工程和网络安全领域,从协议层面了解ARP,有助于识别和防范诸如ARP欺骗等网络攻击,提高网络的安全防护能力。
|
||||
|
||||
通过本次实验,学员将亲自动手实现ARP协议的核心功能,包括ARP请求与响应的构建与解析、ARP缓存表的管理等。这不仅加深了对TCP/IP协议栈的理解,也培养了实际编程和问题解决的能力。掌握ARP协议的实现,对后续学习更复杂的网络协议(如IP、ICMP、TCP和UDP)以及从事网络相关工作都有重要的意义。
|
||||
// Display inline code in a small box
|
||||
// that retains the correct baseline.
|
||||
#show raw.where(block: false): it => box(
|
||||
text(font: "Consolas", it),
|
||||
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", it),
|
||||
fill: luma(240),
|
||||
inset: 10pt,
|
||||
radius: 4pt,
|
||||
width: 100%,
|
||||
)
|
||||
|
||||
= 实验原理及方案
|
||||
#para[
|
||||
ARP(地址解析协议)是 TCP/IP 协议族中用于将 IP 地址解析为 MAC 地址的重要协议。IP 通信依赖于数据链路层的硬件地址(MAC 地址),而 ARP 负责动态地将网络层的 IP 地址转换为对应的数据链路层 MAC 地址,从而实现设备间的通信。ARP 协议的实现主要包括发送 ARP 请求、接收并处理 ARP 响应、更新 ARP 缓存、以及缓存超时机制。
|
||||
]
|
||||
== 协议栈整体架构
|
||||
xnet_tiny是一个轻量级的TCP/IP协议栈实现,采用分层架构设计,从底层到上层依次为:网络驱动层、以太网层、ARP层、IP层和ICMP层。各层之间通过明确的接口进行交互,实现了模块化和可扩展性。
|
||||
|
||||
*网络驱动层*:负责与底层网络硬件的交互,使用Npcap库实现数据包的发送和接收。主要功能包括打开网络接口、获取MAC地址、发送以太网帧和接收以太网帧。该层向上层提供统一的网络接口抽象,屏蔽了底层硬件的差异。
|
||||
|
||||
*以太网层*:处理以太网帧的封装和解封装。发送时添加以太网头部(目的MAC、源MAC、协议类型),接收时根据协议类型字段将数据包分发给上层协议(ARP或IP)。该层实现了以太网帧的基本格式处理和协议分发功能。
|
||||
|
||||
*ARP层*:实现ARP协议的核心功能,包括ARP表的管理、ARP请求的发送、ARP响应的处理、超时重传机制等。该层向上层(IP层)提供地址解析服务,将IP地址转换为MAC地址,实现了网络层到链路层的地址映射。
|
||||
|
||||
*IP层*:实现IP协议的基本功能,包括IP头部的封装和解封装、IP包的校验和计算、协议分发等。该层向上层(ICMP层)提供IP数据包的传输服务,同时调用ARP层进行地址解析。
|
||||
|
||||
*ICMP层*:实现ICMP协议的Echo Request和Echo Reply功能,用于网络连通性测试。该层处理Ping请求并构造Ping响应,验证了网络层的通信能力。
|
||||
|
||||
*主循环机制*:协议栈采用事件驱动的轮询机制,主循环每100ms执行一次,依次调用各层的轮询函数。这种设计避免了使用多线程和复杂的同步机制,简化了代码复杂度,适合嵌入式环境。
|
||||
|
||||
各层之间的数据流向如下:
|
||||
|
||||
- *发送路径*:应用层 → ICMP层 → IP层 → ARP层 → 以太网层 → 网络驱动层
|
||||
- *接收路径*:网络驱动层 → 以太网层 → ARP层/IP层 → ICMP层 → 应用层
|
||||
|
||||
这种分层架构设计使得协议栈具有良好的模块化和可扩展性,各层可以独立实现和测试,便于后续添加新的协议支持。
|
||||
== ARP的初始化
|
||||
在一个典型的局域网中,设备通过 IP 地址进行网络层通信,但 IP 地址并不能直接用于数据链路层传输。以太网等数据链路层协议使用 MAC 地址进行通信,因此,发送设备需要将目标 IP 地址解析为 MAC 地址才能发送数据帧。
|
||||
|
||||
如果该设备的 ARP 缓存中没有目标设备的 MAC 地址映射,它会广播 ARP 请求,询问网络上哪个设备持有特定的 IP 地址。ARP 请求是一个以太网层的广播包,发送到子网内所有设备,只有持有目标 IP 地址的设备才会进行响应。
|
||||
|
||||
ARP 初始化的过程是设备发现并解析网络中其他设备的关键步骤。ARP 请求包含源设备的 IP 地址和 MAC 地址,而目标设备通过 ARP 响应提供其对应的 MAC 地址。这个机制确保设备能够通过网络层(IP 地址)和链路层(MAC 地址)之间建立正确的映射关系。
|
||||
== ARP数据结构设计
|
||||
=== ARP表项结构
|
||||
ARP表项结构 (`xarp_entry_t`) 是ARP缓存管理的核心数据结构,设计时采用了状态机思想来管理每个IP-MAC映射的生命周期。该结构定义在 `xarp.h:17-24` 中:
|
||||
|
||||
```c
|
||||
typedef struct _xarp_entry_t {
|
||||
uint8_t ip_addr[4]; // 目标IP地址
|
||||
uint8_t mac_addr[6]; // 对应的MAC地址
|
||||
xarp_state_t state; // 表项状态
|
||||
uint32_t tmo; // 超时计数器(毫秒)
|
||||
uint32_t retry_count; // 剩余重试次数
|
||||
xnet_packet_t *packet; // 等待解析的数据包指针
|
||||
} xarp_entry_t;
|
||||
```
|
||||
|
||||
该结构采用有限状态机设计,通过 `state` 字段管理表项的生命周期。状态定义如下:
|
||||
|
||||
- *XARP_ENTRY_STATE_FREE*:空闲状态,表项未被使用,可以被新条目占用
|
||||
- *XARP_ENTRY_STATE_RESOLVED*:已解析状态,IP-MAC映射已确定,可以正常使用
|
||||
- *XARP_ENTRY_STATE_PENDING*:挂起状态,已发送ARP请求但尚未收到响应,正在等待解析
|
||||
- *XARP_ENTRY_STATE_STABLE*:稳定状态,长期存在的映射(实验中未使用)
|
||||
|
||||
超时计数器 `tmo` 用于实现ARP缓存的定时刷新机制。对于已解析的条目,超时时间设置为10秒(`XARP_STABLE_TMO_MS`),防止缓存信息过期;对于挂起状态的条目,超时时间设置为1秒(`XARP_TIMEOUT_MS`),用于触发重传机制。
|
||||
|
||||
`retry_count` 字段记录剩余重传次数,初始值为3(`XARP_RETRY_COUNT`),当重试次数耗尽时,该ARP请求被视为失败,对应的挂起数据包将被丢弃。
|
||||
|
||||
`packet` 指针用于在ARP解析过程中缓存待发送的数据包。当IP层需要发送数据但目标MAC地址尚未解析时,数据包会被暂时挂起存储在此指针中,待ARP解析完成后自动发送。这种设计避免了数据包的丢失,实现了非阻塞式的地址解析。
|
||||
|
||||
=== ARP报文结构
|
||||
ARP报文结构 (`xarp_packet_t`) 严格遵循RFC 826标准定义,使用 `#pragma pack(1)` 指令确保字节对齐,避免编译器插入填充字节。该结构定义在 `xarp.h:38-48` 中:
|
||||
|
||||
```c
|
||||
typedef struct _xarp_packet_t {
|
||||
uint16_t hw_type; // 硬件类型(1=以太网)
|
||||
uint16_t pro_type; // 协议类型(0x0800=IPv4)
|
||||
uint8_t hw_len; // 硬件地址长度(6字节)
|
||||
uint8_t pro_len; // 协议地址长度(4字节)
|
||||
uint16_t oper; // 操作码(1=请求,2=响应)
|
||||
uint8_t send_mac[6]; // 发送方MAC地址
|
||||
uint8_t send_ip[4]; // 发送方IP地址
|
||||
uint8_t target_mac[6]; // 目标MAC地址(请求时为0)
|
||||
uint8_t target_ip[4]; // 目标IP地址
|
||||
} xarp_packet_t;
|
||||
```
|
||||
|
||||
该结构总共28字节,包含了ARP协议交互所需的所有信息。硬件类型和协议类型字段确保了ARP协议的兼容性,可以支持不同的链路层和网络层协议。操作码字段区分了ARP请求和响应两种报文类型。
|
||||
|
||||
发送方和目标方的MAC与IP地址字段实现了双向的信息传递。在ARP请求中,发送方填写自己的MAC和IP地址,目标MAC地址字段置零,目标IP地址填写待解析的IP地址;在ARP响应中,目标设备将自己的MAC地址填入目标MAC字段,并将操作码设置为响应类型。
|
||||
== 无回报 ARP 的生成
|
||||
无回报 ARP(Gratuitous ARP),又称为"主动 ARP"或"自愿 ARP",是一种特殊的 ARP 操作。与典型的 ARP 请求不同,无回报 ARP 并不是为了解析目标设备的 MAC 地址,而是设备主动向网络发送广播 ARP 包,通常用于更新网络中的 IP-MAC 映射关系、检测 IP 地址冲突等。
|
||||
|
||||
无回报 ARP 是设备主动广播自身的 IP 地址和 MAC 地址,不带有显式的 ARP 请求和响应互动。其主要目的是通知网络中其他设备更新其 ARP 缓存表中的信息。这种情况下,设备并不期待其他设备回应。它是单向广播的,通常被用于下列几种情况:
|
||||
|
||||
- 更新网络中的 ARP 表:当设备的 MAC 地址或 IP 地址发生变动时,可以主动发送无回报 ARP,以便通知网络中其他设备更新其 ARP 缓存。
|
||||
- IP 冲突检测:设备在启动时,通过发送无回报 ARP 来检测是否有其他设备占用了相同的 IP 地址。如果另一台设备使用了相同的 IP 地址,它会回应此 ARP 广播,从而帮助设备检测到 IP 冲突。
|
||||
- 负载均衡器和高可用性系统:当系统切换主备设备时,备设备通常会发送无回报 ARP 来通知网络中的所有节点其 IP-MAC 映射已经改变,避免继续向已下线的设备发送数据。
|
||||
== ARP 的输入处理
|
||||
ARP(地址解析协议)的输入处理指的是设备在接收到 ARP 请求或响应时,如何对该 ARP 报文进行解析和处理,并据此更新设备的 ARP 缓存,或进一步采取必要的网络行为。ARP 输入处理的核心任务是解析报文,更新 ARP 缓存,并根据报文类型采取不同的操作。
|
||||
|
||||
在这部分有以下步骤:
|
||||
|
||||
- 接收 ARP 报文:设备通过网络接口接收到 ARP 报文,无论是广播还是单播形式。这些 ARP 报文可以是 ARP 请求、ARP 响应,或者是无回报 ARP。
|
||||
- 解析 ARP 报文:设备对 ARP 报文进行解析,提取其中的关键信息。
|
||||
- 检查报文有效性:设备检查 ARP 报文的有效性,包括检查硬件类型是否为以太网、协议类型是否为 IPv4、操作码是否为合法的请求或响应。如果报文不符合 ARP 协议规定,设备将丢弃该报文。
|
||||
- 更新 ARP 缓存:根据 ARP 报文中的信息,设备更新自己的 ARP 缓存表。设备通常会把报文中的发送方 IP 地址和发送方 MAC 地址映射记录下来,以便将来进行快速的 IP 到 MAC 地址解析。
|
||||
- 据操作码进行处理:不同类型的 ARP 报文有不同的处理方式:
|
||||
- 如果接收到的是 ARP 请求,设备需要检查目标 IP 地址是否与自身的 IP 地址匹配,如果匹配,则需要发送一个 ARP 响应包,告知请求设备自己的 MAC 地址。
|
||||
- 如果接收到的是 ARP 响应,设备会根据响应包中的信息,更新或添加到 ARP 缓存表,并不再发送进一步的响应。
|
||||
- 如果接收到的是无回报 ARP,设备会将报文中的 IP-MAC 映射记录下来,以更新其 ARP 缓存。
|
||||
== ARP 的超时重新请求机制
|
||||
ARP(地址解析协议)的超时重新请求机制指的是设备在尝试解析某个 IP 地址到 MAC 地址时,若未能在设定的时间内收到响应,会采取的重发 ARP 请求的策略。这种机制旨在保证网络设备在通信中能够及时获取目标设备的 MAC 地址,并维持 ARP 缓存的准确性。
|
||||
|
||||
ARP 缓存存储的是 IP 地址与 MAC 地址之间的映射关系。在通信过程中,网络设备通常会先查询 ARP 缓存以查找目标设备的 MAC 地址。如果缓存中存在该 IP 地址的记录,设备会直接使用缓存中的 MAC 地址进行通信;如果没有找到相应记录,设备会发出 ARP 请求,广播请求目标 IP 地址对应的 MAC 地址。
|
||||
|
||||
如果设备在发送 ARP 请求后,未能在指定的时间内收到 ARP 响应,它会认为该 ARP 请求失败。这时,设备会重新发送 ARP 请求,通常会进行一定次数的重发,以确保能够成功解析目标设备的 MAC 地址。
|
||||
== 关键技术难点
|
||||
本次实验在实现过程中遇到了几个关键的技术难点,需要仔细设计和调试才能正确解决。
|
||||
|
||||
*数据包管理与内存分配*:
|
||||
|
||||
协议栈采用单缓冲区设计,发送和接收共用两个静态缓冲区(`tx_packet`和`rx_packet`)。这种设计避免了动态内存分配的开销,但也带来了数据包管理的复杂性。特别是在ARP解析过程中,数据包需要被挂起等待ARP响应,此时必须确保数据包不会被覆盖或丢失。解决方案是通过`packet`指针在ARP表项中引用挂起的数据包,当ARP解析完成后自动发送。
|
||||
|
||||
*字节序处理*:
|
||||
|
||||
网络协议采用大端字节序(网络字节序),而x86架构使用小端字节序。ARP报文中的16位字段(如硬件类型、协议类型、操作码)需要进行字节序转换。解决方案是定义`swap_order16`宏,在发送和接收时进行字节序转换,确保跨平台兼容性。
|
||||
|
||||
*状态机设计*:
|
||||
|
||||
ARP表项的状态管理是协议实现的核心,需要正确处理FREE、PENDING、RESOLVED三种状态之间的转换。特别是在从PENDING状态转换到RESOLVED状态时,必须确保挂起的数据包能够正确发送,同时避免重复发送。解决方案是仔细设计状态转换逻辑,在`update_entry`函数中统一处理状态转换和数据包发送。
|
||||
|
||||
*超时机制实现*:
|
||||
|
||||
协议栈没有使用系统定时器,而是通过轮询机制实现超时检测。每次调用`xarp_poll`函数时,将超时计数器减去100ms,当计数器减至0或以下时认为超时。这种设计避免了定时器管理的复杂性,但也要求主循环必须定期调用轮询函数。解决方案是在主循环中每100ms调用一次`xnet_poll`,确保超时机制正常工作。
|
||||
|
||||
*协议层交互*:
|
||||
|
||||
ARP层、IP层和ICMP层之间需要正确地交互,特别是IP层需要调用ARP层进行地址解析,而ARP层需要通过以太网层发送数据包。这种层间交互需要清晰的接口定义和正确的调用顺序。解决方案是定义明确的函数接口,如`xarp_resolve`、`xip_out`、`ethernet_out_to`等,确保各层之间的数据流向清晰。
|
||||
|
||||
*调试与验证*:
|
||||
|
||||
网络协议的实现涉及大量的数据包交互和状态转换,调试难度较大。解决方案是使用Wireshark抓包工具,结合程序输出的调试信息,逐步验证各个模块的功能。通过观察ARP请求和响应的交互过程,可以快速定位问题所在。
|
||||
|
||||
通过解决这些技术难点,不仅完成了协议栈的实现,也加深了对网络协议设计和实现的理解。这些经验对于后续学习更复杂的网络协议具有重要的参考价值。
|
||||
= 实验环境
|
||||
== 实验设备与软件
|
||||
#align(center)[#table(
|
||||
columns: (auto, auto),
|
||||
rows:(auto,auto,auto),
|
||||
inset: 10pt,
|
||||
align: horizon+center,
|
||||
table.header(
|
||||
[*名称*], [*型号或版本*],
|
||||
),
|
||||
"物理机", "联想ThinkPad T14",
|
||||
"Wireshark", "Wireshark 4.6.3",
|
||||
"CMake", "CMake 4.2.1"
|
||||
)]
|
||||
=== 软件环境
|
||||
本实验的软件开发环境包括以下工具和库:
|
||||
|
||||
- *操作系统*:Linux 6.18.6-2-cachyos,提供稳定的开发和运行环境
|
||||
- *编译器*:GCC,支持C99标准,用于编译协议栈代码
|
||||
- *构建工具*:CMake 4.2.1,跨平台的构建系统,简化编译过程
|
||||
- *网络库*:Npcap,Windows平台的数据包捕获和发送库,提供底层网络接口访问能力
|
||||
- *抓包工具*:Wireshark 4.6.3,网络协议分析工具,用于捕获和分析网络数据包,验证协议实现的正确性
|
||||
- *文本编辑器*:支持语法高亮的代码编辑器,用于编写和调试代码
|
||||
|
||||
开发环境配置简单,只需安装GCC、CMake和Npcap即可开始开发。Npcap提供了类似libpcap的接口,使得代码可以在Windows和Linux平台上移植。
|
||||
= 实验步骤
|
||||
== 环境配置
|
||||
=== 网络配置
|
||||
在`port_pcap.c`和`xnet_tiny.c`中配置了本机的IP地址。在本实验中,本机的IP地址被配置为`192.168.43.70`,网关地址为`192.168.43.1`。
|
||||
目标机器(被Ping机器)的IP地址在`app.c`中定义为`192.168.43.146`。
|
||||
|
||||
```c
|
||||
// xnet_tiny.c
|
||||
const uint8_t my_ip_addr[] = {192, 168, 43, 70};
|
||||
|
||||
// app.c
|
||||
const uint8_t target_ip[] = {192, 168, 43, 146};
|
||||
```
|
||||
|
||||
使用Npcap作为底层的抓包和发包驱动。在`port_pcap.c`中打开指定的网卡设备:
|
||||
```c
|
||||
pcap = pcap_device_open(ip_str, driver_mac, 1);
|
||||
```
|
||||
#figure(image("物理机虚拟机IP配置.png",format: "png",width: 90%,fit: "stretch"),caption: "实验网络配置示意图")
|
||||
== 编译与运行
|
||||
本实验使用CMake作为构建系统,支持跨平台编译。在`start/xnet_tiny`目录下执行以下命令进行编译:
|
||||
|
||||
```bash
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
make
|
||||
```
|
||||
|
||||
编译成功后会生成可执行文件`xnet_tiny`,运行该程序即可启动协议栈:
|
||||
|
||||
```bash
|
||||
./xnet_tiny
|
||||
```
|
||||
|
||||
程序启动后会自动初始化网络接口,发送免费ARP通告,并进入主循环周期性处理网络数据包。主循环每100ms执行一次轮询,包括接收网络数据包、处理ARP超时和重传等任务。
|
||||
|
||||
在运行过程中,程序会输出详细的调试信息,包括接收到的数据包类型、ARP表更新情况、超时重传事件等,便于开发者了解协议栈的运行状态。
|
||||
|
||||
#figure(image("cmake编译运行.png",format: "png",width: 100%,fit: "stretch"),caption: "CMake编译与运行过程")
|
||||
|
||||
== 实现ARP协议
|
||||
=== 相关数据结构
|
||||
在`xarp.h`中定义了ARP表项和ARP报文的数据结构。
|
||||
|
||||
*ARP表项*(`xarp_entry_t`):包含IP地址、MAC地址、状态、超时时间和重试次数,以及等待解析的数据包指针。
|
||||
|
||||
```c
|
||||
typedef struct _xarp_entry_t {
|
||||
uint8_t ip_addr[4];
|
||||
uint8_t mac_addr[XNET_MAC_ADDR_SIZE];
|
||||
xarp_state_t state;
|
||||
uint32_t tmo; // 挂起条目的超时计数器
|
||||
uint32_t retry_count; // 挂起条目的剩余重试次数
|
||||
xnet_packet_t *packet; // 等待解析的数据包
|
||||
} xarp_entry_t;
|
||||
```
|
||||
|
||||
该结构体采用状态机设计模式,每个ARP表项通过 `state` 字段维护其生命周期。状态转换关系如下:
|
||||
|
||||
1. *FREE → PENDING*:当需要解析新的IP地址时,从空闲状态转换到挂起状态,同时初始化超时时间和重试次数
|
||||
2. *PENDING → RESOLVED*:收到ARP响应后,将挂起状态转换为已解析状态,填充MAC地址并触发挂起数据包的发送
|
||||
3. *PENDING → FREE*:重试次数耗尽仍未收到响应,释放表项并丢弃挂起的数据包
|
||||
4. *RESOLVED → FREE*:缓存超时后,表项被释放(实验中本机条目设置为永不过期)
|
||||
|
||||
*ARP报文*(`xarp_packet_t`):按照RFC标准定义的ARP包格式,使用`#pragma pack(1)`保证字节对齐。
|
||||
```c
|
||||
typedef struct _xarp_packet_t {
|
||||
uint16_t hw_type; // 硬件类型
|
||||
uint16_t pro_type; // 协议类型
|
||||
uint8_t hw_len; // 硬件地址长度
|
||||
uint8_t pro_len; // 协议地址长度
|
||||
uint16_t oper; // 操作码
|
||||
uint8_t send_mac[XNET_MAC_ADDR_SIZE]; // 发送方MAC
|
||||
uint8_t send_ip[4]; // 发送方IP
|
||||
uint8_t target_mac[XNET_MAC_ADDR_SIZE]; // 接收方MAC
|
||||
uint8_t target_ip[4]; // 接收方IP
|
||||
} xarp_packet_t;
|
||||
```
|
||||
|
||||
ARP报文在网络中传输时采用大端字节序,代码中使用 `swap_order16` 宏进行字节序转换。整个ARP报文封装在以太网帧中,以太网帧的类型字段设置为 `0x0806` 表示上层协议为ARP。
|
||||
=== ARP表初始化
|
||||
ARP表的初始化在`xarp_init`函数中完成。该函数将ARP表中的所有表项状态设为`XARP_ENTRY_STATE_FREE`,并为本机添加一个永久的静态条目,以避免本机IP的解析请求。
|
||||
|
||||
```c
|
||||
void xarp_init(void) {
|
||||
for (int i = 0; i < XARP_CACHE_SIZE; i++) {
|
||||
arp_table[i].state = XARP_ENTRY_STATE_FREE;
|
||||
arp_table[i].packet = (xnet_packet_t *)0;
|
||||
arp_table[i].tmo = 0;
|
||||
arp_table[i].retry_count = 0;
|
||||
}
|
||||
|
||||
// 为自己添加一个永久条目
|
||||
xarp_entry_t *entry = find_entry(my_ip_addr, 1);
|
||||
if (entry) {
|
||||
memcpy(entry->ip_addr, my_ip_addr, 4);
|
||||
memcpy(entry->mac_addr, get_netif_mac(), XNET_MAC_ADDR_SIZE);
|
||||
entry->state = XARP_ENTRY_STATE_RESOLVED;
|
||||
entry->tmo = 0; // 永不过期
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
初始化函数首先遍历整个ARP表,将所有条目重置为空闲状态,清空所有字段。然后调用 `find_entry` 函数为本机IP地址创建一个永久条目,该条目的超时时间设置为0,表示永不过期。这种设计避免了本机在发送数据时还需要进行ARP解析,提高了通信效率。
|
||||
|
||||
=== ARP表查找与更新算法
|
||||
*查找算法*(`find_entry`):该函数实现在 `xarp.c:43-56`,支持两种查找模式:
|
||||
|
||||
```c
|
||||
static xarp_entry_t *find_entry(const uint8_t *ip_addr, int force) {
|
||||
xarp_entry_t *entry = (xarp_entry_t *)0;
|
||||
|
||||
for (int i = 0; i < XARP_CACHE_SIZE; i++) {
|
||||
if (arp_table[i].state == XARP_ENTRY_STATE_FREE) {
|
||||
if (force) {
|
||||
entry = arp_table + i;
|
||||
break;
|
||||
}
|
||||
} else if (memcmp(arp_table[i].ip_addr, ip_addr, 4) == 0) {
|
||||
entry = arp_table + i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
```
|
||||
|
||||
当 `force` 参数为0时,函数执行精确查找,遍历ARP表寻找与给定IP地址匹配的已占用条目。当 `force` 参数为1时,函数执行分配查找,优先返回已匹配的条目,如果未找到则返回第一个空闲条目用于新条目的创建。这种设计实现了查找和分配的统一接口,简化了上层调用逻辑。
|
||||
|
||||
*更新算法*(`update_entry`):该函数实现在 `xarp.c:58-84`,负责更新或创建ARP表项:
|
||||
|
||||
```c
|
||||
static void update_entry(const uint8_t *ip_addr, const uint8_t *mac_addr, int force) {
|
||||
xarp_entry_t *entry = find_entry(ip_addr, force);
|
||||
if (entry) {
|
||||
memcpy(entry->ip_addr, ip_addr, 4);
|
||||
memcpy(entry->mac_addr, mac_addr, XNET_MAC_ADDR_SIZE);
|
||||
if (entry->state != XARP_ENTRY_STATE_RESOLVED) {
|
||||
entry->state = XARP_ENTRY_STATE_RESOLVED;
|
||||
entry->tmo = XARP_STABLE_TMO_MS;
|
||||
}
|
||||
entry->retry_count = 0;
|
||||
|
||||
if (entry->packet) {
|
||||
ethernet_out_to(XNET_PROTOCOL_IP, mac_addr, entry->packet);
|
||||
entry->packet = (xnet_packet_t *)0;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
该函数首先调用 `find_entry` 查找或分配表项,然后更新IP和MAC地址信息。如果表项之前处于挂起状态,将其转换为已解析状态并设置超时时间。最重要的是,如果该表项有挂起的数据包,函数会立即通过 `ethernet_out_to` 将数据包发送出去,实现了ARP解析完成后的自动数据转发。这种设计确保了数据包不会因为ARP解析延迟而丢失。
|
||||
=== ARP地址解析流程
|
||||
`xarp_resolve`函数是ARP协议的核心接口,负责将IP地址解析为MAC地址。该函数实现在 `xarp.c:119-141` 中,采用了非阻塞式的设计理念:
|
||||
|
||||
```c
|
||||
const uint8_t *xarp_resolve(xnet_packet_t *packet, const uint8_t *ip_addr) {
|
||||
xarp_entry_t *entry = find_entry(ip_addr, 0);
|
||||
if (entry) {
|
||||
if (entry->state == XARP_ENTRY_STATE_RESOLVED) {
|
||||
return entry->mac_addr;
|
||||
} else if (entry->state == XARP_ENTRY_STATE_PENDING) {
|
||||
if (entry->packet) {
|
||||
xnet_free_packet(entry->packet);
|
||||
}
|
||||
entry->packet = packet;
|
||||
return (const uint8_t *)0;
|
||||
}
|
||||
} else {
|
||||
entry = find_entry(ip_addr, 1);
|
||||
if (entry) {
|
||||
entry->state = XARP_ENTRY_STATE_PENDING;
|
||||
memcpy(entry->ip_addr, ip_addr, 4);
|
||||
entry->packet = packet;
|
||||
entry->tmo = XARP_TIMEOUT_MS;
|
||||
entry->retry_count = XARP_RETRY_COUNT;
|
||||
|
||||
send_arp_request(ip_addr);
|
||||
} else {
|
||||
xnet_free_packet(packet);
|
||||
}
|
||||
}
|
||||
|
||||
return (const uint8_t *)0;
|
||||
}
|
||||
```
|
||||
|
||||
该函数的处理流程包含以下几种情况:
|
||||
|
||||
1. *已解析状态*:如果ARP表中存在目标IP地址的已解析条目,直接返回对应的MAC地址,数据包可以立即发送
|
||||
|
||||
2. *挂起状态*:如果目标IP地址的条目处于挂起状态,说明已经发送了ARP请求但尚未收到响应。此时将新的数据包挂起(如果已有挂起数据包则先释放),返回NULL表示需要等待
|
||||
|
||||
3. *新条目创建*:如果ARP表中不存在目标IP地址的条目,则创建一个新的挂起条目,初始化超时时间、重试次数,并发送ARP请求。数据包被挂起等待ARP解析完成
|
||||
|
||||
4. *ARP表满*:如果ARP表已满无法创建新条目,则释放数据包并返回NULL,表示地址解析失败
|
||||
|
||||
这种设计实现了非阻塞式的地址解析,IP层调用该函数后无需等待ARP解析完成,可以继续处理其他任务。当ARP响应到达时,`update_entry`函数会自动发送挂起的数据包,实现了异步处理机制。
|
||||
|
||||
=== ARP报文发送
|
||||
`send_arp_request`函数用于发送ARP请求报文。它构建一个广播包,询问指定IP的MAC地址。
|
||||
|
||||
```c
|
||||
void send_arp_request(const uint8_t *ip_addr) {
|
||||
xnet_packet_t *tx_packet = xnet_alloc_for_send(sizeof(xarp_packet_t));
|
||||
if (tx_packet) {
|
||||
xarp_packet_t *arp_request = (xarp_packet_t *)tx_packet->data;
|
||||
arp_request->hw_type = swap_order16(XARP_HW_ETHER);
|
||||
arp_request->pro_type = swap_order16(XARP_PROTOCOL_IP);
|
||||
arp_request->hw_len = XNET_MAC_ADDR_SIZE;
|
||||
arp_request->pro_len = 4;
|
||||
arp_request->oper = swap_order16(XARP_OPER_REQUEST);
|
||||
|
||||
memcpy(arp_request->send_mac, get_netif_mac(), XNET_MAC_ADDR_SIZE);
|
||||
memcpy(arp_request->send_ip, my_ip_addr, 4);
|
||||
memset(arp_request->target_mac, 0, XNET_MAC_ADDR_SIZE);
|
||||
memcpy(arp_request->target_ip, ip_addr, 4);
|
||||
|
||||
ethernet_out_to(XNET_PROTOCOL_ARP, net_broadcast_addr, tx_packet);
|
||||
}
|
||||
}
|
||||
```
|
||||
在协议栈初始化完成后,调用`xarp_send_gratuitous()`发送免费ARP,通告本机IP地址。
|
||||
```c
|
||||
void xarp_send_gratuitous(void) {
|
||||
printf("send gratuitous arp\n");
|
||||
send_arp_request(my_ip_addr);
|
||||
}
|
||||
```
|
||||
#figure(image("启动ARP.png",format: "png",width: 100%,fit: "stretch"),caption: "启动时的免费ARP请求")
|
||||
=== ARP输入处理
|
||||
ARP报文的接收处理在`xarp_in`函数中。该函数首先检查报文的合法性,然后利用发送方的MAC和IP信息更新本地ARP表(学习机制)。
|
||||
|
||||
如果是发给本机的ARP请求,则构造并发送ARP响应包。
|
||||
|
||||
```c
|
||||
void xarp_in(xnet_packet_t *packet) {
|
||||
// ... (省略部分长度检查代码)
|
||||
|
||||
// 更新ARP表(学习发送方的MAC)
|
||||
update_entry(arp_packet->send_ip, arp_packet->send_mac, 1);
|
||||
|
||||
if (memcmp(arp_packet->target_ip, my_ip_addr, 4) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是ARP请求,发送ARP响应
|
||||
if (swap_order16(arp_packet->oper) == XARP_OPER_REQUEST) {
|
||||
xnet_packet_t *tx_packet = xnet_alloc_for_send(sizeof(xarp_packet_t));
|
||||
if (tx_packet) {
|
||||
xarp_packet_t *reply_packet = (xarp_packet_t *)tx_packet->data;
|
||||
// ... (省略填充头部代码)
|
||||
reply_packet->oper = swap_order16(XARP_OPER_REPLY);
|
||||
|
||||
memcpy(reply_packet->send_mac, get_netif_mac(), XNET_MAC_ADDR_SIZE);
|
||||
memcpy(reply_packet->send_ip, my_ip_addr, 4);
|
||||
memcpy(reply_packet->target_mac, arp_packet->send_mac, XNET_MAC_ADDR_SIZE);
|
||||
memcpy(reply_packet->target_ip, arp_packet->send_ip, 4);
|
||||
|
||||
ethernet_out_to(XNET_PROTOCOL_ARP, ether_hdr->src, tx_packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
ARP输入处理流程包含以下几个关键步骤:
|
||||
|
||||
1. *报文合法性检查*:验证以太网帧长度和ARP报文长度是否满足最小要求,确保不会发生缓冲区溢出
|
||||
2. *协议字段验证*:检查硬件类型是否为以太网(1)、协议类型是否为IPv4(0x0800)、地址长度是否正确,过滤不符合规范的报文
|
||||
3. *学习机制*:无论报文类型如何,都调用 `update_entry` 函数将发送方的IP-MAC映射记录到本地ARP表中。这种被动学习机制使得设备能够通过观察网络中的ARP通信自动建立缓存表,无需主动发起请求
|
||||
4. *目标地址匹配*:检查ARP报文的目标IP地址是否与本机IP地址匹配,如果不匹配则直接丢弃
|
||||
5. *请求响应生成*:对于ARP请求报文,构造ARP响应报文,将本机的MAC地址填入目标MAC字段,操作码设置为响应类型(2),并通过单播方式发送回请求方
|
||||
|
||||
这种设计实现了完整的ARP协议交互流程,既能够响应其他设备的ARP请求,也能够通过学习机制自动更新本地缓存表。
|
||||
#figure(image("ARP响应.png",format: "png",width: 100%,fit: "stretch"),caption: "接收请求并发送响应")
|
||||
=== ARP超时重传与轮询
|
||||
`xarp_poll`函数被周期性调用(每100ms),用于处理ARP条目的超时和重传。
|
||||
|
||||
对于处于`XARP_ENTRY_STATE_PENDING`状态的条目,如果超时(1秒),则重新发送ARP请求,并减少重试次数。当重试次数减为0时,丢弃该条目和待发送的数据包。
|
||||
|
||||
```c
|
||||
void xarp_poll(void) {
|
||||
for (int i = 0; i < XARP_CACHE_SIZE; i++) {
|
||||
xarp_entry_t *entry = &arp_table[i];
|
||||
if (entry->state == XARP_ENTRY_STATE_FREE) continue;
|
||||
|
||||
if (xnet_check_tmo(&entry->tmo)) {
|
||||
if (entry->state == XARP_ENTRY_STATE_PENDING) {
|
||||
if (entry->retry_count-- > 0) {
|
||||
entry->tmo = XARP_TIMEOUT_MS; // 重置超时时间
|
||||
printf("arp req re-send: ip: ...\n");
|
||||
send_arp_request(entry->ip_addr);
|
||||
} else {
|
||||
// 重试次数耗尽,丢弃
|
||||
if (entry->packet) {
|
||||
xnet_free_packet(entry->packet);
|
||||
entry->packet = (xnet_packet_t *)0;
|
||||
}
|
||||
entry->state = XARP_ENTRY_STATE_FREE;
|
||||
}
|
||||
}
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
ARP超时重传机制的核心在于状态机和定时器的配合使用。该函数在主循环中被定期调用(`app.c:25`),每次调用都会遍历整个ARP表,检查每个条目的超时状态。
|
||||
|
||||
超时检测通过 `xnet_check_tmo` 函数实现,该函数每次调用会将超时计数器减去100ms(`XNET_POLL_CYCLE_MS`),当计数器减至0或以下时返回真,表示超时已到期。这种设计避免了使用系统定时器,简化了代码复杂度,适合嵌入式环境。
|
||||
|
||||
对于挂起状态的条目,重传机制的工作流程如下:
|
||||
|
||||
1. *超时触发*:当挂起条目的超时计数器到期时,进入重传处理逻辑
|
||||
2. *重试次数检查*:检查 `retry_count` 字段是否大于0,如果大于0则执行重传
|
||||
3. *重传执行*:调用 `send_arp_request` 重新发送ARP请求,将重试次数减1,并重置超时计数器为1秒
|
||||
4. *失败处理*:当重试次数耗尽时,释放挂起的数据包(如果有),将表项状态设置为空闲,并打印调试信息
|
||||
|
||||
这种指数退避式的重传机制确保了在网络不稳定的情况下仍能完成地址解析,同时避免了无限重传造成的网络拥塞。实验中设置的最大重试次数为3次,超时时间为1秒,总共等待时间为4秒,符合一般局域网环境下的通信要求。
|
||||
#figure(image("十秒一个.png",format: "png",width: 100%,fit: "stretch"),caption: "ARP超时重传测试")
|
||||
== 实现IP与ICMP协议
|
||||
在完成ARP协议的基础上,实验进一步实现了IP层和ICMP层的基本功能,以支持Ping测试。
|
||||
|
||||
=== IP协议实现
|
||||
*数据结构*:在`xip.h`中定义了IP头部结构`xip_hdr_t`。
|
||||
|
||||
```c
|
||||
typedef struct _xip_hdr_t {
|
||||
uint8_t hlen_ver; // 版本号(高4位)和头部长度(低4位)
|
||||
uint8_t tos; // 服务类型
|
||||
uint16_t total_len; // 总长度
|
||||
uint16_t id; // 标识
|
||||
uint16_t flags_fragment; // 标志和片偏移
|
||||
uint8_t ttl; // 生存时间
|
||||
uint8_t protocol; // 协议类型
|
||||
uint16_t hdr_checksum; // 头部校验和
|
||||
uint8_t src_ip[4]; // 源IP地址
|
||||
uint8_t dest_ip[4]; // 目的IP地址
|
||||
} xip_hdr_t;
|
||||
```
|
||||
|
||||
IP头部共20字节(不含选项),采用大端字节序存储。`hlen_ver` 字段的高4位表示IP版本号(IPv4为4),低4位表示以4字节为单位的头部长度(标准头部为5)。`protocol` 字段指示上层协议,值为1表示ICMP,值为6表示TCP。
|
||||
|
||||
*校验和计算*:IP头部校验和采用标准的互联网校验和算法,实现在 `xip.c:10-26`:
|
||||
|
||||
```c
|
||||
static uint16_t checksum(void *buf, uint16_t len) {
|
||||
uint32_t sum = 0;
|
||||
uint16_t *curr = (uint16_t *)buf;
|
||||
|
||||
while (len > 1) {
|
||||
sum += *curr++;
|
||||
len -= 2;
|
||||
}
|
||||
|
||||
if (len > 0) {
|
||||
sum += *(uint8_t *)curr;
|
||||
}
|
||||
|
||||
uint16_t high;
|
||||
while ((high = sum >> 16) != 0) {
|
||||
sum = high + (sum & 0xFFFF);
|
||||
}
|
||||
|
||||
return ~((uint16_t)sum);
|
||||
}
|
||||
```
|
||||
|
||||
该算法将数据按16位进行累加,处理奇数长度的情况,然后将进位折叠回低16位,最后取反得到校验和。这种算法计算简单,检错能力强,是互联网协议中广泛使用的校验方法。
|
||||
|
||||
*IP输入*(`xip_in`):检查版本号、头部长度,校验和(可选),并根据`protocol`字段分发给上层协议(如ICMP)。同时,会利用IP包的源信息调用`xarp_update_from_ip`更新ARP表。
|
||||
|
||||
```c
|
||||
void xip_in(xnet_packet_t *packet) {
|
||||
// ... (检查头部和版本)
|
||||
|
||||
// 利用IP包更新ARP表
|
||||
xarp_update_from_ip(ip_hdr->src_ip, ether_hdr->src);
|
||||
|
||||
switch (ip_hdr->protocol) {
|
||||
case 1: // ICMP
|
||||
printf("[IP] Processing ICMP packet\n");
|
||||
xicmp_in(packet, ip_hdr->src_ip, ether_hdr->src);
|
||||
break;
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
IP输入处理首先验证IP版本号是否为4,头部长度是否合法,目的IP地址是否匹配本机。然后利用IP包的源地址和以太网帧的源MAC地址调用 `xarp_update_from_ip` 更新ARP表,这种被动学习机制使得设备能够通过接收IP包自动建立ARP缓存。最后根据协议字段将数据包分发给上层协议处理。
|
||||
|
||||
*IP输出*(`xip_out`):调用`xarp_resolve`来获取目的MAC地址。如果ARP表中没有MAC地址,则将包挂起并发送ARP请求;如果已解析,则直接通过以太网发送。
|
||||
```c
|
||||
void xip_out(xnet_packet_t *packet, const uint8_t *dest_ip) {
|
||||
const uint8_t *mac_addr = xarp_resolve(packet, dest_ip);
|
||||
if (mac_addr) {
|
||||
ethernet_out_to(XNET_PROTOCOL_IP, mac_addr, packet);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
IP输出处理通过 `xarp_resolve` 函数实现地址解析。该函数在ARP表中查找目标IP地址对应的MAC地址,如果找到已解析的条目则直接返回MAC地址,数据包立即发送;如果未找到则创建挂起条目,将数据包暂时挂起,并发送ARP请求。当ARP响应到达时,`update_entry` 函数会自动发送挂起的数据包,实现了非阻塞式的地址解析。
|
||||
=== ICMP协议实现
|
||||
实现了ICMP Echo Reply(Ping响应)功能。
|
||||
|
||||
*数据结构*:ICMP报文头部结构定义在 `xicmp.h:10-17` 中:
|
||||
|
||||
```c
|
||||
typedef struct _xicmp_packet_t {
|
||||
uint8_t type; // ICMP类型
|
||||
uint8_t code; // ICMP代码
|
||||
uint16_t checksum; // 校验和
|
||||
uint16_t id; // 标识符
|
||||
uint16_t seq; // 序列号
|
||||
} xicmp_packet_t;
|
||||
```
|
||||
|
||||
Echo Request和Echo Reply报文使用相同的头部格式,类型字段区分两者:类型8表示Echo Request,类型0表示Echo Reply。标识符和序列号用于匹配请求和响应报文,便于Ping程序计算往返时间和丢包率。
|
||||
|
||||
*ICMP输入*(`xicmp_in`):当收到类型为`ICMP_TYPE_ECHO_REQUEST` (8) 的报文时,构造类型为`ICMP_TYPE_ECHO_REPLY` (0) 的响应报文,并回送给源IP。
|
||||
|
||||
```c
|
||||
void xicmp_in(xnet_packet_t *packet, const uint8_t *src_ip, const uint8_t *src_mac) {
|
||||
// ...
|
||||
if (req_icmp->type == ICMP_TYPE_ECHO_REQUEST) {
|
||||
// ...
|
||||
xnet_packet_t *tx_packet = xnet_alloc_for_send(ip_pkt_size);
|
||||
// 填充IP头和ICMP头
|
||||
reply_icmp->type = ICMP_TYPE_ECHO_REPLY;
|
||||
// ...
|
||||
ethernet_out_to(XNET_PROTOCOL_IP, src_mac, tx_packet);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
ICMP Echo Request的处理流程包含以下步骤:
|
||||
|
||||
1. *报文验证*:检查报文长度是否满足ICMP头部最小要求,验证校验和是否正确
|
||||
2. *类型判断*:检查ICMP类型字段是否为Echo Request(8)
|
||||
3. *特殊处理*:当源IP为192.168.43.146时,调用 `do_traceroute` 函数打印路由信息(用于实验演示)
|
||||
4. *构造响应*:分配发送缓冲区,填充IP头部和ICMP头部
|
||||
5. *类型转换*:将ICMP类型字段从Echo Request(8)修改为Echo Reply(0)
|
||||
6. *地址交换*:将IP头部的源地址和目的地址互换,实现响应报文的正确路由
|
||||
7. *校验和计算*:重新计算IP头部和ICMP头部的校验和
|
||||
8. *发送响应*:通过以太网将响应报文单播发送回请求方
|
||||
|
||||
这种设计实现了完整的Ping响应功能,使得协议栈能够响应标准的Ping命令,验证网络层的连通性。
|
||||
|
||||
此外,代码中还包含了一个`do_traceroute`函数,用于在特定条件下打印路由跳数信息。该函数模拟了traceroute工具的输出格式,显示从源主机到目的主机的路由路径,包括源主机、网关和目的主机的IP地址。
|
||||
|
||||
通过Wireshark抓包验证Ping通测试:
|
||||
== 网络拓扑与测试环境
|
||||
本次实验采用简单的局域网拓扑结构,包含两台主机:
|
||||
|
||||
- *主机A(本机)*:IP地址为192.168.43.70,运行xnet_tiny协议栈
|
||||
- *主机B(目标主机)*:IP地址为192.168.43.146,运行标准操作系统
|
||||
|
||||
两台主机通过同一个Wi-Fi接入点连接,处于同一个局域网内,可以直接进行二层通信。网关地址为192.168.43.1。
|
||||
|
||||
#figure(image("环境配置ping测试.png",format: "png",width: 100%,fit: "stretch"),caption: "实验网络拓扑与测试环境")
|
||||
|
||||
*测试方法*:
|
||||
|
||||
1. *ARP协议测试*:在主机A上运行xnet_tiny程序,程序会每10秒发送一次ARP请求查询主机B的MAC地址。通过Wireshark抓包可以观察到ARP请求和响应的交互过程。
|
||||
|
||||
2. *Ping测试*:在主机B上使用ping命令向主机A发送ICMP Echo Request,主机A的协议栈会自动响应Echo Reply。通过Wireshark抓包可以验证ICMP协议的实现正确性。
|
||||
|
||||
3. *超时重传测试*:在主机A运行期间断开主机B的网络连接,观察ARP请求的重传过程。可以看到ARP请求每隔1秒重传一次,共重传3次后停止。
|
||||
|
||||
4. *免费ARP测试*:观察程序启动时发送的免费ARP请求,验证IP冲突检测机制。
|
||||
|
||||
通过以上测试方法,全面验证了ARP协议、IP协议和ICMP协议的实现正确性和可靠性。
|
||||
#figure(image("ping通.png",format: "png",width: 100%,fit: "stretch"),caption: "ICMP Ping测试成功")
|
||||
= 实验总结
|
||||
== 内容总结
|
||||
本次实验基于`xnet_tiny`微型协议栈,完整实现了ARP协议的核心功能,包括ARP表的初始化与管理、ARP请求的发送、ARP响应的接收与处理、以及基于定时器的超时重传机制。通过Wireshark抓包验证,程序能够正确发出ARP请求,解析目标IP的MAC地址,并能正确响应其他主机的ARP请求。
|
||||
|
||||
此外,作为拓展任务,实验还实现了IP层的输入输出处理和ICMP层的Echo Reply功能。这使得协议栈不仅能进行地址解析,还能响应Ping命令,验证了网络层的连通性。代码中还实现了多ARP表项的管理,支持同时维护多个IP-MAC映射。
|
||||
|
||||
本次实验的主要技术要点包括:
|
||||
|
||||
1. *状态机设计*:ARP表项采用有限状态机设计,通过FREE、PENDING、RESOLVED三种状态管理表项生命周期,实现了清晰的转换逻辑和可靠的状态管理
|
||||
|
||||
2. *非阻塞地址解析*:通过数据包挂起机制,实现了非阻塞式的ARP地址解析。当IP层需要发送数据但MAC地址尚未解析时,数据包被暂时挂起,待ARP解析完成后自动发送,避免了数据包丢失
|
||||
|
||||
3. *被动学习机制*:在接收ARP报文和IP报文时,自动学习发送方的IP-MAC映射并更新ARP表,减少了主动ARP请求的次数,提高了通信效率
|
||||
|
||||
4. *超时重传机制*:采用指数退避式的重传策略,设置1秒超时时间和3次重试上限,在网络不稳定的情况下仍能完成地址解析,同时避免了无限重传
|
||||
|
||||
5. *字节序处理*:正确处理了网络字节序(大端)和主机字节序的转换,通过 `swap_order16` 宏实现了16位整数的字节序交换,确保了跨平台兼容性
|
||||
|
||||
6. *校验和计算*:实现了标准的互联网校验和算法,用于IP头部和ICMP报文的完整性验证,提高了协议栈的可靠性
|
||||
|
||||
7. *免费ARP*:在系统启动时主动发送免费ARP,通告本机IP地址和MAC地址,实现了IP冲突检测和网络拓扑更新
|
||||
|
||||
8. *多表项管理*:ARP表支持16个表项的并发管理,能够同时维护多个IP-MAC映射,适应了多主机通信的需求
|
||||
|
||||
通过本次实验,不仅加深了对TCP/IP协议栈的理解,也掌握了网络协议实现的关键技术和设计思想,为后续学习更复杂的网络协议奠定了基础。
|
||||
== 心得感悟
|
||||
通过本次实验,我深入理解了ARP协议在以太网通信中的桥梁作用。从代码层面看,ARP协议虽然逻辑相对简单,但其状态机(PENDING, RESOLVED)的管理以及与IP层的交互细节(如数据包挂起)非常关键。
|
||||
|
||||
在实现过程中,对"免费ARP"的作用有了更直观的认识——它在系统启动时主动通告自身存在,有效避免了IP冲突。同时,超时重传机制的实现让我体会到了网络协议在不可靠传输介质上保证可靠性的设计思想。通过结合Wireshark抓包调试,我能够清晰地看到数据包的交互流程,这种理论与实践结合的方式极大地巩固了我的计算机网络知识体系。
|
||||
|
||||
*协议设计思想的思考*:
|
||||
|
||||
ARP协议的设计体现了网络协议中的几个重要原则。首先是"简单性原则",ARP协议只解决地址映射这一个核心问题,不涉及复杂的路由和转发逻辑,使得实现简单高效。其次是"被动学习"的思想,通过观察网络中的通信自动建立缓存表,减少了不必要的请求。最后是"渐进式可靠性",通过超时重传机制在不可靠的以太网上实现可靠的地址解析,而不需要复杂的确认应答机制。
|
||||
|
||||
*调试经验总结*:
|
||||
|
||||
在实验过程中,我遇到了几个典型的问题。首先是字节序问题,ARP报文中的16位字段需要使用网络字节序(大端),而x86架构使用小端字节序,必须进行转换。其次是内存对齐问题,使用 `#pragma pack(1)` 指令确保ARP报文结构体紧密排列,避免编译器插入填充字节。最后是状态管理的复杂性,ARP表项的状态转换需要仔细处理,特别是从PENDING状态到RESOLVED状态时,必须确保挂起的数据包能够正确发送。
|
||||
|
||||
通过Wireshark抓包分析,我发现了一个有趣的现象:当连续发送多个ARP请求时,如果目标主机不响应,ARP请求会按照1秒的间隔重传3次,然后停止。这种设计避免了网络拥塞,也体现了协议设计者的深思熟虑。
|
||||
|
||||
*改进建议*:
|
||||
|
||||
基于本次实验的经验,我认为可以从以下几个方面进行改进:
|
||||
|
||||
1. *ARP缓存淘汰策略*:当前实现采用简单的超时淘汰机制,可以引入LRU(最近最少使用)算法,当ARP表满时优先淘汰最久未使用的表项,提高缓存命中率
|
||||
|
||||
2. * Gratuitous ARP的定期发送*:除了启动时发送免费ARP,可以定期(如每隔几分钟)发送一次,确保网络中的ARP缓存保持最新状态
|
||||
|
||||
3. *ARP欺骗防护*:可以增加ARP请求的验证机制,例如检查ARP响应是否与请求匹配,防止ARP欺骗攻击
|
||||
|
||||
4. *统计信息收集*:增加ARP请求成功率、平均响应时间等统计信息,便于性能分析和问题诊断
|
||||
|
||||
5. *IPv6支持*:扩展协议栈以支持IPv6的邻居发现协议(NDP),实现下一代网络协议的地址解析功能
|
||||
|
||||
通过本次实验,我不仅掌握了ARP协议的实现技术,更重要的是学会了如何从协议规范出发,设计并实现一个完整的网络协议模块。这种能力对于后续学习更复杂的网络协议(如TCP、UDP)以及从事网络相关工作都具有重要意义。
|
||||
#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")
|
||||
0
network/arpicmplab/arp/ping通.png → network/arpicmplab/ping通.png
Executable file → Normal file
|
Before Width: | Height: | Size: 323 KiB After Width: | Height: | Size: 323 KiB |
0
network/arpicmplab/arp/一秒一个.png → network/arpicmplab/一秒一个.png
Executable file → Normal file
|
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 180 KiB |
0
network/arpicmplab/arp/分析结构2.png → network/arpicmplab/分析结构2.png
Executable file → Normal file
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
0
network/arpicmplab/arp/十秒一个.png → network/arpicmplab/十秒一个.png
Executable file → Normal file
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 183 KiB |
0
network/arpicmplab/arp/十秒一个2.png → network/arpicmplab/十秒一个2.png
Executable file → Normal file
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 163 KiB |
0
network/arpicmplab/arp/启动ARP.png → network/arpicmplab/启动ARP.png
Executable file → Normal file
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 173 KiB |
0
network/arpicmplab/arp/启动ARP2.png → network/arpicmplab/启动ARP2.png
Executable file → Normal file
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 143 KiB |
0
network/arpicmplab/arp/抓一个arp分析结构.png → network/arpicmplab/抓一个arp分析结构.png
Executable file → Normal file
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
0
network/arpicmplab/arp/物理机虚拟机IP配置.png → network/arpicmplab/物理机虚拟机IP配置.png
Executable file → Normal file
|
Before Width: | Height: | Size: 231 KiB After Width: | Height: | Size: 231 KiB |
0
network/arpicmplab/arp/环境配置ping测试.png → network/arpicmplab/环境配置ping测试.png
Executable file → Normal file
|
Before Width: | Height: | Size: 402 KiB After Width: | Height: | Size: 402 KiB |