三LWIP学习笔记之ARP协议
一、ARP协议简介
ARP,全称 Address Resolution Protocol,译作地址解析协议,ARP 协议与底层网络接口密切相关。TCP/IP 标准分层结构中,把 ARP 划分为了网络层的重要组成部分。 当一个主机上的应用程序要向目标主机发送数据时,它只知道目标主机的 IP 地址,而在协议栈底层接口发送数据包时,需要将该 IP 地址转换为目标主机对应的 MAC 地址,这样才能在数据链路上选择正确的通道将数据包传送出去,在整个转换过程中发挥关键作用的就是 ARP 协议了。 在本章中将看到:
ARP 协议的原理;
ARP 缓存表及其创建、维护、查询;
ARP 报文结构;
ARP 层数据包的接收处理;
ARP 层数据包的发送。
ARP 层是将底层链路与协议上层连接起来的纽带,是以太网通信中不可或缺的协议。
1、物理地址与网络地址
网卡的 48 位 MAC 地址都保存在网卡的内部存储器中,另一方面,TCP/IP 协议有自己的地址:32bit 的 IP 地址(网络地址),网络层发送数据包时只知道目的主机的 IP 地址,而底层接口(如以太网驱动程序)必须知道对方的硬件地址才能将数据发送出去。
为了解决地址映射的问题,ARP 协议提供了一种地址动态解析的机制,ARP 的功能是在 32 bit的 IP 地址和采用不同网络技术的硬件地址之间提供动态映射,为上层将底层的物理地址差异屏蔽起来,这样上层的因特网协议便可以灵活的使用 IP 地址进行通信。
2、ARP协议的本质
ARP 协议的基本功能是使用目标主机的 IP 地址,查询其对应的 MAC 地址,以保证底层链路上数据包通信的进行。
举一个简单的例子来看看 ARP 的功能。假如我们的主机(192.168.1.78)需要向开发板(192.168.1.37)发送一个 IP 数据包,当发送数据时,主机会在自己的 ARP 缓存表中寻找是否有目标 IP 地址。如果找到了,也就知道了目标 MAC 地址为(008048123456), 此时主机直接把目标 MAC 地址写入以太网帧首部发送就可以了;如果在 ARP 缓存表中没有找到相对应的 IP 地址,此时比较不幸,我们的数据需要被延迟发送,随后主机会先在网络上发送一个广播(ARP 请求,以太网目的地址为 FFFFFFFFFFFF),广播的 ARP 请求表示同一网段内的所有主机将会收到这样一条信息:“192.168.1.37 的 MAC 地址是什么?请回答”。网络 IP 地址为 192.168.1.37(开发板)的主机接收到这个帧后,它有义务做出这样的回答(ARP 应答):“192.168.1.37 的 MAC 地址是(008048123456)”。 这样,主机就知道了开发板的 MAC 地址,先前被延迟的数据包就可以发送了,此外,主机会将这个地址对保存在缓存表中以便后续数据包发送时使用。 ARP 的实质就是对缓存表的建立、更新、查询等操作。
二、数据结构
源文档中的 etharp.c 和 etharp.h 文件实现了以太网中 ARP 协议的全部数据结构和函数定义,ARP 协议实现过程中有两个重要的数据结构,即 ARP 缓存表和 ARP 报文。
1、ARP表
ARP 协议的核心在于 ARP 缓存表,ARP 的实质就是对缓存表的建立、更新、查询等操作。ARP 缓存表由缓存表项(entry)组成,每个表项记录了一组 IP 地址和 MAC 地址绑定信息,当然除了这两个基本信息外,还包含了与数据包发送控制、缓存表项管理相关的状态、控制信息。LwIP中描述缓存表项的数据结构叫 etharp_entry,这个结构比较简单,如下所示:
————etharp.c————————————————————————— struct etharp_entry
{ struct etharp_q_entry *q; //数据包缓冲队列指针 struct ip_addr ipaddr; //目标 IP 地址 struct eth_addr ethaddr; // MAC 地址 enum etharp_state state; //描述该 entry 的状态 u8_t ctime; //描述该 entry 的时间信息 struct netif *netif; //对应网络接口信息 }; ——————————————————————————————————
描述缓冲队列的数据结构也很简单,叫做 etharp_q_entry,该结构的定义如下:
————etharp.h—————————————————— struct etharp_q_entry
{ struct etharp_q_entry *next; //指向下一个缓冲数据包 struct pbuf *p; //指向数据包 pbuf }; —————————————————————————————————
用一个图来看看 etharp_q_entry 结构在缓存表数据队列中的作用,如图 92 所示。
state 是个枚举类型,它描述该缓存表项的状态,LwIP 中定义一个缓存表项可能有三种不同的状态,用枚举型 etharp_state 进行描述。
————etharp.c————————————————————————— enum etharp_state
{ ETHARP_STATE_EMPTY = 0, //empty 状态 ETHARP_STATE_PENDING, //pending 状态 ETHARP_STATE_STABLE //stable 状态 }; ————————————————————————————————
编译器为 ARP 表预先定义了 ARP_TABLE_SIZE(通常为 10)个表项空间,因此 ARP 缓存表内部最多只能存放 ARP_TABLE_SIZE 条 IP 地址与 MAC 地址配对信息。
————etharp.c—————————————————————— static struct etharp_entry arp_table[ARP_TABLE_SIZE]; //定义 ARP 缓存表 ——————————————————————————————————
ETHARP_STATE_EMPTY 状态(empty) :初始化的时候为empty状态。
ETHARP_STATE_PENDING 状态(pending):表示该表项处于不稳定状态,此时该表项只记录到了IP 地址,但是还未记录到对应的 MAC 地址。 很可能的情况是,LwIP 内核已经发出一个关于该 IP地址的 ARP 请求到数据链路上,但是还未收到 ARP 应答。
ETHARP_STATE_STABLE 状态(stable) :当 ARP 表项被更新后,它就记录了一对完整的 IP 地址和MAC 地址 。
在ETHARP_STATE_PENDING 状态下会设定超时时间(10秒),当计数超时后,对应的表项将被删除;在ETHARP_STATE_STABLE 状态下也会设定超时时间(20分钟),当计数超时后,对应的表项将被删除。
最后一个字段,网络接口结构指针 netif,在 ARP 表项中维护这样一个指针还是很有用的,因为该结构中包含了网络接口的 MAC 地址和 IP 地址等信息,在发送数据包的时候,这些信息都起着至关重要的作用。
ctime 为每个表项的计数器,周期性的去调用一个 etharp_tmr 函数,这个函数以 5 秒为周期被调用,在这个函数中,它会将每个ARP 缓存表项的 ctime 字段值加 1,当相应表项的生存时间计数值 ctime 大于系统规定的某个值时,系统将删除对应的表项。
————etharp.c———————————————————— //稳定状态表项的最大生存时间计数值:240*5s=20min #define ARP_MAXAGE 240 //PENDING 状态表项的最大生存时间计数值:2*5s=10s #define ARP_MAXPENDING 2 void etharp_tmr(void) { u8_t i; for (i = 0; i < ARP_TABLE_SIZE; ++i)
{ //对每个表项操作,包括空闲状态的表项 arp_table[i].ctime++; //先将表项 ctime 值加 1 //如果表项是 stable 状态,且生存值大于 ARP_MAXAGE, //或者是 pending 状态且其生存值大于 ARP_MAXPENDING,则删除表项 if ( ((arp_table[i].state == ETHARP_STATE_STABLE) && //stable 状态 (arp_table[i].ctime >= ARP_MAXAGE)) || //或者 ((arp_table[i].state == ETHARP_STATE_PENDING) && //pending 状态 (arp_table[i].ctime >= ARP_MAXPENDING)) )
{ if (arp_table[i].q != NULL)
{ //如果表项上的数据队列中有数据, free_etharp_q(arp_table[i].q); //则释放队列中的所有数据 arp_table[i].q = NULL; //队列设置为空 } arp_table[i].state = ETHARP_STATE_EMPTY; //将表项状态改为未用,即删除 }//if }//for } ——————————————————————————————————
2、ARP报文
源主机如何告诉目的主机:我需要你的 MAC 地址;而目的主机如何回复:这就是我的 MAC 地址。ARP 报文(或者称之为 ARP 数据包),这就派上用场了。 ARP 请求和 ARP 应答,它们都是被组装在一个 ARP 数据包中发送的,
这里先来看看一个典型的 ARP 包的组成结构。如图 93 所示
以太网目的地址和以太网源地址:分别表示以太网目的MAC地址和源MAC地址,目的地址全1时是特殊地址以太网广播地址。在 ARP 表项建立前,源主机只知道目的主机的 IP 地址,并不知道其 MAC 地址,所以在数据链路上,源主机只有通过广播的方式将 ARP请求数据包发送出去,同一网段上的所有以太网接口都会接收到广播的数据包。
桢类型:ARP-0x0806、IP-0x0800、PPPoE-0x8864
硬件类型:表示发送方想要知道的硬件类型。1-以太网MAC地址
协议类型:表示要映射的协议地址类型,0x0800-表示要映射为IP地址
硬件地址长度和协议地址长度:以太网ARP请求和应答来说分别为6和4,代表MAC地址长度和IP地址长度。在 ARP 协议包中留出硬件地址长度字段和协议地址长度字段可 以使得 ARP 协议在任何网络中被使用,而不仅仅只在以太网中。
op:指出ARP数据包的类型,ARP请求(1),ARP应答(2)
在以太网的数据帧头部中和 ARP 数据包中都有发送端的以太网MAC 地址。对于一个 ARP 请求包来说,除接收方以太网地址外的所有字段都应该被填充相应的值。当接收方主机收到一份给自己的 ARP 请求报文后,它就把自己的硬件地址填进去,然后将该请求数据包的源主机信息和目的主机信息交换位置,并把操作字段 op 置为 2,最后把该新构建的数据包发送回去,这就是 ARP 应答。
关于上图中的这个结构,在 ARP 中用了一大堆的数据结构和宏来描述它们。
————etharp.h———————————————— #ifndef ETHARP_HWADDR_LEN #define ETHARP_HWADDR_LEN 6 //以太网物理地址长度 #endif PACK_STRUCT_BEGIN //我们移植时实现的结构体封装宏 struct eth_addr
{ //定义以太网 MAC 地址结构体 eth_addr,禁止编译器自对齐 PACK_STRUCT_FIELD(u8_t addr[ETHARP_HWADDR_LEN]); } PACK_STRUCT_STRUCT; PACK_STRUCT_END PACK_STRUCT_BEGIN //定义以太网数据帧首部结构体 eth_hdr,禁止编译器自对齐 struct eth_hdr
{ PACK_STRUCT_FIELD(struct eth_addr dest); //以太网目的地址(6 字节) PACK_STRUCT_FIELD(struct eth_addr src); //以太网源地址(6 字节) PACK_STRUCT_FIELD(u16_t type); //帧类型(2 字节) } PACK_STRUCT_STRUCT; PACK_STRUCT_END //定义以太网帧头部长度宏,其中 ETH_PAD_SIZE 已定义为 0 #define SIZEOF_ETH_HDR (14 + ETH_PAD_SIZE) PACK_STRUCT_BEGIN //定义 ARP 数据包结构体 etharp_hdr,禁止编译器自对齐 struct etharp_hdr
{ PACK_STRUCT_FIELD(u16_t hwtype); //硬件类型(2 字节) PACK_STRUCT_FIELD(u16_t proto); //协议类型(2 字节) PACK_STRUCT_FIELD(u16_t _hwlen_protolen); //硬件+协议地址长度(2 字节) PACK_STRUCT_FIELD(u16_t opcode); //操作字段 op(2 字节) PACK_STRUCT_FIELD(struct eth_addr shwaddr); //发送方 MAC 地址(6 字节) PACK_STRUCT_FIELD(struct ip_addr2 sipaddr); //发送方 IP 地址(4 字节) PACK_STRUCT_FIELD(struct eth_addr dhwaddr); //接收方 MAC 地址(6 字节) PACK_STRUCT_FIELD(struct ip_addr2 dipaddr); //接收方 IP 地址(4 字节) } PACK_STRUCT_STRUCT; PACK_STRUCT_END #define SIZEOF_ETHARP_HDR 28 //宏,ARP 数据包长度 //宏,包含 ARP 数据包的以太网帧长度 #define SIZEOF_ETHARP_PACKET (SIZEOF_ETH_HDR + SIZEOF_ETHARP_HDR) #define ARP_TMR_INTERVAL 5000 //定义 ARP 定时器周期为 5 秒,不同帧类型的宏定义 #define ETHTYPE_ARP 0x0806 #define ETHTYPE_IP 0x0800 //ARP 数据包中 OP 字段取值宏定义 #define ARP_REQUEST 1 //ARP 请求 #define ARP_REPLY 2 //ARP 应答 ————————————————————————————
发送 ARP 请求数据包的函数叫 etharp_request,看名字就晓得了。这个函数很简单,它是通过调用 etharp_raw 函数来实现的,调用后者时,需要为它提供 ARP数据包中各个字段的值,后者直接将各个字段的值填写到在一个 ARP 包中发送(该函数并不知道发送的是 ARP 请求还是 ARP 响应,它只管组装并发送,所以称之为 raw)
————etharp.c—————————————————— //函数功能:根据各个参数字段组织一个 ARP 数据包并发送 //参数 netif:发送 ARP 包的网络接口结构 //参数 ethsrc_addr:以太网帧首部中的以太网源地址值 //参数 ethdst_addr:以太网帧首部中的以太网目的地址值 //参数 hwsrc_addr:ARP 数据包中的发送方 MAC 地址 //参数 ipsrc_addr:ARP 数据包中的发送方 IP 地址 //参数 hwdst_addr:ARP 数据包中的接收方 MAC 地址 //参数 ipdst_addr:ARP 数据包中的接收方 IP 地址 //参数 opcode:ARP 数据包中的 OP 字段值,请求ARP为1,应答ARP为2 //注:ARP 数据包中其他字段使用预定义值,例如硬件地址长度为 6,协议地址长度为 4 err_t etharp_raw(struct netif *netif, const struct eth_addr *ethsrc_addr, const struct eth_addr *ethdst_addr, const struct eth_addr *hwsrc_addr, const struct ip_addr *ipsrc_addr, const struct eth_addr *hwdst_addr, const struct ip_addr *ipdst_addr, const u16_t opcode) { struct pbuf *p; //数据包指针 err_t result = ERR_OK; //返回结果 u8_t k; struct eth_hdr *ethhdr; //以太网数据帧首部结构体指针 struct etharp_hdr *hdr; // ARP 数据包结构体指针 //先在内存堆中为 ARP 包分配空间,大小为包含 ARP 数据包的以太网帧总大小 p = pbuf_alloc(PBUF_RAW, SIZEOF_ETHARP_PACKET, PBUF_RAM); if (p == NULL)
{ //若分配失败则返回内存错误 return ERR_MEM; } //到这里,内存分配成功 ethhdr = p>payload; // ethhdr 指向以太网帧首部区域 hdr = (struct etharp_hdr *)((u8_t*)ethhdr + SIZEOF_ETH_HDR);// hdr 指向 ARP 首部 hdr>opcode = htons(opcode); //填写 ARP 包的 OP 字段,注意大小端转换 k = ETHARP_HWADDR_LEN; //循环填写数据包中各个 MAC 地址字段 while(k > 0)
{ k--; hdr>shwaddr.addr[k] = hwsrc_addr>addr[k]; //ARP 头部的发送方 MAC 地址 hdr>dhwaddr.addr[k] = hwdst_addr>addr[k]; //ARP 头部的接收方 MAC 地址 ethhdr>dest.addr[k] = ethdst_addr>addr[k]; //以太网帧首部中的以太网目的地址 ethhdr>src.addr[k] = ethsrc_addr>addr[k]; //以太网帧首部中的以太网源地址 } hdr>sipaddr = *(struct ip_addr2 *)ipsrc_addr; //填写 ARP 头部的发送方 IP 地址 hdr>dipaddr = *(struct ip_addr2 *)ipdst_addr; //填写 ARP 头部的接收方 IP 地址 //下面填充一些固定字段的值 hdr>hwtype = htons(HWTYPE_ETHERNET); //ARP 头部的硬件类型为 1,即以太网 hdr>proto = htons(ETHTYPE_IP); //ARP 头部的协议类型为 0x0800 //设置两个长度字段 hdr>_hwlen_protolen = htons((ETHARP_HWADDR_LEN << 8) | sizeof(struct ip_addr)); ethhdr>type = htons(ETHTYPE_ARP); //以太网帧首部中的帧类型字段,ARP 包 result = netif>linkoutput(netif, p); //调用底层数据包发送函数 pbuf_free(p); //释放数据包 p = NULL; return result; //返回发送结果 } //特殊 MAC 地址的定义,以太网广播地址 const struct eth_addr ethbroadcast = {{0xff,0xff,0xff,0xff,0xff,0xff}}; //该值用于填充 ARP 请求包的接收方 MAC 字段,无实际意义 const struct eth_addr ethzero = {{0,0,0,0,0,0}}; //函数功能:发送 ARP 请求 //参数 netif:发送 ARP 请求包的接口结构 //参数 ipaddr:请求具有该 IP 地址主机的 MAC err_t etharp_request(struct netif *netif, struct ip_addr *ipaddr) { //该函数只是简单的调用函数 etharp_raw,为函数提供所有相关参数 return etharp_raw(netif, (struct eth_addr *)netif>hwaddr, ðbroadcast, (struct eth_addr *)netif>hwaddr, &netif>ip_addr, ðzero, ipaddr, ARP_REQUEST); } ——————————————————————————————————
三、ARP层数据包输入
1、以太网数据包递交
在我们说网卡驱动的时候讲到了数据包接收函数 ethernetif_input,这个函数是源码作者提供的一个以太网数据包接收和递交函数,它的功能是调用底层数据包接收函数 low_level_input 读取网卡中的数据包,然后在将该数据包递交给相应的上层处理。
————ethernetif.c———————————————— static void ethernetif_input(struct netif *netif) { struct ethernetif *ethernetif; //用户自定义的网络接口信息结构,这里无用处 struct eth_hdr *ethhdr; //以太网帧头部结构指针 struct pbuf *p; ethernetif = netif>state; //获得自定义的网络信息结构,无实际意义 p = low_level_input(netif); //调用底层函数读取一个数据包 if (p == NULL) return; //如果数据包为空,则直接返回 //到这里数据包不为空 ethhdr = p>payload; //将 ethhdr 指向数据包中的以太网头部 switch (htons(ethhdr>type))
{ //判断帧类型,注意大小端转换 case ETHTYPE_IP: //对于 IP 包和 ARP 包,都调用注册的 netif>input 函数 case ETHTYPE_ARP: //进行处理
etharp_arp_input(netif, (struct eth_addr*)(netif>hwaddr), p);
if (netif>input(p, netif)!=ERR_OK)
{ //未完成正常的处理,则释放数据包 pbuf_free(p); p = NULL; } break; default: //对于其他类型的数据包,直接释放掉,不做处理 pbuf_free(p); p = NULL; break; }//switch } ——————————————————————————————————————
2、ARP数据包处理
首先,若这个请求的 IP 地址与本机地址不匹配,那么就不需要返回 ARP 应答,但由于该 ARP 请求包中包含了发送请求的主机的 IP 地址 和 MAC 地址,可以将这个地址对加入到 ARP 缓存表中,以便后续使用;其次,如果 ARP 请求与本机 IP 地址匹配,此时,除了进行上述的记录源主机的 IP 地址和 MAC 地址外,还需要给源主机返回一个 ARP 应答。整个过程清楚后,就可以来看具体的代码实现了。
————etharp.c———————————————————— //函数功能:处理 ARP 数据包,更新 ARP 缓存表,对 ARP 请求进行应答 //参数 ethaddr:网络接口的 MAC 地址 void etharp_arp_input(struct netif *netif, struct eth_addr *ethaddr, struct pbuf *p) { struct etharp_hdr *hdr; //指向 ARP 数据包头部的变量 struct eth_hdr *ethhdr; //指向以太网帧头部的变量 struct ip_addr sipaddr, dipaddr; //暂存 ARP 包中的源 IP 地址和目的 IP 地址 u8_t i; u8_t for_us; //用于指示该 ARP 包是否是发给本机的 //接下来判断 ARP 包是否是放在一个 pbuf 中的,由于整个 ARP 包都使用结构 etharp_hdr //进行操作,如果 ARP 包是分装在两个 pbuf 中的,那么对于结构体 etharp_hdr 的操作将 //无意义,我们直接丢弃掉这种类型的 ARP 包 if (p>len < SIZEOF_ETHARP_PACKET)
{ //ARP 包不能分装在两个 pbuf 中 pbuf_free(p); //否则直接删除,函数返回 return; } ethhdr = p>payload; // ethhdr 指向以太网帧首部 hdr = (struct etharp_hdr *)((u8_t*)ethhdr + SIZEOF_ETH_HDR); //hdr 指向 ARP 包首部 //这里需要判断 ARP 包的合法性,丢弃掉那些类型、长度不合法的 ARP 包 if ((hdr>hwtype != htons(HWTYPE_ETHERNET)) || //是否为以太网硬件类型 (hdr>_hwlen_protolen != htons((ETHARP_HWADDR_LEN << 8) | sizeof(struct ip_addr))) || (hdr>proto != htons(ETHTYPE_IP)) || //协议类型为 IP (ethhdr>type != htons(ETHTYPE_ARP)))
{ //是否为 ARP 数据包 pbuf_free(p); //若不符合,则删除数据包,函数返回 return; } //这里需要将 ARP 包中的两个 IP 地址数据拷贝到变量 sipaddr 和 dipaddr 中,因为后面 //会使用这两个 IP 地址,但 ARP 数据包中的 IP 地址字段并不是字对齐的,不能直接使用 SMEMCPY(&sipaddr, &hdr>sipaddr, sizeof(sipaddr)); //拷贝发送方 IP 地址到 sipaddr 中 SMEMCPY(&dipaddr, &hdr>dipaddr, sizeof(dipaddr)); //拷贝接收方 IP 地址到 dipaddr 中 //下面判断这个 ARP 包是否是发送给我们的 if (netif>ip_addr.addr == 0)
{ //如果网卡 IP 地址未配置 for_us = 0; //那么肯定不是给我们的,设置标志 for_us 为 0 } else
{ //如果网卡 IP 地址已经设置,则将目的 IP 地址与网卡 IP 地址比较 for_us = ip_addr_cmp(&dipaddr, &(netif>ip_addr)); //若相等,for_us 被置为 1 } //下面我们开始更新 ARP 缓存表 if (for_us)
{ //如果这个 ARP 包(请求或响应)是给我们的,则更新 ARP 表 update_arp_entry(netif, &sipaddr, &(hdr>shwaddr), ETHARP_TRY_HARD); } else
{//若不是给我们的,也更新 ARP 表,但是不设置 ETHARP_TRY_HARD 标志 update_arp_entry(netif, &sipaddr, &(hdr>shwaddr), 0); } //到这里,ARP 更新完毕,需要对 ARP 请求做出处理 switch (htons(hdr>opcode))
{ //判断 ARP 数据包的 op 字段 case ARP_REQUEST: //如果是 ARP 请求 if (for_us)
{ //且请求中的 IP 地址与本机的匹配,则需要返回 ARP 应答 //ARP 应答的返回很简单,不需要再重新申请一个 ARP 数据包空间, //而是直接将该 ARP 请求包中的相应字段进行改写,构造一个应答包 hdr>opcode = htons(ARP_REPLY); //将 op 字段改为 ARP 响应类型 hdr>dipaddr = hdr>sipaddr; //设置接收端 IP 地址 //设置发送端 IP 地址为网络接口中的 IP 地址 SMEMCPY(&hdr>sipaddr, &netif>ip_addr, sizeof(hdr>sipaddr)); //接下来,设置四个 MAC 地址字段 i = ETHARP_HWADDR_LEN; while(i > 0)
{ //目标 MAC 地址可以直接从原来的 ARP 包中得到 i--; //源 MAC 地址我们已经通过参数 ethaddr 传入 hdr>dhwaddr.addr[i] = hdr>shwaddr.addr[i];//设置 ARP 包的接收端 MAC 地址 ethhdr>dest.addr[i] = hdr>shwaddr.addr[i]; //以太网帧中的目标 MAC 地址 hdr>shwaddr.addr[i] = ethaddr>addr[i]; // ARP 头部的发送端 MAC 地址 ethhdr>src.addr[i] = ethaddr>addr[i]; //以太网帧头部的源 MAC 地址 } //对于 ARP 包中的其他字段的值(硬件类型、协议类型、长度字段等) //保持它们的值不变,因为在前面已经测试过了它们的有效性 netif>linkoutput(netif, p); //直接发送 ARP 应答包 } else if (netif>ip_addr.addr == 0)
{//ARP 请求数据包不是给我们的, 不做任何处理 } //这里只打印一些调试信息,笔者已将它们去掉 else
{ } break; case ARP_REPLY: //如果是 ARP 应答,我们已经在最开始更新了 ARP 表 break; //这里神马都不用做了 default: break; }// switch pbuf_free(p); //删除数据包 p } ————————————————————————————————————
3、ARP攻击
4、ARP缓存表更新
四、ARP层数据包输出
1、ARP层数据处理总流程
2、广播包与多播包的发送
etharp_output 函数被 IP 层的数据包发送函数 ip_output 调用,它首先根据目的 IP地址的类型为数据包选择不同的处理方式:当目的 IP 地址为广播或者多播地址时,etharp_output可以直接根据这个目的地址构造出相应的特殊 MAC 地址,同时把 MAC 地址作为参数,和数据包一起交给 etharp_send_ip 发送;当目的 IP 地址为单播地址时,需要调用 etharp_query 函数在 ARP表中查找与目的 IP 地址对应的 MAC 地址,若找到,则函数 etharp_send_ip 被调用,以发送数据包;若找不到,则函数 etharp_request 被调用它会发送一个关于目的 IP 地址的 ARP 请求包,出现这种情况时,我们还需要将 IP 包挂接的相应表项的缓冲队列中,直到对应的ARP 应答返回时,该数据包才会被发送出去。
————etharp.c—————————————— //函数功能:发送一个 IP 数据包 pbuf 到目的地址 ipaddr 处,该函数被 IP 层调用 //参数 netif:指向发送数据包的网络接口结构 //参数 q:指向 IP 数据包 pbuf 的指针 //参数 ipaddr:指向目的 IP 地址 err_t etharp_output(struct netif *netif, struct pbuf *q, struct ip_addr *ipaddr) { struct eth_addr *dest, mcastaddr; if (pbuf_header(q, sizeof(struct eth_hdr)) != 0) {//调整 pbuf 的 payload 指针,使其指向 return ERR_BUF; //以太网帧头部,失败则返回 } dest = NULL; if (ip_addr_isbroadcast(ipaddr, netif))
{//如果是广播 IP 地址 dest = (struct eth_addr *)ðbroadcast; //dest 指向广播 MAC 地址 } else if (ip_addr_ismulticast(ipaddr))
{//如果是多播 IP 地址 mcastaddr.addr[0] = 0x01; //则构造多播 MAC 地址 mcastaddr.addr[1] = 0x00; mcastaddr.addr[2] = 0x5e; mcastaddr.addr[3] = ip4_addr2(ipaddr) & 0x7f; mcastaddr.addr[4] = ip4_addr3(ipaddr); mcastaddr.addr[5] = ip4_addr4(ipaddr); dest = &mcastaddr; // dest 指向多播 MAC 地址 } else { //如果为单播 IP 地址 //判断目的 IP 地址是否为本地的子网上,若不在,则修改 ipaddr if (!ip_addr_netcmp(ipaddr, &(netif>ip_addr), &(netif>netmask)))
{ if (netif>gw.addr != 0)
{ //需要将数据包发送到网关处,由网关转发 ipaddr = &(netif>gw); //更改变量 ipaddr,数据包发往网关处 } else
{ //如果网关未配置,返回错误 return ERR_RTE; } } //对于单播包,调用 etharp_query 查询其 MAC 地址并发送数据包 return etharp_query(netif, ipaddr, q); } //对于多播和广播包,由于得到了它们的目的 MAC 地址,所以可以直接发送 return etharp_send_ip(netif, q, (struct eth_addr*)(netif>hwaddr), dest); } ————————————————————————————————
广播包:调用函数 ip_addr_isbroadcast 判断目的 IP 地址是否为广播地址,如果是广播包,则目的 MAC 地址不需要查询 arp 表,由于广播 MAC 地址的 48 位均为 1,即目的 MAC 六个字节值为ffffffffffff。
多播包:判断目的 IP 地址是不是 D 类 IP 地址,如果是,则 MAC 地址可以直接计算得出,即将 MAC 地址 01005E000000 的低 23 位设置为 IP 地址的低 23 位。对于以上的两种数据包,etharp_output 直接调用函数 etharp_send_ip 将数据包发送出去。
单播包:要比较目的 IP 和本地 IP 地址,看是否是局域网内的,若不是局域网内的,则将目的IP 地址设置为默认网关的地址,然后再统一调用 etharp_query 函数查找目的 MAC 地址,最后将数据包发送出去。
————etharp_send_ip———————————————————— //函数功能:填写以太网帧头部,发送以太网帧 //参数 p:指向以太网帧的 pbuf //参数 src:指向源 MAC 地址 //参数 dst:指向目的 MAC 地址 static err_t etharp_send_ip(struct netif *netif, struct pbuf *p, struct eth_addr *src, struct eth_addr *dst) { struct eth_hdr *ethhdr = p>payload; //指向以太网帧头部 u8_t k; k = ETHARP_HWADDR_LEN; while(k > 0)
{ k--; ethhdr>dest.addr[k] = dst>addr[k]; //填写目的 MAC 字段 ethhdr>src.addr[k] = src>addr[k]; //填写源 MAC 字段 } ethhdr>type = htons(ETHTYPE_IP); //填写帧类型字段 return netif>linkoutput(netif, p); //调用网卡数据包发送函数 } ————————————————————————————————————
这个函数尤其简单,直接根据传入的参数填写以太网帧首部的三个字段,然后调用注册的底层数据包发送函数将数据包发送出去。
3、单播包的发送
如果给定的 IP 地址不在 ARP 表中,则一个新的 ARP 表项会被创建,此时该表项处于 pending 状态,同时一个关于该 IP 地址的 ARP 请求会被广播出去,再同时要发送的 IP 数据包会被挂接在该表项的数据缓冲指针上;如果 IP 地址在 ARP 表中有相应的表项存在,但该表项处于pending 状态,则操作与前者相同,即发送一个 ARP 请求和挂接数据包;如果 IP 地址在 ARP 表中有相应的表项存在,且表项处于 stable 状态,此时再来判断给定的数据包是否为空,不为空则直接将该数据包发送出去,为空则向该 IP 地址发送一个 ARP 请求。
//函数功能:查找单播 IP 地址对应的 MAC 地址,并发送数据包 //参数 ipaddr:指向目的 IP 地址 //参数 q:指向以太网数据帧的 pbuf err_t etharp_query(struct netif *netif, struct ip_addr *ipaddr, struct pbuf *q) { struct eth_addr * srcaddr = (struct eth_addr *)netif->hwaddr; err_t result = ERR_MEM; s8_t i; //调用函数 find_entry 查找或创建一个 ARP 表项 i = find_entry(ipaddr, ETHARP_TRY_HARD); if (i < 0) { //若查找失败,则 i 小于 0,直接返回 return (err_t)i; } //如果表项的状态为 empty,说明表项是刚创建的,且其中已经记录了 IP 地址 if (arp_table[i].state == ETHARP_STATE_EMPTY) { //将表项的状态改为 pending arp_table[i].state = ETHARP_STATE_PENDING; } if ((arp_table[i].state == ETHARP_STATE_PENDING) || (q == NULL)) {//数据包为空,或 result = etharp_request(netif, ipaddr); //表项为 pending 态,则发送 ARP 请求包 } if (q != NULL) {//数据包不为空,则进行数据包的发送或者将数据包挂接在缓冲队列上 if (arp_table[i].state == ETHARP_STATE_STABLE) {//ARP 表稳定,则直接发送数据包 result = etharp_send_ip(netif, q, srcaddr, &(arp_table[i].ethaddr)); } else if (arp_table[i].state == ETHARP_STATE_PENDING) {//否则,挂接数据包 struct pbuf *p; int copy_needed = 0;//是否需要重新拷贝整个数据包,数据包全由 PBUF_ROM p = q; //类型的 pbuf 组成时,才不需要拷贝 while (p) { //判断是否需要拷贝整个数据包 if(p->type != PBUF_ROM) { copy_needed = 1; break; } p = p->next; } if(copy_needed) { //如果需要拷贝,则申请内存堆空间 p = pbuf_alloc(PBUF_RAW, p->tot_len, PBUF_RAM);//申请一个 pbuf 空间 if(p != NULL) {申请成功,则执行拷贝操作 if (pbuf_copy(p, q) != ERR_OK) {//拷贝失败,则释放申请的空间 pbuf_free(p); p = NULL; } } } else { //如果不需要拷贝 p = q; //设置 p pbuf_ref(p); //增加 pbuf 的 ref 值 } //到这里,p 指向了我们需要挂接的数据包,下面执行挂接操作 if (p != NULL) { struct etharp_q_entry *new_entry; //为数据包申请一个 etharp_q_entry 结构 new_entry = memp_malloc(MEMP_ARP_QUEUE); //在内存池 POOL 中 if (new_entry != NULL) { //申请成功,则进行挂接操作 new_entry->next = 0; //设置 etharp_q_entry 结构的指针 new_entry->p = p; if(arp_table[i].q != NULL) { //若缓冲队列不为空 struct etharp_q_entry *r; r = arp_table[i].q; while (r->next != NULL) {//则找到最后一个缓冲包结构 r = r->next; } r->next = new_entry; //将新的数据包挂接在队列尾部 } else { //缓冲队列为空 arp_table[i].q = new_entry; //直接挂接在缓冲队列首部 } result = ERR_OK; } else { //etharp_q_entry 结构申请失败,则 pbuf_free(p); //释放数据包空间 } // if (p != NULL) }//else if }// if (q != NULL) return result; //返回函数操作结果 }