Plan and execute IPv6 migration for modern infrastructure. Covers dual-stack architecture, IPv6 addressing, DNS considerations, cloud provider IPv6 support, security implications, and the patterns that make IPv6 adoption smooth and reversible.
IPv6 migration engineering is the discipline of planning, deploying, and operating dual-stack, transition, and native IPv6 networks across large-scale, heterogeneous infrastructures. It matters when applications, services, and users demand end-to-end IPv6 connectivity—especially in cloud-native environments, global content delivery, and IoT deployments—where the limitations of IPv4 (address exhaustion, complex NAT, lack of native mobility) become bottlenecks. This page details the practical steps, decisions, and pitfalls in engineering IPv6 migration at scale.
Define the migration scope using a dual-stack baseline: all services must support both IPv4 and IPv6 simultaneously. The starting point is a network inventory that lists every component capable of hosting or forwarding IPv6 traffic: routers, firewalls, load balancers, servers, containers, and databases.
Use ip -6 addr show to verify IPv6 addresses on all hosts. A common failure is missing ip6tables rules, which silently block traffic. Ensure the following are configured:
echo 1 > /proc/sys/net/ipv6/conf/all/forwarding
# Enable IPv6 autoconfiguration on all interfaces
sysctl -w net.ipv6.conf.all.autoconf=1
sysctl -w net.ipv6.conf.all.accept_ra=1
Set accept_ra=1 to allow routers to send Router Advertisements (RAs), which configure prefixes and default gateways. RAs are sent periodically (default every 200 seconds) and can be tuned via ra_interval and ra_mtu.
For infrastructure automation, use ansible to enforce IPv6 settings across hosts:
- name: Configure IPv6 on all servers
hosts: all
tasks:
- name: Enable IPv6 forwarding
lineinfile:
path: /etc/sysctl.conf
line: "net.ipv6.conf.all.forwarding = 1"
create: yes
state: present
- name: Set RA acceptance
lineinfile:
path: /etc/sysctl.conf
line: "net.ipv6.conf.all.accept_ra = 1"
create: yes
state: present
- name: Apply sysctl settings
command: sysctl -p
All services must be reachable over both IPv4 and IPv6. Use ss -46 to inspect listening sockets:
ss -46tuln
This shows all TCP listeners on IPv4 and IPv6. A common oversight: a service listens only on IPv4 (0.0.0.0:8080) but not on :::8080, so IPv6 clients receive a Connection refused error.
For web services, ensure the server is configured to bind to :: (all IPv6 addresses) and 0.0.0.0 (all IPv4 addresses). In nginx, use:
server {
listen 80;
listen 80 ipv6only=on;
listen 443 ssl;
listen 443 ssl ipv6only=on;
server_name example.com;
# ...
}
The ipv6only=on directive disables IPv4-mapped IPv6 addresses, which can cause performance degradation and unnecessary address space usage.
For containerized services, use docker-compose.yml with explicit IPv6 settings:
version: '3.8'
services:
app:
image: myapp:v1
networks:
- web
- backend
networks:
web:
driver: bridge
ipam:
config:
- subnet: 192.168.100.0/24
- subnet: 2001:db8:100::/64
gateway: 2001:db8:100::1
options:
ipv6: "true"
The ipv6: "true" option enables IPv6 on the bridge network, and gateway sets the default IPv6 router. Use docker network inspect web to verify the configuration.
IPv6 requires AAAA records in DNS. Services must have both A and AAAA records. Use dig to check:
dig AAAA example.com
A common failure: AAAA records exist but are not served with edns-client-subnet (ECS) support, causing misrouting. Ensure DNS servers support ECS and are configured to serve records based on client subnet.
For global applications, use dnsmasq or bind with response-policy zones. In bind (named.conf):
zone "example.com" {
type master;
file "example.com.db";
response-policy {
policy "example.com";
};
};
response-policy {
zone "example.com" {
zone "example.com";
rdata "192.168.0.0/16" {
set-rrset "AAAA" "2001:db8:100::1";
};
rdata "2001:db8::/32" {
set-rrset "AAAA" "2001:db8:200::1";
};
};
This routes clients from specific IPv4 and IPv6 subnets to different AAAA records.
Use dig +subnet=2001:db8:100::1 to test ECS behavior. A silent failure: a client receives A records but no AAAA records, even though they exist. This is often due to a missing AAAA record or a DNS resolver that does not advertise support for AAAA via EDNS0.
IPv6-only clients must communicate with IPv4-only services. Use stateless translation (NAT64) and DNS64.
NAT64 maps IPv6 addresses to IPv4 addresses using a prefix, typically 64:ff9b::/96. Configure a NAT64 gateway with tproxy or miredo:
# Configure NAT64 with tproxy (Linux)
ip -6 route add 64:ff9b::/96 dev eth0
ip -6 rule add from 64:ff9b::/96 lookup 100
ip -6 nexthop add id 100 nhop 192.0.2.1 dev eth0
ip -6 route add 0.0.0.0/0 dev eth0 nexthop 100
Ensure DNS64 is enabled: DNS servers rewrite AAAA queries to include synthetic AAAA records. For example, a client querying example.com receives a AAAA record for 64:ff9b::192.0.2.1 if example.com has no AAAA record.
Use dnsmasq for DNS64:
# dnsmasq.conf
dns64 64:ff9b::/96
dns64-synthetic yes
dns64-merge yes
With dns64-merge yes, DNS64 merges synthetic AAAA records with real ones. A silent failure: dns64-merge is off, so only synthetic records are returned, and clients cannot fall back to IPv4.
Deploy a continuous validation system: a set of probes that ping, query, and connect to services over both protocols.
Use ping6 and curl with explicit IPv6 support:
# Ping IPv6-only service
ping6 -c 5 2001:db8:100::1
# Test HTTP over IPv6
curl -v -6 -H "Host: example.com" https://[2001:db8:100::1]
# Test DNS64 resolution
dig AAAA example.com +short
dig AAAA example.com +subnet=2001:db8:100::1
Use tcpdump to capture traffic and verify RA messages, DNS queries, and TCP handshakes:
# Capture RA messages
tcpdump -i eth0 -n -v -s 0 -S -c 10 'icmp6 and type 134'
A common failure: a client receives a Router Advertisement but does not update its default route. This is due to incorrect M (Managed) and O (Other Configuration) flags in RA. Use ip -6 route show to verify the default route and ip -6 route get 2001:db8::1 to trace path to a specific IPv6 address.
A server boots but has no IPv6 addresses. Check ip -6 addr show and ip -6 route show. The root cause is often missing accept_ra=1 or autoconf=1 in sysctl.conf.
The ipv6 module may not be loaded. Use lsmod | grep ipv6 and modprobe ipv6.
A host has IPv6 addresses but cannot reach the internet. Use ip -6 route get 2001:4860:4860::8888 (Google’s IPv6 DNS). If no route is found, the default route is missing.
Check ip -6 route show default and ip -6 route show table main. The default route might be in a different table, or the RA’s default router lifetime is zero.
A client on IPv6-only network cannot reach an IPv4-only service. Use dig AAAA example.com and dig AAAA example.com +subnet=2001:db8::1. If synthetic AAAA records are missing, verify the NAT64 prefix and DNS64 configuration.
Check dig TXT 64:ff9b::1 for the NAT64 prefix. Ensure dns64 is enabled and dns64-synthetic yes is set.
A containerized service responds on IPv4 but fails on IPv6. Use ss -46tuln and docker inspect <container>.
The issue is often a missing ipv6only=on in nginx, or the service not binding to :: in its code. In Go, use:
ln, err := net.Listen("tcp6", "[::]:8080")
A common mistake: using net.Listen("tcp", ":8080"), which binds only to 0.0.0.0:8080, not :::8080.
Clients do not receive RAs. Use tcpdump -i eth0 -n -v 'icmp6 and type 134' to capture RAs.
Check that radvd is running and configured:
# radvd.conf
interface eth0 {
AdvSendAdvert on;
prefix 2001:db8:100::/64 {
AdvOnLink on;
AdvAutonomous on;
AdvRouterAddr off;
};
RDNSS 2001:4860:4860::8888 {
AdvRDNSSLifetime 3600;
};
};
Use systemctl status radvd and journalctl -u radvd to verify the process is active and logging.
Run a weekly IPv6 health check:
#!/bin/bash
# /opt/scripts/ipv6-health-check.sh
set -euo pipefail
echo "=== IPv6 Health Check - $(date) ==="
# Check IPv6 connectivity
ping6 -c 5 2001:4860:4860::8888 || echo "IPv6 connectivity failed"
# Check DNS64 resolution
dig AAAA example.com +short | grep -q '64:ff9b::' || echo "DNS64 not synthesizing AAAA records"
# Check dual-stack reachability
curl -v -6 -H "Host: example.com" https://[2001:db8:100::1] -sSf -o /dev/null || echo "IPv6 HTTP failed"
# Check RA reception
tcpdump -i eth0 -n -v -s 0 -c 10 'icmp6 and type 134' | grep -q 'Router Advertisement' || echo "No Router Advertisements received"
# Check NAT64 translation
curl -v -6 -H "Host: example.com" https://[64:ff9b::192.0.2.1] -sSf -o /dev/null || echo "NAT64 translation failed"
echo "=== End of IPv6 Health Check ==="
Schedule this with cron:
# Run weekly
0 0 * * 0 /opt/scripts/ipv6-health-check.sh
This script captures the most common IPv6 migration failures and provides a simple, repeatable validation process.
IPv6 migration engineering is not a one-time deployment but an ongoing operational discipline. The key is to ensure that every layer—network, host, service, DNS, and application—understands and supports IPv6. The most common failures are silent: clients receive RA messages but don’t use them, services bind to IPv4 only, DNS64 synthesizes AAAA records but they’re not used. By using standard tools and configurations, and by testing end-to-end connectivity, engineering teams can build resilient, production-ready IPv6 deployments.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy