WireGuard mesh
This is part of my series on renovating my homelabs to ring in the roaring ’20s.
In this post I’m going to set up WireGuard, an exciting new in-kernel VPN technology from Jason Donenfield. It will be landing in the core Linux kernel very soon (probably in 5.6)!
Setting up a VPN will let me access devices at any of my sites without having to expose all of those devices directly to the internet. WireGuard has some great features that make it my choice over incumbent VPN’s like OpenVPN or IPSec…and the set up is quick and painless.
Sections:
Why WireGuard?
WireGuard is an awesome new technology and very appealing as a replacement for existing VPN solutions. Its biggest selling point simplicity, as you’ll see below as we trivially build a VPN.
It is easy to configure and secure by default - there is no need to pick a crypto algorithm or create port triggering rules to protect your VPN from the open internet.
It’s based on proven public key encryption paradigms. Wireguard is implemented in <5k lines of code - delegating to trusted crypto libraries instead of reimplementing them. Its simplicity allowed the team to patch the route based networking exploit from late 2019 before the CVE was even public.
And WireGuard is “formally verified”, the code equivalent of a mathematical proof, meaning that the design is provably sound.
Finally, WireGuard runs as a kernel module on most *nixes. It is measurably faster and more efficient than other userland VPN programs (this is especially noticeable compared to OpenVPN on mobile devices like laptops and phones where this affects battery life).
Bonus: Linus loves it.
Installing WireGuard
We can install WireGuard according to the instructions in the offical installation documentation:
$ sudo dnf copr enable jdoss/wireguard
$ sudo dnf install -y wireguard-dkms wireguard-tools
Note: if the running kernel is a different version than the installed kernel-devel
package, you will need to reboot. If you don’t, you’ll see an error like: Unable to access interface: Protocol not supported
in the WireGuard logs and the interface will not come up.
Running WireGuard
Generating a keypair
Before we can start WireGuard, we need to generate a keypair:
$ sudo su
$ umask 077
$ mkdir -p /etc/wireguard/keys
$ wg genkey | tee /etc/wireguard/keys/private | wg pubkey > /etc/wireguard/keys/public
wg-quick config file
I’m going to run WireGuard with wg-quick
, but you can do everything wg-quick does by hand - see the official WireGuard quickstart for details.
My quick config is going in /etc/wireguard/wg0.conf
:
[Interface]
# Name = A
Address = 172.16.4.1
ListenPort = 51820
PostUp = wg set %i private-key <(cat /etc/wireguard/keys/private)
wg-quick Interface configuration break-down:
-
# Name - this line is a comment and is ignored by
wg-quick
. It helps me know which device is which. -
Address - this is the address this machine will have in WireGuard. It is not the same as the physical interface IP address. I set mine to a completely different IP range (172.16.4.0/12) than my LAN (192.168.0.0/16) to disambiguate those addresses.
Consult RCF1918 for the full private IP spec, or quickly check the wiki page for Private Networking for the allowed private CIDRs.
-
ListenPort - this is the port that WireGuard will listen for traffic on. Note: WireGuard communicates over UDP, so when opening firewall ports UDP needs to be allowed on this port.
-
PostUp - allows us to execute arbitrary commands after the interface is started. This snippet adds the WireGuard private key to the runtime. We could hardcode “PrivateKey = xxx”, but reading it from the key file at runtime is better security practice.
At this point I also forwarded 51820/udp
from my Gateway to this server. How you configure this in your router varies, but mine has a page with a table for port forwards where I added it in. We should also note the Gateway’s Public IP - mine was 135.84.202.203.
WireGuard is an “undiscoverable” service - it does not respond to incoming requests on this port unless they are encrypted with this server’s public-key, meaning a peer would have to already know this WireGuard server exists to be able to detect the service. This means that conventional methods for checking if a remote service is listening on a port, like executing a telnet 51820
, will not work for WireGuard.
Since we can’t just test that the wg server is listening, we need to add a peer and verify connectivity to be assured that everything is working.
Adding a peer
On another machine, we repeat the above installation and configuration. This time, our /etc/wireguard/wg0.conf
looks like:
[Interface]
# Name = B
Address = 172.16.20.10
ListenPort = 51820
PostUp = wg set %i private-key <(cat /etc/wireguard/keys/private)
Because this peer will not be permanently listening for wg traffic on this network, I did not port forward from the router to this peer. Since Peer A is port-forwarded, Peer B will be reachable thanks to NAT hole-punching - the router will assign a random public port and forward it to Peer B, and Peer A will send traffic to that port.
Back on Peer A
I need to add Peer B
’s public key (from /etc/wireguard/keys/public
), and AllowedIP
(its Address
):
[Interface]
# Name = A
...
[Peer]
# Name = B
PublicKey = <Peer B's public key>
AllowedIPs = 172.16.20.10
Likewise, on Peer B
, I add Peer A
’s data. Notice I also add Peer A
’s Endpoint
address since it is the one with the fixed public port-forward.
[Interface]
# Name = B
...
[Peer]
# Name = A
PublicKey = <Peer A's public key>
AllowedIPs = 172.16.4.1
Endpoint = 135.84.202.203:51820
Now, we can bring up WireGuard on both machines with:
$ sudo systemctl start wg-quick@wg0
Like I mentioned before, WireGuard is “undiscoverable” - it is not a chatty protocol, and only sends data on the VPN when it’s asked to. So we need to ask Peer B to send some data to Peer A over wg0 before it will open the VPN tunnel:
$ ping 172.16.4.1
and check the status of the tunnel on Peer A with:
$ sudo wg
interface: wg0
public key: <redacted>
private key: (hidden)
listening port: 51820
peer: <redacted>
endpoint: 72.83.31.122:41920
allowed ips: 172.16.20.1/32
latest handshake: 1 second ago
transfer: 0 KiB received, 0 KiB sent
Note that Peer B is randomly assigned a port by its NAT gateway since we did not pin it with a port-forward.
If the tunnel is not connecting, a firewall might be blocking it. See the masquerading section below for firewalld configs!
LAN Peering
Now that we’ve established a point-to-point encrypted connection, we can tweak a few settings to allow us to route traffic through WireGuard to do cool things like make the other devices on Peer A’s LAN accessible to Peer B, or send Peer B’s DNS traffic through a PiHole on Peer A (I will be writing up this as part of my PiHole and CoreDNS blog post).
Peer A’s LAN is 192.168.0.0/16
. Let’s say there’s a device on this LAN that we want to reach - Device A-2
- with IP 192.168.0.9
.
On Peer B, we can make that LAN accessible by adding more AllowedIPs
to the wg0.conf:
[Interface]
# Name = B
...
[Peer]
# Name = A
...
AllowedIPs = 172.16.4.1/32, 192.168.0.0/16 # <- we add CIDRs here and wg-quick automatically creates routing rules
When we restart wg-quick ($ sudo systemctl restart wg-quick@wg0
), we can see routes are now added to the route table that send requests to 192.168.0.0/16
via wg0
:
$ sudo ip route
...
192.168.0.0/16 dev wg0 scope link
However, we still can’t ping 192.168.0.9
. Peer A
needs to be able to recieve the traffic we’re sending over WireGuard and forward it to the appropriate device on it’s LAN. For this we need to enable two things: IP forwarding and masquerading.
IP Forwarding
We can enable IP forwarding on Peer A
temporarily with:
$ sudo sysctl -w net.ipv4.ip_forward=1
To make this change permanent, we also add this line to /etc/sysctl.d/99-sysctl.conf
:
net.ipv4.ip_forward = 1
Masquerading
Masquerading lets Peer A forward and route traffic that it receives over WireGuard to other IPs, and also allows it to send the replies back to their original sources.
The ArchWiki points out at this point that it’s important for any internet-facing device to have a properly configured firewall. Fedora Server comes with firewalld
, but it might not be enabled on your system by default. Start it with:
$ sudo systemctl enable --now firewalld
We need to open port 51820/udp
in firewalld’s active zone, and enable masquerading. We can do this temporarily to test if it works by:
$ sudo firewall-cmd --zone=FedoraServer --add-port 51820/udp
$ sudo firewall-cmd --zone=FedoraServer --add-masquerade
Note: my default zone was “FedoraServer”, yours might differ. You can check your zones with firewall-cmd --list-all-zones
.
We should now (after restarting wg-quick on both peers), be able to ping Device A-2
at 192.168.0.9
from Peer B
!
Our traffic goes through Peer A
over WireGuard, no matter if we’re on the same LAN or somewhere else in the world, as long as we can establish a tunnel :)
Finally, I added this firewalld command to my wg0.conf so that it is added and removed as WireGuard is started and stopped:
[Interface]
# Name = A
...
PostUp = firewall-cmd --zone=FedoraServer --add-port 51820/udp && firewall-cmd --zone=FedoraServer --add-masquerade
PostDown = firewall-cmd --zone=FedoraServer --remove-port 51820/udp && firewall-cmd --zone=FedoraServer --remove-masquerade
Note: you can have multiple PostUp/PostDown values, they are all executed.
You could also simply add --permanent
to the firewall-cmd
snippets to write these changes out permanently.
What’s next?
With LAN peering configured, I hope you’re starting to imaging the possibilities that this network topology allows. Sometime soon I’ll be writing about how we can send Peer B’s (and C’s, and D’s, and on, and on) DNS traffic through a PiHole on Peer A (or maybe just on Peer A’s LAN??) - watch for that as part of my PiHole and CoreDNS blog post).
Read the other articles in this series here:
- New Year, New Lab
- #TODO - Epyc EKWB liquid cooled server build
- ZFS on Linux, ZED, and Postfix
- Configuring Postfix with Gmail
- WireGuard VPN mesh
- PiHole and DNS over WireGuard
- Private DNS with CoreDNS
- #TODO - VFIO GPU Passthrough
- #TODO - Networking: Unifi, VLANs, and (Core)DNS localzones over WireGuard
- Rescuing a bad Fedora upgrade via chroot