The elevator pitch for this post: configuring a Raspberry Pi to run a deluged BitTorrent server only through OpenVPN.
Read on if that sounds like fun.
The backstory
My home network has included a PiHole device for a few years running on a Raspberry Pi Zero.
These little things are great at blocking ads across your entire network. I let mine run DHCP too since it’s a lot nicer to work with than my ISP-supplied router.
I also recently added a big NAS to my network. It’s quite old but holds up fine for normal file storage although isn’t capable of anything fancy like say, running a BitTorrent client, which is something I’ve always desired.
So I think to myself, “what if I upgraded the Pi and ran a torrent server too.”
What EXACTLY are we doing?
Specifically, I’ll be showing my setup on Raspbian Core involving OpenVPN, deluged and network namespaces, all packaged in systemd units for “set and forget”- ness.
I’m running this on a Raspberry Pi 3B – so quad core, 1GB memory.
EDIT: I now run this on a Pi 2B which is a little slower but is able to run with much less power with a little tuning.
To false starts and dead ends
The VPN requirement caused most of my issues in making this work.
While I need all BitTorrent traffic to go through the VPN I obviously don’t want the PiHole’s DNS lookups to be affected (nor the XLink Kai server I experimented with running on here!)
When I started searching for how to direct specific applications through OpenVPN I was finding esoteric solutions like marking all traffic running as a specific user to be redirected via nftables. It was all a bit hacky and odd.
Then I found out about network namespaces.
These are new to me and kind of magic, and it’s key to my solution.
In modern Linux you can declare multiple network namespaces, each basically being a completely separate network stack with it’s own devices and traffic rules. We can then launch applications within the namespace, so all their networking obeys the new rules.
There’s a few different ways of achieving these with namespaces, but they’re mostly down to how you construct your virtual network for the namespace to communicate with the normal network.
Ultimately I think the code below is a concise and understandable way of achieving the result.
Chapter #1, Making a name(space) for yourself
First off we’ll make a script to create the namespace and set up the traffic rules.
#!/bin/bash NSNAME="ovpn"; ip netns delete ${NSNAME}; ip netns add ${NSNAME}
We need to provide a DNS server for our namespace. To do this we create a folder with our namespace name at /etc/netns/
and any files we put inside will be symlinked in to /etc
in our namespace.
I’m using 1.1.1.1 in my resolv.conf
rather than the PiHole resolver already running on the device because 1) I don’t care about blocking ads here, and 2) that’s likely a good way to leak my IP.
mkdir -p /etc/netns/${NSNAME}/; echo "nameserver 1.1.1.1" > /etc/netns/${NSNAME}/resolv.conf;
Next we need to mesh our networks together.
I’m using a macvlan device in bridge mode which lets us quite painlessly connect our namespace to the local physical network.
In my code eth0 is the physical network device I’m bridging with since I’m using the Raspberry Pi’s built in ethernet port. You can see your network devices with ip link
.
# Bring up the namespace’s loopback interface ip netns exec ${NSNAME} ip link set lo up # Create a macvlan, attach it to the namespace and bring it up ip link add macvlan${NSNAME} link eth0 type macvlan mode bridge ip link set macvlan${NSNAME} netns ${NSNAME} ip netns exec ${NSNAME} ip link set macvlan${NSNAME} up
The final two lines handle addressing on the physical network for this namespace and therefore the IP addresses could be different in your setup.
# Set the IP for the namespace – our deluged server will be available at this address on the local network. # In theory you could use dhcp here, but I had issues getting it to work. ip netns exec ${NSNAME} ip addr add 192.168.1.5/24 dev macvlan${NSNAME} # Set the default gateway within the namespace. ip netns exec ${NSNAME} ip route add default via 192.168.1.1
Great! Running stuff in this network should now work, since it’s basically a clone of the default namespace at the moment.
Chapter #2, VPN
I’m not going to go in to too much detail about setting up an OpenVPN script as this will vary by providor.
My VPN provider has a generator for OpenVPN scripts which are configured to allow local network traffic and send everything else through the VPN. This was perfect.
The only change I made was adding the auth-user-pass
argument to automate providing my login credentials. All you do is give it a path to a file with username and password on consecutive lines.
# Add auth-user-pass to your OpenVPN script and point it to a file containing your credentials auth-user-pass /root/vpn_credentials ... # Meanwhile, in your file at /root/vpn_credentials (hopefully chowned to root and chmoded to 600) # put your username and password on seperate lines my-vpn-username my-vpn-password
All we need to do now is start this OpenVPN connection within our network namespace – it will get routes from the VPN provider to redirect all traffic but they’ll only apply within it’s namespace, so we’ve effectively isolated the VPN connection.
ip netns exec ovpn openvpn --config /root/vpn_connect.conf
Once it’s connected you can see whether your IP is different by running the same command in- and outside of the namespace.
# This will just return your public IP # This should print your real IP, e.g, 1.2.3.4 # You can specify IPv4/6 with the '-4' or '-6' argument to curl. curl icanhazip.com # Now let’s try it within our VPN namespace # This should print your VPN IP, e.g, 4.3.2.1 ip netns exec ovpn curl icanhazip.com
Chapter #3, a torrent of information
Finally we want to run deluged within our namespace.
This is as trivial as starting it within the namespace as we did with OpenVPN, and because local network traffic is unaffected we can connect thin clients without a problem.
See the Deluge documentation for setup instructions.
Once you’re ready you can start deluged with:
ip netns exec ovpn deluged
Chapter #4, wrapping it all up in systemd
In the current setup, what happens if the VPN gets disconnected?
The VPN namespace will revert to being just like the default namespace and our BitTorrent traffic will go over our connection plain. That’s bad.
Since systemd has ways of defining dependencies and automatically restarting failed units I thought it would be a great way to wire everything up.
First a unit to create our network namespace, simply calling the script described earlier. I’ve saved this at /etc/systemd/system/ovpn-namespace.service
.
[Unit] Description=ovpn Network Namespace # Tell systemd to start this unit once the network is up After=network-online.target [Service] Type=simple # The command to run ExecStart=/root/netns-up.sh # Automatically restart if this unit fails Restart=on-failure # Time to wait before forcefully stopped. TimeoutStopSec=300 [Install] # Hinting that our yet-to-be-defined VPN unit depends on this unit WantedBy=vpn.service
Next a unit to bring up the VPN connection in the network namespace, saved at /etc/systemd/system/vpn.service
.
[Unit] Description=OpenVPN Daemon Documentation=man:openvpn After=network-online.target ovpn-namespace.service Requires=ovpn-namespace.service [Service] Type=simple ExecStart=ip netns exec <namespace name> openvpn --config /root/vpn_connect.conf Restart=on-failure TimeoutStopSec=300 [Install] WantedBy=deluged.service
…and finally a unit for deluged at /etc/systemd/system/deluged.service
.
[Unit] Description=Deluge Bittorrent Client Daemon Documentation=man:deluged After=network-online.target mnt-NAS\x2dDownloads.mount vpn.service ovpn-namespace.service Requires=ovpn-namespace.service vpn.service mnt-NAS\x2dDownloads.mount [Service] Type=simple ExecStart=/sbin/ip netns exec <namespace name> sudo -u "debian-deluged" /usr/local/bin/deluged -d -l /var/log/deluged/daemon.log -L info --logrotate Restart=on-failure TimeoutStopSec=300 [Install] WantedBy=multi-user.target
Note the “Requires” in my deluged unit also includes a network mount – the download target for deluged is on my NAS. This automagically works for any mounts defined in /etc/fstab
, with the dependency name being based on the local mount point – here, my mount is /mnt/NAS-Download
.
Also note the -l
/ -L
arguments to deluged which enable logging and specify the log file and log level – you can omit these, but it’s certainly useful for debugging your deluged setup.
All that’s left is to enable the deluged unit to run on startup and see if it’s working.
systemctl enable deluged
Thanks folks, I hope this is useful for someone.