1. Blog
  2. Golang
  3. kubernetes
  4. Rust
  5. 关于作者

了解IPv6以及ND协议

IPv6 从诞生到现在已经很多年了,国家也从政策层面开始推进 IPv6 部署,现在的大部分互联网服务都已经增加了 ipv6 支持。

但目前的很多开发者对 IPv6 完全没有概念,只对 IPv4 熟悉。本文以初学者的角度出发,结合 RFC 已发布标准试图以更利于理解的方式介绍 IPv6 和相关协议。

很多人不会去看 RFC ,一个是原文为英文,二是一些 RFC 内容较长,专业度较高,不够通俗。 但 rfc 是标准文档,涵盖了所有的细节,是 blog 文章无法取代的。如果需要详细的了解网络相关知识,rfc 文档是必不可少的。

推荐一个 rfc 中文网站,支持中英文对照查看 https://rfc2cn.com , 再也不用担心看不懂 rfc 文档啦,so easy。

IPv6 简述

IPv6 在rfc8200定义。

相对于 IPv4,IPv6 将 IP 地址大小从 32 bit 增加到 128 bit,可以认为 IPv6 地址永远用不完了; 精简了 IP 包头部;通过选项的方式来进行 IP 扩展;增加了流标记;更原生的方式支持了身份认证,数据加密。

IPv6 不再或减少使用了 NAT 技术,在 IPv6 的部署中,一般不再使用 NAT 了。

对于硬件转发来说,IPv6 转发效率相对会更高了。

IPv6 地址

rfc4291 规定了 IPv6 地址。

IPv6 有三类寻址:

相对于 IPv4 ,IPv6 使用多播代替了广播。 还有一个非常重要的点,IPv6 强制每个接口都要有 链路本地地址(link local) 地址,以fe08:开头,就像 IPv4 的 169.254.0.0/16 地址。 现在知道为啥网卡上都有一个 IPv6 地址了吧。

IPv6 地址在文本上使用 16 进制编码,每 16bit 用“:”分割,一共有 8 节。

ABCD:EF01:2345:6789:ABCD:EF01:2345:6789

IPv6 地址比较长,部分情况还支持了缩略写法:

::1 是不是也很眼熟,就是 IPv6 的 loopback,相当于 IPv4 的 127.0.0.1

在 IPv4 到 IPv6 过渡期间,IPv4 地址还能使用 IPv6 表示。例如:0:0:0:0:0:0:13.1.68.3,不过这种情况一般不使用。

IPv6 也有 CIDR 表示的格式,ipv6-address/prefix-length,虽然可以用 CIDR 表示,但 IPv6 没有“子网掩码”这个概念。

地址类型:

Address type Binary prefix IPv6 notation
Unspecified 保留 00…0 (128 bits) ::/128
Loopback 回环地址 00…1 (128 bits) ::1/128
Multicast 多播地址 1111 1111 FF00::/8
Link-Local Unicast 链路本地地址 1111 1110 10 FE80::/10
Global Unicast 全球单播地址 其他剩余的  

单播地址:

|         n bits         |   m bits  |       128-n-m bits         |
+------------------------+-----------+----------------------------+
| global routing prefix  | subnet ID |       interface ID         |
+------------------------+-----------+----------------------------+

除以二进制 000 开头的地址外(ipv4 in ipv6), 所有全局单播地址都有一个 64 位 interface ID 字段(此时 n+m=64)。

IPv6 单播地址中的接口标识符(interface ID)用于标识链路上的接口。 它们在子网前缀中必须是唯一的。建议不要将同一接口标识符分配给链路上的不同节点。 在更广泛的范围内,它们也可能是独一无二的。在某些情况下,接口的标识符将直接从该接口的链接层地址派生。 同一接口标识符可用于单个节点上的多个接口,只要它们连接到不同的子网。

也就是说,能够进行路由寻址的单播地址前缀固定为 64 bit,内网接口寻址固定为 64 bit。

举个例子:ISP 给你的 IPv6 地址应当是一个最小可路由到的 IPv6 CIDR, 并且格式应当为 abcd:ef01:2345:6789::/64,像这样:

$ rdisc6 ppp-telecom -1
...
 Prefix                   : 240e:398:e12:2b35::/64
...

从 ISP 路由通告中拿到的 prefix 为 240e:398:e12:2b35::/64,ISP 控制 n+m=64 bit 的前缀 240e:398:e12:2b35, 局域网控制分配 64 bit 的接口 ID。

