bigywater.tech
网络协议 · 25

邻居发现 NDP:取代 ARP 的 NS / NA / RS / RA

IPv6 没有 ARP,邻居的链路层地址怎么解析?“邻居不可达检测”到底是怎样的状态机?为什么 ND 报文的跳数限制必须是 255?这篇在本机的 en0 上抓真实的 NS/NA,让一个邻居从 STALE 走到 REACHABLE,再去解析一个不存在的地址。

5.15 s
对 STALE 邻居发数据后,DELAY 阶段结束、本机发出单播 NS 的时刻;5.38 s 收到 NA
4 个 NS
解析不存在的邻居时在 4.5 秒内发出的 NS 个数,间隔 1.0~1.9 秒,发往被请求节点组播地址
255
抓到的全部 ND 报文的跳数限制;应用用普通套接字发 RS 会被拒绝

缓存与状态

实测 · 本机执行输出
  en0 上的邻居缓存 172 项(另有 5 项 incomplete):状态 S=169;R=3(R = REACHABLE,S = STALE,D = DELAY,P = PROBE;incomplete = 还在解析)
  过期时间(剩余):最长 24.0 小时、最短 3 秒;STALE 项最长可以放 24 小时,但下一次发数据时会先重新确认
  en0 的参数:基础可达时间 30s0ms,当前随机化后的可达时间 15s,NS 重传间隔 1s0ms;标志:0x201 PERFORMNUD DAD
  默认路由器列表里有 4 项,其中接口是 en0 的 0 项(其余是 utun 隧道接口)——en0 这条链路上没有收到过路由器通告(RA),所以没有 IPv6 默认路由器,也没有学到全球前缀
  前缀列表里 en0 的条目:有,且“No advertising router”(只有自己的链路本地前缀),没有任何路由器通告过的前缀

邻居缓存里绝大多数是 STALE(没用过、需要时再确认),REACHABLE 很少。en0 上没有收到过路由器通告,所以既没有默认路由器,也没有学到全球前缀。

邻居不可达检测

实测 · 本机执行输出
  选了缓存里一个状态为 STALE 的邻居(地址不写出)发一个 UDP 包。状态变化(相对发包时刻):D @ 0.0s → P @ 5.3s → R @ 5.6s
    +  5150.9 ms  本机 发 邻居请求 NS,以太网目的 单播给邻居,跳数限制 255,选项 [1],校验和正确
    +  5379.1 ms  邻居 发 邻居通告 NA,以太网目的 单播给本机,跳数限制 255 标志 R=0 S=1 O=1,选项 [2],校验和正确
  线上共 2 个相关的 ND 报文;发 UDP 数据的那一下并不等 NUD:数据包立刻就发出去了(数据先发,NUD 在后台确认)

往 STALE 邻居发数据后:先 DELAY 约 5 秒,若上层没有确认可达就进入 PROBE,单播 NS,收到 NA 后回到 REACHABLE。数据包本身不等这个过程。

地址解析

实测 · 本机执行输出
  目标地址是随机的链路本地地址(后 24 位决定被请求节点组播地址,形如 ff02::1:ff__:____);抓到发给它的 NS 4 个
    +     0.0 ms  NS #1:IPv6 目的是被请求节点组播地址(与目标地址的后 24 位对应),以太网目的 MAC = 33:33 + 组播地址的后 4 字节,跳数限制 255,选项 [1](1 = 源链路层地址,内容是本机 MAC)
    +  1571.2 ms  NS #2:IPv6 目的是被请求节点组播地址(与目标地址的后 24 位对应),以太网目的 MAC = 33:33 + 组播地址的后 4 字节,跳数限制 255,选项 [1](1 = 源链路层地址,内容是本机 MAC)
    +  2581.7 ms  NS #3:IPv6 目的是被请求节点组播地址(与目标地址的后 24 位对应),以太网目的 MAC = 33:33 + 组播地址的后 4 字节,跳数限制 255,选项 [1](1 = 源链路层地址,内容是本机 MAC)
    +  4504.9 ms  NS #4:IPv6 目的是被请求节点组播地址(与目标地址的后 24 位对应),以太网目的 MAC = 33:33 + 组播地址的后 4 字节,跳数限制 255,选项 [1](1 = 源链路层地址,内容是本机 MAC)
  NS 之间的间隔(毫秒):[1571, 1010, 1923];ndp -i 给出的重传间隔是 1s0ms,实测间隔在 1.0 到 1.9 秒之间不等,为什么比 1 秒长没有深究
  之后 ndp -an 里这个地址的状态:(incomplete);第二次 sendto:成功

