三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 地址为(00­80­48­12­34­56), 此时主机直接把目标 MAC 地址写入以太网帧首部发送就可以了;如果在 ARP 缓存表中没有找到相对应的 IP 地址,此时比较不幸,我们的数据需要被延迟发送,随后主机会先在网络上发送一个广播(ARP 请求,以太网目的地址为 FF­FF­FF­FF­FF­FF),广播的 ARP 请求表示同一网段内的所有主机将会收到这样一条信息:“192.168.1.37 的 MAC 地址是什么?请回答”。网络 IP 地址为 192.168.1.37(开发板)的主机接收到这个帧后,它有义务做出这样的回答(ARP 应答):“192.168.1.37 的 MAC 地址是(00­80­48­12­34­56)”。 这样,主机就知道了开发板的 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 结构在缓存表数据队列中的作用,如图 9­2 所示。 

        

  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 包的组成结构。如图 9­3 所示

          

  以太网目的地址和以太网源地址:分别表示以太网目的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, &ethbroadcast,     (struct eth_addr *)netif­>hwaddr, &netif­>ip_addr, &ethzero,     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 *)&ethbroadcast; //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 六个字节值为ff­ff­ff­ff­ff­ff。

  多播包:判断目的 IP 地址是不是 D 类 IP 地址,如果是,则 MAC 地址可以直接计算得出,即将 MAC 地址 01­00­5E­00­00­00 的低 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; //返回函数操作结果
posted @ 2018-01-29 11:24  丢崽  阅读(6327)  评论(0编辑  收藏  举报