Fast, Modern, Secure VPN Tunnel

I've been following up WireGuard very closely for quite some years now.

Recently it appeared on the radar of more people after it received some love from none other than Linus Torvalds himself: Linux Kernel Mailing List

On Wed, Aug 1, 2018 at 9:37 PM David Miller davem@xxxxxxxxxxxxx wrote:

Fixes keep trickling in:

Pulled.

Btw, on an unrelated issue: I see that Jason actually made the pull request to have wireguard included in the kernel.

Can I just once again state my love for it and hope it gets merged soon? Maybe the code isn't perfect, but I've skimmed it, and compared to the horrors that are OpenVPN and IPSec, it's a work of art.

Linus

So what is WireGuard anyway?

WireGuard aims to be a fast, secure and modern VPN solution, competing directly with the likes of IPSec and OpenVPN.

With its low footprint, it's small enough to run even on embedded devices, and makes it a lot easier to audit.

One thing I absolutely agree with, is that it's way easier to setup and configure than IPSec and OpenVPN.

I've spent a night setting it up again, performing some tests, and then comparing it to Libreswan's IPSec implementation.

Specs

For my tests I ran 2 Centos 7.5.1804 Digital Ocean containers in New York and in Amsterdam.

They both use the lowest specs as I only needed enough resources to perform network tests.

Installing the software

I first updated both containers and rebooted them in order to have all the latest updates.

$ sudo yum update -y && reboot

Then it was time to install the actual software.

$ sudo curl -Lo /etc/yum.repos.d/wireguard.repo https://copr.fedorainfracloud.org/coprs/jdoss/wireguard/repo/epel-7/jdoss-wireguard-epel-7.repo
$ sudo yum install epel-release
$ sudo yum install wireguard-dkms wireguard-tools

Please note that since WireGuard hasn't been included in the mainline kernel yet, WireGuard is complied as a kernel module and loaded through DKMS.

Network overview

These are the current network devices present on the 2 Centos droplets:

[root@wg01-ny ~]# ip a s dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 4a:e1:5d:8f:e2:bb brd ff:ff:ff:ff:ff:ff
inet 159.65.254.75/20 brd 159.65.255.255 scope global eth0
valid_lft forever preferred_lft forever
inet 10.17.0.5/16 brd 10.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::48e1:5dff:fe8f:e2bb/64 scope link
valid_lft forever preferred_lft forever

[root@wg01-ams ~]# ip a s dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 46:22:eb:9c:e5:b2 brd ff:ff:ff:ff:ff:ff
inet 142.93.143.85/20 brd 142.93.143.255 scope global eth0
valid_lft forever preferred_lft forever
inet 10.18.0.5/16 brd 10.18.255.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::4422:ebff:fe9c:e5b2/64 scope link
valid_lft forever preferred_lft forever

Configuring WireGuard

Wireguard uses a simple but effective commandline tool called wg

When invoked without options, it simply shows the currently loaded configuration. Since we haven't configured anything at this point, nothing will be shown.

[root@wg01-ny ~]# wg
[root@wg01-ny ~]#

Setting up the secrets

WireGuard can generate a base64-encoded private/public key.

When just invoking the command with the option, it will just keep on generating new pairs. You may want to actually copy it to a file for later use.

[root@wg01-ny ~]# wg genkey
APLNPxerJO8QAQea8oOE2xFNrmqjuEq7HgNDCY3xU04=
[root@wg01-ny ~]# wg genkey > privatekey

You can even get the public key from the private key like so:

[root@wg01-ny ~]# wg pubkey < privatekey
p1ceMyQI3NoT9bdwKRTZdXXe1xaJ2Ndy0cHZMsKlJQk=

Again, might be a smart thing to actually save that to a file, although it's not really needed.

[root@wg01-ny ~]# wg pubkey < privatekey > publickey

Configuring the network interface

First up, lets create the WireGuard network interface

[root@wg01-ny ~]# ip link add dev wg0 type wireguard
[root@wg01-ny ~]# wg
interface: wg0

