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
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
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-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/32:: Peers can choose any IP address for the wireguard interface. The only restriction is that the values in one peer’s
Addresssection must match the other’s
wg-quick’s routing inference only works if the subnet in
Addresscontains the address in
AllowedIPs. Ditto for
PrivateKeyin one peer’s
[Interface]section must match the
PublicKeyin every other peer’s
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
[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
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
iptables(8) rules to the gateway’s
... [Peer] # GATEWAY PublicKey = GATEWAY_PUBLICKEY AllowedIPs = 0.0.0.0/0, ::/0 Endpoint = GATEWAY_PUBLIC_IP:51820 ...
[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
Breaking down the
iptables rules, we have:
iptables -A FORWARD -i %i -j ACCEPT:: Enable packet forwarding for the wireguard interface (
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.
iptablesspecific strings, but
PUBLIC_IFACEis something we should replace in the configs with a real interface name like
ip6tables ...:: Same as above, but for IPv6.
PostDown commands just delete (
-D) the rules added by the
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
- In the jumpbox’s
[Interface]section, we add
PostUpcommands 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 ...
[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
We only setup forwarding to the VLAN over IPv4. We could add the
ip6tables(8) commands to enable IPv6 as well.
Similarly to the gateway example, we should replace
a real interface name like
eth1. We could also add more of these
commands to enable forwarding to other networks or the public
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 ...
... [Peer] # SERVER1 ... [Peer] # SERVER3 ...
... [Peer] # SERVER1 ... [Peer] # SERVER2 ...
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.