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.
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.com
, 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.com
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.
Improved Rule-based Routing
The prior solution relies on us knowing the explicit endpoint IP that should be exempt from the tunnel, but WireGuard endpoints can roam, which means this rule may go stale. Fortunately, we are able to set an fwmark
on all packets going out of WireGuard's UDP socket, which will then be exempt from the tunnel:
# wg set wg0 fwmark 1234
# ip route add default dev wg0 table 2468
# ip rule add not fwmark 1234 table 2468
# ip rule add table main suppress_prefixlength 0
We first set the fwmark
on the interface and set a default route on an alternative routing table. Then we indicate that packets that do not have the fwmark
should go to this alternative routing table. And finally we add a convenience feature for still accessing the local network, whereby we allow packets without the fwmark
to use the main routing table, not the WireGuard interface's routing table, if it matches any routes in it with a prefix length greater than zero, such as non-default local routes. This is the technique used by the wg-quick(8)
tool.
Improving the Classic Solutions
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.
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 link 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: