These days when most people think about a VPN, they are thinking of a service that changes the endpoint of their internet connection to look like it’s coming from another location. Here we’re going to implement that with WireGuard.

The main thing we add to our first WireGuard tunnel is turning one end of the tunnel into a routing server for clients to connect to. This requires enabling routing and NAT, and specifying in WireGuard which addresses or ranges are allowed through the tunnel.

On the clients, we specify all addresses, (minus any private network ranges, although clients can provide an option to take care of that). This causes WireGuard to establish itself as the default route for the internet on the client device.

On the server, we specify the local network applied to the tunnel which, in conjunction with its routing and NAT, then acts as a gateway for that private network.

Server Configuration

IP Forwarding

You will need to enable routing for both IPv4 and IPv6 to use any layer 3 VPN which is where WireGuard fits. On most linux systems this can be achieved by adding two lines to either /etc/sysctl.conf or a file under /etc/sysctl.d/. I placed a file here named 99-enablerouting.conf with the following lines:

net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1

If you are only using IPv4 you can leave out the IPv6 line.

Server Keys

In this example we’ll be using the following keypair for the server. I list them here to refer to when they are used in the examples. Obviously you should generate your own keys.

Private Key: wO9e53VcNnvSH98+BPkNxH/vGbSZpMEsPxyUHFojRE4=
Public Key: rdFwleVXivqC5VmTy4wty5jl47FIJLg2+jagsNBtsm4=

Server WireGuard Config

New things we will be adding to this config are PostUp and PostDown entries to enable forwarding, and NAT masquerading for IPv4.

(I’m using a public allocated block for IPv6. It does work to use a private IPv6 block and you can even use masquerading although it’s not recommended. A device will not default to using a private IPv6 block address as a default route even if one is available and will prefer the IPv4 address.)

The example configs will be using IPv6 addresses from the RFC 3849 documentation only prefix 2001:DB8::/32. You will also need to adjust the interface name in the iptables masquerade line if your server’s internet facing interface is not eth0 as in the example.

We’ll also be adding preshared keys for peer pairs to show how they work. These can be used to harden the encryption against future attack by helping to secure the key exchange process. Pro Custodibus has a good article discussing their use.

[Interface]
Address = 10.100.100.1/24, 2001:DB8:ABC:DEF::1/64
ListenPort = 51820
PrivateKey = wO9e53VcNnvSH98+BPkNxH/vGbSZpMEsPxyUHFojRE4=

PostUp = iptables -w -A FORWARD -i %i -j ACCEPT; iptables -w -t nat -A POSTROUTING -s 10.100.100.0/24 -o eth0 -j MASQUERADE; ip6tables -w -A FORWARD -i %i -j ACCEPT
PostDown = iptables -w -D FORWARD -i %i -j ACCEPT; iptables -w -t nat -D POSTROUTING -s 10.100.100.0/24 -o eth0 -j MASQUERADE; ip6tables -w -D FORWARD -i %i -j ACCEPT

[Peer]
#Android Client
PublicKey = icmI7+LgbhkCz+UO23onHA8DcJVn8frozJOoojcpm0c=
PresharedKey = 6j4fIkUWaLv1ppuDB1aKxqBBM52vrZH9f4ky0nkXjH4=
AllowedIPs = 10.100.100.2/32, 2001:DB8:ABC:DEF::2/64

[Peer]
#Windows Client
PublicKey = wrSyUP2vxPXxX8j9iVpuZRYr5BhluYUddBOQX/QwUkI=
PresharedKey = iZRkXtjx/8UhnBo1Nw4ER8/o80HIOGVbq25msW2FXFE=
AllowedIPs = 10.100.100.3/32, 2001:DB8:ABC:DEF::3/64

[Peer]
#Linux Client
PublicKey = qp5xfPFtCa1B9uxfNbBUj07mDxRWVdLqnAz3IPBrwwE=
PresharedKey = Z8K3Qb4FnKcM/ojtvVPVPi91X6u3WQlIdDtYtOgDPNo=
AllowedIPs = 10.100.100.4/32, 2001:DB8:ABC:DEF::4/64

Client Configuration

Default Route and local network access

In order to have the WireGuard client set the connection as the default route, you specify the server peer to AllowedIPs = 0.0.0.0/0, ::/0. This is essentially specifying all valid addresses as allowed through the tunnel. This will signal to WireGuard to set up that peer as the default route, however how WireGuard handles routing differs slightly across different operating systems.

On both Windows and Android, this will also cause the client to block any traffic outside of the tunnel by default, but there is an option to disable this.

