Routing & Network Namespace Integration

Like all Linux network interfaces, WireGuard integrates into the network namespace infrastructure. This means an administrator can have several entirely different networking subsystems and choose which interfaces live in each.

WireGuard does something quite interesting. When a WireGuard interface is created (with ip link add wg0 type wireguard), it remembers the namespace in which it was created. "I was created in namespace A." Later, WireGuard can be moved to new namespaces ("I'm moving to namespace B."), but it will still remember that it originated in namespace A.

WireGuard uses a UDP socket for actually sending and receiving encrypted packets. This socket always lives in namespace A – the original birthplace namespace. This allows for some very cool properties. Namely, you can create the WireGuard interface in one namespace (A), move it to another (B), and have cleartext packets sent from namespace B get sent encrypted through a UDP socket in namespace A.

(Note that this same technique is available to userspace TUN-based interfaces, by creating a socket file-descriptor in one namespace, before changing to another namespace and keeping the file-descriptor from the previous namespace open.)

This opens up some very nice possibilities.

Ordinary Containerization

The most obvious usage of this is to give containers (like Docker containers, for example) a WireGuard interface as its sole interface.

container # ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
17: wg0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1423 qdisc noqueue state UNKNOWN group default qlen 1
    link/none 
    inet 192.168.4.33/32 scope global wg0
       valid_lft forever preferred_lft forever

Here, the only way of accessing the network possible is through wg0, the WireGuard interface.

Container Network Namespace Diagram

The way to accomplish a setup like this is as follows:

First we create the network namespace called "container":

# ip netns add container

Next, we create a WireGuard interface in the "init" (original) namespace:

# ip link add wg0 type wireguard

Finally, we move that interface into the new namespace:

# ip link set wg0 netns container

Now we can configure wg0 as usual, except we specify its new namespace in doing so:

# ip -n container addr add 192.168.4.33/32 dev wg0
# ip netns exec container wg setconf wg0 /etc/wireguard/wg0.conf
# ip -n container link set wg0 up
# ip -n container route add default dev wg0

And voila, now the only way of accessing any network resources for "container" will be via the WireGuard interface.

Note that Docker users can specify the PID of a Docker process instead of the network namespace name, to use the network namespace that Docker already created for its container:

# ip link set wg0 netns 879

Routing All Your Traffic

A less obvious usage, but extremely powerful nonetheless, is to use this characteristic of WireGuard for redirecting all of your ordinary Internet traffic over WireGuard. But first, let's review the old usual solutions for doing this:

The Classic Solutions

The classic solutions rely on different types of routing table configurations. For all of these, we need to set some explicit route for the actual WireGuard endpoint. For these examples, let's assume the WireGuard endpoint is demo.wireguard.io, which, as of writing, resolves to 163.172.161.0. Further, let's assume we usually connect to the Internet using eth0 and the classic gateway of 192.168.1.1.

Replacing The Default Route

The most straightforward technique is to just replace the default route, but add an explicit rule for the WireGuard endpoint:

# ip route del default
# ip route add default dev wg0
# ip route add 163.172.161.0/32 via 192.168.1.1 dev eth0

This works and is relatively straightforward, but DHCP daemons and such like to undo what we've just did, unfortunately.

Overriding The Default Route

So, instead of replacing the default route, we can just override it with two more specific rules that add up in sum to the default, but match before the default:

# ip route add 0.0.0.0/1 dev wg0
# ip route add 128.0.0.0/1 dev wg0
# ip route add 163.172.161.0/32 via 192.168.1.1 dev eth0

This way, we don't clobber the default route. This also works quite well, though, unfortunately when eth0 goes up and down, the explicit route for demo.wireguard.io will be forgotten, which is annoying.

Rule-based Routing

Some folks prefer to use rule-based routing and multiple routing tables. The way this works is we create one routing table for WireGuard routes and one routing table for plaintext Internet routes, and then add rules to determine which routing table to use for each:

# ip rule add to 163.172.161.0 lookup main pref 30
# ip rule add to all lookup 80 pref 40
# ip route add default dev wg0 table 80

Now, we're able to to keep the routing tables separate. Unfortunately the downside is that explicit endpoint rules still need to be added, and there's no cleanup when the interface is removed, and more complicated routing rules now need to be duplicated.

Improving the Classic Solution

The WireGuard authors are interested in adding a feature called "notoif" to the kernel to cover tunnel use cases. This would allow interfaces to say "do not route this packet using myself as an interface, to avoid the routing loop". WireGuard would be able to add a line like .flowi4_not_oif = wg0_idx, and userspace tun-based interfaces would be able to set an option on their outgoing socket like setsockopt(fd, SO_NOTOIF, tun0_idx);. Unfortuantely this hasn't yet been merged, but you can read the LKML thread here.

The New Namespace Solution

It turns out that we can route all Internet traffic via WireGuard using network namespaces, rather than the classic routing table hacks. The way this works is that we move interfaces that connect to the Internet, like eth0 or wlan0, to a namespace (which we call "physical"), and then have a WireGuard interface be the sole interface in the "init" namespace.

