Les relais VPN à grande capacité, les passerelles mandataires d'obscurcissement et les routeurs d'interconnexion d'entreprise interviennent fréquemment comme intermédiaires de transit entre les tunnels distants et les services applicatifs internes. Dans une architecture logicielle classique, le transfert du trafic impose à un démon en espace utilisateur d'exécuter des boucles perpétuelles de lecture et d'écriture entre descripteurs de fichiers de sockets. Face à des débits cumulés de 20 à 40 Gb/s sur des dizaines de milliers de sessions simultanées, cette méthode traditionnelle sature rapidement le bus mémoire et épuise les ressources processeur.
Chaque appel traditionnel à read(2) sur un socket d'entrée contraint le noyau Linux à dupliquer les octets de charge utile à travers la frontière de privilèges, depuis les tampons de socket du noyau (sk_buff) vers la mémoire du processus. Immédiatement après, le mandataire effectue un write(2) ou un send(2) pour acheminer la charge utile vers le socket de sortie, obligeant le système d'exploitation à procéder à une seconde copie en sens inverse vers l'espace noyau. Sous un trafic soutenu, les cœurs CPU consacrent jusqu'à 40 % de leurs cycles d'horloge aux instructions de copie mémoire (copy_user_generic_string), tout en expulsant les clés cryptographiques actives des caches de niveau 1 et 2 (L1 et L2).
Le noyau Linux met à disposition une alternative technique éprouvée grâce aux appels système de raccordement de tubes : splice(2) et vmsplice(2). En faisant transiter les paquets réseau à travers un anneau de tampons de tubes (pipe buffer ring) au sein même du noyau sans jamais copier les données vers l'espace utilisateur, ces primitives permettent d'atteindre un fonctionnement véritablement zéro-copie sur les relais VPN.
Les contraintes de la copie mémoire dans le routage conventionnel
Pour comprendre l'ampleur du goulet d'étranglement lors du transit de paquets à haut débit, observons la trajectoire d'un flux chiffré :
Acheminement classique en espace utilisateur (2 copies, 4 changements de contexte) :
[ Carte réseau d'entrée ] ---> [ sk_buff entrant ]
|
copy_to_user (Étape 1 : Noyau -> Espace utilisateur)
v
[ Tampon mémoire applicatif ]
|
copy_from_user (Étape 2 : Espace utilisateur -> Noyau)
v
[ Carte réseau de sortie ] <--- [ sk_buff sortant ]
Cette approche conventionnelle induit trois contraintes matérielles majeures :
- Multiplication de la bande passante du bus mémoire : Chaque octet de transit traverse quatre fois le bus mémoire principal : écriture DMA de la carte réseau vers la RAM, lecture RAM vers les registres CPU, écriture registres vers la RAM utilisateur, puis réécriture dans les tampons de sockets noyau. À 40 Gb/s, le bus mémoire encaisse 160 Gb/s de flux cumulé.
- Pollution du cache processeur : Le transit continu de mégaoctets de données à travers les registres du processeur vide le cache haute vitesse L1/L2, évincant les tables de substitution et contextes de session des chiffrements ChaCha20-Poly1305 ou AES-GCM.
- Surcoût des changements de contexte : L'alternance entre
read(2)etwrite(2)génère des millions de transitions entre modes utilisateur et noyau, gaspillant des cycles processeur à rafraîchir le tampon de traduction d'adresses (TLB).
Architecture de splice(2) : Anneau de tampons de pages
L'appel système splice(2) transfère les flux directement entre deux descripteurs de fichiers — dont au moins l'un doit être un tube (pipe) — sans duplication d'octets entre l'espace noyau et l'espace utilisateur.
Transfert intra-noyau zéro-copie via l'anneau de pages du tube :
[ Socket entrant ] ═════════════════════════════════════════╗
| ║
splice(sock_in -> pipe) ║
v ║
[ struct pipe_inode_info ] ║ Aucune copie mémoire
* Anneau struct pipe_buffer (Pointeurs de page et offset) ║ (Échange de références)
| ║
splice(pipe -> sock_out) ║
v ║
[ Socket sortant ] ═════════════════════════════════════════╝
Fonctionnement interne des tampons de tubes
Au cœur de splice(2) se trouve l'implémentation des tubes sous Linux (fs/pipe.c). Un tube n'est pas une simple suite d'octets, mais un anneau circulaire de descripteurs struct pipe_buffer pointant vers des pages de mémoire physique (struct page *) :
- Échange de pointeurs de pages : Lorsque
splice(2)récupère les données d'un socket dans un tube, le noyau ne recopie pas les octets. Il prélève directement les références aux pages physiques associées au tampon du socket entrant (skb_frag_t) et inscrit leurs adresses dans les emplacements de l'anneau du tube. - Incrémentation des références : Le système incrémente le compteur de références de la page (
page_ref_inc). La charge utile reste ancrée en mémoire vive tandis que la structure d'encadrement initiale du socket est libérée. - Transmission directe vers la sortie : Lors de l'appel ultérieur à
splice(2)pour pousser les données du tube vers le socket de sortie, la couche réseau attache ces mêmes pages à la liste de dispersion/regroupement (scatter-gather) pour transmission directe par la carte réseau via DMA.
Passerelle zéro-copie avec vmsplice(2)
Si splice(2) gère le transfert direct de descripteur à descripteur au sein du noyau, certaines passerelles VPN doivent préfixer des en-têtes personnalisés, injecter des jetons d'authentification ou inspecter des métadonnées de routage. C'est ici qu'intervient vmsplice(2).
L'appel vmsplice(2) projette un tableau de vecteurs mémoire utilisateur (struct iovec) directement dans un tube du noyau :
ssize_t vmsplice(int fd, const struct iovec *iov, unsigned long nr_segs, unsigned int flags);
Associé à l'indicateur SPLICE_F_GIFT, l'espace utilisateur cède la propriété des pages mémoire au noyau. Si ces zones respectent l'alignement de page, le noyau incorpore directement ces pages dans l'anneau du tube sans la moindre duplication. Un appel splice(2) standard peut ensuite acheminer ces pages vers l'interface réseau physique.
Mise en œuvre pratique : Relais zéro-copie haute performance
Un relais de transit performant dédie une paire de tubes bidirectionnels à chaque session de tunnel actif. La boucle de traitement exécute des appels splice(2) non bloquants coordonnés par epoll(7) :
#define _GNU_SOURCE
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#define RELAY_CHUNK_SIZE 65536
// Acheminement de paquets entre socket d'entrée et socket de sortie
int relay_socket_pair(int fd_in, int fd_out, int pipefd[2]) {
// Étape 1 : Raccordement du socket d'entrée vers le tube intermédiaire
ssize_t spliced_in = splice(fd_in, NULL, pipefd[1], NULL,
RELAY_CHUNK_SIZE,
SPLICE_F_MOVE | SPLICE_F_NONBLOCK);
if (spliced_in <= 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) return 0;
return -1; // Socket clos ou anomalie réseau
}
// Étape 2 : Raccordement du tube intermédiaire vers le socket de sortie
ssize_t spliced_out = 0;
while (spliced_out < spliced_in) {
ssize_t written = splice(pipefd[0], NULL, fd_out, NULL,
spliced_in - spliced_out,
SPLICE_F_MOVE | SPLICE_F_NONBLOCK);
if (written <= 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// Données résiduelles dans le tube ; epoll signalera la disponibilité
break;
}
return -1;
}
spliced_out += written;
}
return spliced_out;
}
Optimisation du dimensionnement des tubes
Par défaut, Linux alloue 16 pages (64 Ko) par tube. Sur les passerelles gérant de gros volumes de trafic, cette capacité restreinte peut générer une contre-pression prématurée. Les administrateurs peuvent étendre la taille des tubes grâce à fcntl et la commande F_SETPIPE_SZ :
// Extension de la capacité du tube à 1 Mo (256 pages) pour flux intensifs
fcntl(pipefd[0], F_SETPIPE_SZ, 1048576);
La limite maximale à l'échelle du système se configure via sysctl :
# Fixation du plafond de contenance des tubes à 4 Mo
sysctl -w fs.pipe-max-size = 4194304
Intégration dans l'architecture RiceGuard VPN
RiceGuard, le moteur réseau de couche 3 conçu par VPNRice, applique les transferts par tubes sur l'ensemble de son réseau de relais :
- Injection d'en-têtes par vmsplice : Lors de l'encapsulation de flux de transit, RiceGuard élabore les trames protocolaires en espace utilisateur et les transmet via
vmsplice(2)avec l'optionSPLICE_F_GIFT, évitant toute duplication de tampon. - Découplage des flux inter-centres : Pour les liaisons dorsales où les flux chiffrés voyagent entre points de présence, les fils de RiceGuard relient les tunnels clients aux liaisons de transport directement avec
splice(2). - Gains d'efficience matérielle : Sur des bancs d'essai traitant 40 Gb/s de trafic répartis sur 12 000 connexions actives, le passage aux transferts
splice(2)a réduit l'usage processeur de 68 % à 22 %, tout en diminuant la latence de transit de 32 %.
Foire aux questions
Est-il possible d'utiliser splice(2) directement entre deux sockets sans tube ?
Non. Selon la conception du noyau Linux, l'un des descripteurs passés à splice(2) doit obligatoirement être un tube. Néanmoins, l'allocation d'une paire de tubes (pipe2(2)) est quasi instantanée et l'anneau interne constitue une file d'attente d'une grande efficacité.
splice(2) fonctionne-t-il avec les connexions chiffrées comme TLS ou WireGuard ?
Oui. Lorsque le chiffrement ou le déchiffrement est pris en charge directement dans le noyau (comme avec kTLS ou les interfaces WireGuard en espace noyau), splice(2) traite directement les flux réseau déchiffrés ou chiffrés sans transition vers l'espace utilisateur.
Que se passe-t-il si un paquet arrive sous forme fragmentée dans le tampon du socket ?
Le noyau Linux gère la fragmentation de manière transparente. splice(2) parcourt chaque fragment de la liste de dispersion et alloue les entrées successives dans l'anneau pipe_buffer, préservant l'intégrité totale des paquets lors du relais.



