Network namespaces to separate network applications
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.