When using a default VPN application all traffic is routed through the VPN host. In most cases this is the desired behaviour but there are cases where only certain applications are to be rerouted. A quick but resource intensive solution is to isolate these applications in a virtual machine. An alternative is to use Linux’s namespaces and iptables.

Configuring the network

My current network setup only consists of an ethernet interface named ‘enp3s0’ which is configured by a dhcpcd service. The dhcpcd service is responsible for determine routing rules and the address of the interface. Prior configuration from ip addr

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: enp3s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 4c:ed:fd:c2:aa:21 brd ff:ff:ff:ff:ff:ff
    inet 192.168.0.21/24 brd 192.168.0.255 scope global dynamic noprefixroute enp3s0
       valid_lft 604755sec preferred_lft 529155sec

Routes from ip route

default via 192.168.0.1 dev enp3s0 proto dhcp src 192.168.0.21 metric 202
192.168.0.0/24 dev enp3s0 proto dhcp scope link src 192.168.0.21 metric 202

Before continuing make note of the assigned address which in this case is 192.168.0.21. For the remainder of the section, I’m assuming commands are executed by a privileged user so run as root or prefix each command with sudo.

# Set enp3s0 down to clear addresses
ip link set enp3s0 down

# Prevent dhcpcd from managing network devices
systemctl stop dhcpcd

# Configure enp3s0 as a slave of br0 so any packets recieved through enp3s0
# are encapsulated and forwarded to the master device br0
ip link add br0 type bridge
ip link set enp3s0 master br0

ip link set br0 up
ip link set enp3s0 up

ip addr add 192.168.0.21/24 dev br0
ip route add default via 192.168.0.1

After this you should be able to access the internet. I initally had some problems since dhcpcd used the DNS server provided by my gateway. I resolved this by modifying my ‘resolv.conf’ configuration.

echo "namespace 8.8.8.8" >> /etc/resolv.conf

Now we will create a network namespace, add a virtual ethernet device, and configure routing. Further reading about network namespaces can be found here: Network namespaces manual

# Add a network namespace called vpn
ip netns add vpn

# Create a virtual ethernet device pair
ip link add veth1 type veth peer name vethp1

# Any packets from veth1 are forwarded to the bridge br0
ip link set veth1 master br0

# Move one end of the virtual ethernet device pair to the network namespace
ip link set vethp1 netns vpn

# Assign an addresses to br0 which will act as a gateway
ip addr add 10.0.0.1/24 dev br0

# '-n vpn' will execute the command within the network namespace
ip -n vpn addr add 10.0.0.2/24 dev vethp1

ip link set veth1 up
ip -n vpn link set lo up
ip -n vpn link set vethp1 up

# Add a default route to our bridge.
ip -n vpn route add default via 10.0.0.1

At this point if we try to ping a host outside the 10.0.0.0 network it will hang. Enable IP forwarding so the bridge functions as a gateway by forwarding requests to next best hop. Kernel network parameters

echo 1 > /proc/sys/net/ipv4/ip_forward 

NAT rules

There is one additional change that is required before the namespace can access the internet. An IP header contains a source and destination address to determine where to send a datagram and where a receiver should send information back. The address set of 10.0.0.0/24 belongs to an internal network so hosts outside will be unable to route a response to the originator. This can be resolved with NAT(Network address translation) which will replace the source address of outbound datagrams with an address that is visible from the outside and will restore the original address of inbound datagrams.

iptables -t nat -A POSTROUTING -o br0 -s 10.0.0.2 -j MASQUERADE

Setting up the VPN

For my current needs, I simply exec openvpn within the network namespace.

ip netns exec vpn openvpn --config vpn-config.ovpn

For this setup, extra precaution is required since an unexpected failure of the vpn may cause an IP leakage.

Execute applications within the network namespace

The use of ip netns exec starts with an empty environment so any variables required for X, Wayland, and others have to be exported. I could not determine how to enter a network namespace as an unprivileged user, but I suspect it will require the use of user namespaces or modification of the sudoers list, hence sudo is required for the initial call but changed back to the original user.

#!/usr/bin/env sh
sudo ip netns exec vpn \ 
    env $(printenv | grep "XDG\|I3\|SSH\|PASSWORD_STORE\|DBUS\|WAYLAND\|SWAY\|XCURSOR\|QT") \
    runuser -u $(id -un) -g $(id -gn) -- $@

I used ip netns exec over nsenter since nsenter does not create a mount point for applications that are unaware of network namespaces as stated here: ip-netns manual

Be aware of applications that use existing seasons when executed. For example, if you have firefox running and you execute firefox it will use the existing season. To get around this use firefox profiles and start firefox by executing firefox -P

Further steps

If you want the changes to persists through reboots, you’ll need to do distro dependent changes.