AAAA 回答给出 IPv6 目的地址,另一次真实连接才能验证服务是否工作。

AAAA 发布 IPv6 地址,不证明该地址可达

一条 AAAA 资源记录保存一个一百二十八位 IPv6 地址,A 记录则保存 IPv4 地址。同一个名称可以只有其中一类、同时拥有两类,或者在每类中包含多个地址。A 与 AAAA 属于不同记录集,因此可以使用不同 TTL。发布 AAAA 让地址能够通过 DNS 被发现,却不会替你创建接口、路由、监听套接字或可用应用;这些条件需要在实际服务路径上成立。 坏掉的样子 AAAA 查询成功之后,连接仍可能失败。发布的地址可能已经停用,服务可能只监听 IPv4,网络也可能缺少可用 IPv6 路由。两种地址族的防火墙与 TLS 配置也可能不同。只通过 IPv4 做可用性检查,会漏掉这些问题,而双栈用户可能正沿着公开的 IPv6 路径遭遇延迟或错误。记录存在与服务可用是两个需要分别观察的事实。 Happy Eyeballs 通过协调地址发现和连接尝试,减少部分双栈连接延迟,但它属于客户端算法,并不是 DNS 对所有客户端的保证。具体回退行为、截止时间和应用协议仍会影响结果。某个浏览器在一次测试中迅速改用 A 记录成功,不能推出错误 AAAA 对所有调用方无害。尤其对于没有相同行为的库、设备或后台任务,应验证其真实连接策略。 缺少 AAAA 也应准确描述。权威服务器可以对只有 A 的名称返回成功 DNS 状态,却没有 AAAA 数据,这通常称为 NODATA;它不同于表示名称不存在的 NXDOMAIN。仅有 A 的服务也不是在所有 IPv6-only 网络中都绝对不可达:具备相应基础设施时,DNS64 可以合成 AAAA,再通过 NAT64 访问 IPv4 服务器。不能忽略翻译网络这一明确反例。 ...

十二月 30, 2027 · 3 分钟 · XB Field Notes
IPv6 分片由原始发送端产生,中间路由器对超大未分片报文返回 Packet Too Big。

IPv6 路由器报告超大报文,由源端决定怎样适应路径

IPv6 路由器在向目的地转发报文时,不会替它进行分片。如果报文大于下一条链路所能承载的大小,路由器通常丢弃它,并发送 ICMPv6 Packet Too Big 反馈。源端随后可以调整报文大小,或在适当情况下通过 IPv6 分片扩展头发送分片;这些分片在目的端重组。明确各阶段由谁负责,是判断超大报文故障的基础。 坏掉的样子 假定路由器会自动拆开过大的 IPv6 包,会掩盖与大小相关的故障。相同地址与路由上,小 echo 可以成功,大 echo 却失败。端点本地接口可能支持原始大小,中间链路却不支持。应检查整条路径约束,不能仅因接口配置了 MTU 1500,就推断该大小能够端到端通过,也不能用一次小探测成功替代完整大小检查。 Packet Too Big 反馈缺失,会使经典发现更难学到这个限制。分片被过滤,或者其中一片丢失,也可能阻止正确重组。能够生成分片扩展头,不证明沿途策略都允许相应报文。不能把源端分片当作正确路径大小与必要控制流量的万能替代,它有自己的交付条件,需要通过对应流量验证,而不是只检查发送选项已经开启。 反方向也有自己的路径估计。请求发送端学到 MTU,不会自动教会响应端怎样发送较大回复。因此双向 echo 测试需要分别了解两个方向。TCP 分段还不同于 IP 分片:TCP 发送端学到限制后,可以构造更小的段,而不必把每个 IP 包再分片。应先确定观察的是哪一层的切分,避免看到多个包就统称为分片。 快速验证 将脚本保存为 check-ipv6-fragmentation.py,准备 Docker 后运行 python3 check-ipv6-fragmentation.py。一次性环境使用 Python 3.11.16、iproute2 6.1.0、iputils ping 和 tcpdump 4.99.3,在 Linux 6.12.76 上验证。NET_ADMIN 与 SYS_ADMIN 支持四个嵌套命名空间。两台路由器和两个端点使用新建 veth,不修改宿主接口或路由: ...

十月 6, 2027 · 3 分钟 · XB Field Notes
多种 IPv6 写法可以表示相同一百二十八位地址,区域和端口则提供额外端点信息。

IPv6 文本规范化保留地址,端点上下文仍须另外处理

IPv6 地址包含一百二十八位,通常写成八组十六进制数字。文本可以省略组内前导零,并用一次 :: 代替连续全零组。这些规则缩短写法而不丢失地址。RFC 4291 定义有效形式,RFC 5952 推荐一致的显示与交换形式。语法是否有效,和拼写是否符合推荐规范,是相关但不同的问题,不能用一个判断替代另一个。 坏掉的样子 直接比较原始字符串,可能把同一个地址当成多个身份。大写字母、前导零和展开的零组都能改变文本,却不改变底层位值,从而拆散资产记录,或破坏应用原本想进行的地址比较。应先使用适合输入类型的库解析,再比较地址值。仅转换成小写,不会处理所有零组和压缩差异,因此不能称为完整规范化。 压缩本身也有限制。:: 最多出现一次,否则无法确定各个省略段的长度。推荐表示应压缩最长连续零组;长度相同时选择最前一组,并且不要压缩单个零字段。一般 IPv6 语法允许部分推荐显示规则不采用的写法。规范化工具应区分有效但非首选的形式与真正非法输入,而不是把两者都报告成相同错误。 地址也未必构成完整端点。URL 中的 IPv6 字面量需要括号,将地址内部冒号与端口分隔符区别开。链路本地目的地还可能需要区域标识,以选择正确接口范围。如果没有明确比较策略就删除区域,或把映射 IPv4 地址扁平化,可能丢失调用方建立正确连接所需的信息。字符串变短,不代表端点含义仍完整。 快速验证 将脚本保存为 check-ipv6-text.py,准备 Docker 后执行 python3 check-ipv6-text.py。固定 Python 3.11.16 镜像只运行标准库解析,不产生网络流量。程序比较有效变体、拒绝错误输入并检查端点组成,没有配置示例地址、发送 IPv6 报文或证明这些地址具有可用路由。解析验证与网络可达性在这里明确分开: import json,subprocess,uuid image="python@sha256:528257d48c1da0dcecc2e725d1ae34498d60c965f1241e39cd6a85a8859bdf84" program=r""" from ipaddress import IPv6Address from urllib.parse import urlsplit import json variants=['2001:0DB8:0000:0000:0000:ff00:0042:8329','2001:db8:0:0:0:ff00:42:8329','2001:db8::ff00:42:8329'] parsed=[IPv6Address(v) for v in variants];assert len(set(parsed))==1 and len(set(variants))==3 tie=IPv6Address('2001:0:0:1:0:0:1:1').compressed;assert tie=='2001::1:0:0:1:1' single=IPv6Address('2001:db8::1:2:3:4:5').compressed;assert single=='2001:db8:0:1:2:3:4:5' bad=[] for text in ['2001::db8::1','2001:db8:1:2:3:4:5:6:7','2001:db8::gg','2001:db8::1/64']: try:IPv6Address(text);raise AssertionError('invalid address accepted') except ValueError:bad.append(text) url=urlsplit('https://[2001:db8::1]:8443/health');assert IPv6Address(url.hostname)==IPv6Address('2001:db8::1') and url.port==8443 try:urlsplit('https://2001:db8::1:8443/health').port;raise AssertionError('unbracketed endpoint accepted') except ValueError:bad_url=True mapped=IPv6Address('::ffff:192.0.2.1');assert str(mapped.ipv4_mapped)=='192.0.2.1' zone_a=IPv6Address('fe80::1%eth0');zone_b=IPv6Address('fe80::1%eth1') assert zone_a!=zone_b and zone_a.packed==zone_b.packed print(json.dumps({'variants':variants,'same_128_bits':len(set(parsed))==1,'normalized':str(parsed[0]), 'leftmost_tie':tie,'single_zero_retained':single,'rejected':bad,'url_host':url.hostname,'url_port':url.port, 'unbracketed_url_port_rejected':bad_url,'mapped_python_text':mapped.compressed,'mapped_ipv4':str(mapped.ipv4_mapped), 'zone_scope_ids':[zone_a.scope_id,zone_b.scope_id],'same_bits_different_scopes':True})) """ name='xbcatm-socket-'+uuid.uuid4().hex[:10] try: result=subprocess.run(['docker','run','--rm','-i','--name',name,'--network','none', '--memory','128m','--pids-limit','64',image,'python3','-'],input=program, capture_output=True,text=True,timeout=60) assert result.returncode==0,result.stderr print(json.dumps(json.loads(result.stdout),ensure_ascii=False,sort_keys=True)) finally: subprocess.run(['docker','rm','-f',name],capture_output=True,text=True,timeout=10) 三种字符串表示同一地址,包括展开的大写形式 2001:0DB8:0000:0000:0000:ff00:0042:8329 与 2001:db8::ff00:42:8329。原始字符串集合有三项,解析后的地址集合只有一项,规范输出为后者。这个对照直接说明:如果应用身份指的是底层地址,原始拼写就不是可靠索引键,不能因为文字不同便建立多个独立对象。 ...

