L'acheminement de flux de données chiffrées à très haut débit exige une synchronisation millimétrée entre la gestion des sockets et les files d'attente réseau de bas niveau. Les démons VPN conventionnels s'exécutent pour la plupart dans l'espace utilisateur (user space), échangeant des trames réseau au travers d'interfaces de caractères virtuelles TUN/TAP. Chaque paquet traversant le tunnel subit ainsi au minimum deux changements de contexte utilisateur-noyau et nécessite de multiples duplications de mémoire entre les tampons applicatifs et les structures réseau du noyau Linux (sk_buff).

WireGuard élimine ces goulots d'étranglement structurels en intégrant l'intégralité de sa logique au cœur de la pile réseau du noyau Linux. La clé de voûte de cette architecture réside dans le fichier source drivers/net/wireguard/socket.c. Ce sous-système prend en charge la création, l'association de ports, le multiplexage et le routage des sockets UDP directement en espace noyau. Au lieu d'aborder les sockets comme de simples points d'extrémité applicatifs, WireGuard raccorde ses sockets natifs directement sur la voie rapide de réception des paquets.

Cycle de vie des sockets et initialisation Dual-Stack

Dès la création d'une interface WireGuard (comme wg0), le module noyau alloue les structures cryptographiques nécessaires et exécute la fonction wg_socket_init(). Contrairement aux applications qui sollicitent les appels système POSIX classiques (socket(), bind()), WireGuard s'interface avec le réseau via les fonctions internes du noyau :

int wg_socket_init(struct wg_device *wg, u16 port)
{
    struct net *net = dev_net(wg->dev);
    struct socket *new_4 = NULL, *new_6 = NULL;
    struct udp_port_cfg port_4 = {
        .family = AF_INET,
        .local_ip.s_addr = htonl(INADDR_ANY),
        .local_udp_port = htons(port),
        .use_saddr = false
    };
#if IS_ENABLED(CONFIG_IPV6)
    struct udp_port_cfg port_6 = {
        .family = AF_INET6,
        .local_ip6 = IN6ADDR_ANY_INIT,
        .local_udp_port = htons(port),
        .use_saddr = false,
        .ipv6_v6only = true
    };
#endif
    /* Création des sockets noyau IPv4 et IPv6 sur un port d'écoute unique */
    ret = udp_sock_create(net, &port_4, &new_4);
    if (ret < 0)
        return ret;
#if IS_ENABLED(CONFIG_IPV6)
    ret = udp_sock_create(net, &port_6, &new_6);
    if (ret < 0) {
        sock_release(new_4);
        return ret;
    }
#endif
    /* Enregistrement des gestionnaires d'encapsulation directe */
    setup_udp_tunnel_sock(net, new_4, &tunnel_cfg);
    setup_udp_tunnel_sock(net, new_6, &tunnel_cfg);
    return 0;
}

L'architecture dual-stack alloue deux pointeurs struct socket indépendants au sein du même espace de noms réseau (dev_net(wg->dev)) : un pour IPv4 (AF_INET) et un pour IPv6 (AF_INET6). Lorsque la prise en charge d'IPv6 est activée dans le noyau, le paramètre ipv6_v6only est explicitement configuré à true. Cette décision évite l'ambiguïté des adresses IPv4 mappées en IPv6 et garantit que les flux de données suivent des trajectoires prévisibles sans coût de traduction intermédiaire.

Les deux sockets sont reliés au même port UDP. Lorsque le numéro de port spécifié est égal à 0, le noyau attribue un port éphémère disponible au premier socket et réassigne exactement le même port au second socket, assurant une cohérence parfaite pour les pairs distants.

Interception zero-copy des datagrammes via udp_tunnel_sock

Dans la programmation classique de sockets, un paquet entrant réveille un thread d'écoute epoll, plaçant la charge utile dans la file de réception du socket avant sa lecture par recvmsg(). Sous forte charge réseau, cette approche engendre de la latence et une dégradation des caches CPU.

WireGuard s'affranchit totalement de la file de réception UDP conventionnelle en configurant un gestionnaire d'encapsulation via setup_udp_tunnel_sock(). La structure struct udp_tunnel_sock_cfg définit le pointeur de rappel encap_rcv = wg_receive.

Pilote de la carte réseau physique (Ring Buffer)
            │
            ▼ (NAPI poll / NET_RX_SOFTIRQ)
     ip_rcv() / ip6_rcv()
            │
            ▼
        udp_rcv()
            │
            ▼
