Skip to main content

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

Learn about Linux namespaces by isolating a VPN connection in the net namespace.
Image
Laptop keyboard with Ethernet patch cable

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.

Topics:   Containers   Security   Linux administration  
Author’s photo

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

Try Red Hat Enterprise Linux

Download it at no charge from the Red Hat Developer program.