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 sudo
s! Sure, one could wrap this in a script, but:
- It’s still kinda ugly
- Entering a password is annoying
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 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 sudo
s 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
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:
- Add the user to
sudoers
- Set the sticky bit on
ip
- Use
capabilities(7)
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:
PrivateKey
under[Interface]
PublicKey
under[Peer]
AllowedIPs
(set to0.0.0.0, ::0/0
) under[Peer]
Endpoint
under[Peer]
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:
- Curse at DBus
- Make sure your
$DBUS_SESSION_BUS_ADDRESS
is set to a socket you can reach from your filesystem. Network namespaces separate a lot of things, and ‘abstract sockets’, whatever the hell that is, and for whatever reason DBus uses them by default (I think), are one of them. So keep an eye on that.
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
- Automate the process of bringing up namespaces (incredible advice)
- Download all configs from whatever VPN provider you have,
sed
out the fieldswg setconf
doesn’t like, make a script to switch configs on the fly (usefzf
maybe) and reload the namespace. - Make a script that runs
$0
in a namespace, create symlinks in$PATH
named after every binary you want to always run under a tunnel. (basically, make shims) - If you use
ufw
, make sure to enable it in the namespaces!
Closing thoughts
I tried not to go terribly technical into anything, hopefully by simply following the instructions everything just works for you.