Contents

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

flowchart LR CLIENT[IPv6-only Client] PI[Raspberry Pi Router<br/>Jool CLAT] WG[WireGuard Tunnel] VPS[VPS] NAT64[Provider NAT64] IPV4[IPv4 Internet] CLIENT --> PI PI --> WG WG --> VPS VPS --> NAT64 NAT64 --> IPV4

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.

sequenceDiagram participant Host participant CLAT participant WG participant VPS participant NAT64 participant IPv4 Host->>CLAT: Ping 1.1.1.1 CLAT->>WG: 64:ff9b::101:101 WG->>VPS: Encapsulated IPv6 VPS->>NAT64: 64:ff9b::101:101 NAT64->>IPv4: 1.1.1.1 IPv4-->>NAT64: Reply NAT64-->>VPS: IPv6 VPS-->>WG: Forward WG-->>CLAT: Return traffic CLAT-->>Host: Native IPv4 reply

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:

flowchart TB A[Router CLAT] B[VPS wg0] C[Route Lookup] D[64:ff9b::/96 via wg0] A --> B B --> C C --> D D --> B

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

flowchart LR Client["192.168.26.x"] CLAT["Jool CLAT"] WG["WireGuard"] VPS["VPS"] NAT64["Provider NAT64"] Internet["IPv4 Internet"] Client --> CLAT CLAT --> WG WG --> VPS VPS --> NAT64 NAT64 --> Internet

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.