[root@wg01-ams ~]# ip link add dev wg0 type wireguard
[root@wg01-ams ~]# wg
interface: wg0

As we can see, they have been created, but the configuration is empty.

We can add IP configuration just like any regular network interface!

[root@wg01-ny ~]# ip a a dev wg0 192.168.50.1/24
[root@wg01-ams ~]# ip a a dev wg0 192.168.50.2/24

Now we need to specify which port we want our network interface to listen to. This is the equivalent of configuring the listener/server part.

WireGuard will use UDP for communication.
Note that the port will only be opened when the network interface is actually UP.

[root@wg01-ny ~]# wg set wg0 listen-port 51820
[root@wg01-ams ~]# wg set wg0 listen-port 51820

Time to attach the previously generated privatekey to our wg0 interface.

[root@wg01-ny ~]# wg set wg0 private-key /root/privatekey
[root@wg01-ny ~]# wg
interface: wg0
public key: p1ceMyQI3NoT9bdwKRTZdXXe1xaJ2Ndy0cHZMsKlJQk=
private key: (hidden)
listening port: 51820

[root@wg01-ams ~]# wg set wg0 private-key /root/privatekey
[root@wg01-ams ~]# wg
interface: wg0
public key: jZl2dkBcnAEER/SRoQwufTu4BTW0fY6JFTM5hpj76QY=
private key: (hidden)
listening port: 51820

We can now bring the interfaces up.

[root@wg01-ny ~]# ip link set wg0 up
[root@wg01-ny ~]# ip a show wg0
3: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
link/none
inet 192.168.50.1/24 scope global wg0
valid_lft forever preferred_lft forever
inet6 fe80::1e1a:603a:951e:d508/64 scope link flags 800
valid_lft forever preferred_lft forever

[root@wg01-ams ~]# ip link set wg0 up
[root@wg01-ams ~]# ip a s dev wg0
3: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
link/none
inet 192.168.50.2/24 scope global wg0
valid_lft forever preferred_lft forever
inet6 fe80::53a3:d95d:a3fd:b530/64 scope link flags 800
valid_lft forever preferred_lft forever

Once they are up, it's time to configure the peering.
You need to associate a peer (publickey of the machine connecting) and the allowed IPs that can be transmitted from it.

Note that specifying the endpoint is not necessary, this is handy for road-warrior setups as WireGuard allows you to connect from anywhere as long as the key is the same and you are using an allowed internal IPaddress.

[root@wg01-ny ~]# wg set wg0 peer jZl2dkBcnAEER/SRoQwufTu4BTW0fY6JFTM5hpj76QY= allowed-ips 192.168.50.0/24 endpoint 142.93.143.85:51820
[root@wg01-ny ~]# wg
interface: wg0
public key: p1ceMyQI3NoT9bdwKRTZdXXe1xaJ2Ndy0cHZMsKlJQk=
private key: (hidden)
listening port: 51820

peer: jZl2dkBcnAEER/SRoQwufTu4BTW0fY6JFTM5hpj76QY=
endpoint: 142.93.143.85:51820
allowed ips: 192.168.50.0/24

[root@wg01-ams ~]# wg
interface: wg0
public key: jZl2dkBcnAEER/SRoQwufTu4BTW0fY6JFTM5hpj76QY=
private key: (hidden)
listening port: 51820

peer: p1ceMyQI3NoT9bdwKRTZdXXe1xaJ2Ndy0cHZMsKlJQk=
endpoint: 159.65.254.75:51820
allowed ips: 192.168.50.0/24

Testing

And we're done!

Here are some test I made:

Ping

WAN to WAN

[root@wg01-ny ~]# ping -c 5 142.93.143.85
PING 142.93.143.85 (142.93.143.85) 56(84) bytes of data.
64 bytes from 142.93.143.85: icmp_seq=1 ttl=58 time=81.0 ms
64 bytes from 142.93.143.85: icmp_seq=2 ttl=58 time=79.9 ms
64 bytes from 142.93.143.85: icmp_seq=3 ttl=58 time=79.8 ms
64 bytes from 142.93.143.85: icmp_seq=4 ttl=58 time=79.8 ms
64 bytes from 142.93.143.85: icmp_seq=5 ttl=58 time=79.7 ms

--- 142.93.143.85 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 4006ms
rtt min/avg/max/mdev = 79.791/80.089/81.032/0.566 ms

Through the tunnel

[root@wg01-ny ~]# ping -c 5 192.168.50.2
PING 192.168.50.2 (192.168.50.2) 56(84) bytes of data.
64 bytes from 192.168.50.2: icmp_seq=1 ttl=64 time=80.0 ms
64 bytes from 192.168.50.2: icmp_seq=2 ttl=64 time=80.1 ms
64 bytes from 192.168.50.2: icmp_seq=3 ttl=64 time=80.1 ms
64 bytes from 192.168.50.2: icmp_seq=4 ttl=64 time=80.3 ms
64 bytes from 192.168.50.2: icmp_seq=5 ttl=64 time=80.1 ms

--- 192.168.50.2 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 4005ms
rtt min/avg/max/mdev = 80.079/80.165/80.317/0.197 ms

As we can see, the ping lag isn't really noticeable.

IPERF

I ran iperf to test the speed on both peers, directly, and then through the tunnel.

[root@wg01-ams ~]# iperf -s

Server listening on TCP port 5001
TCP window size: 85.3 KByte (default)

[ 4] local 142.93.143.85 port 5001 connected with 159.65.254.75 port 48562
[ ID] Interval Transfer Bandwidth
[ 4] 0.0-10.0 sec 341 MBytes 286 Mbits/sec
[ 4] local 142.93.143.85 port 5001 connected with 159.65.254.75 port 48564
[ 4] 0.0-10.0 sec 348 MBytes 291 Mbits/sec
[ 4] local 192.168.50.2 port 5001 connected with 192.168.50.1 port 44420
[ 4] 0.0-10.1 sec 145 MBytes 120 Mbits/sec
[ 4] local 192.168.50.2 port 5001 connected with 192.168.50.1 port 44422
[ 4] 0.0-10.2 sec 131 MBytes 107 Mbits/sec

Here we see that going through the tunnel cuts the bandwidth almost more than half. This isn't very surprising. Later we'll compare to IPSec.

Interface Status

One of the things I like about WireGuard is the nice traffic overview:

[root@wg01-ny ~]# wg
interface: wg0
public key: p1ceMyQI3NoT9bdwKRTZdXXe1xaJ2Ndy0cHZMsKlJQk=
private key: (hidden)
listening port: 51820

peer: jZl2dkBcnAEER/SRoQwufTu4BTW0fY6JFTM5hpj76QY=
endpoint: 142.93.143.85:51820
allowed ips: 192.168.50.0/24
latest handshake: 4 minutes, 51 seconds ago
transfer: 18.22 MiB received, 590.49 MiB sent

[root@wg01-ams ~]# wg
interface: wg0
public key: jZl2dkBcnAEER/SRoQwufTu4BTW0fY6JFTM5hpj76QY=
private key: (hidden)
listening port: 51820

peer: p1ceMyQI3NoT9bdwKRTZdXXe1xaJ2Ndy0cHZMsKlJQk=
endpoint: 159.65.254.75:51820
allowed ips: 192.168.50.0/24
latest handshake: 5 minutes, 4 seconds ago
transfer: 590.23 MiB received, 18.22 MiB sent

Saving and restoring configuration

So far we configured our WireGuard adhoc.
Saving our current configuration and reloading it is very trivial.

Show the current config

[root@wg01-ny ~]# wg showconf wg0
[Interface]
ListenPort = 51820
PrivateKey = KIVBXnjqn4nNCxenTs7DLeGHjtDxCPDJDONyF9oX3G0=

[Peer]
PublicKey = jZl2dkBcnAEER/SRoQwufTu4BTW0fY6JFTM5hpj76QY=
AllowedIPs = 192.168.50.0/24
Endpoint = 142.93.143.85:51820

Save current config

[root@wg01-ny ~]# wg showconf wg0 > wg0.conf

Load config

[root@wg01-ny ~]# wg setconf wg0 ./wg0.conf

Networking

Because your WireGuard interface looks like any other regular network interface, you can do a whole lot of cool things with it.
But if you like to have things simple like me, one of the nicest things about it is that it appears as a simple route in Linux. No dark and confusing xfrm states like in IPSEC.

[root@wg01-ny ~]# ip route
default via 159.65.240.1 dev eth0
10.17.0.0/16 dev eth0 proto kernel scope link src 10.17.0.5
159.65.240.0/20 dev eth0 proto kernel scope link src 159.65.254.75
192.168.50.0/24 dev wg0 proto kernel scope link src 192.168.50.1

Process

You won't notice much of WireGuard except for an entry per created interface in the processlist:

[root@wg01-ny ~]# ps aux | grep [w]g
root 2818 0.0 0.0 0 0 ? S< 18:13 0:00 [wg-crypt-wg0]

Comparing to IPSEC

IPSec is way more complicated. You need to know the various Phases (IKE), the different modes, know what a Security Association is. Not to mention it will challenge your sanity if something goes wrong and you need to start troubleshooting. Oh yeah, and it uses a TCP and UDP port.

In order to compare speeds, I've kept the configuration as dead simple as possible.

Installting LibreSwan

yum install libreswan

Configure the secrets store

[root@wg01-ny ~]# ipsec newhostkey --output /etc/ipsec.secrets
[root@wg01-ny ~]# ipsec initnss --nssdir /etc/ipsec.d
Initializing NSS database
[root@wg01-ny ~]#

Configuring the tunnel on both ends

[root@wg01-ny ~]# cat /etc/ipsec.d/ny-ams.conf
conn ny-ams
left=159.65.254.75
right=142.93.143.85
authby=secret
# use auto=start when done testing the tunnel
auto=add
[root@wg01-ny ~]# cat /etc/ipsec.d/ny-ams.secrets
159.65.254.75 142.93.143.85 : PSK "SndKlP7VQC+tyYQNpuTBsSfPekuRUAYO23KY8DNGuOGvRQMti4AtTvwzh/kbiAVX"

[root@wg01-ams ~]# cat /etc/ipsec.d/ams-ny.conf
conn ams-ny
left=142.93.143.85
right=159.65.254.75
authby=secret
# use auto=start when done testing the tunnel
auto=add

[root@wg01-ams ~]# cat /etc/ipsec.d/ams-ny.secrets
142.93.143.85 159.65.254.75 : PSK "SndKlP7VQC+tyYQNpuTBsSfPekuRUAYO23KY8DNGuOGvRQMti4AtTvwzh/kbiAVX"

Reloading secrets and configurations

[root@wg01-ny ~]# ipsec setup start
[root@wg01-ams ~]# ipsec setup start

[root@wg01-ny ~]# ipsec auto --rereadsecrets
002 forgetting secrets
002 loading secrets from "/etc/ipsec.secrets"
002 loading secrets from "/etc/ipsec.d/ny-ams.secrets"

[root@wg01-ams ~]# ipsec auto --rereadsecrets
002 forgetting secrets
002 loading secrets from "/etc/ipsec.secrets"
002 loading secrets from "/etc/ipsec.d/ams-ny.secrets"

[root@wg01-ny ~]# ipsec auto --add ny-ams
002 "ny-ams": deleting non-instance connection
002 added connection description "ny-ams"

[root@wg01-ams ~]# ipsec auto --add ams-ny
002 "ams-ny": deleting non-instance connection
002 added connection description "ams-ny"

Starting the tunnels

