Frontend Labs
⋯

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:

  1. Start the AAAA and A DNS lookups in parallel; don't wait for both.
  2. Start a TCP connect to the first address (prefer IPv6) as soon as it's known.
  3. If it hasn't connected within a short delay (~250 ms), start a parallel connect to an address of the other family.
  4. First connection to complete wins; cancel the rest.
  5. 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.4 won't work without CLAT.
  • localhost may resolve to ::1 first — a service bound only to 127.0.0.1 will refuse IPv6 loopback connections. Bind to both or to :: (with bindv6only=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))"

Generated from real-life-example.md · 8 Oct 2026 16:36