Austin Adams

Running Select Applications through OpenVPN

August 9, 2015

Note: I base this method heavily on a great article by Sebastian Thorarensen.

To isolate VPN applications from applications running ‘bare,’ I use the following method, which involves nsdo. It has the handy effect of putting each instance of the openvpn client in its own network namespace, allowing you to run a bunch of VPNs at the same time, each available only to programs you choose.

First, I leverage the Arch openvpn package’s openvpn@.service systemd service (based on upstream’s slightly different version), which I’ll paste here for posterity:

[Unit]
Description=OpenVPN connection to %i

[Service]
PrivateTmp=true
Type=forking
ExecStart=/usr/bin/openvpn --cd /etc/openvpn --config /etc/openvpn/%i.conf --daemon openvpn@%i --writepid /run/openvpn@%i.pid --status-version 2
PIDFile=/run/openvpn@%i.pid
CapabilityBoundingSet=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SYS_CHROOT CAP_DAC_READ_SEARCH
LimitNPROC=10
DeviceAllow=/dev/null rw
DeviceAllow=/dev/net/tun rw

[Install]
WantedBy=multi-user.target

(For instance, you’d start an openvpn instance configured in /etc/openvpn/foo.conf with systemctl start openvpn@foo.)

However, the unit needs a few modifications. First, calling setns() to change network namespaces requires CAP_SYS_ADMIN, a capability the vanilla unit does not provide. Second, to make OpenVPN keep the same network namespace across VPN reconnections or daemon restarts (e.g., after a suspend), a separate unit must set up the destination network namespace. To solve both issues, I put the following in /etc/systemd/system/openvpn@.service.d/netns.conf, a systemd drop-in unit:

[Unit]
Requires=netns@%i.service
After=netns@%i.service

[Service]
# Needed to call setns() as ip netns does
CapabilityBoundingSet=CAP_SYS_ADMIN

And then created a netns@.service in /etc/systemd/system:

[Unit]
Description=network namespace %I

[Service]
Type=oneshot
ExecStart=/bin/ip netns add %I
ExecStop=/bin/ip netns del %I
RemainAfterExit=yes

By default, openvpn manually runs ifconfig or ip to set up its tun device. Luckily for us, you can configure openvpn to run a custom script instead. (though you have to set script-security >= 2. :( )

I named my script /usr/local/bin/vpn-ns, so here’s the relevant snippet from my openvpn configuration file:

# ...
# (my other configuration)
# ...

# script should run `ip`, not openvpn
route-noexec
ifconfig-noexec
up "/usr/local/bin/vpn-ns"
route-up "/usr/local/bin/vpn-ns"
script-security 2

Using Sebastian’s script as a basis, I hacked together the following. Notice that it guesses the name of the network namespace based on the name of the instance’s configuration file (e.g., /etc/openvpn/foo.conffoo).

#!/bin/bash
# based heavily on http://naju.se/articles/openvpn-netns

[[ $EUID -ne 0 ]] && {
    echo "$0: this program requires root privileges. try again with 'sudo'?" >&2
    exit 1
}

# convert a dot-decimal mask (e.g., 255.255.255.0) to a bit-count mask
# (like /24) for iproute2. this probably isn't the most beautiful way.
tobitmask() {
    bits=0
    while read -rd . octet; do
        (( col = 2**7 ))
        while (( col > 0 )); do
            (( octet & col )) || break 2
            (( bits++ ))
            (( col >>= 1 ))
        done
    done <<<"$1"
    echo $bits
}

# guess name of network namespace from name of config file
basename="$(basename "$config")"
ns="${basename%.conf}"
netmask="$(tobitmask "$route_netmask_1")"

case $script_type in
    up)
        ip -netns "$ns" link set dev lo up
        ip link set dev $dev up netns "$ns" mtu "$tun_mtu"
        ip -netns "$ns" addr add "$ifconfig_local/$netmask" dev "$dev"
    ;;
    route-up)
        ip -netns "$ns" route add default via "$route_vpn_gateway"
    ;;
    *)
        echo "$0: unknown \$script_type: '$script_type'" >&2
        exit 2;
    ;;
esac

Now, once you’ve told systemd to start openvpn@foo, you can run any application you’d like under the new namespace:

$ nsdo foo firefox

Alternatively, if you don’t want to bother with nsdo:

$ sudo ip netns foo sudo -u $USER firefox

addendum: configuring veth

Note: if you’re curious about veth, Scott Lowe’s handy blog post, where I found the commands below, serves as a good introduction.

Suppose that I want to use nsdo+openvpn as described above to tunnel an application that also provides a server (for RPC, for instance). That is, I run an application that binds to a port in a namespace, but I want to connect to it outside of that namespace.

With the setup I’ve described up to this point, I simply cannot do this. Certainly, network namespaces separate running programs from one another – an application can’t cross the line willy-nilly. For instance, I could not use netcat to listen on a port in one namespace and then connect to it from another:

$ nsdo foo nc -l -p 5050 <<<"hi!" &
$ nc -v localhost 5050 <<<"hello"
localhost [127.0.0.1] 5050 (mmcc): Connection refused
$ nsdo foo !!
hi!
hello

So (as far as I know) I have no other choice but to use veth, a kernel feature designed to allow network namespaces to communicate. veth interfaces act just like any interface but come in pairs – one for each namespace.

You can set them up with a systemd service like the following (I’ve named it foo-veth.service):

[Unit]
Description=veth for foo netns
After=netns@foo.service

[Service]
Type=oneshot
RemainAfterExit=yes
# configure our end
ExecStart=/usr/bin/ip link add ns-foo up type veth peer name ns-def netns foo
ExecStart=/usr/bin/ip addr add 10.0.255.1/24 dev ns-foo
# configure vpn end
ExecStart=/usr/bin/ip -netns foo link set dev ns-def up
ExecStart=/usr/bin/ip -netns foo addr add 10.0.255.2/24 dev ns-def
# tear down everything
ExecStop=/usr/bin/ip link del ns-foo

[Install]
WantedBy=netns@foo.service

(Note: I’ve chosen not to make this example systemd service generic – like netns-veth@.service – because I currently use veth with only one vpn/namespace and I’m not sure how I’d assign unique IP addresses otherwise.)

Now, make netns@foo start the new service automatically and then (this time) start it manually:

# systemctl enable foo-veth
# systemctl start foo-veth

For convenience, make the name of the namespace resolve to the IP address assigned to its corresponding veth interface:

# printf 'foo\t10.0.255.2\n' >>/etc/hosts

Done! You can now reach servers running in namespaces by simply connecting to the namespace by name. If a hypothetical application listens on port 5050 in namespace foo, for instance, you can access it by pointing your client to foo:5050:

$ curl foo:5050
Hello, world!