[root@wg01-ny ~]# ipsec auto --up ny-ams
002 "ny-ams" #1: initiating Main Mode
104 "ny-ams" #1: STATE_MAIN_I1: initiate
106 "ny-ams" #1: STATE_MAIN_I2: sent MI2, expecting MR2
108 "ny-ams" #1: STATE_MAIN_I3: sent MI3, expecting MR3
002 "ny-ams" #1: Peer ID is ID_IPV4_ADDR: '142.93.143.85'
004 "ny-ams" #1: STATE_MAIN_I4: ISAKMP SA established {auth=PRESHARED_KEY cipher=aes_256 integ=sha2_256 group=MODP2048}
002 "ny-ams" #2: initiating Quick Mode PSK+ENCRYPT+TUNNEL+PFS+UP+IKEV1_ALLOW+IKEV2_ALLOW+SAREF_TRACK+IKE_FRAG_ALLOW+ESN_NO {using isakmp#1 msgid:46ccb863 proposal=defaults pfsgroup=MODP2048}
117 "ny-ams" #2: STATE_QUICK_I1: initiate
004 "ny-ams" #2: STATE_QUICK_I2: sent QI2, IPsec SA established tunnel mode {ESP=>0xe96d5984 <0x41768443 xfrm=AES_CBC_128-HMAC_SHA1_96 NATOA=none NATD=none DPD=passive}

[root@wg01-ams ~]# ipsec auto --up ams-ny
002 "ams-ny" #3: initiating Quick Mode PSK+ENCRYPT+TUNNEL+PFS+UP+IKEV1_ALLOW+IKEV2_ALLOW+SAREF_TRACK+IKE_FRAG_ALLOW+ESN_NO {using isakmp#1 msgid:6861e091 proposal=defaults pfsgroup=MODP2048}
117 "ams-ny" #3: STATE_QUICK_I1: initiate
004 "ams-ny" #3: STATE_QUICK_I2: sent QI2, IPsec SA established tunnel mode {ESP=>0xbf1bbb5a <0xe1740c2c xfrm=AES_CBC_128-HMAC_SHA1_96 NATOA=none NATD=none DPD=passive}

Once we brought up the tunnel, traffic matching the IPSec policies will be routed through the tunnel.

Testing

Ping

[root@wg01-ny ~]# ping -c 5 142.93.143.85
PING 142.93.143.85 (142.93.143.85) 56(84) bytes of data.
64 bytes from 142.93.143.85: icmp_seq=1 ttl=64 time=81.4 ms
64 bytes from 142.93.143.85: icmp_seq=2 ttl=64 time=80.2 ms
64 bytes from 142.93.143.85: icmp_seq=3 ttl=64 time=80.1 ms
64 bytes from 142.93.143.85: icmp_seq=4 ttl=64 time=80.1 ms
64 bytes from 142.93.143.85: icmp_seq=5 ttl=64 time=80.2 ms

As we can see, ping latency is pretty much about the same.

IPERF

[root@wg01-ams ~]# iperf -s

Server listening on TCP port 5001
TCP window size: 85.3 KByte (default)

[ 4] local 142.93.143.85 port 5001 connected with 159.65.254.75 port 49166
[ ID] Interval Transfer Bandwidth
[ 4] 0.0-10.1 sec 125 MBytes 103 Mbits/sec
[ 4] local 142.93.143.85 port 5001 connected with 159.65.254.75 port 49168
[ 4] 0.0-10.1 sec 156 MBytes 130 Mbits/sec

Traffic routed through the tunnel also has about the same bandwidth drop as with WireGuard.

Your mileage may vary, however, according to WireGuard's own benchmarks, it outperforms IPSEC and OpenVPN in general and without optimizations.

Conclusion

I hope I sparked an interest in you to look into WireGuard yourselves and experiment with it.

WireGuard takes away the complicated parts of setting up secure tunnels. With the likely inclusion into the mainline kernel, and all the work to make it perform more optimally, I wouldn't be surprised if it would at some point become the de facto standard on Linux for securing connections.

However, IPSec isn't likely to go away as it's the industry standard for interconnecting different companies site-to-site using various appliances of all make and models.

But as WireGuard becomes more known and mature, it's very likely to be included in different appliances, specially if those are Linux based.