NSWG Part 2: Hocus Pocus

In my previous post about nswg I showed how to use it to execute commands in a different network namespace and how to use a veth pair to connect it to the default namespace.

In this post I show the wireguard related functionality of nswg. Originally the title was going to be 'NSWG Part 2: Wireguard' but I thought the current title would give it a mysterious touch and it might sell better. It is also a correct title because you are about to behold a magic trick.

Indeed, wireguard interfaces posses "psychic powers". They can summon connections to interfaces from other network... namespaces... 👻 👻 👻 (booo! booo!).

But don't worry you won't need to get an exorcism after reading this post. It is all a trick and I will also show the sleight of hand.

The goal of this post is to build the following scheme of namespaces and connections.

%3 default Default namespace wg_server wg_server default--wg_server veth pair wg_client wg_client wg_server--wg_client wireguard "psychic" connection internet Internet internet--default physical device
Connections diagram

On top of the diagram, we have the elements that exist before we do anything. It shows the default network namespace connected to the internet through a physical device. Then we have the parts that will be created

The example uses IPv4, but note that nswg also supports IPv6.

Preparation

The nswg repository has the path examples/wireguard_connections with the code we will be running. You can either clone the repository or download the files. To run the example you also need to have nswg somewhere in your PATH.

The example folder contains three bash scripts:

gen_wg_files.sh
It generates two wireguard configuration files wgserver.conf and wgclient.conf. The server file should be used with wg-quick, it uses the postUp option to forward packets coming from the client to the outside network. It also guesses the interface to use for this forwarding, so it is important to run this script inside the network namespace containing the wireguard server. We won't use this script directly, it is called by set_up_server_namespace.sh.
set_up_server_namespace.sh
Creates the wg_server namespace, generates the wireguard configuration files and sets up a wireguard interface configured with the generated wgserver.conf file.
set_up_client_namespace.sh
Sets up the wg_client namespace and sets up a wireguard interface with the generated wgclient.conf file.

wg_server

Let us look at the contents of set_up_server_namespace.sh

#!/bin/sh
    
gateway=$(nswg guess-gw)
    
nswg -n wg_server \
     -ipa "10.0.0.1/24" \
     -ipb "10.0.0.2/24" \
     -gw "$gateway" \
     connect
     
nswg -n wg_server run ./gen_wg_files.sh

sudo ip netns exec wg_server wg-quick up ./wgserver.conf

It starts by trying to obtain the ip of the gateway network. You can check the output of nswg guess-gw to make sure it has the right value. If it doesn't, you can change it manually in the script. Note that you must include the subnet in CIDR notation.

After, nswg is used to create a new namespace wg_server and a veth pair is used to connect it to the default namespace. -n provides the name of the network namespace to use (and it is created if it doesn't exist already), -ipa provides the ip for the veth interface in the default network namespace and -ipb for the one in wg_server. -gw provides the ip of the gateway in the default network namespace.

Then gen_wg_files.sh is executed inside the wg_server. That is so it gets the right gateway in that namespace.

Finally wg-quick is used to configure a wireguard interface using wgserver.conf.

Let us now run this script, ./set_up_server_namespace.sh. Now you should see wg_server in the list of namespaces when you run ip netns list.

The new namespace should have three interfaces lo, wgserver and the third one should have the form nswg_***_b. The *s in the middle are randomly generated when the interface is created. It's veth pair in the default namespace has the same name but it finishes wit _a instead of _b.

The output of nswg -n wg_server run ip addr when I was preparing this post was

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    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
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: nswg_jeY_b@if13: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether de:77:78:2a:9d:f5 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.0.0.2/24 scope global nswg_jeY_b
       valid_lft forever preferred_lft forever
    inet6 fe80::dc77:78ff:fe2a:9df5/64 scope link
       valid_lft forever preferred_lft forever
3: wgserver: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
    link/none
    inet 10.0.1.1/24 scope global wgserver
       valid_lft forever preferred_lft forever

You can check you are able to reach the internet inside wg_server by pinging the Quad9 DNS with nswg -n wg_server run ping 9.9.9.9. Note that using a name in the ping command (for instance nswg -n wg_server run ping www.wikipedia.org) doesn't work because we did not specify a name server for this namespace (it might actually work in some cases, for instance if you are using the nscd service). That is done with nswg's -dns option, which we will use for the client.

Hocus Pocus

Everything is set up now for the magic trick. Here we proceed differently than in the previous section. Let us first run the client script and see its effect before looking at its contents. I know asking you to run the code before looking at it might sound suspicious, but don't worry, you can trust me. Muahahaha! muahahaha!.

Let us now cast the spell

🪄
./set_up_client_namespace.sh
⭐ ⭐ ⭐

And magic just happened before your eyes...

Now wg_client should appear in the list of namespaces (which you can get with ip netns list).

wg_client should contain only two interfaces lo and another one with the pattern nswg_*** where the *s represent a three characters string randomly generated. That one is the wireguard client.

The output of nswg -n wg_client run ip addr I ran when preparing this post is

1: lo:  mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    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
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: nswg_DuK:  mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
    link/none
    inet 10.0.1.2/24 scope global nswg_DuK
       valid_lft forever preferred_lft forever

Look well and make sure I am not hiding any interface in this output ...

Let us now look at the contents of the wireguard configuration file for the client wgclient.conf. Your generated file should be identical except for the private and public keys.

[Interface]
PrivateKey=ANpOCRwJN1A1lRn/nV4qoVNGBMF4s6j50BmisqV9/EY=
ListenPort=21841
Address=10.0.1.2/24
DNS=9.9.9.9


[Peer]
PublicKey=2CWVEVo4G2+eEwo1MQePK1ME2oveX4DsqZz+BpF8oTU=
EndPoint=10.0.0.2:51820
AllowedIps=0.0.0.0/0 , ::/0

PersistentKeepalive = 25
Look at the line EndPoint=10.0.0.2:51820 in the [Peer] section. That is the ip and port where wireguard connects to the server.

Let us now see the output of nswg -n wg_client run route and make sure I am not hiding any route...

Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         0.0.0.0         0.0.0.0         U     0      0        0 nswg_DuK
10.0.1.0        0.0.0.0         255.255.255.0   U     0      0        0 nswg_DuK

Note that the only two routes involve the wireguard interface. But in order then to connect to the server, it needs a route to 10.0.0.2 in the server namespace. Looking at the routes and available interfaces this is impossible. The wireguard connection could not have been established

Behold!

$ nswg -n wg_client run ping www.wikipedia.org
PING dyna.wikimedia.org (208.80.154.224) 56(84) bytes of data.
64 bytes from text-lb.eqiad.wikimedia.org (208.80.154.224): icmp_seq=1 ttl=50 time=291 ms
64 bytes from text-lb.eqiad.wikimedia.org (208.80.154.224): icmp_seq=2 ttl=50 time=85.6 ms
64 bytes from text-lb.eqiad.wikimedia.org (208.80.154.224): icmp_seq=3 ttl=50 time=56.5 ms
64 bytes from text-lb.eqiad.wikimedia.org (208.80.154.224): icmp_seq=4 ttl=50 time=68.0 ms
64 bytes from text-lb.eqiad.wikimedia.org (208.80.154.224): icmp_seq=5 ttl=50 time=75.4 ms

The sleight of hand

The reason the connection works is that the socket the wireguard interface uses to send and receive packets stays in the namespace where it was created. This means it can connect to any ip for which there is a route in the namespace it was created even if it is moved to a different network namespace.

Let us now look at the contents of set_up_client_namespace.sh.

#!/bin/sh

nswg -n wg_client \
     -c ./wgclient.conf \
     -win wg_server \
     wireguard

Here the wireguard sub-command is used. -n is used to provide the namespace where the wireguard interface will end up. -c is used to provide the path to the wireguard configuration file. -win is used to provide the network namespace where the wireguard interface will be created (win= wireguard interface namespace).

When -win is not provided, the wireguard interface is created in the namespace where the command is run. In this example, we could have run the same command in the default namespace without using -win and that would have worked as well. This is because in the default network namespace there is a route to 10.0.0.2 in wg_server through the veth interface.

Note also that a DNS was configured for wg_client since the configuration file has the DNS line. This is implemented by writing the provided DNS ip to the file /etc/netns/wg_client/resolv.conf, which is bind mounted to /etc/resolv.conf when something runs with nswg -n wg_client run ....

Finally be aware that nswg only supports two of the options supported by wg-quick: Address and DNS.