Unix

Per-App VPN on Linux: Tunnel Individual Application Traffic with Network Namespaces

A VPN is a great tool that improves your anonymity and security. But staying on a VPN all the time can be inconvenient: maybe you want to access your online banking from your real IP while a couple of new episodes of your favorite show are downloading. On Linux, you can solve this using network namespaces and tunnel the traffic of specific applications. I’ll show you how to do it.

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 netns list, or just ip netns, 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
Managing netns and veth with the ip command
Managing netns and veth with the ip command

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 , which means we can replace sudoipnetnsexecns1iplinkwith sudo ip netns exec ns_1 ip link with sudo ip -n ns_1 link.

Verify that the interfaces were moved to the correct network namespaces
Verify that the interfaces were moved to the correct network namespaces

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
Connecting our veth pair
Connecting our veth pair

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

Indeed, the routing tables differ
Indeed, the routing tables differ

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
Connection established successfully
Connection established successfully

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

The rules list is empty
The rules list is empty

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
As you can see, enp0s3 is no longer in the init netns
As you can see, enp0s3 is no longer in the init netns

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 &
Wireshark running in the ns_2 network namespace
Wireshark running in the ns_2 network namespace

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

$ sudo ip netns del ns_2
enp0s3 won’t be released until the Wireshark process is terminated, even after ns_2 has been deleted
enp0s3 won’t be released until the Wireshark process is terminated, even after ns_2 has been deleted

Let’s close Wireshark and see if that helps.

After the process exited, enp0s3 returned to the initial network namespace
After the process exited, enp0s3 returned to the initial network namespace

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 running ifconfig to assign an IP to the tun interface. Instead, the required parameters are provided via environment variables.
  • route-noexec — same idea as ifconfig-noexec, but for routes: it won’t add routes automatically; the necessary parameters are passed via environment variables.
  • route-up /full/path/to/script — runs the script with the environment variable $script_type set to route-up.
  • up /full/path/to/script — like route-up, but runs after the tun/tap interface is brought up, with $script_type=up. Executes before the user directive (i.e., before privileges are dropped).
  • down /full/path/to/script — like route-up, but runs after the tun/tap interface is torn down, with $script_type=down. Executes after the user directive (i.e., with reduced privileges).
  • script-security 2 — permits execution of custom scripts.

The full list of environment variables that OpenVPN passes can be found in man openvpn, 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
client
dev tun
proto tcp
remote 127.0.0.1 1194
resolv-retry infinite
nobind
#user nobody
#group nobody
persist-key
persist-tun
ca /etc/openvpn/client/ca.crt
cert /etc/openvpn/client/openvpn-client.crt
key /etc/openvpn/client/openvpn-client.key
### on client 1
tls-auth /etc/openvpn/client/ta.key 1
remote-cert-tls server
cipher AES-256-GCM
verb 3
script-security 2
ifconfig-noexec
route-noexec
up "/etc/openvpn/client/ovpn_control.sh"
route-up "/etc/openvpn/client/ovpn_control.sh"
down "/etc/openvpn/client/ovpn_control.sh"

where ovpn_control.sh is the script that will process the directives.

Now we need a resolv.conf for the netns. It’s stored at /etc/netns/<netns-name>/resolv.conf, 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.conf. If netns can’t find /etc/netns/ovpn/resolv.conf, it will fall back to /etc/resolv.conf. 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/bash
DNS1=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
;;
esac

Managing the VPN client

In the previous article, the client was managed solely with .service 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, or exec as the first command-line argument ($1).
  • Executes the corresponding function based on the argument.

  • If start: launches openvpn in the background using the configuration shown below, and writes logs to /tmp/ovpn_log.txt.

  • If stop: terminates the openvpn process with a SIGTERM (kill -15 or just kill). This allows openvpn to handle the signal and pass down via the $script_type environment 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.

  • restart runs stop first, then start.

  • exec runs whatever follows inside the netns, under the user’s identity.

info

When the session ends, the system apparently kills processes with SIGKILL (kill -9), 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/bash
x_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
;;
esac

Let’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 /tmp/ovpn_log.txt—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 &
Firefox launched successfully inside the netns
Firefox launched successfully inside the netns

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 /etc/sudoers, 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
OpenVPN and the netns were stopped successfully
OpenVPN and the netns were stopped successfully

.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 /etc/systemd/system/openvpn-client@.service.d/ and place netns.conf in it. On Ubuntu, this will most likely just be /etc/systemd/system/openvpn@.service.d/.

$ 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@.service file in /etc/systemd/system/. Thanks to the config we added earlier, this service will start after the initial run of systemctl start openvpn-client@openvpn-client.conf. 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 $ ip netns. 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.sh 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/bash
DNS1=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
;;
esac

Keep in mind this script is no longer compatible with openvpn_netns.sh, so I recommend saving the ovpn_control.sh for systemd under a different name and updating its reference in openvpn_client.conf. 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
The openvpn_client service started successfully
The openvpn_client service started successfully

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 $ ip netns list.

netns@openvpn_client started successfully
netns@openvpn_client started successfully

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 &
One-liners
One-liners

Stop the client:

$ sudo systemctl stop openvpn-client@openvpn_client.service

Then verify that the netns is still present (ip netns). To delete it, use the command

$ sudo systemctl stop netns@openvpn_client.service
The netns stays up after the OpenVPN client service is stopped
The netns stays up after the OpenVPN client service is stopped

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.

it? Share: