Setting up WireGuard VPN
WireGuard is the new kid on the block for creating a secure and maintainable VPN wherever you are. Contrary, to OpenVPN, set-up is relatively easy: you don’t have a thousand nobs to configure, it handles reconnects well, and it also claims to be faster, although I never really ran into issues in that regard. The superlatives don’t stop.
As always, it turns out that the devil is in the details, and if your set-up is
a bit different than everyone’s cup of tea then suddenly all the tutorials out
there don’t really work for you anymore. I tried to set it up on my Raspberry
Pi but I found that a combination of factors (not
running Raspbian, not
using iptables) made my
life a bit more difficult. Every tutorial out there seems to assume that you do,
but thanks to some wonderful resources I managed to figure out a way that works
for me. In this post I’ll be going over how to install and configure WireGuard
on a Raspberry Pi (running Arch Linux Arm) using
nftables
as our firewall.
As to why you would need a VPN? Well, you probably
don’t but there are valid reasons
to do want one and mine happens to be “let’s see if I can get this working.” And
finally before we begin a quick disclaimer: this post is mainly an amalgamation
of existing tutorials out there. Most importantly, the Arch Wiki on
WireGuard and this article on
setting up Wireguard with
nftables
were crucial in getting everything working for me. Tobias
Kappé also gets a shout out for pointing me in the
right direction and sharing parts of his configuration with me, which helped a
lot.
Installing WireGuard
Part of the charm of WireGuard is that it has been baked into the Linux Kernel, so in theory you don’t have to install it. Unfortunately, at the time of writing the kernel version supplied by Arch Linux Arm (5.4.51; WireGuard was included from 5.6 onward) does not include it yet so we’ll have to add it. In addition, we don’t strictly need to install the utilities, but they do make our lives easier, so let’s install those as well.
1
# pacman -S wireguard-dkms linux-raspberrypi4-headers wireguard-tools
After this is done, we can start setting up WireGuard on the server.
Configuring the WireGuard server
Okay, to clarify one thing, WireGuard doesn’t have a concept of hosts and
clients. All machines are peers and are theoretically equally capable to do
anything. We are going to pretend the distinction does exist because that
makes explaining things easier. With that out of the way, we can set up our
WireGuard interface. I’ll use
systemd-networkd
to create a persistent configuration, but there are other ways.
NetworkManager can do it, and
wireguard-tools
itself includes the wg-quick
utility. The configuration
consists of two parts: setting up the network device and creating a network
configuration. The device will handle the low-level actual data transfer, and
the network configuration will actually route things over it.
Creating the network device
In this section, we’ll be creating the virtual network device for the WireGuard
set-up. This device can be called anything, but convention is to use wg0
for
your first WireGuard interface. Before we can set up anything, we’ll need to
generate a private key for the server. We can do this (and capture the public
key on the way) by using the wg
tool:
1
$ wg genkey | tee wg0.privkey | wg pubkey > wg0.pubkey
With our new keys, we can actually create a network configuration file. Let’s
start with the following example. It will set up a virtual device wg0
listening on UDP port 51871. More details can be found in the systemd.netdev
man-page.
1
2
3
4
5
6
7
8
9
10
11
# File: /etc/systemd/network/99-wg0.netdev
[NetDev]
Name=wg0
Kind=wireguard
Description=WireGuard tunnel wg0
[WireGuard]
# The UDP port where we'll receive WireGuard packages
ListenPort=51871
# The contents of wg0.privkey
PrivateKey=VGhpcyBpcyBub3QgdGhlIGdyZWF0ZXN0IGtleSBpbiA=
Because this file contains your private key, you want to protect it properly.
You can ensure only systemd-networkd
can read it by setting the permissions to
640
and setting the owner to root:systemd-network
.
We could restart systemd-networkd
right now and see the results of our
handiwork, but it doesn’t do too much until we actually configure the interface.
So let’s get to that now.
Configuring the network device
The network configuration (shown below) is actually nothing special, but there
is one thing to highlight: the IP address. Every peer within your VPN should
have a private IP address. There’s plenty of private ranges to choose from, but
in this post I’ll use addresses in the range
10.91.0.0/16
because 1. it’s available and 2. it is unlikely to clash with private addresses
used by your common household routers, which tend to use 192.128.0.0/16
or
10.0.0.0/15
. You can also add an IPv6 address in addition to the IPv4 one, but
that would just clutter the examples.
1
2
3
4
5
6
7
# File: /etc/systemd/network/99-wg0.network
[Match]
Name=wg0
[Network]
# Choose the first address in the range, and mark the subnet.
Address=10.91.0.1/16
After adding the network configuration, we can finally restart
systemd-networkd
and admire our progress:
1
2
3
4
5
6
7
8
9
10
11
$ sudo systemctl restart systemd-networkd
$ ip a | tail -4
4: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
link/none
inet 10.91.0.1/16 brd 10.91.255.255 scope global wg0
valid_lft forever preferred_lft forever
$ sudo wg
interface: wg0
public key: dGhlIHdvcmxkIHRoaXMgaXMganVzdCBhIHRyaWJ1dGU=
private key: (hidden)
listening port: 51871
That’s it! We have a WireGuard interface now! Not that it’s useful on its own without a peer to connect to it, or even reachable without fixing some firewall rules. So, let’s do the absolute minimum there, open the necessary UDP port temporarily, and call it a day. We’ll get back to it later.
1
# nft add rule inet filter input udp dport 51871 accept
Using the VPN: adding the first client peer
Now that the server is ready for use, we can try connecting to it. As every node
in the VPN is a peer, setting up a client is entirely similar to setting up the
server, and so we’ll start by creating a network
device and configuring a network
device except this time we’ll use 10.91.0.2
as the IP address for originality.
With our two peers set-up, we can tell them about each other. We do this by
modifying the .netdev
files and adding a WireGuardPeer
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Peer 1, file: /etc/systemd/network/99-wg0.netdev
# Original contents, shown for context but unmodified:
[NetDev]
Name=wg0
Kind=wireguard
Description=WireGuard tunnel wg0
[WireGuard]
ListenPort=51871
PrivateKey=VGhpcyBpcyBub3QgdGhlIGdyZWF0ZXN0IGtleSBpbiA=
# New additions:
[WireGuardPeer]
PublicKey=aHR0cHM6Ly95b3V0dS5iZS9kUXc0dzlXZ1hjUSMuLi4=
AllowedIPs=10.91.0.2
This new [WireGuardPeer]
section configures a few things, not all of which are
relevant, not all of which are shown in the example. More details can be found
in the man
page
as always, but of interest to us are the following:
- the
PublicKey
of the other peer, used for secure authorization between them, - the
AllowedIPs
IP addresses that should be routed through this peer, and most importantly, Endpoint
says where to find the peer. Once the other peer sends an authenticated packet, this will be updated to the correct address, but it is still required for an initial meeting point.
The VPN host peer doesn’t necessarily know where the client can be found (the
client peer may be a laptop roaming the world) so the Endpoint
is not
mandatory. In our client configuration however, should include it. We also need
to route all the VPN traffic through that peer. In the end, the client
configuration may look something like this:
1
2
3
4
5
6
# Fragment of peer 2, file: /etc/systemd/network/99-wg0.netdev
[WireGuardPeer]
PublicKey=dGhlIHdvcmxkIHRoaXMgaXMganVzdCBhIHRyaWJ1dGU=
AllowedIPs=10.91.0.1/16
# May also be an IP-address
Endpoint=your-vpn.example.com:51871
Now, if we restart systemd-networkd
on both our machines, we can now start
testing out the connection. If you did everything right, it might look a little
something like this:
1
2
3
4
5
6
7
8
9
10
11
$ ping -c 5 10.91.0.1 # from the client peer
PING 10.91.0.1 (10.91.0.1) 56(84) bytes of data.
64 bytes from 10.91.0.1: icmp_seq=1 ttl=64 time=3.16 ms
64 bytes from 10.91.0.1: icmp_seq=2 ttl=64 time=0.761 ms
64 bytes from 10.91.0.1: icmp_seq=3 ttl=64 time=0.755 ms
64 bytes from 10.91.0.1: icmp_seq=4 ttl=64 time=0.851 ms
64 bytes from 10.91.0.1: icmp_seq=5 ttl=64 time=0.713 ms
--- 10.91.0.1 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 4043ms
rtt min/avg/max/mdev = 0.713/1.247/3.157/0.955 ms
Using the proxy as a VPN
With the tunnel set up, it’s now time to start configuring it as a VPN and send
all of our traffic over it. The client part of this is easy, as we can simply
adjust the AllowedIPs
parameter in the configuration. If we set it to
0.0.0.0/0
all IPv4 traffic should be directed over the tunnel. However, if you
actually attempt to use your connection now, you’ll notice that it doesn’t work.
This is because we are still blocked by two things, which we’ll deal with now.
Enabling forwarding in the kernel
The default kernel on Arch Linux Arm does not allow IP forwarding by default. We
can enable it using sysctl
. You can make this change permanent by adding a
file to /etc/sysctl.d
and load it using sysctl --system
, or a more specific
command if you really need to.
1
2
3
# File: /etc/sysctl.d/99-forwarding.conf
net.ipv4.ip_forward=1
net.ipv6.conf.all.forwarding=1
Adjusting the firewall
Note, this section assumes your
nftables
rules are structured like the examples section on the Arch wiki, with atable inet filter
that contains achain
forinput
,output
, andforward
. If your structure is different, transpose this section as needed.
Unless you have a very good reason, having your system forward all packets indiscriminately is a bad idea, which is why most firewall configurations don’t allow it by default. However, forwarding packets is exactly what a VPN is supposed to do, so we’re going to adjust our firewall to allow it, but only from the VPN. In all, we need to modify the firewall to do quite a few things:
- open WireGuard’s listening port permanently,
- allow forwarding packets received via WireGuard, and
- rewrite forwarded packets so they look like they came from this server (masquerade)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#!/usr/bin/nft -f
# Define some variables for easy reference
define wan = eth0
define vpn = wg0
define vpn_net = 10.91.0.0/16
table inet filter {
chain input {
# New: allow access to WireGuard, see point 1.
udp dport 51871 accept;
}
# chain output unchanged
chain forward {
# New: Forward all established and related traffic. Drop invalid traffic. Related to point 2.
ct state established,related accept
ct state invalid drop
# New: Allow WireGuard traffic to access the internet via wan, related to point 2.
iifname $vpn oifname $wan ct state new accept
}
}
# New: add a router section for point 3.
table ip router {
# Both need to be set even when one is empty.
chain prerouting {
type nat hook prerouting priority 0;
}
chain postrouting {
type nat hook postrouting priority 100;
# Masquerade WireGuard traffic.
# All WireGuard traffic will look like it comes from the servers IP address.
oifname $wan ip saddr $vpn_net masquerade
}
}
Once we reload the firewall configuration, everything should just work™ and all the traffic should flow through the VPN tunnel, hidden from view until it comes out the other end and is clearly visible again.
We have now connected two peers to each other, but this approach scales to any number. Note that not every peer needs to know about every other peer, but only the ones they will directly connect to. To keep with the client and host terminology: the host needs to know about all of the clients, but the clients only need to know about the host.
Final remarks
I hope you’ve learned something from my struggle to get WireGuard to work on my Raspberry Pi what we can do with it. However, Linux is not the only context where you can use your newly created VPN. The official android app makes it super easy to use on your phone, and apparently other things that I don’t own support it too.
The methods shown here are just one of the ways in which you can set up your own private WireGuard tunnels. I don’t even use the same method across all my Linux machines, as each has their own requirements based on their use.
One thing I noticed to my pleasant surprise is that the tunnels tend to survive the frequent SD-card lock-ups causing the rest of the machine to become unresponsive. Which I know for certain because it happened while I was finishing up this post and I kept the use of my VPN.
Anyway, I hope this guide has been at least somewhat useful and that it helps someone with their unique requirements to set up their VPN properly too. If you find any mistakes in this article, feel free to drop me an email.