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: