WireGuard in a separate Linux network namespacePublished: 2021-07-21
This posts covers the implementation of an isolated WireGuard instance through the use of Linux network namespaces. The Linux kernel supports network namespaces more or less fully since kernel 2.6.29. They allow you to isolate network system resources - a separate network stack running independently from (and next to) your normal network stack. The advantage of running WireGuard in a separate network namespace is you do not need to bother with things like routing; in the new namespace, all traffic goes through the WireGuard interface, since it's the only interface available.
WireGuard configuration is covered extensively elsewhere, so we'll skip that. Just note you need to have your WireGuard setup up and running before you can move it to a new network namespace. Hence, the instructions below assume a functional WireGuard setup. I got a VPN from Mullvad, amongst others because they support WireGuard.
Setting up a network namespace
On a modern Linux distribution, setting up a new network namespace is pretty straightforward, using the
To set up a new network namespace (called wireguard), issue the following command:
# ip netns add wireguard # ip netns wireguard (id: 0)
Voilà, easy peasy. You got your separate namespace right there! Now let's move our WireGuard instance into it.
Moving WireGuard into it
As mentioned before, make sure your WireGuard setup works before moving it to the new 'wireguard' namespace. Now stop your WireGuard instance so we can move it. We're assuming standard interface names (
# wg-quick down wg0
DNS= stanzas, uncomment these. While
wg-quick can handle those entries, it cannot handle namespaces. Hence why we'll be using
wg setconf, which doesn't support those entries. If you leave them in, you'll see
"Line unrecognized: ..." errors pop up.
Enter the following commands:
# ip link add wg0 type wireguard # wg setconf wg0 /etc/wireguard/wg0.conf # ip link set wg0 netns wireguard
Next, configure the IP addresses, bring up the interface and set the default route:
# ip -n wireguard addr add 18.104.22.168/32 dev wg0 # ip -n wireguard addr add dead:beef:1234:5678::1:90ab/128 dev wg0 # ip -n wireguard link set wg0 up # ip -n wireguard route add default dev wg0
That's it, you should have your WireGuard instance running in its own namespace right now. Since the new namespace is separate, you cannot query its interfaces (or the programs using it) issuing the usual commands (
netstat, ... ). If you run
wg show, e.g., its output will be empty.
wg, you need to specify the namespace. For this, we need to work through
ip and specify the wireguard namespace, using the
# ip netns exec wireguard wg show interface: wg0 public key: sdqsdqd private key: (hidden) listening port: 20815 peer: asdfjqku endpoint: 22.214.171.124:13091 allowed ips: 0.0.0.0/0, ::/0 latest handshake: 59 seconds ago transfer: 1.06 GiB received, 1.12 GiB sent persistent keepalive: every 25 seconds
As you might have deduced from the setup commands,
ip has a shorthand to specify the intended namespace for its own calls, using the
-n argument. For external commands you need to rely on
Setting up DNS
Despite the namespace being a separate networking stack, DNS lookups from the new namespace will still be performed through the default DNS server until you configure DNS for the new namespace. Similarly to the default
/etc/resolv.conf, the new one needs a
resolv.conf living under
# cat /etc/netns/wireguard/resolv.conf nameserver 126.96.36.199
Until you set the nameserver, lookups will still be done through the one defined in the default network namespace. So if you use a network namespace because of privacy reasons, be sure to verify your DNS settings.
Unlike the namespace itself, it seems
/etc/netns/$namespace/ and its contents persist across reboots. Similarly to the DNS settings, you'll also want to load separate firewall rules, since the new namespace isn't firewalled.
Running applications in the new namespace
You need superuser privileges to use the new namespace, but you don't want to run your application to run as root unless it needs to. With the following command, you can have an application run in the 'wireguard' namespace, as your own user:
$ sudo -E ip netns exec wireguard sudo -E -u \#$(id -u) -g \#$(id -g) lighttpd
With this command, you'll call
ip as root, preserving the environment, and execute lighttpd in the 'wireguard' namespace as your own user (with the same UID and GUID).
In a normal network environment, you might want a killswitch, so all traffic is cut when the VPN interface goes down, and traffic isn't routed over unencrypted networks. Mullvad allows you to download a WireGuard configuration with integrated killswitch. When using a separate namespace, there is no need for such a killswitch anymore - your WireGuard connection is the only network connection in the namespace, so when it goes down, all traffic gets halted anyway.
Automation and integration
One could throw all this into
/etc/rc.local, but the systemd we all love/loathe has done away with that, so it's not the most future proof solution (and whether you're a systemd adept or not, it's still very hacky). Network namespaces support is in the works with systemd. Debian's ifupdown has no namespace support either, so at least a modern Debian distribution does not allow you to automate it all (besides throwing it into a script). So for now, condensing all the commands above into a single, custom systemd service (or one for each application to be run in the separate namespace, depending on a general namespace setup one) seems to be the way to go.
You'll find a example of a working unit file below. I christened it
email@example.com (%i translates to wireguard, see the systemd specifiers for explanation). Paths apply to Debian Bullseye, check or modify them according to your Linux distribution.
To be able to use the namespace in dependent unit files, the
NetworkNamespacePath= directive is critical. Both the 'base' unit file and the dependent unit files need to have it set. For dependent unit files, you can (and should) use the
JoinsNamespaceOf= directives; the first to ensure the dependent unit file only gets started when the base one is up and running, and the second so it uses the same namespace.
[Unit] Description=WireGuard namespace service [Service] NetworkNamespacePath=/run/netns/%i Type=oneshot RemainAfterExit=yes ExecStartPre=-/bin/ip netns delete %i ExecStart=/bin/ip netns add %i ExecStart=/bin/ip link add wg0 type wireguard ExecStart=/usr/bin/wg setconf wg0 /etc/wireguard/wg0.conf ExecStart=/bin/ip link set wg0 netns %i ExecStart=/bin/ip -n %i addr add 188.8.131.52/32 dev wg0 ExecStart=/bin/ip -n %i addr add dead:beef:1234:5678::1:90ab/128 dev wg0 ExecStart=/bin/ip -n %i link set wg0 up ExecStart=/bin/ip -n %i route add default dev wg0 # Kill namespace when stopped ExecStop=/bin/ip netns delete %i [Install] WantedBy=multi-user.target WantedBy=network-online.target