IPv6 地址包含一百二十八位,通常写成八组十六进制数字。文本可以省略组内前导零,并用一次 :: 代替连续全零组。这些规则缩短写法而不丢失地址。RFC 4291 定义有效形式,RFC 5952 推荐一致的显示与交换形式。语法是否有效,和拼写是否符合推荐规范,是相关但不同的问题,不能用一个判断替代另一个。
坏掉的样子
直接比较原始字符串,可能把同一个地址当成多个身份。大写字母、前导零和展开的零组都能改变文本,却不改变底层位值,从而拆散资产记录,或破坏应用原本想进行的地址比较。应先使用适合输入类型的库解析,再比较地址值。仅转换成小写,不会处理所有零组和压缩差异,因此不能称为完整规范化。
压缩本身也有限制。:: 最多出现一次,否则无法确定各个省略段的长度。推荐表示应压缩最长连续零组;长度相同时选择最前一组,并且不要压缩单个零字段。一般 IPv6 语法允许部分推荐显示规则不采用的写法。规范化工具应区分有效但非首选的形式与真正非法输入,而不是把两者都报告成相同错误。
地址也未必构成完整端点。URL 中的 IPv6 字面量需要括号,将地址内部冒号与端口分隔符区别开。链路本地目的地还可能需要区域标识,以选择正确接口范围。如果没有明确比较策略就删除区域,或把映射 IPv4 地址扁平化,可能丢失调用方建立正确连接所需的信息。字符串变短,不代表端点含义仍完整。
快速验证
将脚本保存为 check-ipv6-text.py,使用 Python 3.12 或更新版本执行 python3 check-ipv6-text.py。本轮用 Python 3.12.14 原样运行此脚本,只调用标准库解析,不产生网络流量。程序比较有效变体、拒绝错误输入并检查端点组成,没有配置示例地址、发送 IPv6 报文或证明这些地址具有可用路由。解析验证与网络可达性在这里明确分开:
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}))
三种字符串表示同一地址,包括展开的大写形式 2001:0DB8:0000:0000:0000:ff00:0042:8329 与 2001:db8::ff00:42:8329。原始字符串集合有三项,解析后的地址集合只有一项,规范输出为后者。这个对照直接说明:如果应用身份指的是底层地址,原始拼写就不是可靠索引键,不能因为文字不同便建立多个独立对象。
对于 2001:0:0:1:0:0:1:1,两个零段等长,第一段被缩短,结果是 2001::1:0:0:1:1。有效输入 2001:db8::1:2:3:4:5 只省略一个零组,规范输出会恢复为 2001:db8:0:1:2:3:4:5。实验明确验证单零组规则,没有推荐凡是能够缩短字符串就进行压缩。规范化追求一致性,而不只是字符数量更少。
包含两个 ::、组数过多或非十六进制字符的输入都解析失败。结尾带 /64 的字符串也不能交给仅接受地址的构造器,因为它属于另一类输入。契约包含前缀时,应选择接口或网络解析器,不能悄悄删除后缀,只为让地址构造器接受。前缀是否允许、是否必须保留,应由字段含义明确决定。
带括号的 URL https://[2001:db8::1]:8443/health 能正确分离主机与端口;读取故意去掉括号的对应 URL 的端口时,程序抛出 ValueError。实验还识别 ::ffff:192.0.2.1 内的 IPv4 部分,本次 Python 3.12.14 构建的压缩输出为 ::ffff:192.0.2.1,而 ipv4_mapped 属性给出 192.0.2.1。这些结果说明被测解析器的行为,没有承诺所有运行时都对特殊地址采用完全相同格式。
fe80::1%eth0 与 fe80::1%eth1 的二进制地址字节相同,保留范围后却不相等。区域属于附加上下文,不在一百二十八位地址之内。实验没有联系这些接口,只检查表示方式能够保留差异,不证明任一带范围目标已经配置或可以访问。把纯地址用于比较时,应明确是否有意排除了这项连接信息。
存储文本前先确定比较契约
先决定字段保存地址、网络前缀、接口配置、URL,还是套接字端点,再选用对应解析器并保留必要组成。单独保留原始输入有助于解释配置错误,规范表示则便于一致显示与索引。解析或规范化都不会认证谁控制地址,也不会证明目标服务安全。它们解决表示和解释问题,不提供应用访问所需的全部信任属性。
关联日志或应用地址规则时,应记录如何处理映射 IPv4 和区域标识,并针对等价有效写法、非法输入和带上下文案例验证策略。目标是稳定解释预期端点,而不是把所有内容压成最短字符串。只有地址位值和周边连接上下文都明确保留,格式处理才不会在清理外观时悄悄改变调用方真正想表达的目的地。
来源
- RFC 4291 IPv6 addressing,有效 IPv6 语法、压缩写法和嵌入 IPv4 地址,已核验 2026-10-15。
- RFC 5952 IPv6 text representation,推荐的零压缩、等长选择和小写形式,已核验 2026-10-15。
- RFC 3986 URI syntax,URI 中的 IP 字面量括号及端口分隔,已核验 2026-10-15。
- Python 3.12 ipaddress,解析后的地址比较、映射地址与范围标识,已核验 2026-10-15。