A brief overview of Linux namespaces
Linux implements namespaces, a feature that isolates different classes of system resources. They’re used extensively in containerization projects, for example Docker. There are several types of namespaces: pid, net, mnt, user, uts, and ipc.
We’re interested in network namespaces (netns), which isolate network resources. Each netns can have its own interfaces, sets of IP addresses and ports (sockets), routing tables, firewall rules, and so on. You can move interfaces from one netns to another. A physical interface (e.g., eth0) can be in only one netns at a time.
By default, all interfaces and processes live in the initial (initial) netns; it has no explicit name and does not appear in listings. When a netns is torn down, any physical interfaces it contained are moved back to the initial netns. The teardown actually occurs only after the last process in that netns exits. For example, even if you delete a specific netns, as long as a process started inside it is still running, the physical interface won’t be moved back to the initial netns until that process terminates. We’ll cover this scenario later.
A Quick Primer
netns is what lets us tunnel traffic on a per-application basis. First, we need to get familiar with the control commands.
Network Namespace Basics
Network namespaces (netns) are managed by the ip utility from the iproute2 package. To interconnect netns, you can use a pair of virtual interfaces called veth. Let’s look at an example of creating multiple netns and linking them together. To do this, run:
$ sudo ip netns add ns_1
$ sudo ip netns add ns_2
You can check for these netns with the command ip , or just ip , since list is the default action. We’ll add a virtual pair using the command
$ sudo ip link add dev virt01 type veth peer name virt02

The interfaces are in place; now move virt01 into the ns_1 netns and virt02 into ns_2.
$ sudo ip link set virt01 netns ns_1
$ sudo ip link set virt02 netns ns_2
If no error messages appeared, the operation succeeded. To run a command inside a netns, use the command
$ sudo ip netns exec <netns name> <command to execute>
For example, to list the interfaces available inside the netns ns_1, use
$ sudo ip netns exec ns_1 ip link
After checking the man page, we learn that you can run ip commands inside a netns using $ sudo ip -n

By default, interfaces created in or moved into a netns come up disabled (down), including lo.
Let’s assign IP addresses to our interfaces virt01 and virt02 and bring them to the UP state. We’ll use a beefed-up command we picked up from the man pages:
$ sudo ip -n ns_1 addr add 10.0.0.1/24 dev virt01
$ sudo ip -n ns_2 addr add 10.0.0.2/24 dev virt02
$ sudo ip -n ns_1 link set dev virt01 up
$ sudo ip -n ns_2 link set dev virt02 up
$ sudo ip -n ns_1 addr show
$ sudo ip -n ns_2 addr show

As I mentioned, each netns has its own routing table—let’s verify that.

To check connectivity between ns_1 and ns_2, we’ll use the ping command.
$ sudo ip netns exec ns_1 ping -c 4 10.0.0.2

As you recall, 10.0.0.2 is assigned to the virt02 interface in ns_2. You can connect a network namespace to the physical eth0 in much the same way.
Executing commands and launching processes inside a network namespace (netns)
As you may recall, commands inside a netns are executed using
$ sudo ip netns exec <netns name> <command>
To avoid typing all of this every time, let’s launch bash!
$ sudo ip netns exec ns_1 bash
After that, all commands will run inside the netns — we can also check the firewall status. To return, type exit or CTRL .

I always keep firewall rules in place, so this is definitely a separate ruleset. 😉
Let’s move on to an example that ends with returning a physical interface to the initial netns. To start, we’ll check the current location of enp0s3 and move it into ns_2.
$ sudo ip link set dev enp0s3 netns ns_2

Bring it up in the netns ns_2 and launch Wireshark as the user (eakj) in the same namespace.
$ sudo ip -n ns_2 link set dev enp0s3 up
$ sudo ip -n ns_2 link
$ sudo ip netns exec ns_2 sudo -u eakj wireshark 2>/dev/null &

Now let’s delete the netns ns_2 and check whether enp0s3 returns to the initial netns.
$ sudo ip netns del ns_2

