IPv6 协议

本页目录

一台设备已经有 fe80:: 地址,却无法访问互联网;另一台已经有全局地址,却解析不了域名。两者并不矛盾:地址、下一跳、DNS 与路径包大小来自不同机制。理解 IPv6,需要把这些状态逐项连接起来,而不能把“自动配置成功”理解成所有网络能力同时就绪。

查阅问题核心判据
一个包到底多长40 B 基本头加 Payload Length,扩展头也占载荷长度
TCP/UDP 头在哪里沿 Next Header 链解析,不能总取偏移 40
有地址为何仍不能上网分别核对地址状态、路由、邻居、DNS 与 PMTU
大包由谁拆分源端;中间路由器反馈 Packet Too Big
SLAAC 是否必须用 MAC不必,稳定不透明 IID 与临时地址均有用途
IPv6 是否自动安全地址空间、IPsec 能力与实际访问控制分别判断

地址:先分清表示、范围与接口

IPv6 地址为 128 bit,通常写成八组十六进制。每组前导零可省略,连续全零组可以用一次 :: 压缩;只能出现一次,否则无法确定各处省去了多少组。例如 2001:0db8:0001:0000:0000:0000:0000:000a 与 2001:db8:1::a 表示同一地址。

地址/前缀含义不能据此推断的事
::未指定地址,可用于特定初始化报文的源地址不能当普通可达主机地址
::1本机回环不能转发到另一台设备
fe80::/10链路本地单播范围无法跨路由器访问;常见地址构造使用 64 bit IID
ff00::/8多播,包含作用域信息不是所有多播都只限当前链路
全局单播地址为跨链路单播通信提供地址有地址不保证路由发布、防火墙放行或服务监听

IPv6 没有广播地址,邻居发现等使用相应多播组。任播则从单播地址空间分配,同一地址可在多处提供服务,由路由选择其中一处;“最近”是路由意义,不保证地理最近。RFC 4291:地址架构

fe80::1%eth0 中 %eth0 是本机用来指定作用域接口的标识,不是线上 128 bit 地址的一部分。两张网卡所在链路都可能存在 fe80::1,因此只给地址可能不够。对于普通以太网 SLAAC,/64 前缀与 64 bit IID 配合;这不表示 IPv6 路由表只允许 /64,聚合路由与主机路由仍有各自长度。

基本头:固定的是入口,不是所有头部总长度

字段位数读取方式
Version4值为 6
Traffic Class8DSCP 与 ECN 的分类、拥塞语义
Flow Label20与源/目的等一起辅助识别流
Payload Length16基本头之后的字节数
Next Header8下一个扩展头或上层协议编号
Hop Limit8每次路由转发递减,耗尽则丢弃
Source / Destination各 128源地址与目的地址
基本头固定 40 B;后续长度由实际的头链决定 IPv6 基本头:40 B Next Header = 0 Hop-by-Hop:8 B Next Header = 17 UDP:8 B + 数据 本例 UDP 数据 100 B Payload Length = 8 + 8 + 100 = 116 B 整个 IPv6 包 = 40 + 116 = 156 B Next Header 标明紧随其后的类型;Payload Length 包含扩展头,不包含 40 B 基本头。

普通非 jumbogram 包的 Payload Length 最大为 65535,整个包还要加 40 B 基本头。不能照搬 IPv4 Total Length 的含义。Jumbogram 有额外规则,不应把长度字段为零一概当作“任意大包”或解析器免检开关。

IPv6 没有基本头校验和,也没有 IHL;选项通过扩展头表达。TCP/UDP 等仍有自己的校验规则,其中伪首部纳入 IPv6 地址等信息,但校验不提供密码学认证。支持 IPsec 也不意味着每个 IPv6 包已加密或认证。RFC 8200:基本头及上层接口

Flow Label 能做什么

源端为同一流使用一致的非零标签,有助于网络在不深入读取传输端口时做流分类或负载分担;零标签仍有定义。20 bit 空间可能碰撞,设备通常结合源/目的等字段使用,不能拿标签当全网唯一连接 ID。

标签也不自动预留带宽或提高优先级;资源调度需要网络策略。应用、内核版本和配置都会影响标签生成,不能把某个 Linux 默认行为推广成协议要求。RFC 6437:Flow Label

扩展头:必须按类型解码

基本头 Next Header=6 可以直接跟 TCP;若为 0,则先进入 Hop-by-Hop Options,再根据该头的 Next Header 继续。上图的 UDP 从第 48 字节开始,若解析器固定从第 40 字节读端口,就会把扩展头字段当作端口。

头类型编号长度解析要点
Hop-by-Hop / Destination Options0 / 60总长为 (Hdr Ext Len+1)×8
Routing43通用头含长度和路由类型,具体内容按类型解释
Fragment44固定 8 B,无通用的 Hdr Ext Len 字段
AH51有自己的 Payload Len 规则,单位为 4 B
ESP50有独立封装格式,Next Header 位于其尾部结构
No Next Header59不再跟另一个协议头

因此不能对所有扩展头执行同一个“读第二字节、乘八、跳过去”的循环。安全解析还需要检查实际剩余长度、允许的头链和资源上限,遇到无法解释的加密载荷也不能继续猜端口。

RFC 8200 给出推荐排列次序;Hop-by-Hop 若存在,必须紧跟基本头。不同头的处理节点不同,普通转发节点并不需要解释每一种终点选项。扩展头支持情况会影响真实路径可达性,应分别验证端点和中间设备,而非只证明报文字节符合格式。RFC 8200:扩展头

UDP 为什么不总在第40字节?

把一条链看成按类型驱动的游标移动。Options 的第二字节是长度编码,Fragment 同一位置却不能套用这个公式;选择完整首片示例后,仍要扣出 Fragment Header 才能读 UDP。截断在边界附近时,“还没拿到字节”应与“已经读到下一个协议”区分。

正在呈现知识画面
UDP 为什么不总在第40字节?

Next Header 决定怎样读长度。Options 与 Fragment 不能共用一种长度公式;完整链之前,固定偏移猜端口会读错对象。

分片算例:没有 DF 位,只有源端分片

当转发出口无法容纳当前 IPv6 包时,路由器不做 IPv6 分片,而是丢弃并生成相应 ICMPv6 Packet Too Big(Type 2、Code 0)。把这简称为“DF 永远是 1”会误导字段解析:IPv6 根本没有 DF 位。源端通过 PMTUD 调整发送大小,也可在需要时生成 Fragment Header。RFC 8201:IPv6 PMTUD

下面是独立教学算例:原包 3000 B,含基本头 40 B、可分片部分 2960 B,无其他扩展头;源端已知 PMTU=1280。每片额外需要 8 B 分片头,非末片数据容量为 floor((1280−40−8)/8)×8=1232 B。

源端分片:3000 B 原包,路径 MTU 为 1280 B 原包:40 B 基本头 + 2960 B 可分片部分 本例没有其他扩展头;每片新增 8 B Fragment Header 1232 B 数据 offset=0,M=1 1232 B 数据 offset=154,M=1 496 B 数据 offset=308,M=0 整包 1280 B 整包 1280 B 整包 544 B 偏移单位为 8 B;每片有 40 B 基本头与 8 B 分片头,路由器不会替源端完成这一步。
片数据区间OffsetMPayload Length整包长
10~12310112401280
21232~2463154112401280
32464~29593080504544

末片的 308×8+496=2960 给出原可分片部分长度。重组还要等前面区间到齐,不能只看 M=0;重叠分片按 IPv6 规则导致整份重组丢弃。若有必须随每片携带的其他头,容量需要重新扣除,不能永久套用 1232。首片还必须包含完整的 IPv6 头链及上层头,不能随意切开到后片。RFC 8200:分片与重组

1280 B 是 IPv6 的最小链路 MTU 要求,较小物理传输单元需要链路下层适配;它不是所有路径的实际最大值。过滤必要 ICMPv6 会破坏大小反馈;TCP MSS 也不能替 UDP 应用解决全部分包问题。

自动配置:四种信息分别建立

先形成可用的链路本地地址,主机可以通过 RS 请求 RA,也可以收到路由器主动多播的 RA。RA 的目的并不总是单播。下面区分最容易混淆的信息:RFC 4861:路由器发现

信息主要来源生命周期或条件
默认路由器RA Router Lifetime 等寿命为零不表示 RA 内所有前缀都无效
前缀在链路上PIO 的 L 标志与是否生成地址分开判断
SLAAC 地址PIO 的 A 标志、合适前缀及本地 IID受 preferred/valid lifetime 约束
DNS 服务器RA RDNSS、DHCPv6 或静态配置不由“已有地址”自动推得

例如收到 A=1、L=0 的合适 PIO,主机可以形成地址,但不能仅凭这条 PIO 认定整个前缀上的所有目的都可直接做邻居解析。M 标志指示有状态地址配置,O 指示其他配置;O 并非专指 DNS,M 也不是“额外获取 DNS”。是否启用对应客户端还取决于主机实现和管理配置。

地址通常先处于 tentative,通过 DAD 后才能按相应规则使用。Preferred Lifetime 到期后地址 deprecated,通常避免为新通信选它,但在 Valid Lifetime 到期前仍有使用意义;有效期到期后不再作为有效地址。这样,更新前缀可以逐步迁移连接,而非立即切断全部旧地址。RFC 4862:SLAAC

RA 的 RDNSS 选项可直接提供递归 DNS 服务器,因此“SLAAC 必须再跑 DHCPv6 才有 DNS”不成立。反过来,只收到一个地址配置结果也不证明默认路由与 DNS 均已存在。RFC 8106:RA DNS 选项

稳定地址与临时地址各解决什么

IID 不必嵌入 MAC。稳定地址有利于地址持续性;临时地址通过轮换降低跨时间关联。临时地址有 preferred/valid lifetime、提前生成替代地址等约束,具体寿命不能统一写死为 24 小时,更不能认为轮换后所有已建连接立即换源地址。

实际源地址选择还受目的作用域、地址状态、策略和应用绑定影响。临时地址不隐藏应用账号、DNS 查询或其他跟踪信号,也不让入站防火墙失去意义。RFC 8981:临时地址

IPv4 共存:封装和翻译改变的东西不同

机制数据路径适合解释的问题
双栈主机与网络提供两种协议某一地址族故障为何被另一族掩盖
DS-LiteB4 将 IPv4 封装进 IPv6,AFTR 解封装并承担 NAT44接入网怎样用 IPv6 承载共享 IPv4 出口
NAT64 + DNS64合成相应 AAAA,IPv6 流量在翻译器处转为 IPv4IPv6 客户端怎样访问只有 IPv4 的服务
464XLATCLAT 做无状态翻译,PLAT 做有状态 NAT64IPv4 应用怎样穿过 IPv6 接入网

DS-Lite 保留隧道内 IPv4 语义;NAT64 改变网络层地址族,两者并非同一种“套个 IPv6 头”。对于 NAT64,DNS64 帮助按名称访问,但应用硬编码 IPv4 字面量时不会经过这种 AAAA 合成;464XLAT 可为相应 IPv4 应用提供衔接。DS-Lite RFC 6333、NAT64 RFC 6146、464XLAT RFC 6877

双栈应用可用 Happy Eyeballs 安排候选连接,降低某个地址族失效带来的等待;它涉及解析、排序与延迟启动竞争,不是所有请求都同时完整发送两遍。成功回退可能掩盖 IPv6 故障,诊断时仍需分别看两族的路径和连接结果。RFC 8305

本地复算:长度、字段和数据区间

import struct
from ipaddress import IPv6Address

src = IPv6Address('2001:db8:1::a')
dst = IPv6Address('2001:db8:2::b')
assert src.exploded == '2001:0db8:0001:0000:0000:0000:0000:000a'
traffic_class, flow_label = 0xBA, 0x12345
word = (6<<28) | (traffic_class<<20) | flow_label
header = struct.pack('!IHBB16s16s', word, 116, 0, 64, src.packed, dst.packed)
assert len(header) == 40
assert word>>28 == 6 and (word>>20)&255 == 0xBA
assert word&0xfffff == flow_label
assert 40 + struct.unpack('!H', header[4:6])[0] == 156

payload = bytes(i%251 for i in range(2960))
capacity = ((1280-40-8)//8)*8
pieces = []
for start in range(0,len(payload),capacity):
    data = payload[start:start+capacity]
    more = int(start+len(data)<len(payload))
    offset = start//8
    frag_header = struct.pack('!BBHI', 17, 0, (offset<<3)|more, 123456)
    assert len(frag_header) == 8
    pieces.append((offset,more,8+len(data),48+len(data),data))
assert [x[:4] for x in pieces] == [(0,1,1240,1280),(154,1,1240,1280),(308,0,504,544)]
assert b''.join(x[4] for x in pieces) == payload
assert pieces[-1][0]*8 + len(pieces[-1][4]) == 2960
print('IPv6 字段、长度、分片偏移与区间检查通过')

示例验证局部编码和计算关系,随机载荷并非完整 UDP 报文;未向网络发送数据或修改接口配置。

继续查阅