Using network namespaces to force VPN use on select applications
Or flexible half-baked tools working together
by on 2020-10-20I’ve had enough of paying for 1Gbps fiber connection and only using 100Mbps because of using my server at Kimsufi as my main gate to the internet.
The goal is now to use my VPN connection to protect my websurf only (no webmaster needs to get my home IP). The rest is more or less not-as-private, and will be using my ISP’s (high bandwidth) connection (games, GPG-signed updates). This usage differs from other examples that make seldom use of the physical realm: here we will use the physical connection by default, and “containing” specific applications.
You should probably be up to speed with what WireGuard is and how it works: see e.g. a previous entry.
§ steps
§ manual
Create an empty netns
and populate it with
lo
.
ip netns add vpn
ip -n vpn link set lo up
The wg1
interface’s “place of birth” is the physical
realm. Thus, we first need to create wg1
in the unnamed
namespace before moving it to the vpn
netspace. This is
needed because wg1
will send (encrypted) packets to the
server through a real interface. If we create wg1
in the
vpn
namespace, WireGuard won’t know how to reach the
outside world (including the WireGuard server).
ip link add wg1 type wireguard
ip link set wg1 netns vpn
Then we use wg(8)
and ip(8)
to setup our
VPN link
ip netns exec vpn wg setconf wg1 /etc/wireguard/wg1.conf
ip -n vpn a add 10.X.Y.PPP/24 dev wg1
ip -n vpn a add aaaa::ffff/64 dev wg1
ip -n vpn link set wg1 up
ip -n vpn route add default dev wg1
ip -n vpn -6 route add default dev wg1
Don’t forget to put a resolv.conf(5)
file in
/etc/netns/vpn
as written in ip-netns(8)
’
manpage. Now, testing time:
for v in 4 6; do
printf '%s' "default IPv$v: " ; curl -s$v https://icanhazip.com
printf '%s' "VPN IPv$v: " ; ip netns exec vpn curl -s$v https://icanhazip.com
done
It is not yet possible to use systemd.networkd(5)
to set
everything up correctly, though there seems to be ongoing work on
that front; let’s hope it gets a bit more attention than my own bug report
to systemd.
§ usage
% ip netns exec vpn ping -c1 try.popho.be
setting the network namespace "vpn" failed: Operation not permitted
ip netns
can’t run as user. I can’t be bothered to
install and configure sudo
properly, so we’ll rely on yet
another piece of software instead: firejail(1)
% firejail --noprofile --netns=vpn firefox
firejail
does its job, but this is a pain to type each
time. Instead, we’ll wrap firefox in a short script.
We put run-with-vpn
in one of the first items of
$PATH
(in ~/bin
for example):
#!/bin/sh
# Aptly linking this script allows us to run any program in a network namespace
# Removing the first item of PATH revokes the precedence of our script, and we
# SHOULD end up using firejail on the REAL binary we target.
# We suppose that firejail does NOT reside next to our script, but further away
# in $PATH
if [ -e /var/run/netns/vpn ] ; then # Network NameSpace exists
if command -v firejail >/dev/null 2>&1 ; then
PATH=${PATH#*:} firejail --noprofile --netns=vpn "$(basename $0)" "$@" &
fi
else
notify-send "NO VPN" "$(basename $0) is running with NO VPN"
PATH=${PATH#*:} "$(basename $0)" "$@" &
fi
Now, we create a symlink firefox
to
run-with-vpn
:
% cd ~/bin
% ln -s run-with-vpn firefox
% command -v firefox
/home/moviuro/bin/firefox
% sh -x "$(!!)"
+ '[' -e /var/run/netns/vpn ']'
+ command -v firejail
++ basename /home/moviuro/Documents/setup/bin/firefox
+ PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/home/moviuro/.local/share/flatpak/exports/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl
+ firejail --noprofile --netns=vpn firefox
With this, we can simply launch firefox
from our shell
or rofi
, and it’ll be run in our VPN NetNS.
§ automatic with a systemd.service(5)
[Unit]
Description=Start a VPN Network Namespace
[Service]
Type=oneshot
ExecStart=/usr/bin/ip netns add vpn
ExecStart=/usr/bin/ip -n vpn link set lo up
ExecStart=/usr/bin/ip link add wg1 type wireguard
ExecStart=/usr/bin/ip link set wg1 netns vpn
ExecStart=/usr/bin/ip netns exec vpn wg setconf wg1 /etc/wireguard/wg1.conf
ExecStart=/usr/bin/ip -n vpn a add 10.X.Y.PPP/24 dev wg1
ExecStart=/usr/bin/ip -n vpn a add aaaa::ffff/64 dev wg1
ExecStart=/usr/bin/ip -n vpn link set wg1 up
ExecStart=/usr/bin/ip -n vpn route add default dev wg1
ExecStart=/usr/bin/ip -n vpn -6 route add default dev wg1
ExecStop=/usr/bin/ip netns delete vpn
RemainAfterExit=true
[Install]
WantedBy=default.target
§ side-effects
The software that runs in a seperate NetNS can’t possibly leak your other addresses (so, that’s cool if you use any modern browser shipping far too many “features” such as: WebRTC…).
My desktop now has one IP that will only do websurf: TCP/80 and 443,
UDP 443 and 53; I can filter even more what is allowed outbound in
pf
.
My machine is no longer blocked by my server’s pf
if it
tries to initiate connection to known-bad IP addresses (C&C,
SpamHaus’ DROP list,
etc.).
§ conclusion
I achieved what I set out to do, however I’m a bit dissatisfied about
having two VPN interfaces (wg0
and wg1
)
connecting to the same VPN on the same machine, and the tooling is far
from perfect:
wg0
is just a usual WireGuard interface, withoutAllowedIPs=0.0.0.0/0
; this one allows me to reach some services such as my IRC bouncer or, as shown below, my lying DNS server;wg1
is a WireGuard interface that we have to create by hand, half configure withwg(8)
, half withip(8)
;systemd
in its race to assimilate everything under the sun, still doesn’t support some features that I needed.