Secure torrenting with OpenBSD rdomains


I’ve been spending my recent time off tinkering with my neglected home network. OpenBSD has been a core feature of my home network ever since I was a student, I usually have some sort of OpenBSD server as well as an OpenBSD router. I like it’s simplicity and its stability, but most of all I appreciate OpenBSD as a learning tool. The kernel code is much easier to navigate and read than the Linux kernel, so if you’re interested in learning more about operating systems it’s a great place to start.

My most recent home network project was to set up a secure torrenting and media playback system on my OpenBSD server. This seemed like a good excuse to learn more about some of OpenBSD’s networking features: I want my torrent client to only be able to communicate over a VPN network, with all other processes on the same machine using the default network.

To do this I need to be able to set up two separate routing tables - one for the torrent client (transmission) to route over the VPN interface, and one for everything else. Routing tables are a set of rules used to determine which path to send a packet to its destination. Any networked machine will have a routing table and contains the topology of the immediate network surrounding it.

On a typical machine, all interfaces are connected to a single routing table. OpenBSD’s routing domains (rdomain(4)) allow you to have multiple routing tables on a single machine. Setting up multiple rdomains allows you to segment the network on a machine between different network paths.

So if I configure transmission into a separate rdomain with the VPN interface, I can completely segment transmission from the rest of my network.

Configuring OpenVPN

We need to configure the tun0 interface created by OpenVPN into a separate rdomain from the default (rdomain0).

First launch OpenVPN, this will create the tun0 interface. I use a configuration file provided by ProtonVPN.

# /usr/local/sbin/openvpn --cd /etc/openvpn --config /etc/openvpn/proton.ovpn

Note that we’re launching OpenVPN in the default rdomain0 for now. We need our VPN client to be able to connect to the internet: if we launched it in a new rdomain it wouldn’t be able to route to the ProtonVPN servers and set up our tunnel interface.

Once the client has established a connection, we can move it into a separate rdomain, in this case rdomain 1:

# ifconfig tun0 rdomain 1

As part of establishing our VPN tunnel, OpenVPN will add routes provided to it by the server. You can see this in the log output for OpenVPN (snipped for brevity):

2021-09-25 15:32:24 /sbin/ifconfig tun0 mtu 1500 netmask up
2021-09-25 15:32:24 /sbin/route add -net -netmask
add net gateway
2021-09-25 15:32:24 /sbin/route add -net -netmask
add net gateway
2021-09-25 15:32:24 /sbin/route add -net -netmask
add net gateway
2021-09-25 15:32:24 /sbin/route add -net -netmask
add net gateway

We need to add these routes again to rtable1:

# route -T1 add -net -netmask
# route -T1 add -net -netmask
# route -T1 add -net -netmask

To verify, we can check the routes in rtable 1 match what we just added:

# route -T1 show
Routing tables

Destination        Gateway            Flags   Refs      Use   Mtu  Prio Iface
0/1                UGS        0        0     -     8 tun0
128/1              UGS        0        0     -     8 tun0
10.20/16           UGS        0        0     -     8 tun0          UHh        3        3     -     8 tun0          UHl        0        0     -     1 tun0

And we can verify that rtable1 use the routes through the VPN interface by default and that it can succesfully access the internet:

$ route -T1 exec curl

Configuring tun0 rdomain automatically

In the above section we manually moved the tun0 interface into rdomain1 after it had established a connection. Unfortunately we can’t just force OpenVPN to run in rdomain1 on initial connect. OpenVPN needs to be able to route to the internet to establish the tunnel and rdomain1 has no route to the internet.

So we need OpenVPN to start in rdomain0, determine the VPN gateway address through the default route, then move tun0 into rdomain1 and add the VPN gateway route.

Fortunately OpenVPN can allow us to execute custom scripts during specific events. We can use the down directive to execute a specified script after the tun0 interface closes, and the route-noexec and route-up directive to execute our own routing script instead of OpenVPN adding our routes for us.

We can therefore add the following to our proton.ovpn config:

down /etc/openvpn/
route-up /etc/openvpn/

The script simply contains the following:


ifconfig tun0 rdomain 0

We move tun0 back into rdomain 0 so that it can route back to the internet when the interface goes down.

The route-noexec directive stops OpenVPN from adding routes automatically, but will expose the routes as environment variables that can be passed into the route-up script.

Our script is as follows:


ifconfig $dev rdomain 1
ifconfig $dev $ifconfig_local $route_vpn_gateway mtu 1500 netmask up
route -T1 add -net $route_vpn_gateway -netmask $route_vpn_gateway
route -T1 add -net -netmask $route_vpn_gateway
route -T1 add -net -netmask $route_vpn_gateway

