13.3 Dual Stack, Happy Eyeballs, and IPv6-Only Transition
A host having an IPv6 address doesn't mean the application will definitely use IPv6; even if a domain has both A and AAAA records, it doesn't mean clients will mechanically try IPv6 first, then fall back to IPv4 after three seconds. Actual connection establishment also involves the system resolver, address prioritization, concurrent attempts, proxies, and network quality.
DNS Returns Records; the Client Chooses a Path
- A record saves an IPv4 address;
- AAAA record saves IPv6 address;
- IPv6 nibble usage in reverse PTR records uses
ip6.arpa; - Ordinary authoritative DNS servers typically respond based on query type and won't automatically return only AAAA records just because the client "supports IPv6."
dig example.com A
dig example.com AAAA
getent ahosts example.comdig shows DNS protocol answers; getent more closely follows ordinary applications using system name-service configuration. Differences can come from /etc/hosts, NSS, caches, split DNS, search domains, or local resolvers. DNS over IPv4 can return AAAA, and DNS over IPv6 can return A; the query’s transport family does not determine its record type.
Use getaddrinfo() in Python to obtain socket-ready addresses:
import socket
def stream_candidates(host: str, port: int):
seen = set()
for family, socktype, proto, _, sockaddr in socket.getaddrinfo(
host,
port,
family=socket.AF_UNSPEC,
type=socket.SOCK_STREAM,
proto=socket.IPPROTO_TCP,
):
key = (family, sockaddr)
if key not in seen:
seen.add(key)
yield family, socktype, proto, sockaddrThe order of returned addresses is influenced by RFC 6724 address selection rules, platform policies, and network configuration. Applications must still handle multiple candidates. This function only enumerates them; getaddrinfo() can block before returning. It does not implement connection racing or asynchronous DNS scheduling.
Happy Eyeballs isn't serial fallback
The simplest client tries addresses in sequence. If the first IPv6 path is a black hole, a few seconds of connect timeout will make users mistakenly think the entire service is slow.
The core of Happy Eyeballs v2 is: sorting DNS results and initiating connection attempts to different address families at short intervals; the first successful connection wins, and the rest are canceled. It also includes details on DNS queries, connection attempt order, and historical preferences.
t=0 ms start first preferred-family attempt
t=250 ms if no winner, start next suitable attempt
... continue paced attempts
winner use connection; cancel/close othersThe timeline begins when the first candidate is available, excluding earlier DNS time. 250 ms is a suggested default in the RFC and should not be hardcoded as the optimal value for all networks. Excessive concurrency also wastes socket, port, and server resources.
Python’s socket.create_connection() tries multiple addresses; do not assume it implements RFC 8305 connection racing. Per-address connect timeouts also need not equal the total call budget. Production clients should prefer mature HTTP/network libraries that explicitly support Happy Eyeballs and test scenarios such as IPv6 black holes, slow AAAA records, and single-stack environments.
Verify IPv4 and IPv6 separately
curl -4 --verbose --connect-timeout 3 https://example.com/
curl -6 --verbose --connect-timeout 3 https://example.com/
ip -6 route get 2001:db8::10
ping -6 -c 3 2001:db8::10
traceroute -6 -n 2001:db8::102001:db8::10 is a documentation placeholder: replace it with a test target. A command cannot make it reachable by itself. If a proxy is configured, curl -4/-6 may control only the connection to that proxy, so establish the actual path first. curl -4/-6 is a diagnostic check and does not imply that a particular address family should be permanently enforced in production applications. If IPv6 fails while IPv4 succeeds, continue verifying AAAA records, source address selection, default routes, NDP, ICMPv6, MTU, firewalls, and return path routing.
How does the server listen on a dual stack?
Whether IPv6 wildcard :: accepts IPv4-mapped connections depends on the platform and IPV6_V6ONLY. Do not assume the default behavior on a Linux host is portable.
Python can request a dual-stack socket on supported platforms. This example accepts one TCP connection, sends one line, and exits; it is not an HTTP server. Run it in a test environment with port 8080 available:
import socket
if not socket.has_dualstack_ipv6():
raise RuntimeError("this platform does not expose a dual-stack IPv6 socket")
with socket.create_server(
("::", 8080),
family=socket.AF_INET6,
dualstack_ipv6=True,
) as server:
server.settimeout(10)
connection, peer = server.accept()
with connection:
connection.settimeout(3)
print(peer)
connection.sendall(b"connected\n")IPv4 clients on this IPv6 socket normally appear as ::ffff:IPv4 mapped addresses. Normalize parsed addresses consistently in logs and address access rules so one IPv4 peer is not treated as two identities. Real services also need request parsing, concurrency, deadlines, limits, logging, and graceful shutdown. A more explicit approach is to separately create IPv4 and IPv6 listeners, managed by the service framework.
Three Types of Transition Deployments
Dual Stack
Hosts and networks run both IPv4 and IPv6 simultaneously, with applications using address selection and Happy Eyeballs to choose available paths. The advantage is native support for both protocols; the cost is the need to operate two separate routing systems, security policies, monitoring, and capacity.
Dual stacking itself doesn't require NAT64. NAT64 only comes into play when an IPv6-only client needs to access an IPv4-only service.
IPv6-in-IPv4 Tunnel
Tunnels encapsulate IPv6 packets within IPv4 to traverse IPv4-only regions. They are still used in controlled networks and specific products, but RFC 7526 deprecates the anycast 6to4 relay mechanism and related configurations and requires 6to4 to be disabled by default. It explicitly does not deprecate basic unicast 6to4 or 2002::/16. Historical automatic tunnels should not be the default substitute for native IPv6.
Tunnels introduce additional headers, MTU, observability, and security boundaries. The tunnel endpoints, effective MTU, routing, and filtering policies must be clearly defined.
IPv6-only + DNS64/NAT64
When the client has only IPv6 and the destination has only IPv4:
- Normally when no usable native AAAA answer exists, DNS64 synthesizes a NAT64-prefixed AAAA from A records;
- The client sends IPv6 to the synthetic address;
- The NAT64 translator extracts the IPv4 destination and maintains the necessary translation state;
- IPv4 services see the IPv4 traffic on the translator side.
IPv6-only client
-> synthesized AAAA under Pref64
-> NAT64 translator
-> IPv4-only serverThe IETF well-known prefix is 64:ff9b::/96, and operators may also use a network-specific prefix. RFC 6052 defines other allowed prefix lengths, and it is not permissible to simply append a 32-bit IPv4 string arbitrarily.
This demonstrates only the bits of /96 embedding, not routability or production suitability. RFC 6052 does not permit the well-known prefix for non-global IPv4 addresses such as RFC 1918 ranges; private-address translation needs an appropriate prefix and policy:
import ipaddress
def embed_ipv4_in_96(prefix: str, ipv4: str) -> ipaddress.IPv6Address:
network = ipaddress.IPv6Network(prefix)
if network.prefixlen != 96:
raise ValueError("this example only implements RFC 6052 /96 embedding")
value = int(network.network_address) | int(ipaddress.IPv4Address(ipv4))
return ipaddress.IPv6Address(value)
assert str(embed_ipv4_in_96("64:ff9b::/96", "192.0.2.33")) == \
"64:ff9b::c000:221"NAT64 is not transparent to all applications: IPv4 literals, embedded addresses in the payload, certain non-TCP/UDP protocols, address-family-specific APIs, and DNSSEC validation boundaries all may require additional handling. 464XLAT adds a client-side stateless CLAT to translate IPv4 from an IPv4-only application into IPv6; the provider-side PLAT performs stateful NAT64. This supports some IPv4-literal and IPv4-only API cases without assuming the application first obtains a DNS64 answer.
The Trust Boundaries of DNSSEC and DNS64
A synthesized AAAA lacks an original authoritative-zone signature for that record. A trusted validating resolver can validate the original DNS data before synthesis, with the client relying on that boundary. A client that validates independently needs the original data and a compatible local synthesis process after validation. Do not present a synthesized AAAA as an authoritative signed record or disable validation as a repair. Ordinary DNS64 also does not automatically fall back to A merely because an existing native AAAA leads to a broken IPv6 path.
IPv6 Doesn't Need NAT, But That Doesn't Mean It Doesn't Need Firewalls
A global unicast address is merely a routable address type. Whether it is actually reachable from the public internet is determined jointly by routing, stateful firewall, ACL, host-bound address listening, cloud security groups, and application authentication.
Security baselines should cover at least:
- Inbound/outbound policies equivalent to IPv4 and IPv6;
- Necessary ICMPv6, rather than completely blocking it;
- link-local, ULA, global, etc., different scopes;
- rogue RA/NDP risk;
- Impact of temporary addresses on logging, assets, and access control;
- Whether IPv6 tunnels and unmanaged addresses bypass existing monitoring.
The vast /64 makes sequential enumeration inefficient, but attackers can discover active addresses through DNS, certificate transparency logs, service scans, fixed IID, and application leaks. A large address space is not equivalent to access control.
Validation Matrix Before Release
| Scenario | Mandatory Tests |
|---|---|
| IPv4-only client | A record, IPv4 listener, TLS/HTTP, return path |
| IPv6-only client | AAAA or DNS64, IPv6 route, NDP, PMTUD, TLS/HTTP |
| dual-stack client | address sorting, Happy Eyeballs, fallback delay during single-stack failure |
| IPv6-only to IPv4-only | Pref64, DNS64 synthesis, NAT64 sessions, literals, and DNSSEC boundaries |
| load balancer/CDN | A/AAAA backend consistency, health check protocols, true client addresses in logs |
Fault drills must at least include: failed AAAA records, IPv6 silent packet loss, misblocked ICMPv6 Packet Too Big messages, IPv4 functioning while IPv6 backend services are not published, and exhausted NAT64 capacity.
Lesson Summary
The challenge of dual stacking isn't having an additional address type; it's two potentially independent paths that could both fail. Happy Eyeballs reduces user wait time by racing paths in a controlled manner to mitigate single-path failures; IPv6-only networks can access IPv4-only services via DNS64/NAT64, but the translation boundary must be clearly tested and monitored.