ggaaooppeenngg

为什么计算机科学是无限的但生命是有限的

neighboring subsystem 浅析

neighbor 在协议栈里指的是同一个 LAN 下面的邻居,也就是他们在 L3 上通过媒介或者点对点连接在了一起。邻居子系统同时也可以理解为一个 L2 和 L3 地址的转换器。邻居子系统的目的就在于上层协议不应该关心下层的地址信息,我发送过来的 IP 地址应该让下层来决定发送到哪个 MAC 地址。neighbor solicitation and neighbor advertisement,可以分别对应 ARP 的 request 请求和 ARP 的 reply 请求。

邻居子系统主要缓存两大块的内容,一个是 L3 到 L2 的映射解析的缓存,一个是 L2 头的缓存,缓存 L2 头的原因是大部分 L2 的头基本上是重复的,所以通过缓存头部可以加快协议的封装。

以下有几个相关结构体需要介绍一下。

struct neighbour 代表的是一个邻居的信息,比如 L2 和 L3 地址等。

struct neigh_table 代表的是一种邻居协议的接口(比如 ARP)。

struct neigh_params 代表的是邻居协议在每个设备上的不同参数。

struct neigh_ops 邻居对应的一些操作函数。

struct hh_cache 缓存 L2 的头部,不是所有的设备都支持头部缓存。

struct rttablestruct dst_enry, IPv4 的路由缓存信息是通过 struct rttable 缓存的。

这是 dst_entry, hh_cacheneighbour 之间的关系。

neighbor 结构是用 hash 存储的,key 是 L3 地址加设备(对应的 device 结构体)加一个随机值。hash 表通过 neigh_hash_allocneigh_hash_free 来分配和释放。neigh_lookup 就是通过 key 从 hash table 中找到对应的 neighbour 结构体,实现都不复杂,这里理解就可以了。

一般邻居子系统的缓存流程是这样的,如果 L3 的请求到达,地址解析缓存没有命中,把 L3 packet 入队,开始neighbor solicitation 等收到 neighbor advertisement 之后再出队并且发送。

邻居状态信息 NUD(Netowork Unreachablility) 状态

NUD state 本来是 IPv6 协议里的邻居关系的定义,但是在内核中沿用到了 IPv4 里面。

  • NUD_NONE 这个 neighbour 刚开始建立,没有相关状态
  • NUD_INCOMPLETE 发送了获取 L2 地址的请求(通过 ARP 或者其他协议),但是没收到回复,并且之前不存在老缓存
  • NUD_REACHABLE neighbour 地址被缓存了,并且是可达的
  • NUD_FAILED 因为发送地址解析请求失败了,标志邻居不可达
  • NUD_STALE NUD_DELAY NUD_PROBE 是解析请求确认邻居可达的过程中的中间状态
  • NUD_NOARP 标志不需要用解析协议(虽然是 NOARP,但是别的协议也用这个标记),这是一个特殊情况。
  • NUD_PERMANET 标志邻居的地址解析是静态配置的

这些是基本状态,根据这些基本状态组合了几个相对有语义的状态。

  • NUD_VALID 表示该地址会是一个有效的地址

    NUD_PERMANENT NUD_NOARP NUD_REACHABLE NUD_PROBE NUD_STALE NUD_DELAY

  • NUD_CONNECTED 是 NUD_VALID 的子集,去除了待决的中间状态

    NUD_PERMANENT NUD_NOARP NUD_REACHABLE

  • NUD_IN_TIMER 表示在这个状态下正在执行一个定时任务,一般是状态不明了的时候

    NUD_INCOMPLETE NUD_DELAY NUD_PROBE

邻居的可达性可以通过两点来确认,收到了一个地址解析协议的单播回复,或者通过外部信息确认(比如收到了这个邻居的 TCP 报文,当然这个 IP 可能不是自己的邻居,但是可以可以确定对应网关『作为邻居』的可达性)。

确认邻居信息

IP 层会调用ipv4_confirm_neigh 来确认映射地址,如果有 gateway 用 gateway,没有 gateway 开始查缓存。在 net/ipv4/ip_output.c 中的对应代码,就是查缓存的 neighbour 结构体,如果这个结构体不存在的话就要开始confirm 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
neigh = __ipv4_neigh_lookup_noref(dev, nexthop);

if (unlikely(!neigh))
neigh = __neigh_create(&arp_tbl, &nexthop, dev, false);
if (!IS_ERR(neigh)) {
int res;

sock_confirm_neigh(skb, neigh);
res = neigh_output(neigh, skb);

rcu_read_unlock_bh();
return res;
}