These config files can be imported into the relevant apps on each platform. I have no experience with iOS or Mac OS so I don’t have documentation or examples for those platforms.

DNS

For this example I have used the Cloudflare 1.1.1.1 DNS service. Because I am providing IPv6 access I have included both IPv6 and IPv4 addresses for this service. You can copy this directly or specify your own DNS servers as necessary.

Client Keys

We have three clients to document so we’ve generated 3 key sets to use.

Android Client

Private Key: YNAgumwVa2n9M02xJqxuOSu2RnNTUSd8OgJ7lnGC03c=
Public Key: icmI7+LgbhkCz+UO23onHA8DcJVn8frozJOoojcpm0c=
Preshared Key: 6j4fIkUWaLv1ppuDB1aKxqBBM52vrZH9f4ky0nkXjH4=

Windows Client

Private Key: WJiH3N/ZBkklPDqwX1viIS6z9u+vy5i8AIFg+NW1fEM=
Public Key: wrSyUP2vxPXxX8j9iVpuZRYr5BhluYUddBOQX/QwUkI=
Preshared Key: iZRkXtjx/8UhnBo1Nw4ER8/o80HIOGVbq25msW2FXFE=

Linux Network Manager Client

Private Key: OLOpnZiz/299GLraAsD1cpTPHZVmup4XFINtAS9lo0Q=
Public Key: qp5xfPFtCa1B9uxfNbBUj07mDxRWVdLqnAz3IPBrwwE=
Preshared Key: Z8K3Qb4FnKcM/ojtvVPVPi91X6u3WQlIdDtYtOgDPNo=

Android

[Interface]
PrivateKey = YNAgumwVa2n9M02xJqxuOSu2RnNTUSd8OgJ7lnGC03c=
Address = 10.100.100.2/24, 2001:DB8:ABC:DEF::2/64
DNS = 2606:4700:4700::1111, 2606:4700:4700::1111, 1.1.1.1, 1.0.0.1

[Peer]
PublicKey = rdFwleVXivqC5VmTy4wty5jl47FIJLg2+jagsNBtsm4=
PresharedKey = 6j4fIkUWaLv1ppuDB1aKxqBBM52vrZH9f4ky0nkXjH4=
#In the following, possibly include 10.100.100.0/24, see below
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = server.example.com:51820

The Android app allows you to import the tunnel config two ways, either from a file or from a qr code. If you want to transfer the file to the device make sure you do it in a secure way as it includes the private key for the device and the pre-shared key.

From the app touch the + button and select import from file or archive then browse to the file on your device.

You can also use qrencode from the system where the keys were generated to generate a qr code. In the app if you select scan from qr code it provides a tip to generate it with

qrencode -t ansiutf8 < tunnel.conf

This will dump a qrcode in the terminal which can be scanned directly.

By default this connection will route all traffic through the tunnel which means the local network you’re connected to physically won’t be accessible. In most cases this is exactly what you want but if you don’t want this you can tap on the tunnel and edit it using the pencil icon at the top. Under the peer configuration there will be a checkbox labelled Exclude private IPs which will change the AllowedIPs option to:

AllowedIPs = 0.0.0.0/5, 8.0.0.0/7, 11.0.0.0/8, 12.0.0.0/6, 16.0.0.0/4, 32.0.0.0/3, 64.0.0.0/2, 128.0.0.0/3, 160.0.0.0/5, 168.0.0.0/6, 172.0.0.0/12, 172.32.0.0/11, 172.64.0.0/10, 172.128.0.0/9, 173.0.0.0/8, 174.0.0.0/7, 176.0.0.0/4, 192.0.0.0/9, 192.128.0.0/11, 192.160.0.0/13, 192.169.0.0/16, 192.170.0.0/15, 192.172.0.0/14, 192.176.0.0/12, 192.192.0.0/10, 193.0.0.0/8, 194.0.0.0/7, 196.0.0.0/6, 200.0.0.0/5, 208.0.0.0/4, ::/0, 1.1.1.1/32, 1.0.0.1/32

This has effectively excluded all IPv4 private network blocks, and explicitly added the designated IPv4 DNS servers to make sure they are still tunnelled. One thing I noticed though was that it didn’t also explicitly include the private network block used in the tunnel (in this case 10.100.100.0/24). This doesn’t seem to break routing, but if you want to be able to connect to the server or other devices connected to that subnet, you will need to add it to the list yourself. If your original configuration includes it, as mentioned in the comment in the example, it will be retained when selecting Exclude private IPs.

Windows

