文盲

读书无多,识字不广

ARP简介

文盲 / 2024-01-21


ARP(Address Resolution Protocol)全称翻译为地址解析协议,按照TCP/IP一书中原话描述为:“提供了一种在IPv41地址和各种网络技术使用的硬件地址之间的映射”。而所谓地址解析,则:“为发现两个地址之间映射关系的过程”。

ARP帧

ARP帧

ARP帧结构如上图所示2

  • 前14个字节为以太网头部:
    • 前两个字段分别为目的和源的以太网地址,以太网目的地址全1为广播地址,即 ff:ff:ff:ff:ff:ff
    • 最后一个字段为固定值 0x0806 ,表示ARP协议
  • 之后的8个字节为固定大小:
    • 前四个字段指定了后面4个字段的类型和大小
      • 硬件类型:硬件地址类型,以太网中为1
      • 协议类型:映射协议的协议地址类型,IPv4为 0x0800
      • 硬件大小和协议大小:分别指出硬件地址和协议地址的字节数,在以太网中的使用IPv4的ARP而言,值分别为6和4
    • Op字段:指定ARP请求类型
      • 1:ARP请求
      • 2:ARP应答
      • 3:RARP请求
      • 4:RARP应答
  • 可变大小字节中,根据前面的类型大小字段填充,同样使用IPv4的ARP而言,分别为源MAC、源IP3、目的MAC、目的IP

ARP交互

创建台Linux虚机进行抓包学习:

  ┌────────────────────────┐
  │          VM-1          │
  │                        │
  │    IP: 192.168.0.53    │
  │                        │
  │ MAC: 00:0c:29:82:ba:8b │
  │                        │
  └────────────────────────┘

然后在其网口使用 tcpdump -i ens160 arp -w arp.pcap 命令进行抓包,然后用WireShark打开 arp.pcap 文件查看并过滤详细信息。一次常见的ARP交互应该如下图所示4ARP抓包

抓包 Info 的翻译5很是到位,请求方首先向广播域发送一个广播请求,向广播域内所有机器询问 Who has 192.68.0.53? Tell 192.168.0.112 ;广播域内所有机器均会收到该报文,并检测自身IP地址,如与询问的地址不一致,则不会理睬,如发现自身IP地址与询问地址一致,则响应请求报文 192.168.0.53 is at 00:0c:29:82:ba:8b,也就是第二个报文。

观察原始报文,比对实际是否与第一节中的理论一致,且进一步分析。

请求报文

ff ff ff ff ff ff 96 f6 1f e1 26 f9 08 06 00 01
08 00 06 04 00 01 96 f6 1f e1 26 f9 c0 a8 00 70
00 00 00 00 00 00 c0 a8 00 35 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
  • 前14字节为 ff ff ff ff ff ff 96 f6 1f e1 26 f9 08 06 ,即源MAC为 96:f6:1f:e1:26:f9 目的MAC为 ff:ff:ff:ff:ff:ff ,类型为固定值 0x0806
  • 接下来8字节为 00 01 08 00 06 04 00 01 ,由于是以太网ARP,硬件类型为 0x0001 ,协议类型为IPv4,故值为 0x0800 ;字节数分别为 0x06 0x04 ,硬件地址字节数为6,协议地址字节数为4;ARP类型为 0x0001 ,表示ARP请求报文
  • 之后20字节为 96 f6 1f e1 26 f9 c0 a8 00 70 00 00 00 00 00 00 c0 a8 00 35 ,拆分为源MAC为 96:f6:1f:e1:26:f9 ,源IP为 c0 a8 00 70 转为十进制表示即为 192.168.0.112 ;目的MAC为 00:00:00:00:00:00 由于是不知道目的MAC,故8字节全部置0,目的IP为 c0 a8 00 35 转为十进制即为 192.168.0.53

响应报文

