Secure torrenting with OpenBSD rdomains
Introduction
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 10.20.0.6 10.20.0.1 mtu 1500 netmask 255.255.0.0 up
2021-09-25 15:32:24 /sbin/route add -net 10.20.0.0 -netmask 255.255.0.0 10.20.0.1
add net 10.20.0.0: gateway 10.20.0.1
2021-09-25 15:32:24 /sbin/route add -net 185.159.158.100 -netmask 255.255.255.255 10.0.0.1
add net 185.159.158.100: gateway 10.0.0.1
2021-09-25 15:32:24 /sbin/route add -net 0.0.0.0 -netmask 128.0.0.0 10.20.0.1
add net 0.0.0.0: gateway 10.20.0.1
2021-09-25 15:32:24 /sbin/route add -net 128.0.0.0 -netmask 128.0.0.0 10.20.0.1
add net 128.0.0.0: gateway 10.20.0.1
We need to add these routes again to rtable1:
# route -T1 add -net 10.20.0.0 -netmask 255.255.0.0 10.20.0.1
# route -T1 add -net 0.0.0.0 -netmask 128.0.0.0 10.20.0.1
# route -T1 add -net 128.0.0.0 -netmask 128.0.0.0 10.20.0.1
To verify, we can check the routes in rtable 1 match what we just added:
# route -T1 show
Routing tables
Internet:
Destination Gateway Flags Refs Use Mtu Prio Iface
0/1 10.20.0.1 UGS 0 0 - 8 tun0
128/1 10.20.0.1 UGS 0 0 - 8 tun0
10.20/16 10.20.0.1 UGS 0 0 - 8 tun0
10.20.0.1 10.20.0.6 UHh 3 3 - 8 tun0
10.20.0.6 10.20.0.6 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 icanhazip.com
185.159.158.101
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/down-rdomain.sh
route-noexec
route-up /etc/openvpn/add-route.sh
The down-rdomain.sh
script simply contains the following:
#!/bin/sh
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 add-route.sh
script is as follows:
#!/bin/sh
ifconfig $dev rdomain 1
ifconfig $dev $ifconfig_local $route_vpn_gateway mtu 1500 netmask 255.255.0.0 up
route -T1 add -net $route_vpn_gateway -netmask 255.255.0.0 $route_vpn_gateway
route -T1 add -net 0.0.0.0 -netmask 128.0.0.0 $route_vpn_gateway
route -T1 add -net 128.0.0.0 -netmask 128.0.0.0 $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
transmission_daemon_class=daemon
transmission_daemon_flags=
transmission_daemon_logger=
transmission_daemon_rtable=1
transmission_daemon_timeout=30
transmission_daemon_user=_transmission
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
USER PID %CPU %MEM VSZ RSS TT STAT STARTED TIME COMMAND RTABLE
_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 192.168.2.1/24 rdomain 0
# ifconfig pair1 192.168.2.8/24 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
Internet:
Destination Gateway Flags Refs Use Mtu Prio Iface
0/1 10.18.0.1 UGS 1 18421 - 8 tun0
128/1 10.18.0.1 UGS 0 11485 - 8 tun0
10.18/16 10.18.0.1 UGS 0 0 - 8 tun0
10.18.0.1 10.18.0.31 UHh 3 3 - 8 tun0
10.18.0.31 10.18.0.31 UHl 0 26618 - 1 tun0
192.168.2/24 192.168.2.8 UCn 1 0 - 4 pair1
192.168.2.1 fe:e1:ba:d0:ff:cf UHLc 0 16523 - 3 pair1
192.168.2.8 fe:e1:ba:d1:47:b7 UHLl 0 54540 - 1 pair1
192.168.2.255 192.168.2.8 UHb 0 0 - 1 pair1
As we configured pair1 to 192.168.2.8, we should be able to connect to transmission on that address from our default rdomain:
$ nc -v 192.168.2.8 9091
Connection to 192.168.2.8 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> { 192.168.2.8 }
relay www {
listen on 0.0.0.0 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 (192.168.2.8). 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.
Conclusion
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.