๐ฐ๏ธ Running an IPv6-mostly network behind an ISP without IPv6
TL;DR I once built a joke website asking whether Odido had IPv6 yet. Their lawyers sent me a letter. Now I’m switching to Odido. They still don’t have IPv6. So I’m bringing my own.
The setup, and the irony
In 2024 I registered heeftodidoalipv6.nl โ ‘does Odido have IPv6 yet?’ โ which answered, in big friendly letters, no. I got a letter from a law firm in Amsterdam for my trouble.
Two years later, the answer is still no, and I’m moving house onto an Odido connection anyway. Which leaves me with a problem I find genuinely funny: my ISP is about to take my IPv6 away.
Except it isn’t, because my IPv6 does not come from my ISP. It comes from my own ASN, AS202585, and it reaches my house through a tunnel. The ISP underneath is just a dumb pipe that moves IPv4 packets. That’s the whole trick, and it’s the thing I want to write down properly.
What ‘IPv6-mostly’ actually means
There are three things people mix up, so let’s be precise.
IPv6-only means the clients have no IPv4 address at all. Not a private one, not a CGNAT one, nothing. ifconfig shows a v6 address and that’s it.
NAT64/DNS64 is what makes that survivable. Your resolver notices a name has no AAAA record, and synthesises one inside a special prefix โ the well-known one is 64:ff9b::/96. So ipv4.tlund.se, which is IPv4-only, comes back as 64:ff9b::c1b2:3e2e. The client connects to that over IPv6, the NAT64 gateway strips the prefix back off, and talks IPv4 to the far end on the client’s behalf. The client never knows.
IPv6-mostly (RFC 8925) is the polite version: you still run DHCPv4, but you hand out option 108, ‘IPv6-only preferred’. Clients that understand it turn their IPv4 stack off for a while. Clients that don’t, get a normal v4 lease and carry on. It’s the migration-friendly option.
My VLAN 64 is the strict version โ no DHCPv4 at all. I sniffed the wire for six seconds and caught exactly zero IPv4 packets, which is the point.
The other half is telling clients where the NAT64 prefix is, which is RFC 8781: you advertise PREF64 in your router advertisements. In radvd that’s two lines:
interface vlan06 {
AdvSendAdvert on;
AdvLinkMTU 1468;
prefix 2a12:bec0:5e1:64::/64 {
AdvOnLink on;
AdvAutonomous on;
};
nat64prefix 64:ff9b::/96 {
};
RDNSS 2a12:bec0:5e1:64::1 {
};
};
Modern macOS, iOS and Android pick up nat64prefix and will do 464XLAT or happy-eyeballs-ish things with it. It also means literals and legacy sockets have a fighting chance.
It’s so beautiful ๐ฅฐ
The shape of it
vlan64 clients (IPv6 only)
โ
โ RA: /64 + PREF64 64:ff9b::/96 + MTU 1468
โผ
OPNsense โโโโ tayga (NAT64) โโโโ 192.168.255.0/24 โโโ
โ โ
โ GRE tunnel over IPv4 โ NAT to
โผ โ 185.244.24.145
my router at Nikhef (AS202585) โโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
the internet
The pieces:
- OPNsense at home, terminating the ISP connection.
- A GRE tunnel from the OPNsense box to my router in Nikhef. Carried over IPv4, which matters enormously โ see below.
- BGP inside the tunnel, so the home router announces its prefixes and the core router doesn’t need static routes.
- tayga for NAT64, translating
64:ff9b::/96into a private v4 pool. - unbound with the
dns64module for the DNS half.
The things that bit me
This is the useful part. The architecture above is easy. These are not.
Your NAT64 exit address should be yours
The obvious way to do NAT64 is to translate into a private pool and then NAT that out of your WAN address, like any other IPv4 traffic. It works, and it’s what I did at first.
It’s also a waste of an ASN. Every IPv4-only service my v6-only clients touch saw my consumer ISP address โ with its consumer geolocation, its consumer reputation, and no PTR record I control. And when I change ISP, it changes.
So now the NAT64 pool translates to an address out of my own space, routed to the house through the tunnel:
nat on gre0 inet from 192.168.255.0/24 to any -> 185.244.24.145
It costs one IPv4 address, which is exactly one more than I’d like to spend, but the result is stable: my v6-only clients appear on the IPv4 internet as my network, from an address that doesn’t change when my ISP does.
Two details. First, that address has to actually be routed to you and be local on the box โ I put it on the loopback, not on the tunnel interface, because a connected /28 pointing down a point-to-point tunnel is a routing loop waiting for a packet addressed to an unused address in it. Second, if you want it to answer pings, inbound sessions have to reply back down the tunnel, not out your default route, or the replies get source-NATed to your ISP address and silently vanish. On pf that’s reply-to.
Policy routing, not a default route
This one cost me a working LAN, so learn it from me instead.
The instinct is: I want my tunnel to carry my IPv6, so I’ll make the tunnel my default IPv6 gateway. Don’t โ not while anything on your network still uses ISP address space.
My normal LAN gets its addressing from the ISP’s prefix delegation. The moment the tunnel became the default route, that LAN’s traffic went out the tunnel with a source address belonging to my ISP, and got dropped by the first BCP38 filter it met. Ping from the LAN: 100% loss. Ping from the v6-mostly VLAN: fine. Very confusing if you don’t know what you just did.
The correct answer is a firewall rule with a gateway on it, so only that VLAN’s traffic is policy-routed into the tunnel, and everything else keeps using the ISP’s default. In OPNsense that’s setting Gateway on the interface’s pass rule. The tunnel gateway stays at defaultgw=0.
The default route only becomes the right move once nothing on the network uses ISP space any more โ which for me is exactly what the Odido switch forces, because there won’t be any ISP IPv6 to use.
Mind the MTU, and tell the clients
A GRE tunnel over PPPoE gives you 1492 โ 24 = 1468 bytes. IPv6 routers don’t fragment, so if you get this wrong the symptom is the worst kind: small packets fine, DNS fine, ping fine, and web pages that hang forever.
You can rely on Path MTU Discovery and hope nobody filters ICMPv6 Packet Too Big. Or you can just tell the clients with AdvLinkMTU 1468 in the RA and stop thinking about it. Do the second one. Do both, actually.
While testing this, know that FreeBSD’s ping6 sets IPV6_USE_MIN_MTU by default and fragments everything at 1280. I spent a while convinced I had a hard 1280-byte ceiling before remembering the -u flag exists. The ceiling was in my test tool.
DNS64 applies to everyone
unbound’s dns64 module has no concept of ‘only for this VLAN’ unless you build views for it. Mine doesn’t. Which means my normal dual-stack LAN also resolves IPv4-only names into 64:ff9b::, prefers the synthesised AAAA, and reaches those sites through NAT64.
I found this out when a laptop on the normal LAN reported its IPv4 address as my NAT64 exit address. Nothing was broken โ but a machine with a perfectly good IPv4 path was taking a detour through a tunnel and a translator to reach IPv4-only sites, which is not what I intended.
If you want NAT64 scoped to one VLAN, you need a separate resolver instance or unbound views. Decide deliberately; don’t find out from a ‘what is my IP’ page.
Config that only exists in RAM
Unrelated to IPv6, entirely self-inflicted, and worth your time anyway.
Some of my FRR configuration had been typed into vtysh and never written into OPNsense’s config. It worked perfectly โ right up until a service restart regenerated frr.conf from the saved configuration, dropped three settings that had only ever existed in the running process, and withdrew my /48 from the global routing table for four minutes.
If you configure a daemon through a management layer, configure it through the management layer. And before restarting anything, diff what’s running against what’s stored.
Which brings us to Odido
So what actually changes when the ISP swaps?
Almost nothing, and that’s the entire point of building it this way.
The tunnel is carried over IPv4 and anchored to whatever the WAN address happens to be. NAT64’s exit address belongs to my ASN and is routed through the tunnel. The BGP session lives inside the tunnel. None of that cares who provides the pipe, or whether that pipe has IPv6.
Two things do need attention:
The LAN needs renumbering. Today my normal LAN uses the ISP’s delegated prefix. Odido won’t delegate anything, because Odido doesn’t do IPv6. So the LAN moves onto a /64 from my own /48, statically โ at which point every network in the house is on my own address space, and the tunnel finally can become the default IPv6 gateway.
A dynamic address is a problem for GRE. Odido, to their credit, doesn’t CGNAT โ you get a publicly routable IPv4. But it’s dynamic on a consumer subscription, and GRE has no concept of roaming: the far end has my current address configured, and a renumber at 3am breaks the tunnel until I fix it by hand.
There are three honest answers to that. Pay for a business subscription and get a static address. Keep GRE and script the update โ hook the ‘WAN address changed’ event, reach into the core router, rewrite the tunnel endpoint. Or change transport.
I’m going to change transport, because WireGuard treats this as normal rather than exceptional: the far end lists the peer by public key with no endpoint at all, my side initiates with a keepalive, and a renumber re-pins on the next handshake. Nothing to run, nothing to break. It also authenticates and encrypts a tunnel that currently carries a BGP session in plaintext, which is a good idea regardless of who my ISP is.
It isn’t free. GRE costs 24 bytes of overhead; WireGuard over IPv4 costs 60. On a 1500-byte path that’s 1476 versus 1440 โ and on a network whose whole design depends on advertising an honest MTU, giving up 36 bytes is a real trade. I think roaming and authentication are worth it. If your ISP’s address turns out to be stable in practice, scripting the GRE endpoint is a perfectly defensible choice that keeps the bytes.
What isn’t optional is picking one, and alarming on the tunnel being down โ because once the LAN lives on tunnelled address space, a dead tunnel isn’t a degraded VLAN, it’s no IPv6 in the house.
Was it worth it?
I run an IPv6-only network segment in my house, behind an ISP that has spent a decade not deploying IPv6, using address space I hold myself, and the transition to that ISP will be a non-event for it.
There’s a serious point buried under the pettiness. IPv6 deployment in the Netherlands is not blocked on technology. The protocol is thirty years old. My home router does it. Odido’s CPE can do it. All of your devices at home can probably do it. Hell, I came across a WiFi enabled extractor hood (afzuigkap) the other day and it probably supports IPv6 too.
The hard part is organisational will, and “we’ll get to it” has been the answer for long enough that a hobbyist can build a better-addressed network in a spare evening than a national carrier ships to millions of customers. Odido is the single company in the way of the Netherlands having >50% of its total internet traffic being IPv6 capable and IPv4 is getting more scarse by the day.
The part that really grinds my gears is that Odido doesn’t communicate about it at all. No rough plans, no communication from management teams or tech teams. Tell us what’s taking so long, then at least we can understand why. Currently it’s just ignorance.
Anyway. Still no IPv6, Odido. The website’s gone, but the question stands.
For those interested, I added a link to the original letter (censored) in the original blogpost.