嵌入式技术论坛
直播中

百灵千岛酱

8年用户 527经验值
擅长:处理器/DSP
私信 关注
[经验]

掌握LwIP中ARP的实现原理与作用

1. ARP 的背景
对于网络世界来说,有 IP 地址就代表了身份。不过在我们常用的网络拓扑类型中,IP 地址并不能准确表达我们的身份。在 ipv4 中牵扯到私有网络地址与子网划分,加上交换机,路由器设备的存在,这些极大扩展了可用的 IP 地址数量,在设备的 MAC 地址与 IP 地址的共同作用下使得更多的设备能连接到网络中。ipv6 拥有极大数量的 IP 地址,同时就没有 ARP 报文,但是会有一个其他类似的功能。
在实际使用中,ARP (Address Resolution Protocol)地址解析协议 就起到了沟通 IP 地址与 MAC 地址的作用。
ARP 的报文比较简单,就是两个功能:ARP request,ARP response;即一个 ARP 查询报文,一个 ARP 回复报文。
学习目标:
掌握 ARP 报文的作用。
掌握 lwip 中 ARP 的实现原理。
掌握 lwip 的 ARP 策略,能简单修改原生逻辑。
2. ARP 报文格式
ARP 报文与 IP 报文都是附着在 ETH 帧之上,可以看到 ARP 报文长度共有 28 字节;包含的内容包括【发送端】与【接收端】的【以太网地址】与【IP 地址】。
以太网目的地址,要发送到的地址,对于不知道的地址,可以全部置为 1;
以太网源地址,也就是发送端的地址;
帧类型 0x0806,表示为 ARP 报文;0x0800 表示 IP 报文;对于 ARP 报文,”帧类型”的实际上与”协议类型”字段是同一个数据;
硬件类型,两个字节, 1 表示硬件地址;当然实际上,“ 0, 2, 3 “都没有太大意义;
协议类型,与帧类型是一致的;在数据填充时,可以把 “协议类型” 拷贝到 “帧类型” 字段;
硬件地址长度,6 个字节,再长也是 6 个字节;
协议地址长度,4个字节,别多想 ipv4 就是 4 个字节,ipv6 也不用这一套;
op,2 个字节; 1 表示 ARP_REQUEST,2 表示 ARP_REPLY;
发送端以太网地址,意义如名称;
发送端 IP 地址,意义如名称;
目的以太网地址,意义如名称,不知道就置空;
目的 IP 地址,意义如名称;
ARP 只有两个报文类型:request 与 response ( reply ) 。
是在知道 IP 地址的基础上,使用 request 查询该 IP 地址可以使用的 MAC 地址,而不是反过来。
一旦收到 ARP 请求,发现该 IP 与本机 IP 地址相符就回复 response ,如果不是就忽略。
为了能维护一个 MAC 与 IP 的映射表,会有一个 ARP 缓存表的存储结构;ETH 帧的收发都会尝试更新这个 ARP 缓存表。
ARP 报文的收发解析的主要功能就是维护这个 ARP 缓存表,以便在发送 IP 报文时可以直接填充 【以太网目的地址】与【以太网源地址】。
3. lwip 对 ARP 功能的实现
根据上述的 ARP 报文,就该意识到有两个比较重要的结构体需要实现:ETH 报文的结构体,以及 ARP 报文的结构体。
/** struct eth_addr and ip4_addr2 */
#define ETH_HWADDR_LEN 6
#define PACK_STRUCT_FIELD(x) x
#define PACK_STRUCT_FLD_8(x) PACK_STRUCT_FIELD(x)
#define PACK_STRUCT_FLD_S(x) PACK_STRUCT_FIELD(x)
struct eth_addr {
PACK_STRUCT_FLD_8(u8_t addr[ETH_HWADDR_LEN]);
} PACK_STRUCT_STRUCT;
struct ip4_addr2 {
PACK_STRUCT_FIELD(u16_t addrw[2]);
} PACK_STRUCT_STRUCT;
/** Ethernet header */
struct eth_hdr {
#if ETH_PAD_SIZE
PACK_STRUCT_FLD_8(u8_t padding[ETH_PAD_SIZE]);
#endif
PACK_STRUCT_FLD_S(struct eth_addr dest);
PACK_STRUCT_FLD_S(struct eth_addr src);
PACK_STRUCT_FIELD(u16_t type);
} PACK_STRUCT_STRUCT;
/** the ARP message, see RFC 826 ("Packet format") */
struct etharp_hdr {
PACK_STRUCT_FIELD(u16_t hwtype);
PACK_STRUCT_FIELD(u16_t proto);
PACK_STRUCT_FLD_8(u8_t hwlen);
PACK_STRUCT_FLD_8(u8_t protolen);
PACK_STRUCT_FIELD(u16_t opcode);
PACK_STRUCT_FLD_S(struct eth_addr shwaddr);
PACK_STRUCT_FLD_S(struct ip4_addr2 sipaddr);
PACK_STRUCT_FLD_S(struct eth_addr dhwaddr);
PACK_STRUCT_FLD_S(struct ip4_addr2 dipaddr);
} PACK_STRUCT_STRUCT;
以上的结构是为了能方便得按照报文格式来填充数据,还需要一个用来管理 ARP 映射表的结构;
/** struct for queueing outgoing packets for unknown address
* defined here to be accessed by memp.h
*/
struct etharp_q_entry {
struct etharp_q_entry *next;
struct pbuf *p;
};
struct etharp_entry {
#if ARP_QUEUEING
/** Pointer to queue of pending outgoing packets on this ARP entry. */
struct etharp_q_entry *q;
#else /* ARP_QUEUEING */
/** Pointer to a single pending outgoing packet on this ARP entry. */
struct pbuf *q;
#endif /* ARP_QUEUEING */
ip4_addr_t ipaddr;
struct netif *netif;
struct eth_addr ethaddr;
u16_t ctime;
u8_t state;
};
static struct etharp_entry arp_table[ARP_TABLE_SIZE];
OK,描述到这里,lwip 对 ARP 报文的支持所需要的基础数据结构已经完备了;但是除了数据结构,也应当有对应的算法才能实现 TCP/IP 所要求的规范,即实现 ARP 程序。我们先不看具体实现,先通过我们上面对 ARP 功能的描述,先猜测一下应该需要什么功能:
发送数据时,根据 ARP 表填充 ETH 帧的地址。
如果 ARP 表没有对应的 IP 与 MAC 地址的条目,应当发送 ARP request 来查询。
在收到 ARP response 时,应当把报文中的 IP 与 MAC 地址添加到 ARP表中。
应当提供一个查询 ARP 表的功能,并能更新 ARP 映射关系。
3.1 lwip 的 ARP 缓存表维护
上面的函数已经能完成对 ARP 数据包的发送,解析,查找功能;因为一个网络中,设备不可能是一直都不会变的。比如 DHCP 给你分配了一个 IP ,在你不使用后会收回这些资源,也许会分配给其他人;也就是说,IP 与 MAC 的对应关系,是会随着时间改变而改变的。因此,lwip 维护的 ARP 表是需要频繁更新的。
而为了表示 ARP 缓存表中,IP 与 ARP 的对应关系(后面称为一个表项),每个表项都会有个 state 来表示状态,也就是上面的 u8 state 字段。lwip 定义的表项状态共有 6 种,当然是在你支持静态 ARP 映射表的情况下。
/** ARP states */
enum etharp_state {
ETHARP_STATE_EMPTY = 0, /* 空表 */
ETHARP_STATE_PENDING, /* 只记录了 IP ,而没有记录 MAC,一般是发送了 arp_request 的短暂状态 */
ETHARP_STATE_STABLE, /* 记录了 IP 和 MAC */
ETHARP_STATE_STABLE_REREQUESTING_1, /* stable 状态下,ctime 继续增大引发广播或者单播一个 arp_request ,并置为 REREQUESTING_1 */
ETHARP_STATE_STABLE_REREQUESTING_2 /* 已经发送了 arp_request 但是没有回复,会从 REREQUESTING_1 置为 REREQUESTING_2 */
#if ETHARP_SUPPORT_STATIC_ENTRIES
,ETHARP_STATE_STATIC /* 静态表,不会别 ARP 管理修改的条目 */
#endif /* ETHARP_SUPPORT_STATIC_ENTRIES */
};
这些表项的状态,都会影响 ARP 缓存表的更新;而更新策略与上面的一些函数的执行逻辑有关,比较重要的逻辑是:
pending 状态下,对应的 pbuf 不会被发出,只有 >= stable 才会被发出。
pending 状态下,ctime 超时会直接导致条目被删除,置为 empty。
stable 状态下, ctime 超时会被字节置为 pending 。
stable 状态下,会定时检查 ctime ,在达到一定阀值时会通过广播或者单播发送一个 arp_request,并被置为 rerequesting_1 状态。
在 arp_tmr 触发时,如果已经为 rerequesting_1 状态,会被置为 rerequesting_2。
rerequesting_1 与 rerequesting_2 到底什么意义:避免连续(指在多次)发送 arp_request 而设计的,也许是怕网络环境干扰。
3.2 lwip 提供的 ARP 功能函数
我们通过分析已经获悉,ARP 程序肯定要具备上述 4 种功能;不过,在 Lwip 的实际实现中,肯定不止这 4 种基础功能,肯定要有更多的函数来实现这个目的。关于 lwip 的 ARP 内容的代码,是存放在 lwip/src/core/ipv4/etharp.c 中。
为了梳理 ARP 的功能,我们选择其中的 5 个函数来学习 lwip 的 ARP 功能实现:
/* 查找 ARP 映射表中 ipaddr 条目,并返回相对位置 */
static s8_t etharp_find_entry(const ip4_addr_t *ipaddr, u8_t flags, struct netif* netif);
/* 尝试发送 IP 数据包, 单播,多播或者广播数据 */
err_t etharp_output(struct netif *netif, struct pbuf *q, const ip4_addr_t *ipaddr);
/* 查询 ARP 表,不存在则调用 etharp_request 获取,存在则发送数据 */
err_t etharp_query(struct netif *netif, const ip4_addr_t *ipaddr, struct pbuf *q);
/* 尝试发送 ARP 请求数据包 */
err_t etharp_request(struct netif *netif, const ip4_addr_t *ipaddr);
/* 解析 ARP 数据包 */
void etharp_input(struct pbuf *p, struct netif *netif);
其中,最难以理解得是 etharp_find_entry 与 etharp_query 函数,对于这两个函数我们主要分析其意义;剩下的 3 个函数,我们则具体看看代码的实现。通过这 5 个比较有代表性的函数,我们来理解一个表项中的 state ,ctime,p 这些值究竟怎样影响整个 ARP 报文的发送与解析策略。
3.2.1 函数 etharp_find_entry
该函数位于 lwip/src/core/ipv4/etharp.c 中,这个函数的作用可以通过注释来描述:
/**
* Search the ARP table for a matching or new entry.
*
* If an IP address is given, return a pending or stable ARP entry that matches
* the address. If no match is found, create a new entry with this address set,
* but in state ETHARP_EMPTY. The caller must check and possibly change the
* state of the returned entry.
*
* If ipaddr is NULL, return a initialized new entry in state ETHARP_EMPTY.
*
* In all cases, attempt to create new entries from an empty entry. If no
* empty entries are available and ETHARP_FLAG_TRY_HARD flag is set, recycle
* old entries. Heuristic choose the least important entry for recycling.
*
* @param ipaddr IP address to find in ARP cache, or to add if not found.
* @param flags See @ref etharp_state
* @param netif netif related to this address (used for NETIF_HWADDRHINT)
*
* @Return The ARP entry index that matched or is created, ERR_MEM if no
* entry is found or could be recycled.
*/
通过上面的描述,我们能知道 etharp_find_entry 的策略如下:
如果参数中 IP 地址被给定,则返回一个 stable 或者 pending 状态的表项;
如果 IP 地址被给定,但是没有处于 stable 或者 pending 状态的表项,则会找一个 empty 的表项并返回;
如果 IP 地址为空,则会创建一个 empty 的表项并返回;
如果很不巧,ARP 表中没有处于 empty 的表项;在 flag 为 ETHARP_FLAG_TRY_HARD 的情况下,找一个最不重要的表项置空并返回;
ETHARP_FLAG_FIND_ONLY 与 ETHARP_FLAG_TRY_HARD 的区别,就是前者尽量找到一个表项;后者必须找到一个表项(哪怕是清空之前已经存在的);
上面的函数描述中有一个黑洞,ETHARP_FLAG_TRY_HARD 必须找到一个表项,哪怕是通过回收一个最不重要的表项的方式;那最不重要的表项是什么表项呢,我们可以一起看看代码
/* b) choose the least destructive entry to recycle: // 1 -> 4, 优先级就是这样
* 1) empty entry
* 2) oldest stable entry
* 3) oldest pending entry without queued packets
* 4) oldest pending entry with queued packets
*
* { ETHARP_FLAG_TRY_HARD is set at this point }
*/
/* 1) empty entry available? */
if (empty < ARP_TABLE_SIZE) {
i = empty;
LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE, ("etharp_find_entry: selecting empty entry %"U16_F"
", (u16_t)i));
} else {
/* 2) found recyclable stable entry? */
if (old_stable < ARP_TABLE_SIZE) {
/* recycle oldest stable*/
i = old_stable;
LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE, ("etharp_find_entry: selecting oldest stable entry %"U16_F"
", (u16_t)i));
/* no queued packets should exist on stable entries */
LWIP_ASSERT("arp_table.q == NULL", arp_table.q == NULL);
/* 3) found recyclable pending entry without queued packets? */
} else if (old_pending < ARP_TABLE_SIZE) {
/* recycle oldest pending */
i = old_pending;
LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE, ("etharp_find_entry: selecting oldest pending entry %"U16_F" (without queue)
", (u16_t)i));
/* 4) found recyclable pending entry with queued packets? */
} else if (old_queue < ARP_TABLE_SIZE) {
/* recycle oldest pending (queued packets are free in etharp_free_entry) */
i = old_queue;
LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE, ("etharp_find_entry: selecting oldest pending entry %"U16_F", freeing packet queue %p
", (u16_t)i, (void *)(arp_table.q)));
/* no empty or recyclable entries found */
} else {
LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE, ("etharp_find_entry: no empty or recyclable entries found
"));
return (s8_t)ERR_MEM;
}
/* { empty or recyclable entry found } */
LWIP_ASSERT("i < ARP_TABLE_SIZE", i < ARP_TABLE_SIZE);
etharp_free_entry(i);
}
我在这里解释一下,为什么会是 1->4 这个顺序:
空表项与最老的 state 最先被使用的原因,就是因为 ARP 表是需要随时更新的,太老的表项可能本身就是错误的或者过时的,因此先删除是没有问题的。
处于 pending 状态且没有待发送数据的表项与处于 pending 状态但是有待发送数据的表项相比;有待发送数据的表项上挂载了一些数据,有可能是 PING 命令之类或者其他的数据,删除这些是比较危险的;而没有待发送数据的表项就没有这个顾虑。
3.2.2 函数 etharp_query
该函数位于 lwip/src/core/ipv4/etharp.c 中,这个函数其实是一些策略的合集:
/**
* Send an ARP request for the given IP address and/or queue a packet.
*
* If the IP address was not yet in the cache, a pending ARP cache entry
* is added and an ARP request is sent for the given address. The packet
* is queued on this entry.
*
* If the IP address was already pending in the cache, a new ARP request
* is sent for the given address. The packet is queued on this entry.
*
* If the IP address was already stable in the cache, and a packet is
* given, it is directly sent and no ARP request is sent out.
*
* If the IP address was already stable in the cache, and no packet is
* given, an ARP request is sent out.
*
* @param netif The lwIP network interface on which ipaddr
* must be queried for.
* @param ipaddr The IP address to be resolved.
* @param q If non-NULL, a pbuf that must be delivered to the IP address.
* q is not freed by this function.
*
* @NOTE q must only be ONE packet, not a packet queue!
*
* @return
* - ERR_BUF Could not make room for Ethernet header.
* - ERR_MEM Hardware address unknown, and no more ARP entries available
* to query for address or queue the packet.
* - ERR_MEM Could not queue packet due to memory shortage.
* - ERR_RTE No route to destination (no gateway to external networks).
* - ERR_ARG Non-unicast address given, those will not appear in ARP cache.
*
*/
其实,顺着代码看下来,策略也很清晰:
如果不能找到表项,那没有办法,只能一个新的 empty 表项,发送一个 arp_request 并置为 pending;如果 pbuf 中有数据继续执行,没有直接返回。
如果找到表项,那最好,直接发送数据就行了(这里指真实的 IP 数据,而不是 ARP 数据),然后返回。
如果处于 pending 状态(其实走到这一步,empty 也已经是 pending 了),而且 pbuf 中有数据,那就挂载了对应的 pbuf 链表上,等待发送;这里有一些拷贝的动作,只要 pbuf 链表中有不是 PBUF_ROM 的的节点,整个 pbuf 数据都需要拷贝到新的数据链中,谨防被意外修改。
3.2.3 函数 etharp_request
const struct eth_addr ethbroadcast = {{0xff,0xff,0xff,0xff,0xff,0xff}};
const struct eth_addr ethzero = {{0,0,0,0,0,0}};
/**
* Send a raw ARP packet (opcode and all addresses can be modified)
*
* @param netif the lwip network interface on which to send the ARP packet
* @param ethsrc_addr the source MAC address for the ethernet header
* @param ethdst_addr the destination MAC address for the ethernet header
* @param hwsrc_addr the source MAC address for the ARP protocol header
* @param ipsrc_addr the source IP address for the ARP protocol header
* @param hwdst_addr the destination MAC address for the ARP protocol header
* @param ipdst_addr the destination IP address for the ARP protocol header
* @param opcode the type of the ARP packet
* @return ERR_OK if the ARP packet has been sent
* ERR_MEM if the ARP packet couldn't be allocated
* any other err_t on failure
*/
static 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 ip4_addr_t *ipsrc_addr,
const struct eth_addr *hwdst_addr, const ip4_addr_t *ipdst_addr,
const u16_t opcode);
/**
* Send an ARP request packet asking for ipaddr to a specific eth address.
* Used to send unicast request to refresh the ARP table just before an entry
* times out
*
* @param netif the lwip network interface on which to send the request
* @param ipaddr the IP address for which to ask
* @param hw_dst_addr the ethernet address to send this packet to
* @return ERR_OK if the request has been sent
* ERR_MEM if the ARP packet couldn't be allocated
* any other err_t on failure
*/
static err_t
etharp_request_dst(struct netif *netif, const ip4_addr_t *ipaddr, const struct eth_addr* hw_dst_addr)
{
return etharp_raw(netif, (struct eth_addr *)netif->hwaddr, hw_dst_addr,
(struct eth_addr *)netif->hwaddr, netif_ip4_addr(netif), ðzero,
ipaddr, ARP_REQUEST);
}
/**
* Send an ARP request packet asking for ipaddr.
*
* @param netif the lwip network interface on which to send the request
* @param ipaddr the IP address for which to ask
* @return ERR_OK if the request has been sent
* ERR_MEM if the ARP packet couldn't be allocated
* any other err_t on failure
*/
err_t
etharp_request(struct netif *netif, const ip4_addr_t *ipaddr)
{
LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE, ("etharp_request: sending ARP request.
"));
return etharp_request_dst(netif, ipaddr, ðbroadcast);
}
etharp_request 已经是一个功能函数了,功能函数就表明很容易看懂。OK,我们看看到底干了什么。
首先 eth 帧的广播地址是全 1 的,再使用 etharp_raw 函数发送时,arp 协议中的 hw 字段,全部设置为 0;这里是协议要求,无可厚非。
可以看到 lwip 这里确实是发送了一个 arp_request 的数据包出去。
3.2.4 函数 etharp_input
/**
* Responds to ARP requests to us. Upon ARP replies to us, add entry to cache
* send out queued IP packets. Updates cache with snooped address pairs.
*
* Should be called for incoming ARP packets. The pbuf in the argument
* is freed by this function.
*
* @param p The ARP packet that arrived on netif. Is freed by this function.
* @param netif The lwIP network interface on which the ARP packet pbuf arrived.
*
* @see pbuf_free()
*/
void
etharp_input(struct pbuf *p, struct netif *netif)
{
struct etharp_hdr *hdr;
/* these are aligned properly, whereas the ARP header fields might not be */
ip4_addr_t sipaddr, dipaddr;
u8_t for_us;
LWIP_ERROR("netif != NULL", (netif != NULL), return;);
hdr = (struct etharp_hdr *)p->payload;
/* RFC 826 "Packet Reception": */
if ((hdr->hwtype != PP_HTONS(HWTYPE_ETHERNET)) ||
(hdr->hwlen != ETH_HWADDR_LEN) ||
(hdr->protolen != sizeof(ip4_addr_t)) ||
(hdr->proto != PP_HTONS(ETHTYPE_IP))) {
LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE | LWIP_DBG_LEVEL_WARNING,
("etharp_input: packet dropped, wrong hw type, hwlen, proto, protolen or ethernet type (%"U16_F"/%"U16_F"/%"U16_F"/%"U16_F")
",
hdr->hwtype, (u16_t)hdr->hwlen, hdr->proto, (u16_t)hdr->protolen));
ETHARP_STATS_INC(etharp.proterr);
ETHARP_STATS_INC(etharp.drop);
pbuf_free(p);
return;
}
ETHARP_STATS_INC(etharp.recv);
#if LWIP_AUTOIP
/* We have to check if a host already has configured our random
* created link local address and continuously check if there is
* a host with this IP-address so we can detect collisions */
autoip_arp_reply(netif, hdr);
#endif /* LWIP_AUTOIP */
/* Copy struct ip4_addr2 to aligned ip4_addr, to support compilers without
* structure packing (not using structure copy which breaks strict-aliasing rules). */
IPADDR2_COPY(&sipaddr, &hdr->sipaddr);
IPADDR2_COPY(&dipaddr, &hdr->dipaddr);
/* this interface is not configured? */
if (ip4_addr_isany_val(*netif_ip4_addr(netif))) {
for_us = 0;
} else {
/* ARP packet directed to us? */
for_us = (u8_t)ip4_addr_cmp(&dipaddr, netif_ip4_addr(netif));
}
/* ARP message directed to us?
-> add IP address in ARP cache; assume requester wants to talk to us,
can result in directly sending the queued packets for this host.
ARP message not directed to us?
-> update the source IP address in the cache, if present */
etharp_update_arp_entry(netif, &sipaddr, &(hdr->shwaddr),
for_us ? ETHARP_FLAG_TRY_HARD : ETHARP_FLAG_FIND_ONLY);
/* now act on the message itself */
switch (hdr->opcode) {
/* ARP request? */
case PP_HTONS(ARP_REQUEST):
/* ARP request. If it asked for our address, we send out a
* reply. In any case, we time-stamp any existing ARP entry,
* and possibly send out an IP packet that was queued on it. */
LWIP_DEBUGF (ETHARP_DEBUG | LWIP_DBG_TRACE, ("etharp_input: incoming ARP request
"));
/* ARP request for our address? */
if (for_us) {
/* send ARP response */
etharp_raw(netif,
(struct eth_addr *)netif->hwaddr, &hdr->shwaddr,
(struct eth_addr *)netif->hwaddr, netif_ip4_addr(netif),
&hdr->shwaddr, &sipaddr,
ARP_REPLY);
/* we are not configured? */
} else if (ip4_addr_isany_val(*netif_ip4_addr(netif))) {
/* { for_us == 0 and netif->ip_addr.addr == 0 } */
LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE, ("etharp_input: we are unconfigured, ARP request ignored.
"));
/* request was not directed to us */
} else {
/* { for_us == 0 and netif->ip_addr.addr != 0 } */
LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE, ("etharp_input: ARP request was not for us.
"));
}
break;
case PP_HTONS(ARP_REPLY):
/* ARP reply. We already updated the ARP cache earlier. */
LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE, ("etharp_input: incoming ARP reply
"));
#if (LWIP_DHCP && DHCP_DOES_ARP_CHECK)
/* DHCP wants to know about ARP replies from any host with an
* IP address also offered to us by the DHCP server. We do not
* want to take a duplicate IP address on a single network.
* @todo How should we handle redundant (fail-over) interfaces? */
dhcp_arp_reply(netif, &sipaddr);
#endif /* (LWIP_DHCP && DHCP_DOES_ARP_CHECK) */
break;
default:
LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE, ("etharp_input: ARP unknown opcode type %"S16_F"
", lwip_htons(hdr->opcode)));
ETHARP_STATS_INC(etharp.err);
break;
}
/* free ARP packet */
pbuf_free(p);
}
可以看到,一旦 ARP 报文到达,都会被这个函数所判断执行;逻辑是这样的:
如果收到的是给自己的 ARP request ,就回复一个 ARP reply 报文;如果不是给自己的,记录一些有用 ARP 的信息后,就忽略掉。
如果是给自己的 ARP reply 报文,会有一些逻辑与 DHCP 过程有关,可以在 DHCP 章节查看。
3.2.5 函数 etharp_output
/**
* Resolve and fill-in Ethernet address header for outgoing IP packet.
*
* For IP multicast and broadcast, corresponding Ethernet addresses
* are selected and the packet is transmitted on the link.
*
* For unicast addresses, the packet is submitted to etharp_query(). In
* case the IP address is outside the local network, the IP address of
* the gateway is used.
*
* @param netif The lwIP network interface which the IP packet will be sent on.
* @param q The pbuf(s) containing the IP packet to be sent.
* @param ipaddr The IP address of the packet destination.
*
* @return
* - ERR_RTE No route to destination (no gateway to external networks),
* or the return type of either etharp_query() or ethernet_output().
*/
err_t
etharp_output(struct netif *netif, struct pbuf *q, const ip4_addr_t *ipaddr)
{
const struct eth_addr *dest;
struct eth_addr mcastaddr;
const ip4_addr_t *dst_addr = ipaddr;
LWIP_ASSERT("netif != NULL", netif != NULL);
LWIP_ASSERT("q != NULL", q != NULL);
LWIP_ASSERT("ipaddr != NULL", ipaddr != NULL);
/* Determine on destination hardware address. Broadcasts and multicasts
* are special, other IP addresses are looked up in the ARP table. */
/* broadcast destination IP address? */
if (ip4_addr_isbroadcast(ipaddr, netif)) {
/* broadcast on Ethernet also */
dest = (const struct eth_addr *)ðbroadcast;
/* multicast destination IP address? */
} else if (ip4_addr_ismulticast(ipaddr)) {
/* Hash IP multicast address to MAC address.*/
mcastaddr.addr[0] = LL_IP4_MULTICAST_ADDR_0;
mcastaddr.addr[1] = LL_IP4_MULTICAST_ADDR_1;
mcastaddr.addr[2] = LL_IP4_MULTICAST_ADDR_2;
mcastaddr.addr[3] = ip4_addr2(ipaddr) & 0x7f;
mcastaddr.addr[4] = ip4_addr3(ipaddr);
mcastaddr.addr[5] = ip4_addr4(ipaddr);
/* destination Ethernet address is multicast */
dest = &mcastaddr;
/* unicast destination IP address? */
} else {
s8_t i;
/* outside local network? if so, this can neither be a global broadcast nor
a subnet broadcast. */
if (!ip4_addr_netcmp(ipaddr, netif_ip4_addr(netif), netif_ip4_netmask(netif)) &&
!ip4_addr_islinklocal(ipaddr)) {
#if LWIP_AUTOIP
struct ip_hdr *iphdr = LWIP_ALIGNMENT_CAST(struct ip_hdr*, q->payload);
/* According to RFC 3297, chapter 2.6.2 (Forwarding Rules), a packet with
a link-local source address must always be "directly to its destination
on the same physical link. The host MUST NOT send the packet to any
router for forwarding". */
if (!ip4_addr_islinklocal(&iphdr->src))
#endif /* LWIP_AUTOIP */
{
#ifdef LWIP_HOOK_ETHARP_GET_GW
/* For advanced routing, a single default gateway might not be enough, so get
the IP address of the gateway to handle the current destination address. */
dst_addr = LWIP_HOOK_ETHARP_GET_GW(netif, ipaddr);
if (dst_addr == NULL)
#endif /* LWIP_HOOK_ETHARP_GET_GW */
{
/* interface has default gateway? */
if (!ip4_addr_isany_val(*netif_ip4_gw(netif))) {
/* send to hardware address of default gateway IP address */
dst_addr = netif_ip4_gw(netif);
/* no default gateway available */
} else {
/* no route to destination error (default gateway missing) */
return ERR_RTE;
}
}
}
}
#if LWIP_NETIF_HWADDRHINT
if (netif->addr_hint != NULL) {
/* per-PCB cached entry was given */
u8_t etharp_cached_entry = *(netif->addr_hint);
if (etharp_cached_entry < ARP_TABLE_SIZE) {
#endif /* LWIP_NETIF_HWADDRHINT */
if ((arp_table[etharp_cached_entry].state >= ETHARP_STATE_STABLE) &&
#if ETHARP_TABLE_MATCH_NETIF
(arp_table[etharp_cached_entry].netif == netif) &&
#endif
(ip4_addr_cmp(dst_addr, &arp_table[etharp_cached_entry].ipaddr))) {
/* the per-pcb-cached entry is stable and the right one! */
ETHARP_STATS_INC(etharp.cachehit);
return etharp_output_to_arp_index(netif, q, etharp_cached_entry);
}
#if LWIP_NETIF_HWADDRHINT
}
}
#endif /* LWIP_NETIF_HWADDRHINT */
/* find stable entry: do this here since this is a critical path for
throughput and etharp_find_entry() is kind of slow */
for (i = 0; i < ARP_TABLE_SIZE; i++) {
if ((arp_table.state >= ETHARP_STATE_STABLE) &&
#if ETHARP_TABLE_MATCH_NETIF
(arp_table.netif == netif) &&
#endif
(ip4_addr_cmp(dst_addr, &arp_table.ipaddr))) {
/* found an existing, stable entry */
ETHARP_SET_HINT(netif, i);
return etharp_output_to_arp_index(netif, q, i);
}
}
/* no stable entry found, use the (slower) query function:
queue on destination Ethernet address belonging to ipaddr */
return etharp_query(netif, dst_addr, q);
}
/* continuation for multicast/broadcast destinations */
/* obtain source Ethernet address of the given interface */
/* send packet directly on the link */
return ethernet_output(netif, q, (struct eth_addr*)(netif->hwaddr), dest, ETHTYPE_IP);
}
这个函数会被 IP 层的输出函数直接调用以发送数据;IP 层的数据会有广播,多播,单播的数据,所以在该函数中,处理方式也是不一样的。
对于 IP 地址是广播地址的,eth 的地址也要设置为广播;
对于 IP 地址是多播地址的,eth 的地址也要设置为多播;
对于 IP 地址是单播地址且本网段内的,那直接继续下一步的操作;
对于 IP 地址是单播地址但不是本网段的,也就是需要路由器的转发,eth 的地址要设置为网关的地址;
etharp_cached_entry 的作用应该是加快处理速度,如果连续的数据发送,每次发送都要检索一边 arp 表会比较浪费;cached 的参与可以明显提供速度。
如果是 ARP 缓存表中找不到表项,那啥也别说了,调用 etharp_query 挂载起来吧,等待后面有机会再发送吧。
3.3 ARP / IP 报文在 lwip 中的流向
通过上面的函数,我们能大概了解具体的 ARP 功能的实现;对于 IP 数据包在整个 lwip 的流向,可以通过下面这个图来了解。在看图之前,还需要理解在 lwip 提供的三个接口:
IP 报文输入接口:tcpip_input
IP 报文的输出接口:netif->output = etharp_output
ETH 帧的输出接口:netif->linkoutput = ethernetif_linkoutput;

4. 拓展
4.1 ARP 攻击的原理
名字听起来很厉害,实际上就是通过 ARP 报文来达到干扰服务的目的。ARP 的报文可以使得设备不能正常发送 IP 数据。
欺骗,劫持,基本是属于一类;如果你把该发往别人的 IP 地址的 ETH 帧拿走,就要在别人的机器里将对应的 ARP 缓存换为你的。这样的话,别人的设备可能就会有断网,丢包的现象;毕竟,你这都没有提供服务,别人不掉线才怪。
DOS 攻击,也是一类;这基本是属于纯属的瘫痪手段,让设备只能处理 ARP 报文而忽略其他的报文来达到这个目的。
当然还有些更新型,更加巧妙的攻击方式;但是攻击的原理,都是依赖于 ARP 的 request 与 reply 功能。
4.2 IPv6 如何处置 IPv4 的 APR 功能
ipv6 没有 ARP 协议,但是为了实现类似的效果;ipv6 拥有 NDP (Neighbor Discovery Protocol) 协议,这是 ARP 与 ICMP 的集合。
4.3 ARP,IP,ICMP,IGMP,UDP,TCP,User Data 在 ETH 帧上的关系
ETH 是最底层的层级;
ARP 和 IP 是同一个层级的;
ICMP,IGMP,UDP,TCP 是同一个层级的;
User Data 是最上层的层级,MQTT,HTTP 这些都属于这一层;

从图片上可以看出来,每一层的具体范围;以太网帧指得就是 ETH 帧。
4.4 Wireshark 抓取标准 ARP 报文
开发板使用 ETH 的方式接入交换机,选用 lwip 2.0.2 的协议栈,可以使用 RT-Thread 的自动配置功能;在虚拟机上搭一个 ubuntu 的平台,安装上 wireshark ,把网卡的驱动都安装好,就可以开始抓取 ARP 报文了。
开发板 : IP: 192.168.12.107 MAC:00 80 e1 0f 54 46
Ubuntu: IP: 192.168.12.200 MAC:00 0c 29 fa e3 4c
ARP 的报文,在一个比较大的网络中,比如公司网络中可能会比较多;可以取个巧办法,不以 arp 的条件筛选报文,而使用 eth 的条件筛选。这样的话,无论是 ARP,DHCP 还是其他的什么报文,都可以抓上。如图所示:
这是一个 request 报文,比如,在启动开发板使用 ping 命令来 ping 我们的 ubuntu 平台,抓出的信息被圈起来了。

可以看到 ETH 帧的 dst 地址是广播地址全为1;而内部的 ARP 报文的 Target mac ( dst ) 的地址则全为0;这个抓包同我们上面的分析是一致的。
这是一个 reply 报文,开发板向 ubuntu 平台发送 ARP request 后的 ARP reply报文。

reply 报文的 ARP 报文部分,就是已经确定的 MAC 与 IP 地址了;而 ARP 的攻击的方式,一般都是从这里入手。毕竟一个广播帧任何设备都可以收到,在正常情况下,只有对应的才会回复;倘若是个 ARP 攻击设备,不管收到啥,都回复 reply 报文,并改为自己的 MAC 地址,其他设备上的 ARP 缓存表不一会儿就乱了。
4.5 在 Ubuntu 上查看 ARP 缓存表的信息

在 root 的权限下,可以使用 arp -d xxxx 命令删除指定的表项,搭配 ping 与 wireshark 可以看到一个设备在没有 ARP 缓存表项时设备的策略。
没有对应的表项,就先发送 ARP 报文以获取对应的 IP 与 MAC 关系,然后再发送 ping 命令。
如果有对应的表项,就直接发送 ping 命令,wireshark 就不能抓到 arp 的报文了。
Attention:我这里能看到 10 与 12 的原因是,我们这边的子网掩码是 16 位,也就是说,是属于同一个网段的,并不是分属 10 与 12 网段。一般不是本网段的 IP 包在数据发送时,直接把 ETH 的 dst 设置为网关地址了。
4.6 VLAN 功能
在学习交换机的功能时,vlan 是一个可以划分不同组的一项功能;一旦在交换机中设置了 vlan 功能,只有相同的 vlan 的设备才能直接通信。而 vlan 标识的功能实现,就是在 ETH 帧上附着了 VLAN 字段,通过该字段来划分对应的网络。
既然是实现在 ETH 帧上,ARP ,IP 等报文也是符合这个标准的。
4.7 lwip 的 ARP 缓存表策略探究
lwip 的 ARP 缓存表的更新,基本是放在 etharp_input 函数中,这也可以理解;毕竟收到 arp_reply 数据包后更新 ARP 缓存表就行了。
那我们如果在收到 arp_reply 的情况下,并不更新 ARP 缓存表,还会有数据发出吗?我们注释掉这个函数来试一下:
void etharp_input(struct pbuf *p, struct netif *netif)
{
···
/* ARP message directed to us?
-> add IP address in ARP cache; assume requester wants to talk to us,
can result in directly sending the queued packets for this host.
ARP message not directed to us?
-> update the source IP address in the cache, if present */
// etharp_update_arp_entry(netif, &sipaddr, &(hdr->shwaddr),
// for_us ? ETHARP_FLAG_TRY_HARD : ETHARP_FLAG_FIND_ONLY);
···
}
开发板上发送 ping 命令:

Ubuntu 上抓包:

由于开发板上屏蔽了 arp 缓存表的更新,即使收到了 ARP reply 也不会更新 ARP 表,则永远也不会有 PING 包从开发板发出,取而代之的是 ARP request 数据包。
再上面代码的基础上,选择从 Ubuntu 上 PING 我们的开发板,在此之前先清空对应 ARP 的表项。
Ubuntu 删除表项并发送 ping 命令:

Ubuntu 上抓包:
可以看到 Ubuntu 发出了 ping 包,而发出的原因就是 Ubuntu 上已经由了 ARP 缓存,可以看到第 3174 帧就是 arp reply 。如果我们再注释掉,或者修改了 lwip 中的 arp reply 的代码,就可以实现比较初级的 ARP 攻击手段;怕挨打,就不做了,将来各位在实践时,哪怕是做出来也建议不要公开。
后面收到的 arp request 基本都是开发板发出的,因为开发板上没有对应的 ARP 表项,PING 的回复包一包也发不回来,导致Ubuntu 这边是 ping lost 状态。

原作者:xiangxistu

更多回帖

×
20
完善资料,
赚取积分