一、ARP的输入处理
1、在上节课中,我们实现了无回报ARP包的发送,那么当我们本地的协议栈收到别人发过来的ARP数据包的时候应当怎么去处理它呢?
》》①如果收到的是ARP的请求包,我们就给对方一个response,并且将对方的ip和mac地址缓存到我们的ARP表的表项当中 ②如果收到的是ARP的响应包,就直接把对方的ip+mac地址缓存下来即可,这里的过程如下图:
2、当我们ping 192.168.254.1的时候,就会向改ip地址发送一个ICMP的包,为了完成ping操作,对方的ip还必须将数据包原样返回,并计算一下参数附加在里面
1)在xnet_tiny.h中添加arp的输入处理函数:xarp_in()
void xarp_in(xnet_packet_t* packet);
然后在以太网的输入函数中添加对于arp包的处理(放到ethernet_in处理函数里面):
switch (swap_order16(hdr->protocol)) {
//在此处添加收到ARP包的处理
case XNET_PROTOCOL_ARP:
remove_header(packet, sizeof(xether_hdr_t));
xarp_in(packet);
break;
case XNET_PROTOCOL_IP: {
break;
}
}
具体的过程就是①先移除以太网的包头 ②走xarp_in函数的处理流程
2)下面我们在xnet_tiny.c中来具体实现xarp_in()函数:
//处理接收到的ARP包
void xarp_in(xnet_packet_t* packet) {
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+mac)的ARP请求/响应
if (!xipaddr_is_equal_buf(&netif_ipaddr, arp_packet->target_ip)) {
return;
}
if (!xmacaddr_is_equal_buf(netif_mac, arp_packet->target_mac)) {
return;
}
//根据不通的操作码来做不同的处理
switch (opcode)
{
case XARP_REQUEST:
xarp_make_response(arp_packet);
update_arp_entry(arp_packet->sender_ip, arp_packet->sender_mac);
break;
case XARP_REPLY:
update_arp_entry(arp_packet->sender_ip, arp_packet->sender_mac);
break;
}
}
}
主要的步骤就是:
①首先检查arp包的合法性,没有通过检查的话就直接丢弃掉
② 通过xipaddr_is_equal_buf()函数,只去处理发给本地协议栈(ip+mac)的ARP请求/响应(xipaddr_is_equal_buf函数我们一会在下面继续实现)
③最后我们来根据不通的操作码来做不同的处理:如果是ARP请求的话,我们就先对请求做出响应,(xarp_make_response同样也在后面实现),然后再去更新我们本地的ARP缓存表;如果是ARP的响应的话,我们就只需要更新ARP表即可
3)继续在xnet_tiny.c中实现xarp_make_response函数,其过程可以参考xarp_make_request
主要功能就是收到对方的ARP请求之后,把自己的ip+mac给对方发过去
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);
response_packet->hw_len = XNET_MAC_ADDR_SIZE;
response_packet->pro_len = XNET_IPV4_ADDR_SIZE;
response_packet->opcode = swap_order16(XARP_REPLY);
memcpy(response_packet->sender_mac, netif_mac, XNET_MAC_ADDR_SIZE);
memcpy(response_packet->sender_ip, netif_ipaddr.array, XNET_IPV4_ADDR_SIZE);
memcpy(response_packet->target_mac, arp_packet->sender_mac, XNET_MAC_ADDR_SIZE);
memcpy(response_packet->target_ip, arp_packet->sender_ip, XNET_IPV4_ADDR_SIZE);
return ethernet_out_to(XNET_PROTOCOL_ARP, arp_packet->sender_mac, packet);
}
通过上述代码我们不难发现,所谓的响应(make_response)其实就是生成一个ARP的响应包来把本地的ip+mac打包发送给对方
4)在xnet_tiny.c中实现update_arp_entry():
static void update_arp_entry(uint8_t* ip_addr, uint8_t* mac_addr) {
memcpy(arp_entry.ipaddr.array, ip_addr, XNET_IPV4_ADDR_SIZE);
memcpy(arp_entry.macaddr, mac_addr, XNET_MAC_ADDR_SIZE);
arp_entry.state = XNET_ENT_OK;
}
由此我们不难发现update_arp_entry的任务就是负责更新arp表项
决定表项是不是正确的映射关系就通过arp_entry.state = XNET_ENT_OK来决定
我们在xnet_tiny.h中添加对arp_entry状态的定义:
#define XARP_ENTRY_FREE 0 //标识arp空闲
#define XNET_ENT_OK 1 //arp表解析成功
5)最后我们来定义一下xipaddr_is_equal_buf的宏函数,用来判断两个ip地址是否相同:
#define xipaddr_is_equal_buf(addr,buf) (memcmp((addr)->array,(buf),XNET_IPV4_ADDR_SIZE)==0)
然后我们在以太网的输入部分打下一个断点,在虚拟机中ping我们的vs2019,观察能不能进入
之后进入xarp_in函数里面
①观察arp_packet的源ip+mac,target的ip+mac是否符合我们的预期
②然后观察有没有通过包的合法性检查
③观察是否是发给我们协议栈的
然后进入make_response
之后我们通过Wireshark等软件观察能否收到vs2019的ARP响应包
如果最后一步出问题的原因收不到的话,毫无疑问你只需要检查xarp_make_response函数即可
至此我们完成了ARP的输入处理全过程!
补充:如果你的虚拟机收不到ping请求属于正常现象,因为我们还没有实现ICMP包的响应!
二、ARP的超时重新请求
1、为什么需要ARP的超时重传?或者说为什么要给我们的每个ARP表项添加一个定时器?
》》arp_entry需要定时更新的根本原因就在于我们本地记录的ip<>mac地址的映射关系不一定是正确的,因为对方的机器随时可能从网络中上线/下线,这时候DHCP服务器很有可能就把这个ip地址分配给别人使用了;而且由于MAC地址是一个平面的概念,它具备plug-and-use的特性,你从一个机器上拔下来插到另外一个机器上面毫无影响,即使你把你的网卡拔下来,插到美国的一台PC机上,仍然可以正常使用,这就导致MAC地址也会发生变化,因此我们就需要每隔一段时间扫描一下arp_entry,把那些到时的表项重新向网络上发送请求,根本目的是为了确保表项的正确性。
在这里我们可以浅总结一下:我们将ip<>mac的映射关系记录下来是为了提高速率,而设置定时器,甚至是删除一下表项则是为了适应网络的动态变化。
2、那么我们应该在哪里去实现这个功能呢?
》》在我们自己的协议栈当中增加可靠的机制
首先因为我们的数据包在网络中传输很有可能会丢包,因为网路是不可靠的链路,同时对方的协议栈如果处理的负荷比较大的话,也有可能导致我们的数据包被丢弃,因此“靠人不如靠自己”,我们自己的协议栈定期去检查表项的可靠性(设置一个定时器,到时间了就重新向表项中的主机发送ARP请求)
除此之外我们还需要设置另外一个超时值,因为我们重新发送ARP请求之后还需要判断一下在指定的时间内有没有收到响应,从而避免的等待时间过长的这种情况的出现,如果超过这个时间还没有收到响应的话,我们就会再发送一个请求,直到对方给了回复或者超过最大请求次数的话就将该表项删除。
3、为什么还需要重发?
》》重发的根本原因就在于我们通过以太网发出去的包并不一定能够保证对方一定能收到,并且正确地发回我们的机器当中去
下面我们来具体实现一下ARP的超时重传机制吧!
1)在port_pcap.c中添加时间的头文件:#include<time.h>
并且添加xsys_get_time函数:其中clock()表示的是从程序启动到调用clock()的时间
而clock()/CLOCKS_PER_SEC表示的是从程序启动到调用经过多少秒
const xnet_time_t xsys_get_time(void) {
//clock()表示的是从程序启动到调用这个函数的时间
//clock()/CLOCKS_PER_SEC表示的是从程序启动到调用经过多少秒
return clock() / CLOCKS_PER_SEC;
}
2)在xnet_tiny.h中添加xnet_time_t的定义
typedef uint32_t xnet_time_t; //时间类型,返回当前系统跑了多少s
const xnet_time_t xsys_get_time(void);
在xnet_tiny.h中添加xarp_poll的定义
void xarp_poll(void);
然后在xnet_poll函数中调用xarp_poll
3)在xnet_tiny.h中添加:arp表项的超时时间;arp表挂起时(需要重新查询)最大的查询次数;
arp表项挂起超时时间(查一次最多等多久)
#define XARP_CFG_ENTRY_OK_TMO (5) //arp表项的超时时间设置为5s
#define XARP_CFG_MAX_RETRIES 4 //arp表挂起时(需要重新查询)最大的查询次数
#define XARP_CFG_ENTRY_PENDING_TMO (1) //arp表项挂起超时时间(查一次最多等多久)
在xnet_tiny.h中添加arp表项的几个状态:
#define XARP_ENTRY_FREE 0 //arp表项空闲
#define XNET_ENT_OK 1 //arp表项解析成功
#define XARP_ENTRY_PENDING 2 //arp表项正在解析
注:OK表示ARP表的ip<>mac的转换关系是正确的,pending表示不能够确定关系是否正确,需要重新查询
4)现在我们来实现xarp_poll函数:
需要注意的是xarp_poll是各一个固定的周期去查询一次,而以太网的查询是一直在进行的
先实现一个简单的case,即活跃的arp表项:减到0就重新请求并且设置为解析中的状态
case XARP_ENTRY_OK:
if (--arp_entry.tmo == 0) {
xarp_make_request(&arp_entry.ipaddr);
arp_entry.state = XARP_ENTRY_PENDING;
arp_entry.tmo = XARP_CFG_ENTRY_PENDING_TMO;
}
然后是解析的case的实现:
case XARP_ENTRY_RESOLVING:
if (--arp_entry.tmo == 0) { // 重试完毕,回收
if (arp_entry.retry_cnt-- == 0) {
arp_entry.state = XARP_ENTRY_FREE;
} else { // 继续重试
xarp_make_request(&arp_entry.ipaddr);
arp_entry.state = XARP_ENTRY_PENDING;
arp_entry.tmo = XARP_CFG_ENTRY_PENDING_TMO;
}
}
break;
完整的实现:
void xarp_poll(void) {
if (xnet_chek_tmo(&arp_timer, XARP_TIME_PERIOD)) {
//判断arp表项的状态是不是活跃active的
switch (arp_entry.state)
{
case XARP_ENTRY_PENDING:
if (--arp_entry.tmo == 0) {
if (arp_entry.retry_cnt-- == 0) {
arp_entry.state = XARP_ENTRY_FREE;
}
else {
xarp_make_request(&arp_entry.ipaddr);
arp_entry.state = XARP_ENTRY_PENDING;
arp_entry.tmo = XARP_CFG_ENTRY_PENDING_TMO;
}
}
break;
case XARP_ENTRY_OK:
if (--arp_entry.tmo == 0) {
xarp_make_request(&arp_entry.ipaddr);
arp_entry.state = XARP_ENTRY_PENDING;
arp_entry.tmo = XARP_CFG_ENTRY_PENDING_TMO;
}
default:
break;
}
}
}
5)定义一个宏XARP_TIME_PERIOD 来设置arp表的扫描时间,即多久调用一次xarp_poll
#define XARP_TIME_PERIOD 1 //arp扫描周期(1s扫描一次)
定义一下arp的定时器:
static xnet_time_t arp_timer; //arp扫描的定时器
6)实现xnet_check_tmo函数,用来检查arp表项是否超时,同时通过指针可以更新timer的值
特别地:在这里指定sec=0表示用于获取系统当前运行的秒数;非0的时候才进行arp的超时检查
//检查是否超时,同时通过指针可以更新timer的值
int xnet_check_tmo(xnet_time_t* timer, uint32_t sec) {
xnet_time_t cur = xsys_get_time();
if (sec == 0) { //0特殊地,用于获取当前的时间
*timer = cur;
return 0;
}
else if (cur - *timer >= sec) { //非0检查超时
*timer = cur; //当超时的时候,才更新时间
return 1;
}
return 0;
}
在xarp_init中给arp的timer进行初始化:
void xarp_init(void) {
arp_entry.state = XARP_ENTRY_FREE;
//获取初始时间 ,给我们的timer初始化
xnet_check_tmo(&arp_timer, 0);
}
下面让我们一起进入愉快的调试环节吧🤷♀️:
①首先检查一下计时器是否正常工作
②使用虚拟机ping 192.168.254.2,观察5s之后能否进入XARP_ENTRY_OK的处理函数
③把网卡禁用观察能否进入XARP_ENTRY_PENDING的处理函数
注意要把虚拟机和物理机的网卡都给他干掉
如果上面的三个断点你都能进入的话,那么恭喜你完成了本章的所有内容的🎉🎉
同时,到这里我们已经实现了arp超时重传的所有功能!!!
下一节课我们会继续带领大家实现IP协议的相关功能,期待的话多多关注吧 🤞💕💕