18. Dual-stack, Happy Eyeballs, and transition mechanisms
18.1 Dual-stack
A host runs IPv4 and IPv6 simultaneously; each connection uses one or the other.
getaddrinfo returns both A and AAAA; the resolver/OS orders them (RFC 6724 —
generally "IPv6 first, but prefer the family with a matching source address
scope").
18.2 Happy Eyeballs (RFC 8305)
Naively "try AAAA, on failure try A" means a broken-IPv6 user eats a full TCP timeout (tens of seconds) on every connection. Happy Eyeballs v2:
- Start the AAAA and A DNS lookups in parallel; don't wait for both.
- Start a TCP connect to the first address (prefer IPv6) as soon as it's known.
- If it hasn't connected within a short delay (~250 ms), start a parallel connect to an address of the other family.
- First connection to complete wins; cancel the rest.
- Cache the winner's family per destination for a while.
Result: broken IPv6 costs ~250 ms once, not 30 s every time. Browsers, curl,
Go's dialer, and modern libc all do this. If you write raw socket code, do it
too.
18.3 Transition mechanisms (know the names)
| Mechanism | What it does | Where you meet it |
|---|---|---|
| Dual-stack | run both | the "correct" answer, when feasible |
| NAT64 + DNS64 | IPv6-only client → IPv4 server. DNS64 synthesizes a AAAA in 64:ff9b::/96 from the A record; NAT64 gateway translates 64:ff9b::x.y.z.w ↔ IPv4 |
mobile carriers (T-Mobile US), IPv6-only cloud subnets, iOS app-store requirement |
| 464XLAT | adds a client-side CLAT so IPv4-only apps work over an IPv6-only + NAT64 network | Android on IPv6-only cellular |
| DS-Lite | IPv4 client → IPv4 internet over an IPv6-only access network, with CGNAT at the ISP (AFTR) | some ISPs |
| MAP-E / MAP-T | stateless IPv4-over-IPv6 with per-CPE port ranges | some ISPs |
| 6in4 / 6to4 / Teredo / ISATAP | tunnel IPv6 over IPv4 | mostly legacy; 6in4 tunnels (HE.net) still used by hobbyists |
| NPTv6 | 1:1 stateless prefix translation (not NAT/PAT) | multihoming without PI space |
18.4 Operational gotchas
- NAT64/DNS64 breaks IPv4 literals and DNSSEC validation on the client —
apps that hardcode
1.2.3.4won't work without CLAT. localhostmay resolve to::1first — a service bound only to127.0.0.1will refuse IPv6 loopback connections. Bind to both or to::(withbindv6only=0).- Firewall rules must be duplicated for
ip6tables/nftables inet. A common breach: v4 locked down, v6 wide open. - Logging/allowlists must handle 128-bit addresses and the fact that one host rotates through many privacy addresses.
18.5 War story
An API allowlist keyed on client IP started rejecting a partner intermittently.
The partner was on an IPv6-first network; some requests came from a stable GUA
(allowlisted) and some from rotating temporary privacy addresses (not).
Solutions considered: allowlist the partner's whole /64, have them use a
stable-address egress proxy, or switch to mutual TLS auth. Lesson: IPv6
source addresses are many-per-host and time-varying by design; don't build
security on "the client IP."
Try it yourself
getent ahosts example.com # see A and AAAA and the resolver's ordering
curl -v --connect-timeout 5 https://example.com # watch which family wins
curl -4 ... / curl -6 ... # force a family
python3 -c "import socket; print(socket.getaddrinfo('example.com', 443))"