Comment fonctionne le routage cryptographique WireGuard
Comprenez pourquoi AllowedIPs sert à la fois de table de routage et de contrôle d’accès, puis lisez le handshake Noise et chaque fichier wg0.conf.
Fonctionnement de WireGuard, en une idée
WireGuard associe chaque paquet à une clé publique. Ce mécanisme s’appelle le routage par clés cryptographiques, et il constitue toute la conception du logiciel : la ligne AllowedIPs située à côté d’un peer est la table de routage des paquets qui quittent votre machine et la liste de contrôle d’accès des paquets reçus de ce peer. Un seul paramètre, deux fonctions. Lisez AllowedIPs de cette manière et chaque fichier de configuration WireGuard devient compréhensible.
Il n’existe ni table de sessions indexée par adresse IP, ni base de données d’utilisateurs. Un peer est une clé publique associée à l’ensemble des adresses que cette clé peut utiliser. Le handshake et les timers servent à maintenir cette association lorsque le réseau sous-jacent change. Si vous voulez d’abord mettre en place un tunnel fonctionnel sans entrer dans la théorie, créez-en un avec un VPN WireGuard auto-hébergé sur votre propre VPS, puis revenez ici lorsqu’une ligne de configuration vous surprend.
AllowedIPs est une table de routage et une liste de contrôle d’accès
Commencez par le sens sortant. Votre kernel route un paquet vers l’interface wg0 de la manière habituelle, via la table de routage principale. WireGuard compare ensuite l’adresse de destination de ce paquet aux préfixes autorisés de tous les pairs, stockés dans une même table et parcourus du préfixe le plus long au plus court. Une correspondance désigne un pair, qui désigne une clé publique, qui désigne une clé de session et un endpoint UDP. Le paquet est chiffré pour ce pair, puis envoyé vers cet endpoint.
Si le AllowedIPs d’aucun pair ne couvre la destination, rien n’est envoyé, car aucune clé ne permet de le chiffrer.
ping: sendmsg: Required key not availableCette erreur a une seule signification : l’adresse que vous avez essayé d’atteindre n’est listée chez aucun pair. Une autre erreur, ping: sendmsg: Destination address required, signifie qu’un pair a bien correspondu, mais que WireGuard ne possède aucun endpoint pour celui-ci, car aucun endpoint n’a été configuré ni appris jusque-là.
Passons maintenant au sens entrant. Un paquet UDP arrive sur le port d’écoute. WireGuard retrouve la session à partir de l’index du destinataire présent dans l’en-tête, vérifie le compteur avec une fenêtre glissante de protection contre les rejeux, puis déchiffre et authentifie la charge utile. Il ne lit le paquet interne qu’après ces vérifications. L’adresse source de ce paquet interne doit alors appartenir au AllowedIPs du pair émetteur. Dans le cas contraire, le paquet est abandonné. Lorsque le débogage dynamique est activé, le kernel affiche la raison dans une ligne comme celle-ci :
wg0: Packet has unallowed src IP (10.8.0.9) from peer 2 (203.0.113.10:51820)C’est pourquoi un pair côté serveur reçoit un /32. Un pair configuré avec AllowedIPs = 10.8.0.2/32 peut envoyer des paquets depuis 10.8.0.2, et depuis aucune autre adresse. Remplacez cette valeur par 0.0.0.0/0 et ce client pourra injecter des paquets en usurpant n’importe quelle adresse source située dans votre tunnel, y compris celle d’un autre client.
Les préfixes qui se chevauchent sont résolus selon leur spécificité, car la recherche utilise la correspondance avec le préfixe le plus long. Des préfixes identiques définis pour deux pairs se comportent différemment : l’entrée est associée au dernier pair configuré, et le premier pair cesse de recevoir ce trafic sans qu’aucune erreur ne soit affichée. wg show wg0 allowed-ips affiche la table réellement présente dans le kernel. C’est cette table qui fait foi lorsque le fichier sur disque et l’état en cours ne correspondent plus.
Lire un fichier de configuration en tenant compte du routage par clé cryptographique
Côté serveur :
[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = <server private key>
[Peer]
PublicKey = <laptop public key>
AllowedIPs = 10.8.0.2/32Côté client :
[Interface]
Address = 10.8.0.2/32
PrivateKey = <laptop private key>
[Peer]
PublicKey = <server public key>
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25Le même mot-clé n’a pas le même rôle des deux côtés. Côté client, il signifie « envoyer chaque destination à ce peer ». Côté serveur, il signifie « accepter uniquement cette adresse depuis ce peer ». L’asymétrie vient des valeurs, pas du rôle.
La moitié de ces clés ne relève pas du protocole. Address, DNS, MTU, PostUp et SaveConfig appartiennent à wg-quick, le script shell qui active l’interface. Le kernel ne les voit jamais. wg-quick strip wg0 affiche la configuration réduite que l’outil wg charge réellement. C’est le moyen le plus rapide d’observer cette séparation.
Ce que fait réellement le handshake
Le handshake de WireGuard utilise Noise_IKpsk2, issu du Noise Protocol Framework. La partie IK est utile pour l’administration système : la clé publique statique du responder est déjà connue de l’initiator, car il s’agit de la PublicKey dans votre bloc [Peer], et l’initiator envoie sa propre clé publique statique dans le premier message, chiffrée. Il n’y a donc pas d’échange de certificats ni d’aller-retour d’identification. Un observateur passif ne peut pas déterminer quelle clé appelle, sauf s’il détient la clé privée du responder.
Le coût est un aller-retour. Le message d’initiation fait 148 octets, la réponse fait 92 octets, puis les données commencent immédiatement à circuler. Chaque pair génère une nouvelle paire de clés éphémères Curve25519 pour chaque handshake. Les clés de session proviennent d’une chaîne de résultats Diffie-Hellman qui combine les clés statiques et éphémères. Les clés privées éphémères sont ensuite supprimées, ce qui assure la confidentialité persistante : quelqu’un qui enregistre votre trafic aujourd’hui et vole la clé privée du serveur l’année prochaine ne pourra toujours pas lire les données enregistrées.
Une initiation de handshake contient un horodatage TAI64N. Chaque pair mémorise le plus grand horodatage reçu de l’autre pair. Une initiation rejouée est donc rejetée. Les paquets de données contiennent un compteur de 64 bits utilisé comme nonce. Le récepteur conserve une fenêtre glissante des compteurs récemment vus. Les relectures et les réordonnancements importants sont ainsi gérés sans état de connexion de type TCP.
Les clés de session ont une durée de vie courte. Les temporisations sont intégrées au code et ne sont pas configurables.
The data behind this chart
[
{
"label": "REKEY_TIMEOUT",
"seconds": 5,
"notes": "resend a handshake initiation that got no answer"
},
{
"label": "KEEPALIVE_TIMEOUT",
"seconds": 10,
"notes": "send a keepalive after receiving data and sending none back"
},
{
"label": "REKEY_ATTEMPT_TIME",
"seconds": 90,
"notes": "give up on the handshake and report the peer as down"
},
{
"label": "REKEY_AFTER_TIME",
"seconds": 120,
"notes": "sender begins a fresh handshake for a new session key"
},
{
"label": "REJECT_AFTER_TIME",
"seconds": 180,
"notes": "the old session key is refused and traffic stops"
}
]Il s’agit de constantes de la spécification du protocole, et non de mesures. Le 5 d’entre elles régit tout le cycle de vie de la session. Après 120 secondes d’utilisation, l’émetteur lance un nouveau handshake. Après 180 secondes, l’ancienne clé est refusée immédiatement. Le trafic s’arrête donc jusqu’à la fin d’un nouveau handshake. Une initiation sans réponse est renvoyée toutes les 5 secondes, puis abandonnée après 90 secondes. C’est pourquoi wg show affiche latest handshake sous forme d’ancienneté relative, et pourquoi un tunnel sain et actif conserve une faible ancienneté. Une ancienneté qui augmente alors que vous envoyez activement du trafic indique que les handshakes échouent, et non que le tunnel est inactif.
Pourquoi un pair n’a pas de rôle client ou serveur
Les deux extrémités exécutent un code identique et utilisent le même format de configuration. Il n’existe pas de mode serveur. L’asymétrie que vous percevez vient de Endpoint, et Endpoint est facultatif.
Un pair dont l’endpoint est configuré peut lancer un handshake. Un pair qui n’en a pas attend, puis apprend l’adresse et le port de l’autre côté grâce au premier paquet correctement authentifié. Cet endpoint appris est enregistré et mis à jour chaque fois qu’un paquet valide arrive depuis une nouvelle adresse. C’est ainsi que fonctionne le roaming : un laptop qui passe du Wi-Fi à un réseau mobile conserve le même tunnel, car une session est identifiée par une clé et un index, et non par une adresse IP. Rien ne se reconnecte, car rien n’a jamais été connecté au sens TCP du terme.
Le même mécanisme entraîne un fait important à connaître : le pair qui possède l’adresse publique conserve la dernière adresse IP publique connue de l’autre côté, et wg show l’affiche.
Primitives fixes, sans négociation
WireGuard ne contient aucune liste de suites cryptographiques. ChaCha20-Poly1305 assure le chiffrement authentifié, Curve25519 l’accord de clé, BLAKE2s le hachage et HKDF la dérivation de clé. Chaque déploiement utilise ces primitives. Il n’y a donc aucune phase de négociation à analyser ni aucun mécanisme de repli vers une option plus faible. Le compromis est réel : si l’une de ces primitives est cassée, il faut publier une nouvelle version de l’ensemble du protocole et la mettre à jour aux deux extrémités, et non modifier la configuration. Cette décision unique supprime la majeure partie du code et des modes de défaillance d’un tunnel basé sur TLS. C’est l’essentiel de la comparaison présentée dans WireGuard face à OpenVPN.
Pourquoi le port ne répond pas à un scanner
Chaque message de handshake contient un champ appelé mac1. Il s’agit d’un MAC (code d’authentification de message) calculé sur le message avec une clé dérivée de la clé publique statique du responder. Un émetteur qui ne connaît pas cette clé publique ne peut pas produire de mac1 valide. Le récepteur abandonne alors le paquet sans envoyer la moindre réponse. Il n’y a ni erreur, ni reset, ni message ICMP.
Le résultat visible est un scan UDP qui ne reçoit aucune réponse.
sudo nmap -sU -p 51820 vpn.example.comnmap indique open|filtered. C’est la même réponse que pour un port dont les paquets sont ignorés silencieusement par un pare-feu. Le comportement du port est identique, que WireGuard soit à l’écoute ou non, du moins pour les clients qui ne possèdent pas déjà votre clé publique.
Un second champ, mac2, protège contre la pression exercée par un déni de service. Lorsque le récepteur est sous charge, il répond à une initiation valide par une réponse cookie de 64 octets liée à l’adresse source de l’émetteur. Il refuse d’effectuer les opérations coûteuses de cryptographie à clé publique tant que l’émetteur ne lui renvoie pas ce cookie. Cela vérifie que l’adresse source est réelle avant de consommer du temps CPU, et ce mécanisme ne s’active que sous charge.
Pourquoi 0.0.0.0/0 transforme un pair en route par défaut
Comme AllowedIPs est la table de routage, AllowedIPs = 0.0.0.0/0, ::/0 revendique toutes les destinations pour ce pair. C’est tout le principe du tunnel complet.
Le routage qui permet à cette configuration de fonctionner est plus intéressant que la ligne elle-même. Une route par défaut classique via wg0 provoquerait une boucle, car le paquet UDP chiffré qui transporte votre trafic doit lui aussi quitter la machine et correspondrait à sa propre route par défaut. wg-quick évite ce problème avec le routage par stratégie. Il marque les paquets sortants de WireGuard avec un fwmark, place la route par défaut du tunnel dans une table de routage distincte et ajoute des règles afin que seul le trafic non marqué l’emprunte. Exécutez ip rule show pour voir le résultat :
32764: from all lookup main suppress_prefixlength 0
32765: not from all fwmark 0xca6c lookup 51820
32766: from all lookup main0xca6c correspond à 51820 en hexadécimal, et 51820 est également le numéro de la table. La règle suppress_prefixlength 0 demande à la table principale d’ignorer sa propre route par défaut. Les routes plus spécifiques, comme celle de votre sous-réseau local, restent donc prioritaires, tandis que tout le reste est dirigé vers la table du tunnel. Un tunnel partiel ne nécessite rien de tout cela : une liste plus limitée, comme AllowedIPs = 10.8.0.0/24, 10.20.0.0/16, devient une série de routes ordinaires dans la table principale.
Un tunnel complet ne résout pas à lui seul la résolution des noms, car le résolveur appris par votre client auprès du réseau local reste généralement configuré et sa route est plus spécifique. C’est une tâche distincte, traitée dans DNS qui fuit en dehors d’un tunnel WireGuard.
À quoi sert réellement PersistentKeepalive
WireGuard n’envoie rien lorsqu’il n’y a aucun trafic. Il n’y a ni heartbeat, ni actualisation de session, ni paquet sur le réseau. Ce silence préserve la batterie, facilite le cas du scanner décrit plus haut et pose problème dans une configuration précise.
Un peer derrière un NAT (network address translation) ou un pare-feu stateful n’est joignable depuis l’extérieur que tant qu’un mapping existe dans cet équipement, et ce mapping a été créé par un paquet sortant. La durée de vie courante d’un mapping UDP commence autour de 30 secondes. Une fois le mapping expiré, le middlebox bloque les paquets provenant du réseau public, et le tunnel semble arrêté jusqu’à ce que le peer derrière le NAT envoie quelque chose. PersistentKeepalive = 25 envoie un paquet authentifié vide toutes les 25 secondes. Cette fréquence reste inférieure à la durée de vie courante la plus courte et maintient donc le mapping ouvert.
Configurez-le sur le peer derrière le NAT. Un serveur doté d’une adresse publique et d’un port UDP ouvert n’en a pas besoin. Le configurer sur ce serveur ne fait qu’ajouter du trafic. Ne le confondez pas avec le keepalive automatique, qui se déclenche 10 secondes après qu’un peer a reçu des données et n’a rien à envoyer en retour. Celui-ci est toujours actif et ne peut pas être configuré.
Le routage de votre LAN dans le tunnel n’est pas une fonctionnalité de WireGuard
Supposons que le pair B se trouve sur un réseau domestique 192.168.50.0/24 et que le pair A doive y accéder. Deux systèmes distincts doivent être configurés correctement, et un seul d’entre eux relève de WireGuard.
Rôle de WireGuard : ajouter 192.168.50.0/24 à la valeur AllowedIPs de B sur A. A route ainsi ce préfixe vers B et accepte de B les paquets dont l’adresse source appartient à ce préfixe. Sans cette configuration, le routage par cryptokey ne possède aucune clé pour la destination ni aucune autorisation pour la source.
Rôle du noyau : sur B, net.ipv4.ip_forward doit avoir la valeur 1, sinon le noyau supprime tout paquet déchiffré qui ne lui est pas destiné. La chaîne de forward du firewall de B doit autoriser le trafic. Les hôtes du LAN doivent disposer d’une route de retour vers 10.8.0.0/24, ou B doit appliquer une source NAT afin que les réponses repassent par B.
Le rôle de WireGuard s’arrête lorsqu’il remet le paquet déchiffré au kernel. Tout ce qui suit relève du routage et du filtrage Linux classiques. C’est pourquoi cette panne apparaît dans les compteurs de nft list ruleset ou dans ip -s link show wg0, et non dans wg show. Si vous préférez gérer les peers avec une interface web, exécuter wg-easy dans Docker génère les entrées des peers, mais les règles de forwarding restent à la charge de l’hôte. La même séparation s’applique lorsqu’une couche de coordination distribue le préfixe à votre place : annoncer un réseau privé depuis un VPS avec un subnet router Tailscale remplace la modification manuelle de AllowedIPs sur chaque peer, mais le sysctl de forwarding et les règles du firewall du routeur lui-même restent à configurer.
Pourquoi WireGuard reste dans le noyau
wg0 est un pilote de périphérique réseau. Les paquets y parviennent via la pile de routage normale, sont chiffrés dans le contexte softirq, puis sortent par une socket UDP sans jamais passer par l’espace utilisateur. C’est ce qui permet ce débit. C’est aussi pourquoi le module reste limité à environ quatre mille lignes de code, ce qui facilite sa revue et son intégration dans le noyau Linux mainline 5.6 en mars 2020. Ubuntu 24.04 et Debian 13 l’intègrent. Seul le paquet wireguard-tools manque donc.
Le fait qu’il s’agisse d’une interface normale a des conséquences pratiques. tcpdump -ni wg0 affiche les paquets internes en clair, tandis que tcpdump -ni eth0 udp port 51820 affiche les paquets externes chiffrés. Leur comparaison permet d’identifier immédiatement le sens qui pose problème. netfilter et la mise en forme du trafic traitent wg0 comme n’importe quelle autre liaison. Lorsque le module du noyau n’est pas disponible, par exemple avec une virtualisation de conteneurs qui partage le noyau de l’hôte, wireguard-go implémente le même protocole dans l’espace utilisateur au moyen d’un périphérique TUN. Le débit diminue réellement, car chaque paquet franchit deux fois la frontière du noyau.
Ce que WireGuard ne protège pas
Le modèle de menace est volontairement limité, et un protocole aussi discret peut facilement susciter de fausses attentes. Il faut le dire clairement.
- Il ne masque pas l’utilisation de WireGuard. Les messages de handshake ont des tailles fixes, le premier octet indique le type de message et le transport utilise UDP. La deep packet inspection le reconnaît facilement, et un réseau qui bloque les VPN peut l’interdire. L’obfuscation a été volontairement exclue.
- Il ne masque ni le volume ni le timing. Les payloads sont complétés uniquement jusqu’à une limite de 16 octets. Un observateur voit donc toujours quand vous envoyez des données et en estime approximativement le volume.
- Il conserve le dernier endpoint connu. Le peer qui possède l’adresse publique enregistre l’adresse IP publique actuelle de l’autre côté, et
wg showl’affiche. Avec une adresse de tunnel fixe dans la configuration, cela forme un identifiant stable qui suit un utilisateur d’un réseau à l’autre. Sur votre propre VPS, ce comportement ne pose pas de problème. C’est aussi pourquoi les services commerciaux ajoutent une couche au-dessus du protocole. - Il authentifie une clé, pas une personne. Le détenteur du fichier de clé privée est le peer. Définissez les permissions de
/etc/wireguardsur 700 et celles des fichiers de clé sur 600. - Il n’existe ni liste de révocation ni expiration. L’accès prend fin lorsque vous supprimez l’entrée du peer sur tous les serveurs qui la contiennent. Les clés statiques restent valides jusqu’à leur suppression.
Rien de tout cela ne rend WireGuard peu sûr. Cela le rend compact, et c’est précisément l’objectif : il authentifie et chiffre les communications, puis laisse la gestion des identités et l’attribution des adresses à la solution que vous construisez au-dessus. Une couche de coordination du type décrit dans WireGuard comparé à Tailscale comble précisément cette lacune, en utilisant le même data plane que celui présenté précédemment. Une fois cette couche en place, la décision suivante consiste à déterminer qui peut accéder à un service que vous exécutez dans le tunnel. C’est ce que définit le choix entre Tailscale serve et funnel pour un port donné.
FAQ
Qu’est-ce que le routage par clé cryptographique dans WireGuard ?
Le routage par clé cryptographique est la règle qui associe chaque paquet à une clé publique. Chaque entrée de pair contient une liste de préfixes dans AllowedIPs. En sortie, WireGuard sélectionne le pair en comparant la destination du paquet avec la liste de chaque pair, en privilégiant le préfixe le plus long. Cette liste joue donc le rôle de table de routage. En entrée, une fois le paquet déchiffré et authentifié, son adresse source interne doit appartenir à cette même liste pour ce pair. Sinon, le paquet est rejeté. La liste joue donc aussi le rôle de liste de contrôle d’accès. WireGuard n’a ni configuration de routage distincte ni pare-feu interne distinct, car cette même liste assure les deux fonctions.
Dois-je configurer PersistentKeepalive sur les deux pairs ?
Non. Configurez-le du côté situé derrière un NAT (traduction d’adresses réseau) ou un pare-feu à états, généralement le client. WireGuard n’envoie rien lorsqu’il est inactif. La correspondance qui permet au pair distant de joindre ce pair expire donc, souvent en moins d’une minute, et le tunnel semble alors fonctionner dans un seul sens. PersistentKeepalive = 25 envoie un paquet authentifié vide toutes les 25 secondes et maintient la correspondance ouverte. Un pair disposant d’une adresse publique et d’un port UDP ouvert n’en a pas besoin.
Pourquoi ping dans le tunnel affiche-t-il « Required key not available » ?
Parce que l’adresse de destination ne figure dans le AllowedIPs d’aucun pair. Le routage par clé cryptographique n’a donc trouvé aucune clé avec laquelle chiffrer le paquet, et le noyau a refusé de l’envoyer. Exécutez wg show wg0 allowed-ips et comparez sa sortie avec l’adresse que vous utilisez pour le ping. L’erreur similaire Destination address required correspond à un autre problème : un pair a été identifié, mais WireGuard ne possède aucun endpoint pour ce pair, car aucun n’a été configuré et aucun paquet authentifié n’est encore arrivé de ce pair.
Un pare-feu peut-il détecter et bloquer WireGuard ?
Oui. WireGuard authentifie et chiffre votre trafic, sans chercher à se dissimuler. Les messages de handshake ont une taille fixe de 148 et 92 octets. Le premier octet de chaque message indique son type. Le transport utilise UDP. Une inspection approfondie des paquets peut donc identifier facilement le protocole. Les réseaux qui bloquent UDP ou qui identifient les protocoles par leur signature l’arrêteront. Masquer le tunnel consiste à l’encapsuler dans un autre protocole. Il s’agit d’un outil distinct, pas d’un réglage WireGuard.