[Interface]
PrivateKey = WJiH3N/ZBkklPDqwX1viIS6z9u+vy5i8AIFg+NW1fEM=
Address = 10.100.100.3/24, 2001:DB8:ABC:DEF::3/64
DNS = 2606:4700:4700::1111, 2606:4700:4700::1111, 1.1.1.1, 1.0.0.1

[Peer]
PublicKey = rdFwleVXivqC5VmTy4wty5jl47FIJLg2+jagsNBtsm4=
PresharedKey = iZRkXtjx/8UhnBo1Nw4ER8/o80HIOGVbq25msW2FXFE=
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = server.example.com:51820

To import the config to the windows application, you need to securely transfer the file to the machine then click the Add Tunnel button in the app, and open the file. It will create a tunnel based on the filename.

Similar to the Android app, the windows application will by default block any untunnelled traffic when it is active. How it handles this is quite different though. The WireGuard creator’s website has a good description of this. As it explains there, if you want to allow local untunnelled traffic there is a checkbox labelled “Block untunneled traffic” that will be selected after importing the config. Unchecking this will change the AllowedIPs to:

AllowedIPs = 0.0.0.0/1, 128.0.0.0/1, ::/1, 8000::/1

This still allows all traffic through the tunnel as a default route, but Windows will still send local traffic outside the tunnel. There are no issues relating to the tunnel address ranges as on the Android app.

Desktop Linux/Network Manager

[Interface]
PrivateKey = OLOpnZiz/299GLraAsD1cpTPHZVmup4XFINtAS9lo0Q=
Address = 10.100.100.4/24, 2001:DB8:ABC:DEF::4/64
DNS = 2606:4700:4700::1111, 2606:4700:4700::1111, 1.1.1.1, 1.0.0.1

[Peer]
PublicKey = rdFwleVXivqC5VmTy4wty5jl47FIJLg2+jagsNBtsm4=
PresharedKey = Z8K3Qb4FnKcM/ojtvVPVPi91X6u3WQlIdDtYtOgDPNo=
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = server.example.com:51820

The most common way to manage networks on desktop Linux systems is network manager, which has various different front-ends for managing it. I’m a KDE user so I use their plasma-nm tool for most things. However another tool that works regardless of desktop environment is their nmcli tool.

My first attempt at configuring it manually did not work and it took a lot of tweaking and searching to figure out what I’d done wrong. When manually adding the tunnel I had been adding the server IPs as the gateway address for IPv4 and IPv6. However, the way Network Manager handles setting up the default route breaks if you configure the gateway addresses. @cks@mastodon.social has an excellent post on their blog that in part covers this issue. Referring to the WireGuard specific configuration properties they point out that the ipv4/ipv6-auto-default-route option by its default value will handle this if the relevant ipv4/ipv6-never-default option is not explicitly set and any peer has the default route set in its AllowedIPs as we have done.

Thankfully, if you are just importing the configuration file, it handles this properly and you won’t run into this issue. I thought it was worth mentioning if you were going to try to manually create the tunnel.

As for importing the config, I actually ran into problems with plasma-nm importing the config. One issue was that it was setting the IP addresses to the network address instead of the host address specified (for example, 10.100.100.0/24 instead of 10.100.100.4/24). Using the nmcli tool to import it worked correctly though. After transferring the file securely to the client, you can import it with

nmcli connection import type wireguard file server.conf

The tunnel will be created with the name based on the configuration file supplied. This, (on a standard setup), creates a config file at /etc/NetworkManager/system-connections/server-[UUID].nmconnection. This file will contain the private keys for the connection and on my system created the VPN as usable by any other user.

KDE’s plasma-nm does provide an interface to configure which users can control the connection, under the General configuration tab by deselecting All users may connect to this network and then clicking the Advanced button to select which users may use it.

The private key and preshared keys can also be moved to the KDE wallet by going to the WireGuard Interface tab and under the Private key field selecting Store password for this user only (encrypted). Then clicking on the Peers button and doing the same thing under the Preshared key field. However it’s important to keep in mind that this effectively requires KDE to provide those secrets to NetworkManager to enable the connection, although if you’re generally using just one desktop environment anyway that wouldn’t be an issue.

Other desktop environments likely have their own facilities to do similar things, hopefully without the bugs I encountered importing the tunnel in KDE.

Conclusion

Although only three systems have been presented here, clients exist for more, and especially on Linux, there are many different tools and utilities for setting it up. Just here we are using wg-quick on the server and NetworkManager on the client. Hopefully the gotchas I have run into can be avoided in your own setup process.

My next WireGuard entry will be primarily about using it to set up an IPv6 tunnel on a network without native IPv6 available, although I’m hoping to use it as an example of the versatility of this tool.