线上有两类"玄学故障":加了 AAAA 记录后一部分用户打不开;服务明明起着,IPv6 客户端连不上。根都在地址层与套接字层。
内容按四层走:地址怎么表示 → NAT 改了什么 → 报文长什么样 → 双栈机器绑在哪,结论按 JDK 17 + Linux 6.x 核对。
1. 地址结构:分类地址已废弃,一切按 CIDR
IPv4 是 32 位点分十进制。A/B/C 类(按首位划 8/16/24 位网络号)是 1981 年的产物,1993 年 CIDR 落地后在路由层面已不再使用:信息全在"前缀长度"里,192.168.10.37/26 的 /26 才有意义。
| 前缀 | 点分掩码 | 地址总数 | 可用主机 | 典型用途 |
|---|---|---|---|---|
| /8 | 255.0.0.0 | 16,777,216 | 16,777,214 | 云厂商大块分配 |
| /16 | 255.255.0.0 | 65,536 | 65,534 | 常见分配粒度 |
| /24 | 255.255.255.0 | 256 | 254 | 单段内网 |
| /30 | 255.255.255.252 | 4 | 2 | 点对点互联 |
| /31 | 255.255.255.254 | 2 | 2 | RFC 3021,两端都可用 |
| /32 | 255.255.255.255 | 1 | 1 | 主机路由 |
两处易记反:/31 在 RFC 3021 下不再浪费网络号与广播号;/32 是主机路由而非网段。判同网段只能拿掩码做按位与——10.0.1.5/16 与 10.0.2.5/24 会一方认为同段、一方认为不同,这种不对称是"能 ping 通却连不上"的经典来源。
$ ipcalc 192.168.10.37/26 # 典型输出
Network: 192.168.10.0/26 HostMin: 192.168.10.1 HostMax: 192.168.10.62
Hosts/Net: 62
2. NAT:常态化的代价
IPv4 耗尽后的事实续命方案不是 IPv6 而是 NAT:把"私有地址 + 端口"映射成"公网地址 + 端口"。代价不在性能,在连接语义被改写。
| NAT 破坏的东西 | 表现 | 后果 |
|---|---|---|
| 端到端可达 | 外网不能主动连内网主机 | 需端口映射、反向隧道、中继 |
| 端口空间 | 单 IP 仅 65535 端口,按五元组复用 | 高并发下连接建立失败 |
| 载荷带 IP 的协议 | FTP 主动模式、SIP、部分联机协议 | 对端拿到私有地址,无法回连 |
| 分片 | 改地址必须重算校验和,分片需重组 | 部分设备直接丢弃 |
| 连接跟踪 | 每条连接占一条 conntrack | 表满后新连接被丢 |
| P2P | 双方都在 NAT 后 | 只能 STUN 打洞或 TURN 中继 |
$ iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE # 出口 SNAT
$ iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 8080 \
-j DNAT --to-destination 10.0.0.12:8080 # 入站端口映射
$ conntrack -C && sysctl net.netfilter.nf_conntrack_max # 看跟踪表余量
$ dmesg | grep -i "nf_conntrack: table full"
容器里这层是隐藏的:Docker bridge 一层、宿主机再一层,"内网看到的源 IP 全是网关"。要拿真实源地址只能靠 X-Forwarded-For 或 PROXY protocol。
3. IPv6 的表示法:三条硬规则
128 位、8 组、每组 4 位十六进制。写错了是解析失败,不是"容忍":
- 每组前导零可省(
0db8→db8); ::只能出现一次,2001::db8::1非法(无法确定各压缩几组);- RFC 5952 规定不得用
::压缩单个零组,不得混排两种 v4 写法。
| 完整形式 | 规范写法 | 说明 |
|---|---|---|
| 2001:0db8:0000:0000:0000:0000:0000:0001 | 2001:db8::1 | 连续零组压缩 |
| 2001:0db8:0000:0001:0000:0000:0000:0001 | 2001:db8:0:1::1 | 只压最长零段 |
| 2001:0db8:0000:0000:0001:0000:0000:0001 | 2001:db8::1:0:0:1 | 等长零段压靠前一段 |
| 0000:0000:0000:0000:0000:0000:0000:0000 | :: | 未指定地址(DAD 期间作源) |
| 0000:0000:0000:0000:0000:ffff:0a00:0105 | ::ffff:10.0.1.5 | v4 映射,双栈套接字内部用 |
$ ip -6 addr show dev eth0 # 典型输出
inet6 2001:db8:1f0a:2c00::5/64 scope global dynamic
inet6 fe80::a00:27ff:fe4e:66a1/64 scope link
链路本地地址(fe80::/10)永远存在、无需配置,是邻居发现与 SLAAC 的底座。
3.1 类型与前缀:没有广播
| 类型 | IPv4 对应 | IPv6 表现 | 说明 |
|---|---|---|---|
| 单播 | 有 | 有 | 一对一 |
| 组播 | 224.0.0.0/4 | ff00::/8 | 接管了广播的职能 |
| 任播 | 靠 BGP 宣告同前缀 | 同地址多台宣告,路由取最近 | 一对"最近的一台" |
| 广播 | 255.255.255.255 | 不存在 | 用 ff02::1(all-nodes)替代 |
常用前缀:::1/128 环回、::/128 未指定(DAD 源地址占位)、fe80::/10 链路本地(邻居发现与下一跳)、fc00::/7 ULA(替代 10/8 与 192.168/16)、2000::/3 全局单播、ff00::/8 组播。广播取消的直接后果是 ARP 没了,改用 NDP,也不再能"广播探测整个网段"。
$ tcpdump -ni eth0 'icmp6 and (ip6[40] == 135 or ip6[40] == 136)' # NS/NA
$ ip -6 neigh show && ip -6 route show default
4. 报文头:定长 40 字节 vs 变长选项
IPv4 基本头 20 字节(含选项最长 60),每跳都要读选项、重算首部校验和。IPv6 把这两件事一起砍掉:基本头固定 40 字节,可选功能挪进扩展头链,首部校验和取消。
| 项 | IPv4 | IPv6 |
|---|---|---|
| 基本头 | 20 字节(最长 60) | 固定 40 字节 |
| 地址字段 | 源 4 + 目的 4 | 源 16 + 目的 16 |
| 首部校验和 | 有,每跳重算 | 无 |
| 分片 | 路由器可分片 | 仅源端分片(Fragment 头) |
| 选项 | 挤在首部内 | 扩展头链(Hop-by-Hop/Routing/Fragment) |
| 生存时间 | TTL | Hop Limit |
| 上层协议 | Protocol | Next Header |
| 地址解析 | ARP + 广播 | NDP + 组播 |
定长头的代价是"新功能都挂扩展头",而不少中间设备对扩展头处理不完整会直接丢包——这是"IPv6 能通但某段不通"的常见根源。
5. 自动配置:SLAAC 与 DHCPv6
| 维度 | SLAAC | DHCPv6(stateful) |
|---|---|---|
| 谁定地址 | RA 前缀 + 主机自生成接口标识 | 服务器分配并记租约 |
| 需要服务器 | 不需要 | 需要 |
| 中心租约表 | 无 | 有,可审计 |
| DNS 下发 | RDNSS(RFC 8106) | option 23 |
| RA 开关 | A 标志 | M/O 标志 |
| 适用 | 家庭、容器/云网络 | 企业审计、固定分配 |
$ sysctl net.ipv6.conf.eth0.use_tempaddr # 1 = 启用隐私临时地址
$ rdisc6 -1 eth0 # 抓 RA,看前缀与 A/M/O flags
6. 过渡技术:双栈、隧道、翻译
| 类别 | 代表 | 原理 | 适用面 | 主要局限 |
|---|---|---|---|---|
| 双栈 | 主机与网关同时配 v4/v6 | 两套栈并行、独立路由 | 最稳,长期方向 | 仍需 v4 地址,运维两套 |
| 隧道 | 6in4、6rd、DS-Lite、GRE | 一种协议封装进另一种 | 过渡期孤岛互联 | MTU 变小、PMTUD 易失效 |
| 翻译 | NAT64 + DNS64 | 改写地址/端口跨协议访问 | 纯 v6 网访问存量 v4 服务 | 破坏端到端,载荷带 IP 的协议失效 |
$ dig +short AAAA ipv4only.arpa @10.0.0.53
64:ff9b::c000:aa # 落在 RFC 6052 前缀 64:ff9b::/96 内即为 DNS64 合成
隧道最常踩 MTU:外层封装吃掉 20~60 字节,内层仍按 1500 发,遇不允许分片的 ICMPv6 被丢就表现为小包通、传文件卡死。
7. 监听绑定:0.0.0.0 和 [::] 不是一回事
bind(0.0.0.0:8080) 只接受 IPv4;bind([::]:8080) 在 IPV6_V6ONLY=0 时接受 IPv6 和 IPv4(v4 客户端以 ::ffff:a.b.c.d 映射地址进来)。默认值来自 net.ipv6.bindv6only。
$ ss -lntp | grep 8080
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("java",pid=5123,fd=88)) # 仅 v4
LISTEN 0 4096 [::]:8080 [::]:* users:(("java",pid=5123,fd=88)) # 双栈
| 绑定写法 | 实际接受 | 依赖 | 现象 |
|---|---|---|---|
0.0.0.0 |
仅 IPv4 | — | IPv6 客户端 Connection refused |
[::] 且 V6ONLY=0 |
v4 + v6 | bindv6only=0(多数发行版默认) |
两者都通 |
[::] 且 V6ONLY=1 |
仅 v6 | bindv6only=1 或显式 setsockopt |
v4 连不上 |
127.0.0.1 |
仅 v4 回环 | — | localhost 解析到 ::1 时本机也连不上 |
[::1] |
仅 v6 回环 | — | 镜像问题 |
# Spring Boot 3.3.x
server:
port: 8080
address: "::" # 双栈;只要 v4 写 0.0.0.0
server {
listen 80;
listen [::]:80; # nginx 不会自动继承,必须显式写
# 或一条:listen [::]:80 ipv6only=off;
}
-Djava.net.preferIPv4Stack=true 让 InetAddress 只返回 v4 结果,长期用等于放弃 v6。
8. 排错:五种典型现象
| 现象 | 原因 | 定位 | 处理 |
|---|---|---|---|
| 移动网络用户打不开,桌面正常 | AAAA 指向未监听 v6 的地址 | dig +short AAAA 域名、curl -6 |
补 v6 监听或暂撤 AAAA |
curl http://[::1]:8080 拒绝 |
只绑了 v4 | ss -lntp |
改 address: "::" |
| 放行 v4 没放行 v6 | nftables/ip6tables 是独立规则集 | nft list ruleset |
两套规则同步生成 |
| 内网 v6 通、跨段不通 | 网关未下发前缀路由或 RA | ip -6 route、ping -6 fe80::1%eth0 |
补路由与 RA |
| 校验把合法地址判非法 | 手写正则漏 ::/单零组/映射地址 |
拿 2001:db8::1、::ffff:10.0.1.5 试 |
用库解析,不用正则 |
校验那条:手写正则几乎必错,要覆盖压缩、大小写、映射地址、zone id。
import ipaddress
ipaddress.ip_address("2001:db8::1").version # 6
ipaddress.ip_address("2001:db8::1::2") # ValueError:`::` 两次
ipaddress.ip_address("::ffff:10.0.1.5").version # 6
9. 边界:什么情况不值得动
| 场景 | 更合适的做法 | 理由 |
|---|---|---|
| 端到端直连(P2P、VoIP、联机) | 双栈 + STUN,TURN 兜底 | 打洞成功率不是 100% |
| 大量存量 v4-only 设备 | 双栈并保留 NAT44 | 翻译层破坏载荷带 IP 的协议 |
| 只需内网隔离 | ULA + 不设默认路由 | 比 NAT 彻底,不需改写地址 |
| 当前无 v6 需求 | 维持 v4,但把监听改 :: |
成本为零,避免迁移才发现问题 |
一句话:IPv4 靠 NAT 掩盖问题,IPv6 把问题还原成"每台机器都有地址",所有依赖"地址不可达"的假设都要重查。
10. 自测
10.0.1.5/16与10.0.2.5/24互相认为对方在同网段吗?这种不对称会引发什么现象?- NAT 让 P2P 变难的根本原因是什么?STUN 在什么条件下才有效?
2001:db8::1:0:0:1展开是哪 8 组?2001::db8::1为什么非法?- IPv6 没有广播,"找网段内所有主机"靠什么?ARP 被什么替代?
- IPv6 取消首部校验和,可靠性由谁保证?会带来什么新的丢包风险?
- 服务绑
0.0.0.0:8080,curl http://[::1]:8080会怎样?改哪项配置能同时支持两者?