当 TCP 收到报文(比如对端的 SYN/ACK 时),这种外部信息说明其实这个节点是可达的(不是来自 gateway),也可以更新缓存。

另外,neigh_connectneigh_suspect 是两个状态转换时会调用的函数。

当 neigh 进入 NUD_REACHABLEneigh_connectneigh->output 的函数指向 connected_output 这个函数,它会在调用 dev_queue_xmit 之前填充 L2 头部,把包直接发出去。

当从 NUD_REACHBLE 转换成 NUD_STALE 或者 NUD_DELAYneigh_suspect 会强制进行可达性的确认,通过把 neighbor->output 指向 neigh_ops->output, 也就是 neigh_resolve_output,它会在调用 dev_queue_xmit 之前先把地址解析出来,等把地址解析完成以后再把缓存的包发送出去。

更新邻居信息

邻居信息更新的入口函数就是 neigh_update,这个函数定义如下。

1
2
int neigh_update(struct neighbour *neigh, const u8 *lladdr, u8 new,
u32 flags, u32 nlmsg_pid)

首先做一些预先检查,如果是管理员配置,原来的配置就不能是 PERMANENT 或者 NOARP 的。

1
2
3
if (!(flags & NEIGH_UPDATE_F_ADMIN) &&
(old & (NUD_NOARP | NUD_PERMANENT)))
goto out;

标记为 dead,然后 goto out

1
2
if (neigh->dead)
goto out;

如果更新为无效的标记的话,删除 timer,并且 supect 一下,如果是 NUD_CONNECTED 状态。
如果是需要标记为失败的 neigh(之前是 INCOMPLETE|NUD_PROBE),则调用 neigh_invalidate,让这个 neigh 无效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (!(new & NUD_VALID)) {
neigh_del_timer(neigh);
if (old & NUD_CONNECTED)
neigh_suspect(neigh);
neigh->nud_state = new;
err = 0;
notify = old & NUD_VALID;
if ((old & (NUD_INCOMPLETE | NUD_PROBE)) &&
(new & NUD_FAILED)) {
neigh_invalidate(neigh);
notify = 1;
}
goto out;
}

接下来是三个条件,一个是 device 没有硬件地址,用 neigh->ha,如果 lladdr 提供了并且老的是有效的,使用老的地址,如果 lladdr 没有提供,直接使用老的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* Compare new lladdr with cached one */
if (!dev->addr_len) {
/* First case: device needs no address. */
lladdr = neigh->ha;
} else if (lladdr) {
/* The second case: if something is already cached
and a new address is proposed:
- compare new & old
- if they are different, check override flag
*/
if ((old & NUD_VALID) &&
!memcmp(lladdr, neigh->ha, dev->addr_len))
lladdr = neigh->ha;
} else {
/* No address is supplied; if we know something,
use it, otherwise discard the request.
*/
err = -EINVAL;
if (!(old & NUD_VALID))
goto out;
lladdr = neigh->ha;
}

如果NUD_CONNECTED 更新 confirm 的时间,更新『更新』的时间。

1
2
3
if (new & NUD_CONNECTED)
neigh->confirmed = jiffies;
neigh->updated = jiffies;

NEIGH_UPDATE_F_OVERRIDE_ISROUTER 标记的是当前 neigh 是一个 router。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* If entry was valid and address is not changed,
do not change entry state, if new one is STALE.
*/
err = 0;
update_isrouter = flags & NEIGH_UPDATE_F_OVERRIDE_ISROUTER;
if (old & NUD_VALID) {
if (lladdr != neigh->ha && !(flags & NEIGH_UPDATE_F_OVERRIDE)) {
update_isrouter = 0;
if ((flags & NEIGH_UPDATE_F_WEAK_OVERRIDE) &&
(old & NUD_CONNECTED)) {
lladdr = neigh->ha;
new = NUD_STALE;
} else
goto out;
} else {
if (lladdr == neigh->ha && new == NUD_STALE &&
!(flags & NEIGH_UPDATE_F_ADMIN))
new = old;
}
}

如果是更新操作,删除老的 timer,如果需要 timer,更新新的 timer,并且设置新状态。

1
2
3
4
5
6
7
8
9
10
11
12
if (new != old) {
neigh_del_timer(neigh);
if (new & NUD_PROBE)
atomic_set(&neigh->probes, 0);
if (new & NUD_IN_TIMER)
neigh_add_timer(neigh, (jiffies +
((new & NUD_REACHABLE) ?
neigh->parms->reachable_time :
0)));
neigh->nud_state = new;
notify = 1;
}

更新 neigh->ha,如果 lladdrneigh->ha 不同的话。

1
2
3
4
5
6
7
8
9
10
11
12
if (lladdr != neigh->ha) {
write_seqlock(&neigh->ha_lock);
memcpy(&neigh->ha, lladdr, dev->addr_len);
write_sequnlock(&neigh->ha_lock);
neigh_update_hhs(neigh);
if (!(new & NUD_CONNECTED))
neigh->confirmed = jiffies -
(NEIGH_VAR(neigh->parms, BASE_REACHABLE_TIME) << 1);
notify = 1;
}
if (new == old)
goto out;

根据状态调用 connect 和 suspect

1
2
3
4
if (new & NUD_CONNECTED)
neigh_connect(neigh);
else
neigh_suspect(neigh);

如果之前老的不是 NUD_VALID,就会把 skb 从 arp_queue,并且释放 arp_queue。

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
if (!(old & NUD_VALID)) {
struct sk_buff *skb;

/* Again: avoid dead loop if something went wrong */

while (neigh->nud_state & NUD_VALID &&
(skb = __skb_dequeue(&neigh->arp_queue)) != NULL) {
struct dst_entry *dst = skb_dst(skb);
struct neighbour *n2, *n1 = neigh;
write_unlock_bh(&neigh->lock);

rcu_read_lock();

/* Why not just use 'neigh' as-is? The problem is that
* things such as shaper, eql, and sch_teql can end up
* using alternative, different, neigh objects to output
* the packet in the output path. So what we need to do
* here is re-lookup the top-level neigh in the path so
* we can reinject the packet there.
*/
n2 = NULL;
if (dst) {
n2 = dst_neigh_lookup_skb(dst, skb);
if (n2)
n1 = n2;
}
n1->output(n1, skb);
if (n2)
neigh_release(n2);
rcu_read_unlock();

write_lock_bh(&neigh->lock);
}
__skb_queue_purge(&neigh->arp_queue);
neigh->arp_queue_len_bytes = 0;
}

更新 flag,发送 notify 通过 rtnetlink 通知 nlmsg_pid 对应的进程,

1
2
3
4
5
6
7
8
9
10
out:
if (update_isrouter) {
neigh->flags = (flags & NEIGH_UPDATE_F_ISROUTER) ?
(neigh->flags | NTF_ROUTER) :
(neigh->flags & ~NTF_ROUTER);
}
write_unlock_bh(&neigh->lock);

if (notify)
neigh_update_notify(neigh, nlmsg_pid);

这就是邻居信息的更新流程。

当然还有一个缓存项是 L2 header 的缓存这里就简单略过了。

ARP

接下来说一下 ARP 相关的内容,ARP 本身的格式其实很简单。

邻居子系统有一个比较关键的协议就是 ARP,这里简单介绍一下 ARP 协议,我们来看一下 ARP 的格式。

先是两个字节的 L2 类型,然后是两个字节的 L3 类型。一般 L2 是 1 (以太网),L3 是 0x0800 (IPV4),接着跟着的是 L2 类型的长度和 L3 类型的长度,这里 Ethernet 的 MAC 地址是 6 个字节,IPV4 的 IP 地址是 4 个字节,接着是两个字节的操作,1 表示请求,2 表示回复,然后是发送端 L2 的地址,L3 的地址,接着是接收端 L2 和 L3 的地址。整个翻译过来就是『我(MAC 是 0a:00:27:00:00:00 IP 是 192.168.56.1)广而告之(MAC 广播地址),问一下谁知道 192.168.56.102 的地址』。

gARP (gratuitous ARP)

听起来像是一个无理由的 ARP 请求,实际上是一种主动通知的 ARP 请求。gARP 本身不是一个查询请求,而是一个通知请求,主要运用于主动通知 L2 地址改变,重复地址发现(请求解析自己的地址,如果收到回复说明有地址重复)。

还有就是 VIP,一般的作用是在本地网路中有两台机器,一台作为备机,一台作为主机,当主机 failover 的时候,备机可以继续『冒充』主机的 IP 地址,具体的做法就是主动发送请求,解析的 MAC 和 IP 都和 source 一样,老的 server 肯定不会回答这个 ARP,交换机上已经没有这个端口的缓存,会进行广播,让所有的接收者都会更新自己的缓存。也就是发送了一个一去不复返的请求,让所有的邻居更新了自己的 ARP 缓存,从而替代了老 server 的 IP,这就是 VIP 通过 ARP 实现的 failover。

总结

邻居子系统很大一部分的作用就是解析和缓存地址映射,主要是通过 ARP 来完成,而且 ARP 本身也有很多使用的姿势,也就上面说到的 gARP,邻居子系统是沟通路由子系统,以及 L2 和 L3 的桥梁。

参考:

  1. Understanding Linux Network Internals