Wireguard is a nifty little tool. The tagline describes it as an “extremely simple yet fast and modern VPN that utilizes state-of-the-art cryptography”. I have been using it for years to tunnel into my servers, as an Internet gateway, and as a jumpbox into the servers’ VLAN. Recently, I figured out how to configure it as a secure mesh between the servers.

Before looking at the specific cases, let us be clear that wireguard operates at a low level in the stack. All it does is make the wireguard network device type available. Everything else is done by the standard ip(8) command. This is obscured when we use the wg-quick(8) helper, but that turns out to be just a shell script ultimately calling ip(8).

The basic shape of a wireguard config is this:

[Interface]
Address = ...      # the IP address and subnet the local wg0 interface will get
SaveConfig = false # set this to false so that wireguard does not overwrite
                   # this config file
PostUp = ...       # shell commands to run after the interface comes up; this
                   # is where you setup forwarding
PostDown = ...     # shell commands to run after the interface comes down; this
                   # is where you clean up the PostUp commands
ListenPort = 51820 # port on the public internet
PrivateKey = ...   # this machine's private key

[Peer]
# PEER1
PublicKey = ...  # PEER1's public key, derived from its private key
AllowedIPs = ... # the IP address this peer is allowed to use on the wireguard
                 # interface
Endpoint = ...   # PEER1's public IP address

[Peer]
# PEER2
PublicKey = ...
AllowedIPs = ...
# Endpoint = ... # if Endpoint is not configured, then this machine does not 
                 # know how to reach PEER2... unless PEER2 first sends some
                 # packets to this machine, in which case the wireguard device
                 # will remember the sending address

# More peers

Once a config is saved to /etc/wireguard/wg0.conf, we can bring up the network interface with systemctl start wg-quick@wg0. Both the wg(8) and wg-quick(8) tools use this config file, so we need to read both man pages to make sense of the file.

A lot of the values in the following configs come in pairs which must match. If the paired values do not match, then one of the wireguard peers is probably going to silently discard any packets it receives. The paired values are:

  • *_WG_IP4/24 and *_WG_IP4/32 :: Peers can choose any IP address for the wireguard interface. The only restriction is that the values in one peer’s Address section must match the other’s AllowedIPs section. Additionally, wg-quick’s routing inference only works if the subnet in Address contains the address in AllowedIPs. Ditto for *_WG_IP6/64 and *_WG_IP6/128.
  • *_PRIVATE_KEY and *_PUBLIC_KEY :: The PrivateKey in one peer’s [Interface] section must match the PublicKey in every other peer’s [Peer] section.

Bridge to a server

Bridge to server diagram

The first scenario is connecting a laptop to a server. Connecting two machines is essentially all wireguard does, and everything else is just applications of this.

[Interface]
Address = LAPTOP_WG_IP4/24, LAPTOP_WG_IP6/64
PrivateKey = LAPTOP_PRIVATE_KEY

[Peer]
# SERVER1
PublicKey = SERVER1_PUBLIC_KEY
AllowedIPs = SERVER1_WG_IP4/32, SERVER1_WG_IP6/128
Endpoint = SERVER1_PUBLIC_IP:51820
Laptop wg0.conf
[Interface]
Address = SERVER1_WG_IP4/24, SERVER1_WG_IP6/64
SaveConfig = false
ListenPort = 51820
PrivateKey = SERVER1_PRIVATE_KEY

[Peer]
# laptop
PublicKey = LAPTOP_PUBLIC_KEY
AllowedIPs = LAPTOP_WG_IP4/32, LAPTOP_WG_IP6/128
Server1 wg0.conf

Internet gateway

Internet gateway diagram

To route all of a laptop’s networking through a gateway, we specify AllowedIPs = 0.0.0.0/0, ::/0 in the laptop’s [Peer] section, and add some iptables(8) rules to the gateway’s [Interface] section.

