yy / NDP

Created Tue, 01 Oct 2024 00:00:00 +0800 Modified Sun, 19 Apr 2026 07:54:45 +0200
5013 Words

ND协议/NDP原理

邻居发现 - S600-E V200R021C00 配置指南-IP业务 - 华为 (huawei.com)

odhcpd 中继模式原理、局限以及解决方案 | Silent Blog (icpz.dev)

IPv6的邻居状态

NDP的一个重要特征是检测同一链路上以前相连通的两个节点现在是否依然连通,这是通过NUD(Neighbor Unreachability Detection,邻居不可达检测)完成的。NUD帮助维护多个邻居条目组成的邻居缓存,每个邻居都有相应的状态,状态之间可以迁移。

RFC2461中定义了5种邻居状态,分别是:

• 未完成(Incomplete):表示正在解析地址,但邻居链路层地址尚未确定。

• 可达(Reachable):表示地址解析成功,该邻居可达。

• 陈旧(Stale):表示可达时间耗尽,未确定邻居是否可达。

• 延迟(Delay):邻居可达性未知。Delay状态不是一个稳定的状态,而是一个延时等待状态。

• 探测(Probe):邻居可达性未知,正在发送邻居请求探针以确认可达性.

邻居状态迁移过程

邻居状态的具体迁移过程如图所示:

neigh_state

下面以A、B两个邻居节点之间相互通信过程中A节点的邻居状态变化为例(假设A、B之前从未通信),说明邻居状态迁移的过程。

1、A先发送NS报文,并生成缓存条目,此时,邻居状态为Incomplete。

2、若B回复NA报文,则邻居状态由Incomplete变为Reachable,否则固定时间后邻居状态由Incomplete变为Empty,即删除表项。

3、经过邻居可达时间,邻居状态由Reachable(默认30s)变为Stale,即未知是否可达。

4、如果在Reachable状态,A收到B的非请求NA报文,且报文中携带的B的链路层地址和表项中不同,则邻居状态马上变为Stale。

5、在Stale状态若A要向B发送数据,则邻居状态由Stale变为Delay,并发送NS请求。

6、在经过一段固定时间后,邻居状态由Delay(默认5s)变为Probe(每隔1s发送一次NS报文,连续发送3次),其间若有NA应答,则邻居状态由Delay变为Reachable。

7、在Probe状态,A每隔1s发送单播NS,发送3次后,有应答则邻居状态变为Reachable,否则邻居状态变为Empty,即删除表项。

odhcpd基本架构

源码目录结构非常清晰,见名知意

odhcpd
├── CMakeLists.txt
├── COPYING
├── README
└── src
    ├── config.c
    ├── dhcpv4.c
    ├── dhcpv4.h
    ├── dhcpv6.c
    ├── dhcpv6.h
    ├── dhcpv6-ia.c
    ├── ndp.c
    ├── netlink.c
    ├── odhcpd.c
    ├── odhcpd.h
    ├── router.c
    ├── router.h
    └── ubus.c

这里同时也给出一个示例配置(ra relay|ndp relay)

# cat /etc/config/dhcp
config dhcp
        option interface 'wan'
        option ifname 'wan1_4096'
        option master '1'
        option ra 'relay'
        option ndp 'relay'
        option dhcpv4 'disabled'
        option dhcpv6 'disabled'
 
config dhcp
        option interface 'lan'
        option ifname 'br0'
        # option ndproxy_slave '1'
        option ra 'relay'
        option ndp 'relay'
        option dhcpv4 'disabled'
        option dhcpv6 'disabled'
 
config odhcpd 'odhcpd'
        option legacy '0'
        option maindhcp '0'
        option leasefile '/tmp/hosts/odhcpd'
        option leasetrigger '/tmp/odhcpd-update'
        option loglevel '7'

由于重点是分析 RA relay 和 NPD relay,这两部分与 route.cndp.c 相关,据此去分析 odhcpd 的架构

首先找到整个程序的入口,在 odhcpd.cmain 函数中,可以看到首先调用了 netlink_init,当系统支持 ipv6 时,会调用 ndp_init,最后会调用 odhcpd_run

