IPv6 的头为什么是固定 40 字节?没有了校验和、选项和分片字段,这些东西去了哪里?这篇在 lo0 上抓 ping6 的真实 IPv6 头逐字节解码,看流标签和跳数限制,再手工拼一个带扩展头链的包让 tcpdump 和 scapy 一起解码。
请求的前 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 位值,同一个套接字的包相同,路由器可据此识别一条流。
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 和自己的解析程序互相验证。