Pakkit.net
← Back to blog

Infrastructure

Going IPv6-Only Is a DNS Problem First

When you stand up an IPv6-only host, the first thing to break usually isn't your application — it's name resolution, because the resolver and the services behind the names have to be reachable over v6 too.

  • Infrastructure
  • Networking
  • IPv6
  • DNS
Illustration of an IPv6-only host resolving a name through an AAAA-capable resolver to a reachable service, while an IPv4-only dependency sits broken on a dashed dead end.

I went to stand up a batch of IPv6-only hosts — no IPv4 at all, on purpose, to stop burning a scarce v4 range — and the first thing that fell over wasn’t any application. It was DNS. Not because IPv6 name resolution is hard, but because every assumption baked into the existing setup quietly depended on IPv4 still being there.

If you’re about to go v6-only, budget for the name-resolution work first. The app is the easy part.

A resolver you can’t reach is not a resolver

The host’s /etc/resolv.conf pointed at the usual internal resolvers. Those resolvers only listen on IPv4. A v6-only box has no route to a v4 address, so every lookup just… didn’t. No error that says “your resolver is unreachable” — queries time out and you get a pile of confusing downstream failures that look like everything except DNS.

The fix is obvious once you’ve named the problem: point the box at a resolver it can actually reach over IPv6. A public IPv6 resolver (Cloudflare’s 2606:4700:4700::1111, for instance) gets you resolving again in seconds. But that swap has a tail, which is the rest of this post.

Public names resolve; your internal names might not

Point a v6-only host at a public IPv6 resolver and public services with AAAA records just work — they resolve, and because they’re reachable over v6, they connect. Great for pulling packages and code from anything with a real public presence.

Internal-only names are the catch. A public resolver has never heard of your private hostnames — they’re not in public DNS, by design. So the moment a v6-only box needs to reach an internal service by name, it’s stuck. Two ways out:

  • A static /etc/hosts entry. Look the address up from a dual-stack host first (getent ahostsv6 the-internal-name), then pin it. Cheap, but it’s a manual mapping you now own and have to keep honest.
  • An internal resolver that actually answers over IPv6. The real fix if v6-only is going to be a normal thing rather than a one-off, but it’s infrastructure someone has to stand up and run.

It’s transport, not just resolution

This is the part that cost me the most time, so I’ll say it plainly: getting the name to resolve does not mean you can reach the thing. Resolution and transport are two separate problems, and v6-only fails them independently.

A static /etc/hosts entry or a fresh AAAA record makes the name work. The connection still requires that the service actually has an IPv6 address and is listening on it. If there’s no NAT64 sitting between your v6-only host and the v4 world — and in a lot of internal networks there isn’t — then an IPv4-only service is simply unreachable, no matter how perfectly the name resolves.

A name that resolves to an address you can’t route to is a more convincing failure than no name at all. It looks solved right up until the connection hangs.

So before you depend on any host as, say, a package source or a config server from a v6-only box, prove it end to end from somewhere dual-stack: does it have an AAAA record, and can you actually open a TCP connection to it over v6? If either answer is no, the name working is a trap.

Dual-stack is the boring default that just works

Here’s the unglamorous conclusion. If you don’t have a hard reason to be v6-only, give the box an IPv4 address too. Dual-stack means your existing internal DNS keeps working, internal services stay reachable, and you skip every problem above. It’s the choice that doesn’t generate a war story, which is exactly why it’s usually right.

Go v6-only deliberately — when v4 space is genuinely scarce and the savings are real — and when you do, treat name resolution as a first-class part of the build, not an afterthought you’ll notice when something times out.

The order I check things now

When a v6-only host can’t reach something, I stopped guessing and started walking the path in order:

  1. Can the box reach its resolver over IPv6 at all? (A v4 resolver in resolv.conf is the classic silent failure.)
  2. Does the name even have an AAAA record? Public names usually do; internal ones often don’t.
  3. If it resolves, can I open a v6 TCP connection to the actual service — not just ping the name?
  4. If not, is there a NAT64 in the path, or is this an IPv4-only service I simply can’t reach from here?

It’s the same instinct as the old “it’s always DNS” joke, with an IPv6 asterisk: it’s always DNS and then it’s transport. If you’re working through your own v6-only migration and want to compare notes, my inbox is open — and the homelab messy-parts notes cover why DNS earns its reputation in the first place.