// odhcpd.c
int main(int argc, char **argv) {
	...
	/* 对netlink初始化
		借助libnl库,创建与netlink通信的套接字,使用了NETLINK_ROUTE协议族
		并将其加入uloop的事件循环中
	*/
	if (netlink_init())
		return 4;

    // 检查系统是否支持ipv6
	if (ipv6_enabled()) {
        // 初始化route,为netlilnk事件注册route回调
		if (router_init())
			return 4;

		if (dhcpv6_init())
			return 4;

        // 初始化ndp,为netlilnk事件注册ndp回调
		if (ndp_init())
			return 4;
	}

	...
	// 读取配置 注册事件循环
	odhcpd_run();
	return 0;
}

先来看一下几个 init 函数在做什么:

netlink_init 函数如下,创建了与 netlink 通信的套接字,记录套接字对应的描述符,设置 netlink 套接字的回调函数 cb_rtnl_valid,加入多个Netlink组接收特定类型的网络事件,注册事件。

odhcpd_register 函数会将向 uloop 添加监听文件描述符,内部采用 epoll 模型,当套接字有数据可读,即调用回调函数处理

// netlink.c
int netlink_init(void) {
	...

	rtnl_event.sock = create_socket(NETLINK_ROUTE);
	if (!rtnl_event.sock) {
		syslog(LOG_ERR, "Unable to open nl event socket: %m");
		goto err;
	}

	rtnl_event.ev.uloop.fd = nl_socket_get_fd(rtnl_event.sock);
	...

	// 此处设置了netlink的回调函数 cb_rtnl_valid
	nl_socket_modify_cb(rtnl_event.sock, NL_CB_VALID, NL_CB_CUSTOM,
			cb_rtnl_valid, NULL);

	/* Receive IPv4 address, IPv6 address, IPv6 routes and neighbor events */
    // 加入多个Netlink组 以接收特定类型的网络事件
	if (nl_socket_add_memberships(rtnl_event.sock, RTNLGRP_IPV4_IFADDR,
				RTNLGRP_IPV6_IFADDR, RTNLGRP_IPV6_ROUTE,
				RTNLGRP_NEIGH, RTNLGRP_LINK, 0))
		goto err;
	// 注册事件,通过uloop监听描述符
	odhcpd_register(&rtnl_event.ev);

	return 0;
	...
}

接下来看一下 cb_rtnl_valid 函数,这里接收了来自 netlink 的消息后,根据类型,调用对应的处理函数

// netlink.c
static int cb_rtnl_valid(struct nl_msg *msg, _unused void *arg) {
	struct nlmsghdr *hdr = nlmsg_hdr(msg);
	int ret = NL_SKIP;
	bool add = false;

	switch (hdr->nlmsg_type) {
	case RTM_NEWLINK:
		ret = handle_rtm_link(hdr);
		break;

	case RTM_NEWROUTE:
		add = true;
		/* fall through */
	case RTM_DELROUTE:
		ret = handle_rtm_route(hdr, add);
		break;

	case RTM_NEWADDR:
		add = true;
		/* fall through */
	case RTM_DELADDR:
		ret = handle_rtm_addr(hdr, add);
		break;

	case RTM_NEWNEIGH:
		add = true;
		/* fall through */
	case RTM_DELNEIGH:
		ret = handle_rtm_neigh(hdr, add);
		break;

	default:
		break;
	}

	return ret;
}

route_init ndp_init 代码如下,可以发现都是调用 netlink_add_netevent_handler 函数,添加回调处理函数

这里没有贴出 dhcpv6_init 的代码,但可以推测也是调用 netlink_add_netevent_handler (不信自行验证)

// router.c
static struct netevent_handler router_netevent_handler = { .cb = router_netevent_cb, };
int router_init(void) {
	...
	if (netlink_add_netevent_handler(&router_netevent_handler) < 0) {
		syslog(LOG_ERR, "Failed to add netevent handler");
		ret = -1;
	}
	...
}
// ndp.c
static struct netevent_handler ndp_netevent_handler = { .cb = ndp_netevent_cb, };
int ndp_init(void) {
	...
	if (netlink_add_netevent_handler(&ndp_netevent_handler) < 0) {
		syslog(LOG_ERR, "Failed to add ndp netevent handler");
		ret = -1;
	}
	...
}

