Setup IPv6 in WireGuard
By Christopher Burg
Introduction
Over the holiday break I finished upgrading all of my self-hosted services to make them available via IPv6. The upgrade was straightforward for all of my services except one: my WireGuard VPN server. I wanted to provide my VPN clients with IPv6 connectivity without using NAT. Unfortunately most of the guides online use NAT for IPv4 and IPv6. NAT is necessary for IPv4, but goes against the very design of IPv6. I wanted to provide each client with a globally routable IPv6 address. This ended up being simple once I figured it out.
I previous wrote a guide for setting up WireGuard to share a public static IPv4 address. The formatting was ruined in the transition from my old WordPress blog to this statically generated one, but it explains how to setup a WireGuard VPN server for IPv4. This post will explain how to setup a WireGuard VPN server for IPv6.
Preamble
First a major caveat. As of this writing, I've had this iteration of my VPN server running for two days. I've tested it on my home network, on my phone's cellular network, and on an IPv4 only network through my travel router. IPv6 appears to work in all cases. I haven't finished thorough testing though. There may be a number of corner cases that don't work. I will update this guide as I find and fix them. Therefore, if you find something broken, check back and I might have discovered it and posted the solution already.
I also pieced my setup together by taking bits and pieces of several online guides. I pillaged from this guide, this guide, this guide, and this guide. If there are unnecessary configuration steps in this guide, I likely took it from one of these guides and didn't test my final setup without the configuration. Such mistakes are entirely my fault.
In order to follow this guide, you need a source of IPv6 addresses and specifically a separate subnet from your hosting network. My ISP provides me with a /48 prefix. This gives me roughly a bajillion addresses and subnets.
Conventions Used in This Guide
For the purposes of this guide, I'm going to use the following IP addresses (2001:db8 is the prefix reserved for examples and documentation for those not aware):
2001:db8:1234::/48: The prefix provided by our ISP.
2001:db8:1234:9999::/64: The subnet of our hosting network. The subnet 9999 was chosen for readability purposes.
2001:db8:1234:ffff::/64: The subnet provided to our WireGuard VPN clients. Throughout this guide remember that the subnet with numbers is for our hosting network and the subnet with letters is for our VPN clients.
2001:db8:1234:9999::1: The IPv6 address used by clients to connect to our WireGuard VPN server.
2001:db8:1234:ffff::1: The IPv6 address of the WireGuard interface. This differs from the above address in that a client only uses it after it has connected to the WireGuard VPN server through the above address. Once connected, clients will use this address as their gateway.
2001:db8:1234:ffff::1:1: The IPv6 address provided to the first of our WireGuard VPN clients. This address will be incremented subsequently so our second client will have an IPv6 address of 2001:db8:1234:ffff::1:2, our third 2001:db8:1234:ffff::1:3, etc.
I will also use en0 as the name of the network interface of our WireGuard VPN server, 51820 as the port used to connect to WireGuard on our server, and wg-vpn.conf as the name of our WireGuard configuration file.
The Actual Guide
The first thing you will need to do is ensure that the router between your hosting network and ISP is setup to route the subnet for our VPN clients to our VPN server. How you do this will depend on both your ISP and your router. For my Ubiquiti Cloud Gateway Ultra, I created a static route that routes the entire 2001:db8:1234:ffff::/64 subnet to 2001:db8:1234:9999::1. You will also need to configure your router's firewall to allow incoming WireGuard traffic to your VPN server.
I'm hosting my VPN server on Fedora Server. By default, IPv6 forwarding isn't enabled on Fedora Server. I enabled it by adding net.ipv6.conf.all.forwarding=1 to /etc/sysctl.conf and issued the sysctl -p command to load the changes. I also added several other lines. /etc/sysctl.conf on my VPN server contains the following contents:
net.ipv4.ip_forward=1
net.ipv6.conf.en0.proxy_ndp=1
net.ipv6.conf.all.forwarding=1
net.ipv6.conf.en0.accept_ra=2
The first line enables IPv4 forwarding. It's enabled by default on Fedora Server, but I added the line just to ensure it's always enabled. The second line enables proxying NDP packets, which is used by IPv6 to discover neighboring devices. The final line enables router advertisements while IPv6 forwarding is enabled. I added it to the file when I was trying to dole out IPv6 addresses through another method. I'm not sure if it's needed for my final setup. This is one of those potentially unnecessary configuration steps I mentioned in the preamble for this guide.
The only port I opened on my VPN server's firewall is 51820 for UDP packets. WireGuard only uses UDP so you don't need to open the port for TCP. I also enabled masquerading capabilities. Masquerading capabilities are only needed for NAT, which means it's only necessary for IPv4. You don't need to enable it if you're only using IPv6 on your VPN server.
Everything else happens in the WireGuard configuration file located at /etc/wireguard/wg-vpn.conf. The first section of the configuration file sets up the interface:
[Interface]
PrivateKey = <Your Server's Private Key>
Address = 2001:db8:1234:ffff::1/64
SaveConfig = false
ListenPort = 51820
This is all pretty basic. PrivateKey obvious contains your server's private key that was generated with wg genkey. Address is the IPv6 address of the WireGuard interface. Clients will use this address as their gateway once they've connected to our VPN server. SaveConfig determines if the current configuration is saved when the WireGuard interface is shutdown. I generate my configuration files with Ansible so I don't want any state maintained and disable this feature. ListenPort is the port through which clients will connect to the VPN server.
The next section is where the heavy lifting is done. PostUp commands run when the WireGuard server is being started. PostDown commands run when the WireGuard server is being stopped. In this case, the PostDown commands simply undo the PostUp commands. Also note that %i stands for the WireGuard interface name, which is wg-vpn since the configuration file is wg-vpn.conf.
PostUp = ip6tables -A FORWARD -i en0 -o %i -j ACCEPT;
PostUp = ip6tables -A FORWARD -i %i -j ACCEPT;
PostUp = ip -6 neighbor add proxy 2001:db8:1234:ffff::1:1 dev en0
PostUp = ip -6 neighbor add proxy 2001:db8:1234:ffff::1:2 dev en0
PostUp = ip -6 neighbor add proxy 2001:db8:1234:ffff::1:3 dev en0
PostDown = ip6tables -D FORWARD -i en0 -o %i -j ACCEPT;
PostDown = ip6tables -D FORWARD -i %i -j ACCEPT;
PostDown = ip -6 neighbor del proxy 2001:db8:1234:ffff::1:1 dev en0
PostDown = ip -6 neighbor del proxy 2001:db8:1234:ffff::1:2 dev en0
PostDown = ip -6 neighbor del proxy 2001:db8:1234:ffff::1:3 dev en0
PostUp = ip6tables -A FORWARD -i en0 -o %i -j ACCEPT; and PostUp = ip6tables -A FORWARD -i %i -j ACCEPT; enable IPv6 forwarding between the server's network interface (en0 in this example) and the WireGuard interface (wg-vpn in this example). This allowed IPv6 traffic originating from our clients to be forwarded out of the VPN server's network interface and return traffic routed from the VPN server's network interface to the appropriate client.
PostUp = ip -6 neighbor add proxy 2001:db8:1234:ffff::1:1 dev en0 and the following two lines setup NDP proxies for each client. You could probably replace all of these lines with PostUp = ip -6 neighbor add proxy 2001:db8:1234:ffff::/64 dev en0. Again I generate my configuration file with an Ansible playbook so it's just as easy for me to loop through my list of clients and add a line per client. As mention above, the PostDown lines simply undo the PostUp lines when the WireGuard interface is stopped.
The final part of the file contains configuration information for each peer:
[Peer]
PublicKey = <The Client's Public Key>
AllowedIPs = 2001:db8:1234:ffff::1:1/128
[Peer]
PublicKey = <The Client's Public Key>
AllowedIPs = 2001:db8:1234:ffff::1:2/128
[Peer]
PublicKey = <The Client's Public Key>
AllowedIPs = 2001:db8:1234:ffff::1:3/128
This is straightforward. PublicKey contains the client's public key and AllowedIPs contains the IPv6 address we're assigning to the client. Running systemctl start wg-quick@wg-vpn.service will bring the WireGuard interface up so clients can start connecting.
The configuration file for each client is simple:
[Interface]
Address = 2001:db8:1234:ffff::1:1/64
PrivateKey = <The Client's Private Key>
[Peer]
AllowedIPs = ::/0
Endpoint = [2001:db8:1234:9999::1]:51820
PublicKey = <Your Server's Public Key>
Address is the globally routable IPv6 address our VPN server is assigning to the client. PrivateKey is the client's private key. There's similarly little to say about the [Peer] section, which contains connection information for the VPN server.
AllowedIPs = ::/0 tells the client to route all IPv6 traffic through the WireGuard interface. Endpoint contains the IPv6 address clients use to connect to the VPN server. Note the square brackets. Because IPv6 addresses use ':' as a separator and ':' is also used by convention to separate an IP address from a port number, the square brackets are used to disambiguate the ':' in the IPv6 address from the ':' differentiating the port number.
When the client connects to the VPN server, it will have the globally routable IPv6 address of 2001:db8:1234:ffff::1:1. If you connect to a website that tests IPv6 connectivity such as this IPv4 and IPv6 connectivity test, it should show 2001:db8:1234:ffff::1:1 as your IPv6 address.
There you have it, a WireGuard VPN server that provides clients with globally routable IPv6 addresses.