The Problem I Hit When Migrating to an IPv6-Only Datacenter
About eight months ago, my VPS provider announced additional charges for public IPv4 addresses, with a clear roadmap toward IPv6-only infrastructure. I manage a cluster of around 15 servers — most of which only need internal communication — so I decided to try going IPv6-only to cut costs.
The problems hit immediately in the first week: a bunch of external services had no IPv6 support. The apt mirror repository for an older distro was IPv4-only. The payment gateway our client used? Also IPv4-only. Commercial software license server? IPv4. I stared at a wall of connection errors feeling like I’d jumped the gun.
Adding IPv4 to each machine meant linearly scaling costs. Reverting the whole cluster to dual-stack would defeat the original goal. After a week of research, I found the right solution: NAT64 combined with DNS64 — allowing IPv6-only servers to connect to IPv4-only services completely transparently, without changing a single line of application code.
Why Can’t IPv6-Only Servers Connect to IPv4 Natively?
IPv6 and IPv4 cannot communicate directly — they are fundamentally different in address structure and packet headers. When an IPv6-only server wants to connect to 203.0.113.5 (IPv4), it needs to solve two problems:
- Routing: A way to encode an IPv4 address into IPv6 form so packets know where to go — that’s what NAT64 handles
- DNS: When resolving a domain that only has an A record (IPv4), the DNS resolver must return a synthesized AAAA record — that’s what DNS64 does
Technically: DNS64 creates a synthetic AAAA record by combining the NAT64 prefix (typically 64:ff9b::/96 per RFC 6052) with the IPv4 address. When a client sends a packet to that synthesized IPv6 address, the NAT64 gateway receives it, strips out the original IPv4 address, and forwards it to the real destination using the gateway’s own IPv4 address. The entire process is transparent to the application.
Three Solutions and Why I Chose NAT64
Option 1: Dual-Stack — Simple but Doesn’t Scale
Assign a public IPv4 to each machine. No extra configuration needed, but costs scale linearly with the number of machines. Fine for 15–20 servers, but financially impractical when scaling to 50–100.
Option 2: Application-Level Proxy (Squid, SOCKS5)
Use a dual-stack machine as an HTTP proxy or SOCKS5 gateway. I tried this — it works for HTTP/HTTPS, but binary protocols, gRPC, and custom TCP connections each require per-application configuration. With dozens of different services, maintenance becomes a real headache.
Option 3: NAT64 + DNS64 — Transparent at the Network Layer
This solution operates entirely below the application layer. Just point the DNS on each IPv6-only machine to the DNS64 server, and all outbound TCP/UDP connections are handled automatically. This is what I’ve been running in production for the past six months.
Deploying Tayga (NAT64) and BIND9 (DNS64)
Architecture
I use a single Linux machine with both IPv4 and IPv6 as the gateway — it runs both Tayga and BIND9:
- Tayga: stateful NAT64 translator running in userspace
- BIND9: DNS64 — synthesizes AAAA records from A records
- NAT64 prefix:
64:ff9b::/96(well-known prefix) - IPv4 pool for Tayga:
192.168.255.0/24(private range)
Step 1: Install and Configure Tayga
On Ubuntu/Debian:
sudo apt update && sudo apt install tayga
Create the configuration file /etc/tayga.conf:
tun-device nat64
ipv4-addr 192.168.255.1
prefix 64:ff9b::/96
dynamic-pool 192.168.255.0/24
data-dir /var/db/tayga
Start Tayga and configure the interface:
sudo mkdir -p /var/db/tayga
sudo tayga --mktun
sudo ip link set nat64 up
sudo ip addr add 192.168.255.1/24 dev nat64
sudo ip -6 route add 64:ff9b::/96 dev nat64
sudo tayga
Step 2: Enable Forwarding and Configure iptables
# Enable IP forwarding
echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.conf
echo "net.ipv6.conf.all.forwarding = 1" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
# NAT IPv4 traffic from Tayga pool to the internet
sudo iptables -t nat -A POSTROUTING -s 192.168.255.0/24 -j MASQUERADE
# Allow forwarding through the nat64 interface
sudo ip6tables -A FORWARD -i nat64 -j ACCEPT
sudo ip6tables -A FORWARD -o nat64 -j ACCEPT
Step 3: Configure DNS64 on BIND9
sudo apt install bind9
Add the dns64 block to /etc/bind/named.conf.options:
options {
directory "/var/cache/bind";
# DNS64: synthesize AAAA from A record using NAT64 prefix
dns64 64:ff9b::/96 {
clients { any; };
# Do not synthesize for RFC1918 (private IPv4) — public addresses only
mapped { !rfc1918; any; };
exclude { 64:ff9b::/96; ::ffff:0:0/96; };
};
allow-query { localhost; 2001:db8::/48; };
recursion yes;
forwarders {
8.8.8.8;
1.1.1.1;
};
# DNS64 breaks DNSSEC validation — must disable
dnssec-validation no;
};
sudo systemctl restart bind9 && sudo systemctl enable bind9
Step 4: Systemd Service for Tayga
Tayga doesn’t create a systemd service on its own — you need to write one manually:
# /etc/systemd/system/tayga.service
[Unit]
Description=Tayga NAT64 daemon
After=network.target
[Service]
Type=forking
ExecStartPre=/usr/sbin/tayga --mktun
ExecStartPre=/bin/ip link set nat64 up
ExecStartPre=/bin/ip addr add 192.168.255.1/24 dev nat64
ExecStartPre=/bin/ip -6 route add 64:ff9b::/96 dev nat64
ExecStart=/usr/sbin/tayga --pidfile /run/tayga.pid
PIDFile=/run/tayga.pid
ExecStopPost=/usr/sbin/tayga --rmtun
Restart=on-failure
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now tayga
Step 5: Configure IPv6-Only Servers
On each IPv6-only server, point DNS to the gateway (assuming the gateway’s IPv6 address is 2001:db8::1):
# /etc/resolv.conf
nameserver 2001:db8::1
# Add route for NAT64 prefix via gateway
sudo ip -6 route add 64:ff9b::/96 via 2001:db8::1
Testing
# Test DNS64 — should return AAAA in 64:ff9b::x.x.x.x format
dig AAAA example.com @2001:db8::1
# Ping an IPv4 address directly via NAT64
ping6 64:ff9b::8.8.8.8
# Test actual connectivity from an IPv6-only machine
curl -6v https://example.com
# View Tayga's mapping table
sudo tayga --list
Pitfalls I Fell Into — Documented So You Don’t Have To
DNSSEC must be disabled on the DNS64 resolver: Because DNS64 synthesizes AAAA records that don’t exist on authoritative servers, DNSSEC validation will fail across the board. This is a trade-off worth thinking through carefully — I accepted it because the DNS64 server only serves internal traffic and isn’t exposed to the internet.
Tayga runs in userspace — not the kernel: Performance is lower than kernel-level solutions like SIIT. For typical HTTP API calls or database query workloads you won’t notice the difference, but if you need multi-Gbps throughput, it’s worth looking into alternatives.
Traceroute doesn’t work perfectly: NAT64 can’t handle ICMP embedded addresses, so traceroute through NAT64 will be missing hops. Network debugging is a bit harder — I typically debug from the gateway side rather than from the IPv6-only machine.
iptables rules don’t persist across reboots: Install iptables-persistent or add the rules to your systemd service:
sudo apt install iptables-persistent
sudo netfilter-persistent save
Results After 6 Months in Production
This setup has been running stably on our office server cluster. The cost reduction compared to buying public IPv4 for each machine is significant. More importantly — the dev team didn’t need to change anything; all TCP/UDP connections to IPv4-only services work normally, including apt updates, curl, database connections, and license servers.
NAT64 + DNS64 isn’t a permanent solution — as legacy services gradually migrate to IPv6, you can phase this setup out. But during the current transition period, it’s the most practical and effective way to move toward IPv6 without being held back by old infrastructure.
