LwIP协议栈之ARP(Address Resolution Protocal)协议详解

  • Post author:
  • Post category:其他




前言

ARP,全称Address Resolution Protocol,译作地址解析协议,是位于 TCP/IP 协议栈底层的协议。

任何网络的通信都是基于底层硬件链路的,底层的数据链路有着自己的一套寻址机制,在以太网中,往往是通过一个48位的MAC地址来标示不同的网络通信设备的。

TCP/IP协议的上层是使用IP地址作为各个主机间通信寻址机制的。


ARP作用

:当源主机上层要向目标主机发送数据时,它只知道目标主机的IP地址,此时

源主机需要将该IP地址转换为目的主机对应的MAC地址

,这样才能在数据链路上选择正确的通道将数据传送出去。



ARP协议简介

在ARP背后有一个基本概念,就是每个网络接口有一个硬件地址(一个48位的值,标识不同的以太网或令牌环网络接口),在硬件层次上进行的数据帧交换必须有正确的硬件接口地址。

知道主机的 I P 地址并不能让内核发送一帧数据给主机。内核(如以太网驱动程序)必

须知道目的端的硬件地址才能发送数据。

  • ARP的功能:在32位的IP地址和采用不同网络技术的硬件地址之间提供动态映射。ARP协议的基本功能就是通过目标设备的IP地址,查询目标设备的MAC地址,以保证通信的进行。
  • ARP协议实现的核心是

    ARP缓存表

    ,ARP的实质就是对对缓存表的建立、更新、查询等操作。
  • ARP缓存表是由一个个的缓存表项(entry)组成的,LWIP中描述缓存表项的数据结构叫etharp_entry,源代码如下:
struct etharp_entry {
#if ARP_QUEUEING
	struct etharp_q_entry *q; // 数据包缓冲队列指针
#endif
	struct ip_addr ipaddr; // 目标 IP 地址
	struct eth_addr ethaddr; // MAC 地址
	enum etharp_state state; // 描述该 entry 的状态
	u8_t ctime; // 描述该 entry 的时间信息
	struct netif *netif; // 相应网络接口信息
};

ARP_QUEUEING是编译选项,表示是否允许缓存表项有数据包缓冲队列,在opt.h里面设置。

其中state是个美剧类型,表示该缓存表项的状态,如下所示:

	enum etharp_state {
	ETHARP_STATE_EMPTY = 0,
	ETHARP_STATE_PENDING,
	ETHARP_STATE_STABLE
};

LWIP内核通过数组的方式来创建ARP缓存表,如下

static struct etharp_entry arp_table[ARP_TABLE_SIZE];



工作流程

  • 在初始状态下,每个表项都处于ETHARP_STATE_EMPTY 状态;
  • 当表项记录到了IP地址,还未记录到对应该IP地址的MAC地址,此时就处于ETHARP_STATE_PENDING 状态,发往该表项中IP地址处的数据包会被连接在表项对应的数据包缓冲队列上,此时LWIP 内核会发出一个广播 ARP 请求到数据链路上,以让对应 IP 地址的主机回应其 MAC 地址,当源主机接收到 MAC 地址时,它就更新对应的 ARP 表项。
  • 当 ARP 表项得到更新后,它就完全记录了一对 IP 地址和 MAC 地址,此时该表项就处于 ETHARP_STATE_STABLE 状态。
  • ctime字段记录表项处于某个状态的时间,当某表项的ctime值大于规定的表项最大生存值时,该表项会被内核删除。



ARP数据包

ARP数据包是正确建立ARP缓存的基础,ARP数据包可以分为

ARP请求数据包



ARP应答数据包

,ARP数据包到达底层链路时会被加上以太网数据包头发送出去,最终呈现在链路上的数据报头格式如下图。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xACHhjHO-1596272754674)(images/11.jpg)]



以太网数据报头

  • 以太网包头中的前两个字段是以太网的目的MAC地址和源MAC地址。目的地址为全1的特殊地址是广播地址。

    1. 在ARP表项建立前,源主机只知道目的主机的IP地址,并不知道其MAC地址,所以在数据链路上,源主机只有通过广播的方式将ARP请求数据包发送出去。
    2. 电缆上的所有以太网接口都要接收广播的数据包,并检测数据包是否是发给自己的,通过对照目的IP地址来实现
    3. 如果是发给自己的,目的主机需要回复一个ARP应答数据包给源主机,以告诉源主机自己的MAC地址。
  • 以太网帧类型表示后面数据的类型,对于ARP请求或应答数据包来说,该字段的值为0x0806,对于IP数据包来说,该字段的值为0x0800。



ARP数据报头

  • 硬件类型字段表示硬件地址的类型,它的值为1即表示以太网MAC地址,长度为6个字节。协议类型字段表示要映射的协议地址类型。它的值与包含IP数据报的以太网数据帧头中的类型字段的值相同。
  • 两个1字节的字段,意见地址长度和协议地址长度分别指出硬件地址和协议地址的长度,以字节为单位。对于以太网上 ARP 请求或应答来说,它们的值分别为 6 和 4。
  • 操作字段op指出四种操作类型,它们是ARP请求(值为1)、ARP应答(值为2)、RARP请求(值为3)和RARP应答(值为4)。
  • 接下来的四个字段是发送端的以太网 MAC 地址、发送端的 I P 地址、目的端的以太网MAC 地址和目的端的IP地址。


请求和应答过程描述

  • 在以太网的数据帧报头中和 A R P 请求数据帧中都有发送端的以太网 MAC 地址
  • 对于一个 ARP 请求来说,除目的端 MAC 地址外的所有其他的字段都有填充值。
  • 当目的主机收到一份给自己的 ARP 请求报文后,它就把自己的硬件地址填进去
  • 将该请求数据包的源主机信息和目的主机信息交换位置,并把操作字段 op 置为 2,最后把该新构建的数据包发送回去,这就是** ARP 响应**。

源代码如下:

struct etharp_hdr {
	PACK_STRUCT_FIELD(struct eth_hdr ethhdr); // 14 字节的以太网数据报头
	PACK_STRUCT_FIELD(u16_t hwtype); // 2 字节的硬件类型
	PACK_STRUCT_FIELD(u16_t proto); // 2 字节的协议类型
	PACK_STRUCT_FIELD(u16_t _hwlen_protolen); // 两个 1 字节的长度字段
	PACK_STRUCT_FIELD(u16_t opcode); // 2 字节的操作字段 op
	PACK_STRUCT_FIELD(struct eth_addr shwaddr); // 6 字节源 MAC 地址
	PACK_STRUCT_FIELD(struct ip_addr2 sipaddr); // 4 字节源 IP 地址
	PACK_STRUCT_FIELD(struct eth_addr dhwaddr); // 6 字节目的 MAC 地址
	PACK_STRUCT_FIELD(struct ip_addr2 dipaddr); // 4 字节目的 IP 地址
} PACK_STRUCT_STRUCT;



ARP表查询

下面主要介绍ARP表的创建,更新和查询等操作。



重要函数介绍

第一个函数是find_entry,该函数最重要的输入是一个IP地址,返回值是该IP地址对应的ARP缓存表项索引。函数声明原型如下,

static s8_t find_entry(struct ip_addr *ipaddr, u8_t flags)
  • 主要功能:寻找一个匹配的ARP表项或者创建一个新的ARP表项,并返回该表项的索引号,若参数ipaddr为给定的非空的的内容,则函数需要返回一个处于pending或stable的索引表项;若没有匹配的表项,则该函数需要返回一个empty表项。
  • 工作流程:

    1. 首先找到上次的表项索引是否为要找的内容,如果是则直接返回该索引号
    2. 如果不是,则必须检索整个ARP表。对于每个表项首先判断它是否为empty状态,find_entry只关心第一个状态为empty的表项索引值。如果一个表项不是empty状态,则判断它是不是pending状态。
    3. 如果匹配,则返回该索引值,更新etharp_cached_entry 为该索引值,若不匹配,则判断该索引的数据包指针是否为空
    4. 如果一个表项也不是pending状态,则判断是不是stable状态
    5. 如果最终还是没有找到匹配的表项,就需要为find_entry调用者返回一个empty的表项索引。

另一个重要的函数是 etharp_query,该函数的功能是向给定的 IP 地址发送一个数据包或者发送一个 ARP 请求。

etharp_query 函数原型如下所示

err_t etharp_query(struct netif *netif, struct ip_addr *ipaddr, struct pbuf *q)
  • 工作流程:

    1. 首先判断给定的ipaddr是否合法,对于空IP地址、广播IP地址、多播IP地址不予处理
    2. 将ipaddr作为参数调用函数find_entry,函数返回一个ARP表项索引,该表项可能是原来已经有的,此时该表项应该是pending或stable状态;该表项也可能是新申请得到的,此时该表项应该是 empty 状态。
    3. 根据返回的表项索引找到该ARP表项,判断该表项是否为 empty 状态,如果是,说明该表项是新申请的,则将该表项状态设置为 pending 状态。
    4. 判断要发送的数据包是否为空,或者判断ARP表项是否为pending状态,这两个条件只要有一个成立,就发送一个ARP请求出去,发送ARP请求的函数是etharp_request。
    5. 如果待发送的数据包不为空,就根据 ARP 表项的状态作不同的处理:若 ARP 表项处于 stable 状态,则直接调用函数 etharp_send_ip 发送数据包;若 ARP 表项处于 pending状态,则需要将该数据包挂接到表项的待发送数据链表上,由于 pending 状态的表项必然在第(4)步中发出了一个 ARP 请求,当内核接收到 ARP 回应时,会将表项设置为 stable状态,并将其链表上的数据全部发送出去。



ARP传输流程

关于ARP的传输过程,就需要介绍一个函数update_arp_entry,该函数用于更新ARP缓存表中的表项或者在缓存表中插入一个新的表项。该函数会在收到一个IP数据包或ARP数据包后被调用。

源代码如下所示:

static err_t
update_arp_entry(struct netif *netif, struct ip_addr *ipaddr, struct eth_addr *ethaddr, u8_t flags)
{
	s8_t i; // 两个变量,不解释
	u8_t k;
	i = find_entry(ipaddr, flags); // 查找或新建一个 ARP 表项,返回其索引值
	if (i < 0) return (err_t)i; // 如果为不合法的索引值,则更新缓存表失败
	else
	{
		arp_table[i].state = ETHARP_STATE_STABLE; // 否则将对应表项状态改为 stable
		arp_table[i].netif = netif; // 记录下网络接口
	}
	
	k = ETHARP_HWADDR_LEN; // 这一段是更新缓存表项中的 MAC 地址
	while (k > 0) {
		k--;
		arp_table[i].ethaddr.addr[k] = ethaddr->addr[k];
	}
	arp_table[i].ctime = 0; // 生存时间值置 0
	#if ARP_QUEUEING //该 ARP 表项上有未发送的队列,则把这些队列发送出去
	while (arp_table[i].q != NULL) { // 只要缓冲链表中海有数据则循环
		struct pbuf *p;
		struct etharp_q_entry *q = arp_table[i].q; // 记录下缓冲链表表头
		arp_table[i].q = q->next; // 缓冲链表表头指向下一个节点
		p = q->p; // 取得记录下的缓冲链表表头指向的数据包
		memp_free(MEMP_ARP_QUEUE, q); // 释放记录下的缓冲链表表头
		etharp_send_ip(netif, p, (struct eth_addr*)(netif->hwaddr), ethaddr); // 发送数据包
		pbuf_free(p); // 释放数据包缓存空间
	}
#endif
	return ERR_OK;
}

LWIP的数据包接收与发送的全过程入下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nYvILGJS-1596272754677)(images/13.png)]

  • ARP的功能:

    1. 当有数据包输入时,更新arp表,如果是ip包则递交给ip层,如果是arp包,则针对不同的arp包类型做相应的响应;
    2. 当向目的ip发送一个数据包的时候,需要通过arp实现ip到MAC地址的映射,必要时需要发送广播数据包获得目标机器的MAC地址。
  • LWIP利用netif.input指向的函数

    接收以太网数据包

    ,通常这个函数为ethernet_input,更底层的函数接收到数据包后将数据包递交给ethernet_input,ethernet_input再对其进行处理。

  • 以太网的帧类型可以是:IP, ARP,甚至是pppoe,wlan等。ethernet_input 根据以太网首部的类型字段判断收到的数据包的类型,分别递交给图中两个函数。

  • 对于ip类型的数据包,etharp_ip_input首先检查是否开启了ETHARP_TRUST_IP_MAC,若开启了就要用这个帧中的信息和update_arp_entry函数来更新arp表,然后丢弃以太网帧首部,将IP报文通过ip_input函数递交给ip层。

  • 对于ARP类型的数据包,etharp_arp_input函数首先利用数据包头信息更新arp表的内容,然后再判断该ARP数据包的类型,

    如果是 ARP 请求包

    ,则首先判断这个包是不是给自己的,如果是给自己的,则在原有包的基础上重组一个 ARP 应答包发送出去,否则直接忽略。

    如果是ARP应答包

    ,主要工作就是更新arp表,但这一步已经在arp包刚进来的时候处理完了,故不需要重复做。

  • LWIP利用netif.output指向的函数发送ip数据包,函数为etharp_output。主要是将IP数据包打包成以太网帧数据,递交给netif.linkoutput 函数来发送;

  • etharp_output函数接收IP层要发送的数据包,并将数据包发送出去由于是发送 ip 数据包,所以函数一开始需要增加缓冲区大小,大小为以太网的数据首部的大小。然后检查 ip地址,可以分为广播包,多播包,单播包(单播包又分为是局域网内部还是局域网外面)。



参考资料

老衲五木:TCP/IP协议栈LwIP的设计与实现



版权声明:本文为XMJYever原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。