bigywater.tech
网络协议 · 23

IPv6 头部:固定 40 字节与扩展头

IPv6 的头为什么是固定 40 字节?没有了校验和、选项和分片字段,这些东西去了哪里?这篇在 lo0 上抓 ping6 的真实 IPv6 头逐字节解码,看流标签和跳数限制,再手工拼一个带扩展头链的包让 tcpdump 和 scapy 一起解码。

40 字节
IPv6 头固定大小,比 IPv4 的 20 字节(无选项)多 20 字节;下载同样 1 MiB,头部占 0.73% 对 0.54%
0x30e00
内核给 ping6 请求填的非零流标签(20 位);同一个套接字的包流标签相同
255 与 1
本机局域网上 25 秒 275 个 IPv6 包的跳数限制只有这两种取值

固定头

实测 · 本机执行输出
  请求的前 40 字节(IPv6 头):60030e00 00103a40 00000000 00000000 00000000 00000001 00000000 00000000 00000000 00000001
  请求:版本 6,流量类别 0x00,流标签 0x30e00,载荷长度 16,下一个头 58(ICMPv6),跳数限制 64,源 ::1,目的 ::1;整个包 56 字节 = 40 + 16
  应答:版本 6,流量类别 0x00,流标签 0x00000,载荷长度 16,下一个头 58(ICMPv6),跳数限制 64,源 ::1,目的 ::1;整个包 56 字节 = 40 + 16
  ICMPv6 校验和:抓到 0x09ca;自己用“伪头部(源、目的、上层长度、下一个头 58)+ ICMPv6 报文”算出 0x09ca(lo0 上的包可能被卸载 / 省略校验和)
  ICMPv6 的校验和覆盖伪头部,因为 IPv6 头自己没有校验和字段(IPv4 有)

版本 4 位、流量类别 8 位、流标签 20 位、载荷长度 16 位、下一个头 8 位、跳数限制 8 位,再加 16 字节源和 16 字节目的地址,共 40 字节。没有头校验和、没有分片字段,路由器每转发一次只需把跳数限制减 1。

校验和去哪了

实测 · 本机执行输出
  ping 127.0.0.1:IP 总长 84 字节(IPv4 头 20 + ICMP 8 + 载荷 56);ping6 ::1:IP 总长 56 字节(IPv6 头 40 + ICMP 8 + 载荷 8)——两个 ping 默认载荷大小不同(56 与 8),所以总长不能直接比;每个包的 IP 头:IPv4 20 字节(无选项时)、IPv6 固定 40 字节,多 20 字节
  IPv4:下载 1048576 字节,lo0 上共 109 个 IP 包、合计 1054268 字节(不含 lo0 的 4 字节链路层头);SYN 通告的 MSS = 16344;所有包的 IP + TCP 头合计 5692 字节,占 0.54%
  IPv6:下载 1048576 字节,lo0 上共 107 个 IP 包、合计 1056304 字节(不含 lo0 的 4 字节链路层头);SYN 通告的 MSS = 16324;所有包的 IP + TCP 头合计 7728 字节,占 0.73%
  MSS 差:20 字节(IPv6 头比 IPv4 头多 20 字节,MSS 少 20)

IPv6 头没有校验和,上层的 ICMPv6、TCP、UDP 校验和要覆盖包含源 / 目的地址的伪头部。自己按伪头部算出的 ICMPv6 校验和与抓到的值一致。

流标签与流量类别

