Home VPN with Wireguard

If you have an interest in Linux networking, by now you've probably heard of Wireguard. In case you haven't, it's a newer cross-platform VPN whose main attraction is that it's way way easier to set up on Linux than other VPNs that have come before it. If you've ever had to set up an IPSec VPN using Racoon or Openswan or StrongSwan or any other weird animal-based tool, then you know how much of a royal pain in the ass it is.

While I was on a tear improving my home network, I decided to finally bite the bullet and set Wireguard up on my router and laptop, allowing me to securely connect to my home network from anywhere. This post outlines what I did to make that happen, in case you want to replicate it in your own home network.

Fair bit of warning though: I'm assuming that you already have basic knowledge of networking: subnetting, routing, that kind of stuff.

My Home Network

To give you an idea of what I'm working with, the VPN server here will be my router running Ubuntu Linux, and the client will be my laptop running Arch. My router setup is fairly basic for a home-built Linux router: it has two network interfaces, one internal and one external, has IP forwarding turned on in the kernel, and a bunch of iptables rules to handle forwarding packets around and NAT.

For the examples throughout this post, we'll call the VPN server router and the client laptop. We'll assume that my home network's subnet is 10.0.0.0/24, my router's internal network interface is called intf-internal and my external interface is called intf-external. Since we need a subnet for the VPN clients to use, we'll use 10.1.0.0/24 for that.

I'll be giving you a crash-course in the basics of Wireguard here, but if you want a deeper understanding of it you should visit Wireguard's website for a more in-depth overview.

Installation

Wireguard comes baked into the Linux kernel these days, but you'll need to install the tools to be able to work with it.

On Debian-based systems:

sudo apt install wireguard

On Arch Linux:

sudo pacman -S wireguard-tools

Key Generation

Wireguard uses public-key encryption for all communication, and you need to generate keypairs for each peer in the VPN.

I like to be fancy and do all in mostly one line and have the public key printed:

wg genkey | sudo tee /etc/wireguard/priv.key | wg pubkey | sudo tee /etc/wireguard/pub.key
sudo chown 600 /etc/wireguard/priv.key

Server Configuration

WireGuard

Okay! Now that we have our keys generated it's to configure Wireguard.

Here's my config in /etc/wireguard/wg-home.conf:

[Interface]
# This Address both tells us the server's VPN address (10.1.0.1) as well as the
# subnet (10.1.0.0/24)
Address = 10.1.0.1/24
# This is the UDP port the VPN server will be listening on
ListenPort = 51820
# This is the contents of /etc/wireguard/priv.key on the server, which you
# generated earlier. NOTE: This is the literal key, NOT the path to the key file
PrivateKey = <server priv.key>

[Peer]
# This is the pub.key from our laptop. Like the priv key, this should be the
# literal key, not the path to the key file.
PublicKey = <client pub.key>
# This is the IP address we've assigned to our client
AllowedIPs = 10.1.0.11/32

# You can keep adding more [Peer] blocks as you have more machines

This is a straight-forward config: under [Interface] we have an address with a subnet defined, a port to listen on, and a peer defined. Under [Peer] the AllowedIPs option in Wireguard configs is interesting because it essentially defines what routes the peer on the other side handles for us; anything destined for an IP in one of these subnets will get sent to that peer. In this case it only handles traffic for itself, so we configure a single IP address, so it's a single /32.

Since we saved this file as /etc/wireguard/wg-home.conf, it will correspond to a virtual interface on the router called wg-home. We want this interface to come up automatically when the server boots up, and there are various ways to do that depending on what network management system you use (systemd-networkd, NetworkManager, etc.) but I prefer to just use the systemd service units that are available: wg-quick@<interface name>.service

sudo systemctl enable [email protected]
sudo systemctl start [email protected]

If everything worked, you should be able to use the wg show command to see that the configuration is applied:

$ sudo wg show
interface: wg-home
  public key: <server pub.key>
  private key: (hidden)
  listening port: 51820

peer: <client pub.key>
  allowed ips: 10.1.0.11/32

Iptables

Since we have custom iptables rules to handle the routing and NATing, we also need some rules to allow

# Allow outside clients to connect to Wireguard
-A INPUT -i intf-external -p udp --dport 51280 -j ACCEPT
# Anything coming from the VPN can talk to the router directly
-A INPUT -i wg-home -j ACCEPT

# VPN clients are allowed to talk to the rest of the network, and vice versa
-A FORWARD -i wg-home -o intf-internal -j ACCEPT
-A FORWARD -i intf-internal -o wg-home -j ACCEPT

If you're running IPv6 on your network, don't forget to also add the same rules via ip6tables!

Client Configuration

Okay we're in the home stretch! We just need to configure the client. Just like the server, I've saved this config to /etc/wireguard/wg-home.conf so that it will show up as the wg-home virtual interface.

[Interface]
Address = 10.1.0.11/24
PrivateKey = <client priv.key>
DNS = 10.0.0.1, home.your.net

[Peer]
PublicKey = <server pub.key>
AllowedIPs = 10.1.0.0/24, 10.0.0.0/24
Endpoint = vpn.your.net:51820

This is quite a bit like the server's config, but with some interesting changes. First, in [Interface] we define DNS. Any IPs listed here will be used as DNS servers, and any non-IPs will be used as search domains. Using your router for DNS with a search of your home network's domain means you can reference your home computers by hostname, like ssh router, once you're connected to the VPN.

Under [Peer] you'll see an Endpoint, which is the external hostname for the router. You'll also see that AllowedIPs is way different, 10.1.0.0/24, 10.0.0.0/24. This says that those two subnets are available on the other side of the VPN, and any traffic destined for them should be sent over the VPN. Be careful to not set this too wide, or otherwise you may disrupt local (to your laptop) traffic if the public network you connected to uses 10.x.x.x.

This sort of setup is typically called "split tunnel" in the VPN world, where only a subset of traffic is being sent through the VPN. If instead you wanted all of your traffic sent over the VPN, you could just set AllowedIPs = 0.0.0.0/0, ::/0, which will send all IPv4 and IPv6 traffic over.

I also like to use the systemd service unit to manage this connection, but I don't enable it so it doesn't automatically start when my laptop boots. Instead I just start it ad-hoc when I want to connect to home.

sudo systemctl start [email protected]

Just like the server, you can use wg show to see the status:

$ sudo wg show
interface: wg-home
  public key: <client pub.key>
  private key: (hidden)
  listening port: 59184

peer: <server pub.key>
  endpoint: <router external IP>:51820
  allowed ips: 10.1.0.0/24, 10.0.0.0/24
  latest handshake: 4 seconds ago
  transfer: 1.52 KiB received, 2.09 KiB sent

And if we ping our server's VPN address:

$ fping 10.1.0.1
10.1.0.1 is alive

Hooray!

Things to note

If your server has a tendency to change IPs (like home internet connections do), and your Endpoint in your client's config points to a dynamic DNS address, you'll need to either restart [email protected] every time that happens, or use the reresolve-dns.sh script that comes with the Wireguard tools to handle this. The Arch Linux wiki has a good writeup on how to set that up.

The systemd service units make use of the wg-quick program to do the setup of the VPN tunnels. You may think it's magical how it routes packets, but as a matter of fact it just sets up an interface and routes, which you can inpect just like any other interface or route in Linux:

$ ip addr show dev wg-home
80: wg-home: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
    link/none
    inet 10.1.0.11/24 scope global wg-home
       valid_lft forever preferred_lft forever

$ ip route | grep wg-home
10.0.0.0/24 dev wg-home scope link
10.1.0.0/24 dev wg-home proto kernel scope link src 10.1.0.11