Simple and Secure VPN in FreeBSD – Introducing WireGuard
Wireguard is a Virtual Private Network (VPN) technology that aims to enable the easy deployment and configuration of servers and clients. Wireguard is intended to replace the use of IPSec or OpenVPN for many VPN applications.
Wireguard offers both kernel and userspace implementations. The first kernel implementation was offered for Linux, but there are now in-kernel implementations for FreeBSD and OpenBSD, with a NetBSD implementation in progress. The main userspace implementation of wireguard is a go application and it is available via the wireguard-go tool.
A wireguard network configuration is formed of interfaces and peers. Each interface is defined by its private key, peers are defined by their public key and the set of addresses they are allowed to use. This forms a network that uses cryptokey routing.
With cryptokey routing, hosts are defined by their cryptographic keys, where public keys are used as identifiers. This means that most of the configuration for wireguard requires dealing with these public keys. Wireguard does not specify mechanisms for key distribution and management, and it is fair to compare this with the distribution of ssh keys. Unlike ssh, the public keys need to be available for both sides of the connection.
Wireguard uses INI files for static configuration, a config file might look like:
[Interface] PrivateKey = EO9b4bV1qg0veyFelERv69v4eamKu/evGZ/Hbiq82Eg= ListenPort = 51820 [Peer] PublicKey = ZBF6vo22sBkT0ro/xnlZ0EZsFGTYPTOvY8luFFtJwwk= AllowedIPs = 10.10.0.2/32 [Peer] PublicKey = fnpAo3hMf9UOtPpM+CUtpB+ETvB5XjNLIuTGsX+qnCc AllowedIPs = 10.10.0.138/32 [Peer] PublicKey = Tpdhd68aaDAQfNsnqzoViMl1h+yHeYuVZ22xrY1BM0I= AllowedIPs = 10.10.0.3/32, 192.168.2.1/24
The allowed IP addresses act as a routing table. When traffic is sent, the packet is compared to the peer’s AllowedIPs, and if it matches the address or the subnet, the packet is encrypted using the peer’s public key and forwarded over the tunnel in its direction.
On receipt of a packet, AllowedIPs acts as a receive filter. The packet is first decrypted and authenticated, and then it is evaluated against the AllowedIPs to verify that it is allowed to enter the network.
Wireguard makes a serious effort to make the tools it offers consistent on all platforms. To do this, there are two components that determine whether the kernel or userspace implementation is used. The first is a method to create a wireguard interface; this is the component that changes between kernel and userspace versions, then there is a single configuration wg(8) tool to manage the VPN.
This approach intends to smooth over platform configuration differences. The platform specific part is creating and configuring the network interface, but the VPN configuration should all be manageable with a single consistent tool.
FreeBSD has an in-kernel implementation of wireguard that landed in the tree in November 2020. The in-kernel implementation is still very fresh, so in this article we will use the userspace tool, as it is available in the most supported versions of FreeBSD today. The in-kernel implementation key generation is still performed using the wg(8) tool.
We can install wg(8) and wireguard-go (the wireguard userspace implementation) from ports with:
# pkg install wireguard wireguard-tools
Our host’s identity is defined by its key pair. We need to generate a wireguard private key using the wg genkey command; wireguard helpfully (if frustratingly) insists that we don’t place the private key in a world readable file. To generate the key and append it to a file, we need to temporally change our umask:
# (umask 0077; wg genkey > private.key) # cat private.key MOtM8w02ONtGK9vmLCXlx6em+5pRTm6C0z7HCeIrPlY=
This command creates a private key and places it into a file called private.key. We can view the private key by catting the file; anyone with the private key can act as the server, so you should be very careful with your wireguard private keys. Wireguard tools will typically censor the private key in output unless additional flags are given.
Did you know?
You can maximize the power of your FreeBSD infrastructure with our Support Subscription!Check it out!
Wireguard uses our public key as our identity, wg(8) will show us the public key for a private key with the wg pubkeycommand:
# wg pubkey < private.key APWANIrAD3drcSNUdDuUuXLsCRksKxuQxwBHf56UxiM=
First, let’s configure our server side. The server is going to act as the central host in our network with several clients connecting to it for access. This host needs to have a stable public IP and a known shared port to accept wireguard traffic on.
We need to generate keys for the server:
wg-server (umask 0077; wg genkey > server.key) wg-server # cat server.key AJKFyhDoLfLnlclEcKV97bRonVKGgEbRGi7vGfeYNnw= wg-server # wg pubkey < server.key # show the public key for this private key j2klzAC0RnWOOUAcZw+/GEtp5URCSwf9r3yW+JYJRRE=
Next, there are a few steps to set up the network. We need to create a wireguard interface using wireguard-go:
wg-server # wireguard-go wg0 INFO: (wg0) 2021/01/08 14:35:42 Starting wireguard-go version 0.0.20200320
This interface needs to be configured to have an address in our private network on the host and a route to direct traffic into the wireguard tunnel. We use the wg(8) tool to configure the interface’s private key.
wg-server # ifconfig wg0 inet 10.10.0.1/24 10.10.0.1 wg-client # route add 10.10.0.0/24 -interface wg0 wg-server # wg set wg0 private-key ./server.key listen-port 51820
If we don’t specify the listen port here, then wireguard will select a random port when the wg0 interface is brought up. For a destination server like this, we need to have a consistent port between addresses, and so we use one common to wireguard here.
wg-server # ifconfig wg0 down wg-server # ifconfig wg0 up
Finally, we need to configure each peer with its public key and the IP addresses it is allowed to use:
wg-server # wg set wg0 peer <peer pubkey> allowed-ips 10.10.0.2/32
Each client needs to go through similar steps: first create a private key to identify the client:
wg-client (umask 0077; wg genkey > client.key) wg-client # cat client.key gM3ydSBBTZOw1nzIyV/nxJuONB8/MNe/RhcOikQbE3Y= wg-client # wg pubkey < client.key # show the public key for this private key jILr21Xt+3sXDZepu5Z3syuJMXyc29f5GBXHZFPulEE=
We need to create the wireguard interface by running wireguard-go:
wg-client # wireguard-go wg0 # create the wg0 interface
This interface is then configured for the internal VPN network. wireguard-go creates a tun interface, and tun interfaces need to be configured with a source and destination address. As that is not important here, we can use the same address for both parts. To make sure we can reach the internal network properly, we also need to add a route:
wg-client # ifconfig wg0 inet 10.10.0.2/24 10.10.0.2 wg-client # route add 10.10.0.0/24 -interface wg0
With the interface configured, we can now configure wireguard:
wg-client # wg set wg0 private-key ./client.key
Finally, we need to configure the wg0 interface with an endpoint for the server:
wg-client # ifconfig wg0 down wg-client # ifconfig wg0 up wg-client # wg set wg0 peer <peer pubkey> allowed-ips 10.10.0.0/24 endpoint 192.0.2.42:51820
When we configured the client peer, we told wireguard where to find the server with the endpoint parameter. In this case the client doesn’t listen on a predictable port, but instead it first has to create the wireguard tunnel with the server when traffic needs to be sent.
Now that we have created and configured our client, we need to add its public key and allowed-ip addresses to the server.
wg-server # wg set wg0 peer jILr21Xt+3sXDZepu5Z3syuJMXyc29f5GBXHZFPulEE= allowed-ips 10.10.0.2/32
Finally we can test the set up using ping:
wg-client # ping 10.10.0.1 PING 10.10.0.1 (10.10.0.1): 56 data bytes 64 bytes from 10.10.0.1: icmp_seq=0 ttl=64 time=3.253 ms 64 bytes from 10.10.0.1: icmp_seq=1 ttl=64 time=1.358 ms 64 bytes from 10.10.0.1: icmp_seq=2 ttl=64 time=1.089 ms 64 bytes from 10.10.0.1: icmp_seq=3 ttl=64 time=1.649 ms ^C
Wireguard is not a ‘chatty’ protocol. If there is no traffic to send from the client for a long time, and it doesn’t have a fixed address, the server can ‘forget’ how to reach the client.
It might be the case that the client is behind a NAT network. To support this case, wireguard supports the persistent-keepalive configuration option. This option is disabled by default, but it allows our client to receive packets from the server when it is not sending traffic itself.
RFC4787 recommends a 2 minute timeout interval for NATs and middleboxes, but recent UDP protocols (such as QUIC), recommend sending packets every 30 seconds to prevent middleboxes from losing state for UDP flows.
If you need the tunnel connection kept alive, add the persistent-keepalive parameter:
wg-client # wg set wg0 peer <peer pubkey> allowed-ips 10.10.0.0/24 persistent-keepalive 30 endpoint 192.0.2.42:51820
It can help to run wireguard-go in the foreground and you can enable debug output from wireguard-go with the LOG_LEVEL environment variable:
# export LOG_LEVEL=DEBUG # wireguard-go -f wg0 INFO: (wg0) 2021/01/11 10:43:49 Starting wireguard-go version 0.0.20201118 DEBUG: (wg0) 2021/01/11 10:43:49 Debug log enabled DEBUG: (wg0) 2021/01/11 10:43:49 Routine: event worker - started DEBUG: (wg0) 2021/01/11 10:43:49 Routine: encryption worker - started DEBUG: (wg0) 2021/01/11 10:43:49 Routine: decryption worker - started DEBUG: (wg0) 2021/01/11 10:43:49 Routine: TUN reader - started DEBUG: (wg0) 2021/01/11 10:43:49 Routine: handshake worker - started INFO: (wg0) 2021/01/11 10:43:49 Device started INFO: (wg0) 2021/01/11 10:43:49 UAPI listener started
If you are launching wireguard-go using sudo, remember that sudo uses its own environment:
$ sudo LOG_LEVEL=debug wireguard-go -f wg0
wireguard-go doesn’t seem to always detect that the wg0 interface has been brought up and ends up not creating the UDP sockets required to send packets. You can check this in sockstat by looking for wireguard-go listening on UDP for v4 and v6, or you can check the wireguard-go log. If you don’t see a “Interface set up” message in the log, try toggling it by taking wg0 up and down:
# ifconfig wg0 down # ifconfig wg0 up
Wireguard does not appear to offer a mechanism to decrypt packets based on pre-shared keys in the way that IPSec enables. This might appear in the future and I would check with wireshark.
Even without decryption, we can verify that packets are making it through the wg interface and across the network by checking for packets on the client and server’s wg port. If 51820 is the server’s port, then listening with tcpdump for udp traffic should return some packets:
# tcpdump udp and port 51820 [tj@freebsd-wg-client] $ sudo tcpdump udp and port 51820 tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on hn0, link-type EN10MB (Ethernet), capture size 262144 bytes 19:54:31.217241 IP freebsd-wg-client.internal.internet.net.45052 > 188.8.131.52.51820: UDP, length 116 19:54:32.248686 IP freebsd-wg-client.internal.internet.net.45052 > 184.108.40.206.51820: UDP, length 116 19:54:33.321003 IP freebsd-wg-client.internal.internet.net.45052 > 220.127.116.11.51820: UDP, length 116 19:54:34.027525 IP 18.104.22.168.51820 > freebsd-wg-client.internal.internet.net.45052: UDP, length 32 19:54:34.348493 IP freebsd-wg-client.internal.internet.net.45052 > 22.214.171.124.51820: UDP, length 116 19:54:35.398882 IP freebsd-wg-client.internal.internet.net.45052 > 126.96.36.199.51820: UDP, length 116 19:54:36.425414 IP freebsd-wg-client.internal.internet.net.45052 > 188.8.131.52.51820: UDP, length 116 19:54:37.448561 IP freebsd-wg-client.internal.internet.net.45052 > 184.108.40.206.51820: UDP, length 116 19:54:38.520893 IP freebsd-wg-client.internal.internet.net.45052 > 220.127.116.11.51820: UDP, length 116
The above capture is from a debugging session where the wg interface on the client was configured with the wrong address. It shows ping packets going from the client to the server, but no ICMP replies returning. These packets were encrypted, but their presence helped point to which part of the configuration wasn’t working.
A wireguard kernel implementation was committed to FreeBSD in November 2020. The wg tool doesn’t support the use of the in-kernel implementation yet, and so ifconfig has to be used to configure the interface. So, while all the concepts remain the same, you need to use ifconfig, as of the writing of this article, there is not yet any documentation in the man page for how to set up wireguard.
On FreeBSD-13 we can create and configure the in-kernel wireguard like so:
# ifconfig wg create listen-port 51820 private-key `cat server.key` # ifconfig wg0 peer public-key <peer's public key> endpoint 192.168.2.42:51820 allowed-ips 10.10.0.2/24
This has been an introduction on how you can use wireguard on FreeBSD to create VPNs. We haven’t covered the detail of static configuration or how best to manage key distribution to a large number of hosts. You can check wireguard.com for more information, documentation and pointers to tools that help with distribution of keys and mass configuration.
Meet the Author: Tom Jones
Tom Jones is an Internet Researcher and FreeBSD developer that works on improving the core protocols that drive the Internet. He is a
contributor to open standards in the IETF and is enthusiastic about using FreeBSD as a platform to experiment with new networking ideas as they progress towards standardisation.
Like this article? Share it!
You might also want be interested in
Get more out of your FreeBSD networking
Networking is crucial to many companies. If you have a FreeBSD networking implementation or a driver you’re looking at, our team can help you further enable your efforts.