可以看到,这里是将各个 handler 加入到链表 netevent_handler_list 中了

// netlink.c
int netlink_add_netevent_handler(struct netevent_handler *handler) {
	if (!handler->cb)
		return -1;

	list_add(&handler->head, &netevent_handler_list);

	return 0;
}

那么顺便看一下这个链表在哪里会有用,可以看到在 call_netevent_handler_list 函数中会调用每个 handlecb,而该函数又会被多个函数调用,这里列出两个函数 handle_rtm_route handle_rtm_neigh,由此可见,在 netlink 中当某个事件发生时,就会触发上述添加的回调函数。是否还记得上面 netlink_init 中对套接字设置的回调函数 cb_rtnl_valid ,那里就是调用这些 handle_xxx 的入口。

比如路由或者邻居发生变化时,handle_rtm_routehandle_rtm_neigh 会调用 call_netevent_handler_list ,然后继续调用到 handle->cb 函数,即调用了 router_netevent_cb ndp_netevent_cb

// netlink.c
static void call_netevent_handler_list(unsigned long event, struct netevent_handler_info *info) {
	struct netevent_handler *handler;

	list_for_each_entry(handler, &netevent_handler_list, head)
		handler->cb(event, info);
}

static int handle_rtm_route(struct nlmsghdr *hdr, bool add) {
    ...
    avl_for_each_element(&interfaces, iface, avl) {
        ...
        call_netevent_handler_list(add ? NETEV_ROUTE6_ADD : NETEV_ROUTE6_DEL, &event_info);
        ...
    }
    ...
}

static int handle_rtm_neigh(struct nlmsghdr *hdr, bool add) {
    ...
    avl_for_each_element(&interfaces, iface, avl) {
        ...
        call_netevent_handler_list(add ? NETEV_ROUTE6_ADD : NETEV_ROUTE6_DEL, &event_info);
        ...
    }
    ...
}

接下来在 config.codhcpd_run 中,下边调用了 odhcp_reload,该函数从 /etc/config/dhcp 读取配置完成配置初始化,最后遍历每一个 interface 重新加载服务,这里的每一个 interface 即对应了配置文件中的 config/dhcp 配置节,具体的接口就是配置项 ifname

// config.c
void odhcpd_reload(void) {
	...
	if (!uci_load(uci, "dhcp", &dhcp)) {
		...
		/* 1. Global settings */
		uci_foreach_element(&dhcp->sections, e) {
			struct uci_section *s = uci_to_section(e);
			if (!strcmp(s->type, "odhcpd"))
				set_config(s);
		}

		/* 2. DHCP pools */
		uci_foreach_element(&dhcp->sections, e) {
			struct uci_section *s = uci_to_section(e);
			if (!strcmp(s->type, "dhcp"))
				set_interface(s);
		}
		...
	}
	...

	avl_for_each_element_safe(&interfaces, i, avl, tmp) {
		...
			reload_services(i);
		} else
			close_interface(i);
	}
	...
}

reload_services 函数中,对各个接口上的功能进行了设置

// config.c
void reload_services(struct interface *iface) {
	if (iface->ifflags & IFF_RUNNING) {
		syslog(LOG_DEBUG, "Enabling services with %s running", iface->ifname);
		router_setup_interface(iface, iface->ra != MODE_DISABLED);
		dhcpv6_setup_interface(iface, iface->dhcpv6 != MODE_DISABLED);
		ndp_setup_interface(iface, iface->ndp != MODE_DISABLED);
		...
	} else {
		...
	}
}

这里还是看和 router/ndp 相关的两个函数 router_setup_interface ndp_setup_interface,这里两个函数都是创建套接字,然后设置选项,设置过滤哪些类型的数据,设置套接字来数据时的回调函数,将套接字对应的文件描述符加入 uloop 事件循环

