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