局域网中的接口根据自己的物理地址(或其他)生成(重点关注 EUI-64 生成方式) 64 位的 interface id,组合起来就是接口的 IPv6 地址。

是不是很简单,根本不用 dhcp,只要保证一个内网中的物理地址唯一就可以完美“分配”IPv6 地址,这就是无状态配置的基础(SLAAC).

多播地址:

|   8    |  4 |  4 |                  112 bits                   |
+------ -+----+----+---------------------------------------------+
|11111111|flgs|scop|                  group ID                   |
+--------+----+----+---------------------------------------------+
                            +-+-+-+-+
flgs is a set of 4 flags:   |0|R|P|T|
                            +-+-+-+-+

flag 中第一位保留为 0

T=0 表示由 Internet 分配号码管理局(IANA)分配的永久分配(“已知”)多播地址;T=1 表示非永久分配(“瞬态”或“动态”分配)多播地址。

P 标志的定义和用法见RFC3306, R 标志的定义和用法见RFC3956

scop 是一个 4 位多播作用域值,用于限制多播组的作用域。数值如下:

0 保留 1 接口本地作用域 2 链接本地作用域 3 保留 4 管理员本地作用域 5 站点本地作用域 6(未分配)7(未分配)8 组织本地作用域 9(未分配)A(未分配)B(未分配)C(未分配)D(未分配)E 全局作用域 F 保留

group ID, 标识给定范围内的永久或暂时多播组。RFC3306中提供了多播组 ID 字段结构的其他定义。

以 RDP 中的 Router Solicitation 包为例,目的地址就是本地多播地址 ff02::2 :

IP6 (flowlabel 0x13fc4, hlim 255, next-header ICMPv6 (58) payload length: 8) fe80::216:3eff:fe67:55c5 > ff02::2: [icmp6 sum ok] ICMP6, router solicitation, length 8

展开为FF02:0:0:0:0:0:0:2,FF 为 11111111 的 hex 编码,0 表示 flags 为|0|0|0|0|,2 表示 scope 为 Link-Local,0:0:0:0:0:0:2表示所有 IPv6 路由器组(这是预定义的 group ID)。

IPv6 头

固定头:

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version| Traffic Class |           Flow Label                  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Payload Length        |  Next Header  |   Hop Limit   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
+                                                               +
|                                                               |
+                         Source Address                        +
|                                                               |
+                                                               +
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
+                                                               +
|                                                               |
+                      Destination Address                      +
|                                                               |
+                                                               +
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
name descriptions
Version 4 位互联网协议版本号=6
Traffic Class 8 位流量等级字段
Flow Label 20 位流量标签
Payload Length 16 位无符号整数,IPv6 有效负载的长度,即该 IPv6 报头之后的数据包的剩余部分,以八位字节为单位。
Next Header 8 位选择器。标识紧跟在 IPv6 标头之后的标头类型。
Hop Limit 8 位无符号整数。转发数据包的每个节点递减 1。
Source Address 数据包发起方的 128 位地址。
Destination Address 数据包的预期收件人的 128 位地址

看起来比 IPv4 的 header 要简单许多,更多的扩展功能都在扩展头里面。对于路由转发来说,一般只需要解析固定头就可以判定需要转发到何处了。需要解包的字节更少了,效率也会更高。

IPv6 报头中 Next Header 遵循IANA-PN 中的类型。

IPv6 报头中的 20 位流标签字段由源用于标记要在网络中作为单个流处理的数据包序列。IPv6 流标签的当前定义可在RFC6437中找到。

IPv6 标头中的 8 位流量类别字段由网络用于流量管理。接收到的数据包或片段中的流量类位的值可能与数据包源发送的值不同。 RFC2474RFC3168中规定了区分服务和显式拥塞通知的当前使用流量类别字段。

扩展头:

在 IPv6 中,可选的 internet 层信息编码在单独的报头中,这些报头可以放在数据包中的 IPv6 报头和上层报头之间。 IPv6 数据包可携带零个、一个或多个扩展报头,每个扩展报头由前一报头的下一报头字段标识:

+---------------+------------------------
|  IPv6 header  | TCP header + data
|               |
| Next Header = |
|      TCP      |
+---------------+------------------------

