Klara

WireGuard is a Virtual Private Network (VPN) technology that enables the easy deployment and configuration of encrypted network tunnels. WireGuard is intended to replace the use of IPSec or OpenVPN for many VPN applications.

WireGuard offers both kernel and userspace implementations. The in-kernel version is faster, but must be customized for each OS, and not every OS supports this yet. The first kernel implementation was offered for Linux, but there is now an in-kernel implementation for FreeBSD, and also one for OpenBSD, with a NetBSD implementation in progress.

WireGuard needs crypto primitives that were not present in the FreeBSD kernel—so high quality native support required a large amount of work and communication with the upstream WireGuard project.

FreeBSD kernel-mode WireGuard—currently flagged experimental—is available from the wireguard-kmod package. The main userspace implementation of wireguard is a golang application, and it is available via the wireguard-go package. 

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 TOML files for static configuration. Here's a simple example config file:

[Interface]
PrivateKey = EO9b4bV1qg0veyFelERv69v4eamKu/evGZ/Hbiq82Eg=
ListenPort = 444

[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 address stanzas act as a routing table. When traffic is sent, the packet is compared to the peer’s AllowedIPs. 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.

Installation

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. A single configuration tool—wg(8)—manages these interfaces, whether they are created in kernel or userspace mode.

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 available from ports or packages, but the in-kernel implementation is still considered experimental—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-go wireguard-tools

Creating Keys

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 access your tunnels, so you should be very careful with WireGuard private keys. WireGuard tools will typically censor the private key in output unless additional flags are given. 

WireGuard uses your public key to identify you to remote systems as well as encrypt your data. The wg pubkey command will display the public key derived from a given private key:

# wg pubkey < private.key
APWANIrAD3drcSNUdDuUuXLsCRksKxuQxwBHf56UxiM=

Server Side

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 444

If we don’t specify the listen port here, then WireGuard will select a random dynamic port when the wg0 interface is brought up. For a destination server like this, we need to have a consistent port between addresses. If possible, it's a good idea to use a non-dynamic port—one below 49152—since WireGuard traffic is UDP, and many routers don't expect stable port assignments in the dynamic range.

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 

FreeBSD Client

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:444

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:444

Debugging

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 444 is the server’s port, then listening with tcpdump for udp traffic should return some packets:

# tcpdump udp and port 444 
[tj@freebsd-wg-client] $ sudo tcpdump udp and port 444
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 > 52.178.136.74.444: UDP, length 116
19:54:32.248686 IP freebsd-wg-client.internal.internet.net.45052 > 52.178.136.74.444: UDP, length 116         
19:54:33.321003 IP freebsd-wg-client.internal.internet.net.45052 > 52.178.136.74.444: UDP, length 116
19:54:34.027525 IP 52.178.136.74.444 > freebsd-wg-client.internal.internet.net.45052: UDP, length 32          
19:54:34.348493 IP freebsd-wg-client.internal.internet.net.45052 > 52.178.136.74.444: UDP, length 116         
19:54:35.398882 IP freebsd-wg-client.internal.internet.net.45052 > 52.178.136.74.444: UDP, length 116
19:54:36.425414 IP freebsd-wg-client.internal.internet.net.45052 > 52.178.136.74.444: UDP, length 116
19:54:37.448561 IP freebsd-wg-client.internal.internet.net.45052 > 52.178.136.74.444: UDP, length 116
19:54:38.520893 IP freebsd-wg-client.internal.internet.net.45052 > 52.178.136.74.444: 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.

Conclusion

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.

Back to Articles

What makes us different, is our dedication to the FreeBSD project.

Through our commitment to the project, we ensure that you, our customers, are always on the receiving end of the best development for FreeBSD. With our values deeply tied into the community, and our developers a major part of it, we exist on the border between your infrastructure and the open source world.