国际化域名 IDN 与 Punycode:从 RFC 3492 到中文域名实战

一、为什么需要 Punycode

域名系统(DNS)诞生于 1980 年代,设计之初只考虑 ASCII 字符。一个合法的 DNS 标签只能包含字母(a-z)、数字(0-9)与连字符(-),长度不超过 63 字符。这意味着全球用户只能用英文注册域名,中文用户、日文用户、阿拉伯文用户被排除在外。

国际化域名(IDN,Internationalized Domain Name) 的目标很直接:让用户能用母语注册与访问域名,如 例子.公司みんな.jpمثال.ایران。但 DNS 基础设施不能直接处理 Unicode,于是需要一种编码方案,把 Unicode 字符串转换为 DNS 能识别的 ASCII 字符串,同时保证可逆性。这就是 Punycode

二、Punycode 在 IDN 标准栈中的位置

Punycode 不是孤立的标准,它是 IDN 标准栈的一环:

标准全称作用
RFC 3490Internationalizing Domain Names in ApplicationsIDNA2003 应用层转换协议(已过时)
RFC 3491Nameprep:A Stringprep Profile for IDN字符归一化(大小写折叠、NFKC)
RFC 3492Punycode:A Bootstring Encoding of Unicode核心编码算法(本文重点)
RFC 5890IDNA:ProtocolIDNA2008 协议(替代 IDNA2003)
RFC 5891IDNA:Internationalized Domain Name Registration注册与解析流程
RFC 5892-5895IDNA2008 配套映射规则字符映射表与上下文规则

演进要点:IDNA2003(RFC 3490/3491/3492)在 2010 年被 IDNA2008(RFC 5890/5891)取代。IDNA2008 不再对字符做映射,而是定义明确的「允许 / 禁止」规则,由注册商按策略处理。但 Punycode 算法本身(RFC 3492)始终未变,仍是所有 IDN 编码的事实标准。

三、Bootstring 算法核心思路

Punycode 基于 Bootstring 算法,这是一种通用的 Unicode→ASCII 编码框架。核心思路分三步:

第 1 步:分离基本字符

将输入字符串中的 ASCII 字符(码点 < 128)直接复制到输出,用「-」分隔基本部分与编码部分。若没有基本字符则省略分隔符。

输入:bücher
基本字符:b, u, c, h, e, r → "bcher-"
非基本字符:ü(码点 252)

第 2 步:按码点升序编码非基本字符

从码点 128 开始升序遍历,每找到一个新字符就计算其位置 delta 值。delta 累加了「跳过多少已处理字符」与「新字符比上一个字符大多少」两个量。

处理 ü(码点 252):
  delta += (252 - 128) × (已处理字符数 + 1)
  插入 ü 到正确位置

第 3 步:base 36 编码与 bias 自适应

delta 值用 base 36 编码(0-9a-z),并通过 bias 自适应算法 调整阈值 t 使输出分布均匀。bias 算法的核心是 damp(首次衰减)、skew(分布偏移)与 numPoints(已处理字符数补偿)。

固定常量(RFC 3492 第 5 节):

base = 36        # 编码基数
tmin = 1         # 阈值下限
tmax = 26        # 阈值上限
skew = 38        # 分布偏移
damp = 700       # 首次衰减系数
initial_bias = 72
initial_n = 128  # 起始码点

阈值 t 的计算:t = max(tmin, min(tmax, k - bias)),其中 k 是当前累积权重。bias 越大,t 越小,输出的字符越多——这就是「自适应」的本质。

四、编码实战:例子 → fsqu00a

以中文「例子」为例,逐步演示编码过程:

输入:例子(码点 20363, 23376)

1. 分离基本字符:无 ASCII 字符,基本部分为空
   output = ""
   不需要 "-" 分隔符

