Skip to content

邻居子系统

实现二层通信,在ipv4场景下根据路由结果里的目标IP,通过arp报文获取该IP的mac地址,然后填充到报文里的目的mac。 一般来说报文里的源MAC会填充为发送设备的mac。

ARP处理

普通以太网设备对应表项里的neigh->ops 都为arp_hh_ops

c
static const struct neigh_ops arp_hh_ops = {
	.family =		AF_INET,
	.solicit =		arp_solicit,            //调用这个函数发送 arp request消息
	.error_report =		arp_error_report,   //向上层报告error,通过发送icmp消息,路由不可达
	.output =		neigh_resolve_output,  
	.connected_output =	neigh_resolve_output,
};

TCP发包时的arp处理

ip_finish_output2
    --> ip_neigh_for_gw   // 如果没找到网关,那就是同子网,查找目的ip的表项
	    --> ip_neigh_gw4  // 查找是否有ipv4网关的表项
		    --> __ipv4_neigh_lookup_noref
			--> __neigh_create  // 如果没找到,就新建
		--> ip_neigh_gw6  // 查找是否有ipv6网关的表项
	--> neigh_output     //如果没有错误,则通过该函数把包发出去
	    --> neigh_hh_output        //如果表项是connected状态且有缓冲数据,则有快速路径
		--> neigh_resolve_output   //其他场景
		    --> neigh_event_send
			    --> neigh_event_send_probe
				    --> __neigh_event_send  // 不是NUD_CONNECTED,NUD_DELAY,NUD_PROBE 这三个状态的其中之一,就发arp request

neigh_event_send如果返回0则直接根据表项里的mac地址填充到报文里的目的mac, 报文里的源mac是dev的mac, 然后发出去。返回1说明要么包丢弃,要么skb暂时缓冲在neigh表项的一个队列里(incomplete状态时),等收到arp response后再发。

返回 0 的情况:
处于 NUD_CONNECTED | NUD_DELAY | NUD_PROBE 这三种状态其中一种
处于 NUD_STALE 状态, 修改为 NUD_DELAY

返回 1 的情况:
处于 NUD_NONE 状态的, 修改状态为 NUD_INCOMPLETE,设置定时器,发送 arp request,skb暂放入一个表项维护的队列里
处于 NUD_INCOMPLETE 状态的, skb暂放入一个表项维护的队列里

内核参数arp_announce 用于主动发arp request场景,在arp_solicit发送arp请求时调用。决定arp request报文里的src ip 的值。
0 默认值,只要skb->saddr是本机的IP就能作为arp报文里的src ip
1 skb->saddr是本地IP,同时跟网卡IP,目标IP都是同子网,才能作为arp报文里的src ip
2 选取网卡的主IP

收到arp报文后的处理

接收到arp requets后一般会发送are response, 如下参数是更细致的控制。

arp_ignore
0 默认值,主要IP是本地的,不管是不是配置在接收包的网卡上面,都响应
1 只有IP是配置在接收包的网卡上,才响应
2 不仅P是配置在接收包的网卡上,而且跟src ip 是同一个子网才响应

arp_filter
1 反向路由的目标网卡是接收消息的网卡时才允许响应,否则就被过滤掉。 计数器 为nstat -r -z | grep ArpFilter
0 默认值,即使请求的IP是其他网卡的, 也运行发response, 这可能是个错误,但 可以理解, 因为IP是属于主机的,不是属于某个网卡的

收到arp response后一般是更新下已有的表现, 如下参数是更细致的控制。

arp_accept
定义收到免费ARP且ip不在arp表项里是的行为
0 默认值, 不创建表项
1 创建表项

ip 命令

代码实现上arp表是全局变量, 但 ip neigh 在不同网络空间中执行的结果不一样。可以认为是隔离的,原因是 在neigh_dump_table函数里会检查每一个arp记录,对应的网卡的网络空间跟当前执行命令的网络空间是否一致, 一致才会返回结果。

c
static int neigh_dump_table(struct neigh_table *tbl, struct sk_buff *skb,
			    struct netlink_callback *cb,
			    struct neigh_dump_filter *filter)
{
	struct net *net = sock_net(skb->sk);
	struct neighbour *n;
	int rc, h, s_h = cb->args[1];
	int idx, s_idx = idx = cb->args[2];
	struct neigh_hash_table *nht;
	unsigned int flags = NLM_F_MULTI;

	if (filter->dev_idx || filter->master_idx)
		flags |= NLM_F_DUMP_FILTERED;

	rcu_read_lock_bh();
	nht = rcu_dereference_bh(tbl->nht);

	for (h = s_h; h < (1 << nht->hash_shift); h++) {
		if (h > s_h)
			s_idx = 0;
		for (n = rcu_dereference_bh(nht->hash_buckets[h]), idx = 0;
		     n != NULL;
		     n = rcu_dereference_bh(n->next)) {
			if (idx < s_idx || !net_eq(dev_net(n->dev), net))    // 判断网络空间是否一致
				goto next;
			if (neigh_ifindex_filtered(n->dev, filter->dev_idx) ||
			    neigh_master_filtered(n->dev, filter->master_idx))
				goto next;
			if (neigh_fill_info(skb, n, NETLINK_CB(cb->skb).portid,
					    cb->nlh->nlmsg_seq,
					    RTM_NEWNEIGH,
					    flags) < 0) {
				rc = -1;
				goto out;
			}
next:
			idx++;
		}
	}
	rc = skb->len;
out:
	rcu_read_unlock_bh();
	cb->args[1] = h;
	cb->args[2] = idx;
	return rc;
}

ip -s neigh可以显示更详细的信息, 下面例子中第一项里
192.168.3.11 的mac地址为 68:ec:c5:f1:da:6f,属于 eth0设备。
2312/0/2312 最近一次used,confirmed,updated的时间,自系统启动后的秒数。
probes 4 代表neigh->probes的值

192.168.3.11 dev eth0 lladdr 68:ec:c5:f1:da:6f  ref 1 used 2312/0/2312probes 4 REACHABLE
192.168.3.1 dev eth0 lladdr f8:20:a9:3c:eb:74  ref 1 used 3/255/3probes 1 DELA

ip monitor neigh可以实时观察neigh表项的状态变化, 注意这里只记录是定时器超时时发生的状态变化, 当发送路径neigh_event_send里触发的状态变化不会体现。

# ip -ts monitor neigh
[2025-06-26T22:50:45.728950] 192.168.3.11 dev eth0 FAILED
[2025-06-26T22:50:45.729083] Deleted 192.168.3.11 dev eth0 FAILED
[2025-06-26T22:50:45.729511] 192.168.3.11 dev eth0 lladdr 68:ec:c5:f1:da:6f REACHABLE

内核相关的数据结构

全局变量struct neigh_table arp_tbl 是arp表,所有网络空间共用。arp_tbl->nht指向hash表,里面保存 所有的表项,能动态扩容。

c
RCU_INIT_POINTER(tbl->nht, neigh_hash_alloc(3));

struct neighbour部分字段的解释:
neigh->used 在每次发包时更新, 单位:jiffies
neigh->updated 表项的状态(比如NUD_STALE变为NUD_DELAY)发生变化的最近一次的时间 单位:jiffies
neigh->confirmed 收到arp request后更新表项或者添加删除时时间 单位:jiffies

arp状态迁移

每个邻居表项都有定时器, 超时就执行neigh_timer_handler会改变状态,如下是具体的状态迁移: 状态迁移

参考:https://blogs.oracle.com/linux/post/arp-internals

ipv6 ndp 处理

Released under the MIT License.