WG + IPv6 + NAT = Dumb.
There are a variety of reasons why I have always had remote access to my home network when away from home (e.g., privacy, Pi-hole, Plex, petc.). Throughout the years, it's taken a variety of forms: SSH tunneling, OpenVPN (first TUN, then TAP), and now WireGuard.
Of the various solutions, WireGuard has definitely been the most straightfoward to configure. Seriously, 10/10: would recommend. In fact, there are probably some interesting and specific use cases that could be the subject of a future blog post. Unfortunately, none of those are this.
First, some background
WireGuard uses a cryptokey routing table, where each peer (identified by a public key) has an associated set of allowed IP addresses. (See the whitepaper, pg. 4.) This enables some of those interesting use cases to which I alluded, but it also proves problematic in the context of IPv6.
See, the list of allowed IPs is static (both at the client and the server; both are "peers"), so in instances where there is a changing IPv6 prefix (as is the case with my ISP), there is no easy way to update a client's IPv6 address. Further, in contrast to OpenVPN, there is no mechanism that can be used to push configuration options to a client, nor can DHCP be used.
That means, absent the drivel that follows, I don't think it would be possible to access the IPv6 internet without a headache every time I get a different IPv6 prefix from my ISP. And you might ask: who even cares about the IPv6 internet (at least right now)? Good question; I ask myself that sometimes. "But it's the future™," they'll say. Anyway, enough about the why (and, apparently most importantly, the why not).
And now, the foreground
The initial solution to my IPv6 problem was to pick an internal prefix for IPv6 traffic, so each WireGuard peer would have its own unique local address (in the fd00::/8 address space). I then set up an outbound NAT rule to translate traffic having the source IPv6 prefix to the interface address of my WAN interface.
I am fairly certain that the general consensus is that IPv6 and NAT don't go together. Not so much that they can't, just that performance may be better and mainly that the vast address space of IPv6 obviates the need for it. So, maybe not so dumb. Also, it isn't dumb if it works.
And this solution is where things stayed for many months. Working access to IPv6 paired with the fact that most of my VPN-based traffic appeared to still be largely IPv4 meant that I didn't feel the need to address one last qualm, which brings us to the reason for this post: temporary addresses.
Unfortunately, my hacky solution meant that all of the IPv6 traffic traveling over the VPN appeared to come from my firewall (i.e., {IPv6 prefix}::0). Is this really a big deal? No. It really, really isn't, becuase that's how IPv4 NAT has worked forever. But it bothered me for two reasons:
- I don't want to have tons of traffic seemingly originating from my firewall device. I subscribe to the widely held belief that security through obscurity is not security, and I have no problem with making that address known. I do have a problem with routinely accessing websites from a {IPv6 Prefix}::0/56 address (i.e., an IPv6 address ending in 18 zeroes in hexadecimal format), which could potentially be taken to mean that all 4,722,366,482,869,645,213,696 (272) of my addresses in that address space are associated with one another. Is that paranoid? Most likely (though I have no doubt companies are doing all they can do analyze IPv6 source addresses). Somewhat more reasonably, I don't like the fact that having multiple devices using the same IP address would make it easier to tie the devices together. Either way, I paid for 128 bits, I'm going to use 128 bits.
- There is something appealing to rotating through source addresses periodically. This is definitely related to #1, but barring some indication made by the device (e.g., cookies, tokens, session identifiers, GET variables, etc.), there is nothing tying one temporary IPv6 address to another temprorary IPv6 address.
The answer? Aliases! Instead of having an outbound NAT rule to translate fd00::/8 traffic to my singular WAN interface address, I created a rule that uses an alias for the translation, which is populated with random, rotating IPv6 addresses. Stupid like a fox amirite?
Unfortunately, that is easier said than done. See, OPNsense (and likely others) don't offer the option to use just any alias for outbound NAT. Rather, the outbound NAT UI only presents "Host(s)" aliases for selection. Further, even if I could select whatever alias I want, there is no way to create an alias with a dynamic prefix. The dream would be an alias that could be specified as "::{suffix}," where the alias prepends the current IPv6 WAN prefix to the alias for use in whatever firewall rules (a la dynv6).
Fortunately, aliases can be maintained from the command line using pfctl
. The script below makes use of pfctl
and some unimpressive Bash and Python to keep an alias ("wg_IPv6_NAT") stocked with random IPv6 addresses having the prefix that I am currently assigned by my ISP.
#!/bin/sh
# Get current IPv6 prefix
IPv6_prefix=$(ifconfig wan_stf | grep inet6 | awk -F" " '{print $2}' | sed 's/::/:/')
# Check if we still have the same prefix based on the first entry in the alias
curr_IPv6_prefix=$(pfctl -t wg_IPv6_NAT -T show | head -n 1 | grep -o "[0-9a-f]\+:[0-9a-f]\+:[0-9a-f]\+:[0-9a-f]\+:")
# Expire old table entries
toGen=2
if [ "$curr_IPv6_prefix" == "$IPv6_prefix" ]; then
# After 20 minutes
pfctl -t wg_IPv6_NAT -T expire 1200
else
# Or if we got a new IPv6 prefix
pfctl -t wg_IPv6_NAT -T flush
# And if we did, generate 20 addresses instead of just two
toGen=20
fi
# Store a set of random IPv6 addresses as a pf table
for i in `seq 1 1 $toGen`
do
# Generate a random /64 suffix
rand_suffix=$(python3 -c "import random; print(':'.join('{:x}'.format(random.randint(0, 2**16 - 1)) for i in range(4)))")
randIPv6="$IPv6_prefix$rand_suffix"
echo Will add $randIPv6
# Append the random IPv6 address to the list that is passed to pfctl
randList="$randList $randIPv6"
done
pfctl -t wg_IPv6_NAT -T add $randList
The main takeaways from the script are that the IPv6 prefix is pulled from the WAN interface, compared to an existing alias entry, and used to either: 1) generate two new addresses if the prefix of the first alias entry is unchanged with respect to the WAN prefix; or 2) flush the entire table and generate 20 new addresses if there is a new prefix. The random addresses are then generated using a quick Python call and passed to pfctl
.
This script is called every minute to generate two new addresses, and old addresses fall out of the alias after 20 minutes. This means that I will have a rolling pool of ~40 addresses to draw from. Long term, I will likely tidy up the script, but for now it works.
Closing Thoughts
I've been running this setup since the week that I started this blog post, which is to say for several months now. It's been a set-it-and-forget-it type deal, which is excellent. Though that could be because IPv6 connectivity is relatively unimportant in today's Internet, so I don't know if I would notice if there were issues.
Aside from potentially revising the script, there is one other feature that I would consider implementing: word addresses. How cool would it be to always access the internet from an IPv6 address having words in it (e.g., "::dead:beef" or "::b0b:ca7"). A quick search yields a script for generating hex words. Combining such a technique with my script would be relatively trivial and could even be applied to all traffic out of my network to the open Internet (not just WireGuard traffic). Maybe someday.