This article is part of a series of how I built a WireGuard tunnel for getting IPv6 connectivity. Where the last step was to figure out how to route packets from devices in my private network through the WireGuard tunnel to the Internet.
I’ve explored three different methods for solving this:
I’ll try to show how to set each of them up and try to convey their pros and cons.
You should always consider IPv6-PD first!
Consider any other option only if:
- you have a “weird” setup or want to support an esoteric use case (like I do e.g. with too many local subnets for too long a public prefix)
- you’re willing to set up, debug and maintain a somewhat experimental configuration
- you more or less understand the tradeoffs
- all of the above!
I’ll assume the following has been set up:
- default OpenWRT networks named “LAN”, “WAN”, “WAN6”
- default OpenWRT firewall rules
- an ULA prefix of
- an IPv6 WireGuard tunnel with the endpoint on our OpenWRT router being
- the remote WireGurad tunnel end point forwards the whole
to our OpenWRT router
NPTv6 (Network Prefix Translation)
This is probably the least publicly documented method of all. Discussions and tutorials are scarce. Its use cases are esoteric and probably better solved in other ways. But it’s the most interesting method, because it’s conceptually even simpler than NAT6, but only viable with IPv6 addresses.
NPT basically means that you swap the prefix part of an IPv6 address with another same-sized prefix. It exploits two facts about IPv6 addresses. The first one is that prefixes can be at most 64 bits long (i.e. for a
/64) leaving the interface identifier (i.e. the second half of the IPv6 address) untouched. The second one is that interface identifiers are basically random (i.e. because they’re either derived from (globally) unique MAC addresses or they’re randomly generated temporary addresses) and hence won’t clash. This allows for stateless, NAT-like behavior (i.e.without the “expensive” tracking of NATed connections).
You can configure NPT to be bidirectional which maps prefixes in both directions basically creating a 1:1 mapping. If you’re doing this you’re probably better off just announcing multiple prefixes to your devices or creating custom routes to bridge two networks.
An even more esoteric use case is when you create one or more unidirectional mappings allowing you to multiplex multiple networks onto one. This works great, because the interface identifiers are basically random and can be left as they are. In my tests having one-way mappings still managed to route the responses correctly although strictly speaking it shouldn’t. 🤨 I suspect that this worked accidentally, because of the standard firewall “conntrack” (i.e. connection tracking) rules. 🤔
On the “Network > Interfaces” page edit the “WAN6” interface and set “Protocol” to “unmanaged”. And make sure the “WAN6_WG” addresses say
2000:30:40:50::2/64 (note the
/64 at the end).
Similar to the NAT6 case we need a custom firewall script. You have to install the
iptables-mod-nat-extra package. I’ve created a Gist for the script. Save it to /etc/firewall.npt6 and instruct the firewall to run it when being reloaded by adding the following section to /etc/config/firewall:
config include 'npt6' option path '/etc/firewall.npt6' option reload '1'
After restarting the firewall with
/etc/init.d/firewall restart you should be good to go.
As described at the top of the firewall script you can configure mappings by adding
npt6 config sections to /etc/config/firewall (sorry, there’s no UI for this 😅).
config npt6 option src_interface 'lan' option dest_interface 'wan6_wg' option output '1'
This is the minimal setup. Just add more sections for more source and destination network pairs. Run
/etc/init.d/firewall reload to apply new configurations.
In my tests all devices could connect to IPv6 services on the internet without problems. But devices always preferred IPv4 connections over IPv6 ones. This was tricky to solve, but it comes down to this:
When a domain has both public/global IPv4 and IPv6 addresses your devices tries to determine how to connect to it. It’ll generally prefer IPv6 over IPv4, but actually its more complicated than that. All IPv4 addresses are treated as global during address selection while IPv6 addresses are classified differently depending on the prefix. It just so happens that from the outside it looks something like this: global IPv6 address > IPv4 addresses > IPv6 ULAs. It’s a little more complicated
Since we don’t have a global IPv6 address, IPv4 is preferred assuming that private IPv4 addresses will generally be NATed to the Internet while ULA prefixes won’t. 😞
This was tricky to solve. All related questions on the Internet revolved around how to prefer IPv4 over IPv6, but the solution was not invertible. It boils down to changing /etc/gai.conf to classify your ULA prefix the same as a global ones. You can accomplish this by adding a
label line for your ULA (i.e.
fd00:11:22::/48 here) and giving it the same label (i.e. the last number on the line) as the line with
1 here for me). Finding this out took me a week of trial and error until I resigned to doing the address selection algorithm by hand. 😅
I had to uncomment all the
label configuration lines and then add my custom line, because once you add a custom rule all the default ones will be reset. So to add a rule on top of the default ones I ended up with the following (note that I only added the last line, all others were part of Ubuntu’s default configuration):
... label ::1/128 0 label ::/0 1 label 2002::/16 2 label ::/96 3 label ::ffff:0:0/96 4 label fec0::/10 5 label fc00::/7 6 label 2001:0::/32 7 label fd00:11:22::/48 1 ...
I only added my network’s ULA to preserve the default behavior as much as possible and only make an exception for my network specifically. so this will change the behavior only when the device has addresses from this specific ULA.
You have to restart applications for them to pick up changes to /etc/gai.conf.
- multiple internal networks can be multiplexed onto one upstream network (even when the upstream prefix is too long (e.g. for IPv6-PD))
- internal devices are not directly reachable from the Internet (with unidirectional mapping) (this is not a replacement for a firewall!)
- very little documentation and online resources
- for your devices to use IPv6 by default you have to muck with address selection preferences on each and every one of them
- it doesn’t fall back to IPv4 when the IPv6 tunnel goes down