// router.c
int router_setup_interface(struct interface *iface, bool enable) {
	...

	if (!enable && iface->router_event.uloop.fd >= 0) {
		...
	} else if (enable) {
		...
		if (iface->router_event.uloop.fd < 0) {
			/* Open ICMPv6 socket */
			iface->router_event.uloop.fd = socket(AF_INET6, SOCK_RAW | SOCK_CLOEXEC,
								IPPROTO_ICMPV6);
			if (iface->router_event.uloop.fd < 0) {
				...
			}

			if (setsockopt(iface->router_event.uloop.fd, SOL_SOCKET, SO_BINDTODEVICE,
						iface->ifname, strlen(iface->ifname)) < 0) {
				...
			}

			/* Let the kernel compute our checksums */
			if (setsockopt(iface->router_event.uloop.fd, IPPROTO_RAW, IPV6_CHECKSUM,
						&val, sizeof(val)) < 0) {
				...
			}

			/* This is required by RFC 4861 */
			val = 255;
			if (setsockopt(iface->router_event.uloop.fd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS,
						&val, sizeof(val)) < 0) {
				...
			}

			if (setsockopt(iface->router_event.uloop.fd, IPPROTO_IPV6, IPV6_UNICAST_HOPS,
						&val, sizeof(val)) < 0) {
				...
			}

			/* We need to know the source interface */
			val = 1;
			if (setsockopt(iface->router_event.uloop.fd, IPPROTO_IPV6, IPV6_RECVPKTINFO,
						&val, sizeof(val)) < 0) {
				...
			}

			if (setsockopt(iface->router_event.uloop.fd, IPPROTO_IPV6, IPV6_RECVHOPLIMIT,
						&val, sizeof(val)) < 0) {
				...
			}

			/* Don't loop back */
			val = 0;
			if (setsockopt(iface->router_event.uloop.fd, IPPROTO_IPV6, IPV6_MULTICAST_LOOP,
						&val, sizeof(val)) < 0) {
				...
			}

			/* Filter ICMPv6 package types */
			ICMP6_FILTER_SETBLOCKALL(&filt);
			ICMP6_FILTER_SETPASS(ND_ROUTER_ADVERT, &filt);
			ICMP6_FILTER_SETPASS(ND_ROUTER_SOLICIT, &filt);
			if (setsockopt(iface->router_event.uloop.fd, IPPROTO_ICMPV6, ICMP6_FILTER,
						&filt, sizeof(filt)) < 0) {
				...
			}
			// 将套接字加入uloop事件循环 并设置回调函数 handle_icmpv6
			iface->router_event.handle_dgram = handle_icmpv6;
			iface->ra_sent = 0;
			odhcpd_register(&iface->router_event);
		} else {
			...
		}

		memset(&mreq, 0, sizeof(mreq));
		mreq.ipv6mr_interface = iface->ifindex;
		inet_pton(AF_INET6, ALL_IPV6_ROUTERS, &mreq.ipv6mr_multiaddr);
		// 如果该接口RA配置为中继模式 且是主接口 则直接从主接口发送一次RS消息
		if (iface->ra == MODE_RELAY && iface->master) {
			inet_pton(AF_INET6, ALL_IPV6_NODES, &mreq.ipv6mr_multiaddr);
			forward_router_solicitation(iface);
		} else if (iface->ra == MODE_SERVER) {
			...
		}

		...
	}
out:
	...

	return ret;
}
// ndp.c
int ndp_setup_interface(struct interface *iface, bool enable) {
	...

	snprintf(procbuf, sizeof(procbuf), "/proc/sys/net/ipv6/conf/%s/proxy_ndp", iface->ifname);
	procfd = open(procbuf, O_WRONLY);

	...

	if (enable) {
		struct sockaddr_ll ll;
		struct packet_mreq mreq;
		struct icmp6_filter filt;
		int val = 2;
		// 如果开启npd且是relay模式 则设置对应接口的 /proc/sys/net/ipv6/conf/<ifname>/proxy_ndp为1
		if (write(procfd, "1\n", 2) < 0) {}

		/* Open ICMPv6 socket */
		iface->ndp_ping_fd = socket(AF_INET6, SOCK_RAW | SOCK_CLOEXEC, IPPROTO_ICMPV6);
		...

		if (setsockopt(iface->ndp_ping_fd, SOL_SOCKET, SO_BINDTODEVICE,
			       iface->ifname, strlen(iface->ifname)) < 0) {
			...
		}

		if (setsockopt(iface->ndp_ping_fd, IPPROTO_RAW, IPV6_CHECKSUM,
				&val, sizeof(val)) < 0) {
			...
		}

		/* This is required by RFC 4861 */
		val = 255;
		if (setsockopt(iface->ndp_ping_fd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS,
			       &val, sizeof(val)) < 0) {
			...
		}

		if (setsockopt(iface->ndp_ping_fd, IPPROTO_IPV6, IPV6_UNICAST_HOPS,
			       &val, sizeof(val)) < 0) {
			...
		}

		/* Filter all packages, we only want to send */
		ICMP6_FILTER_SETBLOCKALL(&filt);
		if (setsockopt(iface->ndp_ping_fd, IPPROTO_ICMPV6, ICMP6_FILTER,
			       &filt, sizeof(filt)) < 0) {
			...
		}


		iface->ndp_event.uloop.fd = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC, htons(ETH_P_IPV6));
		if (iface->ndp_event.uloop.fd < 0) {
			...
		}

		...

		if (setsockopt(iface->ndp_event.uloop.fd, SOL_SOCKET, SO_ATTACH_FILTER,
				&bpf_prog, sizeof(bpf_prog))) {
			...
		}

		...

		if (bind(iface->ndp_event.uloop.fd, (struct sockaddr*)&ll, sizeof(ll)) < 0) {
			...
		}

		...

		if (setsockopt(iface->ndp_event.uloop.fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP,
				&mreq, sizeof(mreq)) < 0) {
			...
		}
		// 将套接字加入uloop事件循环 并设置回调函数 handle_solicit
		iface->ndp_event.handle_dgram = handle_solicit;
		odhcpd_register(&iface->ndp_event);

		...
	}

	...

 out:
	...

	return ret;
}