96 f6 1f e1 26 f9 00 0c 29 82 ba 8b 08 06 00 01
08 00 06 04 00 02 00 0c 29 82 ba 8b c0 a8 00 35
96 f6 1f e1 26 f9 c0 a8 00 70
  • 前14字节为 96 f6 1f e1 26 f9 00 0c 29 82 ba 8b 08 06 ,目的MAC 96:f6:1f:e1:26:f9由于其已经从请求报文中获取到了对端地址,故直接将MAC地址置为了请求报文的源MAC地址进行单播,而不是接着发送广播,源MAC 00:0c:29:82:ba:8b ,为自身MAC,同样类型为 0x0806
  • 之后8字节一样为 00 01 08 00 06 04 00 02 ,其余均与请求报文相同,不再赘述。Op字段为 0x02 ,表示报文类型为ARP响应报文
  • 最后20字节为 00 0c 29 82 ba 8b c0 a8 00 35 96 f6 1f e1 26 f9 c0 a8 00 70 ,源MAC为自己MAC 00:0c:29:82:ba:8b ,源IP c0 a8 00 35192.168.0.53 ;目的MAC为 96:f6:1f:e1:26:f9 ,目的IP c0 a8 00 70192.168.0.112

ARP缓存

机器经过ARP交互之后,一般会将收到的ARP进行缓存,以提高效率。如果向已经缓存的地址使用arping命令发送arp请求,则默认会将缓存的mac填入目的mac。为每个接口记录维护了从网络地址到硬件地址的映射,在Linux中,可以使用 arp 命令查看缓存条目,arp 命令较为简单,使用 arp --help 查看帮助之后便可立即使用,在此不再赘述。一个常规的执行结果如下:

➜  ~ arp -n
Address                  HWtype  HWaddress           Flags Mask            Iface
192.168.0.1              ether   34:96:72:24:8c:94   C                     ens160
192.168.0.103            ether   96:f6:1f:e1:26:f9   C                     ens160

五列从左至右依次为IP地址,硬件类型,硬件地址,标志,本地端口,其中标记有 C、M、P三种类型,根据man手册所描述:

Each complete entry in the ARP cache will be marked with the C flag. Permanent entries are marked with M and published entries have the P flag.

其中,C一般表示为通过ARP协议动态学到的条目;M一般为手动输入条目;P则表示发布条目,一般用于配置代理ARP。

更新示例

使用两台虚机A和B,使用ARP命令查看A的ARP缓存:

➜  ~ arp -n                                         
Address                  HWtype  HWaddress           Flags Mask            Iface
192.168.0.114            ether   d4:d2:d6:b5:aa:0e   C                     ens160
192.168.0.1              ether   34:96:72:24:8c:94   C                     ens160
192.168.0.103            ether   96:f6:1f:e1:26:f9   C                     ens160

此时并未在缓存中看到虚机B的地址信息,在虚机A中使用 ping 命令对虚机B发起请求,然后再查看ARP表的信息:

➜  ~ ping 192.168.0.53 -c 1
PING 192.168.0.53 (192.168.0.53) 56(84) bytes of data.
64 bytes from 192.168.0.53: icmp_seq=1 ttl=64 time=0.675 ms

--- 192.168.0.53 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.675/0.675/0.675/0.000 ms
➜  ~ 
➜  ~ arp -n               
Address                  HWtype  HWaddress           Flags Mask            Iface
192.168.0.114            ether   d4:d2:d6:b5:aa:0e   C                     ens160
192.168.0.1              ether   34:96:72:24:8c:94   C                     ens160
192.168.0.53             ether   00:0c:29:82:ba:8b   C                     ens160
192.168.0.103            ether   96:f6:1f:e1:26:f9   C                     ens160
➜  ~ 

此时A机器的ARP表中学到了B机器的地址映射信息

超时时间

根据TCP/IP详解一书中的描述而言,ARP完整条目的一般设计的超时时间为20分钟,不完整条目的超时时间为3分钟,然而在Linux的实际验证中,发现实现远比这个复杂。

接着上面的例子,再将虚机B关机,查看ARP表的信息:

➜  ~ arp -n
Address                  HWtype  HWaddress           Flags Mask            Iface
192.168.0.114            ether   d4:d2:d6:b5:aa:0e   C                     ens160
192.168.0.1              ether   34:96:72:24:8c:94   C                     ens160
192.168.0.53             ether   00:0c:29:82:ba:8b   C                     ens160
192.168.0.103            ether   96:f6:1f:e1:26:f9   C                     ens160
➜  ~ 