+---------------+----------------+------------------------
|  IPv6 header  | Routing header | TCP header + data
|               |                |
| Next Header = |  Next Header = |
|    Routing    |      TCP       |
+---------------+----------------+------------------------

+---------------+----------------+-----------------+-----------------
|  IPv6 header  | Routing header | Fragment header | fragment of TCP
|               |                |                 |  header + data
| Next Header = |  Next Header = |  Next Header =  |
|    Routing    |    Fragment    |       TCP       |
+---------------+----------------+-----------------+-----------------

每个扩展头是 8 个八位字节的整数倍,以便为后续头保留 8 个八位字节的对齐。 每个扩展标头内的多个八位元字段在其自然边界上对齐,即,宽度为 n 个八位元的字段从标头开始以 n 个八位元的整数倍放置,n=1、2、4 或 8。

扩展头从 IPv6 header 中的 next header type 指定开始,可以链式设置。使用多个扩展标头时,需要按照建议的顺序排序。

IPv6 标头或任何扩展标头的 next header 值为 59 表示该标头后面没有任何内容。

以 ICMP6 包为例:

ethertype IPv6 (0x86dd), length 62: (flowlabel 0x13fc4, hlim 255, next-header ICMPv6 (58) payload length: 8) fe80::216:3eff:fe67:55c5 > ff02::2: [icmp6 sum ok] ICMP6, router solicitation, length 8

中 next-header 为 58 (ICMP6),表示该 IP 包 data 部分就是 icmp6 header 和 data,在 icmp6 header 中 next header type 应当为 59。

所有扩展标头,都需要使用以下格式:

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Next Header  |  Hdr Ext Len  |                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                               +
|                                                               |
.                                                               .
.                  Header-Specific Data                         .
.                                                               .
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

IPv6 的完整实现包括以下扩展头的实现:

Hop-by-Hop-Options Header 用于携带可选信息,这些信息可由每个节点沿着数据包的传递路径进行检查和处理。

Routing Header 用于 IPv6 源列出一个或多个中间节点,这些节点将在到达数据包目的地的途中“访问”。此功能与 IPv4 的松散源代码和记录路由选项非常相似。

Fragment Header 向其目的地发送比路径 MTU 中适合的数据包大的数据包。通俗一点就是 IP 分片。

Destination Options Header 用于承载仅需要由数据包的目的地节点检查的可选信息。


与 IPv4 不同,IPv6 节点不需要强制执行最大数据包生存期。 这就是 IPv4“生存时间”字段在 IPv6 中重命名为“跃点限制”的原因。 实际上,很少有 IPv4 实现符合限制数据包生存期的要求,因此这在实践中不是一个变化。 任何依赖互联网层(IPv4 或 IPv6)来限制数据包生存期的上层协议都应该升级,以提供自己的机制来检测和丢弃过时的数据包。

NDP

了解了基本的 IPv6,来看看 IPv6 是如何部署的。

Neighbor Discovery for IP version 6 (IPv6) 由rfc4861 定义。

节点(主机和路由器)使用邻居发现来确定已知驻留在连接链路上的邻居的链路层地址,并快速清除无效的缓存值。 主机还使用邻居发现来查找愿意代表其转发数据包的邻居路由器。 最后,节点使用该协议主动跟踪哪些邻居是可访问的,哪些是不可访问的,并检测更改的链路层地址。 当路由器或到路由器的路径出现故障时,主机会主动搜索运行中的备用路由器。

可以看到,ND 协议类似 IPv4 中的 arp 协议。

该协议解决了与连接到同一链路的节点之间的交互相关的一组问题。它定义了解决以下每个问题的机制:

邻居发现定义了五种不同的 ICMP 数据包类型:一对路由器请求和路由器广告消息、一对邻居请求和邻居广告消息以及重定向消息。这些信息的目的如下:

直接通过现实的案例来看,下面的数据包是 IPv6 从 ISP 获取 IPv6 配置的过程:

03:25:01.807801 IP6 (flowlabel 0x85abb, hlim 255, next-header ICMPv6 (58) payload length: 8) fe80::92e2:baf6:678b:91b4 > ff02::2: [icmp6 sum ok] ICMP6, router solicitation, length 8
03:25:01.813171 IP6 (class 0xe0, hlim 255, next-header ICMPv6 (58) payload length: 104) fe80::ce1a:faff:fee8:2a00 > fe80::92e2:baf6:678b:91b4: [icmp6 sum ok] ICMP6, router advertisement, length 104
    hop limit 64, Flags [none], pref medium, router lifetime 1800s, reachable time 0ms, retrans timer 0ms
        source link-address option (1), length 8 (1): cc:1a:fa:e8:2a:00
        0x0000:  cc1a fae8 2a00
        mtu option (5), length 8 (1):  1492
        0x0000:  0000 0000 05d4
        rdnss option (25), length 40 (5):  lifetime 4294967295s, addr: 240e:56:4000:8000::69 addr: 240e:56:4000::218
        0x0000:  0000 ffff ffff 240e 0056 4000 8000 0000
        0x0010:  0000 0000 0069 240e 0056 4000 0000 0000
        0x0020:  0000 0000 0218
        prefix info option (3), length 32 (4): 240e:398:e12:112e::/64, Flags [onlink, auto], valid time 2592000s, pref. time 604800s
        0x0000:  40c0 0027 8d00 0009 3a80 0000 0000 240e
        0x0010:  0398 0e12 112e 0000 0000 0000 0000

首先客户端向所有路由多播地址发送 router solicitation ,并携带自己的物理地址,如果有路由器收到,那也顺便就知道了客户端的物理地址了,可以直接回消息。

路由器收到路由请求后,会回复 router advertisement,包含了许多必要信息:

客户端收到路由通告后,获取的这些信息,足够本地配置 IPv6 了。

收到 prefix 后,按照上面的 prefix + interface id 的方式组合,就是端口的 IPv6 地址,还包含了 mtu 和 dns,直接配置上就完成了,是不是非常的简单。相比于 IPv4 , 根本就不需要 dhcp 的参与,也不需要介于二层和三层的 arp 协议。这就是IPv6 Stateless Address Autoconfiguration (SLACC)

还有后续, 还应注意,在将地址分配给接口之前,必须执行重复地址检测,以防止多个节点同时使用同一地址。 在确定使用这个 IPv6 地址之前还要进行重复地址检测,重复地址检测使用 Neighbor Solicitation 和 Neighbor Advertisement 来完成。

IP6 (hlim 255, next-header ICMPv6 (58) payload length: 32) fe80::3015:60ff:fe28:956b > fe80::216:3eff:fe67:55c5: [icmp6 sum ok] ICMP6, neighbor solicitation, length 32, who has fe80::216:3eff:fe67:55c5
          source link-address option (1), length 8 (1): 32:15:60:28:95:6b

通俗一点就是,问 “有没有人用这个地址啊,有人用就说一声,没人用我就用了啊”。重复地址检测是可以被关闭的。

客户端配置完,但是此时的路由器还不知道客户端使用的是哪个 IPv6 地址,需要由客户端发布邻居通告,告诉局域网内的节点自己的存在。

IP6 (flowlabel 0xfd344, hlim 255, next-header ICMPv6 (58) payload length: 24) fe80::3015:60ff:fe28:956b > ff02::1: [icmp6 sum ok] ICMP6, router advertisement, length 24
        hop limit 0, Flags [other stateful], pref medium, router lifetime 1800s, reachable time 0ms, retrans timer 0ms
          source link-address option (1), length 8 (1): 32:15:60:28:95:6b

此时邻居节点/路由器就知道这个地址以及物理地址了,可以对这个地址进行通信了。

dhcpv6

dhcpv6 由 rfc8415定义

虽然 SLAAC 简单方便,但是在企业管理场景中,网关没有各个节点的“状态”,如果需要在 IPv6 地址分配上做更多工作或其他需要 dhcp 的场景。 就需要启用 dhcpv6 服务器了。

dhcpv6 也可以和 SLAAC 协同工作,例如使用 SLAAC 下发 IP 地址,使用 dhcp 下发 dns ,ntp 等其他信息。

在 router advertisement 中,有两个 flag M 和 O,分别配置接口的 AdvManagedFlag 和 AdvOtherConfigFlag。

也就是节点收到路由通告后就知道是不是要开始 dhcp 请求了.

dhcpv6 还定义了 prefix delegation, 你可以将使用 dhcpv6 获取到的前缀进行子网化后再分给下级节点。 例如 可以将收到的 /48 前缀子网化为/64 前缀,并将一个/64 前缀分配给下级网络中的每个链路。(但是这个 prefix 不得超过 64)。

dhcp6 的内容相对过于复杂,不在这里说明。