8.4 扩展头部——链接式的无限扩展
在上一节,我们盯着 IPv6 那个只有 40 字节的固定头部看了半天。它干净、纯粹,把所有不必要的负担都甩掉了。
但你心里肯定有个疑问:如果有些可选功能必须得加怎么办? 比如加密、分片,或者是路由记录?
在 IPv4 时代,这个问题是通过「Options」解决的——也就是在主头部后面塞上一堆不定长的字段。这是个万恶之源:变长头部让硬件加速痛苦不堪,路由器不得不为了解析那几个可能存在的选项字段,把整个包剥开来看。
IPv6 的回答是:别放在里面,把它放在外面。
这就是 Extension Headers(扩展头部)。它是 IPv6 设计中最精妙的部分之一:它把协议变成了一条链。
链条的秘密:nexthdr
还记得我们在上一节提到的 nexthdr 字段吗?
当时我说它是 IPv6 的「罗塞塔石碑」。现在我们就来看看这块石头是怎么转动的。
扩展头部的核心逻辑是链接。在 IPv6 主头里,nexthdr 指向的不是 TCP 或 UDP,而是紧跟在 IPv6 头部后面的那个头部是谁。
这就像玩寻宝游戏:
- 你打开 IPv6 的盒子,里面有一张纸条写着
nexthdr = 43(Routing Header)。 - 你去找 Routing Header 的盒子,打开它,里面又有一张纸条写着
nexthdr = 17(UDP)。 - 你再去拿 UDP 的盒子。
这就是为什么每个扩展头部都必须有一个 Next Header 字段。它就是指向下一个盒子的钥匙。只有到了最后一个扩展头,Next Header 才会指向真正的主角——比如 TCP、UDP 或 ICMPv6。
这种设计带来的最大好处是:中间的路由器只需要看 IPv6 头就够了。除非遇到了特殊的 Hop-by-Hop 头部,否则路由器根本不关心后面链了多少扩展头,直接根据 IPv6 头部转发。这让转发性能相比于 IPv4 的 Options 有了质的飞跃。
规则:链条的秩序
虽然扩展头部是自由的,但链条并不是混乱的。有几条铁律你必须遵守:
- 顺序必须严格执行:你不能先看 Fragment 再看 Routing,包里什么顺序排列,你就必须按什么顺序处理。
- 每个扩展头通常只能出现一次:除了 Destination Options(目的选项头),它允许出现两次(原因后面讲)。
- 8 字节对齐:这是为了内存访问效率,所有扩展头的长度必须是 8 的倍数。不够怎么办?填(Padding)。
RFC 2460 给出了一个「推荐顺序」,虽然不是强制的,但大家最好都照着做,否则世界会乱套:
- IPv6 Header
- Hop-by-Hop Options(这个必须是老大,紧挨着 IPv6 头)
- Destination Options(第一次出现,给路由看的)
- Routing Header
- Fragment Header
- Authentication Header
- Encapsulating Security Payload Header
- Destination Options(第二次出现,给目标主机看的)
- Upper-layer header (TCP/UDP/etc)
当链条断裂:未知协议处理
如果在解链的过程中,你突然遇到了一个不认识的 nexthdr 值怎么办?
比如内核某个版本还没支持某种新协议,或者恶意包故意塞了个奇怪的数字。
这时候不能直接丢弃,也不能强行往下走。正确的做法是向发送方投诉。
内核会调用 icmpv6_param_prob() 发回一个 ICMPv6 的 Parameter Problem 消息,错误码设为 ICMPV6_UNK_NEXTHDR(unknown Next Header)。
类比时刻:想象你收到一个快递包裹,打开一层盒子,里面写着「请转交给 XX 部门」,但你根本没听说过这个部门。这时候你只能把包裹退回给发件人,并附上一张纸条:「我们这儿没这个地儿」。这就是 ICMPv6 做的事。
深入四种核心扩展头
Linux 内核用常量定义了所有扩展头的类型(见本书末尾的表 8-2)。每个扩展头(除了 Hop-by-Hop)都通过 inet6_add_protocol() 注册了自己的处理回调。
我们来聊聊最常用的这几种。
1. Hop-by-Hop Options(逐跳选项头)
这是唯一的特权阶级。
只有它必须紧挨着 IPv6 头,也只有它强迫路径上的每一个路由器都必须处理。正因为如此,它用起来得非常小心——一旦滥用,整个互联网的路由效率都会被拖垮。
它的特权决定了它不能走普通的注册流程。在内核中,它有一个专门的 ipv6_parse_hopopts() 方法来处理,这个方法是在 ipv6_rcv() 里直接调用的。
它里面包含了一堆 TLV(Type-Length-Value)格式的选项。这里有几个典型的例子:
- Router Alert (IPV6_TLV_ROUTERALERT):告诉路由器「嘿,这个包你得看一眼」。主要用于 RSVP(资源预留)或组播数据包。
- Jumbo (IPV6_TLV_JUMBO):普通的 IPv6 载荷最大是 65535 字节。如果你需要传输「巨型帧」(Jumbograms),超过 2^16 字节,就需要这个选项来扩展长度字段。
- Pad1 / PadN:就是填充用的「废纸」,为了凑齐 8 字节对齐。
Pad1占 1 字节,PadN占 N 字节。
2. Routing Header(路由头)
这是 IPv4 Source Routing 的继任者。
如果你想让数据包「途经 X 路由器,再到我」,你就用这个头。它允许发送端指定一个或多个中间必须经过的地址。
历史的回响:这东西在 IPv4 时代因为安全风险(可以被用来进行 IP 欺骗攻击)而被很多网络直接封禁。在 IPv6 里,虽然它还在,但很多严谨的运维依然会默认丢弃包含 Routing Header 的包。
3. Fragment Header(分片头)
这是 IPv6 分片机制的核心。
在 IPv4 里,中间路由器如果发现接口 MTU 太小,是可以把包分片的。这叫「Path MTU Discovery (PMTUD) 失败后的保底方案」。
但在 IPv6 里,这个保底方案被移除了。 IPv6 规定:中间路由器绝对禁止分片。分片只能发生在源主机。
这意味着如果源主机发了一个 5000 字节的包,结果中间有个路由器的 MTU 只有 1500,路由器不会分片,而是直接把包扔掉,并给源主机发回一个 ICMPv6 Packet Too Big 消息。源主机收到后,不得不缩小包的大小重发。
这就是 IPv6 的 PMTUD 机制——它是一种「强硬」的协商。
内核实现分片的是 ip6_fragment() 方法,而重组则在 net/ipv6/reassembly.c 里。
这里有一个内核注册 Fragment 协议处理器的代码片段,展示了它是如何挂接到链条上的:
static const struct inet6_protocol frag_protocol = {
.handler = ipv6_frag_rcv,
.flags = INET6_PROTO_NOPOLICY,
};
int __init ipv6_frag_init(void)
{
int ret;
// 注册 IPPROTO_FRAGMENT (44) 对应的处理函数
ret = inet6_add_protocol(&frag_protocol, IPPROTO_FRAGMENT);
// ...
}
(net/ipv6/reassembly.c)
当一个包的 nexthdr 是 44 时,内核就会知道:「哦,后面跟的是分片头,我得把碎片收齐了再往上交」。
4. Destination Options(目的选项头)
这是唯一允许出现两次的扩展头。为什么?因为它有两个完全不同的应用场景:
- 出现在 Routing Header 之前:这时候,它的信息是给 Routing Header 里指定的那些「中转路由器」看的。
- 出现在 Routing Header 之后(或者没有 Routing Header):这时候,它的信息是给最终目的地主机看的。
IPv6 初始化:从以太网类型开始
我们讲了这么多结构,现在来看看这套系统是如何在内核里启动的。
一切的起点是 inet6_init()。这个函数就像是 IPv6 子系统的总指挥,它初始化了 procfs、注册了 TCPv6/UDPv6 的协议处理函数、启动了邻居发现和路由子系统。
但最关键的一步,是告诉网络核心:「收到以太网类型为 0x86DD 的帧,交给我」。
这是通过 dev_add_pack() 完成的,和 IPv4 的做法几乎一模一样:
static struct packet_type ipv6_packet_type __read_mostly = {
.type = cpu_to_be16(ETH_P_IPV6), // 0x86DD
.func = ipv6_rcv, // 处理回调
};
static int __init ipv6_packet_init(void)
{
dev_add_pack(&ipv6_packet_type);
return 0;
}
(net/ipv6/af_inet6.c)
从此以后,只要网卡收到了一个以太网帧,其 Type 字段是 0x86DD,内核就会直接跳转到 ipv6_rcv() 函数。
ipv6_rcv() 就是整个 IPv6 接收路径的入口。在这里,内核会开始检查版本号、校验(如果有的话),然后顺着我们在前面讲的那条由 nexthdr 串起来的链条,一步步解包,最终把数据送到上层协议的手里。
这一节的伏笔
到现在为止,我们处理的所有问题(分片、路由)其实都假设了一个前提:你的地址已经配置好了。
但 IPv6 的地址是 128 位的,没人愿意手敲。它是怎么跑到你机器上去的?是 DHCP 嘛?还是什么魔法?
下一节,我们将进入 IPv6 最具代表性的特性:Autoconfiguration(自动配置)。你会发现,即使没有 DHCP 服务器,Linux 也能凭空变出一个全球唯一的地址来。