WireGuard: fast, modern, secure VPN tunnel. WireGuard securely encapsulates IP packets over UDP.
Goal
What I would like to achieve, in this article, is to provide a comprehensive guide for a redirect-gateway vpn using wireguard with a twist. The client machine should reach internet through the wireguard vpn server. No other communications should be allowed from the client and that means if we drop the VPN connection, client can not go to the internet.
Intro - Lab Details
Here are my lab details. This blog post will help you understand all the necessary steps and provide you with a guide to replicate the setup. You should be able to create a wireguard VPN server-client between two points. I will be using ubuntu 20.04 as base images for both virtual machines. I am also using LibVirt and Qemu/KVM running on my archlinux host.
Wireguard Generate Keys
and the importance of them!
Before we begin, give me a moment to try explaining how the encryption between these two machines, in a high level design works.
Each linux machines creates a pair of keys.
- Private Key
- Public Key
These keys have a unique relationship. You can use your public key to encrypt something but only your private key can decrypt it. That mean, you can share your public keys via an cleartext channel (internet, email, slack). The other parties can use your public key to encrypt traffic towards you, but none other decrypt it. If a malicious actor replace the public keys, you can not decrypt any incoming traffic thus make it impossible to connect to VPN server!
Public - Private Keys
Now each party has the other’s public keys and they can encrypt traffic for each other, BUT only you can decrypt your traffic with your private key. Your private key never leaves your computer.
Hopefully you get the idea.
Wireguard Server - Ubuntu 20.04 LTS
In order to make the guide easy to follow, I will start with the server part.
Server Key Generation
Generate a random private and public key for the server as-root
wg genkey | tee /etc/wireguard/privatekey | wg pubkey > /etc/wireguard/publickey
Make the keys read-only
chmod 0400 /etc/wireguard/*key
List keys
ls -l /etc/wireguard/
total 8
-r-------- 1 root root 45 Jul 12 20:29 privatekey
-r-------- 1 root root 45 Jul 12 20:29 publickey
UDP Port & firewall
In order to run a VPN service, you need to choose a random port that wireguard VPN server can listen to it.
eg.
61194
open firewall
ufw allow 61194/udp
Rule added
Rule added (v6)
view more info
ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip
To Action From
-- ------ ----
22/tcp ALLOW IN Anywhere
61194 ALLOW IN Anywhere
22/tcp (v6) ALLOW IN Anywhere (v6)
61194 (v6) ALLOW IN Anywhere (v6)
So you see, only SSH
and our VPN
ports are open.
Default Incoming Policy is: DENY.
Wireguard Server Configuration File
Now it is time to create a basic configuration wireguard-server file.
Naming the wireguard interface
Clarification the name of our interface does not matter, you can choose any name. Gets it’s name from the configuration filename! But in order to follow the majority of guides try to use something from wg+
. For me it is easier to name wireguard interface:
- wg0
but you may seen it also as
- wg
without a number. All good!
Making a shell script for wireguard server configuration file
Running the below script will create a new configuration file /etc/wireguard/wg0.conf
PRIVATE_KEY=$(cat /etc/wireguard/privatekey)
NETWORK=10.0.8
cat > /etc/wireguard/wg0.conf <<EOF
[Interface]
Address = ${NETWORK}.1/24
ListenPort = 61194
PrivateKey = ${PRIVATE_KEY}
EOF
ls -l /etc/wireguard/wg0.conf
cat /etc/wireguard/wg0.conf
Note I have chosen the network 10.0.8.0/24
for my VPN setup, you can choose any Private Network
10.0.0.0/8 - Class A
172.16.0.0/12 - Class B
192.168.0.0/16 - Class C
I chose a Class C (256 IPs) from a /8
(Class A) private network, do not be confused about this. It’s just a Class-C /24
private network.
Let’s make our first test with the server
wg-quick up wg0
[#] ip link add wg0 type wireguard
[#] wg setconf wg0 /dev/fd/63
[#] ip -4 address add 10.0.8.1/24 dev wg0
[#] ip link set mtu 1420 up dev wg0
It’s Alive!
Kernel Module
Verify that wireguard is loaded as a kernel module on your system.
lsmod | grep wireguard
wireguard 212992 0
ip6_udp_tunnel 16384 1 wireguard
udp_tunnel 16384 1 wireguard
Wireguard is in the Linux kernel since March 2020.
Show IP address
ip address show dev wg0
3: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420
qdisc noqueue state UNKNOWN group default qlen 1000
link/none
inet 10.0.8.1/24 scope global wg0
valid_lft forever preferred_lft forever
Listening Connections
Verify that wireguard server listens to our specific UDP port
ss -nulp '( sport = :61194 )' | column -t
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
UNCONN 0 0 0.0.0.0:61194 0.0.0.0:*
UNCONN 0 0 [::]:61194 [::]:*
Show wg0
wg show wg0
interface: wg0
public key: <public key>
private key: (hidden)
listening port: 61194
Show Config
wg showconf wg0
[Interface]
ListenPort = 61194
PrivateKey = <private key>
Close wireguard
wg-quick down wg0
What is IP forwarding
In order for our VPN server to forward our client’s network traffic from the internal interface wg0 to it’s external interface and on top of that, to separate it’s own traffic from client’s traffic, the vpn server needs to masquerade all client’s traffic in a way that knows who to forward and send back traffic to the client. In a nuthshell this is IP forwarding and perhaps the below diagram can explain it a little better.
To do that, we need to enable the IP forward feature on our linux kernel.
sysctl -w net.ipv4.ip_forward=1
The above command does not persist the change across reboots. To persist this setting we need to add it, to sysctl configuration. And although many set this option global, we do not need to have to do that!
Wireguard provides four (4) stages, to run our own scripts.
Wireguard stages
- PreUp
- PostUp
- PreDown
- PostDown
So we can choose to enable IP forwarding at wireguard up and disable it on wireguard down.
IP forwarding in wireguard configuration file
PRIVATE_KEY=$(cat /etc/wireguard/privatekey)
NETWORK=10.0.8
cat > /etc/wireguard/wg0.conf <<EOF
[Interface]
Address = ${NETWORK}.1/24
ListenPort = 61194
PrivateKey = ${PRIVATE_KEY}
PreUp = sysctl -w net.ipv4.ip_forward=1
PostDown = sysctl -w net.ipv4.ip_forward=0
EOF
verify above configuration
wg-quick up wg0
[#] sysctl -w net.ipv4.ip_forward=1
net.ipv4.ip_forward = 1
[#] ip link add wg0 type wireguard
[#] wg setconf wg0 /dev/fd/63
[#] ip -4 address add 10.0.8.1/24 dev wg0
[#] ip link set mtu 1420 up dev wg0
Verify system control setting for IP forward:
sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1
Close wireguard
wg-quick down wg0
[#] wg showconf wg0
[#] ip link delete dev wg0
[#] sysctl -w net.ipv4.ip_forward=0
net.ipv4.ip_forward = 0
Verify that now IP Forward is not enabled.
sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 0
Routing via firewall rules
Next on our agenda, is to add some additional rules on our firewall. We already enabled IP Forward feature with the above and now it is time to update our firewall so it can masquerade our client’s network traffic.
Default Route
We need to identify the default external network interface on the ubuntu server.
DEF_ROUTE=$(ip -4 -json route list default | jq -r .[0].dev)
usually is eth0
or ensp3
or something similar.
firewall rules
- We need to forward traffic from the internal interface to the external interface
- We need to masquerade the traffic of the client.
iptables -A FORWARD -i %i -o ${DEF_ROUTE} -s ${NETWORK}.0/24 -j ACCEPT
iptables -t nat -A POSTROUTING -s ${NETWORK}.0/24 -o ${DEF_ROUTE} -j MASQUERADE
and also we need to drop these rules when we stop wireguard service
iptables -D FORWARD -i %i -o ${DEF_ROUTE} -s ${NETWORK}.0/24 -j ACCEPT
iptables -t nat -D POSTROUTING -s ${NETWORK}.0/24 -o ${DEF_ROUTE} -j MASQUERADE
See the -D
after iptables.
iptables is a CLI (command line interface) for netfilters. So think the above rules as filters on our network traffic.
WG Server Conf
To put everything all together and also use wireguard PostUp
and PreDown
to
wg genkey | tee /etc/wireguard/privatekey | wg pubkey > /etc/wireguard/publickey
chmod 0400 /etc/wireguard/*key
PRIVATE_KEY=$(cat /etc/wireguard/privatekey)
NETWORK=10.0.8
DEF_ROUTE=$(ip -4 -json route list default | jq -r .[0].dev)
WG_PORT=61194
cat > /etc/wireguard/wg0.conf <<EOF
[Interface]
Address = ${NETWORK}.1/24
ListenPort = ${WG_PORT}
PrivateKey = ${PRIVATE_KEY}
PreUp = sysctl -w net.ipv4.ip_forward=1
PostDown = sysctl -w net.ipv4.ip_forward=0
PostUp = iptables -A FORWARD -i %i -o ${DEF_ROUTE} -s ${NETWORK}.0/24 -j ACCEPT ; iptables -t nat -A POSTROUTING -s ${NETWORK}.0/24 -o ${DEF_ROUTE} -j MASQUERADE
PreDown = iptables -D FORWARD -i %i -o ${DEF_ROUTE} -s ${NETWORK}.0/24 -j ACCEPT ; iptables -t nat -D POSTROUTING -s ${NETWORK}.0/24 -o ${DEF_ROUTE} -j MASQUERADE
EOF
testing up
wg-quick up wg0
[#] sysctl -w net.ipv4.ip_forward=1
net.ipv4.ip_forward = 1
[#] ip link add wg0 type wireguard
[#] wg setconf wg0 /dev/fd/63
[#] ip -4 address add 10.0.8.1/24 dev wg0
[#] ip link set mtu 1420 up dev wg0
[#] iptables -A FORWARD -i wg0 -o ens3 -s 10.0.8.0/24 -j ACCEPT ; iptables -t nat -A POSTROUTING -s 10.0.8.0/24 -o ens3 -j MASQUERADE
testing down
wg-quick down wg0
[#] iptables -D FORWARD -i wg0 -o ens3 -s 10.0.8.0/24 -j ACCEPT ; iptables -t nat -D POSTROUTING -s 10.0.8.0/24 -o ens3 -j MASQUERADE
[#] ip link delete dev wg0
[#] sysctl -w net.ipv4.ip_forward=0
net.ipv4.ip_forward = 0
Start at boot time
systemctl enable wg-quick@wg0
and status
systemctl status wg-quick@wg0
Get the wireguard server public key
We have NOT finished yet with the server !
Also we need the output of:
cat /etc/wireguard/publickey
output should be something like this:
wr3jUAs2qdQ1Oaxbs1aA0qrNjogY6uqDpLop54WtQI4=
Wireguard Client - Ubuntu 20.04 LTS
Now we need to move to our client virtual machine.
It is a lot easier but similar with above commands.
Client Key Generation
Generate a random private and public key for the client
wg genkey | tee /etc/wireguard/privatekey | wg pubkey > /etc/wireguard/publickey
Make the keys read-only
chmod 0400 /etc/wireguard/*key
Wireguard Client Configuration File
Need to replace <wireguard server public key>
with the output of the above command.
Configuration is similar but simpler from the server
PRIVATE_KEY=$(cat /etc/wireguard/privatekey)
NETWORK=10.0.8
WG_PORT=61194
WGS_IP=$(ip -4 -json route list default | jq -r .[0].prefsrc)
WSG_PUBLIC_KEY="<wireguard server public key>"
cat > /etc/wireguard/wg0.conf <<EOF
[Interface]
Address = ${NETWORK}.2/24
PrivateKey = ${PRIVATE_KEY}
[Peer]
PublicKey = ${WSG_PUBLIC_KEY}
Endpoint = ${WGS_IP}:${WG_PORT}
AllowedIPs = 0.0.0.0/0
EOF
verify
wg-quick up wg0
[#] ip link add wg0 type wireguard
[#] wg setconf wg0 /dev/fd/63
[#] ip -4 address add 10.0.8.2/24 dev wg0
[#] ip link set mtu 1420 up dev wg0
[#] wg set wg0 fwmark 51820
[#] ip -4 route add 0.0.0.0/0 dev wg0 table 51820
[#] ip -4 rule add not fwmark 51820 table 51820
[#] ip -4 rule add table main suppress_prefixlength 0
[#] sysctl -q net.ipv4.conf.all.src_valid_mark=1
[#] iptables-restore -n
start client at boot
systemctl enable wg-quick@wg0
Get the wireguard client public key
We have finished with the client, but we need the output of:
cat /etc/wireguard/publickey
Wireguard Server - Peers
As we mentioned above, we need to exchange public keys of server & client to the other machines in order to encrypt network traffic.
So as we get the client public key and run the below script to the server
WSC_PUBLIC_KEY="<wireguard client public key>"
NETWORK=10.0.8
wg set wg0 peer ${WSC_PUBLIC_KEY} allowed-ips ${NETWORK}.2
after that we can verify that our wireguard server and connect to the wireguard client
wireguard server ping to client
$ ping -c 5 10.0.8.2
PING 10.0.8.2 (10.0.8.2) 56(84) bytes of data.
64 bytes from 10.0.8.2: icmp_seq=1 ttl=64 time=0.714 ms
64 bytes from 10.0.8.2: icmp_seq=2 ttl=64 time=0.456 ms
64 bytes from 10.0.8.2: icmp_seq=3 ttl=64 time=0.557 ms
64 bytes from 10.0.8.2: icmp_seq=4 ttl=64 time=0.620 ms
64 bytes from 10.0.8.2: icmp_seq=5 ttl=64 time=0.563 ms
--- 10.0.8.2 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 4103ms
rtt min/avg/max/mdev = 0.456/0.582/0.714/0.084 ms
wireguard client ping to server
$ ping -c 5 10.0.8.1
PING 10.0.8.1 (10.0.8.1) 56(84) bytes of data.
64 bytes from 10.0.8.1: icmp_seq=1 ttl=64 time=0.752 ms
64 bytes from 10.0.8.1: icmp_seq=2 ttl=64 time=0.639 ms
64 bytes from 10.0.8.1: icmp_seq=3 ttl=64 time=0.622 ms
64 bytes from 10.0.8.1: icmp_seq=4 ttl=64 time=0.625 ms
64 bytes from 10.0.8.1: icmp_seq=5 ttl=64 time=0.597 ms
--- 10.0.8.1 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 4093ms
rtt min/avg/max/mdev = 0.597/0.647/0.752/0.054 ms
wireguard server - Peers at configuration file
Now the final things is to update our wireguard server with the client Peer (or peers).
wg showconf wg0
the output should be something like that:
[Interface]
ListenPort = 61194
PrivateKey = <server: private key>
[Peer]
PublicKey = <client: public key>
AllowedIPs = 10.0.8.2/32
Endpoint = 192.168.122.68:52587
We need to append the peer section to our configuration file.
so /etc/wireguard/wg0.conf
should look like this:
[Interface]
Address = 10.0.8.1/24
ListenPort = 61194
PrivateKey = <server: private key>
PreUp = sysctl -w net.ipv4.ip_forward=1
PostDown = sysctl -w net.ipv4.ip_forward=0
PostUp = iptables -A FORWARD -i %i -o ens3 -s 10.0.8.0/24 -j ACCEPT ; iptables -t nat -A POSTROUTING -s 10.0.8.0/24 -o ens3 -j MASQUERADE
PreDown = iptables -D FORWARD -i %i -o ens3 -s 10.0.8.0/24 -j ACCEPT ; iptables -t nat -D POSTROUTING -s 10.0.8.0/24 -o ens3 -j MASQUERADE
[Peer]
PublicKey = <client: public key>
AllowedIPs = 10.0.8.2/32
Endpoint = 192.168.122.68:52587
SaveConfig
In None of server or client wireguard configuration file, we didn’t declare this option. If set, then the configuration is saved on the current state of wg0 interface. Very useful !
SaveConfig = true
client firewall
It is time to introduce our twist!
If you mtr or traceroute our traffic from our client, we will notice that in need our network traffic goes through the wireguard vpn server !
My traceroute [v0.93]
wgc (10.0.8.2) 2021-07-21T22:41:44+0300
Packets Pings
Host Loss% Snt Last Avg Best Wrst StDev
1. 10.0.8.1 0.0% 88 0.6 0.6 0.4 0.8 0.1
2. myhomepc 0.0% 88 0.8 0.8 0.5 1.0 0.1
3. _gateway 0.0% 88 3.8 4.0 3.3 9.6 0.8
...
The first entry is:
1. 10.0.8.1
if we drop our vpn connection.
wg-quick down wg0
We still go to the internet.
My traceroute [v0.93]
wgc (192.168.122.68) 2021-07-21T22:45:04+0300
Packets Pings
Host Loss% Snt Last Avg Best Wrst StDev
1. myhomepc 0.0% 1 0.3 0.3 0.3 0.3 0.0
2. _gateway 0.0% 1 3.3 3.3 3.3 3.3 0.0
This is important because in some use cases, we do not want our client to directly or unsupervised talk to the internet.
UFW alternative for the client
So to avoid this issue, we will re-rewrite our firewall rules.
A simple script to do that is the below. Declares the default policy to DENY for everything and only accepts ssh incoming traffic and outgoing traffic through the vpn.
DEF_ROUTE=$(ip -4 -json route list default | jq -r .[0].dev)
WGS_IP=192.168.122.69
WG_PORT=61194
## reset
ufw --force reset
## deny everything!
ufw default deny incoming
ufw default deny outgoing
ufw default deny forward
## allow ssh
ufw allow 22/tcp
## enable
ufw --force enable
## allow traffic out to the vpn server
ufw allow out on ${DEF_ROUTE} to ${WGS_IP} port ${WG_PORT}
## allow tunnel traffic out
ufw allow out on wg+
# ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), deny (outgoing), disabled (routed)
New profiles: skip
To Action From
-- ------ ----
22/tcp ALLOW IN Anywhere
22/tcp (v6) ALLOW IN Anywhere (v6)
192.168.122.69 61194 ALLOW OUT Anywhere on ens3
Anywhere ALLOW OUT Anywhere on wg+
Anywhere (v6) ALLOW OUT Anywhere (v6) on wg+
DNS
There is caveat. Please bare with me.
Usually and especially in virtual machines they get DNS setting through a local LAN. We can either allow traffic on the local vlan or update our local DNS setting so it can go only through our VPN
cat > /etc/systemd/resolved.conf <<EOF
[Resolve]
DNS=88.198.92.222
EOF
systemctl restart systemd-resolved
let’s try this
# ping google.com
ping: google.com: Temporary failure in name resolution
Correct, fire up wireguard vpn client
wg-quick up wg0
ping -c4 google.com
PING google.com (142.250.185.238) 56(84) bytes of data.
64 bytes from fra16s53-in-f14.1e100.net (142.250.185.238): icmp_seq=1 ttl=115 time=43.5 ms
64 bytes from fra16s53-in-f14.1e100.net (142.250.185.238): icmp_seq=2 ttl=115 time=44.9 ms
64 bytes from fra16s53-in-f14.1e100.net (142.250.185.238): icmp_seq=3 ttl=115 time=43.8 ms
64 bytes from fra16s53-in-f14.1e100.net (142.250.185.238): icmp_seq=4 ttl=115 time=43.0 ms
--- google.com ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3004ms
rtt min/avg/max/mdev = 42.990/43.795/44.923/0.707 ms
mtr shows:
mtr google.com
My traceroute [v0.93]
wgc (10.0.8.2) 2021-07-21T23:01:16+0300
Packets Pings
Host Loss% Snt Last Avg Best Wrst StDev
1. 10.0.8.1 0.0% 2 0.6 0.7 0.6 0.7 0.1
2. _gateway 0.0% 1 0.8 0.8 0.8 0.8 0.0
3. 192.168.1.1 0.0% 1 3.8 3.8 3.8 3.8 0.0
drop vpn connection
# wg-quick down wg0
[#] ip -4 rule delete table 51820
[#] ip -4 rule delete table main suppress_prefixlength 0
[#] ip link delete dev wg0
[#] iptables-restore -n
# ping -c4 google.com
ping: google.com: Temporary failure in name resolution
and that is perfect !
No internet access for our client. Only when our vpn connection is up!
WireGuard: fast, modern, secure VPN tunnel
That’s it!
Below my personal settings -as of today- for LibreDNS using systemd-resolved service for DNS resolution.
sudo vim /etc/systemd/resolved.conf
basic settings
[Resolve]
DNS=116.202.176.26:854#dot.libredns.gr
DNSOverTLS=yes
FallbackDNS=88.198.92.222
Cache=yes
apply
sudo systemctl restart systemd-resolved.service
verify
resolvectl query analytics.google.com
analytics.google.com: 0.0.0.0 -- link: eth0
-- Information acquired via protocol DNS in 144.7ms.
-- Data is authenticated: no; Data was acquired via local or encrypted transport: yes
-- Data from: network
Explain Settings
DNS setting
DNS=116.202.176.26:854#dot.libredns.gr
We declare the IP of our DoT service. Using : as a separator we add the no-ads TCP port of DoT, 854. We also need to add our domain in the end to tell systemd-resolved that this IP should respond to dot.libredns.gr
Dns Over TLS
DNSOverTLS=yes
The usually setting is yes. In older systemd versions you can also select opportunistic.
As we are using Lets Encrypt systemd-resolved can not verify (by default) the IP inside the certificate. The type of certificate can verify the domain dot.libredns.gr
but we are asking the IP: 116.202.176.26 and this is another type of certificate that is not free. In order to “fix” this , we added the #dot.libredns.gr
in the above setting.
FallBack
Yes not everything has Five nines so you may need a fall back dns to .. fall. Be aware this is cleartext traffic! Not encrypted.
FallbackDNS=88.198.92.222
Cache
Last but not least, caching your queries can give provide you with an additional speed when browsing the internet ! You already asked this a few seconds ago, why not caching it on your local system?
Cache=yes
to give you an example
resolvectl query analytics.google.com
analytics.google.com: 0.0.0.0 -- link: eth0
-- Information acquired via protocol DNS in 144.7ms.
-- Data is authenticated: no; Data was acquired via local or encrypted transport: yes
-- Data from: network
second time:
resolvectl query analytics.google.com
analytics.google.com: 0.0.0.0 -- link: eth0
-- Information acquired via protocol DNS in 2.3ms.
-- Data is authenticated: no; Data was acquired via local or encrypted transport: yes
-- Data from: cache