Configuring Transmission and OpenVPN
Published on .This is a description of how I configured Transmission and OpenVPN on Arch Linux, using iproute2 network namespaces to isolate the VPN connection.
Background
A VPN, or Virtual Private Network, is a networking technique which extends a private network across the public internet. You may have seen VPN companies advertising at the beginning of YouTube videos or podcasts. It is an industry that, like mail-order matresses and meal delivery services, is both high in margin and high in customer turnover, making it perfect for mass advertising campaigns.
The most common reason for using a VPN as an individual is to hide your IP address when browsing online. This may be especially relevant when using BitTorrent, a peer-to-peer filesharing protocol. Transmission is a daemon which manages shared files over the BitTorrent protocol.
OpenVPN is open-source software used to manage VPN connections. OpenVPN implements both a VPN server as well as a client, although, I'll only use the client application in this article.
First, I'll show you the most basic way to configure a VPN to route all traffic on a host through the VPN. Then, I'll show you how to isolate VPN traffic to certain applications, such as Transmisison.
Starting Out: Baby Steps
By default, Arch Linux ships a systemd unit file that can be used to start OpenVPN. Here it is:
# /usr/lib/systemd/system/openvpn-client@.service
[Unit]
Description=OpenVPN tunnel for %I
After=network-online.target
Wants=network-online.target
Documentation=man:openvpn(8)
Documentation=https://openvpn.net/community-resources/reference-manual-for-openvpn-2-6/
Documentation=https://community.openvpn.net/openvpn/wiki/HOWTO
[Service]
Type=notify
PrivateTmp=true
WorkingDirectory=/etc/openvpn/client
ExecStart=/usr/bin/openvpn --suppress-timestamps --nobind --config %i.conf
User=openvpn
Group=network
AmbientCapabilities=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SETPCAP CAP_SYS_CHROOT CAP_DAC_OVERRIDE
CapabilityBoundingSet=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SETPCAP CAP_SYS_CHROOT CAP_DAC_OVERRIDE
LimitNPROC=10
DeviceAllow=/dev/null rw
DeviceAllow=/dev/net/tun rw
ProtectSystem=true
ProtectHome=true
KillMode=process
[Install]
WantedBy=multi-user.target
The @
symbol in the filename indicates that this is
a parameterized service - that is to say, we can run multiple
instances of it. The name of each instance comes after the
@
symbol. In this case, the name of the instance is
the name of the configuration file seen in this line:
ExecStart=/usr/bin/openvpn --suppress-timestamps --nobind --config %i.conf
My VPN provider makes available an .ovpn
configuration file for connecting with OpenVPN. We can drop this
file into /etc/openvpn/client/myvpn.conf
. After this
OpenVPN still needs a username and password for authentication.
Let's add an auth file in the same directory as the conf file.
Since we're storing passwords in plain text, we should make sure that the permissions for the folder are restricted (on my distribution, this is the case by default):
[root]# stat -c "%a %n" /etc/openvpn/client
750 /etc/openvpn/client
The auth file will be at
/etc/openvpn/client/myvpn.auth
, containing the
username and the password of my VPN account.
We'll have to tell OpenVPN where this file is. Let's go ahead and
copy the systemd unit file into /etc
, so we can edit
it.
[root]# cp /usr/lib/systemd/system/openvpn-client@.service /etc/systemd/system/myvpn.service
As an aside here, you can also edit unit files with
systemctl edit
command. This will allow you to
override certain options in the unit file while keeping the rest
the same. The advantage is that any future updates pushed by the
upstream will be able to take effect. However, because we're
going to be changing how this unit file functions, to avoid
confusion, I'd like to give it a new name. This way, any unit or
script which depends on the old functionality will not be
affected.
Although because I'm only running this on my local laptop, the choice doesn't really matter - either would be fine. Perhaps on a production server it would be more significant.
Anyways, we'll change the ExecStart
line as follows:
# /etc/systemd/system/myvpn.service
# ...
ExecStart=/usr/bin/openvpn \
--suppress-timestamps \
--nobind \
--config myvpn.conf \
--auth-user-pass myvpn.auth
# ...
We don't need to run multiple instances of this, so I'll hard-code everyting that was previously generic.
With that, we can start the service:
[root]# systemctl daemon-reload
[root]# systemctl start myvpn
And the VPN will connect. Great success!
[root]# systemctl status myvpn
● myvpn.service - OpenVPN tunnel for my VPN
Loaded: loaded (/etc/systemd/system/myvpn.service; disabled; preset: disabled)
Active: active (running) since Sat 2024-12-28 22:11:03 EST; 15s ago
Invocation: ad06942050584cb1b8973139921984b5
Docs: man:openvpn(8)
https://openvpn.net/community-resources/reference-manual-for-openvpn-2-6/
https://community.openvpn.net/openvpn/wiki/HOWTO
Main PID: 1379899 (openvpn)
Status: "Initialization Sequence Completed"
Tasks: 1 (limit: 28629)
Memory: 2.4M (peak: 3.1M)
CPU: 54ms
CGroup: /system.slice/myvpn.service
└─1379899 /usr/bin/openvpn --suppress-timestamps --nobind --config myvpn.conf --auth-user-pass myvpn.auth
A Gentle Introduction to Namespaces
The most common configuration of any VPN provider is to set the VPN to be the default route for your computer, routing all traffic through the VPN. This is usually the desired behavior. However, in my case, I only want the traffic from Transmission to go through the VPN. There's no reason to send any other traffic to them; the VPN provider isn't inherintly more trustworthy than my ISP.
OpenVPN works by creating a kind of virtual network device, TUN, to route traffic to the VPN. After the VPN is started, all traffic goes through this TUN device.
In order to route some traffic to the VPN, and other traffic to the normal internet, we can create a namespace with iproute2. We can link the namespace to the host machine with a virtual ethernet pair.
A namespace is a copy of the network stack with its own configuration, routes, and network devices. If we put the VPN in a namespace, all of the traffic inside of that namespace will be routed through the VPN, while all traffic outside of the namespace will go to the internet as normal.
We'll start by creating the namespace:
[root]# ip netns add myvpn
Then, we'll create a virtual ethernet pair. By placing one virtual device inside the namespace, and one device outside of the namespace, we are able to route packets between the host and programs that are sandboxed inside of the namespace.
[root]# ip link add veth_host type veth peer name veth_openvpn
[root]# ip link set veth_openvpn netns myvpn
Then, we'll add some addresses, and bring the interfaces up. I'll use addresses in the 10.1.1.0/24 subnet, but you can use what works best on your system. You'll want to avoid collisions with already-existing networks.
[root]# ip addr add 10.1.1.1/24 dev veth_host
[root]# ip link set veth_host up
We can use ip netns exec
to run commands inside of
the network namespace:
[root]# ip netns exec myvpn ip addr add 10.1.1.2/24 dev veth_openvpn
[root]# ip netns exec myvpn ip link set veth_openvpn up
We will set the default gateway, to route traffic from the namespace to the host:
[root]# ip netns exec myvpn ip route add default via 10.1.1.1 dev veth_openvpn
And we'll bring up the loopback interface inside of the namespace. For some reason, this isn't done by default, so we have to do it manually.
[root]# ip netns exec myvpn ip link set lo up
We can check everything is connected:
[root]# ip link
1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: wlan0: mtu 1500 qdisc noqueue state UP mode DORMANT group default qlen 1000
link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff
40: veth_host: mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff link-netns myvpn
[root]# ip netns exec myvpn ip link
1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
39: veth_openvpn: mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff link-netnsid 0
[root]# ip netns exec myvpn ip route
default via 10.1.1.1 dev veth_openvpn
10.1.1.0/24 dev veth_openvpn proto kernel scope link src 10.1.1.2
Now, we need to set up IP forwarding so that packets can be forwarded from inside of the namespace out to the public internet. First, we'll enable IP forwarding in the kernel:
[root]# echo 1 >/proc/sys/net/ipv4/ip_forward
Then, we'll add a POSTROUTING rule that masquerades traffic coming from the namespace’s subnet (10.1.1.0/24) out the physical interface (e.g. eth0, eno1, wlan0, etc.).
The following will forward the traffic out ANY interface; if it's
desired, the interface can be specified with -o IFACE
.
[root]# iptables -t nat -A POSTROUTING -s 10.1.1.0/24 -j MASQUERADE
We can check that we can reach the outside world from inside the namespace:
[root]# ip netns exec myvpn ping 8.8.8.8 -c 3
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=57 time=23.9 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=57 time=21.8 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=57 time=24.3 ms
--- 8.8.8.8 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2001ms
rtt min/avg/max/mdev = 21.818/23.318/24.281/1.075 ms
Also, we should configure DNS by creating
/etc/netns/myvpn/resolv.conf
. Your VPS provider
probably also has some DNS nameservers you can use, or you can
use some public-facing DNS servers.
Be aware of DNS leakage, especially if you use nameservers other than what the VPN company provides.
# /etc/netns/myvpn/resolv.conf
nameserver 1.1.1.1 # Cloudflare
nameserver 8.8.8.8 # Google
We can test that it works:
[root]# ip netns exec myvpn ping google.com -c 3
PING google.com (172.217.16.142) 56(84) bytes of data.
64 bytes from zrh04s06-in-f142.1e100.net (172.217.16.142): icmp_seq=1 ttl=54 time=184 ms
64 bytes from zrh04s06-in-f142.1e100.net (172.217.16.142): icmp_seq=2 ttl=54 time=206 ms
64 bytes from zrh04s06-in-f142.1e100.net (172.217.16.142): icmp_seq=3 ttl=54 time=128 ms
--- google.com ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2001ms
rtt min/avg/max/mdev = 127.653/172.478/205.870/32.939 ms
Persistence is Key
Unfortunately, all of these commands will need to be re-run when the system is rebooted. What we need is a way for these changes to persist.
The simplest way is to create a systemd unit file that runs the commands for us.
# /etc/systemd/system/myvpn-ns.service
[Unit]
Description=Network namespace for OpenVPN client
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=true
### Create the namespace
# Create netns for OpenVPN
ExecStart=/usr/bin/ip netns add myvpn
# Create virtual ethernet pair for OpenVPN
ExecStart=/usr/bin/ip link add veth_host type veth peer name veth_openvpn
ExecStart=/usr/bin/ip link set veth_openvpn netns myvpn
# Assign IP addresses and bring the interfaces up
ExecStart=/usr/bin/ip netns exec myvpn ip addr add 10.1.1.2/24 dev veth_openvpn
ExecStart=/usr/bin/ip netns exec myvpn ip link set veth_openvpn up
ExecStart=/usr/bin/ip addr add 10.1.1.1/24 dev veth_host
ExecStart=/usr/bin/ip link set veth_host up
# Route traffic from the namespace to the host
ExecStart=/usr/bin/ip netns exec myvpn ip route add default via 10.1.1.1 dev veth_openvpn
# Bring up loopback interface in the namespace
ExecStart=/usr/bin/ip netns exec myvpn ip link set lo up
### Remove the namespace
# Remove virtual ethernet pair for OpenVPN
ExecStop=/usr/bin/ip netns exec myvpn ip link set veth_openvpn down
# Note, this also deletes veth_openvpn
ExecStop=/usr/bin/ip link delete veth_host
# Remove netns for OpenVPN
ExecStop=/usr/bin/ip netns delete myvpn
[Install]
WantedBy=multi-user.target
Note the line RemainAfterExit=true
. Because of this
line, systemd will consider the unit to be active even after all
of the ExecStart
commands are run. The
ExecStop
commands are only run therefore when
systemd stops the service.
In other words, if the service is active, the namespace should be
available to use, and if the service is inactive, the namespace
should not exist. RemainAfterExit
is useful for
units which modify some state on the system, such as this one.
We can change the service file for OpenVPN to depend on this namespace being available.
# /etc/systemd/system/myvpn.service
[Unit]
Description=OpenVPN tunnel for my VPN
After=network-online.target
Wants=network-online.target
After=myvpn-ns.service # Only start after myvpn-ns is started
Requires=myvpn-ns.service # Require myvpn-ns for this unit
Documentation=man:openvpn(8)
Documentation=https://openvpn.net/community-resources/reference-manual-for-openvpn-2-6/
Documentation=https://community.openvpn.net/openvpn/wiki/HOWTO
[Service]
Type=notify
PrivateTmp=true
WorkingDirectory=/etc/openvpn/client
ExecStart=/usr/bin/openvpn \
--suppress-timestamps \
--nobind \
--config myvpn.conf \
--auth-user-pass myvpn.auth
User=openvpn
Group=network
AmbientCapabilities=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SETPCAP CAP_SYS_CHR
OOT CAP_DAC_OVERRIDE
CapabilityBoundingSet=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SETPCAP CAP_SYS_C
HROOT CAP_DAC_OVERRIDE
LimitNPROC=10
DeviceAllow=/dev/null rw
DeviceAllow=/dev/net/tun rw
ProtectSystem=true
ProtectHome=true
KillMode=process
# Use our network namespace
NetworkNamespacePath=/run/netns/myvpn
[Install]
WantedBy=multi-user.target
Systemd provides a helpful NetworkNamespacePath
option for us. With this option, OpenVPN will be run inside the
network namespace, just like if we were using ip netns
exec
.
Configuring the Uncomplicated Firewall
We'll use ufw to make the forwarding rules persist. First, we'll enable forwarding in the kernel:
# /etc/ufw/sysctl.conf
net/ipv4/ip_forward=1
Then, we'll add the iptables NAT table rules:
# /etc/ufw/before.rules
# ...
# NAT table rules
*nat
:POSTROUTING ACCEPT [0:0]
# Allow traffic from 10.1.1.0/24 (the openvpn netns subnet) to be masqueraded
-A POSTROUTING -s 10.1.1.0/24 -j MASQUERADE
# To limit the forwarding to one interface:
# -A POSTROUTING -s 10.1.1.0/24 -o eth0 -j MASQUERADE
COMMIT
# ...
And we'll enable ufw to be run on boot:
[root]# ufw enable
Firewall is active and enabled on system startup
Finishing Up: Configuring Transmission
Finally, we'll edit the transmission unit file to depend on this
network namespace, with systemctl edit transmission
:
### Editing /etc/systemd/system/transmission.service.d/override.conf
### Anything between here and the comment below will become the contents of the drop-in file
[Unit]
Requires=myvpn.service
After=myvpn.service
[Service]
NetworkNamespacePath=/run/netns/myvpn
### Edits below this comment will be discarded
### /usr/lib/systemd/system/transmission.service
# [Unit]
# Description=Transmission BitTorrent Daemon
# Wants=network-online.target
# After=network-online.target
#
# [Service]
# User=transmission
# Type=notify
# ExecStart=/usr/bin/transmission-daemon -f --log-level=error
# ExecReload=/bin/kill -s HUP $MAINPID
# NoNewPrivileges=true
# MemoryDenyWriteExecute=true
# ProtectSystem=true
# PrivateTmp=true
#
# [Install]
# WantedBy=multi-user.target
Now, when we start transmission, systemd will auto-magically create the network namespace, and start the VPN connection for us. Pretty neat.
[root]# systemctl enable --now transmission
[root]# systemctl status transmission
● transmission.service - Transmission BitTorrent Daemon
Loaded: loaded (/usr/lib/systemd/system/transmission.service; enabled; preset: disabled)
Drop-In: /etc/systemd/system/transmission.service.d
└─override.conf
Active: active (running) since Sat 2024-12-28 23:51:27 EST; 8s ago
Invocation: d87255d2690348d3afd6617795b5d64f
Main PID: 1744098 (transmission-da)
Status: "Uploading 1.49 KBps, Downloading 0.00 KBps."
Tasks: 3 (limit: 28629)
Memory: 3.1M (peak: 4.4M)
CPU: 217ms
CGroup: /system.slice/transmission.service
└─1744098 /usr/bin/transmission-daemon -f --log-level=error
There's one more step to configuring transmission. Transmission
is inside of the namespace, listening on addres
10.1.1.2
, and we'll connect to it using
transmission-remote
from address
10.1.1.1
. We'll have to add the host to the host
whitelist:
// /var/lib/transmission/.config/transmission-daemon/settings.json
// ...
"rpc-whitelist": "127.0.0.1,::1,10.1.1.1",
// ...
We'll tell transmission to reload its configuration:
[root]# pkill -s SIGHUP transmission
Then, we can connect like this:
[root]# transmission-remote 10.1.1.2 -l
ID Done Have ETA Up Down Ratio Status Name
1 100% 593.8 MB Done 0.0 0.0 0.00 Idle Cool.Torrent.Name
And of course, transmission-remote-gtk
can be
configured to use the same host.
In addition to specifying the namespace with in a systemd unit file
or with ip netns exec
, we can also use firejail
to launch applications:
[user]$ firejail --netns=myvpn [program and arguments]
This has the advantage of not requiring superuser permissions.
Conclusion
This is the best way that I've found to configure OpenVPN in a "split" configuration, where some traffic is routed to the internet while other traffic is routed through the VPN. Although complicated to set up, the systemd unit files make it easy to take the VPN online or offline as needed.
The final system will have two systemd units; one to bring the namespace up (myvpn-ns) and one for the VPN itself (myvpn), in addition to the NAT forwarding rules stored in ufw. Any software which is desired to run inside of the VPN can be run inside of the network namespace as shown above. We can use systemd unit dependencies to ensure that the VPN is started before software that depends on it.
This technique of isolating applications inside of a network namespace is not just applicable to VPN routing, but could be used for a variety of sandboxing applications as well.