DIY Devices - The VPN
2021-07-28 DIY devices Wireguard VPNThere are times where it's really useful to be on my home network when I'm away. In the past I have achieved this with SSH port forwarding. It works well enough for most things - but it's a bit of a faff changing web proxies and of course it doesn't work easily for everything. I thought it would be easy just to set up OpenVPN. I was wrong.
Fast forward a few years and I saw Brian Moses tweeting about Tailscale, so I took a closer look. It's a really impressive service. And if I had to recommend a VPN to someone, it would be this.
The thing about Tailscale, though, is that it relies on a third party provider. It sounds like a great company with high ideals - but then look what happened to WhatsApp. Plus, I also have my own VPS and, well, I wanted to dig deeper, learn more and generally make it more complicated because that's more fun. It uses Wireguard underneath and the more I read about it, the more impressive it was - and when you have the likes of Linus Torvalds calling it a "work of art", it's something to be taken seriously.
There are so many different ways to configure any network - and a VPN is no different. I had thought that it might be nice to use my home router as the main VPN peer but my home IP, while pretty stable, is not guaranteed to stay stable. And while I can use DDNS to fix that, it's just another thing to go wrong. So, to prototype things, I thought I would get started by using my Debian VPS - hosted on Digital Ocean - to get things going.
And get it going I did. I am now able to connect to my home network and those of two separate families (with conflicting 192.168.0.0/24 subnets) and do so from anywhere - even my phone. Coupled with JuiceSSH I can do pretty much whatever I want wherever I am (although it's not conducive to easy working).
How? #
Installing on Debian (Buster) was just a matter of:
echo "deb http://deb.debian.org/debian/ buster-backports main" >> /etc/apt/sources.list
apt-get install wireguard wireguard-dkms wireguard-tools linux-headers-$(uname -r)
Then I chose an available subnet for a new VPN network and created a config file. I didn't do this in one go of course, because I spent ages reading about all the different options, getting it wrong and trying again. The best documentation I found was this repo by someone called pirate.
Go and read it, but in short:
- each VPN peer (endpoint) needs a public and private key. And each peer which needs to talk to another peer needs the public key of the other one.
- If you want to alter iptables as wireguard starts and stops, do so with
PostUp
andPostDown
- If you're routing anything (which a bounce server will) then you need to
enable forwarding both in the kernel and iptables
# to enable kernel relaying/forwarding ability on bounce servers
echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf
echo "net.ipv4.conf.all.proxy_arp = 1" >> /etc/sysctl.conf
sudo sysctl -p /etc/sysctl.conf - If you have a bounce server, then you will want to enable firewall rules to stop forwarding any old traffic.
AllowedIPs
is where a whole load of magic happens. This is essentially short for "If you want to reach these networks then send packets to this peer". In the case of simple endpoints, like a laptop, this is just the laptop's VPN IP address (using CIDR subnet notation - so a/32
prefix). If you're routing between networks then you can add additional networks with commas.
Conclusion #
I have stayed with my "prototype", using the VPS as the central peer. And I'm
really happy about it all. I love Wireguard. I love how easy it is to work with
(once you get what's going on). And it's SOOO much easier than OpenVPN. The one
downside I've found is that it breaks when the kernel gets updated - so
apt upgrade
can be bad. This doesn't matter for most of my peers since I can
also SSH to them - but for the remote peers inside other home networks this can
be bad. I haven't solved this yet, because it's not been too pressing, but I
expect a simple script would do the job.
Appendix #
The actual setup #
The example below shows:
- the main bounce server (vpn.example.com): this takes care of all routing and connections
- a laptop (laptop.example.com): a simple client node
- a relay server (relay.example.com): this sits on the VPN inside my home network and handles any traffic to it: 10.0.0.0/16
- and another network (other.example.com): this is, for example, a raspberry pi sitting in a relative's house where you need to do tech support. The interesting thing here is that the actual network at the other end is a 192.168.0.0/24 network - but it uses a different address and translates it.
The bounce server #
/etc/wireguard/wg0.conf
[Interface]
# Name = vpn.example.com
Address = 10.0.254.1/32
ListenPort = 51820
PrivateKey = ${vpn-private-key}
# My internal DNS server
DNS = 10.0.0.1
PostUp = iptables -I FORWARD 1 -i %i -j ACCEPT
PostDown = iptables -D FORWARD -i %i -j ACCEPT
[Peer]
# Name = laptop.example.com
PublicKey = ${laptop-public-key}
AllowedIPs = 10.0.254.9/32
[Peer]
# Name = relay.example.com
PublicKey = ${relay-public-key}
AllowedIPs = 10.0.254.10/32,10.0.0.0/16
[Peer]
# Name = other.example.com
PublicKey = ${other-public-key}
AllowedIPs = 10.0.254.11/32,172.16.0.0/24
Laptop #
Nothing special here - just that it can access any 10.0.0.0/16 or 172.16.0.0/16 address via the bounce server, and it uses my internal DNS server.
[Interface]
# Name = laptop.example.com
PrivateKey = ${laptop-private-key}
Address = 10.0.254.9/32
DNS = 10.0.0.1, 10.0.0.2
[Peer]
# Name = vpn.example.com
PublicKey = ${vpn-public-key}
AllowedIPs = 10.0.0.0/16, 172.16.0.0/16
Endpoint = ${bounce-server-ip:port}
Relay #
The relay sits inside my home network. It could be my router, but it's not - it's a VM (but could be a docker container). Notable things:
- When bringing up the wireguard interface it enables port forwarding and post routing NAT to the internal network. And reverses it on shutdown.
- The Allowed IPs do not include 10.0.0.0/16 since it's already on that network (albeit on a different network interface) - it's just the VPN subnet (10.0.254.0/24) and the "other" network 172.16.0.0/16.
- PersistentKeepalive just tells wireguard how often to ping the bounce server. See the core documentation for more on this, but it's because of UDP.
- Traffic is allowed in and out - forwarding can occur from the internal LAN to the VPN - although it requires a static route to be set on endpoints or the router.
[Interface]
# Name = relay.example.com
PrivateKey = ${relay-private-key}
Address = 10.0.254.10/32
PostUp = sysctl net.ipv4.ip_forward=1
PostUp = iptables -A FORWARD -i %i -j ACCEPT
PostUp = iptables -A FORWARD -i eth0 -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = sysctl net.ipv4.ip_forward=0
PostDown = iptables -D FORWARD -i %i -j ACCEPT
PostDown = iptables -D FORWARD -i eth0 -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]
# Name = vpn.example.com
PublicKey = ${vpn-public-key}
AllowedIPs = 10.0.254.0/24,172.16.0.0/16
Endpoint = ${bounce-server-ip:port}
PersistentKeepalive = 25
Other #
This builds on the previous examples but it also adds address translation.
- Any traffic coming in for the 172.16.0.0/24 network gets magically rewritten for 192.168.0.0/24. In this example, it's a bit pointless, but in real life, I have two 192.16.0.0/24 networks I need to connect to - this avoids collisions; Reference
- Any traffic from the device LAN (which is outside the VPN) on
eth0
gets dropped. So there is no route for other devices on the remote network into the VPN. This differs from the relay above. I don't trust the network this device sits in. As a result, though, we also need to track established and related connections otherwise it won't work - AllowedIPs could just be 10.0.0.0/16 since the VPN subnet is contained within. It's separate just for explicitness.
[Interface]
# Name = other.example.com
PrivateKey = ${other-private-key}
Address = 10.0.254.11/32
PostUp = sysctl net.ipv4.ip_forward=1
PostUp = iptables -A FORWARD -i %i -j ACCEPT
PostUp = iptables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
PostUp = iptables -A FORWARD -i eth0 -j DROP
PostUp = iptables -t nat -A PREROUTING -d 172.16.0.0/24 -i %i -j NETMAP --to 192.168.0.0/24
PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = sysctl net.ipv4.ip_forward=0
PostDown = iptables -D FORWARD -i %i -j ACCEPT
PostDown = iptables -D FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
PostDown = iptables -D FORWARD -i eth0 -j DROP
PostDown = iptables -t nat -D PREROUTING -d 172.16.0.0/24 -i %i -j NETMAP --to 192.168.0.0/24
PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]
# Name = vpn.example.com
PublicKey = ${vpn-public-key}
AllowedIPs = 10.0.254.0/24,10.0.0.0/16
Endpoint = ${bounce-server-ip:port}
PersistentKeepalive = 25
Bounce server firewall rules #
If you're enabling forwarding on a public facing server, then you will want to stop some of that. Yours will be different - but this is a good start
# /etc/iptables/rules.v4
# Install iptables-persistent
# `sudo apt-get install iptables-persistent`
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
COMMIT
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
# ==============================================================================
# Service rules
# basic global accept rules - ICMP, loopback, traceroute, established all accepted
-A INPUT -s 127.0.0.0/8 -d 127.0.0.0/8 -i lo -j ACCEPT
-A INPUT -p icmp -j ACCEPT
-A INPUT -m state --state ESTABLISHED -j ACCEPT
-A INPUT -m conntrack --ctstate INVALID -j DROP
# enable traceroute rejections to get sent out
-A INPUT -p udp -m udp --dport 33434:33523 -j REJECT --reject-with icmp-port-unreachable
# SSH, Wireguard
-A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
-A INPUT -p udp -m udp --dport 51820 -j ACCEPT
-A INPUT -j DROP
# ==============================================================================
# Forwarding rules
-A FORWARD -j DROP
COMMIT
Install on Raspberry Pi #
Installing Wireguard on a Raspberry Pi is slightly more involved, but this works really well. See the initial link for any further updates.
## https://github.com/adrianmihalko/raspberrypiwireguard
sudo apt-get install raspberrypi-kernel-headers
echo "deb http://deb.debian.org/debian/ unstable main" | sudo tee --append /etc/apt/sources.list.d/unstable.list
sudo apt-get install dirmngr
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 8B48AD6246925553
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 7638D0442B90D010
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 04EE7237B7D453EC
printf 'Package: *\nPin: release a=unstable\nPin-Priority: 150\n' | sudo tee --append /etc/apt/preferences.d/limit-unstable
sudo apt-get update
sudo apt-get install wireguard
sudo reboot