This script essentially performs what the automatic route creation would have done, except we begin with the ifconfig $dev rdomain1 line to first move our tun interface into rdomain1.

Configuring Transmission

Now that we’ve confirmed that the VPN tun0 interface is the default route for rdomain 1, we can place the transmission daemon within this rdomain.

Doing so is simple on OpenBSD, you can use rcctl to set the transmission_daemon default rtable, like so:

# rcctl set transmission_daemon rtable 1

And to confirm:

# rcctl get transmission_daemon

After you start the transmission daemon, you can check what rdomain processes are running in with ps aux -o rtable:

$ ps aux -o rtable
-- snip
_transmi 10922  0.0  0.1  5204  5752 ??  S      Sun02PM    0:24.80 /usr/local/bin/t      1

You can also use the -T1 flag with netstat to see transmission listening on port 51413 and port 9091 in rtable1:

$ netstat -T1 -an | grep LISTEN
tcp          0      0  *.51413                *.*                    LISTEN
tcp          0      0  *.9091                 *.*                    LISTEN
tcp6         0      0  *.51413                *.*                    LISTEN

Using pair interfaces to route between rdomains

The diagram below demonstrates the current configuration - we have two separate rdomains. On the left we have rdomain0, our default rdomain, with the physical re0 interface that connects into our local network. On the right we have rdomain1, with the default route using the virtual tun0 interface that connects to our VPN.

         ▲                   ▲
         │ LAN               │ VPN
         │                   │
  ┌──────┴──────┐     ┌──────┴───────┐
  │     re0     │     │     tun0     │
  │             │     │              │
  │             │     │              │
  └─────────────┘     └──────────────┘
      RDOMAIN0            RDOMAIN1

At the moment, rdomain0 is completely separate from rdomain1 and cannot connect. However, I’d like to be able to access the transmission web interface from other machines in my local network. An interface can only be in a single rdomain, so we need a way to connect these two rdomains so we can access the web interface of transmission in rdomain1.

OpenBSD’s pair(4) interface can be used to create a virtual Ethernet interface pair. An administrator can create a pair interface and configure each interface for a separate rdomain and connect them together.

We can use pair(4) to create a virtual interface for each rdomain and patch them together, like so:

# ifconfig pair0 rdomain 0
# ifconfig pair1 rdomain 1
# ifconfig pair0 patch pair2

The diagram below demonstrates the new configuration.

         ▲                   ▲
         │ LAN               │ VPN
         │                   │
  ┌──────┴──────┐     ┌──────┴───────┐
  │     re0     │     │     tun0     │
  │        pair0│ ────┤pair1         │
  │             │     │              │
  └─────────────┘     └──────────────┘
      RDOMAIN0            RDOMAIN1

We have now created a route to each rdomain, allowing us to access Transmission in rdomain1 from rdomain0.

We can verify this route by looking at our routing table for rtable1:

$ route -T1 show
Routing tables

Destination        Gateway            Flags   Refs      Use   Mtu  Prio Iface
0/1                UGS        1    18421     -     8 tun0
128/1              UGS        0    11485     -     8 tun0
10.18/16           UGS        0        0     -     8 tun0         UHh        3        3     -     8 tun0         UHl        0    26618     -     1 tun0
192.168.2/24        UCn        1        0     -     4 pair1        fe:e1:ba:d0:ff:cf  UHLc       0    16523     -     3 pair1        fe:e1:ba:d1:47:b7  UHLl       0    54540     -     1 pair1        UHb        0        0     -     1 pair1

As we configured pair1 to, we should be able to connect to transmission on that address from our default rdomain:

$ nc -v 9091
Connection to 9091 port [tcp/*] succeeded!

L7 proxying with relayd

We now have a pair interface that will allow us to access the transmission daemon from rdomain0. However, I’d like to be able to access the transmission web server from other machiens on my network, not just the server it’s hosted on. To do this I can set up relayd to proxy traffic from rdomain0 to the tranmission daemon listening in rdomain1.

This is very simple to do with relayd(8) with the configuration below:

# $OpenBSD: relayd.conf,v 1.5 2018/05/06 20:56:55 benno Exp $
table <transmissionhost> { }

relay www {
        listen on port 80

        forward to <transmissionhost> port 9091 check tcp

The above will configure relayd to listen on port 80 and relay (L7 proxy) to our transmission daemon listening on the pair interface we set up in our previous step ( Relayd can be used as a load balanced proxy, which is what the check tcp directive is for. Our use case is simplified with a single host but you could very easily add failover hosts here. The check tcp uses a simple tcp connect to check the health of the transmission daemon.


That’s it! With transmission isolated in a separate rdomain that must use the tun0 interface, we can’t accidentally leak torrent traffic in the event of any network issues with the VPN server.

#openbsd #rdomain