2. 主循环:
   - 找最小码点 m = 20363(例)
   - delta += (20363 - 128) × 1 = 20235
   - 处理「例」:将 delta=20235 编码为 base 36
     k=36 时 t=max(1, min(26, 36-72))=1
     20235 >= 1,输出 (1 + (20235-1) % 35) = (1 + 20234 % 35) = (1 + 24) = 25 → 'p'
     q = (20235 - 1) / 35 = 578
     k=72 时 t=max(1, min(26, 72-72))=1
     578 >= 1,输出 (1 + (578-1) % 35) = (1 + 577 % 35) = (1 + 12) = 13 → 'd'
     q = (578 - 1) / 35 = 16
     k=108 时 t=max(1, min(26, 108-72))=26
     16 < 26,输出 16 → 'q'
     bias = adapt(delta=20235, numPoints=2, firstTime=true)
   - delta = 0, n = 20364

   - 找下一个最小码点 m = 23376(子)
   - delta += (23376 - 20364) × 2 = 6024
   - 处理「子」:将 delta=6024 编码...
   ...

最终输出:fsqu00a(仅含小写字母与数字,符合 Punycode 规范)。加上 xn-- 前缀后变成 xn--fsqu00a,这是 DNS 中实际存储的形式。

五、xn— 前缀的语义

xn-- 是 RFC 5891 规定的 ACE(ASCII Compatible Encoding)前缀。它的作用是双重的:

  1. 标识:让 DNS 解析器与浏览器识别这是一个 Punycode 编码的国际化标签
  2. 隔离:避免与既有的 ASCII 域名冲突(理论上没有人会注册以 xn-- 开头的纯 ASCII 域名)

浏览器的工作流程:

用户输入「例子.com」
  ↓ 浏览器 IDNA 转换
DNS 查询「xn--fsqu00a.com」
  ↓ DNS 返回 IP
浏览器地址栏显示「例子.com」(Unicode 形式供用户阅读)

六、多标签域名的逐标签处理

完整域名按点号分隔为多个标签,每个标签独立判断与转换:

输入:例子.工具盒子.com
分解为:["例子", "工具盒子", "com"]

标签 1「例子」含非 ASCII → Punycode 编码 → "fsqu00a" → "xn--fsqu00a"
标签 2「工具盒子」含非 ASCII → Punycode 编码 → "h6qx3vv4bk65b" → "xn--h6qx3vv4bk65b"
标签 3「com」纯 ASCII → 原样保留

最终输出:xn--fsqu00a.xn--h6qx3vv4bk65b.com

关键规则:只有含非 ASCII 字符的标签才编码,纯 ASCII 标签(如 comcnorg)原样保留。这也是为什么工具需要做「标签级详情」展示——让用户清楚地看到每个标签的处理路径。

七、长度限制与边界场景

标签长度上限

RFC 1035 规定单个 DNS 标签不超过 63 字符。对 ACE 标签而言,这 63 字符包含 xn-- 前缀(4 字符)。这意味着 Punycode 编码部分最多 59 字符。

中文字符的码点普遍在 19968-40959 之间(常用 CJK 统一表意文字),单字符编码后约需 4-5 个 base 36 字符。粗略估算:59 ÷ 4.5 ≈ 13,即一个 ACE 标签大约能容纳 13 个中文字符。

完整域名长度

RFC 1035 规定完整域名(含点号)不超过 253 字符。

尾部点号

FQDN(Fully Qualified Domain Name)末尾可能有一个点号,如 example.com.。本工具在编码前会去掉尾部点号,避免影响标签拆分。

连续点号

连续点号意味着空标签,属于非法输入。本工具会返回错误提示「域名包含空标签(连续点号)」。

八、Emoji 域名:能编码但不建议

Emoji 属于 Unicode 增补平面字符(码点 > 65535),需要代理对表示。JavaScript 字符串中 length 属性返回的是 UTF-16 单元数而非码点数,直接用 for...i 遍历会拆错字符。正确做法是用 for...of 或展开运算符按码点遍历:

// 错误:拆分代理对
for (let i = 0; i < str.length; i++) str.charCodeAt(i);

// 正确:按码点遍历
for (const ch of str) ch.codePointAt(0);

🎉 的码点是 127881,Punycode 编码后为 q266a,即 xn--q266a。理论上能编码,但实际使用存在严重限制:

  1. 多数顶级域名不支持 Emoji 注册:ICANN 政策禁止 Emoji 出现在顶级域名
  2. 浏览器支持参差不齐:部分浏览器会拒绝显示 Emoji 域名
  3. 钓鱼与欺诈风险:Emoji 域名常被用于钓鱼,浏览器可能直接拦截

九、IDN 同形字钓鱼:安全风险与防御

Punycode 最大的安全风险来自 同形字攻击(Homograph Attack)。不同文字系统中存在视觉上相似但码点不同的字符:

