Building containers by hand using namespaces: Use a net namespace for VPNs

Image by Michael Schwarzenberger from Pixabay
Over the last year, I've been writing articles for Enable Sysadmin about some of the most-used Linux namespaces. In a previous article, I demonstrated several techniques for creating and interacting with the net namespace. This article extends that exercise by manufacturing a namespace for a single VPN connection. If you want to follow along with this article, be sure to step through the first part. It lays the groundwork that you'll need to set up to follow along with this one.
Of course, you don't have to manage namespaces by hand. Linux containers provide this functionality through user-friendly applications, including Podman, Buildah, and Kubernetes. However, I find that creating namespaces is a good way to grasp fundamental building blocks.
If you followed along with my net namespaces article, you connected several network namespaces together with a virtual switch, so in this article, you're ready to explore creating a single namespace with an OpenVPN connection. I derived this example from a good example on James MCM's GitHub.
I will take this opportunity to demonstrate a few different facets of the net namespace. Performing this task is more suited to a language other than Bash, as you will discover below. You will need two different sessions attached to the host where you are working to accomplish the goal. For clarity, I will note when work is done on the host (in the root namespace) and when it is done in the new namespace.
I'll assume you are not running a firewall on your local machine for this example. And as in the previous article, I recommend setting some variables to make this adapt more easily to the situation.
Lay the groundwork
Start in a shell in the root namespace. Unlike other objectives in this series, you need to use elevated privileges, as I don't know a way to modify network interfaces in the root namespace without using sudo
or full root access.
Begin by setting some environment variables to make this more easily adjustable:
#on the host
export VPN_NAMESPACE=vpn-ns
VETH1=host
VETH2=vpn-namespace
VETH1_IP=172.25.0.1
VETH2_IP=172.25.0.2
SUBNET_BITS=24
VPN_NETWORK=172.25.0.0/$SUBNET_BITS
MAIN_INTERFACE=`ip route |grep default |awk '{print $5}'`
As mentioned in my last article, you can create a directory under /etc/netns
for the namespace to help with resolv.conf
issues. In this case, you can use this directory to store a copy of the resolv.conf
file that you will mount later:
$ sudo mkdir -p /etc/netns/$VPN_NAMESPACE
$ sudo chown -R $USER /etc/netns/$VPN_NAMESPACE/
$ sudo echo "nameserver 37.235.1.174" > /etc/netns/$VPN_NAMESPACE/resolv.conf
Note: The nameserver 37.235.1.174 is listed on freedns.zone, which claims not to do any logging. While no one can verify this short of a court order, I prefer these servers to Google's 8.8.8.8, which is well known for harvesting data.
You might have noticed that I prepopulated the resolv.conf
file. Normally, NetworkManager or resolvd
handles this. However, this example uses OpenVPN on the command line, and while it does have up
and down
sections in its .ovpn
files, for simplicity, the script mounts the prepopulated resolv.conf
. The command below creates a script that will run when OpenVPN connects to a specific endpoint. This is defined per VPN connection and, therefore, will not be called unless added to each .ovpn
file:
$ sudo -Eu root bash -c 'cat << EOF > /etc/openvpn/set_container_resolver.sh
#!/usr/bin/env bash
case \$script_type in
up)
mount -o bind /etc/netns/${VPN_NAMESPACE}/resolv.conf /etc/resolv.conf
;;
down)
umount /etc/resolv.conf
;;
esac
exit 0
EOF'
$ sudo chmod +x /etc/openvpn/set_container_resolver.sh
Next, open a new terminal to create the new namespace where the OpenVPN connection will be established. The environment variables are almost the same as above, except VPN_NETWORK
and MAIN_INTERFACE
are replaced by the PS1
variable. The PS1
variable controls the Bash shell prompt. This is cosmetic and helps distinguish the different namespaces:
# open a new terminal/shell
$ unshare --user --map-root-user --net --mount --fork
VPN_NAMESPACE=vpn-ns
VETH1=host
VETH2=vpn-namespace
VETH1_IP=172.25.0.1
VETH2_IP=172.25.0.2
SUBNET_BITS=24
PS1='\u@${VPN_NAMESPACE} $'
The first line in the code block below creates the VETH pair from within the new namespace and connects it to the namespace associated with pid 1
.
The ip link
command has a subcommand called netns
, and when a namespace has no name you can identify it by PID instead:
ip link add $VETH2 netns $$ type veth peer name $VETH1 netns 1
ip addr add 127.0.0.1/8 dev lo
ip link set lo up
ip link set $VETH2 up
ip addr add $VETH2_IP/$SUBNET_BITS dev $VETH2
ip route add default via $VETH1_IP dev $VETH2
While you can attach a VETH
device to the root namespace, you cannot set it to UP
, nor can you give it an IP. Doing so requires elevated privileges in the root namespace. Additionally, the system probably does not have IP forwarding enabled. The following commands, issued in the root namespace, will accomplish both of these tasks:
# on the host
$ sudo ip link set $VETH1 up
$ sudo ip addr add $VETH1_IP/$SUBNET_BITS dev $VETH1
$ sudo iptables -t nat -A POSTROUTING -s $VPN_NETWORK -o $MAIN_INTERFACE -j MASQUERADE
$ sudo sysctl -q net.ipv4.ip_forward=1
This next part is optional. As you created a net namespace with the unshare
command, it will not be available or visible to the ip netns
command. This is because unshare
creates its processes in /proc/
while the ip netns
command expects namespaces to be located in /run/netns/
. There are a few ways to get around this problem, but perhaps the simplest is to bind mount /proc/
into /run/netns
, effectively mounting the namespace in both locations.
You may wish to do this because creating a namespace with just the ip netns
command only creates the network namespace. It also does so as root. This means if you are looking for an extra layer of isolation via the user namespace, you cannot use the ip netns
command. However, the ip
command provides a very convenient way to interact with already running namespaces. The below commands will bind mount the vpn-ns namespace you are using:
$ sudo mkdir -p /run/netns
$ sudo -Eu root bash -c 'touch /run/netns/$VPN_NAMESPACE'
$ sudo -Eu root bash -c "mount -o bind /proc/`lsns |grep unshare |awk '{print $4}' |uniq`/ns/net /run/netns/$VPN_NAMESPACE"
[ Get a hands-on introduction to daily life as a developer crafting code on OpenShift. Get the eBook OpenShift for Developers. ]
Test the VPN
Run the following command in both the net namespace and the root namespace:
$ curl ifconfig.co/city
This should return the same result (whether accurate or not). You can use this result to verify whether the VPN connection is successfully isolated to a specific namespace.
The following is a brief overview of an OpenVPN file. This article is not intended to be an OpenVPN tutorial, so I will not dive in very deep. In theory, you should use the same steps for Wireguard, PPTP, or most other VPN protocols supported by Linux.
OpenVPN config files
OpenVPN uses files commonly post-fixed with .ovpn
. As with most files in Linux, the file extension is meant for human readability and not for the program. I could very easily have a .txt
file or no extension at all, and OpenVPN would not care as long as the contents are what it expects. Here is the OpenVPN file I am using with some details such as certificates and remote IP/URLs altered or removed:
script-security 2
up /etc/openvpn/set_container_resolver.sh
down /etc/openvpn/set_container_resolver.sh
dev tun
persist-tun
persist-key
cipher AES-256-CBC
auth SHA256
tls-client
client
resolv-retry infinite
remote some.example.com 1194 udp
verify-x509-name "vpn-ns-demo" name
auth-user-pass
remote-cert-tls server
comp-lzo adaptive
<ca>
-----BEGIN CERTIFICATE-----
-----END CERTIFICATE-----
</ca>
<cert>
-----BEGIN CERTIFICATE-----
-----END CERTIFICATE-----
</cert>
<key>
-----BEGIN PRIVATE KEY-----
-----END PRIVATE KEY-----
</key>
key-direction 1
<tls-auth>
-----BEGIN OpenVPN Static key V1-----
-----END OpenVPN Static key V1-----
</tls-auth>
The important bits for this tutorial are at the top. The up
and down
sections of the file reference the script I created earlier, which mounts and unmounts the resolv.conf
file. Without this, the resolv.conf
does not get updated and, depending on the VPN endpoint, this means internet traffic will not resolve. The OpenVPN website has some pointers for getting started with the client configuration files if you are interested in reading further.
Test the VPN
Inside the namespace, start the VPN:
$ openvpn --config Downloads/pfSense-UDP4-1194-stratusvpn-config.ovpn
Note: As you are not feeding a credentials file into the openvpn
command, it will consume the foreground shell after you log in. This is part of the reason it is useful to bind mount the namespace where the ip netns
command can interact with it.
There are two ways to interact with the namespace now. You can find the pid and use nsenter
:
$ sudo nsenter -t `lsns |grep unshare |awk '{print $4}' |uniq -a`
This gives you a root shell where you can run the curl
command:
# curl ifconfig.co/city
Windsor
Note: The shell prompt did not change because the PS1
variable is tied to the Bash session because it was not globally exported.
Confirm that the root namespace registers in a different city:
$ curl ifconfig.co/city
Sioux Falls
This works fine for several programs but not for a browser. Trying to launch a browser results in odd behavior:
# firefox
Running Firefox as root in a regular user's session is not supported.
($HOME is /root which is owned by nobody.)
This is the real reason to bind mount the namespace. You can get around this by using the ip netns
command:
# On the host
$ sudo ip netns exec $VPN_NAMESPACE sudo -u $USER firefox
You now have a browser running in a namespace that sends all of its traffic out of the VPN connection while the rest of the system continues to use the local connection.
Wrap up
You've now created a net namespace to use for VPN communication so that only applications that run in the namespace are routed through the VPN.
Creating your own namespaces allows you the flexibility to customize your environment to your specific needs, without downloading a container full of binaries you didn't intentionally install. Namespaces provide a very customizable interface for tasks such as isolating application traffic.






Steve Ovens
Steve is a dedicated IT professional and Linux advocate. Prior to joining Red Hat, he spent several years in financial, automotive, and movie industries. More about me