Lors de l'examen de protocoles de tunnelisation sécurisée, l'attention des architectes se porte naturellement sur la robustesse du chiffrement et la signature des paquets. Des algorithmes récents comme ChaCha20-Poly1305 (RFC 8439) garantissent mathématiquement qu'aucun observateur passif ne peut déchiffrer les échanges ou injecter de fausses données. Pourtant, le chiffrement seul ne répond pas à une menace inhérente aux réseaux IP non connectés : l'attaque par rejeu (replay attack).

Un espion surveillant un lien réseau n'a nul besoin de casser la clé de session pour perturber un service. En interceptant des paquets UDP chiffrés légitimes pour les réémettre quelques instants plus tard, un assaillant peut désynchroniser une session, dupliquer des transactions ou perturber la logique des protocoles applicatifs. De plus, pour les chiffrements par flux comme ChaCha20, la réutilisation d'un même vecteur d'initialisation (nonce) sur deux paquets distincts est destructrice : une simple opération XOR permettrait de déduire le flux de clé sous-jacent.

Afin d'écarter définitivement les attaques par rejeu tout en préservant des débits maximaux sur des liaisons soumises à la gigue, WireGuard déploie une architecture anti-rejeu fondée sur la RFC 6479. L'association de compteurs d'émission sur 64 bits et d'une fenêtre glissante bitmap sans décalage physique assure une protection infaillible sans dégradation face au désordre des paquets.

+-----------------------------------------------------------------------------+
|               MÉCANISME DE FENÊTRE GLISSANTE BITMAP RFC 6479                |
|                                                                             |
|      Zone rejetée (trop ancienne)            Fenêtre bitmap active (2048)   |
|  ◄───────────────────────────┤──────────────────────────────────────────►   |
|   Compteur < (T - 2048)      │               de (T - 2048) à T           │  |
|                              │                                           │  |
|  ... [Paquet k] [Paquet k+1] │ [Bit 0] [Bit 1] ... [Bit i] ... [Bit 2047]│  |
|                              │    │       │           │           │      │  |
|                              │ Le plus    │       Paquet reçu     │      │  |
|                              │ ancien     │     (Test et marquage)│      │  |
|                              └────────────┴───────────┬───────────┴──────┘  |
|                                                       │                     |
|                                                       ▼                     |
|                                            Si Bit == 1 : REJET (Rejeu)      |
|                                            Si Bit == 0 : ACCEPTÉ et bit = 1 |
|                                            Si Compteur > T : AVANCE FENÊTRE |
+-----------------------------------------------------------------------------+

Le péril de la réutilisation de nonce et le compteur 64 bits

Le standard ChaCha20-Poly1305 requiert trois paramètres : une clé symétrique de 256 bits, la charge utile en clair et un nonce unique. WireGuard emploie un compteur séquentiel sur 64 bits directement comme nonce pour ChaCha20, assorti d'une étiquette d'authentification Poly1305 de 16 octets.

L'unicité du nonce est une exigence absolue. Si l'émetteur venait à chiffrer deux messages distincts avec le même couple clé-nonce :

$$C_1 = P_1 \oplus Keystream(K, N)$$ $$C_2 = P_2 \oplus Keystream(K, N)$$

Un espion calculant la somme disjonctive (XOR) des deux messages chiffrés éliminerait purement et simplement le flux cryptographique :

$$C_1 \oplus C_2 = P_1 \oplus P_2$$

Cette vulnérabilité algébrique permet des analyses statistiques des textes clairs et l'altération des étiquettes Poly1305. Pour parer à ce risque, WireGuard incrémente strictement son compteur 64 bits à chaque paquet émis, en débutant à zéro dès l'ouverture de la session.

Le volume permis par un compteur 64 bits dépasse $1.8 \times 10^{19}$ paquets, ce qui rend son épuisement matériellement impossible en conditions normales. À 100 Gbit/s continus, il faudrait plusieurs décennies pour atteindre le plafond. Néanmoins, le protocole Noise IKpsk2 renouvelle périodiquement les clés éphémères toutes les quelques minutes, réinitialisant le compteur à zéro sous une clé neuve.