ascii "a"     (U+0061)
cyrillic "а"  (U+0430)
greek "α"     (U+03B1)

攻击者可注册 аpple.com(用西里尔字母 а),其 Punycode 为 xn--80ak6aa92e.com,视觉上与 apple.com 几乎无法区分。

防御措施

层级防御措施
浏览器对混合文字标签显示 Punycode 原形(如 Chrome 显示 xn--...
注册商限制每个域名只能使用一种文字(不混用拉丁与西里尔)
IDNA2008禁止易混淆字符(如 ZWNJ 零宽连接符)
用户警惕地址栏显示 xn-- 前缀的域名,谨慎输入密码

浏览器策略:当标签含多种文字(如 latin + cyrillic)时,Chrome / Firefox 会直接显示 xn-- 原形而非 Unicode 形式,这是对同形字钓鱼的第一道防线。

十、纯原生 TypeScript 实现要点

本站 Punycode 工具采用纯原生 TypeScript 零依赖实现,核心要点:

1. 码点切分用 for...of

JavaScript 字符串的 for...icharCodeAt 处理代理对时会拆错字符。必须用 for...of 或展开运算符按码点遍历,才能正确处理 Emoji 与增补平面字符。

2. bias 自适应是核心

bias 算法是 Punycode 的灵魂。编码与解码共用同一个 adapt 函数,保证两端一致。首次使用 damp 衰减,之后减半,最后加 numPoints 比例补偿。

3. 异常包装避免 UI 崩溃

charToDigit 可能抛错(输入含非法字符),需在外层用 try-catch 包装为 PunycodeResult 错误对象,避免 UI 层需要额外处理。

4. 标签级处理而非整体编码

不能对整个域名做一次 Punycode 编码,必须按点号分隔为标签后逐个处理。原因有二:

  • 不同标签可能使用不同文字(如 中文.example.com
  • ASCII 标签(如 com)不应被编码

5. ACE 前缀判断要大小写不敏感

虽然 RFC 规定 xn-- 必须小写,但用户输入可能大小写混杂。判断时应统一转小写,避免漏判 XN--Xn--

十一、工具矩阵联动

Punycode 工具与本站其他工具形成联动:

工具联动场景
URL 工具URL 中的 host 部分可能是 IDN,先用 Punycode 转换再解析更准确
Base64 工具通用二进制编码,可作为 Punycode 的替代方案对比(不推荐用于 IDN)
Base32 工具TOTP 密钥编码场景,与 Punycode 的「DNS 安全字符」需求形成对比
Hex 工具字节级调试,可用于查看 Punycode 编码结果的字节分布
JWT 工具JWT 的 iss/sub 字段可能含域名,IDN 场景下需先 Punycode 编码

十二、常见陷阱

陷阱 1:用 String.length 判断字符数

"🎉".length 返回 2(代理对),不是 1。判断字符数必须用 [...str].length

陷阱 2:忘记处理尾部点号

FQDN 末尾的点号(如 example.com.)不是标签,编码前必须去掉,否则会产生空标签错误。

陷阱 3:对 ASCII 标签也加 xn— 前缀

comcnorg 等纯 ASCII 标签不应被编码。只有含非 ASCII 字符的标签才需要 Punycode 编码与 xn-- 前缀。

陷阱 4:忽略 IDNA 映射规则

IDNA2008 要求注册前做字符映射(NFKC 归一化、禁止字符过滤)。直接用 Punycode 编码原始 Unicode 可能产生与浏览器不一致的结果。本工具聚焦 Punycode 算法本身,不做 IDNA 映射,适合调试与学习场景。

陷阱 5:假设浏览器一定显示 Unicode

浏览器对混合文字标签会显示 xn-- 原形,而非 Unicode。不能假设「用户看到的域名」与「输入的 IDN」一致。

总结

Punycode 是 IDN 生态的核心算法,纯原生 TypeScript 零依赖即可实现。理解 Bootstring 的 bias 自适应、码点升序编码、标签级处理三个核心点,就能应对绝大多数 IDN 场景。实际使用中务必警惕同形字钓鱼与 Emoji 域名风险,优先信任浏览器显示的 xn-- 前缀作为安全线索。

本站 Punycode 编解码工具 已集成上述全部能力,欢迎试用。