Tu veux un VPN pour ton boulot (accès au LAN d'entreprise via tunnel) et simultanément Netflix qui voit ton IP française pour te servir le catalogue FR. Ou tu veux que Steam télécharge en direct (1 Gbps ISP) et que tout le reste passe par le VPN. Solution : split-tunnel par tables de routage Linux, plus puissant que le split-tunnel d'AllowedIPs WireGuard.
Ce guide implémente le pattern fwmark + ip rule + ip route qui permet de router certaines applications via le VPN et d'autres directement, sans toucher au tunnel WireGuard lui-même.
Le concept en 4 étapes
- Marquer le trafic d'une application avec
iptables -j MARK(basé sur cgroup, user, ou destination). - Créer une table de routage alternative pour le trafic marqué (ex: table 200).
- Ajouter une règle ip-rule qui dit "trafic avec mark X → utilise table 200".
- Peupler la table 200 avec une route par défaut différente (soit
wg0, soiteth0direct).
Résultat : le kernel inspecte chaque paquet sortant et décide table 200 (VPN) ou table principale (ISP direct).
Setup de base
Pré-requis : un tunnel WireGuard configuré et actif. Voir le guide setup Contabo WireGuard si tu pars de zéro.
Vérifie d'abord que iproute2 est installé (devrait l'être sur toute distro récente) :
ip -V
# iproute2-6.x.x
Cas A — Exclure Steam du VPN (téléchargements direct ISP)
Steam s'identifie par un cgroup quand on le lance depuis systemd. On va router son trafic via l'interface ISP eth0 au lieu de wg0.
Étape 1 — Créer un cgroup pour Steam
sudo mkdir -p /sys/fs/cgroup/net_cls/steam
sudo bash -c 'echo 0x42 > /sys/fs/cgroup/net_cls/steam/net_cls.classid'
Étape 2 — Marquer les paquets sortants depuis ce cgroup
sudo iptables -t mangle -A OUTPUT -m cgroup --cgroup 0x42 -j MARK --set-mark 200
Étape 3 — Créer la table de routage 200 avec route via ISP
# Ajoute la table dans /etc/iproute2/rt_tables
echo "200 isp_direct" | sudo tee -a /etc/iproute2/rt_tables
# Récupère la gateway ISP
ISP_GW=$(ip route | awk '/default/ && /eth0/ {print $3}')
sudo ip route add default via $ISP_GW dev eth0 table isp_direct
# Règle ip-rule : trafic marqué 200 → table isp_direct
sudo ip rule add fwmark 200 table isp_direct
Étape 4 — Lancer Steam dans ce cgroup
# Wrapper Steam
cgexec -g net_cls:steam /usr/games/steam
Ou via systemd .slice pour les setups Flatpak / Steam Snap.
Steam télécharge maintenant en direct ISP (jusqu'à 1 Gbps fibre) pendant que tout le reste de ta machine continue par le tunnel WireGuard.
Cas B — Router Netflix via ISP pour catalogue FR
Netflix tu veux qu'il voie ton IP française. Approche par destination IP plutôt que par cgroup, car Netflix utilise plusieurs CDN.
Étape 1 — Récupérer les ranges Netflix
Netflix publie ses prefixes BGP : AS2906. Tu peux les sortir avec whois -h whois.radb.net -- '-i origin AS2906' ou utiliser une liste maintenue :
curl -s https://api.bgpview.io/asn/2906/prefixes | jq -r '.data.ipv4_prefixes[].prefix' > /tmp/netflix-ranges.txt
Étape 2 — Marquer trafic vers ces ranges
while read range; do
sudo iptables -t mangle -A OUTPUT -d "$range" -j MARK --set-mark 200
done < /tmp/netflix-ranges.txt
Étape 3 — Utilise la même table isp_direct
(Étapes 3 du cas A déjà en place.)
Au prochain netflix.com, le DNS résout, le paquet TCP part avec mark 200, le kernel l'envoie via eth0 direct, Netflix voit ton IP française et te sert le bon catalogue.
Cas C — Inverse : forcer seulement Slack/Zoom via VPN
Tu travailles depuis l'étranger, tu veux que ton trafic boulot (Slack, Zoom, GitHub) passe par le VPN d'entreprise, et tout le reste en direct.
Setup symétrique :
- Mark par défaut = direct
- Mark applications boulot = via VPN
# Table secondary pour VPN
echo "201 work_vpn" | sudo tee -a /etc/iproute2/rt_tables
sudo ip route add default dev wg0 table work_vpn
sudo ip rule add fwmark 201 table work_vpn
# Marker pour Slack (par destination)
for d in $(dig +short slack.com slack-edge.com app.slack.com); do
sudo iptables -t mangle -A OUTPUT -d "$d" -j MARK --set-mark 201
done
# Zoom (ranges publiés par Zoom dans leur doc admin)
sudo iptables -t mangle -A OUTPUT -d 3.7.35.0/25 -j MARK --set-mark 201
sudo iptables -t mangle -A OUTPUT -d 3.21.137.128/25 -j MARK --set-mark 201
# ... compléter avec la liste officielle Zoom
Important : la route par défaut de la table principale doit pointer vers eth0 (ISP direct), pas wg0. Vérifie avec ip route show table main.
Pratique : script split-tunnel.sh complet
Pour ne pas re-rentrer les commandes à chaque boot, voici un script idempotent qu'on lance via systemd :
#!/usr/bin/env bash
# /usr/local/bin/split-tunnel.sh
set -euo pipefail
# Configuration
ISP_IF="eth0"
VPN_IF="wg0"
# Récupère gateway ISP
ISP_GW=$(ip route | awk -v iface="$ISP_IF" '/default/ && $0 ~ iface {print $3; exit}')
# Crée tables si absentes
grep -q '^200 isp_direct' /etc/iproute2/rt_tables || echo "200 isp_direct" >> /etc/iproute2/rt_tables
grep -q '^201 work_vpn' /etc/iproute2/rt_tables || echo "201 work_vpn" >> /etc/iproute2/rt_tables
# Routes par défaut dans chaque table
ip route show table isp_direct | grep -q default || ip route add default via "$ISP_GW" dev "$ISP_IF" table isp_direct
ip route show table work_vpn | grep -q default || ip route add default dev "$VPN_IF" table work_vpn
# Règles ip-rule
ip rule show | grep -q "fwmark 0xc8 lookup isp_direct" || ip rule add fwmark 200 table isp_direct
ip rule show | grep -q "fwmark 0xc9 lookup work_vpn" || ip rule add fwmark 201 table work_vpn
# Cgroup Steam (mark 200 → ISP direct)
mkdir -p /sys/fs/cgroup/net_cls/steam
echo 0x42 > /sys/fs/cgroup/net_cls/steam/net_cls.classid
iptables -t mangle -C OUTPUT -m cgroup --cgroup 0x42 -j MARK --set-mark 200 2>/dev/null \
|| iptables -t mangle -A OUTPUT -m cgroup --cgroup 0x42 -j MARK --set-mark 200
echo "Split-tunnel rules applied."
Service systemd associé :
# /etc/systemd/system/split-tunnel.service
[Unit]
Description=Split-tunnel routing rules
After=wg-quick@wg0.service
Wants=wg-quick@wg0.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/split-tunnel.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
sudo systemctl enable --now split-tunnel.service
Cas D — Split par utilisateur Linux
Tu veux que tout le trafic de l'utilisateur kid (compte de ton enfant) passe par le VPN avec filtrage parental, et que ton compte principal eric reste direct.
# Mark par UID
sudo iptables -t mangle -A OUTPUT -m owner --uid-owner kid -j MARK --set-mark 201
# Route vers VPN pour mark 201 (déjà configuré ci-dessus)
Tout curl, firefox, apt lancé sous l'UID kid passe par wg0. Pratique aussi pour exécuter un browser de test (Chromium dans un autre user) qui voit toujours une IP VPN différente.
Diagnostic et debug
Vérifier les règles actives
ip rule show
# Doit montrer ta règle fwmark 200/201 avec le bon priority
ip route show table isp_direct
# Doit montrer default via X dev eth0
ip route show table work_vpn
# Doit montrer default dev wg0
Tracer le routing d'un paquet
# Depuis le shell de l'app à débugger (ou wrap dans cgexec)
ip route get 1.1.1.1 mark 200
# Doit montrer "via 192.168.1.1 dev eth0 table isp_direct"
ip route get 1.1.1.1 mark 201
# Doit montrer "dev wg0 table work_vpn"
Voir le mark dans tcpdump
sudo tcpdump -i any -nn -e 'iptables' 2>/dev/null
# Ou plus simple :
sudo nft monitor trace
Le mark n'est pas appliqué ?
Vérifie l'ordre des chains iptables. La table mangle chain OUTPUT est appelée avant le routing. Si tu vois ta règle au bon endroit avec iptables -t mangle -L OUTPUT -v -n mais que le mark ne s'applique pas, c'est probablement un problème de cgroup non hérité (lance via cgexec explicite).
Limites et gotchas
IPv6
Toutes les commandes ci-dessus sont IPv4 only. Pour IPv6, duplique avec :
sudo ip6tables -t mangle -A OUTPUT ...
sudo ip -6 rule add fwmark 200 table isp_direct
sudo ip -6 route add default via FE80::1 dev eth0 table isp_direct
Si tu utilises notre guide kill switch, n'oublie pas que les règles OUTPUT DROP du killswitch peuvent bloquer le trafic split-tunnel direct. Solution : exempter les marks 200/201 du DROP général.
DNS leak en split-tunnel
Si tu split par IP de destination, le DNS reste résolu par ton DNS principal. Si ce DNS est dans le tunnel, Netflix peut résoudre vers une IP UK alors que tu voulais FR. Solution : configurer un DNS direct pour les domaines split (dnsmasq avec server-specific resolvers).
Le kernel cache les routes
Après modification d'une ip rule, exécute :
sudo ip route flush cache
Sinon les connexions déjà ouvertes peuvent garder leur ancien routing.
Compatibilité Docker / containers
Docker manipule iptables de manière agressive. Si tu cohabites split-tunnel + Docker, attends-toi à des règles écrasées au redémarrage Docker. Solution : lance split-tunnel.service avec After=docker.service ou utilise nftables plutôt que iptables.
Verdict
Le split-tunnel par fwmark + ip rule est la solution la plus flexible sur Linux. Tu peux router par destination, par utilisateur, par cgroup, par processus. Le coût : ~10 lignes de config et un service systemd. C'est largement supérieur au split-tunnel d'AllowedIPs WireGuard qui ne sait filtrer que par destination IP.
Setup recommandé pour la maison :
- VPN par défaut sur tout
- Steam, Netflix, Time Machine → marqués
200→ direct ISP - Kill switch netfilter avec exemption mark 200
Setup recommandé pour le boulot à distance :
- Direct ISP par défaut
- Slack, Zoom, GitHub Enterprise, jumphost → marqués
201→ via VPN entreprise
Et pour la base : le guide WireGuard sur Contabo, les templates prêts à coller, et le monitoring Prometheus + Grafana pour suivre ce qui sort par quelle interface en temps réel.
★ Datacenter Nuremberg GDPR · ✓ IPv4 dédiée incluse · 200+ Mbps garantis
Voir l'offre Contabo30 jours satisfait ou remboursé→