Putting a systemd service behind a VPN
It recently occurred that I had the need to route all traffic by a certain application through a VPN. While premade solutions for this exist using Docker (especially in combination with BitTorrent clients), I preferred looking for something that really just isolates what is needed and leaves the rest alone.
As it turns out systemd has good support for Linux namespaces and sharing them between units.
The basic plan here is as follows:
Create a network namespace
Get internet access into our namespace
Enable the VPN inside the namespace
Run the intended application inside the namespace
Namespace & NAT
While systemd can create a namespace it is not able to link it to a name for
interaction with ip netns
. This is not strictly necessary but
simplifies further setup a lot so we'll take care of it manually.
/etc/systemd/system/netns@.service
:
[Unit] Description=Network namespace %i StopWhenUnneeded=true [Service] Type=oneshot RemainAfterExit=yes # systemd creates a new network namespace for us PrivateNetwork=yes PrivateMounts=no # Take the network namespace that systemd created and name it ExecStart=/bin/sh -c 'ip netns attach %i $$$$' # Clean up the name when we're done ExecStop=/sbin/ip netns delete %i
Starting this unit gets us a namespace with a single interface (lo
for localhost)
inside. What we want to do next is configure a virtual link between the namespace
and the "outside" and a simple NAT setup on top of that.
/etc/systemd/system/veth-setup@.service
:
[Unit] Description=Setup veth for network namespace %i Requires=netns@%i.service Requires=systemd-networkd.service After=netns@%i.service [Service] Type=oneshot RemainAfterExit=yes # Load $ADDRESS and $ROUTES from here EnvironmentFile=/etc/veth-setup.%i.conf # Create a veth pair (vg = "veth guest", vh = "veth host") ExecStart=/sbin/ip link add vh-%i type veth peer name vg-%i # Move one end into the netns and set it up ExecStart=/sbin/ip link set dev vg-%i netns %i ExecStart=/sbin/ip netns exec %i ip l set vg-%i up ExecStart=/sbin/ip netns exec %i ip a add dev vg-%i $ADDRESS ExecStart=/sbin/ip netns exec %i sh -c 'echo "$ROUTES" | while read -r args; do ip r add $args; done' ExecStart=/sbin/ip netns exec %i sysctl -w net.ipv6.conf.vg-%i.disable_ipv6=1
The units are generic but you will have to decide on a name for your namespace now. I will be using 'example'.
/etc/veth-setup.example.conf
:
In this case the 192.168.0.0/16 route is needed so I can reach the software from my home network despite the VPN routing. If you have a more complex setup consider using a reverse proxy.
For convenience reasons we'll let systemd-networkd handle everything related to the host side including NAT setup.
/etc/systemd/network/vh-example.network
:
[Match] Name=vh-example Driver=veth [Network] Address=192.168.255.0/30 IPMasquerade=ipv4 LinkLocalAddressing=no
Finally for all of this to work you still need to configure net.ipv4.ip_forward=1
in the appropriate sysctl config file.
Joining the namespace
At this point you will want modify the unit that enables the VPN connection to
move it into the namespace. Run e.g. systemctl edit openvpn-client@nordvpn.service
and configure as follows:
[Unit] Requires=veth-setup@example.service After=veth-setup@example.service JoinsNamespaceOf=netns@example.service [Service] PrivateNetwork=true
Next do something similar (note the two different lines) for the service you wanted to isolate in the first place:
[Unit] Requires=openvpn-client@nordvpn.service After=openvpn-client@nordvpn.service JoinsNamespaceOf=netns@example.service [Service] PrivateNetwork=true
At this point if you reload systemd and start the service, systemd will first create the namespace, run the setup commands, start the VPN service and then run the initial service. Inside the namespace everything will be routed through the VPN while the rest of the system is entirely unaffected.
Leak protection
To safeguard against bugs or misconfigurations you may want to make sure only VPN traffic can exit from the namespace. Here's an example with iptables and port 1194/UDP for VPN traffic:
Links
The initial idea for this was taken from https://etherarp.net/network-isolation-of-services-with-systemd/index.html.
description of
PrivateNetwork
in the man pagedescription of
JoinsNamespaceOf
in the man page