WireGuard is a relatively new choice when it comes to VPN utilities, but does things very differently to other existing VPN architectures.

From what I’ve learnt WireGuard is not just for VPNs, it’s a stateless, peer-to-peer network tunnelling utility.

There are two things I’ve been meaning to do, set up a VPN connection so I can connect through my Linode from restrictive networks, and creating a simple IPv6 tunnel I can use on IPv4 connections. This is what I’ve documented here.

I will be upfront in saying I’m not a security expert, and the security of the system is not a focus of this article. Where I mention security I will try to be accurate based on the documentation.

I have broken this up into three parts. The first will cover the general setup and a basic configuration of just a private IPv4 network between hosts, with no gateway routing. Part 2 will cover a standard VPN with IPv4 and IPv6 support, and Part 3 will cover using WireGuard as an IPv6 tunnel.

General setup

I use wg-quick with systemd service templates. Quite simply you place a configuration file at /etc/wireguard/[id].conf and then start the associated service with systemctl start wg-quick@[id].

These interfaces aren’t technically servers, you need to configure them with network addresses, and also associate them with any peers that will connect. Peers identify each other with public/private key pairs. Each peer’s public key is configured with matching network addresses.

You can also optionally associate a pre-shared key to add post quantum secrecy. My understanding of this is that asymmetric encryption methods are potentially weak to quantum algorithms, the pre-shared key helps protect the asymmetric handshake so that recorded encrypted traffic can’t be decrypted later.

Key generation is done with the wg tool with various sub commands. Each interface should have a new key pair generated, and if you are using pre-shared keys a new one needs to be generated for each peer on each interface.

As private keys are stored in the configuration files, you should use restrictive file permissions. Create all files as root and run umask 077 beforehand. Keys generation commands output to standard output, so redirect the output to a file to store it. I managed this by creating a folder under /root/wireguard for each interface. Alternatively you could redirect the output to the configuration file with the append redirect >>.

Generating keypair

First generate the private key with wg genkey > private.key. You can then generate the public key with wg pubkey < private.key > public.key. If you generate the private key directly to the config file, wg show will output the public key once the connection is enabled. Configuration of peers will require some parallel configuration as each needs to be configured with the others public key.

Generating preshared keys

For each interface I generate the preshared keys into a file named to identify the connection pair. So for a laptop connecting to my server I run wg genpsk > server-laptop.psk

Local private network

The simplest use case is a private network link for accessing just the other peers. This won’t route any traffic to the internet through the tunnel, which means as no peer is being used as an edge router. Each host needs a configuration for the interface and an entry for each peer that will connect to the private network through it, or for each peer it is establishing a connection to.

In the simplest case you might have one peer set up as a central connecting point, in which case it will act as a bounce server for the network to the other peers. However, you are not limited to one and any peer that is publicly accessible from the internet that can forward packets to other hosts can act as a bounce server.

As an example, let’s have a single bounce server (Peer A) and two client peers (B and C). If all peers want access to each other the packet forwarding will need to be enabled as shown above. For simplicities sake we won’t use pre-shared keys here. We’ll stick to IPv4 and use the network 10.100.100.0/24

This setup should allow all devices to communicate with each other, with communication between Peers B and C going through Peer A. This should demonstrate a very simple setup with the most basic of configurations.

Peer A will act as the bounce server. Its configuration will include entries for each of the client peers. It will listen on the defacto default port of 51820 and be accessible at peer-a.example.com. It uses the keypair:

Private: uBXmvz0Jg5Rb6DhFLtTl21Q9aIUm97NhT5vokE3zpGY= 
Public:  JfD8o6Y8ghPmftAzkET5Xk6DEpTRPpApy+JxUsjbREA=

Peer B will connect to Peer A and use the keypair:

Private: yBM6HMEipII8Sqm0vXWbXIcoaVc4gkQI5Zx7TNC1PG4= 
Public:  G8yK7arrKT6DFD7MRMpEEGyBm4pnBYMa2/Jt/ei2J0w=

Peer C will connect to Peer A and use the keypair:

Private: WKhQvHlooeKxozxgKkHAdW3zH+p4TOXjc3trVBo0C1Q=
Public:  DU4d2RQZDx9fBlMy7gD3b5vt/bhTYMnm/NFgvsKwChg=

Peer A Configuration

[Interface]
Address = 10.100.100.1/24
ListenPort = 51820
PrivateKey = uBXmvz0Jg5Rb6DhFLtTl21Q9aIUm97NhT5vokE3zpGY=

[Peer]
#Peer B
PublicKey = G8yK7arrKT6DFD7MRMpEEGyBm4pnBYMa2/Jt/ei2J0w=
AllowedIPs = 10.100.100.2/32

[Peer]
#Peer C
PublicKey = DU4d2RQZDx9fBlMy7gD3b5vt/bhTYMnm/NFgvsKwChg=
AllowedIPs = 10.100.100.3/32

Important things to note here. The Address uses CIDR notation to identify the local subnet size. The interface is configured with the private key, and peers are identified with their public key.

Peers are configured to only allow traffic from their specific address on the network. If other hosts on the network might be accessing the network from them, their IPs could be added in a comma separated list, or by specifying the whole subnet, as is done on the peers for the bounce server, as we’ll see next.

Peer B Configuration

[Interface]
PrivateKey = yBM6HMEipII8Sqm0vXWbXIcoaVc4gkQI5Zx7TNC1PG4=
Address = 10.100.100.2/24

[Peer]
PublicKey = JfD8o6Y8ghPmftAzkET5Xk6DEpTRPpApy+JxUsjbREA=
AllowedIPs = 10.100.100.0/24
Endpoint = peer-a.example.com:51820

Here we see that the bounce server is set up with an AllowedIPs value identifying the entire subnet. The Endpoint option tells it were to contact the peer. Without this, like in the server example, the host will have to wait for the peer to connect before being able to communicate with it. When it’s there, the host will handshake with the identified peer when the interface is enabled.

Peer C Configuration

[Interface]
PrivateKey = WKhQvHlooeKxozxgKkHAdW3zH+p4TOXjc3trVBo0C1Q=
Address = 10.100.100.3/24

[Peer]
PublicKey = JfD8o6Y8ghPmftAzkET5Xk6DEpTRPpApy+JxUsjbREA=
AllowedIPs = 10.100.100.0/24
Endpoint = peer-a.example.com:51820

As you can see this is identical to Peer B just with its own [Interface] values.