解析新邻居时 NS 发往目标地址对应的被请求节点组播地址,以太网目的 MAC 是 33:33 加后 4 字节;解析一个不存在的邻居时,4.5 秒内发了 4 个 NS(1 个首发加 3 次重传)。

报文格式

实测 · 本机执行输出
  一个 NS 的 ICMPv6 部分(十六进制,目标地址与链路层地址已遮盖):87 00 f7 9b 00 00 00 00 <目标地址 16 字节> 01 01 <MAC 6 字节>
  = 类型 135(135 = 邻居请求)、代码 0、校验和 0xf79b、4 字节保留 + 16 字节目标地址 + 选项(类型 1、长度 1×8 = 8 字节,即 2 + 6 字节 MAC)
  全部 6 个 ND 报文的跳数限制取值:[255];校验和全部正确:True
  类型号:133 路由器请求 RS、134 路由器通告 RA、135 邻居请求 NS、136 邻居通告 NA、137 重定向。NA 的标志位 R = 发送者是路由器、S = 这是对 NS 的回应、O = 覆盖旧的缓存记录
  组播 MAC 映射:IPv6 组播地址的后 4 字节接在 33:33 后面:例如被请求节点组播地址 → 33:33:ff:xx:xx:xx(上面抓到的 NS 的目的 MAC 就是这样)
  用普通 ICMPv6 套接字发路由器请求(类型 133):失败,Operation not permitted——ND 报文只能由内核发,应用不能伪造(也就不能伪造跳数限制 255)

NS 是类型 135 + 4 字节保留 + 16 字节目标地址 + 选项(类型 1 = 源链路层地址);NA 多了 R / S / O 三个标志。跳数限制必须是 255,表明没有经过路由器,应用不能伪造 ND 报文。

对比 ARP

实测 · 本机执行输出
  ARP 缓存(arp -an)en0 上 274 项,其中 incomplete 2 项;NDP 缓存 172 项(+5 项 incomplete)
  IPv4:net.link.ether.inet.max_age: 1200 秒;IPv6 的 STALE 项可以保留的最长剩余时间约 24.0 小时
  ARP 缓存里是 IPv4 地址 ↔ MAC;NDP 缓存里是 IPv6 地址 ↔ MAC,还多了状态机(REACHABLE / STALE / DELAY / PROBE)——ARP 靠定时器过期后重新广播,NDP 在使用时才用单播 NS 探测邻居

ARP 靠广播和定时器;NDP 用多播的被请求节点组减少打扰,并用单播探测验证邻居的可达性。

分步演示

邻居发现 NDP
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。邻居与路由器的地址一律不写出,只写个数与状态;第二节选取的邻居是随机选的一个 STALE 项,发一个 UDP 包即可触发 NUD;第三节的地址是随机生成的、本链路上没有人用的链路本地地址;本机网络没有 IPv6 路由器,所以没有观察到 RS / RA
    • 没有验证:路由器请求 / 通告(RS / RA)的抓包与前缀选项;SLAAC 的重复地址检测(DAD,本机的 DAD 发生在接口启动时,没有重放);重定向(类型 137);NS 重传间隔为什么比 1 秒长;incomplete 状态最终多久放弃、应用会收到什么错误;邻居通告里的 O 标志在地址变化时的作用
    京ICP备2026067502号-1京公网安备11010502063487号