Configuring Transmission and OpenVPN

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.