...
[Peer]
# GATEWAY
PublicKey = GATEWAY_PUBLICKEY
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = GATEWAY_PUBLIC_IP:51820
...
Laptop wg0.conf
[Interface]
Address = GATEWAY_WG_IP4/24, GATEWAY_WG_IP6/64
SaveConfig = false
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o PUBLIC_IFACE -j MASQUERADE; ip6tables -A FORWARD -i %i -j ACCEPT; ip6tables -t nat -A POSTROUTING -o PUBLIC_IFACE -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o PUBLIC_IFACE -j MASQUERADE; ip6tables -D FORWARD -i %i -j ACCEPT; ip6tables -t nat -D POSTROUTING -o PUBLIC_IFACE -j MASQUERADE
ListenPort = 51820
PrivateKey = GATEWAY_PRIVATE_KEY

[Peer]
# LAPTOP
PublicKey = LAPTOP_PUBLIC_KEY
AllowedIPs = LAPTOP_WG_IP4/32, LAPTOP_WG_IP6/128
Gateway wg0.conf

Breaking down the iptables rules, we have:

  • iptables -A FORWARD -i %i -j ACCEPT :: Enable packet forwarding for the wireguard interface (%i).
  • iptables -t nat -A POSTROUTING -o PUBLIC_IFACE -j MASQUERADE :: When sending packets to PUBLIC_IFACE, enable masquerading to make them look like they came from the gateway host. POSTROUTING, ACCEPT, and MASQUERADE are iptables specific strings, but PUBLIC_IFACE is something we should replace in the configs with a real interface name like eth0.
  • ip6tables ... :: Same as above, but for IPv6.

The PostDown commands just delete (-D) the rules added by the PostUp commands.

Jumpbox to different network

Jumpbox diagram

Suppose the machines in the datacenter are connected over a VLAN, and we want the laptop to access this network through one of them. We make two changes:

  • In the laptop’s [Peer] section, we add the VLAN’s address and subnet to AllowedIPs.
  • In the jumpbox’s [Interface] section, we add PostUp commands to enable forwarding for the wireguard interface and masquerading into the VLAN. This is similar to what we did in the gateway example.
...
[Peer]
# JUMPBOX
PublicKey = JUMPBOX_PUBLICKEY
AllowedIPs = JUMPBOX_WG_IP4/32, JUMPBOX_WG_IP6/128, VLAN_IP4/24
Endpoint = JUMPBOX_PUBLIC_IP:51820
...
Laptop wg0.conf
[Interface]
Address = JUMPBOX_WG_IP4/24, JUMPBOX_WG_IP6/64
SaveConfig = false
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o VLAN_IFACE -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o VLAN_IFACE -j MASQUERADE
ListenPort = 51820
PrivateKey = JUMPBOX_PRIVATE_KEY

[Peer]
# LAPTOP
PublicKey = LAPTOP_PUBLIC_KEY
AllowedIPs = LAPTOP_WG_IP4/32, LAPTOP_WG_IP6/128
Jumpbox wg0.conf

We only setup forwarding to the VLAN over IPv4. We could add the equivalent ip6tables(8) commands to enable IPv6 as well. Similarly to the gateway example, we should replace VLAN_IFACE with a real interface name like eth1. We could also add more of these commands to enable forwarding to other networks or the public internet.

Mesh between servers

Mesh diagram

Finally, let us consider the case where we securely network together multiple servers into a mesh. We might want this because they are running in different datacenters, or because we do not trust the datacentre’s VLAN. All we need to do is add more [Peer] sections to each of the machines, one for each other machine.

...
[Peer] # SERVER2
...
[Peer] # SERVER3
...
Server1 wg0.conf
...
[Peer] # SERVER1
...
[Peer] # SERVER3
...
Server2 wg0.conf
...
[Peer] # SERVER1
...
[Peer] # SERVER2
...
Server3 wg0.conf

Conclusion

Wireguard is a very useful tool for connecting multiple machines. The configuration is a bit confusing because it does multiple things, but ultimately, it is only combining simple snippets, each defining separate functionality.