至此对于 odhcpd 的架构已经明了,整体框架是基于 uloop 的事件监听,当套接字有消息可读,就会调用对应的回调函数,在此基础上,netlinkuloop 添加事件监听,并在有消息时调用回调判断消息类型,调用注册到 netlink 事件链表中各个模块的函数,而 router ndp 等对配置的每个接口向 uloop 添加事件监听,并在有消息时调用回调函数。

即:

netlink 会在消息到来调用回调 cb_rtnl_valid,根据消息类型调用各个模块的添加的回调函数 xxx_netevent_cb

routernetlink 对应类型的消息到来会调用回调 router_netevent_cb, 对接口到来的消息会调用 handle_icmpv6

ndpnetlink 对应类型的消息到来会调用回调 ndp_netevent_cb, 对接口到来的消息会调用 handle_solicit

ROUTER

// router.c
/**
 * router_netevent_cb - 处理网络事件的回调函数
 * @event: 发生的网络事件类型
 * @info: 包含事件相关详细信息的结构体指针
 *
 * 此函数根据传入的网络事件类型,执行相应的处理逻辑。主要处理的事件类型包括接口索引变更、
 * IPv6路由添加或删除、IPv6地址列表变更。对于不同的事件类型,执行不同的操作,如更新接口状态、
 * 触发路由更新或地址更新的定时器(trigger_router_advert)等。
 */
