跳到主要内容

8.2 IPv6 地址类型与特殊地址

上一节我们拆解了 IPv6 的头部结构,那个巨大的 128 位地址空间带来了前所未有的地址自由,但也引入了分类上的复杂性。在 IPv4 时代,我们习惯了区分单播、广播和组播,但在 IPv6 里,这个游戏规则变了。

如果你指望在 IPv6 里找到一个类似 255.255.255.255 的广播地址,你会失望的。这不是遗漏,而是设计。

本节我们不仅要建立对 IPv6 地址类型的直觉,还要深入那些在内核代码(特别是 struct in6_addr)中频繁出现的特殊地址——包括那个让人又爱又恨的 Solicited-Node 地址。


8.2.1 三种通信模式

IPv6 的地址架构定义在 RFC 4291 中。地址类型决定了数据包的命运:是去往一个人、最近的一个人,还是一群人。

1. 单播

最直观的模式。单播地址唯一标识一个接口。把包发往单播地址,它会被送到那个唯一的接口。

2. 任播

这是 IPv4 里没有的概念,你可以把它理解为「指向一组接口的单播地址」。

你把包发往一个任播地址,路由器会把它送到这组接口中离你最近的那一个(根据路由协议的度量)。

这个设计非常聪明。想象你有一组内容分发服务器,在全球各地都有镜像。你可以给这组服务器分配同一个任播地址。用户发起请求时,数据包会自动路由到拓扑距离最近的那台服务器,而客户端根本不需要知道这一层——它只知道自己在跟一个地址通信。

3. 组播

组播地址标识一组接口(通常在不同节点上)。发往组播地址的包,该组的所有成员都会收到。

这里有个关键的转折点:IPv6 取消了广播。

为什么?因为广播太吵了。在 IPv4 网络里,ARP 请求通过广播大喊「谁拥有 192.168.1.1?」,同一链路的所有设备都被迫醒来处理这个包,不管跟它有没有关系。

IPv6 不想这么吵。它用组播代替了广播。当你想大喊一声的时候,你不再是向所有人喊,而是向一个「特定的组」喊。如果你不是那个组的成员,你可以安睡。

这也解释了为什么 IPv6 废弃了 ARP,改用了 Neighbor Discovery (NDISC) 协议。NDISC 基于 ICMPv6,它不再用广播链路层大喊大叫,而是优雅地使用组播地址来解析 L3 到 L2 的地址映射。


8.2.2 地址表示与前缀

写 IPv6 地址是个体力活。128 位被分成 8 块,每块 16 位(4 个十六进制数),用冒号隔开:

xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx

但总是写这么多 x 会让人抓狂,所以有两条压缩规则(:: 的妙用):

  1. 前导零可以省略。
  2. 连续的全零块可以用 :: 代替(但只能用一次,否则没法还原)。

关于前缀,它和 IPv4 的子网掩码是一个东西,只是写法变了。我们用 ipv6-address/prefix-length 来表示。

例如 2001:0da7::/32,这表示所有以 2001:0da7 开头的地址都属于这个前缀。/32 意味着前 32 位是网络部分,剩下的是主机部分。


8.2.3 那些必须记住的特殊地址

有些地址在代码里刷屏,你看到它们必须条件反射般地知道它们是谁。

1. 链路本地地址

每个接口都应该有一个。前缀是 fe80::/64

它的作用范围仅限当前链路。路由器看到源或目标是 fe80:: 开头的包,会直接丢弃,绝不转发。

它是 IPv6 正常工作的基石—— Neighbor Discovery、自动配置都离不开它。它是设备之间的「悄悄话」,只有隔壁邻居能听见。

2. 全球单播地址

这是公网身份证。格式很有意思,像千层饼:

[Global Routing Prefix (n bits)] [Subnet ID (m bits)] [Interface ID (128-n-m bits)]
  • Global Routing Prefix:给你的网络分配的号段。
  • Subnet ID:在你的网络里再划分子网。
  • Interface ID:接口标识,子网内唯一。

定义主要参考 RFC 3587。

3. 回环地址

::1。就像 IPv4 的 127.0.0.1。别想太多,就是指自己。

4. 未指定地址

全零,::

这货绝对不能当目的地址用。它主要出现在 DAD(重复地址检测) 过程中,作为源地址发送 NS 消息,意思是「我还没地址,我正在试着占这个坑」。你想用 ip 命令手动配置它是配置不上去的,内核会拦住你。

5. IPv4 映射地址 与 兼容地址

这是过渡时期的产物。

  • IPv4-mapped IPv6::ffff:192.0.2.128。前 80 位是 0,接着 16 位是 1(ffff),最后 32 位是 IPv4 地址。这用于在纯 IPv6 的 socket 结构里表示一个 IPv4 客户端。
  • IPv4-compatible::192.0.2.128。这是早期想法,已废弃。RFC 4291 明确表示这种格式过时了。

6. Site-Local 地址

曾经的 IPv6 私有地址(类似 10.0.0.0/8)。前缀是 fec0::/10。但 RFC 3879 在 2004 年把它干掉了。因为它定义模糊,反而造成路由混乱。现在用全球单播地址配合 Unique Local Addresses (ULA, fc00::/7) 来代替私有地址功能。


8.2.4 内核中的表示:in6_addr

既然我们在折腾内核,就得看看代码里怎么存这玩意儿。直接塞一个 __u8[16] 太死板,有时候我们要按字节操作,有时候按 u16,有时候按 u32

于是 Linux 用了一个 union:

struct in6_addr {
union {
__u8 u6_addr8[16];
__be16 u6_addr16[8];
__be32 u6_addr32[4];
} in6_u;
#define s6_addr in6_u.u6_addr8
#define s6_addr16 in6_u.u6_addr16
#define s6_addr32 in6_u.u6_addr32
};

