Le goulot d'étranglement de la file unique dans les interfaces réseau virtuelles

Les concentrateurs VPN d'entreprise, les passerelles de routage virtuel et les équipements périphériques modernes traitent d'immenses flux de données sur des liaisons 40 GbE et 100 GbE. Alors que les cartes d'interface réseau physiques (NIC) exploitent depuis longtemps des mécanismes matériels multi-files (Receive Side Scaling / RSS), les interfaces réseau virtuelles en espace utilisateur restent fréquemment bridées par des configurations architecturales héritées.

Sous Linux, le mécanisme standard pour transférer des paquets IP de couche 3 entre les démons en espace utilisateur et la pile réseau du noyau repose sur le pilote Universal TUN/TAP (/dev/net/tun). Dans les déploiements traditionnels, le démon réseau ouvre /dev/net/tun une seule fois, obtient un unique descripteur de fichier et configure une seule interface réseau (comme tun0).

Lorsque le débit s'élève à plusieurs gigabits par seconde, cette architecture mono-file rencontre des limites structurelles majeures :

  1. Sérialisation stricte sur le cœur CPU 0 : Parce qu'un unique descripteur de fichier lit et écrit les trames de l'interface virtuelle, l'intégralité du traitement de paquets est confinée au seul cœur de processeur exécutant ce thread d'entrées/sorties. Même sur un serveur d'entreprise doté de 64 cœurs, le cœur 0 atteint une saturation de 100 % tandis que les 63 autres cœurs restent inactifs.
  2. Contention destructrice de verrous dans tun.c : Sur une interface TUN à file unique, tous les flux d'émission et de réception doivent se synchroniser à travers un unique verrou tournant du noyau (tun->lock). Si plusieurs threads tentent de lire ou d'écrire en concurrence sur ce descripteur, les changements de contexte et la contention dégradent considérablement les performances globales du processeur.
  3. Neutralisation de la distribution matérielle : Les interfaces physiques distribuent les paquets entrants sur plusieurs anneaux de réception matériels (RX rings) grâce au hachage Toeplitz. Lorsque ces flux parallèles sont entonnoirés vers une interface virtuelle mono-thread, le bénéfice de l'accélération matérielle est totalement anéanti.

Pour éliminer ce plafond d'évolutivité, le noyau Linux 3.8 a introduit la prise en charge de l'option IFF_MULTI_QUEUE au sein du sous-système TUN/TAP.

Fonctionnement interne de IFF_MULTI_QUEUE dans le noyau Linux

L'architecture multi-files redéfinit l'interaction fondamentale entre les équipements réseau et les descripteurs de fichiers dans le noyau. Au lieu d'imposer une correspondance stricte 1:1 entre struct net_device et un contexte de périphérique caractère, IFF_MULTI_QUEUE permet à plusieurs descripteurs de fichiers indépendants (struct tun_file) de s'attacher à une seule et même interface réseau virtuelle (struct tun_struct).

+-------------------------------------------------------------------------+
|                  Moteur VPN multi-threads en espace utilisateur         |
+-------------------------------------------------------------------------+
|  Thread de travail 0 | Thread de travail 1 |  ...   |  Thread de travail N-1|
|  (Fixé au cœur 0)    | (Fixé au cœur 1)    |        |  (Fixé au cœur N-1)   |
+----------------------+---------------------+--------+-----------------------+
       |                      |                                |
       | fd_0                 | fd_1                           | fd_n-1
       v                      v                                v
+-------------------------------------------------------------------------+
|                 Descripteurs de fichiers indépendants (/dev/net/tun)    |
+-------------------------------------------------------------------------+
       |                      |                                |
       v Lecture/écriture     v Lecture/écriture               v Lecture/écrit
+-------------------------------------------------------------------------+
|                         Sous-système TUN du noyau                       |
|                                                                         |
|  +--------------+    +--------------+              +--------------+     |
|  | tun_file [0] |    | tun_file [1] |     ...      | tun_file[n-1]|     |
|  | File socket  |    | File socket  |              | File socket  |     |
|  +--------------+    +--------------+              +--------------+     |
|         \                   |                             /             |
|          \                  |                            /              |
|           v                 v                           v               |
|   +-----------------------------------------------------------------+   |
|   |               struct tun_struct (net_device: tun0)              |   |
|   |         - Routage de flux via la fonction skb_get_hash()        |   |
|   |         - Distribution sans verrou via le tableau RCU           |   |
|   +-----------------------------------------------------------------+   |
+-------------------------------------------------------------------------+
       |                                                    ^
       v Émission des paquets                               | Réception paquets
+-------------------------------------------------------------------------+
|                  Routage IP et Netfilter dans le noyau Linux            |
+-------------------------------------------------------------------------+

1. Résolution de file sans verrou grâce à RCU

Dans le fichier source drivers/net/tun.c, le noyau gère les files d'attente actives au moyen d'un tableau de pointeurs protégé par Read-Copy-Update (RCU) : struct tun_file __rcu *tfiles[MAX_TAP_QUEUES]. Lorsqu'un paquet sortant est dirigé vers tun0, le noyau sélectionne sa file cible sans solliciter de verrou mutex global :

// Principe de sélection de file dans tun.c au sein du noyau
static struct tun_file *tun_get_queue(struct tun_struct *tun, struct sk_buff *skb)
{
    u32 hash = skb_get_hash(skb);
    u16 numqueues = READ_ONCE(tun->numqueues);
    u16 index = reciprocal_scale(hash, numqueues);
    return rcu_dereference(tun->tfiles[index]);
}

Ce mode de consultation reposant sur un index RCU sans verrou garantit une mise à l'échelle rigoureusement linéaire en fonction du nombre de cœurs processeur raccordés.

2. Routage de flux symétrique

Afin d'éviter tout désordre dans la transmission des paquets au sein des flux TCP ou UDP, le noyau calcule une empreinte symétrique à quatre paramètres (4-tuple) via skb_get_hash(). L'ensemble des paquets appartenant à une même session cliente est systématiquement aiguillé vers la même file, préservant l'intégrité séquentielle de TCP tout en répartissant les différents tunnels sur l'ensemble des cœurs de calcul.

Implémentation de l'allocation multi-files en C

Pour configurer une interface TUN multi-files, l'application en espace utilisateur doit ouvrir /dev/net/tun de façon indépendante pour chaque thread de travail. Chaque thread émet ensuite l'appel ioctl(TUNSETIFF) avec le même nom d'interface et le drapeau IFF_MULTI_QUEUE.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/if.h>
#include <linux/if_tun.h>
#include <errno.h>

/**
 * Attache un nouveau descripteur de fichier à une interface TUN multi-files.
 * @param dev Nom de l'interface réseau (par exemple "tun0")
 * @return Descripteur de fichier en cas de succès, -1 en cas d'erreur
 */
int tun_alloc_mq(const char *dev)
{
    struct ifreq ifr;
    int fd;

    // Ouverture d'une instance indépendante du périphérique caractère
    if ((fd = open("/dev/net/tun", O_RDWR | O_NONBLOCK)) < 0) {
        perror("Échec de l'ouverture de /dev/net/tun");
        return -1;
    }

    memset(&ifr, 0, sizeof(ifr));

    // Mode TUN couche 3, attachement multi-files et suppression des métadonnées
    ifr.ifr_flags = IFF_TUN | IFF_MULTI_QUEUE | IFF_NO_PI;

    if (dev && *dev) {
        strncpy(ifr.ifr_name, dev, IFNAMSIZ - 1);
    }

    // Enregistrement du descripteur auprès de l'interface réseau du noyau
    if (ioctl(fd, TUNSETIFF, (void *)&ifr) < 0) {
        perror("Échec de ioctl(TUNSETIFF)");
        close(fd);
        return -1;
    }

    return fd;
}

Drapeaux complémentaires déterminants : IFF_NO_PI et IFF_VNET_HDR