La réalité du désordre réseau : pourquoi la stricte monotonie échoue

Si l'émetteur incrémente ses compteurs dans un ordre irréprochable ($0, 1, 2, 3, \dots$), les routeurs d'Internet acheminent les datagrammes UDP de façon autonome. En pratique, les flux traversent :

  • Le routage multi-chemins (ECMP) : Les cœurs de réseaux répartissent les datagrammes sur des liaisons de fibres aux temps de propagation variables.
  • Les cartes réseau multi-files (RSS) : La distribution de la réception sur plusieurs cœurs de processeur engendre d'infimes variations d'ordonnancement.
  • La gigue des liaisons sans fil : Les transitions d'antennes 5G et la gestion des tampons Wi-Fi créent des retards sporadiques.

Par conséquent, les paquets parviennent fréquemment dans le désordre au récepteur :

$$\text{Émission :} \quad [1, 2, 3, 4, 5, 6]$$ $$\text{Réception :} \quad [1, 3, 2, 5, 4, 6]$$

Si le récepteur appliquait une règle d'accroissement strict (en refusant tout paquet dont le numéro est inférieur au maximum déjà vu), le paquet 2 serait rejeté dès lors qu'il arrive après le paquet 3. Rejeter des paquets valides en raison d'un simple désordre mineur est désastreux pour les flux TCP transportés dans le tunnel.

Dès que TCP enregistre des pertes, il envoie des acquittements dupliqués (dupACK), active les mécanismes de retransmission rapide, divise sa fenêtre de congestion par deux et fait s'effondrer le débit utile. Le dispositif anti-rejeu doit donc absorber les arrivées désordonnées tout en rejetant formellement les duplicatas malveillants.

+-----------------------------------------------------------------------------+
|                      MATRICE DE DÉCISION DU COMPTEUR REÇU                   |
+--------------------------+----------------------------+---------------------+
| Compteur entrant ($C$)   | Position par rapport à T   | Action du noyau     |
+--------------------------+----------------------------+---------------------+
| $C > T_{\text{sommet}}$  | Dépasse le sommet actuel   | Avancer la fenêtre  |
|                          |                            | Positionner le bit  |
+--------------------------+----------------------------+---------------------+
| $T_{\text{sommet}} - W \le$| Dans la fenêtre active   | Si bit == 0 : REÇU  |
| $C \le T_{\text{sommet}}$|                            | Si bit == 1 : REJET |
+--------------------------+----------------------------+---------------------+
| $C < T_{\text{sommet}} - W$| En deçà de la limite     | REJET IMMÉDIAT      |
|                          |                            | (Paquet trop vieux) |
+--------------------------+----------------------------+---------------------+

L'algorithme RFC 6479 : fenêtre bitmap sans décalage en mémoire

Pour concilier flexibilité d'ordonnancement et sécurité, les premières spécifications IPsec (RFC 4303) avaient introduit des bitmaps de 32 ou 64 bits. Lors de l'arrivée d'un paquet supérieur, le système décalait l'ensemble des bits vers la gauche en mémoire.

Sur des interfaces traitant des millions de paquets par seconde, décaler en mémoire un grand tableau de 1024 ou 2048 bits consomme inutilement des cycles processeur et invalide les lignes de cache CPU. WireGuard adopte l'algorithme perfectionné de la RFC 6479.

Représentation mathématique et indexation circulaire

Au lieu de translater physiquement les octets, la RFC 6479 exploite un tableau circulaire de mots machine. Dans WireGuard, la fenêtre mesure $W = 2048$ bits, soit 32 entiers non signés de 64 bits ($32 \times 64 = 2048$ bits) :

#define COUNTER_WINDOW_SIZE 2048
#define COUNTER_BITS_PER_WORD 64
#define COUNTER_WORDS (COUNTER_WINDOW_SIZE / COUNTER_BITS_PER_WORD) // 32 mots

struct noise_replay_counter {
    u64 counter;
    unsigned long backtrack[COUNTER_WORDS];
    spinlock_t lock;
};

Quand un paquet arrive avec le compteur $C$, l'indice du mot responsable se calcule par une division modulaire :