static void router_netevent_cb(unsigned long event, struct netevent_handler_info *info) {
	struct interface *iface;

	switch (event) {
	case NETEV_IFINDEX_CHANGE:
		/* 当网络接口索引发生变化时,注销并关闭该接口的文件描述符 */
		iface = info->iface;
		if (iface && iface->router_event.uloop.fd >= 0) {
			if (iface->router_event.uloop.registered)
				uloop_fd_delete(&iface->router_event.uloop);

			close(iface->router_event.uloop.fd);
			iface->router_event.uloop.fd = -1;
		}
		break;
	case NETEV_ROUTE6_ADD:
	case NETEV_ROUTE6_DEL:
		/* 当添加或删除IPv6路由时,针对服务器模式的接口更新定时器 */
		if (info->rt.dst_len)
			break;

		avl_for_each_element(&interfaces, iface, avl) {
			if (iface->ra == MODE_SERVER && !iface->master)
				uloop_timeout_set(&iface->timer_rs, 1000);
		}
		break;
	case NETEV_ADDR6LIST_CHANGE:
		/* 当IPv6地址列表发生变化时,对于服务器模式的接口更新定时器 */
		iface = info->iface;
		if (iface && iface->ra == MODE_SERVER && !iface->master)
			uloop_timeout_set(&iface->timer_rs, 1000);
		break;
	default:
		/* 对于未处理的事件类型,不做任何操作 */
		break;
	}
}
// router.c
/* Event handler for incoming ICMPv6 packets */
/*
 * 处理IPv6的ICMP消息。
 * 
 * 参数:
 *  - addr: 源地址的指针。
 *  - data: 数据的指针,即ICMP报文的内容。
 *  - len: 数据长度。
 *  - iface: 当前接口的指针。
 *  - dest: 未使用的参数。
 */
static void handle_icmpv6(void *addr, void *data, size_t len,
		struct interface *iface, _unused void *dest) {
	struct icmp6_hdr *hdr = data; // ICMPv6报文头指针
	struct sockaddr_in6 *from = addr; // 源地址的详细信息

	// 验证ICMPv6报文的有效性,无效则直接返回
	if (!router_icmpv6_valid(addr, data, len))
		return;

	// 根据接口的角色(服务器模式或中继模式)来处理不同的ICMPv6类型
	if ((iface->ra == MODE_SERVER && !iface->master)) { /* 服务器模式 */
		// 对于路由器请求报文,发送路由器通告报文
		if (hdr->icmp6_type == ND_ROUTER_SOLICIT)
			send_router_advert(iface, &from->sin6_addr);
	} else if (iface->ra == MODE_RELAY) { /* 中继模式 */
		// 在中继模式下,根据报文类型和接口状态进行相应的处理
		if (hdr->icmp6_type == ND_ROUTER_SOLICIT && !iface->master) {
			// 转发路由器请求报文给其他中继接口
			struct interface *c;

			avl_for_each_element(&interfaces, c, avl) {
				if (!c->master || c->ra != MODE_RELAY)
					continue;

				forward_router_solicitation(c);
			}
		} else if (hdr->icmp6_type == ND_ROUTER_ADVERT && iface->master)
			// 转发路由器通告报文
			forward_router_advertisement(iface, data, len);
	}
}

NDP

// ndp.c
/*
 * NDP网络事件回调函数
 * 
 * 此函数处理有关IPv6邻居发现协议(NDP)的网络事件,例如地址添加/删除、邻居添加/删除。
 * 根据事件类型执行相应的配置或清理工作。
 *
 * @param event  发生的网络事件类型
 * @param info   包含事件详细信息的结构体指针,例如接口信息、地址或邻居信息
 */
