ch08_3
8.3 IPv6 Header
上一节我们解决了「去哪儿」的问题——地址拿到了, Solicited-Node 组播组也加上了。现在,该解决「怎么走」的问题了。而在真正迈出脚步之前,我们需要先搞清楚 IPv6 数据包长什么样。
这不是简单地看个图填空。IPv6 的头部设计不仅是为了容纳那 128 位的超长地址,更是一次针对 IPv4 历史包袱的「大手术」。有些设计删掉了,有些挪走了,还有些变成了你意想不到的样子。
拿掉保险丝
每个 IPv6 数据包都以 IPv6 头部开场。这个头部的长度是固定 40 字节。
先停一下,品味这个「固定」。在 IPv4 时代,头部长度是可变的(因为有 Options),所以必须有一个 IHL(Internet Header Length)字段来告诉内核「这个头有多长」。到了 IPv6,这个字段消失了。40 字节,雷打不动。
还有一个更激进的变化:IPv6 头部没有校验和(Checksum)。
在 IPv4 里,每经过一个路由器,TTL 减 1,路由器就得老老实实重新计算整个头部的校验和——这在 CPU 还是单核、主频几百兆的年代是一笔不小的开销。IPv6 直接把这笔开销砍掉了:校验工作被甩给了二层(链路层,比如 CRC)和四层(传输层,比如 TCP/UDP 的校验和)。
这样做有一个直接的后果:路由器转发 IPv6 数据包时,修改 hop_limit(相当于 IPv4 的 TTL)不需要重新计算校验和。这在纯软件路由器上,就是实打实的性能提升。
当然,这也有副作用:UDP 在 IPv4 里本来可以校验和填 0 表示「不校验」,但在 IPv6 里,除了极少数特殊的隧道场景(RFC 6935),UDP 必须开启校验和。因为万一中间传错了,没人给你兜底。
头部结构:被切分的流量类别
来看看内核里的定义。这里有一个微妙的细节,很容易在第一次看的时候滑过去:
struct ipv6hdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u8 priority:4,
version:4;
#elif defined(__BIG_ENDIAN_BITFIELD)
__u8 version:4,
priority:4;
#else
#error "Please fix <asm/byteorder.h>"
#endif
__u8 flow_lbl[3];
__be16 payload_len;
__u8 nexthdr;
__u8 hop_limit;
struct in6_addr saddr;
struct in6_addr daddr;
};
(include/uapi/linux/ipv6.h)
注意第一个字节。标准文档(RFC 2460)里说的是 4 位 Version + 8 位 Traffic Class + 20 位 Flow Label。
但在 Linux 的实现里,这第一个字节被拆成了 priority:4 和 version:4。剩下的 4 位 Traffic Class 去哪了?它被塞进了 flow_lbl 数组的第一位里。
也就是说,代码里的 priority 加上 flow_lbl[0] 的高 4 位,才拼凑出一个完整的 8 位 Traffic Class。这是典型的为了节省内存位宽而做的位域操作。
我们拆解一下这个结构体的每个字段,看看它们在 IPv6 的世界里扮演什么角色:
-
version(4 bit): 必须是 6。这没什么好说的,协议身份证。 -
priority(4 bit) /Traffic Class(8 bit): 也就是流量类别。虽然 RFC 2460 定义了这个字段,但并没有具体规定每个值代表什么——这部分是留给 DiffServ(QoS)机制去发挥的。内核代码里保留priority这个名字,其实有点历史遗留的味道。 -
flow_lbl(20 bit): 流标签。这是一个在 RFC 2460 时期就被标记为「实验性」的字段,直到 2011 年的 RFC 6437 才有了比较明确的规范。 它的目的是给一系列属于同一个「流」的数据包打上标签。路由器看到这个标签,理论上可以不查五元组,直接按标签做快转。RFC 6437 甚至建议用它来检测地址欺骗。不过在目前的通用互联网上,你很少能见到它被大规模应用。 -
payload_len(16 bit): 载荷长度。不包括那固定的 40 字节头部。 16 位意味着最大长度是 65,535 字节。如果超过这个数(比如 Jumbo Frame),这个字段就得填 0,然后由 Hop-by-Hop 扩展头里的 Jumbo Payload 选项来接管。我们下一节会细讲这个。 -
nexthdr(8 bit): 下一个头部。这是一个极其关键的设计。 如果没有扩展头,它就是上层协议号:IPPROTO_TCP(6)、IPPROTO_UDP(17) 等。 如果有扩展头,它指向紧跟在 IPv6 头部后面的那个扩展头的类型。这就像链表节点里的next指针。 -
hop_limit(8 bit): 跳数限制。每经过一个路由器减 1。减到 0,包就被丢弃,并回一个 ICMPv6 Time Exceeded 消息。这就是防止数据包在网路里迷路永远转圈圈的机制。 -
saddr/daddr(128 bit): 源地址和目的地址。 这里有个小细节:如果使用了 Routing Header(路由扩展头),daddr可能并不是最终目的地,而是路径上的「下一跳」。这就像你在写快递单时,收件人写的不是你朋友,而是某个中转站。
与 IPv4 的告别
你会发现,这里少了很多 IPv4 的东西:没有 Header Length,没有 Checksum,没有 Options。
IPv4 的 Options 机制曾因为性能问题(导致变长头部、难以硬件加速)而被诟病许久。IPv6 用 Extension Headers(扩展头部)彻底取代了它——这是一个更优雅、更灵活的「链接式」方案。
但我们先不急着展开讲扩展头。现在,盯着这个固定的 40 字节结构,你应该能感受到一种设计哲学上的转变:从复杂的变长结构,回归到了固定、快速、可预测的简单骨架。为了这 40 字节的纯粹性,IPv6 甚至愿意把「校验」这个责任推给别人。
下一节,我们就要把这个简单骨架接上「四肢」——也就是那些功能各异、首尾相连的 Extension Headers。届时你会发现,nexthdr 这一个小小的字段,是如何撑起整个协议栈的扩展性的。