Tunneling traffic just for one Linux system user through a VPN

Namespaces

One Open-Source software to easily to the job: https://github.com/slingamn/namespaced-openvpn

It can be even used with systemd (source):

Before=systemd-user-sessions.service
After=network-online.target
Wants=network-online.target
Documentation=https://github.com/slingamn/namespaced-openvpn

[Service]
Type=notify
WorkingDirectory=/etc/openvpn
ExecStart=/usr/local/sbin/namespaced-openvpn --namespace %i --daemon nsovpn-%i --status /run/namespaced-openvpn/%i.status 10 --cd /etc/openvpn --script-security 2 --config /etc/openvpn/%i.conf --auth-user-pass /etc/openvpn/auth.txt  --writepid /run/namespaced-openvpn/%i.pid
PIDFile=/run/namespaced-openvpn/%i.pid
KillMode=process
ExecReload=/bin/kill -HUP $MAINPID
RestartSec=5s
Restart=on-failure

[Install]
WantedBy=multi-user.target

[Service]
RuntimeDirectory=namespaced-openvpn

i.e. systemctl start nsopenvpn@configfile
Another systemd service can then easily use it by using NetworkNamespacePath=configname inside the [Service] section.
Also with Require=nsopenvpn@configfile it can be assured that the VPN is started before.

It will create for the process a sandboxed network environment. It is quite secure (of IP leaks) however it is also the most restrictive way. It is not possible to access local network services. To do so one would have to either add additional (possible complicated) routes/bridges or create a socat device.

A system user using a shell is also not automatic protected this way.

IPTables

If security is not that important, one can use iptables to do a similar job.

With that local communication is still possible. Also it is easy to really put the whole user inside a VPN (without requiring a special command for starting a shell inside the VPN).

#!/bin/bash

# Mark all traffic with the number 42 of a specific user when its not towards 127.0.0.1
# REPLACE THEUSERTORESTRICT
iptables -t mangle -nvL OUTPUT | grep -q 0x2a ||
    iptables -t mangle -A OUTPUT ! -d 127.0.0.1 -m owner --uid-owner THEUSERTORESTRICT -j MARK --set-mark 42

# Create a new routing table if not exists
grep -q '^42  vpn$' /etc/iproute2/rt_tables ||
    echo '42  vpn' >>/etc/iproute2/rt_tables

# Create a gateway with low priority which routes nowhere (to disconnect when the VPN interface is removed)
ip route add default via 127.0.0.1 metric 10000 table vpn || true

# Get the subnet of tun0 Interface, and replace the last octet with a .1 as this is commonly the gateway. Might need to be replaced
subnet=`ip -o -f inet addr show tun0 | awk '/scope global/ {print $4}' | perl -ne 's/(?<=\d.)\d{1,3}(?=\/)/0/g; print;'`
ip3=${subnet%.*}
gatewayip=$ip3.1

# Add the VPN as gateway for traffic in the routing table
ip route show table vpn | grep -q "metric 11" ||
    ip route add default via $gatewayip dev tun0 metric 11 table vpn


# ip route show table vpn

# All traffic marked with the number 42 should go through the VPN routing table
ip rule | grep -q 0x2a ||
    ip rule add fwmark 42 lookup vpn prio 42

# Masquerade/SNAT on the VPN interface
iptables -t nat -nvL POSTROUTING | grep -q tun0 ||
    iptables -t nat -A POSTROUTING -o tun0 -j MASQUERADE

The script was written with the help of: https://unix.stackexchange.com/a/589605/388631

It is not 100% optimal. It guesses the gateway IP because OpenVPN does not transmit the Gateway IP into ENV when using the OpenVPN option pull-filter ignore redirect-gateway

Also a disconnect of tun0 is protected in a very primitive way.

I use it like this inside a custom OpenVPN systemd script (having the code from above in a file called restrictscript.sh; also don’t forget to option from above inside the vpns configurationfile):

  /usr/sbin/openvpn --script-security 2 --config /etc/openvpn/myvpn.conf --auth-user-pass /etc/openvpn/auth.txt  --writepid /run/namespaced-openvpn/usenetvpn.pid --route-up /etc/openvpn/restrictscript.sh

It is easily testable by suing into the user and then running curl ifconfig.me