static void ndp_netevent_cb(unsigned long event, struct netevent_handler_info *info) {
	struct interface *iface = info->iface;
	bool add = true; // 默认为添加操作

	// 如果接口不存在或NDP模式已禁用,则直接返回
	if (!iface || iface->ndp == MODE_DISABLED)
		return;

	switch (event) {
	case NETEV_ADDR6_DEL:
		// 地址删除操作,设置为删除模式,并清理邻居表
		add = false;
		netlink_dump_neigh_table(false);
		/* fall through */
	case NETEV_ADDR6_ADD:
		// 地址添加操作,为中继设置地址
		setup_addr_for_relaying(&info->addr.in6, iface, add);
		break;
	case NETEV_NEIGH6_DEL:
		// 邻居删除操作,设置为删除模式,并清理邻居表
		add = false;
		/* fall through */
	case NETEV_NEIGH6_ADD:
		// 邻居添加操作
		if (info->neigh.flags & NTF_PROXY) {
			// 处理代理邻居的情况
			if (add) {
				// 添加代理邻居,并设置相关路由
				netlink_setup_proxy_neigh(&info->neigh.dst.in6, iface->ifindex, false);
				setup_route(&info->neigh.dst.in6, iface, false);
				// 载入邻居表
				netlink_dump_neigh_table(false);
			}
			break;
		}

		// 非代理邻居的添加或删除操作
		if (add &&
		    !(info->neigh.state &
		      (NUD_REACHABLE|NUD_STALE|NUD_DELAY|NUD_PROBE|NUD_PERMANENT|NUD_NOARP)))
			break;

		// 设置中继地址和路由
		setup_addr_for_relaying(&info->neigh.dst.in6, iface, add);
		setup_route(&info->neigh.dst.in6, iface, add);

		// 删除操作时,清理邻居表
		if (!add)
			netlink_dump_neigh_table(false);
		break;
	default:
		// 未知事件,不处理
		break;
	}
}
// ndp.c
/* Handle solicitations */
/*
 * 处理邻居请求(Neighbor Solicitation,NS)报文。
 * 
 * 参数:
 * addr - 发送请求的源地址。
 * data - 请求报文的数据指针。
 * len - 请求报文的长度。
 * iface - 接收到请求的网络接口。
 * dest - 未使用的参数。
 */
static void handle_solicit(void *addr, void *data, size_t len,
		struct interface *iface, _unused void *dest) {
	struct ip6_hdr *ip6 = data; // 指向IPv6头部。
	struct nd_neighbor_solicit *req = (struct nd_neighbor_solicit*)&ip6[1]; // 指向邻居请求结构体。
	struct sockaddr_ll *ll = addr; // 源地址的链路层地址。
	struct interface *c;
	char ipbuf[INET6_ADDRSTRLEN];
	uint8_t mac[6];

	/* 邻居请求用于重复地址检测(DAD) */
	bool ns_is_dad = IN6_IS_ADDR_UNSPECIFIED(&ip6->ip6_src);

	/* 如果接口不是中继模式,或者如果是外部接口且不是DAD请求,则不处理 */
	if (iface->ndp != MODE_RELAY || (iface->external && !ns_is_dad))
		return;

	// 校验报文总长度是否合法。
	if (len < sizeof(*ip6) + sizeof(*req))
		return; // 非法的总长度

	// 检查目标地址是否为链路本地、回环或组播地址,若是则不处理。
	if (IN6_IS_ADDR_LINKLOCAL(&req->nd_ns_target) ||
			IN6_IS_ADDR_LOOPBACK(&req->nd_ns_target) ||
			IN6_IS_ADDR_MULTICAST(&req->nd_ns_target))
		return; /* 无效的目标地址 */

	// 记录接收到的请求的目标地址。
	inet_ntop(AF_INET6, &req->nd_ns_target, ipbuf, sizeof(ipbuf));
	syslog(LOG_DEBUG, "Got a NS for %s on %s", ipbuf, iface->name);

	// 获取接口的MAC地址,并检查是否为回环报文。
	odhcpd_get_mac(iface, mac);
	if (!memcmp(ll->sll_addr, mac, sizeof(mac)))
		return; /* 回环报文,不处理 */

	// 遍历所有接口,对于中继模式的接口,如果不是当前接口,并且(是DAD请求或者不是外部接口),则发送ping6。
	avl_for_each_element(&interfaces, c, avl) {
		if (iface != c && c->ndp == MODE_RELAY &&
				(ns_is_dad || !c->external))
			ping6(&req->nd_ns_target, c);
	}

	/* 对于地址不是组播的NS请求,且源地址是链路本地地址的情况,手动发送邻居广告(NA)响应。
	 * 这是处理代理邻居的情况。 */
	if (!IN6_IS_ADDR_MULTICAST(&ip6->ip6_dst) &&
			IN6_IS_ADDR_LINKLOCAL(&ip6->ip6_src)) {
		bool is_proxy_neigh = netlink_get_interface_proxy_neigh(iface->ifindex,
				&req->nd_ns_target) == 1;

		if (is_proxy_neigh)
			send_na(&ip6->ip6_src, iface, &req->nd_ns_target, mac);
	}
}