┌───────────────────────────────────────┐
│ udp_tunnel_sock_cfg.encap_rcv         │  <--- Interception directe dans le noyau
│ (wg_receive dans drivers/net)         │       (Aucune transition d'espace)
└───────────────────────────────────────┘
            │
            ▼
   Validation des Cookies et tags MAC
            │
            ▼
 Files de déchiffrement parallèle (padata)
            │
            ▼
    wg_xmit() transfert vers l'interface wg0

Lorsqu'un datagramme atteint udp_rcv() dans le cœur de la pile réseau, le noyau vérifie si le socket dispose d'un point d'accroche d'encapsulation. Dès la détection d'encap_rcv, le système exécute immédiatement wg_receive(sk, skb) dans le contexte de l'interruption logicielle (NET_RX_SOFTIRQ).

Ce procédé offre des bénéfices techniques majeurs :

  • Traitement sans copie (Zero-Copy) : La mémoire tampon d'origine (skb) demeure inchangée dans la mémoire du noyau, depuis sa prise en charge par la carte réseau physique jusqu'à son déchiffrement complet.
  • Validation immédiate : Le pilote inspecte le préfixe d'en-tête de 4 octets, valide le jeton de cookie et vérifie l'état de la négociation Noise avant de mobiliser les cycles processeur pour les calculs ChaCha20-Poly1305.
  • Suppression des verrous bloquants : Les paquets n'étant jamais stockés dans les tampons de réception habituels, tout phénomène de bufferbloat ou de conflit de verrou de socket se trouve éliminé.

Protection contre les boucles de routage grâce à Fwmark

Un piège récurrent dans la gestion des tunnels VPN est la création d'une boucle de routage : si un paquet chiffré est émis vers l'extérieur sous forme de datagramme UDP, mais que la table de routage par défaut (0.0.0.0/0) renvoie ce même datagramme dans le tunnel, le système entre dans une récursion infinie qui sature la mémoire.

Dans socket.c, WireGuard prévient ces anomalies au niveau même de l'émission des sockets en appliquant une marque de filtrage réseau (fwmark) et en isolant les requêtes de routage :

static struct dst_entry *wg_socket_route4(struct wg_device *wg,
                                         struct sockaddr_in *dst,
                                         u32 fwmark)
{
    struct flowi4 fl = {
        .daddr = dst->sin_addr.s_addr,
        .saddr = 0,
        .flowi4_proto = IPPROTO_UDP,
        .flowi4_mark = fwmark,
        .flowi4_flags = FLOWI_FLAG_ANYSRC
    };
    struct rtable *rt = ip_route_output_key(dev_net(wg->dev), &fl);
    return &rt->dst;
}

Lors de l'envoi des poignées de main ou des paquets chiffrés, wg_socket_send_skb_to_peer() assigne la valeur incoming_handshake_fwmark dans la structure de flux (flowi4_mark ou flowi6_mark). Les administrateurs configurent des règles de routage par règles (ip rule add not fwmark 51820 table 51820), ce qui contraint tous les datagrammes UDP émis par WireGuard à cheminer directement vers la passerelle physique sans être interceptés par la table du tunnel.

De surcroît, le sous-système relie les sockets à l'interface matérielle requise (sk_bound_dev_if) dès lors que src_if4 ou src_if6 est renseigné.

Gestion mémoire et expansion de tampon sk_buff à l'émission

La chaîne d'émission dans socket.c est conçue pour éviter toute réallocation superflue de structures sk_buff. Quand un paquet IP sortant entre dans wg0 via wg_xmit(), le pilote réserve à l'avance l'espace nécessaire aux métadonnées :

  1. 16 octets pour l'étiquette d'authentification Poly1305.
  2. 16 octets pour l'en-tête de transport WireGuard.
  3. 8 octets pour l'en-tête UDP.
  4. 20 octets pour l'en-tête IPv4 (ou 40 octets pour IPv6).

En dimensionnant rigoureusement la marge en tête (headroom) et en fin de paquet (tailroom), socket.c applique les modifications au tampon existant via skb_cow_head(). Le chiffrement s'effectue sur place sans créer de copie, puis udp_tunnel_xmit_skb() transmet la trame directement à la file d'attente de la carte réseau.

Optimisations avancées au sein de l'infrastructure RiceGuard

L'implémentation WireGuard par défaut exploite une seule paire de sockets par interface. Si ce schéma convient aux appareils personnels et aux serveurs de moyenne dimension, les passerelles Anycast de RiceGuard gérant des centaines de milliers de sessions simultanées subissent des contentions de lignes de cache mémoire sur les processeurs multicœurs.

RiceGuard enrichit la gestion des sockets noyau au travers de plusieurs améliorations :

  • Affinité de socket basée sur RSS/RPS : Répartition des flux UDP vers des cœurs CPU dédiés selon le hachage des adresses IP des pairs, éliminant les invalidations de cache L1/L2 lors de l'exécution des routines softirq.
  • Surveillance dynamique du PMTU via sk_error_report : Détection instantanée des messages ICMP réclamant une fragmentation, réajustant immédiatement la taille des paquets sans interruption de session cryptographique.
  • Protection des files d'attente avec SO_RCVBUFFORCE : Ajustement prioritaire de la taille des tampons pour les tunnels authentifiés lors de violentes attaques UDP, préservant la stabilité des connexions applicatives.

En unifiant le cycle de vie des sockets avec les mécanismes profonds du noyau Linux et en écartant les couches de traduction superflues, WireGuard et RiceGuard garantissent une latence prévisible et un cloisonnement réseau rigoureux.