Pour obtenir un débit optimal sur les threads de travail, l'allocation multi-files s'associe à deux drapeaux majeurs :

  • IFF_NO_PI (Sans en-tête d'information de paquet) : Par défaut, le pilote TUN ajoute 4 octets d'en-tête (flags et proto) devant chaque trame. L'activation de IFF_NO_PI retire cette surcharge, permettant aux paquets IP bruts de s'aligner directement dans les zones mémoires pour le chiffrement vectorisé SIMD.
  • IFF_VNET_HDR (En-tête réseau virtuel Virtio) : Active les mécanismes Generic Segmentation Offload (GSO) et Generic Receive Offload (GRO) entre l'espace utilisateur et le noyau. Au lieu d'échanger des trames MTU individuelles de 1500 octets, l'application peut lire et transmettre des super-paquets de 64 Ko, réduisant jusqu'à 75 % le nombre d'appels système.

Meilleures pratiques architecturales pour passerelles VPN multi-files

L'exploitation industrielle de IFF_MULTI_QUEUE nécessite une coordination fine entre les paramètres du noyau, l'ordonnancement des processeurs et la gestion des flux réseau.

Trafic WAN chiffré entrant (Interface physique 100GbE)
                      |
                      v Répartition matérielle RSS
+-------------------------------------------------------------------+
|            Déchiffrement et orientation des flux dans le noyau    |
+-------------------------------------------------------------------+
       |                      |                      |
       v Cœur 0               v Cœur 1               v Cœur 2
+---------------+      +---------------+      +---------------+
| tun0 Queue 0  |      | tun0 Queue 1  |      | tun0 Queue 2  |
| fd_0          |      | fd_1          |      | fd_2          |
+---------------+      +---------------+      +---------------+
       |                      |                      |
       v                      v                      v
+---------------+      +---------------+      +---------------+
| Cœur 0 fixé   |      | Cœur 1 fixé   |      | Cœur 2 fixé   |
| Thread Rice   |      | Thread Rice   |      | Thread Rice   |
+---------------+      +---------------+      +---------------+

1. Affinité stricte des threads aux cœurs processeur

Pour garantir une latence d'acheminement sous la milliseconde, chaque descripteur de file doit être pris en charge par un thread dédié, épinglé à un cœur processeur isolé via pthread_setaffinity_np(). Cette isolation supprime les migrations de threads, préserve les caches L1/L2 et élimine la surcharge liée à la cohérence de cache inter-cœurs.

2. Dimensionnement de la file d'attente de transmission

Chaque descripteur tun_file gère son propre tampon de réception en mémoire noyau. Lors de pics brutaux de trafic, les limites par défaut peuvent provoquer des pertes silencieuses de paquets. Les passerelles doivent calibrer la taille de la file de transmission (txqueuelen) :

# Augmentation de la taille de file d'émission de l'interface multi-files
ip link set dev tun0 txqueuelen 10000

3. Couplage avec le multiplexage d'entrées/sorties asynchrone

Chaque thread surveille son descripteur tun_file à l'aide de mécanismes événementiels déclenchés sur front (epoll avec EPOLLET) ou d'anneaux io_uring. Les threads procèdent à des lectures vectorisées par lots (readv ou recvmmsg), purgeant immédiatement les tampons mémoire lors des saturations de paquets.

Évolutivité des chemins de données multi-cœurs de RiceGuard

Au sein de l'infrastructure de production de VPNRice, le moteur RiceGuard tire parti d'IFF_MULTI_QUEUE pour assurer un routage de paquets sans verrou à travers les clusters de serveurs haute densité.

  1. Topologie de files adaptée aux architectures NUMA : RiceGuard détecte automatiquement la structure des nœuds NUMA et les canaux de mémoire vive. Les files sont allouées de façon symétrique et rattachées aux cœurs partageant le même contrôleur PCIe local, supprimant les engorgements sur les bus inter-processeurs QuickPath / UltraPath.
  2. Traitement par lots vectorisé sans copie : En associant les descripteurs TUN multi-files au drapeau IFF_VNET_HDR et à des modules cryptographiques vectorisés ChaCha20-Poly1305, RiceGuard traite plus de 25 millions de paquets par seconde sur du matériel serveur standard.
  3. Étanchéité des espaces de noms réseau : Chaque interface TUN multi-files est provisionnée dans un espace de noms réseau (Network Namespace) Linux isolé. Cette architecture empêche toute fuite de données en clair vers les tables de routage par défaut ou les interfaces physiques publiques, conjuguant confidentialité stricte et pleine saturation matérielle.