发现虚机A并未感知到虚机B关机这点,ARP 表中没发生变化,依旧使用 ping 主动触发,再查看:

➜  ~ ping 192.168.0.53 -c 1
PING 192.168.0.53 (192.168.0.53) 56(84) bytes of data.

--- 192.168.0.53 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms

➜  ~ 
➜  ~ arp -n            
Address                  HWtype  HWaddress           Flags Mask            Iface
192.168.0.114            ether   d4:d2:d6:b5:aa:0e   C                     ens160
192.168.0.1              ether   34:96:72:24:8c:94   C                     ens160
192.168.0.53                     (incomplete)                              ens160
192.168.0.103            ether   96:f6:1f:e1:26:f9   C                     ens160
➜  ~ 

发现虚机B的映射并未直接被删除,而是将硬件地址变为了 incomplete 状态,且长时间等待后,该条目仍未被删除。此种情况并未出现在书中,需要进一步探究。

经过查询后得知,Linux中有名为 net.ipv4.neigh.default.gc_thresh1 的参数设置,其用于控制触发回收的ARP表条数,原话为:“The garbage collector will not run if there are fewer than this number of entries in the cache” ,而其在虚机中的默认值为128,此时ARP表中只有四条数据,显然不会进行回收,手动将其值更改为2,发现清除成功:

➜  ~ sysctl  -w net.ipv4.neigh.default.gc_thresh1=2
net.ipv4.neigh.default.gc_thresh1 = 2
➜  ~ 
➜  ~ arp -n          
Address                  HWtype  HWaddress           Flags Mask            Iface
192.168.0.103            ether   96:f6:1f:e1:26:f9   C                     ens160

关于Linux中ARP表状态更新的详细逻辑见附1:Linux中ARP状态更新

特殊ARP

代理ARP

顾名思义,所谓代理ARP目的是使一个系统可回答不同主机的ARP请求,以让ARP请求的发送者认为作出响应的系统就是目的主机,但实际目的主机可能在其他地方。

Linux中,可以将 sysctlnet.ipv4.conf.all.proxy_arp 的值更改为1,以支持代理ARP功能;然后便可以使用 ip 等命令进行配置。

免费ARP

免费ARP,英文为Gratuitous ARP,是一种特殊形式的ARP,如果说ARP是为了获取邻居的MAC地址信息,而免费ARP则一般用于检测自己的IP地址是否冲突和更新ARP缓存。

免费ARP报文格式与普通ARP不同【TODO,抓包验证】:

  • 其请求报文的目的IP和发送方IP地址相同
  • 目的IP通常为全f的广播地址

我们可以在Linux中使用 arping 命令来发送免费arp报文:

# 其中,interface为网口名,而target_ip则使用对应网口自己的IP
arping -I <interface> <target_ip>

在同子网下的其他网口抓包,使用wireshark解析可见,包已经被备注为 [Is gratuitous: True] ;而在网上的一些资料中,会将免费arp请求带上 -U 选项,而经过实际抓包测试,发现加不加 -U 选项在数据报文上没有任何区别,只是 -U 会默认 arp 的源IP为你的目的IP,且此IP应与端口IP一致,不然会产生报错:

# 本机地址非107
[root@nomal ~]# arping -I ens160 -U 192.168.0.107
bind: Cannot assign requested address

而常用的在本机检测本机是否与其他节点地址冲突,则也可使用arping命令,需要使用 -D 选项:

# 其中,interface为网口名,而target_ip则使用对应网口自己的IP
arping -I -D <interface> <target_ip>

该选项会将arp报文的源IP置为 0.0.0.0 ,此时如果同子网内有其他节点配置了与本机一致的地址,则本机会收到arp回复,而如果没有重复,则不会收到任何回复。如果地址冲突,则执行结果如下:

[root@nomal ~]# arping -D -I ens160 192.168.0.53
ARPING 192.168.0.53 from 0.0.0.0 ens160
Unicast reply from 192.168.0.53 [00:0C:29:4D:A7:D3]  1.024ms
Sent 1 probes (1 broadcast(s))
Received 1 response(s)

附1:Linux中ARP状态更新

ARP状态类型

