什么是GARP

ARP(Address Resolution Protocol)即地址解析协议, 用于将网络层L3的IP地址转换成数据链路层的L2地址(MAC地址), 常用在诸如以太网, 无线网络等局域网中, 但对如点对点(P2P)网络, 组播与多播IP地址, 都无需使用ARP协议.

一般来说, L3-L2地址的转换通常被成为邻区协议(neighboring protocol), 这种发现邻居节点(neighbor)的协议被统称为邻居发现协议(Neighbor Discovery Protocol, ND). ARP可以看作是ND的ipv4版本(参考RFC826), 在ipv6中则直接称为ND(参考RFC4861).

邻居节点(neighbor)指的是跟主机在同一局域网(LAN)的其他节点

ND协议有两种消息类型:

  • Solicitation Request(也称为Neighbor Solicitation): 用于主机发送消息到网络中查询是否有主机拥有某个L3的IP地址, 该消息可以是单播, 组播或者广播形式.
  • Solicitation Reply(又称为Neighbor Advertisement): 收到Solicitation Request包时发出的回应包(有可能是HOST本身发出的, 也有可能是ARP代理服务器发送的)

那么, 什么是Gratuitous ARP(简称GARP, 免费ARP)了? 简单来说, GARP是一种用于告知网络中其他节点某些特定信息的ARP请求包, 但无须其他节点发送回应包, 常用于如下三种情况:

  • 检测局域网内的IP地址冲突(参考RFC5227)
  • L2地址(MAC地址)发生变化时
  • 虚拟IP(virtual IP), 用于服务器的冗余

接下来就看看GARP在这三种场景的具体应用.

IP冲突检测

当局域网中采用静态IP与动态(基于DHCP)IP配置结合的方式时, 有可能两个节点之间的IP会相同;这时需要使用GARP来检测IP冲突: 网络节点在配置IP时, 发送一个GARP到其他节点, 如果有节点的IP与之相同, 则会回应一个ARP包, 告诉该IP已经有节点使用了; 如果该IP没有被使用, 则不会收到任何ARP的回应.

MAC地址变更

当某个网络节点的L2地址(MAC地址)发生变化时, 网络中的其他节点是无法知道的, 因此需要一个协议来同步这个变化, 以便于其他的节点及时更新ARP缓存. 这样比每个节点自己主动更新会来的更省事方便(也不会导致短暂的网络黑洞, 参见上一篇文章ARP cache不更新导致的网络问题)

服务器冗余

在某些情况下, 为了实现在多个服务器之间的故障恢复(failover), 通常会使用GARP来到达服务器冗余(redundancy)的目的, 这时两个服务器会共享IP或MAC地址, 当某个服务器发生故障(死机, 物理故障等)时, 服务器群里的各个机器通过心跳机制来定时侦测故障, 当备用服务器侦测到与其共享IP的服务器故障时, 会发送一个GARP包, 告诉各个服务器更新对应的ARP缓存, 确保服务能快速恢复.

下面我们就来看一看Linux内核(4.14)是如何处理GARP的.

Linux如何处理GARP

Linux提供了ARP/GARP相关的配置选项, 在/proc/sys/net/ipv4/conf/中有如下几个节点(参考Linux文档ip-sysctl.txt):

  • arp_announce: 控制ARP请求时发送的本地网卡源IP地址的类型, 0(默认)表示可以使用任何本地IP地址, 1表示只使用同一子网内的IP地址, 2表示只使用网卡的首要IP地址(primary ip)
  • arp_igonre: 控制收到ARP请求后发送回包时的目标IP地址类型: 0(默认)表示发送任何本地的IP地址, 1只发送当前收到ARP包网卡的本地地址, 2只回送接收ARP包的网卡与ARP请求方在同一子网的IP地址, 3回送域(scope)为global/link的地址, 不回送host的地址; 4-7保留值, 8表示不回送任何地址
  • arp_notify: 网卡状态变化时是否发送GARP, 0表示什么都不做, 1表示设备UP或MAC地址变化时发送GARP
  • arp_accept: 收到GARP时是否创建ARP缓存, 0不创建, 1表示创建(如果ARP缓存已经包含了GARP包的IP地址, 不管是否开启该选项, 都会更新ARP缓存)
  • drop_gratuitous_arp: 丢弃所有GARP包, 默认是0(关闭)

最后, 不妨看下内核是如何实现GARP的发送的.

当网卡UP或者MAC地址发生变化时(前提是arp_notify处于开启状态)会发送GARP, 设备驱动也可以通过发送NETDEV_NOTIFY_PEERS事件来触发GARP告知其他节点状态的变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

//dev.c
int dev_set_mac_address(struct net_device *dev, struct sockaddr *sa)
{
const struct net_device_ops *ops = dev->netdev_ops;
int err;

if (!ops->ndo_set_mac_address)
return -EOPNOTSUPP;
if (sa->sa_family != dev->type)
return -EINVAL;
if (!netif_device_present(dev))
return -ENODEV;
err = ops->ndo_set_mac_address(dev, sa);
if (err)
return err;
dev->addr_assign_type = NET_ADDR_SET;
//MAC地址发生变化, 发送事件通知其他模块
call_netdevice_notifiers(NETDEV_CHANGEADDR, dev);
add_device_randomness(dev->dev_addr, dev->addr_len);
return 0;
}
EXPORT_SYMBOL(dev_set_mac_address);

内核初始化时注册的通知回调inetdev_event在接收到事件通知后, 就会发送GARP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

//devinet.c
static int inetdev_event(struct notifier_block *this, unsigned long event,
void *ptr)
{
struct net_device *dev = netdev_notifier_info_to_dev(ptr);
struct in_device *in_dev = __in_dev_get_rtnl(dev);

ASSERT_RTNL();

if (!in_dev) {
...
switch (event) {
case NETDEV_REGISTER:
pr_debug("%s: bug\n", __func__);
RCU_INIT_POINTER(dev->ip_ptr, NULL);
break;
case NETDEV_UP:
if (!inetdev_valid_mtu(dev->mtu))
break;
if (dev->flags & IFF_LOOPBACK) {
struct in_ifaddr *ifa = inet_alloc_ifa();

if (ifa) {
INIT_HLIST_NODE(&ifa->hash);
ifa->ifa_local =
ifa->ifa_address = htonl(INADDR_LOOPBACK);
ifa->ifa_prefixlen = 8;
ifa->ifa_mask = inet_make_mask(8);
in_dev_hold(in_dev);
ifa->ifa_dev = in_dev;
ifa->ifa_scope = RT_SCOPE_HOST;
memcpy(ifa->ifa_label, dev->name, IFNAMSIZ);
set_ifa_lifetime(ifa, INFINITY_LIFE_TIME,
INFINITY_LIFE_TIME);
ipv4_devconf_setall(in_dev);
neigh_parms_data_state_setall(in_dev->arp_parms);
inet_insert_ifa(ifa);
}
}
ip_mc_up(in_dev);
/* fall through */
case NETDEV_CHANGEADDR:
// `arp_notify`如果关闭, 则不会发送GARP包
if (!IN_DEV_ARP_NOTIFY(in_dev))
break;
/* fall through */
case NETDEV_NOTIFY_PEERS:
/* Send gratuitous ARP to notify of link change */
inetdev_send_gratuitous_arp(dev, in_dev);
break;
...
}
out:
return NOTIFY_DONE;
}

当收到GARP包是内核是如何处理的, 可以参考net/ipv4/arp.c中的函数arp_process.

参考文献