NSWG Part 3: Docker run

In my previous posts about nswg (part1 and part2) I showed how to use it to run programs in a different network namespace, how to connect network namespaces with a veth pair and how to use wireguard's "psychic" powers to run a wireguard client without having a specific route to the server.

In this post I show how to use these functionalities to route connections of a single docker container through a wireguard connection. In a future post I will show how to do it for multi-container applications (docker compose).

There are other ways

What I propose here is not the only way to route containers traffic through a wireguard connection. For instance there is the post Routing Docker Host And Container Traffic Through WireGuard from the linuxserver.io community. They create a docker network using a bridge interface in the default (host) network namespace. Then a docker wireguard container does the VPN connection and firewall rules in the default network namespace are used to route traffic of other containers through that connection.

I dislike the amount of things that have to be done in the default network namespace to make it work (the new bridge interface and the firewall rules). I feel that conceptually it contrasts with the idea of containers.

What I refer to as the psychic powers of wireguard interfaces, which is their ability to connect to a wireguard server using a route from a different network namespace, allow to do this without changing anything in the default namespace. This is how nswg does it.

The goal

To show how this works we are going to set up the same scheme of connections than in my second post about nswg. I reproduce here the diagram of 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

The default network namespace and wg_server are connected with veth pair through which wg_server can also connect to the internet. wg_client connects to wg_server using psychic powers. The devices in the veth pair use the 10.0.0.0/24 network and the wireguard interfaces use the 10.0.1.0/24 network. The following table shows the ip's of each namespace on both networks.

Namespace veth ip wireguard ip
Default 10.0.0.1 ---
wg_server 10.0.0.2 10.0.1.1
wg_client --- 10.0.1.2

The difference with the previous post is that now we are going to run a docker container running a web server in wg_client. Additionally, we are going to forward port 80 from wg_server to wg_client. The effect is that a browser running in the default network namespace that connects to 10.0.0.2 will see the content served by the client.

Note that the wg_server namespace can be changed for a remote wireguard server. The reason I do it in a network namespace in the local machine is so anyone can reproduce the example in their own computer.

Preparation

The nswg repository has the path examples/docker_run_apache with the code we will be running. It has two of the files that were used in the previous post set_up_server_namespace.sh and gen_wg_files.sh. It also contains the folder www with the file index.html and Dockerfile that creates a docker image that starts an apache web server that server the contents of the www folder.

Let us start by building the docker image with the name nswg_apache. Set your working directory to examples/docker_run_apache and run

docker build -t nswg_apache .

Let us now run gen_wg_files.sh which creates the wg_server namespace, creates the configuration files wgserver.conf and wgclient.conf and sets up the wireguard server. And right away lets use iptables to forward port 80.

./set_up_server_namespace.sh
    
sudo ip netns exec wg_server \
    iptables -t nat -A PREROUTING \
    -p tcp --dport 80 -j DNAT \
    --to-destination 10.0.1.2:80

The docker image

If you look into Dockerfile you'll see that it simply takes the docker httpd image and copies www to the folder that httpd serves inside the container.

Let's run the nswg docker image in detached mode (so we can continue using the terminal after).

docker run -d nswg_apache

Great now it is running. But how do we interact with it? We could have bound a port from the default namespace to port 80 but we didn't (something like -p 127.0.0.1:5000:80 ). We can find the ip of the container and connect to it with the browser. The following obscure one liner prints the ip of the nswg_apache container.

docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}'   \
    "$(docker ps | awk '{if($2=="nswg_apache") {print $1; exit 0;} }')" 

If you visit that ip with your browser you should see the text NSWG TEST.

Here we ran the container using the default network option, which uses a bridge interface to connect it to the default network namespace. We want to configure the network in a different way so lets stop this container and start it differently. The following obscure one line stops the running nswg_apache container.

docker stop "$(docker ps | awk '{if($2=="nswg_apache") {print $1; exit 0;} }')" 

How to do it "manually"

Let us see how to get the connection scheme we want without using any new features of nswg. Later we'll use the docker-run sub-command that automates all the steps.

We start the docker image in detached mode like before and without any network configuration. We also save the output, which is the container id, in the variable cid. This will help make one liners a less cryptic.

cid=$(docker run -d  --network=none nswg_apache)

The container started but there is no ip we can use to connect to it since no network option was given to it. We want to take the isolated network namespace and run the nswg wireguard command on that namespace. To do that we need to give a name to the isolated namespace where the container is currently running. For that we can use ip netns attach some_pid some_name which gives the name some_name to the network namespace where the process with id some_pid is running.

Use cid to get the pid of the container and save it in the variable cpid

cpid=$(docker inspect -f '{{.State.Pid}}' "$cid")
Name the container namespace wg_client
sudo ip netns attach wg_client $cpid

You should see wg_client when you run ip netns list. If you see the list of interfaces in that namespace (with sudo ip -n wg_client addr) you should only see the lo device. So in its current state the container cannot connect to anything.

We can now summon the psychic connection.

nswg -n wg_client -c ./wgclient.conf wireguard

At this point wg_client is connected to the internet (as we saw in my previous post) and also port 80 of wg_server is redirected to it. So, if you visit 10.0.0.2 with your browser you should see NSWG TEST. Alternatively with curl you should get

$ curl 10.0.0.2
<!DOCTYPE html><html><body><h1> NSWG TEST </h1></body></html>

To see how nswg automates let us cleanup the changes we did and stop the container

nswg -n wg_client cleanup
sudo ip netns del wg_client
docker stop $cid    

nswg docker-run

The following command performs all the operations done in the previous section.

nswg -n wg_client \
     -c wgclient.conf \
     docker-run nswg_apache

You can add any additional arguments you need to docker run. As an example let us cleanup the effects of the previous command nswg -n wg_client cleanup and let us add an environment variable to the container

nswg -n wg_client \
     -c wgclient.conf \
     docker-run --env MYVAR=foo nswg_apache

Do note that there are two arguments that are implicitly used inside nswg -d and --network=none. Thus, you should not use any arguments that make the container interactive at startup or that configure the network. If you want to interact with the container you should use docker attach after it is started.

When you run cleanup on a namespace where you have used docker-run the namespace is deleted and the container is stopped.

Let us finish by cleaning up all the modifications we did to your system

nswg -n wg_client cleanup
nswg -n wg_server cleanup