Linux中,ARP的状态共有以下几种,通过二进制标志位区分,其定义如下所示:

// 代码来源于 linux/include/uapi/linux/neighbour.h,中文注释为我的备注
// 2023-11-19 master分支

/*
 * Neighbor Cache Entry States.
 */

#define NUD_INCOMPLETE 0x01 // 请求已发送,但未收到应答
#define NUD_REACHABLE 0x02 // 邻居可达,确定有效
#define NUD_STALE 0x04 // 邻居项有段时间未使用
#define NUD_DELAY 0x08 // 邻居项超时
#define NUD_PROBE 0x10 // 邻居项超时,开始发送请求确认可达性
#define NUD_FAILED 0x20 // 地址解析失败或可达性验证失败

/* Dummy states */
#define NUD_NOARP 0x40 // 设备不支持或无需做地址映射
#define NUD_PERMANENT 0x80 // 邻居项永久有效
#define NUD_NONE 0x00 // 默认新建后的状态

ARP状态更新

ARP的状态更新的主要逻辑位于 linux/net/core/neighbour.c 中的 neigh_timer_handler 函数中:

// 代码来源于 linux/net/core/neighbour.h,中文注释为我的备注
// 2023-11-19 master分支

// 需要更新定时器的状态
#define NUD_IN_TIMER (NUD_INCOMPLETE|NUD_REACHABLE|NUD_DELAY|NUD_PROBE)
// 有效状态
#define NUD_VALID (NUD_PERMANENT|NUD_NOARP|NUD_REACHABLE|NUD_PROBE|NUD_STALE|NUD_DELAY)
// 连接状态
#define NUD_CONNECTED (NUD_PERMANENT|NUD_NOARP|NUD_REACHABLE)
// 代码来源于 linux/net/core/neighbour.c,中文注释为我的备注
// 2023-11-19 master分支

/* Called when a timer expires for a neighbour entry. */

static void neigh_timer_handler(struct timer_list *t)
{
 unsigned long now, next;
 struct neighbour *neigh = from_timer(neigh, t, timer);
 unsigned int state;
 int notify = 0;

 write_lock(&neigh->lock);

 state = neigh->nud_state;
 now = jiffies;
 next = now + HZ;

 if (!(state & NUD_IN_TIMER))
  goto out;

 if (state & NUD_REACHABLE) { // 可达状态
  if (time_before_eq(now,
       neigh->confirmed + neigh->parms->reachable_time)) {
            // 时长未超过reachable_time,更新定时器
   neigh_dbg(2, "neigh %p is still alive\n", neigh);
   next = neigh->confirmed + neigh->parms->reachable_time;
  } else if (time_before_eq(now,
       neigh->used +
       NEIGH_VAR(neigh->parms, DELAY_PROBE_TIME))) {
            // 时长超过reachable_time但未超过delay_probe_time
           // 切换状态至 NUD_DELAY 并更新邻居项操作集和定时器
   neigh_dbg(2, "neigh %p is delayed\n", neigh);
   WRITE_ONCE(neigh->nud_state, NUD_DELAY);
   neigh->updated = jiffies;
   neigh_suspect(neigh);
   next = now + NEIGH_VAR(neigh->parms, DELAY_PROBE_TIME);
  } else {
            // 时长超过delay_probe_time
            // 切换为 NUD_STALE 状态并更新邻居项操作集
   neigh_dbg(2, "neigh %p is suspected\n", neigh);
   WRITE_ONCE(neigh->nud_state, NUD_STALE);
   neigh->updated = jiffies;
   neigh_suspect(neigh);
   notify = 1;
  }
 } else if (state & NUD_DELAY) { // 超时状态
  if (time_before_eq(now,
       neigh->confirmed +
       NEIGH_VAR(neigh->parms, DELAY_PROBE_TIME))) {
            // 可达确认时间已更新,重新迁回NUD_REACHABLE状态
   neigh_dbg(2, "neigh %p is now reachable\n", neigh);
   WRITE_ONCE(neigh->nud_state, NUD_REACHABLE);
   neigh->updated = jiffies;
   neigh_connect(neigh);
   notify = 1;
   next = neigh->confirmed + neigh->parms->reachable_time;
  } else {
            // 时长超过delay_probe_time,切为 NUD_PROBE 状态
   neigh_dbg(2, "neigh %p is probed\n", neigh);
   WRITE_ONCE(neigh->nud_state, NUD_PROBE);
   neigh->updated = jiffies;
   atomic_set(&neigh->probes, 0);
   notify = 1;
            // 设置下次超时时间为重传时间与HZ/100的最大值
   next = now + max(NEIGH_VAR(neigh->parms, RETRANS_TIME),
      HZ/100);
  }
 } else {
  /* NUD_PROBE|NUD_INCOMPLETE */
        // 更新下次超时时间
  next = now + max(NEIGH_VAR(neigh->parms, RETRANS_TIME), HZ/100);
 }

    // 状态为INCOMPLETE或PROBE 且 请求发送次数已经超过了上限
 if ((neigh->nud_state & (NUD_INCOMPLETE | NUD_PROBE)) &&
     atomic_read(&neigh->probes) >= neigh_max_probes(neigh)) {
        // 状态更新为NUD_FAILED
  WRITE_ONCE(neigh->nud_state, NUD_FAILED);
  notify = 1;
  neigh_invalidate(neigh);
  goto out;
 }

    // 状态为NUD_IN_TIMER
 if (neigh->nud_state & NUD_IN_TIMER) {
  if (time_before(next, jiffies + HZ/100))
   next = jiffies + HZ/100;
  if (!mod_timer(&neigh->timer, next))
   neigh_hold(neigh);
 }
    // 状态为 NUD_INCOMPLETE 或 NUD_PROBE
 if (neigh->nud_state & (NUD_INCOMPLETE | NUD_PROBE)) {
        // 发送请求报文
  neigh_probe(neigh);
 } else {
out:
  write_unlock(&neigh->lock);
 }

    // 如果notify置1,对外通知
 if (notify)
  neigh_update_notify(neigh, 0);

 trace_neigh_timer_handler(neigh, 0);

 neigh_release(neigh);
}