实测 · 本机执行输出
  抓到 26 个 UDP over IPv6 包
    套接字 1:4 个包的流标签 ['0xf0d00', '0xf0d00', '0xf0d00', '0xf0d00']——同一个套接字的流标签相同
    套接字 2:4 个包的流标签 ['0xa0900', '0xa0900', '0xa0900', '0xa0900']——同一个套接字的流标签相同
    套接字 3:4 个包的流标签 ['0xd0d00', '0xd0d00', '0xd0d00', '0xd0d00']——同一个套接字的流标签相同
    套接字 4:4 个包的流标签 ['0x20000', '0x20000', '0x20000', '0x20000']——同一个套接字的流标签相同
    套接字 5:4 个包的流标签 ['0xd0000', '0xd0000', '0xd0000', '0xd0000']——同一个套接字的流标签相同
    套接字 6:4 个包的流标签 ['0x50200', '0x50200', '0x50200', '0x50200']——同一个套接字的流标签相同
    设 IPV6_TCLASS=0xb8 的套接字:流量类别 ['0xb8', '0xb8'],流标签 ['0xd0100', '0xd0100']
  6 个不同套接字的流标签互不相同:True;全为 0:False——流标签是 20 位,由发送方内核生成,用来标记一条“流”,路由器可据此做负载均衡而不必解析上层端口

流量类别与 IPv4 的 TOS 用法相同;流标签是内核按套接字生成的 20 位值,同一个套接字的包相同,路由器可据此识别一条流。

本地链路上的 IPv6

实测 · 本机执行输出
  25 秒共 275 个 IPv6 包
    下一个头 17(UDP),跳数限制 255:184 个
    下一个头 17(UDP),跳数限制 1:87 个
    下一个头 58(ICMPv6),跳数限制 255:4 个
  目的地址类别:ff02::fb(mDNS 组播) 271;单播 4
  这 25 秒里所有包的跳数限制只有 255 和 1 两种,没有 64 / 128 这类能被路由到别处的值;跳数限制 255 在邻居发现里用来证明“包没有经过路由器”,这里的 mDNS 也是 255

局域网上的 IPv6 流量(mDNS、邻居发现)跳数限制是 255 或 1,不出本地链路。

扩展头

实测 · 本机执行输出
  构造的包:IPv6 头 40 + 逐跳 8 + 路由头 24 + 分片头 8 + 目的选项 8 + UDP 13 = 101 字节;IPv6 头里的“载荷长度”= 61(包含所有扩展头)
  tcpdump 解码:08:00:01.000000 IP6 (hlim 64, next-header Options (0) payload length: 61) 2001:db8::1 > 2001:db8::2: HBH (rtalert: 0x0000) (padn) RT6 (len=2, type=2, segleft=1, rsv=0x0, [0]2001:db8::3) frag (0xdeadbeef:0|21) DSTOPT (padn) 40000 > 9: UDP, length 5
  自己沿下一个头走:IPv6 头(40 字节) → 逐跳选项(8 字节) → 路由头(24 字节) → 分片头(8 字节) → 目的选项(8 字节) → UDP(13 字节)
  scapy 解码出的层:IPv6 / IPv6ExtHdrHopByHop / IPv6ExtHdrRouting / IPv6ExtHdrFragment / IPv6ExtHdrDestOpt / UDP / Raw
  扩展头合计 48 字节;规范建议的顺序是 逐跳 → 目的选项(第一个)→ 路由 → 分片 → … → 目的选项(第二个)→ 上层;逐跳选项必须紧跟 IPv6 头,因为沿途每个路由器都要检查它

IPv4 的选项和分片字段在 IPv6 里变成可选的扩展头,按“下一个头”串成链;逐跳选项必须紧跟 IPv6 头。这一节是手工构造的包,用 tcpdump、scapy 和自己的解析程序互相验证。

分步演示

IPv6 头部
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。本机网络没有公网 IPv6(en0 只有链路本地地址),所以第一至三节在 lo0 上用 ::1 做真实抓包,第四节抓的是 en0 上的本地链路 IPv6 流量(别的设备的包只统计个数);第五节是手工构造的包(地址为文档前缀 2001:db8::/32),不是抓来的
    • 没有验证:公网上的 IPv6 路径(本机没有 IPv6 路由);MLD 报文里的逐跳选项(加入组播组时没有抓到 MLD 报告);分片头在真实传输中的出现(lo0 不分片);IPv6 路由头类型 0 的弃用与安全问题;流标签在路由器上被用来做负载均衡的实际效果
    京ICP备2026067502号-1京公网安备11010502063487号