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.
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
- wg_server: A network namespace connected to the default namespace through a veth pair and running a wireguard server.
- wg_client: An isolated network namespace that connects to the wireguard server.
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 byset_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.