Using VPNs on Linux with elegance

Disclaimer

There are things in this “article” that could be factually incorrect or outdated, proceed with caution!

TL;DR version

If a proxy will suffice, use proxychains
If you need a VPN use vopono and be happy.
Or read on and be slightly less happy ? with the alternative.

Rant

Everyone and their dog uses a VPN nowadays, and if you are here, you have likely acquainted yourself with all the precautions that come with using one, so I will omit the “you don’t need a VPN, you just need a proxy!” babble.

VPNs are convenient, but one limiting factor is that most VPN software is not supplied with a sane way to route traffic specific to certain programs to them.

Wireguard for Android can do that, thanks to how Android works under the hood, but us unfortunate computer-glued creatures only get to either forward all traffic to our VPN overlords or none at all.

Enter network namespaces

Linux has this wonderful thing called namespaces(7), and the use case that interests us leverages network_namespaces(7) specifically. The idea is to create a namespace for the VPN and run everything you want in that namespace.
Sounds easy! Let’s see an implementation of this idea on the WireGuard webpage.

sudo -E ip netns exec physical sudo -E -u \#$(id -u) -g \#$(id -g) chromium

Good grief! Two sudos! Sure, one could wrap this in a script, but:

This is of course not the WireGuard guys being silly - it’s the lack of user-facing tools on Linux to let you do this without jumping through hoops. Let’s look into it a bit further and get angry or disappointed or whatever…

The funny fish

The BSD fish

The funny OpenBSD fish is here for a reason. Let’s travel back in time to 2009 and read OpenBSD 4.6’s new features. A certain feature stands out.

Support for virtual routing and firewalling with the addition of routing domains.

I have used the Fish Operating System in the past and the above line that goes

sudo -E ip netns exec physical sudo -E -u \#$(id -u) -g \#$(id -g) chromium

On OpenBSD would translate to

route -T1 exec chromium

The setup is not something wildly complicated in either case, but note the lack of sudos in the OpenBSD command. That was 2009. Now that we have found new appreciation for the mighty fish, let’s travel back to present times (2023 where I live) and try to work with things we need to work with.

The funny penguin

Tux thinking really hard

OpenBSD is unfortunately not as convenient to use or as general-purpose as Linux is. Sure, there are people who use it as a daily OS - I am not one of them. So let’s fix the issues we run into on Linux.

Solving the problem

The small “Why does Linux not have this cool OpenBSD thing” rant is over. We will now start actually solving the problem by addressing the sudo part first.

Routing as a user

In order to route stuff through namespaces, we need ip from iproute2 and administrative privileges - or at least a way to invoke it without requiring a password.
There are three ways to go about this:

Capabilities feel like the more granular way of setting permissions here and get rid of sudo. Here’s how they can be used:

# You have ~/.local/bin in your PATH, right?
cp /bin/ip ~/.local/bin/ip.cap 

# Set a capability on the copied binary
setcap CAP_SYS_ADMIN=+ep ~/.local/bin/ip.cap

# Honestly CAP_SYS_ADMIN might be overkill for what
# we need, but I found no other capability that would
# allow ip.cap to run exec

Now, ip.cap can do all sorts of magic as far as network configuration is concerned. Go ahead and run

ip.cap netns exec namespace chromium

and live a happy life…

Preparing the namespace

This probably won’t get you far, you actually need a namespace to work with, so let’s see how we would create one!

# Create the namespace
ip netns add wgns

It’s as simple as that - we now have a namespace named wgns.

Preparing the WireGuard interface

If you’re a WireGuard user, chances are, you have been using wg-quick. Now you won’t be! wg is your new… friend, and he treats your config files in a different way from wg-quick. He won’t be fixing your nameservers for you, nor will he bind your interfaces to addresses. Instead, he will complain if you suggest he does… So with these things, you are on your own.

And that is not a problem! Let’s create the interface first.

# Add an interface to your current namespace
ip link add wg0 type wireguard

The thing you should do next is comment out the Address and DNS fields in your WireGuard config so that wg setconf won’t complain later. Chances are, you will only have these remaining in your config:

Don’t remove other values outright - we will need them later. Now that the config is ready, let’s prepare the interface

# Apply the config to the interface
wg setconf wg0 <PATH TO CONFIG>

# Move the prepared interface into the new namespace
ip link set wg0 netns wgns

# Bind the interface
ip -n wgns addr add <Address field from the config> dev wg0

# Bring it up
ip -n wgns link set wg0 up

# Add a default route
ip -n wgns route add default dev wg0

# Bring up the loopback interface too
ip -n wgns link set lo up

DNS

We’re almost there, what is left unconfigured is name resolution. How Linux handles resolving names for namespaces is luckily very user-friendly.
For every network namespace, you will get a directory named after it in /etc/netns. /etc/netns/wgns in our case. Inside you will find a file called resolv.conf. Just put nameserver <your DNS field from the config>. If neither the directories or the files exist - don’t be shy, create them yourself.
And you are good to go!

Wrapping things nicely

Create a script, name it something you like

#!/bin/sh
NAMESPACE="$1"
shift
ip.cap netns exec "$NAMESPACE" "$@"

Now you can run any command (indeed, even ping) in any namespace as a user.

DBus issues

Things run on DBus, fcitx5 uses DBus. You click a link somewhere and want it opened in a running browser instance? Likely DBus. A whole lot of functionality in Steam depends on DBus but you should not be using Steam anyway. Either way. If you are having issues with any of these, here are your steps:

Proxying things between namespaces

Sometimes, if you are using a VPN properly and have friends on the same network as you, and you have it namespaced and everything, you might want to run some service on some port (like httpd) in that namespace. And you also might want to access this service from your other namespace, like your regular one. A solution to this could be pairs of veth devices… But I’m too lazy for that, I assume so are you - let’s use socat instead and proxy stuff through a good old Unix socket:

#!/bin/sh
NS=$1
PORT=$2
socat UNIX-LISTEN:/tmp/$NS-$PORT,unlink-early,fork TCP4:127.0.0.1:$PORT &
# Assuming you called the wrapping script nsrun
nsrun $NS socat TCP4-LISTEN:$PORT,reuseaddr,fork UNIX-CONNECT:/tmp/$NS-$PORT &

More tips

Closing thoughts

I tried not to go terribly technical into anything, hopefully by simply following the instructions everything just works for you.

Go back