I think this problem may be addressed in a number of ways, depending on the overall pre-existing setup, possibility to alter the provider’s own configuration, actual vpn types to support, and willing to undergo more or less complex setups.
I would also say that for solutions specific to the NordVPN service you might also try and ask them for support, because the nordvpn
command may not necessarily use OpenVPN, it might rather use IPSec, or even PPTP/L2TP.
However, for a generic OpenVPN solution, which you say is fully compatible with your NordVPN service, I think the best approach might be using policy routing, possibly with a few variants.
But first, seen your interest in namespaces, I'd like to mention my opinion about a possible approach using those:
Certainly by using namespaces one would obtain true isolation of networking between users, with the main and notable advantage of being a generic solution for any type of vpn, not just OpenVPN.
But the required tools to set things up are not commonly installed by many Linux distribution, as the problem in question doesn't seem one that can be addressed simply by using the unshare
or ip netns exec
commands, because those allow visibility of the new namespace only to those processes run from then on and under that session of commands, not to the user's entire pre-existing session.
Also, even by installing the required tools to set things up the way they should, the system would require careful setup in order to separate each user into its own network namespace while making use of the same physical net device as well as sharing the unix-domain sockets and services running for the localhost. Setting up bridges (or macvlans), subnets, assigning addresses, maybe NATting, and other complexities. It is probably doable, but likely not simple to make it straight and I think much more complex than the original problem.
So, back to a routing policy approach, as far as I can conceive, the base solution, probably the simplest workable one, likely sufficient for most common use-cases, or as a starting point before venturing into more sophisticated variants, would use separate routing tables per each user’s tunnel to be looked up via routing policy based on user’s UID.
It would have the following main characteristics:
- require a (fairly) standard version of OpenVPN (i.e. not heavily customized by provider)
- require an
iptables
companion rule to mark the relevant traffic based on user’s UID number
- careful integration may be required if used along pre-existing firewall or routing rules
- cannot however identify processes run by the user via
sudo
(because these run with UID 0) nor setuid commands or anyway applications run as root. These are mostly not the case for the typical desktop scenario, but may well happen and would not be handled as intended (i.e. that part of traffic would not go into the vpn)
Everything can be handled by two helper scripts. One to be used by OpenVPN, the other to be used to run openvpn
itself.
Standard OpenVPN uses the ip
command to set things up on the tun/tap device and to install routes pushed by the VPN provider. The problem is that by default the ip
command operates on the main routing tables, which are used by the whole system thus including all its users.
However, OpenVPN allows to specify a different command to be used for the settings it needs to apply, and so we can provide a wrapper over ip
that simply operates on different routing tables.
Such (tentatively named myip.sh
) wrapper script might be like:
#!/bin/bash -
my_session="$(ps -p $$ -o sid --no-header)" || exit 1
real_uid="$(ps -p $$ -o ouid --no-header || ps -p $my_session -o ruid --no-header || echo 0)"
real_ip_cmd=/sbin/ip
if [ $real_uid -ne 0 ] && [[ "$1" = ro* ]] ; then
! [ $2 ] && exec $real_ip_cmd "$1" list table $real_uid
[[ "$2" =~ ^([adcfls]|rep).*$ ]] && exec $real_ip_cmd "$1" "$2" table $real_uid "${@:3}"
fi
exec $real_ip_cmd "$@"
The first two lines serve to try and retrieve the real UID that’s running OpenVPN. We use that UID as table id. This way each user would have its own routing table when he/she starts a vpn.
The if-then-fi
block catches invocations for route
settings (mimicking ip
command’s ability to accept shortened yet unambiguous keywords), and simply prepends all such commands passed by openvpn with a table
option carrying an id equal to the real UID of the user. All other ip
commands (i.e. non route
) pass through untouched.
The other script wraps the openvpn
command, while setting things up to make traffic generated by the user be routed according to its distinct table. The gist of this wrapper script could be included into the provider’s own configuration file, allowing for better seamless integration. This would help in case one would like to establish the vpn non-interactively (e.g. at boot time). However, by using this wrapper script instead, one does not require fiddling with the configuration file, and this is why I've chosen to present this version.
So, such single wrapper script might be like this:
#!/bin/bash
my_session="$(ps -p $$ -o sid --no-header)" || exit 1
real_uid="$(ps -p $$ -o ouid --no-header || ps -p $my_session -o ruid --no-header || echo 0)"
[ $real_uid -eq 0 ] && { echo "will not run for UID 0" >&2 ; exit 1; }
remove_tagging() {
iptables -t mangle -D OUTPUT -m owner --uid $real_uid -j MARK --set-mark $real_uid
ip rule del fwmark $real_uid
ip route flush table $real_uid
} 2>/dev/null
trap 'remove_tagging' EXIT
source <(ip route | sed "s/^/ip route add table ${real_uid} /")
(
ip -o monitor | grep -qm 1 '^[0-9]\+: tun[0-9]\+[[:blank:]]\+inet '
ip rule add fwmark $real_uid lookup $real_uid
iptables -t mangle -A OUTPUT -m owner --uid $real_uid -j MARK --set-mark $real_uid
) &
openvpn --iproute myip.sh --config tunnel-config.ovpn
The first three lines again serve to try and retrieve the real UID of the user, and will not proceed if that is not obtainable.
Then we have a function to be run on EXIT from the script. The function removes the settings put in place by the following sub-shell.
But first we copy the whole current main routing table into the distinct table for the user.
Then we run the sub-shell that waits on a tunX
device to be set up by OpenVPN and then adds the routing rules and the iptables
companion rule that marks the traffic generated by the user from then on. We run this in the background because we need to run OpenVPN in the foreground in case it needs to ask for username and password.
The source
command, the sub-shell (without the ip -o monitor
pipeline) and the remove_tagging
function, are the parts that could go in the vpn configuration file as (respectively) up
, route-up
and route-pre-down
options. Doing so would allow to get rid of this wrapper script entirely.
Finally we run the actual openvpn
command, telling it to use our own myip.sh
script as command to set up networking.
A possible slight variant might be by using more recent versions of iproute2 package (which provides the ip
command), those having the uidrange
option to be used instead of fwmark
in routing rules.
Such variant would thus eliminate the iptables
commands, and replace the ip rule fwmark ..
command with a ip rule uidrange ${real_uid}-${real_uid} lookup $real_uid
command (the same goes for its respective ip rule del ..
command).
Another variant for some support of commands run via sudo
would be to make policy routing based on user’s group (GID) instead of UID. This would have additional basic requirements:
- a unique GID to be existent for the user, as commonly is on many Linux distributions (including Ubuntu)
sudo
run with extra options (-g <user> -u root
or equivalent configuration in sudoers
) to retain original GID number
However, it would still not be able to identify UID 0 processes run by the user outside of such sudo
.
To really address the “UID 0 processes” problem, one might use cgroups, but naturally this has yet additional requirements:
iptables
companion rule to mark the relevant traffic with iptables’s own specific "cgroup" module, which was not yet available on e.g. Ubuntu 14.04
- may conflict with pre-existing cgroup setups already in place
Using cgroups, setting the net-class-id might be handled pretty well via a simple script to be run by pam_exec.so
#!/bin/bash -e
uid=$(id -ru "${PAM_USER}")
mkdir -p /sys/fs/cgroup/net_cls/user/${uid} && echo $uid > /sys/fs/cgroup/net_cls/user/${uid}/net_cls.classid
echo $$ > /sys/fs/cgroup/net_cls/user/${uid}/cgroup.procs
and then adding the following line to the desired config files under /etc/pam.d
:
session optional pam_exec.so /root/set-net-cls-id.sh
The above line might be possibly put in the traditional /etc/pam.d/common-session
file to cover all sessions originated via any interactive service like ssh
, console logins, desktop environments, etc.
With this setup, the iptables
command to be used in the wrapper script would be:
iptables -t mangle -A OUTPUT -m cgroup --cgroup $real_uid -j MARK --set-mark $real_uid
and its respective deletion command with -D
in place of -A
.
A possible variant of the cgroup solution might be its integration into systemd
, so that one could use systemd
's ability to handle cgroups directly without using pam_exec
.
Note however that this cgroup setup cannot work well with Ubuntu 14.04 because that version used to set cgroups for each user's session and would thus overwrite the setup put in place by this solution. You would rather need to set the classid value of the cgroup created on-fly for each session, whose name can be easily obtained by parsing cat /proc/self/cgroup
output.
duplicate-cn
. If –duplicate-cn exist and active comment it and restart OpenVPN server.