WireGuard is a fast and modern VPN protocol.
It is a point-to-point VPN, which means it does not have a client-server architecture, but peers, and does not rely on a PKI, unlike OpenVPN. It is super simple to setup to connect multiple machines together.
WireGuard supports roaming, which means you can switch between network connections and not have to reconnect to your peers. On servers, it’s rarely useful, but when one of the peer is a mobile client like a laptop or a smartphone, it’s a life saver, because the usage of WireGuard is completely transparent.
I’m used to OpenVPN, I even maintain a quite popular script, but WireGuard is better in pretty much all aspects.
Tip
I made a wireguard-install script to automate the installation!
In this post, I will explain how I use WireGuard on my laptop and phone, which forward all their traffic to the server while having a dual-stack connectivity.
The setup is pretty simple : we have 2 peers, one server and one client. Connecting both in a private subnet is easy. The trick to make use of the VPN to forward all of the client’s traffic trough the server is to:
- Make the client’s WireGuard interface its gateway (default route)
- Enable IP routing on the server
- Enable NAT between the WireGuard interface and public interface on the server
We will see how to add multiple clients at the end of the tutorial.
Ready?
Installing WireGuard
WireGuard comes in two parts: the tools, which will allow us to manage the peers and interfaces, and the Linux kernel module. On other platforms such as macOS, non-rooted Android and FreeBSD, the module is replaced by a userspace Go implementation.
FYI, it is planned for the WireGuard module to be integrated in the Linux kernel itself.
WireGuard can run nearly anywhere, all the installation notes are on the website.
I’m usually using Debian 9 or Ubuntu 18.04 on my servers. On Debian, you need to install it from the unstable
repository and on Ubuntu from a PPA.
I recommend the cheap $3.50 VM from Vultr. You should choose the location that is the closest to you. They provide IPv4, IPv6 and 500 GB for traffic per month.
As for my clients, I use the macOS Go client, the Arch Linux build from the community repo and the Android app.
Edit: I use the excellent GUI client for macOS now!
Configuring WireGuard
Here are the steps:
- Add the WireGuard interface on the server
- Add the WireGuard interface on the client
- Add the server as a peer on the client
- Add the client as a peer on the server
- Tune the configuration to make the client’s traffic go trough the server
Configuring the WireGuard interface on the server
The configuration of WireGuard lives in /etc/wireguard
.
We’ll call our interface wg0
, so the config file will be /etc/wireguard/wg0.conf
.
First, let’s assign IP addresses from a private subnet:
[Interface]
Address = 10.66.66.1/24,fd42:42:42::1/64
Then, let’s define the port WireGuard will be listening on:
ListenPort = 1194
Then, let’s generate a private key. WireGuard uses simple Curve25519 public and private keys for cryptography between the peers.
root@wg ~# wg genkey
AMfhWQnwtdht5HWGcW6se4AtBzb9iyTtX4XRKLo3o0A=
Add it to the configuration:
PrivateKey = <server private key>
We’re done!
[Interface]
Address = 10.66.66.1/24,fd42:42:42::1/64
ListenPort = 1194
PrivateKey = <server private key>
You have two ways of starting the interface.
The manual way is with wg-quick
:
root@wg ~# wg-quick up wg0
[#] ip link add wg0 type wireguard
[#] wg setconf wg0 /dev/fd/63
[#] ip address add 10.66.66.1/24 dev wg0
[#] ip address add fd42:42:42::1/64 dev wg0
[#] ip link set mtu 1420 up dev wg0
You can remove the interface with :
root@wg ~# wg-quick down wg0
[#] ip link delete dev wg0
I recommend to use the systemd service, and to enable it:
systemctl start wg-quick@wg0
systemctl enable wg-quick@wg0
root@wg ~# systemctl status wg-quick@wg0
● [email protected] - WireGuard via wg-quick(8) for wg0
Loaded: loaded (/lib/systemd/system/[email protected]; indirect; vendor preset: enabled)
Active: active (exited) since Sun 2019-01-27 11:43:19 UTC; 1min 1s ago
Docs: man:wg-quick(8)
man:wg(8)
https://www.wireguard.com/
https://www.wireguard.com/quickstart/
https://git.zx2c4.com/WireGuard/about/src/tools/man/wg-quick.8
https://git.zx2c4.com/WireGuard/about/src/tools/man/wg.8
Main PID: 7512 (code=exited, status=0/SUCCESS)
Tasks: 0 (limit: 505)
CGroup: /system.slice/system-wg\x2dquick.slice/[email protected]
Jan 27 11:43:19 wg systemd[1]: Starting WireGuard via wg-quick(8) for wg0...
Jan 27 11:43:19 wg wg-quick[7512]: [#] ip link add wg0 type wireguard
Jan 27 11:43:19 wg wg-quick[7512]: [#] wg setconf wg0 /dev/fd/63
Jan 27 11:43:19 wg wg-quick[7512]: [#] ip address add 10.66.66.1/24 dev wg0
Jan 27 11:43:19 wg wg-quick[7512]: [#] ip address add fd42:42:42::1/64 dev wg0
Jan 27 11:43:19 wg wg-quick[7512]: [#] ip link set mtu 1420 up dev wg0
Thus the interface will be automatically added at boot.
You can see the interface status and the public key with wg show
or wg
:
root@wg ~# wg show
interface: wg0
public key: <server public key>
private key: (hidden)
listening port: 1194
Configuring the WireGuard interface on the client
The configuration on the client is essentially the same.
Generate a private with wg genkey
, and assign addresses:
[Interface]
PrivateKey = <client private key>
Address = 10.66.66.2/24,fd42:42:42::2/64
Put this in /etc/wireguard/wg0.conf
, and start the interface :
stanislas@mbp ~> wg-quick up wg0
[#] wireguard-go utun
...
INFO: (utun1) 2019/01/27 14:36:58 Starting wireguard-go version 0.0.20181222
[+] Interface for wg0 is utun1
[#] wg setconf utun1 /dev/fd/63
[#] ifconfig utun1 inet 10.66.66.2/24 10.66.66.2 alias
[#] ifconfig utun1 inet6 fd42:42:42::2/64 alias
[#] ifconfig utun1 up
[+] Backgrounding route monitor
Here I’m using it on macOS so the interface name is utun1
.
Edit: I use the excellent GUI client for macOS now!
Configuring peers
Now that our interfaces are up, let’s configure the peers. It will allow us to make our server and our client communicate.
On the client, add this :
[Peer]
PublicKey = <server public key>
Endpoint = <server public ip>:1194
AllowedIPs = 10.66.66.1/32,fd42:42:42::1/128
Thanks to this, all the packets destined to AllowedIPs
will be encrypted with PublicKey
and sent to Endpoint
.
On the server, it’s basically the same, with the client private IP and without the endpoint:
[Peer]
PublicKey = <client public key>
AllowedIPs = 10.66.66.2/32,fd42:42:42::2/128
Wait… No endpoint? But wasn’t this supposed to be a point-to-point server?
Yes! But WireGuard supports roaming on both ends, and that’s what allows us to have peers on the server without endpoints. As long as the peers (the clients) have the initial endpoint of the server, the server will know where so send the packets back, because the client’s endpoints will be built dynamically.
From the WireGuard website about built-in roaming:
The client configuration contains an initial endpoint of its single peer (the server), so that it knows where to send encrypted data before it has received encrypted data. The server configuration doesn’t have any initial endpoints of its peers (the clients). This is because the server discovers the endpoint of its peers by examining from where correctly authenticated data originates. If the server itself changes its own endpoint, and sends data to the clients, the clients will discover the new server endpoint and update the configuration just the same. Both client and server send encrypted data to the most recent IP endpoint for which they authentically decrypted data. Thus, there is full IP roaming on both ends.
I hope that makes sense. Keep in mind that WireGuard does not create a tunnel like OpenVPN does, but each packet is encapsulated right away.
Now, restart the WireGuard interface on the server and the client. The server does not know how to connect to the client, so the client should sent a packet first.
When restarting the interface, here on the client, we can see that WireGuard added a route:
stanislas@mbp ~> wg-quick up wg0
...
[#] route -q -n add -inet 10.66.66.1/32 -interface utun1
...
stanislas@mbp ~> ip r | grep utun1
10.66.66.1/32 via utun1 dev utun1
We can see the new peer:
stanislas@mbp ~> sudo wg show
interface: utun1
public key: <client public key>
private key: (hidden)
listening port: 52926
peer: <server public key>
endpoint: 95.179.208.197:53
allowed ips: 10.66.66.1/32, fd42:42:42::1/128
Let’s try connecting to it:
stanislas@mbp ~> ping -c 4 10.66.66.1
PING 10.66.66.1 (10.66.66.1): 56 data bytes
64 bytes from 10.66.66.1: icmp_seq=0 ttl=64 time=21.291 ms
64 bytes from 10.66.66.1: icmp_seq=1 ttl=64 time=12.305 ms
64 bytes from 10.66.66.1: icmp_seq=2 ttl=64 time=10.954 ms
64 bytes from 10.66.66.1: icmp_seq=3 ttl=64 time=12.284 ms
--- 10.66.66.1 ping statistics ---
4 packets transmitted, 4 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 10.954/14.209/21.291/4.126 ms
stanislas@mbp ~> ping6 -c 4 fd42:42:42::1
PING6(56=40+8+8 bytes) fd42:42:42::2 --> fd42:42:42::1
16 bytes from fd42:42:42::1, icmp_seq=0 hlim=64 time=10.428 ms
16 bytes from fd42:42:42::1, icmp_seq=1 hlim=64 time=10.541 ms
16 bytes from fd42:42:42::1, icmp_seq=2 hlim=64 time=10.626 ms
16 bytes from fd42:42:42::1, icmp_seq=3 hlim=64 time=10.843 ms
--- fd42:42:42::1 ping6 statistics ---
4 packets transmitted, 4 packets received, 0.0% packet loss
round-trip min/avg/max/std-dev = 10.428/10.610/10.843/0.152 ms
Success!
On the server, you should see that data has been transmitted, and you should also see that a dynamic endpoint is shown:
root@wg ~# wg
interface: wg0
public key: <server public key>
private key: (hidden)
listening port: 53
peer: <client public key>
endpoint: <client's router public IPv4>:52926
allowed ips: 10.66.66.2/32, fd42:42:42::2/128
latest handshake: 2 minutes, 43 seconds ago
transfer: 1.05 KiB received, 988 B sent
The endpoint is the client’s public IP address (the router’s, if it is behing NAT), and, as we did not set a port nor an endpoint, a random port.
You can try to ping your client form the server, it should work (if the client’s firewall is not blocking incoming connections).
Now that our two peers can communicate, let’s make all of our client’s traffic go trough the server.
Forward the traffic of the client trough the server
Enable routing on the server
First we need to enable IPv4 and IPv6 routing on the server, so that it can forward packets.
echo "net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1" > /etc/sysctl.d/wg.conf
sysctl --system
Enable NAT on the server
We want to enable NAT between the server’s public interface (ens3
for me) and the wg0
interface.
For that, we need two iptables commands:
iptables -t nat -A POSTROUTING -o ens3 -j MASQUERADE
ip6tables -t nat -A POSTROUTING -o ens3 -j MASQUERADE
The good news is that WireGuard can execute these for us, when the interface is brought up. To keep things clean, we want to remove them when the interface is brought down, so here is what you need to add to your [Interface]
block on the server:
PostUp = iptables -t nat -A POSTROUTING -o ens3 -j MASQUERADE; ip6tables -t nat -A POSTROUTING -o ens3 -j MASQUERADE
PostDown = iptables -t nat -D POSTROUTING -o ens3 -j MASQUERADE; ip6tables -t nat -D POSTROUTING -o ens3 -j MASQUERADE
That’s it!
Make the server the client’s gateway
We can leverage the AllowedIPs
option to override the default route on the client.
Simply change the line to:
AllowedIPs = 0.0.0.0/0,::/0
Restart the interface. Done, all of your client’s packets are going trough the server!
Adding more clients
Adding more client is a bliss.
The third peer’s configuration file will look like this:
[Interface]
PrivateKey = <client 2 private key>
Address = 10.66.66.3/24,fd42:42:42::3/64
[Peer]
PublicKey = <server public key>
Endpoint = <server public IP>:1194
AllowedIPs = 0.0.0.0/0,::/0
On the server:
[Peer]
PublicKey = <client 2 public key>
AllowedIPs = 10.66.66.3/32,fd42:42:42::3/128
Note that the clients won’t have the other clients as peer since they don’t have valid initial endpoints (= a public IP address and open/forwarded port).
Tips and tricks
Verifying your connection
I usually use ipv6-test.com or ipleak.net to verify that my traffic is going trough the VPN, including IPv6.
Generate a public key from a private key
If you need to get the public key from a private key, you can pipe the private key to wg pubkey
like:
wg genkey | wg pubkey
To get a pair in two files :
wg genkey | tee privatekey | wg pubkey > publickey
Or in your terminal output:
private_key=$(wg genkey)
public_key=$(echo $private_key | wg pubkey)
echo "private key: $private_key"
echo "public key: $public_key"
IPv4, IP6, dual stack…?
Here, we use a dual stack VPN, and the peers connect via IPv4.
I prefer the endpoints to be IPv4 since sometimes I am on IPv4-only network but you could connect to your server via IPv6.
The privates addresses could also be IPv4 only or IPv6 only, but dual stack is the best!
Changing the client’s DNS resolvers
A little tip if you wan to change your client’s DNS resolvers upon connection. There are many reason to do this:
- With the new routes, your local network won’t be accessible. So if the DNS servers pushed by your DHCP server are in the local network, you’re screwed. (Or you add the correct route with
PostUp
on the client) - You want to use a private/self-hosted DNS server, like Pi-hole
- You want to use a specific DNS server on a platform where you can’t without a VPN, like Android
As for me, I currently put Adguard DNS everywhere. It’s especially useful on my Android phone where I don’t have an ad blocker.
To specify DNS servers, add the DNS
option to the client’s [Interface]
block:
[Interface]
...
DNS = 176.103.130.130,176.103.130.131
Bypassing blocked ports and filtered connections
WireGuard uses UDP. A well-known way to bypass blocked ports with OpenVPN is to use TCP on the port 443 to simulate HTTPS, but it’s slower.
On both OpenVPN and WireGuard, I usually connect to the port 53 via UDP, since DNS is never blocked (unless your network does DPI…).
Transferring a configuration file easily to the Android app
I mean it’s not that difficult to transfer a file from my computer to my Android phone, but there is an even better way.
On the Android App, you have 3 means to create an interface:
- Create from file or archive
- Create from a QR Code
- Create from scratch
It’s super easy to generate a QR Code on your computer using qrencode:
qrencode -t ansiutf8 < wireguard-android.conf
Scan the QR Code in your terminal with your phone, and you’re done.
Configuration overview
It’s been a long post, so let’s see how our configuration files look by now.
Peer 1 (server)
[Interface]
Address = 10.66.66.1/24,fd42:42:42::1/64
PostUp = iptables -t nat -A POSTROUTING -o ens3 -j MASQUERADE; ip6tables -t nat -A POSTROUTING -o ens3 -j MASQUERADE
PostDown = iptables -t nat -D POSTROUTING -o ens3 -j MASQUERADE; ip6tables -t nat -D POSTROUTING -o ens3 -j MASQUERADE
ListenPort = 53
PrivateKey = <server private key>
[Peer]
PublicKey = <client 1 public key>
AllowedIPs = 10.66.66.2/32, fd42:42:42::2/128
[Peer]
PublicKey = <client 2 public key>
AllowedIPs = 10.66.66.3/32, fd42:42:42::3/128
Peer 2 (client 1)
[Interface]
PrivateKey = <client 1 private key>
Address = 10.66.66.2/24,fd42:42:42::2/64
DNS = 176.103.130.130,176.103.130.131
[Peer]
PublicKey = <server public key>
Endpoint = <server public IP>:53
AllowedIPs = 0.0.0.0/0,::/0
Peer 3 (client 2)
[Interface]
PrivateKey = <client 1 private key>
Address = 10.66.66.3/24,fd42:42:42::3/64
DNS = 176.103.130.130,176.103.130.131
[Peer]
PublicKey = <server public key>
Endpoint = <server public IP>:53
AllowedIPs = 0.0.0.0/0,::/0
Conclusion
WireGuard is super awesome and easy to setup.
Thanks to this, I can connect safely (encryption) from nearly anywhere (port 53), get IPv6 connection (dual-stack) while blocking ads (AdGuard) and having great speeds!
WireGuard is still being actively developed, and has received lots of support and donations. I have been using it for months to connect servers to each other (blog post incoming), and I never had any issue.
Enjoy!