Let’s close Wireshark and see if that helps.

That should be enough to understand how netns works and how to manage them. Let’s move on to OpenVPN.
Understanding OpenVPN
Not long ago I wrote about how to set up your own OpenVPN on a rented server. If you followed that guide, you already have a client configuration file ready to use. Config files from other VPN providers will work too, since no changes on the server side are required.
Updating the client configuration
Let’s take a look at the new OpenVPN directives.
-
ifconfig-noexec— prevents the client from automatically runningifconfigto assign an IP to thetuninterface. Instead, the required parameters are provided via environment variables. -
route-noexec— same idea asifconfig-noexec, but for routes: it won’t add routes automatically; the necessary parameters are passed via environment variables. -
route-up /— runs the script with the environment variablefull/ path/ to/ script $script_typeset toroute-up. -
up /— likefull/ path/ to/ script route-up, but runs after the tun/tap interface is brought up, with$script_type=up. Executes before theuserdirective (i.e., before privileges are dropped). -
down /— likefull/ path/ to/ script route-up, but runs after the tun/tap interface is torn down, with$script_type=down. Executes after theuserdirective (i.e., with reduced privileges). -
script-security— permits execution of custom scripts.2
The full list of environment variables that OpenVPN passes can be found in man , under the Environmental Variables section.
I hope you noticed the bold text. The up directive will add our netns, and down will remove it. You need root privileges to create and delete a netns. That’s not a problem for up, because it runs before user, which then drops privileges to nobody. But down will clearly be an issue, since the nobody user can’t delete a netns. So the first thing to do in the client config is to comment out or remove the lines
user nobody
group nobody
and add the new directives. Here’s an example of a slightly modified script from the previous article:
openvpn_client.conf
clientdev tunproto tcpremote 127.0.0.1 1194resolv-retry infinitenobind#user nobody#group nobodypersist-keypersist-tunca /etc/openvpn/client/ca.crtcert /etc/openvpn/client/openvpn-client.crtkey /etc/openvpn/client/openvpn-client.key### on client 1tls-auth /etc/openvpn/client/ta.key 1remote-cert-tls servercipher AES-256-GCMverb 3script-security 2ifconfig-noexecroute-noexecup "/etc/openvpn/client/ovpn_control.sh"route-up "/etc/openvpn/client/ovpn_control.sh"down "/etc/openvpn/client/ovpn_control.sh"where ovpn_control. is the script that will process the directives.
Now we need a resolv. for the netns. It’s stored at /, so let’s create the directory:
$ sudo mkdir -p /etc/netns/<netns name>
In our case the name is ovpn. When the script runs, it will write DNS servers to resolv.. If netns can’t find /, it will fall back to /. So make sure that file lists public DNS servers, because DNS queries go through the tunnel and your router’s DNS IP may not be reachable from the server side. 🙂
Example script below. You can replace DNS1 and DNS2 with your preferred ones.
ovpn_control.sh
#!/usr/bin/bashDNS1=208.67.222.222
DNS2=208.67.220.220
case $script_type in up) /usr/sbin/ip netns add ovpn
/usr/sbin/ip link set $dev netns ovpn
/usr/sbin/ip -n ovpn address add $ifconfig_local/30 dev $dev /usr/sbin/ip -n ovpn link set $dev up mtu $tun_mtu [ ! -d /etc/netns/ovpn ] && mkdir -p /etc/netns/ovpn
echo "nameserver $DNS1" > /etc/netns/ovpn/resolv.conf
echo "nameserver $DNS2" >> /etc/netns/ovpn/resolv.conf
;; route-up) /usr/sbin/ip -n ovpn route add default dev $dev ;; down) /usr/sbin/ip netns del ovpn
;;esacManaging the VPN client
In the previous article, the client was managed solely with . files for systemd. Now we’ll look at another option: managing it with a script. This will be useful for those running a distribution without systemd.
Writing the script
I’ll post my script below and briefly explain what it does.
- Takes one of the strings
start,stop,restart, orexecas the first command-line argument ($1). Executes the corresponding function based on the argument.
If
start: launchesopenvpnin the background using the configuration shown below, and writes logs to/.tmp/ ovpn_log. txt If
stop: terminates theopenvpnprocess with aSIGTERM(killor just-15 kill). This allowsopenvpnto handle the signal and passdownvia the$script_typeenvironment variable to another script (see above). The PID is read from the PID file. There’s also a stub to handle the logout case. Details below.restartrunsstopfirst, thenstart.execruns whatever follows inside the netns, under the user’s identity.
info
When the session ends, the system apparently kills processes with SIGKILL (kill ), so OpenVPN doesn’t get a chance to run the down script and remove the netns. If you log out with OpenVPN running, then after logging back in the netns may still be up, but the tun interface will be gone. In that case you’ll need to run
$ sudo ./openvpn_netns.sh restart
Next up is the script itself. Adjust x_user, conf_file, log_file, pid_file, and netns_name to your liking.
openvpn_netns.sh
#!/usr/bin/bashx_user=eakj
conf_file=/etc/openvpn/client/openvpn_client.conf
log_file=/tmp/ovpn_log.txt
pid_file=/var/run/ovpn.pid
netns_name=ovpn
die() { printf "%s\n" "$@" exit 1
}usage() { printf "%s\n\t%s\n" "USAGE:" "$0 start|stop|restart|exec" exit 1
}start_() { [ -f /var/run/netns/$netns_name ] && die "[E:] netns $netns_name is already up. If it shouldn't be, try restart" if [ -f "$conf_file" ]; then printf "%s\n" "[S:] starting with $conf_file" openvpn --writepid "$pid_file" --log "$log_file" --config "$conf_file" &>/dev/null &
else die "[E:] $conf_file not found" fi}stop_() { if [ -f "$pid_file" ]; then _PID=$(cat "$pid_file") ## Found the PID file; now check whether the process ## with that ID actually exists if ps -ef | grep "$_PID" | grep -v grep &>/dev/null; then printf "%s\n" "[S:] stopping openvpn process with PID $_PID" kill $_PID else printf "%s\n" "[E:] $pid_file file exists, but actual process id not found" fi [ -f "$log_file" ] && rm -f "$log_file" &>/dev/null
[ -f "$pid_file" ] && rm -f "$pid_file" &>/dev/null
fi ## The netns may remain if you logged out while OpenVPN was running. ## Check if the process exists, not via the PID file ## (it might be missing), but by parsing the config file name ## it should have been started with. ## If the process isn't found but the netns somehow still exists, ## most likely OpenVPN was killed with signal -9 if ! ps -ef | grep -iE "openvpn.+$(basename $conf_file)$" &>/dev/null && [ -f "/var/run/netns/$netns_name" ]; then ip netns del $netns_name die "[W:] probably openvpn was killed with SIGKILL, deleting $netns_name netns" fi}(( $UID != 0 )) && die "[E:] must run as root"(( $# < 1 )) && usage
case "$1" in start) start_
;; stop) stop_
;; restart) "$0" stop
sleep 5
"$0" start
;; exec) shift ip netns exec $netns_name sudo -u $x_user "$@" ;; *) usage
;;esacLet’s look at some usage examples. We’ll connect to the server, check for the presence of a netns, and run a few commands. If the netns isn’t being created, check /—the clue is most likely there.
$ sudo ./openvpn_netns.sh start
$ ip netns
$ sudo ./openvpn_netns.sh exec ping -c 4 8.8.8.8
$ sudo ./openvpn_netns.sh exec curl ifconfig.co
$ sudo ./openvpn_netns.sh exec firefox &>/dev/null &