十一月 30, 2026 · 1 分钟 · XB Field Notes
阻断 IPv6 邻居通告后,即使 TCP 服务仍监听,客户端也无法解析目标链路地址。

IPv6 先用 ICMPv6 解析邻居,TCP 才能正常通信

以太网链路上的 IPv6 主机,需要先取得邻居的链路层目的地址,才能发送相应帧。邻居发现通过 ICMPv6 的邻居请求和邻居通告完成地址解析,同时还涵盖路由器发现、邻居可达性检测等超出 IPv4 ARP 的功能。把它简单理解为“地址更长的 ARP”,会掩盖配置 IPv6 网络时必须考虑的依赖关系,尤其是控制报文与应用通信之间的联系。 坏掉的样子 拥有有效 IPv6 地址和路由,不代表能够解析下一跳的以太网地址。如果必要的请求或通告被过滤,TCP 服务可以一直正常监听,客户端却在交换应用数据前就失败。只开放应用 TCP 端口,无法修复这个较低层依赖。排查时应把邻居表、相关 ICMPv6 流量和服务监听一起观察,不能只从端口是否开放判断整条路径。 缓存状态还可能隐藏错误。已有可用邻居条目的主机,在破坏性规则安装后可能继续通信;新客户端或条目失效之后,故障才暴露。反过来,STALE 状态也不自动意味着损坏:邻居发现可以利用已有链路地址,随后再检查可达性。应理解状态转换,不能要求每个正常条目永远显示 REACHABLE,否则会把正常缓存生命周期误判为故障。 另一类混淆,是在本地以太网段上解析远端应用服务器地址。目的地位于链路之外时,主机通常解析选中的下一跳路由器,而不是远端服务器。判断邻居表应出现哪个地址之前,需要先确认选路。捕获接口错误,或者已有缓存使本次无需重新发送请求,也能解释空白抓包结果,不能据此证明邻居发现根本没有发生。 快速验证 将脚本保存为 check-neighbor-discovery.py,准备 Docker 后运行 python3 check-neighbor-discovery.py。一次性 Linux 环境使用 Python 3.11.16、iproute2 6.1.0、nftables 1.0.6 和 tcpdump 4.99.3。NET_ADMIN 与 SYS_ADMIN 支持嵌套命名空间和临时 proc 挂载。两个端点通过新建 veth 相连,不修改宿主邻居表或防火墙规则: ...

十一月 16, 2026 · 3 分钟 · XB Field Notes
服务停止后首个地址仍可能保持优先,由客户端连接处理选择可用替代项。

双栈地址排序与最终成功的连接选择是不同步骤

发现 A 与 AAAA 记录,只提供可能的目的地,本身不会选出成功连接。地址排序、源地址选择、路由以及应用连接算法都会参与结果。RFC 6724 描述默认源地址和目的地址选择规则,应用仍可作自己的选择。如果程序只请求 IPv4,那么在任何双栈连接策略开始工作前,它就已经缩小了候选范围。解释行为时,应先确认实际调用方请求了什么。 坏掉的样子 地址偏好不是应用健康探测。某个 IPv6 目的地可能符合本地策略和路由条件,远端服务却已经停止,或者更后面的网络跳点会丢包,因此优先候选仍可能失败。如果假定操作系统已经测试了返回列表中每个端点,就会忽略连接截止时间和尝试其他地址的必要性。查询成功只能证明获得候选信息,不能代替服务通信结果。 RFC 6724 的规则是按顺序比较条件,并不是对网络成本或速度打一个总分。相关属性包括可用性、作用域、策略标签、优先顺序和匹配前缀,但它们不测量应用响应时间。目的地址选择还应与路由表最长前缀匹配区分:后者决定已选目的地的数据包如何发送。混用这两个阶段,会把主机偏好错误解释成已经知道整条路径延迟最低。 Happy Eyeballs 属于另一层,通过协调地址发现和错开连接尝试,减少某个地址族出问题时的等待。按顺序逐个尝试地址的客户端,并不自动实现该算法。立即拒绝连接与静默丢弃连接尝试,可能造成非常不同的等待时间,即使最后都能通过 IPv4 成功。不能只看最终用了另一个地址族,就把过程统称为相同的快速竞速机制。 快速验证 将脚本保存为 check-address-selection.py,准备 Docker 后执行 python3 check-address-selection.py。固定环境使用 glibc 2.36、Python 3.11,以及提供 DNS 的 BIND 9.18.49 和 dnsmasq 2.90。两个真实 HTTP 监听分别使用 IPv4 与 IPv6 回环,端口相同。实验没有公网路径,因此观察到的排序明确属于这个本地环境,不作为所有客户端的默认承诺: ...

十一月 4, 2026 · 3 分钟 · XB Field Notes