Dealing with IPv4 literals via an IPv6 only WAN
Building a 464XLAT home network to help getting to IPv4 literals
I’ve spent more than a few years over the years in various web-based rabbit holes online researching examples of how to try and remove any remaining barriers to turning off IPv4 on my (or any other) WAN. There’s plenty of resources out there but none that I found which completely explain the FULL end-to-end deployment. This blog post will set out to document that a little better for (mainly) future me to use as a reference but obviously for anyone that needs it to reference.
I started to get very motivated to use an IPv6 only (mostly) network where possible, but specifically on my homelan, since the RIPE87 tutorial that Ondřej Caletka delivered. My own “IPv6-first” home network where:
- All client devices receive only IPv6 connectivity
- DNS queries are resolved using DNS64
- IPv4-only websites continue to work
- IPv4 literals (for example ping 1.1.1.1 or ntp connections to an IPv4 address) also work
- As my current WAN connection is a dual stacked one, I created an IPv6 ONLY WAN via an IP Sec site-to-site connection emulated using WireGuard
- A NAT64 running on the other end of the WireGuard tunnel
The proof of concept solution combines:
- Raspberry Pi 4B router running as a headless-Ubuntu Server
- WireGuard (client/peer on the raspberry pi)
- DNS64 service operating on the LAN of the WireGuard Server side (this server is a VM in my private cloud)
- NAT64 service also operating via WireGuard
- Jool SIIT (CLAT) service specifically to deal with IPv4 literals
Final Topology
Addressing
LAN IPv6 Prefix: 2001:db8:26::/64
Router LAN: 2001:db8:26::1
WireGuard Interface on the PI Router: fd80:10:26::2
WireGuard Interface on the VPS Server: fd80:10:26::1
CLAT Internal IPv4 192.0.0.1
NAT64 Prefix 64:ff9b::/96
Architecture
The implementation follows RFC 6877 464XLAT.
DNS64
Clients use DNS64 provided by Unbound which is running directly on the server/VPS.
A query:
AAAA www.example.com
returns:
64:ff9b::5db8:d822
instead of an A record.
Native IPv6 destinations are returned untouched.
Why DNS64 Alone Was Not Enough
DNS64 solved:
curl https://example.com
because the hostname was synthesized to IPv6.
However:
ping 1.1.1.1
still failed.
Why?
Because no DNS lookup occurs.
The client only knows:
1.1.1.1
and has no way to convert it into:
64:ff9b::101:101
This is where CLAT is required.
Jool CLAT Design
Jool performs stateless SIIT translation.
Example:
192.168.26.153
becomes
64:ff9b::c0a8:1a99
and
1.1.1.1
becomes
64:ff9b::101:101
Observed on the router:
64:ff9b::c0a8:1a99 -> 64:ff9b::101:101
which confirmed CLAT translation was functioning.
WireGuard Design
The home network uses a full tunnel.
AllowedIPs = 0.0.0.0/0, ::/0
This introduced an unexpected boot problem.
After reboot:
No WireGuard handshake No Internet IPv6
The tunnel could not establish because native IPv6 was not fully available when WireGuard started.
Solution:
[Unit] After=network-online.target Wants=network-online.target
plus a startup validation of WAN IPv6 reachability before starting the tunnel.
A Difficult Bug: The Missing Jool Kernel Module
After a reboot:
Error: Jool’s socket family doesn’t seem to exist
The cause:
Kernel upgraded ↓ DKMS module built only for old kernel ↓ Jool vanished
Verification:
dkms status
showed:
jool/4.1.15, 7.0.0-1017-raspi
while the router was now running:
7.0.0-1019-raspi
Rebuilding DKMS for the new kernel restored the CLAT functionality.
The Most Interesting Problem
The translated packets appeared on the router:
64:ff9b::c0a8:1a99 -> 64:ff9b::101:101
but never arrived on the VPS.
The cause turned out to be WireGuard AllowedIPs filtering.
Initially:
AllowedIPs = fd80:10:26::2/128
The VPS rejected traffic sourced from:
64:ff9b::c0a8:1a99
because that source did not belong to the peer.
A first attempt added:
AllowedIPs = 64:ff9b::/96
which fixed one issue but created another.
Routing Loop Discovery
Adding:
AllowedIPs = 64:ff9b::/96
caused WireGuard to install:
64:ff9b::/96 dev wg0
on the VPS.
This produced a loop:
Packets never left the VPS.
Final Fix
The solution was to advertise only the translated source range.
For LAN:
192.168.26.0/24
the synthesized prefix is:
64:ff9b::c0a8:1a00/120
The VPS peer was updated to:
AllowedIPs = fd80:10:26::2/128, 64:ff9b::c0a8:1a00/120
instead of:
AllowedIPs = 64:ff9b::/96
This allowed:
64:ff9b::c0a8:1a99
to traverse the tunnel while leaving:
64:ff9b::101:101
routable via the provider NAT64 service.
Final Working Topology
Validation
Successful tests:
ping 1.1.1.1
ping 8.8.8.8
curl http://1.1.1.1
traceroute 1.1.1.1
curl -4 ifconfig.me
All now pass through:
Client → CLAT → WireGuard → VPS → NAT64 → IPv4 Internet
Lessons Learned DNS64 and NAT64 are not enough for IPv4 literals. Jool SIIT works extremely well as a CLAT. WireGuard AllowedIPs influences both reachability and routing. Never advertise the entire NAT64 prefix to the peer. Verify DKMS modules after every kernel upgrade. Packet captures at each hop were more useful than logs. ip route get was the single most useful troubleshooting command during the entire build.
That would make a solid technical blog post and accurately tell the story of the debugging journey from “DNS64 works” all the way through to a fully functional 464XLAT deployment.