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.
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