位于 linux/net/core/neighbour.h 中的 neigh_event_send() 中也有部分逻辑:

static __always_inline int neigh_event_send_probe(struct neighbour *neigh,
        struct sk_buff *skb,
        const bool immediate_ok)
{
 unsigned long now = jiffies;
 // 更新邻居使用的时间戳
 if (READ_ONCE(neigh->used) != now)
  WRITE_ONCE(neigh->used, now);
    // 三种状态直接发送报文
 if (!(READ_ONCE(neigh->nud_state) & (NUD_CONNECTED | NUD_DELAY | NUD_PROBE)))
  return __neigh_event_send(neigh, skb, immediate_ok);
 return 0;
}

static inline int neigh_event_send(struct neighbour *neigh, struct sk_buff *skb)
{
 return neigh_event_send_probe(neigh, skb, true);
}

__neigh_event_send() 则也在 linux/net/core/neighbour.c 中进行了实现:

int __neigh_event_send(struct neighbour *neigh, struct sk_buff *skb,
         const bool immediate_ok)
{
 int rc;
 bool immediate_probe = false;

 write_lock_bh(&neigh->lock);

 rc = 0;
    // conn delay probe这三种状态直接发送报文
 if (neigh->nud_state & (NUD_CONNECTED | NUD_DELAY | NUD_PROBE))
  goto out_unlock_bh;
 if (neigh->dead)
  goto out_dead;

    // 非stale和incomplete状态,即NUD_NONE状态
 if (!(neigh->nud_state & (NUD_STALE | NUD_INCOMPLETE))) {
        // 如果可发送MCAST_PROBES+可发送APP_PROBES的数量不为0
  if (NEIGH_VAR(neigh->parms, MCAST_PROBES) +
      NEIGH_VAR(neigh->parms, APP_PROBES)) {
   unsigned long next, now = jiffies;

   atomic_set(&neigh->probes,
       NEIGH_VAR(neigh->parms, UCAST_PROBES));
   neigh_del_timer(neigh);
   WRITE_ONCE(neigh->nud_state, NUD_INCOMPLETE);
   neigh->updated = now;
   if (!immediate_ok) {
    next = now + 1;
   } else {
    immediate_probe = true;
    next = now + max(NEIGH_VAR(neigh->parms,
          RETRANS_TIME),
       HZ / 100);
   }
   neigh_add_timer(neigh, next);
  } else {
            // 可发送数量为0,状态直接置为NUD_FAILED
   WRITE_ONCE(neigh->nud_state, NUD_FAILED);
   neigh->updated = jiffies;
   write_unlock_bh(&neigh->lock);

   kfree_skb_reason(skb, SKB_DROP_REASON_NEIGH_FAILED);
   return 1;
  }
 } else if (neigh->nud_state & NUD_STALE) {
        // 状态为stale 验证邻居可达性
  neigh_dbg(2, "neigh %p is delayed\n", neigh);
  neigh_del_timer(neigh);
        // 状态更新为NUD_DELAY,启动定时器验证可达性
  WRITE_ONCE(neigh->nud_state, NUD_DELAY);
  neigh->updated = jiffies;
  neigh_add_timer(neigh, jiffies +
    NEIGH_VAR(neigh->parms, DELAY_PROBE_TIME));
 }

 if (neigh->nud_state == NUD_INCOMPLETE) {
        // 状态为incomplete,解析邻居地址中,缓存skb
  if (skb) {
            // 如果队列达到上限,丢弃最老的skb,加入最新的
   while (neigh->arp_queue_len_bytes + skb->truesize >
          NEIGH_VAR(neigh->parms, QUEUE_LEN_BYTES)) {
    struct sk_buff *buff;

    buff = __skb_dequeue(&neigh->arp_queue);
    if (!buff)
     break;
    neigh->arp_queue_len_bytes -= buff->truesize;
    kfree_skb_reason(buff, SKB_DROP_REASON_NEIGH_QUEUEFULL);
    NEIGH_CACHE_STAT_INC(neigh->tbl, unres_discards);
   }
   skb_dst_force(skb);
   __skb_queue_tail(&neigh->arp_queue, skb);
   neigh->arp_queue_len_bytes += skb->truesize;
  }
        // 返回值置为非0,表示数据包被缓存
  rc = 1;
 }
out_unlock_bh:
 if (immediate_probe)
  neigh_probe(neigh);
 else
  write_unlock(&neigh->lock);
 local_bh_enable();
 trace_neigh_event_send_done(neigh, rc);
 return rc;

out_dead:
 if (neigh->nud_state & NUD_STALE)
  goto out_unlock_bh;
 write_unlock_bh(&neigh->lock);
 kfree_skb_reason(skb, SKB_DROP_REASON_NEIGH_DEAD);
 trace_neigh_event_send_dead(neigh, 1);
 return 1;
}

整个状态更新流程如下图所示:

ARP状态更新

附2:Linux中ARP常用命令介绍

arp 命令

一般用于查看 arp 表,如前文中 arp -n 命令;该命令也可使用 ip n 相关命令替代;如果这些命令都没有安装,也可直接查看 /proc/net/arp 文件来查看本机 arp

[root@nomal ~]# arp -n
Address                  HWtype  HWaddress           Flags Mask            Iface
192.168.0.111            ether   96:f6:1f:e1:26:f9   C                     ens160

[root@nomal ~]# ip n
192.168.0.111 dev ens160 lladdr 96:f6:1f:e1:26:f9 REACHABLE

[root@nomal ~]# cat /proc/net/arp 
IP address       HW type     Flags       HW address            Mask     Device
192.168.0.111    0x1         0x2         96:f6:1f:e1:26:f9     *        ens160

arping 命令

发送 arping 数据包,可用于检测 arp 功能配置是否正常;IP地址是否冲突等,相关用法见上文


  1. ARP协议仅被用于IPv4中,IPv6使用NDP(邻居发现协议)替代了ARP ↩︎

  2. 图根据《TCP/IP详解 卷一》图4-2绘制而成 ↩︎

  3. 这里及后文的IP均指IPv4 ↩︎

  4. 图中 192.168.0.112 为我本机地址 ↩︎

  5. 该Info为抓包工具将报文信息翻译为更容易被人类看懂的形式,实际报文并没有 Who 等单词 ↩︎