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 rttable
和 struct dst_enry
, IPv4 的路由缓存信息是通过 struct rttable
缓存的。
这是 dst_entry
, hh_cache
和 neighbour
之间的关系。
neighbor 结构是用 hash 存储的,key 是 L3 地址加设备(对应的 device 结构体)加一个随机值。hash 表通过 neigh_hash_alloc
和 neigh_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 | neigh = __ipv4_neigh_lookup_noref(dev, nexthop); |
当 TCP 收到报文(比如对端的 SYN/ACK 时),这种外部信息说明其实这个节点是可达的(不是来自 gateway),也可以更新缓存。
另外,neigh_connect
和 neigh_suspect
是两个状态转换时会调用的函数。
当 neigh 进入 NUD_REACHABLE
, neigh_connect
把 neigh->output
的函数指向 connected_output
这个函数,它会在调用 dev_queue_xmit
之前填充 L2 头部,把包直接发出去。
当从 NUD_REACHBLE
转换成 NUD_STALE
或者 NUD_DELAY
,neigh_suspect
会强制进行可达性的确认,通过把 neighbor->output
指向 neigh_ops->output
, 也就是 neigh_resolve_output
,它会在调用 dev_queue_xmit
之前先把地址解析出来,等把地址解析完成以后再把缓存的包发送出去。
更新邻居信息
邻居信息更新的入口函数就是 neigh_update
,这个函数定义如下。
1 | int neigh_update(struct neighbour *neigh, const u8 *lladdr, u8 new, |
首先做一些预先检查,如果是管理员配置,原来的配置就不能是 PERMANENT 或者 NOARP 的。
1 | if (!(flags & NEIGH_UPDATE_F_ADMIN) && |
标记为 dead,然后 goto out
1 | if (neigh->dead) |
如果更新为无效的标记的话,删除 timer,并且 supect 一下,如果是 NUD_CONNECTED 状态。
如果是需要标记为失败的 neigh(之前是 INCOMPLETE|NUD_PROBE),则调用 neigh_invalidate
,让这个 neigh 无效。
1 | if (!(new & NUD_VALID)) { |
接下来是三个条件,一个是 device 没有硬件地址,用 neigh->ha,如果 lladdr 提供了并且老的是有效的,使用老的地址,如果 lladdr 没有提供,直接使用老的地址。
1 | /* Compare new lladdr with cached one */ |
如果NUD_CONNECTED
更新 confirm 的时间,更新『更新』的时间。
1 | if (new & NUD_CONNECTED) |
NEIGH_UPDATE_F_OVERRIDE_ISROUTER
标记的是当前 neigh 是一个 router。
1 | /* If entry was valid and address is not changed, |
如果是更新操作,删除老的 timer,如果需要 timer,更新新的 timer,并且设置新状态。
1 | if (new != old) { |
更新 neigh->ha
,如果 lladdr
和 neigh->ha
不同的话。
1 | if (lladdr != neigh->ha) { |
根据状态调用 connect 和 suspect
1 | if (new & NUD_CONNECTED) |
如果之前老的不是 NUD_VALID
,就会把 skb 从 arp_queue,并且释放 arp_queue。
1 | if (!(old & NUD_VALID)) { |
更新 flag,发送 notify 通过 rtnetlink 通知 nlmsg_pid 对应的进程,
1 | out: |
这就是邻居信息的更新流程。
当然还有一个缓存项是 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 的桥梁。
参考:
- Understanding Linux Network Internals