(include/uapi/linux/in6.h)

这种设计让位操作变得极其顺滑,不用搞一堆强制类型转换。


8.2.5 组播地址详解

组播在 IPv6 里是头等公民。所有的组播地址都以 FF 开头(前 8 位)。

剩下的结构分为三段:

  1. Flags (4 bits):控制标志。
  2. Scope (4 bits):控制范围。
  3. Group ID (112 bits):组 ID。

Flags 字段各位含义(目前主要看低 4 位中的高 4 位,即第 0-3 位,注意位编号习惯):

  • Bit 0: 永久保留。
  • Bit 1 (R-flag): 是否内嵌 Rendezvous Point。这是组播路由的高级话题,涉及 PIM-SM 等,超出本书范围(详见 RFC 3956)。
  • Bit 2 (P-flag): 是否基于网络前缀分配。RFC 3306 定义。
  • Bit 3 (T-flag): 0 表示 IANA 永久分配的(知名地址);1 表示临时分配的。

Scope 字段决定了这组播包能跑多远。内核里用宏定义了这些范围,它们对应于 Table 8-1 中的值:

Hex 值描述Linux 内核宏定义
0x01节点本地IPV6_ADDR_SCOPE_NODELOCAL
0x02链路本地IPV6_ADDR_SCOPE_LINKLOCAL
0x05站点本地IPV6_ADDR_SCOPE_SITELOCAL
0x08组织本地IPV6_ADDR_SCOPE_ORGLOCAL
0x0e全球IPV6_ADDR_SCOPE_GLOBAL

8.2.6 必须知道的几个「特殊组播地址」

有些组播地址是硬编码在协议里的,你必须像背 127.0.0.1 一样背下来。

1. 所有节点地址

ff01::1 (节点本地) 和 ff02::1 (链路本地)。 作用:这就像新大楼的广播喇叭。上一节提到的 ndisc_send_na() 发送的邻居通告,就是往这个地址喊的。

2. 所有路由器地址

ff01::2, ff02::2, ff05::2作用:当你需要找路由器商量事儿(比如发送 RS 请求路由公告)时,你就往这儿发。

3. MLDv2 路由器地址

ff02::16。 这是专门给 MLDv2 能力路由器准备的。稍后讨论 MLD 时会看到 Version 2 的 Multicast Listener Report 发往这里。


8.2.7 被请求节点组播地址

这是一个设计极其精巧的机制,也是 ARP 被 NDISC 替代的核心原因。

问题:在 IPv4 世界里,我要找 192.168.1.5 的 MAC 地址,我就发广播。全网所有设备都醒过来,看一眼是不是找我,不是就扔掉。太吵了。

IPv6 的解法:我能不能只让 192.168.1.5 醒过来,而其他人都睡觉?

这就用到了 Solicited-Node Multicast Address

当一个接口配置了一个单播或任播地址时,它必须同时计算并加入一个对应的组播组。计算方法如下:

  1. 取该单播/任播地址的低 24 位
  2. 拼上前缀 ff02:0:0:0:0:1:ff00::/104

结果就是一个在 ff02:0:0:0:0:1:ff00:0000ff02:0:0:0:0:1:ffff:ffff 范围内的组播地址。

为什么这样设计?

假设你有单播地址 2001:db8::1234:5678。 低 24 位是 34:56:78(假设截断对齐)。 对应的 Solicited-Node 地址是 ff02::1:ff34:5678

当你想要解析这个地址的 MAC 地址时,你不用大喊大叫,你只需要往 ff02::1:ff34:5678 发一个 NS 消息。 虽然理论上可能有其他设备的低 24 位刚好撞车(概率是 1/2^24),但在局域网内这基本可以忽略。就算撞了,那是极小概率事件,比起每次都广播吵醒所有人,这个 trade-off 划算得不得了。

内核里怎么干的?

代码在 addrconf_addr_solict_mult()

/*
* 算法核心:保留低 24 位,拼上固定前缀
*/
static void addrconf_addr_solict_mult(const struct in6_addr *addr, struct in6_addr *solicited)
{
memset(solicited, 0, sizeof(struct in6_addr));
solicited->s6_addr32[0] = __constant_htonl(0xff020000);
solicited->s6_addr32[1] = __constant_htonl(0x00000001);
solicited->s6_addr32[2] = __constant_htonl(0xff000000);
solicited->s6_addr[12] = addr->s6_addr[12];
solicited->s6_addr[13] = addr->s6_addr[13];
solicited->s6_addr[14] = addr->s6_addr[14];
solicited->s6_addr[15] = addr->s6_addr[15];
}

(include/net/addrconf.h)

而加入这个组播组的行为,由 addrconf_join_solict() 完成 (net/ipv6/addrconf.c)。


本节回响

这一节我们建立了 IPv6 的通信模型:从三种基本的地址类型(单播、任播、组播),到内核中那个灵活的 in6_addr union,再到取代 IPv4 广播的各种组播魔法。

还记得开头那个关于「噪音」的比喻吗?IPv6 试图通过引入 Solicited-Node Multicast Address 这种机制,把原本吵闹的局域网通信变成精准的「房间通话」。它用极小的碰撞概率换来了巨大的效率提升,这种工程哲学在 ff02::1:ffxx:xxxx 这个小小的地址结构里体现得淋漓尽致。

有了地址,我们就可以定位对方了。但地址只是终点,怎么去那里?这需要路线图。

下一节,我们将进入 IPv6 Routing,看看 Linux 内核是如何通过 fib6_lookup() 在那个庞大的 128 位空间里,为数据包找到下一站的。