Physical Network Namespace Diagram

First we create the "physical" network namespace:

# ip netns add physical

Now we move eth0 and wlan0 into the "physical" namespace:

# ip link set eth0 netns physical
# iw phy phy0 set netns name physical

(Note that wireless devices must be moved using iw and by specifying the physical device phy0.)

We now have these interfaces in the "physical" namespace, while having no interfaces in the "init" namespace:

# ip -n physical link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN mode DEFAULT group default qlen 1000
    link/ether ab:cd:ef:g1:23:45 brd ff:ff:ff:ff:ff:ff
3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DORMANT group default qlen 1000
    link/ether 01:23:45:67:89:ab brd ff:ff:ff:ff:ff:ff

# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

Now we add a WireGuard interface directly to the "physical" namespace:

# ip -n physical link add wg0 type wireguard

The birthplace namespace of wg0 is now the "physical" namespace, which means the ciphertext UDP sockets will be assigned to devices like eth0 and wlan0. We can now move wg0 into the "init" namespace; it will still remember its birthplace for the sockets, however.

# ip -n physical set wg0 netns 1

We specify "1" as the "init" namespace, because that's the PID of the first process on the system. Now the "init" namespace has the wg0 device:

# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
17: wg0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1423 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1
    link/none

We can now configure the physical devices using the ordinary tools, but we launch them inside the "physical" network namespace:

# ip netns exec physical dhcpcd wlan0
# ip netns exec physical wpa_supplicant -iwlan0 -c/etc/wpa_supplicant/wpa_supplicant.conf
# ip -n physical addr add 192.168.12.52/24 dev eth0

And so forth. Finally, we can configure the wg0 interface like usual, and set it as the default route:

# wg setconf wg0 /etc/wireguard/wg0.conf
# ip addr add 10.2.4.5/32 dev wg0
# ip link set wg0 up
# ip route add default dev wg0

Finished! At this point, all ordinary processes on the system will route their packets through the "init" namespace, which only contains the wg0 interface and the wg0 routes. However, wg0 has its UDP socket living in the "physical" namespace, which means it will send traffic out of eth0 or wlan0. Normal processes won't even be aware of eth0 or wlan0, except dhcpcd and wpa_supplicant, which were spawned inside of the "physical" namespace.

Sometimes, however, you might want to open a webpage or do something quickly using the "physical" namespace. For example, maybe you plan to route all your traffic through WireGuard like usual, but the coffee shop at which you're sitting requires you to authenticate using a website before it will give you a real Internet link. So, you can execute select processes (as your local user) using the "physical" interface:

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

This of course could be made into a nice function for .bashrc:

physexec() { sudo -E ip netns exec physical sudo -E -u \#$(id -u) -g \#$(id -g) "$@"; }

And now you can write the following for opening chromium in the "physical" namespace.

$ physexec chromium

When you're done signing into the coffee shop network, spawn a browser as usual, and surf calmly knowing all your traffic is protected by WireGuard:

$ chromium

Sample Script

The following example script can be saved as /usr/local/bin/wgphys and used for commands like wgphys up, wgphys down, and wgphys exec:

#!/bin/bash
set -ex

[[ $UID != 0 ]] && exec sudo -E "$(readlink -f "$0")" "$@"

up() {
    killall wpa_supplicant dhcpcd || true
    ip netns add physical
    ip -n physical link add wgvpn0 type wireguard
    ip -n physical link set wgvpn0 netns 1
    wg setconf wgvpn0 /etc/wireguard/wgvpn0.conf
    ip addr add 192.168.4.33/32 dev wgvpn0
    ip link set eth0 down
    ip link set wlan0 down
    ip link set eth0 netns physical
    iw phy phy0 set netns name physical
    ip netns exec physical dhcpcd -b eth0
    ip netns exec physical dhcpcd -b wlan0
    ip netns exec physical wpa_supplicant -B -c/etc/wpa_supplicant/wpa_supplicant-wlan0.conf -iwlan0
    ip link set wgvpn0 up
    ip route add default dev wgvpn0
}

down() {
    killall wpa_supplicant dhcpcd || true
    ip -n physical link set eth0 down
    ip -n physical link set wlan0 down
    ip -n physical link set eth0 netns 1
    ip netns exec physical iw phy phy0 set netns 1
    ip link del wgvpn0
    ip netns del physical
    dhcpcd -b eth0
    dhcpcd -b wlan0
    wpa_supplicant -B -c/etc/wpa_supplicant/wpa_supplicant-wlan0.conf -iwlan0
}

execi() {
    exec ip netns exec physical sudo -E -u \#${SUDO_UID:-$(id -u)} -g \#${SUDO_GID:-$(id -g)} -- "$@"
}

command="$1"
shift

case "$command" in
    up) up "$@" ;;
    down) down "$@" ;;
    exec) execi "$@" ;;
    *) echo "Usage: $0 up|down|exec" >&2; exit 1 ;;
esac

A small demo of the above:

wgphys command