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
|