$$\text{Indice du mot} = \left(\frac{C}{64}\right) \pmod{32}$$ $$\text{Position du bit} = C \pmod{64}$$

Comme la taille est une puissance de deux, l'opération se résume à une instruction binaire ET extrêmement rapide :

unsigned long word_index = (counter / 64) & (COUNTER_WORDS - 1);
unsigned long bit_mask = 1UL << (counter & (COUNTER_BITS_PER_WORD - 1));

Dynamique du glissement de fenêtre

Lors de la réception, le traitement se scinde en trois voies :

  1. Dans la fenêtre active ($T - W < C \le T$) : Le système évalue le bit désigné. S'il vaut 1, le paquet est un rejeu frauduleux et est immédiatement écarté. S'il vaut 0, le paquet est accepté et le bit passe à 1.
  2. Au-delà du sommet ($C > T$) : Le paquet apporte la plus grande séquence observée. La fenêtre avance. L'algorithme remet à zéro les mots intermédiaires entre l'ancien et le nouveau sommet, réajuste la borne ($T = C$) et positionne le bit.
  3. Hors de portée ($C \le T - W$) : Le paquet accuse un retard de plus de 2048 positions. Le système le rejette sur-le-champ sans mobiliser de ressources cryptographiques pour le déchiffrer.

Cette structure assure un temps d'exécution constant $O(1)$, sans allocation dynamique et avec un respect scrupuleux des mémoires caches du processeur.

Disposition en mémoire du tableau circulaire (32 mots de 64 bits) :
Mot 0        Mot 1        Mot 2              Mot 31
[64 bits]    [64 bits]    [64 bits]   ...    [64 bits]
    ▲
    │
    └── Le curseur avance de manière circulaire sans déplacement physique

Déchiffrement parallèle et synchronisation multicoeur

Dans les environnements serveurs exigeants, déchiffrer des millions de paquets UDP sur un seul cœur sature la bande passante. Dans le noyau Linux, WireGuard répartit le fardeau cryptographique sur tous les cœurs disponibles grâce aux files parallèles padata.

Le cycle de traitement s'organise avec méthode :

  1. Réception : Le pilote réseau place les datagrammes entrants dans la file de réception.
  2. Calcul cryptographique parallèle : WireGuard confie le déchiffrement ChaCha20-Poly1305 aux différents cœurs CPU, chacun validant son étiquette de façon indépendante.
  3. Validation anti-rejeu sérialisée : Les cœurs n'achevant pas leurs calculs au même instant, la mise à jour de la fenêtre anti-rejeu est exécutée dans une phase de clôture légère (wg_packet_consume_data_done) protégée par un verrou atomique.

Cette séparation entre calculs cryptographiques lourds en parallèle et vérification légère sérialisée permet d'atteindre des débits de plusieurs dizaines de gigabits par seconde sans risque de conflit concurrentiel.

Compromis entre largeur de fenêtre et gigue réseau

Le choix d'une fenêtre de 2048 bits résulte d'un équilibre rigoureux entre empreinte mémoire et tolérance aux retards :

  • Une fenêtre trop étroite (ex. 64 bits) provoquerait le rejet erroné d'un paquet dès que celui-ci accumule un retard de 64 trames sur un lien à 10 Gbit/s.
  • Une fenêtre excessivement large (ex. 65 536 bits) alourdirait la consommation de mémoire vive par pair, réduisant l'efficacité du cache processeur face à des dizaines de milliers de tunnels simultanés.

La dimension de 2048 bits offre une marge confortable. Avec un MTU standard de 1420 octets, elle couvre près de 2,9 Mo de données en transit, ce qui surpasse largement les fluctuations courantes constatées sur les liaisons internationales.

Au sein de l'écosystème VPNRice, le protocole RiceGuard s'appuie sur ces fondations cryptographiques. Associé à notre gestion du routage de couche 3 et aux principes détaillés dans notre guide sur les Architectures de tunnels UDP, RiceGuard neutralise les attaques par rejeu tout en prévenant les chutes de débit.

Pour bénéficier d'une protection éprouvée et d'une prise en charge matérielle fluide sur vos postes de travail et téléphones, téléchargez la dernière version sur la page Télécharger VPNRice.