Split Tunneling avec OpenVPN
Objectif
Une solution souple pour faire du split tunneling sous Linux consiste à lancer un client OpenVPN dans un network namespace dédié, de telle sorte que :
- la totalité du trafic des commandes lancées depuis ce namespace passe automatiquement et obligatoirement par le VPN
- sans que le reste de la configuration et du trafic réseau ne soit modifié en aucune manière
Ce qui suit est basé sur cette note de Sebastian Thorarensen (merci !) et testé sur une xubuntu 18.04. Le VPN est fourni par NordVPN. Pour l'essentiel, les commandes devraient fonctionner sans grosse modification avec l'ensemble des solutions OpenVPN, et des distributions Linux basées sur (cette horreur de) systemd.
Installer OpenVPN
Par défaut, OpenVPN n'est pas installé. Le plus simple est de le récupérer pré-configuré avec apt
sudo apt -y install openvpn
Par la suite on se contente de modifier (le moins possible) la configuration par défaut.
Récupérérer les configurations NordVPN
Voir le tuto dédié à Linux sur le site de NordVPN.
sudo wget https://downloads.nordcdn.com/configs/archives/servers/ovpn.zip
Choisir une des configurations proposées, par exemple fr172.nordvpn.com.tcp.ovpn
et la copier vers /etc/openvpn/client/nordvpn.conf
. Remplacer la directive d'authentification
-auth-user-pass
par celle-ci
auth-user-pass login.txt
et créer au même niveau le fichier login.txt
: deux lignes, utilisateur et mot de passe (mode 600, root:root)
Vérifier que le VPN fonctionne
Le VPN doit fonctionner tout seul, indépendamment du namespace. Le client récupère depuis le serveur des routes par défaut qui captent tout le trafic réseau. On le lance, on vérifie que tout fonctionne (routes mises à jour et IP vue depuis Internet différente), et on l'arrête.
État initial
> curl ipinfo.io { "ip": "193.52.24.31", "hostname": "31.0-127.24.52.193.in-addr.arpa", "city": "Issy-les-moulineaux", "region": "Ile-de-France", "country": "FR", "loc": "48.8210,2.2772", "postal": "92130", "org": "AS2200 Renater" }
> ip route default via 10.0.2.2 dev enp0s3 proto dhcp metric 100 10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.15 metric 100 169.254.0.0/16 dev enp0s8 scope link metric 1000 192.168.99.0/24 dev enp0s8 proto kernel scope link src 192.168.99.102 metric 101
Lancer le VPN
> systemctl start openvpn-client@nordvpn.service ==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units === Authentification requise pour démarrer « openvpn-client@nordvpn.service ». Authenticating as: Julien,,, (julien) ==== AUTHENTICATION COMPLETE ===
> systemctl status openvpn-client@nordvpn.service ● openvpn-client@nordvpn.service - OpenVPN tunnel for nordvpn Loaded: loaded (/lib/systemd/system/openvpn-client@.service; disabled; vendor preset: enabled) Active: active (running) since Tue 2018-07-24 15:32:35 CEST; 2s ago Docs: man:openvpn(8) https://community.openvpn.net/openvpn/wiki/Openvpn24ManPage https://community.openvpn.net/openvpn/wiki/HOWTO Main PID: 5431 (openvpn) Status: "Pre-connection initialization successful" Tasks: 1 (limit: 4663) CGroup: /system.slice/system-openvpn\x2dclient.slice/openvpn-client@nordvpn.service └─5431 /usr/sbin/openvpn --suppress-timestamps --nobind --config nordvpn.conf
État après lancement du VPN
curl ipinfo.io { "ip": "185.93.2.42", "city": "Paris", "region": "Île-de-France", "country": "FR", "loc": "48.9333,2.3667", "postal": "93200", "org": "AS60068 Datacamp Limited" }
> ip route 0.0.0.0/1 via 10.7.7.1 dev tun0 default via 10.0.2.2 dev enp0s3 proto dhcp metric 100 10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.15 metric 100 10.7.7.0/24 dev tun0 proto kernel scope link src 10.7.7.246 128.0.0.0/1 via 10.7.7.1 dev tun0 169.254.0.0/16 dev enp0s8 scope link metric 1000 185.93.2.42 via 10.0.2.2 dev enp0s3 192.168.99.0/24 dev enp0s8 proto kernel scope link src 192.168.99.102 metric 101
L'IP vue depuis Internet a changé, elle est passée de 193.52.24.31
à 185.93.2.42
, l'interface du VPN, tun0
, est là, et de nouvelles routes passent par elle. Tout est bien !
Arrêt du VPN
> systemctl stop openvpn-client@nordvpn.service ==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units === Authentification requise pour arrêter « openvpn-client@nordvpn.service ». Authenticating as: Julien,,, (julien) ==== AUTHENTICATION COMPLETE ===
Créer un fichier de service dédié
La configuration par défaut est globale pour la machine. Or on souhaite lancer OpenVPN dans un namespace. En se basant sur cette configuration, on va créer le service dédié ns-openvpn-client@.service
.
sudo cp /lib/systemd/system/openvpn-client@.service /lib/systemd/system/ns-openvpn-client@.service
Dans ce nouveau fichier, il faut modifier la ligne ExecStart
qui lance le client OpenVPN. Sur ma machine, les options suivantes sont présentes
ExecStart=/usr/sbin/openvpn --suppress-timestamps --nobind --config %i.conf
On ajoute les appels au script helper.sh
qui monte le tunnel dans le namespace. La configuration des routes et des interfaces étant faite par ce script, on ajoute également --ifconfig-noexec
et --route-noexec
, ainsi que l'option --script-security 2
sans laquelle le client OpenVPN se montre réticent à lancer un script externe.
ExecStart=/usr/sbin/openvpn --suppress-timestamps --nobind \ --ifconfig-noexec --route-noexec --script-security 2 \ --up helper.sh --route-up helper.sh --down helper.sh \ --config %i.conf
Il faut également modifier la ligne CapabilityBoundingSet
afin d'éviter l'erreur mount --make-shared /var/run/netns failed: Operation not permitted
lors de la création du namespace. Deux possibilités : commenter la ligne ou ajouter la capacité CAP_SYS_ADMIN
.
CapabilityBoundingSet=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SYS_CHROOT CAP_DAC_OVERRIDE CAP_SYS_ADMIN
Copier le fichier helper.sh
dans /etc/openvpn/client
, et demander à systemd de recharger ses services pour prendre en compte ns-openvpn-client@.service
> systemctl daemon-reload ==== AUTHENTICATING FOR org.freedesktop.systemd1.reload-daemon === Authentification requise pour recharger l'état de systemd Authenticating as: Julien,,, (julien) ==== AUTHENTICATION COMPLETE ===
À propos du script helper.sh
Le script helper.sh permet de gérer le namespace si on l'appelle à la main, et de configurer le tunnel lorsqu'il est appelé par le client OpenVPN. Il faut le copier lui aussi dans le répertoire /etc/openvpn/client
. Il crée à cet endroit les fichiers de logs up.log
, route-up.log
et down.log
. Comme ils sont écrasés à chaque appel, ils ne grossissent pas. Ils permettent de voir comment l'interface du tunnel est configurée. Lancé en mode manuel, le script accepte les commandes create-ns
, show-ns
, remove-ns
et clean-log
.
Configurer le namespace
Pour une raison qui reste à déterminer (discutée ci-après), si le namespace est créé directement depuis le script helper.sh
lors de son premier appel par le client OpenVPN (comportement par défaut), le nouveau namespace n'est pas fonctionnel. Pour contourner ce problème, on crée préalablement le namespace à la main, une fois pour toute, à l'aide du script helper.sh
. Par la suite, si le script détecte que le namespace existe lors du up
de l'interface, il ne le détruira pas lors du down
.
> sudo ./helper.sh create-ns Create namespace cat /etc/netns/NordVPN/resolv.conf nameserver 103.86.96.100 nameserver 103.86.99.100 ip netns add NordVPN ip netns exec NordVPN ip link set dev lo up
Les DNS utilisés par le script proviennent du support NordVPN. On vérifie que le namespace a été créé correctement, et que l'interface de loopback est up
.
> sudo ./helper.sh show-ns NordVPN 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever
Par la suite si on veut supprimer le namespace, on utilise
> sudo ./helper.sh remove-ns ip netns delete NordVPN rm -Rf /etc/netns/NordVPN
Quelques alias pratiques
On utilise systemd et les commandes start
/ status
/ stop
pour lancer et arrêter le service, voir enable
/ status
/ disable
si on préfère qu'il soit lancé automatiquement au démarrage du système. Les alias suivants aident un peu si on choisit de ne pas lancer le service au démarrage.
alias vpn-start='sudo systemctl start ns-openvpn-client@nordvpn.service ' alias vpn-status='sudo systemctl status ns-openvpn-client@nordvpn.service ' alias vpn-stop='sudo systemctl stop ns-openvpn-client@nordvpn.service '
Les alias vpn-cmd
et vpn-bash
permettent respectivement de lancer une commande et un Bash dans le namespace NordVPN
alias vpn-cmd='sudo ip netns exec NordVPN ' alias vpn-bash='sudo ip netns exec NordVPN sudo -u julien bash '
Lancer le vpn dans le namespace
On lance le vpn et on vérifie que les commandes exécutées dans le namespace passent par lui
> vpn-start > vpn-cmd curl ipinfo.io { "ip": "185.93.2.42", "hostname": "unn-185-93-2-42.datapacket.com", "city": "Paris", "region": "Île-de-France", "country": "FR", "loc": "48.9333,2.3667", "postal": "93200", "org": "AS60068 Datacamp Limited" }
Et si l'idée vous prend de vouloir ajouter à votre prompt le namespace courant, la commande dont vous avez besoin est ip netns identify
> vpn-bash > ip netns identify NordVPN
Bug Invalid argument
Idéalement, le namespace devrait être créé depuis le script : automatique et transparent. Hélas, comme dit précédemment, le namespace qu'on récupère ainsi n'est pas fonctionnel. Cette commande illustre le problème
> sudo ip netns exec ns1 echo toto setting the network namespace "ns1" failed: Invalid argument
En relançant la commande avec sudo strace ip netns exec [...]
on constate cette erreur, à creuser…
openat(AT_FDCWD, "/var/run/netns/ns1", O_RDONLY|O_CLOEXEC) = 5 setns(5, CLONE_NEWNET) = -1 EINVAL (Invalid argument) write(2, "setting the network namespace \"n"..., 61setting the network namespace "ns1" failed: Invalid argument) = 61 close(5) = 0
La trace complète se trouve dans le fichier invalid.trace.