Skip to main content

Punycode Explained: How Unicode Domains Become xn-- ASCII

How Punycode turns Unicode domain names into ASCII xn-- form, why DNS demands ASCII, and how decoding an xn-- host exposes homograph phishing.

Published By Li Lei
#punycode #idn #dns #phishing #encoding

Punycode Explained: How Unicode Domains Become xn-- ASCII

The first time I saw xn--mnchen-3ya.de in a TLS certificate log, I assumed something had gone wrong. It hadn't. That string is just münchen.de wearing the only clothes DNS will let it travel in. Punycode is the quiet machinery that lets a domain written in Chinese, Arabic, or German still resolve through an infrastructure that was built decades ago to understand a few dozen ASCII characters and nothing else.

This post walks through what Punycode actually does, why the internet still needs it, and how the same decode step that fixes your DNS panel also exposes a phishing trick that has fooled plenty of careful people.

Why DNS still only speaks ASCII

DNS predates the web. When it was designed, a hostname label was defined as a string of letters, digits, and hyphens — the so-called LDH rule — and that definition is baked into resolvers, caches, mail servers, and TLS libraries all over the world. You cannot simply start shipping raw UTF-8 bytes through that pipeline and expect every hop to cope. Some would, many would choke, and the failures would be silent and inconsistent.

So instead of rewriting the entire resolution stack, the standards bodies chose a different path: keep the wire format ASCII, and define a reversible encoding that packs Unicode into the LDH alphabet. That encoding is Punycode, specified in RFC 3492, and the system that wraps it for domain names is called Internationalized Domain Names in Applications, or IDN.

The key design constraint is that the transformation has to be exactly reversible. A resolver encodes 例え.jp to its ASCII form to look it up, and a browser decodes it back to 例え.jp to show you. If the round-trip lost a single code point, the wrong site would load. Punycode is built around that guarantee.

What the xn-- prefix actually means

Here is the one concrete fact worth memorizing: the xn-- prefix marks a domain label as an encoded IDN. When a label contains any non-ASCII character, the whole label is run through the RFC 3492 bootstring algorithm and the result is prefixed with xn--. That four-character tag — the ACE, or ASCII-Compatible Encoding, prefix — tells every resolver and browser "the rest of this label is Punycode, decode it before you display it."

Two details matter. First, Punycode works per label, not per domain. The dots in a hostname are boundaries, not characters to encode. shop.münchen.de has three labels; only the middle one is non-ASCII, so only it becomes xn--mnchen-3ya while shop and de pass through untouched. Second, a pure-ASCII label is never given an xn-- prefix at all, which is why com, org, and ordinary English domains look exactly the same as they always have.

If you want a feel for how text-to-ASCII encodings differ from one another, it is worth comparing Punycode against percent-encoding in the URL encoder — they solve adjacent problems with very different output shapes, and seeing them side by side makes the design choices click.

A worked example

Take the German city domain münchen.de and encode it. The algorithm processes the münchen label like this: it copies the ASCII characters it can keep directly — m, n, c, h, e, n — drops the basic-code-point delimiter, then encodes the position and value of the one non-ASCII character (ü) as the suffix 3ya, using a generalized variable-length integer scheme with bias adaptation. The output:

Input:   münchen.de
Encode:  xn--mnchen-3ya.de
Decode:  münchen.de

Read the encoded label left to right and the structure is visible: xn-- (the ACE tag), mnchen (the ASCII letters in order), - (the delimiter separating basic from encoded), and 3ya (the compressed instructions for reinserting ü at the right spot). Run a Chinese domain through the same path and you get 中文.中国xn--fiq228c.xn--fiqs8s, with each label — including the .中国 top-level domain — encoded independently. You can try both directions yourself in the Punycode converter, which shows a per-label breakdown so you can see exactly which segment maps to which xn-- form.

The homograph phishing angle

This is where Punycode stops being a curiosity and starts being a security tool. Many writing systems contain characters that render almost identically to Latin letters. Cyrillic "а" (U+0430) is visually indistinguishable from Latin "a" at normal font sizes. Greek "ο" looks like "o". An attacker registers аpple.com using the Cyrillic letter, and to your eye it reads as the real Apple domain — but it encodes to a completely different host, something like xn--pple-43d.com, that points wherever the attacker wants.

Modern browsers fight back. Chrome and Firefox apply a display policy: when a label mixes scripts, or matches a known look-alike pattern, they refuse to show the pretty Unicode and display the raw xn-- form in the address bar instead. So if you ever see xn-- where you expected a normal brand name, that is usually the browser protecting you, not a glitch.

When you receive a suspicious link, decode the xn-- host and read what comes back. If xn--pple-43d.com decodes to аpple.com with a Cyrillic first letter, you have your proof. To nail down the exact code point of a sneaky character, drop the decoded text into the Unicode character inspector — it will tell you flatly that the "a" is U+0430, not U+0061, which is the kind of evidence you can put in an incident report.

Encoding correctly: the traps worth knowing

Three mistakes account for most of the broken IDN configurations I have seen.

The first is encoding the whole domain as one string. Punycode is per-label by definition. Feed münchen.de to a naive encoder as a single chunk and you get garbage, because the dot is a label boundary, not a character to compress. Always split on the dot first.

The second is hand-editing the ACE string. The xn-- prefix is case-insensitive — xn--MNCHEN-3YA and xn--mnchen-3ya decode to the same thing because DNS ignores case — but the payload after the prefix is a precise encoding. Flip one character in 3ya and you get a different domain or an invalid one. Never tweak the encoded form by hand; always round-trip through a real codec.

The third is skipping normalization before registration. Registrars apply UTS-46 and Nameprep first: they lowercase the input, apply Unicode NFC normalization, and reject disallowed characters, all before Punycode encoding. A faithful round-trip converter encodes exactly what you type, which is the right behavior for inspection but means MÜNCHEN.DE will not necessarily produce the same xn-- string the registry stored. Lowercase and NFC-normalize a name yourself before you encode anything you intend to register.

One more practical touch: a good IDN converter is email-aware. Paste user@münchen.de and only the part after the last @ is converted, giving user@xn--mnchen-3ya.de with the local part untouched — which is exactly what a legacy SMTP relay that predates SMTPUTF8 expects to receive.

When to reach for it

Punycode shows up more often than its obscurity suggests. You need the xn-- form when you register an internationalized domain and have to paste the ASCII version into a DNS A-record, a CDN origin, or the SAN list of a TLS certificate request. You need the decode direction when a suspicious link arrives and you want to see the real Unicode underneath. And you need a batch mode when you migrate a list of vanity domains into an nginx server_name block that only accepts ASCII.

In every one of those cases the safe move is a tool that runs entirely in your browser, because you are often handling either a host that is not yet registered or a domain you suspect is hostile — and neither should be quietly logged on someone else's server.


Made by Toolora · Updated 2026-06-13