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。原始字符串集合有三项,解析后的地址集合只有一项,规范输出为后者。这个对照直接说明:如果应用身份指的是底层地址,原始拼写就不是可靠索引键,不能因为文字不同便建立多个独立对象。 ...