WireGuard in a separate Linux network namespace


Updated: 2021-07-25

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 iproute2 suite.

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 (wg0).

# wg-quick down wg0

If your /etc/wireguard/wg0.conf contains Address= and 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 1.2.3.4/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 (ip, netstat, ... ). If you run wg show, e.g., its output will be empty.

To query wg, you need to specify the namespace. For this, we need to work through ip and specify the wireguard namespace, using the netns shorthand:

# ip netns exec wireguard wg show
interface: wg0
  public key: sdqsdqd
  private key: (hidden)
  listening port: 20815

peer: asdfjqku
  endpoint: 4.3.2.1: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 netns exec.

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 /etc/netns/$namespace:

# cat /etc/netns/wireguard/resolv.conf
nameserver 183.218.98.93

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).

Killswitch

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 namespace@wireguard.service (%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 BindsTo=and 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 1.2.3.4/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