By the way, be careful when running commands as root and sending them to the background (& at the end). In this case, the command worked fine because I had recently entered my password. If more time had passed and I tried to launch firefox that way, the sudo password prompt would have been backgrounded. You’d then have to use bg, fg, and Ctrl+Z. To make life easier, add the following line to /, where eakj is the username, of course.
eakj ALL=NOPASSWD:/путь/к/openvpn_netns.sh
This will let you run the script with sudo without being prompted for a password.
Try stopping openvpn and removing the netns.
$ sudo ./openvpn_netns.sh stop
$ ip netns

.service Unit Files for systemd
I used the .service files from Austin Adams’s blog as a template. Let’s start modifying them.
First, create the directory / and place netns. in it. On Ubuntu, this will most likely just be /.
$ sudo mkdir /etc/systemd/system/openvpn-client@.service.d/
$ sudo tee /etc/systemd/system/openvpn-client@.service.d/netns.conf << EOF
[Unit]
Requires=netns@%i.service
After=netns@%i.service
[Service]
# Needed to call setns() as ip netns does
CapabilityBoundingSet=CAP_SYS_ADMIN
EOF
Now create the netns@. file in /. Thanks to the config we added earlier, this service will start after the initial run of systemctl . It will create a netns with the same name as the OpenVPN config.
$ sudo tee /etc/systemd/system/netns@.service << EOF
[Unit]
Description=network namespace %I
[Service]
Type=oneshot
ExecStart=/usr/sbin/ip netns add %I
ExecStop=/usr/sbin/ip netns del %I
RemainAfterExit=yes
EOF
You can check the status with the command
$ systemctl status netns@openvpn_client
And to verify that it created the netns, use the command $ . There’s also one caveat here: after stopping the openvpn service with the command
$ sudo systemctl stop openvpn-client@openvpn_client.conf
The netns@openvpn_client service won’t stop, which means the netns will keep running.
You’ll also need to tweak ovpn_control. a bit.
- Add support for recognizing configuration file names.
- Remove the line that creates the netns, since this is now handled by netns@.service.
- Since the netns name now depends on the OpenVPN client config name, create new directories for resolv.conf.
- Remove the down hook/script, since systemd doesn’t call it anyway.
#!/usr/bin/bashDNS1=208.67.222.222
DNS2=208.67.220.220
basename="$(basename "$config")"ns="${basename%.conf}"case $script_type in up) /usr/sbin/ip link set $dev netns $ns /usr/sbin/ip -n $ns address add $ifconfig_local/30 dev $dev /usr/sbin/ip -n $ns link set $dev up mtu $tun_mtu [ ! -f /etc/netns/$ns/resolv.conf ] && mkdir -p /etc/netns/$ns echo "nameserver $DNS1" > /etc/netns/$ns/resolv.conf
echo "nameserver $DNS2" >> /etc/netns/$ns/resolv.conf
;; route-up) /usr/sbin/ip -n $ns route add default dev $dev ;;esacKeep in mind this script is no longer compatible with openvpn_netns., so I recommend saving the ovpn_control. for systemd under a different name and updating its reference in openvpn_client.. Now let’s see how .service files work in practice.
Start the client configuration with systemd.
$ sudo systemctl start openvpn-client@openvpn_client
$ sudo systemctl status openvpn-client@openvpn_client
$ ip netns

Check the status of the netns@openvpn_client service:`
$ systemctl status netns@openvpn_client.service
But even from the previous screenshot, we can tell everything is fine, because the service is visible in the $ list.

I don’t see any point in writing a separate command runner script for systemd, since it’s just a one-liner. 🙂
$ sudo ip netns exec openvpn_client ping -c 2 8.8.8.8
$ sudo ip netns exec openvpn_client curl ifconfig.co
$ sudo ip netns exec openvpn_client sudo -u eakj firefox &>/dev/null &

Stop the client:
$ sudo systemctl stop openvpn-client@openvpn_client.service
Then verify that the netns is still present (ip ). To delete it, use the command
$ sudo systemctl stop netns@openvpn_client.service

Conclusion
Linux network namespaces are a powerful tool that make it easy to isolate a system’s network resources and can be useful not only for large projects but also at home. The VPN trick described in the article is just one of many possible applications.