Punycode 转换

域名里出现中文、德语变音、西里尔字母时,DNS 其实并不认识它们——真正被解析的一直是 xn-- 开头的 ASCII 形式。这里把两种写法互相转出来。

中文句号「。」、全角顿号「.」和半角句点都会被当成分隔符处理,末尾的点会自动忽略。

Punycode 到底做了什么

DNS 协议是个老古董,只允许 a-z、0-9 和连字符。1996 年的时候 Martin Dürst 提出用「把非 ASCII 字符编码成 ASCII 串」的办法把 Unicode 塞进 DNS,后来这个方案被定为 RFC 3492,也就是 Punycode。它的编码串前面固定加 xn-- 前缀,所以看到 xn--bcher-kva.de 就知道这是个国际化域名,解开是 bücher.de。

编码算法叫 bootstring,思路很巧妙:先把所有 ASCII 字符原样复制到输出前面,用连字符收尾,然后把非 ASCII 字符按码点排序后逐个「插入」,每次只记录「相对于上一个插入位置的偏移」和「在剩余字符里的序号」这两个数字,再用一套变长整数规则把它们编成字母和数字。因为偏移量通常很小,编出来的串很短——这就是为什么 bücher 只变成 bcher-kva 而不是一长串。

浏览器为什么默认显示 Punycode

同形异义字攻击(homograph attack)是这里的关键。Unicode 里存在大量「长得像但不是同一个字符」的情况:拉丁字母 a 和西里尔字母 а(U+0430)在多数字体里几乎无法区分,数字 0 和字母 O 同理,还有各种看上去完全一样的希腊字母。攻击者注册 раypal.com(首字母是西里尔字母),在地址栏里显示成 paypal.com,用户点进去以为是真的。2005 年这个手法就被公开演示过。

于是浏览器定了一条规则:如果一个域名里混用了多种文字系统(脚本),地址栏就不显示漂亮的 Unicode 形式,而是直接显示 xn-- 那串东西。反过来,如果整个域名只用一种非拉丁文字(比如纯中文域名),浏览器会友好地显示中文,因为不存在「用中文冒充英文」的空间。上面的示例里有两个专门留着演示这个现象:正常的中文域名会被浏览器显示成中文,而那个西里尔字母的 раypal.com 会被显示成 Punycode。

注册和解析时的实际注意事项

注册中文域名前,先想清楚用户怎么输入。用户不会去打 xn--fiqs8s,他们在地址栏里输入的是中文,浏览器会自动转换,所以这块不用你操心;真正的坑在其它地方:邮件客户端、老旧的系统工具、部分 CDN 的控制台只接受 ASCII 域名,配置的时候要用 Punycode 形式;SSL 证书要签 ASCII 形式(现代 CA 支持带 IDN 的证书,但底层仍按 ASCII 存储);DNS 里配的也必须是 Punycode。

还有几个容易忽略的规范细节:一个标签不能超过 63 个字符(注意是转换后的 ASCII 长度),整串不能超过 253 个字符;标签不能以连字符开头或结尾;域名不区分大小写,工具统一输出小写形式;国际化域名在传输前还要做 IDNA 的规范化处理(Unicode 规范化、大小写折叠、双向文本检查),这些步骤比单纯的 Punycode 复杂得多,生产环境请用 url.domainToASCII() 或语言内置的 IDNA 库,本工具只覆盖 RFC 3492 的编解码和基础长度校验。另外,xn-- 之后那段的合法性靠解码时的校验来兜底,随便编一串字符去解码通常会报错,这正是「用解码来验证」的用法。