13.3 双栈、Happy Eyeballs 与 IPv6-only 过渡
驿道正在从 IPv4 迁往 IPv6,新旧地址并存时,同一个域名可能引出两条质量完全不同的路线。
一台主机有 IPv6 地址,不代表应用一定会用 IPv6;域名同时有 A 和 AAAA,也不代表客户端会机械地“先 IPv6,失败三秒后 IPv4”。真实连接建立还涉及系统解析器、地址排序、并发尝试、代理和网络质量。
DNS 返回记录,不替客户端选择路径
- A record 保存 IPv4 地址;
- AAAA record 保存 IPv6 地址;
- PTR 反向记录中的 IPv6 nibble 使用
ip6.arpa; - 普通权威 DNS 通常根据查询类型回答,不会因为客户端“支持 IPv6”就自动只返回 AAAA。
dig example.com A
dig example.com AAAA
getent ahosts example.comdig 观察 DNS 协议答案;getent 更接近使用系统名称服务配置的普通应用。二者不同可能来自 /etc/hosts、NSS、缓存、split DNS、搜索域或本地 resolver。
Python 中使用 getaddrinfo(),不要自己拼接 DNS 答案:
import socket
def stream_candidates(host: str, port: int):
seen = set()
for family, socktype, proto, _, sockaddr in socket.getaddrinfo(
host,
port,
family=socket.AF_UNSPEC,
type=socket.SOCK_STREAM,
proto=socket.IPPROTO_TCP,
):
key = (family, sockaddr)
if key not in seen:
seen.add(key)
yield family, socktype, proto, sockaddr返回顺序受到 RFC 6724 地址选择规则、平台策略和网络配置影响。应用仍须处理多个候选地址。
Happy Eyeballs 不是串行 fallback
最简单的客户端会依次尝试地址。如果首个 IPv6 路径是黑洞,一个数秒的 connect timeout 会让用户误以为整个服务很慢。
Happy Eyeballs v2 的核心是:对 DNS 结果排序,同时以小的时间间隔安排不同地址族的连接尝试;首个成功的连接获胜,其余尝试取消。它还包含 DNS 查询、连接尝试顺序和历史偏好的细节。
t=0 ms start first preferred-family attempt
t=250 ms if no winner, start next suitable attempt
... continue paced attempts
winner use connection; cancel/close others250 ms 只是 RFC 中的建议性默认量级,不应硬编码为所有网络的最佳值。并发过大也会浪费 socket、端口和服务端资源。
Python 标准库的 socket.create_connection() 会尝试多个地址,但不要假设所有版本与平台都实现了完整 RFC 8305 竞速。生产客户端优先使用已经明确支持 Happy Eyeballs 的成熟 HTTP/网络库,并测试 IPv6 黑洞、慢 AAAA 和单栈环境。
分别验证 IPv4 与 IPv6
curl -4 --verbose --connect-timeout 3 https://example.com/
curl -6 --verbose --connect-timeout 3 https://example.com/
ip -6 route get 2001:db8::10
ping -6 -c 3 2001:db8::10
traceroute -6 -n 2001:db8::10curl -4/-6 是诊断对照,不表示生产应用应长期强制某个地址族。若 IPv6 失败而 IPv4 成功,继续检查 AAAA 地址、源地址选择、默认路由、NDP、ICMPv6、MTU、防火墙和回程路由。
服务端如何监听双栈
IPv6 wildcard :: 是否同时接受 IPv4-mapped connections 取决于平台和 IPV6_V6ONLY。不要把一台 Linux 主机上的默认行为当成可移植契约。
Python 在支持的平台上可以显式请求 dual-stack socket:
import socket
if not socket.has_dualstack_ipv6():
raise RuntimeError("this platform does not expose a dual-stack IPv6 socket")
with socket.create_server(
("::", 8080),
family=socket.AF_INET6,
dualstack_ipv6=True,
) as server:
connection, peer = server.accept()
with connection:
connection.sendall(
b"HTTP/1.1 200 OK\r\n"
b"Content-Length: 3\r\n"
b"Connection: close\r\n"
b"\r\n"
b"ok\n"
)真实服务还需要请求解析、并发、deadline、上限、日志和优雅关闭。另一种更明确的方式是分别创建 IPv4 与 IPv6 listener,由服务框架管理。
三类过渡部署
双栈
主机和网络同时运行 IPv4/IPv6,应用通过地址选择与 Happy Eyeballs 选择可用路径。优点是两种协议都原生;代价是需要同时运营两套路由、安全策略、监控和容量。
双栈本身不需要 NAT64。只有当 IPv6-only 客户端需要访问 IPv4-only 服务时,NAT64 才进入路径。
IPv6-in-IPv4 隧道
隧道把 IPv6 包封装进 IPv4,以穿过 IPv4-only 区域。它仍用于受控网络和特定产品,但自动 6to4 与相关 relay anycast 已被 RFC 7526 废弃,不应作为新部署默认方案。
隧道会引入额外首部、MTU、可观测性和安全边界。必须明确 tunnel endpoint、有效 MTU、路由与过滤策略。
IPv6-only + DNS64/NAT64
当客户端只有 IPv6,而目标只有 IPv4 时:
- DNS64 根据 A record 合成带 NAT64 prefix 的 AAAA;
- 客户端向合成地址发送 IPv6;
- NAT64 translator 提取 IPv4 目标并维护必要的转换状态;
- IPv4 服务看到的是 translator 一侧的 IPv4 流量。
IPv6-only client
-> synthesized AAAA under Pref64
-> NAT64 translator
-> IPv4-only serverIETF well-known prefix 是 64:ff9b::/96,运营者也可以使用 network-specific prefix。RFC 6052 还定义了其他允许的前缀长度,不能对任意字符串简单拼上 32 位 IPv4。
下面只演示 /96:
import ipaddress
def embed_ipv4_in_96(prefix: str, ipv4: str) -> ipaddress.IPv6Address:
network = ipaddress.IPv6Network(prefix)
if network.prefixlen != 96:
raise ValueError("this example only implements RFC 6052 /96 embedding")
value = int(network.network_address) | int(ipaddress.IPv4Address(ipv4))
return ipaddress.IPv6Address(value)
assert str(embed_ipv4_in_96("64:ff9b::/96", "192.0.2.33")) == \
"64:ff9b::c000:221"NAT64 不是对所有应用透明:IPv4 literal、在 payload 中嵌入地址、某些非 TCP/UDP 协议、地址族相关 API 和 DNSSEC 验证边界都可能需要额外处理。移动网络常结合 464XLAT,让 IPv4-only 应用也能经过 IPv6-only 接入网。
DNSSEC 与 DNS64 的信任边界
DNS64 合成的 AAAA 不是权威区原本签名的数据。若客户端本地执行 DNSSEC 验证,它可能识别出合成结果与原始签名不符。部署必须明确验证发生在何处,并遵循 DNS64/DNSSEC 规范设计信任边界,不能简单关闭验证来“修好解析”。
IPv6 不需要 NAT,也不等于不需要防火墙
global unicast address 只是可路由地址类型。是否真正从公网可达仍由路由、stateful firewall、ACL、主机监听地址、云安全组和应用认证共同决定。
安全基线应至少覆盖:
- IPv4 与 IPv6 等价的入站/出站策略;
- 必要 ICMPv6,而不是全部屏蔽;
- link-local、ULA、global 等不同作用域;
- rogue RA/NDP 风险;
- 临时地址对日志、资产与访问控制的影响;
- IPv6 隧道与未受管地址是否绕过既有监控。
庞大的 /64 使顺序穷举低效,但攻击者可以从 DNS、证书透明度日志、服务扫描命中、固定 IID 和应用泄漏中发现活跃地址。地址空间大不是访问控制。
上线前验证矩阵
| 场景 | 必测项目 |
|---|---|
| IPv4-only client | A record、IPv4 listener、TLS/HTTP、回程 |
| IPv6-only client | AAAA 或 DNS64、IPv6 route、NDP、PMTUD、TLS/HTTP |
| dual-stack client | 地址排序、Happy Eyeballs、单族故障时的回退延迟 |
| IPv6-only → IPv4-only | Pref64、DNS64 synthesis、NAT64 session、literal 与 DNSSEC 边界 |
| load balancer/CDN | A/AAAA 后端一致性、健康检查协议族、日志中的真实地址 |
故障演练至少包括:坏 AAAA、IPv6 静默丢包、ICMPv6 Packet Too Big 被误拦、IPv4 正常而 IPv6 后端未发布,以及 NAT64 容量耗尽。
本课小结
双栈的难点不是多一类地址,而是两条可能独立失败的路径。Happy Eyeballs 用节奏受控的竞速降低单一路径故障的用户等待;IPv6-only 网络则可通过 DNS64/NAT64 访问 IPv4-only 服务,但翻译边界必须被明确测试和监控。
规范入口
- RFC 6724:Default Address Selection for IPv6;
- RFC 8305:Happy Eyeballs Version 2;
- RFC 6146:Stateful NAT64;
- RFC 6147:DNS64;
- RFC 6052:IPv6 Addressing of IPv4/IPv6 Translators;
- RFC 6877:464XLAT;
- RFC 7526:Deprecating Anycast Prefix for 6to4 Relay Routers。