paulgorman.org

Selectively VPN Services with Linux Network Namespaces

My little home server does two things, mainly:

After the recent FCC repeal of net neutrality regulations, I signed up for a VPN service as a small protest. Naturally, I’d like to make it more difficult for Comcast to harvest my DNS queries by sending the DNS resolver traffic over the VPN. But if I run OpenVPN, I can’t SSH in for remote access. I need to run some services over the VPN, but leave others out.

Unfortunately, routing doesn’t work that way. There’s no easy way to tell the OS: route most traffic out the default route, but send DNS traffic out the VPN. Routing operates on IP addresses, not ports.

My first thought was to use a virtual machine or LXC container. That would work, but it’s a relatively heavy solution that requires additional ongoing administration (OS updates in the container, etc.).

Then I remembered Linux’s network namespaces. The kernel can provide isolation that gives a process its own network interfaces and its own routing table. Perfect!

My server already has a bridge named br0, which will help connect the isolated network namespace to the outside:

$ ip addr sh br0
3: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 4e:07:25:9d:05:36 brd ff:ff:ff:ff:ff:ff
    inet 10.0.0.2/24 brd 10.0.0.255 scope global br0
       valid_lft forever preferred_lft forever
    inet6 fe80::f64d:30ff:fe6f:129c/64 scope link
       valid_lft forever preferred_lft forever

Creation of a new network namespace is simple:

# ip netns add myns

Start any process in the namespace using the exec subcommand of ip-netns, even a Bash shell:

# ip netns exec myns bash

This isolated namespace does not share the interfaces or routing table of the host. Any process executed in the namespace can’t get out to the network. To run Unbound, our DNS resolver, in the namespace, we’ll need a connection to the host’s bridge.

Linux offers “veth” devices that operate in pairs, like virtual patch cables, to connect software bridges or network namespaces. We create the pair of devices, assign one end to a vpnns network namespace, and plug the other end into the host’s br0 bridge. Here’s a complete shell script to do so:

#!/bin/sh
set -uf
#---------------------------------------------------------------------------
# Create a private network namespace for OpenVPN and things that need to
# use the VPN, like Unbound.
#---------------------------------------------------------------------------
ns=vpnns
ip netns add "$ns"
ip netns exec "$ns" ip li set dev lo up
ip link add veth0 type veth peer name veth1
ip link set veth1 netns "$ns"
ip li set up dev veth0
ip netns exec "$ns" ip addr add 10.0.0.3/24 dev veth1
ip netns exec "$ns" ip li set up dev veth1
ip netns exec "$ns" ip ro add default via 10.0.0.1 dev veth1
ip link set dev veth0 master br0

After that, the 10.0.0.3 interface should be visible on our LAN, and a ping run from inside the vpnns namespace should reach the outside:

# ip netns exec vpnns ping -c3 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=60 time=39.0 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=60 time=43.4 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=60 time=42.2 ms

--- 8.8.8.8 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 39.013/41.583/43.440/1.891 ms

Assuming we’ve already installed and configured OpenVPN and Unbound, modify their systemd unit files to put them in the new network namespace. Both unit files need only two minor changes. Call the netns-vpn-up.sh shell script to set up the network namespace using systemd’s ExecStartPre. (It’s not a problem if multiple services call this script, or if its runs again when the namespace has already been set up.) The other change happens to ExecStart; prefix the existing command with ip netns exec.

$ cat /etc/systemd/system/vpn.service
[Unit]
Description=OpenVPN
Wants=network.target
After=network.target

[Service]
Type=simple
ExecStartPre=-/home/paulgorman/bin/netns-vpn-up.sh
ExecStart=-/bin/ip netns exec vpnns /usr/sbin/openvpn --config /etc/openvpn/ovpn_udp/99.example.com.udp.ovpn

Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

That’s it. Unbound and OpenVPN run in the vpnns namespace. Everything else, including remote SSH access, happens outside the VPN. To later run another command over the VPN, do ip netns exec vpnns mycommand.

#vpn #dns #linux #systemd

⬅ Older Post Newer Post ➡