Homelab Act 1: VPNs
For the last 170 days (according to uptime on one my routers), I've been setting up a small homelab. Homelabs are kind of cool, and, the setup has been interesting. I'll be writing a few posts explaining the steps I took.
- Part 1 (this post)
- Part 2 Servers and 10GbE
- Part 3 NAS, ZFS, and NFS
- Part 4 Services and Applications
VPNs
Unfortunately, I don't have a public IP in my building, so setting up remote access had to be a little more involved than just opening a port for ssh. I found a tiny, passively cooled, quad-port intel NIC, celeron box on amazon, and figured I'd try setting up my own linux router and run my own VPN server in AWS or something.
This took a bit of work.
Networking
Before diving in, I needed to decide how to layout my network and what VPN tools to use. I read a ton about best practices, but this wasn't super helpful, so instead, I just started spinning up VMs and messing with VPN software and playing with my network settings.
Eventually I settled on wireguard for a VPN, mostly because I never actually got OpenVPN working anywhere. I also spent bunch of time trying to get IPSec and layer 2 tunneling to work, but decided that I didn't really want that anyway. Wireguard is easy, fast, and probably secure, so I'm using that for now.
The network is organized as:
- One subnet for each "region"
- My apartment is on the
172.16.1.xxx
subnet (for snobs:172.16.1.0/24
) - A cloud region on the
172.16.2.xxx
subnet- I wanted to be able to use cloud VMs for other services, so I found a provider that suported private networking
- Wireguard speaks IP, so it needs a subnet too. It got
172.16.255.xxx
- Each device connected to the VPN gets an IP on this subnet
- My apartment is on the
- Use one private, made-up subdomain for each "region." For example, you could use
apartment.me.com
,cloud.me.com
, andvpn.me.com
.- I'm using a private subdomains of a domain that I control to ensure that I never clash with anything that actually exists.
- Have a DHCP server and DNS server available in each region (the DHCP host will always be
xxx.xxx.xxx.1
and have hostname/dns namegateway
)- DNS is authoritative for its subdomain
- VPN region just routes DNS to the cloud region
- The VPN
gateway
is just the VPN server - There is no DHCP for VPN subnet since all VPN clients wil have a static IP
- The VPN server/cloud gateway has a public IP
- Probably also want a firewall that drops everything other than VPN traffic (I'm using cloud provider firewall and a firewall on the server)
I haven't figured out ipv6 yet because I'm a bad person.
The 172.16.xxx.xxx
prefix was selected to try and avoid conflicting with commercial subnets (10.xxx.xxx.xxx
) and common private subnets (192.168.xxx.xxx
).
The entire 172.16.0.0/12
subnet is private, so we can do whatever we want in this range.
Unfortunately, a local coffee shop I frequent uses an IP range that clashes with mine, so I have to get clever with routing rules when I am working there.
Install alpine linux on local router
The official install guides are good and more up to date than anything I'd have to say about this.
Setup Basic Networking
In /etc/network/interfaces
:
auto eth0 iface eth0 inet dhcp hostname gateway auto eth1 iface eth1 inet static address 172.16.1.1 netmask 255.255.255.0
eth0
is hooked to my ISP (so I get a DHCP ip), and eth1
was hooked to a tiny TP-Link switch I had laying around.
Firewall and NAT
After spending time banging my head against the iptable
, I gave up and tried using a this thing built into alpine called awall.
There's a pretty good Zero to Awall guide available which can get you started.
I don't want to explain a full example (the docs are again better than what I can do), but here's some highlights.
- Enable packet forwarding
The linux kernel must be told that it is allowed to forward packets. Put
net.ipv4.ip_forward = 1
in asysctl.conf
file on alpine, see https://wiki.alpinelinux.org/wiki/Sysctl.conf This is probably needed for ipv6 as well, if you aren't a bad person who is ignoring ipv6, like me - Most (all?) for awalls config files can be written in
yaml
The Zero To Awall guide has this example:
/etc/awall/private/custom-services.json
:{ "service": { "openvpn": [ { "proto": "udp", "port": 1194 }, { "proto": "tcp", "port": 1194 } ] } }
But, you could also create an equivelent
/etc/awall/private/custom-services.yaml
if you want:service: openvpn: - { proto: udp, port: 1194 } - { proto: tcp, port: 1194 }
Tricks
In case the internet every goes down, I sometimes need to refresh my ISP DHCP lease to get it to come back up.
I stuck a checkinit.sh
script into my $PATH
somewhere, then added it to cron
to run once a minute:
gateway:~# crontab -l # min hour day month weekday command * * * * * checkinet.sh| logger -t checkinet gateway:~# cat $(which checkinet.sh) #!/bin/sh echo "Checking if internet still up" # does not use our dns server, uses isp if ! ping -c5 google.com; then echo "bouncing network interface" ifdown eth0 ifup eth0 #unbound needed to be restarted, dnsmasq appears to be fine with this #sleep 30 #/etc/init.d/unbound restart # idk why this needs to happen else echo "Internet still up!" fi
This is really only testing if I can resolve google.com
, since ping will probably work if I can reach DNS to resolve google, but whatever.
The script gets me back up and going if I unplug stuff or if my ISP flakes out for some reason (which has only happened twice ever, this fixed it the second time), and it's never killed my internet spuriously, so I guess it works?
I also:
- Cranked up the syslog file size and max files to keep around by editing an init file (probably the wrong way to do it)
- Installed the S.M.A.R.T. tools (since there's an SSD in the thing)
- Created a cron job to run smart tests sometimes and log it somewhere (which I've never looked at)
Setup dnsmasq as a DHCP server and DNS server
The arch wiki has wonderful docs for this. Just go read those.
All I really had to do in the end was:
- Turn on DHCP and DNS servers
- Enable
dhcp-authoritative
- Provide useful defaults to connected clients:
dhcp-option=option:router,172.16.1.1
- Enable
- Tell dnsmasq what interfaces to listen on and from where to allow DNS queries
- Tell dnsmasq which domain it is going to be authoritative for
domain=<whatever>.me.com
andlocal=/<whatever>.me.com/
- Configure dnsmasq to resolve gateway.<whatever>.me.com to the
172.16.1.1
host- Create a file called
/etc/hosts.dnsmasq
with the only the line172.16.1.1 gateway
- Tell dnsmasq not to read the
/etc/hosts
file with theno-hosts
configuration option - Then, give dnsmasq the configuration
addn-hosts=/etc/hosts.dnsmasq
- This way, the local networking does not have to be tainted by anything I might want a fixed IP for.
- Create a file called
- Log a lot
dhcp-script=/bin/echo
,log-queries
, andlog-dhcp
Download pi-hole's ad domain blacklist
From https://github.com/notracking/hosts-blocklists. Put the tracking domain lists somewhere then just set:
conf-file=/path/to/domains.txt addn-hosts=/path/to/hostnames.txt
In the dnsmasq config file. See the dnsmasq docs for an explanation of the difference.
Pay for and plug in some sort of Wireless Access Point
I bought a Unifi AP and followed the instructions to set it up. It works.
Setup alpine and DNS on a cloud server somewhere
Same as above mostly, just with a different made-up star trek themed subdomain.
Wireguard
Each device that can connect to the server needs a private/public key pair. The server contains a list of recognized public keys; only the devices in the server config can connect.
There's a wireguard-tooling package available that you can use to generate keys. Generate keys for each device (including the server):
$ umask 077 # make sure no one can read your files $ wg genkey | tee private_key | wg pubkey > public_key $ ls private_key public_key
Once you are done copying the contents of these files into the wireguard configs, delete them.
On the VPN server (cloud instance)
Create a wireguard server config at /etc/wireguard/wg0.conf
.
Note that I am not using the wg-quick
interface for this or the apartment router.
gateway:~# cat /etc/wireguard/wg0.conf [Interface] PrivateKey = ..... # put the contents of the private key file here ListenPort = .... # 51820 seems to be standard port # For each device that can connect to the VPN, create a [Peer] block # gateway router in apartment [Peer] PublicKey = ..... # put the contents of the public key file here # The AllowedIPs list is sort of like a routing table # In this section, we specify which IPs may be reached by directing traffic to this peer. # For the apartment router: # - assign the VPN IP: 172.16.255.2 and # - allow wireguard to route traffic from the VPN subnet to the 172.16.1.0/24 using this peer AllowedIPs = 172.16.255.2/32, 172.16.1.0/24 # laptop [Peer] PublicKey = ..... # put the contents of the public key file here # laptop is assigned a static ip. # this static ip is the only thing I'm allowing the VPN network to access AllowedIps = 172.16.255.3/32 # .... more peers here
Next, configure kernel's networking stack:
- create a new interface named
wg0
- use the
wg
tool to set the interface config file - set a static ip/netmask for this interface/subnet
- Add a routing table entry to route traffic from the cloud subnet to the apartment subnet over the
wg0
interface
This is done on alpine by adding more stuff to /etc/network/interfaces
:
auto wg0 iface wg0 inet static address 172.16.255.1 netmask 255.255.255.0 pre-up ip link add dev wg0 type wireguard pre-up wg setconf wg0 /etc/wireguard/wg0.conf post-up ip route add 172.16.1.0/24 dev wg0 post-down ip link delete wg0
On the apartment gateway
The router in my apartment is a VPN client, maintaining a persistent connection to the VPN server.
In /etc/wireguard/wg0.conf
put something like:
[Interface] PrivateKey = .... # private key associated with this peer [Peer] Endpoint = <public ip of VPN server>:<port of VPN server> PublicKey = ...... # public key goes here PersistentKeepalive = 25 # keep the connection alive at all times # Allow the apartment router to route traffic into: # - VPN subnet # - cloud subnet AllowedIPs = 172.16.255.0/24, 172.16.2.0/24
Create the new interface in /etc/network/interfaces
:
auto wg0 iface wg0 inet static address 172.16.255.2 netmask 255.255.255.0 pre-up ip link add dev wg0 type wireguard pre-up wg setconf wg0 /etc/wireguard/wg0.conf post-up ip route add 172.16.2.0/24 dev wg0 post-down ip link delete wg0
On a "dynamic" VPN client
On machines like my laptop, I want to easily bring the VPN up and down.
This is easy to do with the wg-quick
tool.
wg-quick
allows you to add a few more entries to the config file.
When you run wg-quick up wg0
, it will bring up the interface, configure routing, and PostUp/PostDown scripts.
Here's the config from my (arch linux/systemd) laptop:
[Interface] Address = 172.16.255.3/32 PrivateKey = .... # private key for this device # After coming up, reconfigure my domain resolution. # I'm on the vpn subdomain now. I resolve DNS queries with the cloud region's DNS server PostUp = printf 'domain vpn.me.com\nnameserver 172.16.2.1' | resolvconf -a %i -m 0 -x # dnsmasq caches queries, so restart it to make sure the cache is clean PostUp = systemctl restart dnsmasq # on teardown, undo the DNS resolver tweaks PostDown = resolvconf -d %i [Peer] Endpoint = <server public ip>:<server public port> PublicKey = ...... # public key for the server PersistentKeepalive = 25 # Route *all traffic* through the VPN AllowedIPs = 0.0.0.0/0, ::/0 # Alternatively, we could use a list like: # AllowedIPs = 172.16.255.0/24, 172.16.2.0/24, 172.16.1.0/24 # to route only internal traffic through the VPN. # This list can be as precise as you need it to be.
- Laptop Lid
When my laptop lid closes, I kill the wireguard connection with a systemd unit file. This seems to minimize confusion when I close my laptop and take it somewhere.
In
/etc/systemd/system/wg-down.service
:[Unit] Description=Kill wg when machine goes to sleep After=suspend.target [Service] Type=oneshot ExecStart=sh -c '(ip link show wg0 && wg-quick down wg0) || true' [Install] WantedBy=suspend.target
Tweak dnsmasq config again
Make sure that the DNS servers know how to send queries to each other:
In the apt.me.com dnsmasq config:
# Add other name servers here, with domain specs if they are for # non-public domains. server=/cloud.me.com/172.16.2.1 server=/2.16.172.in-addr.arpa/172.16.2.1
In the cloud.me.com dnsmasq config:
# Add other name servers here, with domain specs if they are for # non-public domains. server=/apt.me.com/172.16.1.1 server=/1.16.172.in-addr.arpa/172.16.1.1 # Allow VPN to use the cloud-region's DNS server server=172.16.2.1@wg0
Plug it all in
I plugged the new router box into the wall (on port 0), and plugged a small 4-port TP-link switch into port 1. Everything else is plugged into the TP-link switch.
Finally: use the system
Good
- Wireguard is rock solid, even on my phone and from an airplane.
- My local network performance is incredible
- The tracker block lists noticeably effect load times for some sites
- The latency/bandwidth I get back to my apartment is low/high, from everywhere I've been in Chicago
Bad
- DHCP lease refreshes are slow for me right now
- When my lease expires, sometimes I'll see connectivity blips
- The latency/bandwidth I get from when connecting to my apartment or cloud instance in Chicago from somewhere like Florida seems poor
- This is probably an issue with my choice of cloud vendor
- The tracker block lists break lots of things, which is sometimes annoying
- Many tracker links are broken (emailed, google sponsored, etc)
- Facebook behaves strangely
- I haven't setup ipv6
Overall, I'm extremely happy with how this turned out.