Je faisais jusqu’ici tourner un VPN classique, centralisé : tous mes appareils se connectaient à un serveur unique qui relayait leurs échanges. Ça marche, mais ça crée une dépendance gênante. Le jour où ce serveur — ou simplement sa liaison internet — tombe, plus rien ne communique, y compris deux appareils par ailleurs parfaitement joignables l’un depuis l’autre. Je trouve ça dommage : la panne d’un tiers ne devrait pas couper une liaison qui, techniquement, n’a pas besoin de lui.
C’est ce qui m’a amené au modèle peer-to-peer (mesh). Plutôt qu’un point de passage central, chaque appareil établit un tunnel chiffré directement avec les autres. Le résultat, c’est de la résilience : tant que deux pairs peuvent s’atteindre sur le réseau, ils communiquent, indépendamment de l’état du reste de l’infrastructure.
Ce billet retrace ma démarche : les quelques notions à poser (tunneling, NAT), le panorama des solutions existantes, et la mise en place concrète de celle que j’ai retenue.
Les bases #
Split tunnel vs full tunnel #
Une première question m’est venue en pensant à cette configuration p2p : par où passe le trafic qui n’est pas un échange entre mes appareils, comme une requête vers un site web quelconque ? C’est toute la différence entre deux modes de fonctionnement d’un VPN.
- Full tunnel : tout le trafic de l’appareil part dans le VPN, y compris la navigation web ordinaire. C’était le mode de ma configuration centralisée : le serveur voyait passer l’intégralité de mon trafic et servait de sortie internet. Utile pour chiffrer une connexion sur un réseau non fiable ou masquer son IP, mais ça fait transiter tout le trafic par un tiers — la latence et le débit en dépendent, et si ce tiers tombe, on perd carrément l’accès à internet.
- Split tunnel : seul le trafic à destination du réseau VPN emprunte les tunnels ; le reste sort directement par la connexion locale de l’appareil. Une requête vers un site web part par mon accès internet normal, et seuls les échanges entre mes appareils passent par le mesh.
Pour mon usage, c’est le split tunnel qui a du sens. Je veux relier mes appareils entre eux, pas router toute ma navigation à travers eux — ce serait absurde de faire remonter une requête web par le mesh pour la faire ressortir ailleurs. Et ça colle à l’objectif de résilience : chaque appareil garde son accès internet propre, le mesh ne sert qu’à ce pour quoi il est fait.
NAT & NAT traversal #
Le p2p pose un problème concret : pour que deux appareils communiquent directement, il faut que chacun sache comment joindre l’autre. Or la plupart de mes appareils (téléphone en 4G, laptop sur un WiFi quelconque, RPi derrière la box d’un client) n’ont pas d’IP publique dédiée ni de port ouvert. Ils sont tous cachés derrière un NAT.
Le rôle du NAT #
Le NAT (Network Address Translation) permet à plusieurs machines d’un réseau privé de partager une seule IP publique. Quand mon laptop (192.168.1.20) contacte un serveur, le routeur réécrit l’adresse source en son IP publique et retient la correspondance dans une table :
192.168.1.20:51820 -> IP_publique:42315 -> serveur:443La réponse revient sur IP_publique:42315, et le routeur sait la renvoyer vers
le bon appareil interne. Ça marche parfaitement pour du trafic sortant… mais
personne ne peut initier une connexion vers mon laptop : de l’extérieur, il
n’existe pas d’entrée dans la table tant que lui n’a pas parlé le premier.
C’est tout le problème du mesh : mes deux pairs sont chacun derrière un NAT, et aucun des deux ne peut « appeler » l’autre directement.
Le hole punching #
L’astuce s’appelle le hole punching, et elle repose sur un tiers de confiance joignable publiquement (un serveur de coordination, appelé « lighthouse » chez Nebula, « DERP/coordination server » ailleurs).
Le principe :
- Chaque pair ouvre une connexion sortante vers le coordinateur. Ce faisant, il crée une entrée dans la table NAT de son routeur (le fameux « trou »).
- Le coordinateur connaît donc l’IP publique + port de chaque pair, tels que vus depuis l’extérieur.
- Quand A veut parler à B, il demande au coordinateur les coordonnées de B.
- A et B se mettent alors à s’envoyer des paquets simultanément. Le premier paquet de A ouvre le trou dans le NAT de A ; le premier paquet de B ouvre le trou dans le NAT de B. Comme chacun a déjà « parlé en premier », les NAT laissent passer les paquets entrants de l’autre.
À partir de là, le coordinateur n’est plus dans la boucle : le trafic circule directement de A vers B, en p2p. C’est exactement la résilience que je cherche : même si le coordinateur tombe ensuite, les tunnels déjà établis continuent.
Les cas qui coincent #
Le hole punching fonctionne avec la majorité des NAT domestiques, mais pas tous. Le facteur déterminant est la façon dont le routeur attribue le port public :
- Cone NAT (endpoint-independent) : le routeur réutilise le même port public quel que soit le destinataire. Le port vu par le coordinateur est donc utilisable par le pair. Le hole punching marche.
- Symmetric NAT : le routeur choisit un port public différent pour chaque destination. Le port que le coordinateur a observé ne correspond plus à celui utilisé pour joindre le pair : le trou est au mauvais endroit. Le hole punching échoue.
On tombe surtout sur du NAT symétrique en dehors de chez soi : réseaux mobiles 4G/5G (CGNAT quasi systématique — c’est le cas de mon téléphone), certaines offres FAI en pénurie d’IPv4, et les réseaux d’entreprise / hôtels / WiFi publics au pare-feu strict. Les box grand public françaises donnent en général une IPv4 publique full-cone : chez moi, le hole punching fonctionne sans souci. Les points délicats de mon parc sont donc le téléphone en 4G et le RPi, posé sur un réseau que je ne maîtrise pas et dont je ne connais pas le comportement NAT.
Quand les deux pairs sont derrière un NAT symétrique (ou un CGNAT), la connexion directe est impossible. Il faut alors un relais.
Coordinateur & relais #
Quand le hole punching échoue, le trafic transite par un nœud public qui relaie les paquets entre les deux pairs. C’est un filet de sécurité, pas le mode nominal : tout passe par un tiers, au prix de la latence et du débit. L’idéal est donc que seul le maillon mal placé (mon téléphone en 4G) l’utilise, et uniquement quand son pair est lui aussi injoignable en direct.
C’est ce qui distingue une vraie solution de mesh d’un WireGuard « nu » : elle fournit à la fois le coordinateur (pour tenter le p2p) et le relais (pour garantir la connectivité en dernier recours).
Ma configuration #
Avant de comparer les solutions, voici le parc que je veux relier. Il mélange deux profils très différents, et c’est précisément ce mélange qui conditionne mes choix.
Des appareils nomades, qui n’ont par définition aucun point d’entrée stable :
- un téléphone Android, le plus souvent en 4G/5G — donc derrière le CGNAT de l’opérateur, le cas le plus défavorable pour une connexion directe ;
- un laptop sous Linux, qui se retrouve sur des réseaux quelconques (maison, bureau, WiFi public), sans IP ni port prévisibles d’un moment à l’autre.
Ces deux-là changent de réseau en permanence : impossible de leur configurer un endpoint fixe. Ce sont typiquement eux qui auront besoin du hole punching, et du relais quand celui-ci échoue.
Des serveurs Linux, plus stables en fonctionnement, mais très inégaux face au réseau :
- un home server derrière ma box, qui dispose d’une IP publique et où je peux ouvrir un port : il est joignable de l’extérieur. C’est le seul de mon parc dans ce cas ;
- un Raspberry Pi hébergé chez un client, sur un réseau que je ne maîtrise pas du tout : je ne peux pas y ouvrir de port et je ne connais même pas leur IP publique. Bien qu’il tourne en permanence, il est injoignable de l’extérieur — côté réseau, il se comporte comme un appareil nomade derrière NAT.
Autrement dit, un seul de mes hôtes à la maison est publiquement joignable. C’est un coordinateur/relais évident, mais je ne veux justement pas que toute la résilience du mesh repose sur cette unique machine (ni sur ma box). Je m’appuie donc aussi sur un second nœud public, hébergé ailleurs, pour disposer de deux points d’entrée redondants. Ce besoin de coordinateurs redondants est le critère qui, on le verra, oriente directement le choix de la solution.
Solutions #
Plusieurs solutions me sont proposées. Je les compare surtout sur trois axes qui découlent de la partie précédente : présence d’un coordinateur et d’un relais, capacité à redonder ce coordinateur (mon vrai objectif de résilience), et effort de self-hosting.
WireGuard mesh #
WireGuard « nu » : on établit à la main un tunnel entre chaque paire de pairs et on configure les clés, IP et endpoints soi-même. La crypto et les performances sont excellentes (c’est la brique sur laquelle plusieurs des autres solutions sont bâties), mais il n’y a ni coordinateur ni relais : aucun hole punching automatique, et chaque pair doit connaître à l’avance un endpoint joignable de l’autre. Sur un maillage de N appareils mobiles derrière NAT, ça devient vite ingérable (N² tunnels à maintenir, endpoints qui changent). C’est la référence de simplicité protocolaire, mais pas une solution de mesh clé en main.
Lien : wireguard.com
NetBird #
WireGuard piloté par un plan de contrôle complet : un management server, un signal server pour le hole punching, et un relais TURN (coturn) en repli. Self-hostable, avec ACL, SSO/OIDC et une belle UI. C’est la solution la plus « produit » du lot, au prix d’une stack à opérer plus lourde (plusieurs composants + base de données).
Liens : netbird.io · dépôt (self-hosted)
Headscale #
Ré-implémentation open source du serveur de contrôle Tailscale : on récupère les clients Tailscale (très aboutis, sur toutes les plateformes) branchés sur son propre control server. Le relais se fait via des serveurs DERP chiffrés de bout en bout (auto-hébergeables). Excellent confort d’usage, mais le control server est un point central dont la redondance reste laborieuse — or c’est justement ce que je veux éviter.
Nebula #
Développé par Slack. Protocole maison basé sur le Noise Framework, avec une PKI à base de certificats (chaque hôte porte son certificat signé par une CA). Les lighthouses jouent le rôle de coordinateur, et n’importe quel hôte public peut être désigné comme relais. Point clé pour moi : on peut déclarer plusieurs lighthouses, ce qui donne une redondance du coordinateur native, sans base de données ni plan de contrôle à part — juste un binaire unique et un fichier de config par hôte.
Comparatif #
| Critère | WireGuard mesh | NetBird | Headscale | Nebula |
|---|---|---|---|---|
| Base | WireGuard | WireGuard | WireGuard (clients Tailscale) | Protocole maison (Noise) |
| Coordinateur | ❌ | ✅ management/signal | ✅ control server | ✅ lighthouse |
| Relais | ❌ | ✅ TURN (coturn) | ✅ DERP | ✅ nœuds relay |
| Coordinateur redondant | — | ⚠️ limité | ⚠️ laborieux | ✅ multi-lighthouse natif |
| Self-hosting | trivial mais tout manuel | lourd (plusieurs services + DB) | moyen (control + DERP) | léger (1 binaire + config) |
| Clients | natif OS | bons | excellents (Tailscale) | corrects (dont Android) |
IX, échange de clés Curve25519, chiffrement AES-256-GCM ou ChaCha20-Poly1305.
Ce qui le distingue, c’est la couche au-dessus : une PKI par certificats
(identité, IP et groupes signés par la CA) et un firewall distribué où
chaque hôte applique localement des règles exprimées en termes de groupes.
Pourquoi Nebula #
Mon besoin premier n’est pas le confort d’onboarding mais la résilience du coordinateur : que mes appareils continuent de se joindre même quand une box tombe. Nebula est la seule option qui offre nativement plusieurs lighthouses redondants, tout en restant p2p à 100 % et légère à opérer (un binaire, pas de base de données). C’est donc la solution que je retiens.
Architecture #
Maintenant que Nebula est choisi, voici comment j’organise le réseau. Quelques notions de vocabulaire propres à Nebula d’abord :
- CA : une autorité de certification que je génère moi-même. Elle signe le certificat de chaque hôte. Un hôte n’est accepté dans le réseau que si son certificat est signé par ma CA.
- certificat d’hôte : il lie la clé de la machine à son IP dans le réseau
Nebula et à des groupes (des étiquettes comme
serveursoumobiles). - lighthouse : le coordinateur. Il aide les pairs à se découvrir et à faire du hole punching. Il doit être joignable publiquement.
- relais : un hôte public qui relaie le trafic quand la connexion directe échoue.
Plan d’adressage #
Je réserve un sous-réseau dédié au mesh, distinct de tous mes réseaux locaux pour
éviter les collisions de routes. J’utilise ici 10.42.0.0/24 (à adapter).
| Hôte | Rôle | IP Nebula | Groupes |
|---|---|---|---|
| Home server | lighthouse + relais | 10.42.0.1 | lighthouse, serveurs |
| Nœud d’ancrage (hébergé ailleurs) | lighthouse + relais | 10.42.0.2 | lighthouse, serveurs |
| RPi (chez le client) | client | 10.42.0.20 | serveurs |
| Laptop | client | 10.42.0.10 | mobiles |
| Téléphone Android | client | 10.42.0.11 | mobiles |
Le RPi tourne en serveur mais, côté réseau, il est traité comme un client : il n’est pas joignable de l’extérieur, il s’appuie sur les lighthouses comme les autres.
Coordinateurs et relais redondants #
C’est le cœur de l’archi et la raison du choix de Nebula. Mes deux hôtes
publics — le home server et le nœud d’ancrage — sont déclarés à la fois
lighthouses et relais. Chaque appareil client connaît l’adresse publique
des deux (static_host_map) et s’enregistre auprès des deux.
Conséquences :
- Si l’un des deux tombe (panne, box coupée, maintenance), l’autre continue d’assurer la découverte des pairs et le relais. Aucun point unique de défaillance côté coordination.
- Une fois deux pairs mis en relation, leur tunnel est direct : les lighthouses ne sont plus dans le chemin de données. Ils ne redeviennent utiles que pour établir de nouvelles liaisons, ou comme relais quand le direct est impossible (typiquement le téléphone en 4G face à un pair mal placé).
Politique de firewall #
Le firewall Nebula est distribué : chaque hôte porte ses propres règles, qui s’appuient sur les groupes prouvés par les certificats. Je pars d’une posture restrictive (tout bloquer en entrée, puis ouvrir au cas par cas) :
- les serveurs exposent aux
mobilesuniquement les services voulus (par ex. SSH, et les services applicatifs précis) ; - les mobiles n’ont pas besoin d’être joignables entre eux : ils n’acceptent quasiment rien en entrée ;
- le trafic de coordination Nebula lui-même est géré par le protocole, pas par ces règles applicatives.
Le détail des règles et des fichiers de configuration vient dans la partie suivante, la mise en place pas à pas.
Mise en place #
1. Installer Nebula #
Nebula se résume à deux outils : le démon nebula (monte le tunnel, sur tous
les hôtes) et nebula-cert (gère la PKI, seulement là où je génère les
certificats). La plupart des distributions le packagent désormais :
# Debian / Ubuntu
sudo apt install nebula
# Fedora
sudo dnf install nebula
# Arch
sudo pacman -S nebula
# Alpine
sudo apk add nebula
# macOS
brew install nebulaÀ défaut de paquet, on récupère les binaires depuis les releases GitHub
(github.com/slackhq/nebula/releases) pour l’architecture voulue (Linux
64/32-bit, ARM pour le RPi, etc.). Sur Android, tout passe par l’application
Nebula.
Selon le packaging, nebula-cert est fourni par le même paquet ou séparément ;
un which nebula-cert le confirme. Le paquet installe généralement aussi un
service systemd nebula.service qui lit /etc/nebula/config.yml.
2. Générer la CA #
Sur ma machine d’admin, dans un dossier dédié, une seule commande :
nebula-cert ca -name "Mon Mesh"Ça produit ca.crt (le certificat public, à diffuser sur tous les hôtes) et
ca.key (la clé privée de la CA — à garder hors ligne et à ne jamais
distribuer : quiconque la possède peut fabriquer des identités valides).
3. Signer un certificat par hôte #
Chaque hôte reçoit un certificat qui fige son IP dans le mesh et ses groupes.
Les commandes doivent être lancées dans le dossier contenant ca.crt/ca.key :
nebula-cert sign -name "home-server" -ip "10.42.0.1/24" -groups "lighthouse,serveurs"
nebula-cert sign -name "ancrage" -ip "10.42.0.2/24" -groups "lighthouse,serveurs"
nebula-cert sign -name "rpi" -ip "10.42.0.20/24" -groups "serveurs"
nebula-cert sign -name "laptop" -ip "10.42.0.10/24" -groups "mobiles"
nebula-cert sign -name "android" -ip "10.42.0.11/24" -groups "mobiles"Chaque commande génère <nom>.crt et <nom>.key. Sur chaque hôte Linux, je
copie trois fichiers dans /etc/nebula/ : le ca.crt commun, plus le .crt et
le .key de cet hôte uniquement. La clé privée d’un hôte ne quitte jamais
cet hôte.
4. Configurer les lighthouses (home server + nœud d’ancrage) #
/etc/nebula/config.yml sur les deux hôtes publics (adapter cert/key au nom
de l’hôte) :
pki:
ca: /etc/nebula/ca.crt
cert: /etc/nebula/host.crt
key: /etc/nebula/host.key
static_host_map:
"10.42.0.1": ["home.exemple.net:4242"]
"10.42.0.2": ["ancrage.exemple.net:4242"]
lighthouse:
am_lighthouse: true
# un lighthouse ne se liste pas lui-même dans "hosts"
listen:
host: 0.0.0.0
port: 4242
punchy:
punch: true
respond: true
relay:
am_relay: true
use_relays: false
tun:
dev: nebula1
firewall:
outbound:
- port: any
proto: any
host: any
inbound:
- port: any
proto: icmp
host: any
- port: 22
proto: tcp
groups: [mobiles]Le static_host_map associe l’IP mesh d’un hôte public à son endpoint réel
(nom DNS ou IP publique + port UDP). Les deux lighthouses se connaissent ainsi
mutuellement.
5. Configurer les clients (laptop, RPi) #
Même principe, mais l’hôte n’est ni lighthouse ni relais : il utilise les deux
lighthouses. /etc/nebula/config.yml :
pki:
ca: /etc/nebula/ca.crt
cert: /etc/nebula/host.crt
key: /etc/nebula/host.key
static_host_map:
"10.42.0.1": ["home.exemple.net:4242"]
"10.42.0.2": ["ancrage.exemple.net:4242"]
lighthouse:
am_lighthouse: false
hosts:
- "10.42.0.1"
- "10.42.0.2"
listen:
host: 0.0.0.0
port: 0 # port éphémère : ces hôtes sont nomades
punchy:
punch: true
respond: true
relay:
relays:
- 10.42.0.1
- 10.42.0.2
am_relay: false
use_relays: true
tun:
dev: nebula1
firewall:
outbound:
- port: any
proto: any
host: any
inbound:
- port: any
proto: icmp
host: anyDeux différences clés avec les lighthouses : listen.port: 0 (port éphémère,
car ces hôtes changent de réseau et n’ont pas de port à exposer), et le bloc
relay qui pointe vers les deux lighthouses en repli.
La règle firewall ci-dessus (ICMP seul) convient au laptop, dans le groupe
mobiles. Le RPi, lui, est dans le groupe serveurs : si je veux l’administrer
en SSH depuis mes appareils, j’y ajoute la même règle entrante que sur les
lighthouses :
- port: 22
proto: tcp
groups: [mobiles]6. Le cas Android #
Le téléphone utilise l’application mobile Nebula (Play Store / F-Droid).
J’y crée un « site » et je lui fournis les mêmes éléments que pour un client
Linux : le ca.crt, le certificat android.crt, sa clé android.key, et la
configuration (lighthouses + static_host_map + relay). L’app joue le rôle du
démon nebula et gère le tunnel en tâche de fond.
7. Lancer et vérifier #
Le paquet fournit un service systemd. Sinon, on lance à la main :
nebula -config /etc/nebula/config.ymlPuis on active le service sur chaque hôte Linux :
sudo systemctl enable --now nebulaVérifications utiles :
# inspecter un certificat (IP, groupes, validité)
nebula-cert print -path /etc/nebula/host.crt
# une fois deux hôtes démarrés, tester la liaison overlay
ping 10.42.0.1 # depuis un client, joindre le home serverJe démarre d’abord les deux lighthouses, puis les clients. Un ping qui répond
sur les IP 10.42.0.x confirme que le tunnel est monté. Pour valider la
résilience, je coupe un lighthouse et je vérifie que les pairs continuent de se
joindre via l’autre.