Les concentrateurs VPN à fort volume, les proxys de terminaison TLS et les routeurs de bordure d'entreprise traitent un flux continuel d'ouvertures de connexions. Sous des charges soutenues de plusieurs dizaines de milliers de requêtes par seconde, la cadence réelle d'un relais ne se résume pas à sa puissance cryptographique ou à sa bande passante. La stabilité globale est fréquemment mise en péril par la prolifération de poignées de main TCP inactives, de balayages de ports automatisés et de tentatives d'épuisement de ressources de type Slowloris.

Dans le fonctionnement habituel de Linux, l'achèvement de la négociation tripartite standard (SYN -> SYN-ACK -> ACK) transforme instantanément une simple requête en un socket complet. Le système place ce socket dans la file d'attente d'acceptation et signale l'événement à l'espace utilisateur via epoll. Le processus mandataire se réveille, exécute accept(), alloue les structures de suivi et insère le descripteur dans sa boucle événementielle, pour finalement attendre des octets applicatifs qui risquent de tarder ou de ne jamais parvenir.

Afin d'éliminer ces changements de contexte processeur superflus et de prémunir l'espace utilisateur contre les vagues de connexions muettes, Linux propose l'option de socket TCP_DEFER_ACCEPT. En instruisant le noyau de différer l'admission d'une connexion jusqu'à ce que le client émette son tout premier octet de charge utile, TCP_DEFER_ACCEPT transfère le filtrage initial dans la mémoire légère du noyau, constituant ainsi un rempart indispensable pour les passerelles VPN denses.


La crise des poignées de main inactives : Réveils Epoll superflus et saturation d'état

Dans le comportement TCP conventionnel, l'établissement d'une liaison s'appuie sur une structure d'attente à deux niveaux dans la pile réseau Linux :

  1. La file SYN Backlog : Lorsqu'un client transmet un paquet TCP SYN, le noyau génère une structure compacte request_sock (d'environ 256 octets) qu'il insère dans la file SYN tout en renvoyant le paquet SYN-ACK.
  2. La file Accept Queue : Dès que le client répond avec le paquet ACK final clôturant la poignée de main, le noyau alloue une structure struct sock complète (immobilisant plus de 2 Ko de mémoire physique verrouillée, en sus des tampons d'émission et de réception), transfère la connexion dans la file d'acceptation du socket d'écoute et active le drapeau de lecture (EPOLLIN).
Le goulet d'étranglement de la file d'acceptation classique :

Client                            Pile réseau du noyau Linux            Proxy en espace utilisateur
  │                                           │                                  │
  │─── SYN ──────────────────────────────────►│                                  │
  │◄── SYN-ACK ───────────────────────────────│ (Création request_sock ~256 o)   │
  │                                           │                                  │
  │─── ACK (Poignée de main sans données) ───►│                                  │
  │                                           │──► Allocation struct sock        │
  │                                           │──► Placement dans Accept Queue   │
  │                                           │──► Réveil EPOLLIN ──────────────►│
  │                                           │                                  │ (Réveil du thread)
  │                                           │◄── accept4() ────────────────────│ (Allocation 4-16 Ko)
  │                                           │                                  │ (Attente passive...)
  │                                           │                                  │
  │... (Client inactif ou sonde de port) .....│..................................│ (Thread bloqué !)

Ce fonctionnement par défaut induit de lourdes faiblesses dans l'architecture des relais réseau :

  • Consommation stérile de cycles processeur : Les threads applicatifs se réveillent sans cesse, invoquent des appels système et mobilisent de la mémoire vive pour des sessions qui ne véhiculent aucune donnée concrète.
  • Engorgement de la table de descripteurs : Les outils de cartographie réseau et robots d'analyse établissent des connexions puis les laissent en sommeil, poussant la table des descripteurs du proxy vers le plafond RLIMIT_NOFILE.
  • Saturation de la file d'attente : Lors des rafales de trafic, les négociations vides saturent l'espace d'attente, provoquant le rejet de clients légitimes avant même que l'espace utilisateur ne puisse les dépiler.

Analyse approfondie : Comment TCP_DEFER_ACCEPT modifie la machine à états du noyau

Activé par l'appel setsockopt(), TCP_DEFER_ACCEPT redéfinit en profondeur la coordination entre le moteur de négociation du noyau et la boucle d'événements applicative :

int timeout_secs = 30;
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);

/* Demande au noyau de reporter la notification accept() jusqu'à réception de données */
int ret = setsockopt(listen_fd, IPPROTO_TCP, TCP_DEFER_ACCEPT, &timeout_secs, sizeof(timeout_secs));

1. Prolongation de l'état SYN_RECV et report de l'allocation complète du socket

Lorsque TCP_DEFER_ACCEPT est configuré sur un socket d'écoute, le noyau reçoit bien le paquet ACK terminal du client mais suspend délibérément l'élévation du socket. Si ce paquet ACK ne contient aucune donnée utile (skb->len == 0), le noyau maintient la connexion dans l'état léger SYN_RECV au sein de la table des request sockets.

La connexion n'est pas promue en struct sock complète, et aucun événement EPOLLIN n'est transmis au socket d'écoute. Du point de vue du client distant, la liaison est opérationnelle. En revanche, pour la machine hôte, elle n'occupe qu'une empreinte mémoire minime et ne réclame aucun cycle de traitement en espace utilisateur.

2. Disponibilité immédiate des données lors de l'appel accept() utilisateur

Dès l'instant où le client transmet son premier paquet applicatif—qu'il s'agisse d'un message TLS ClientHello, d'une commande HTTP CONNECT ou de l'initialisation d'un tunnel WireGuard via TCP—le noyau associe ce paquet à la structure request_sock en attente.

Fonctionnement de TCP_DEFER_ACCEPT :

Client                            Pile réseau du noyau Linux            Proxy en espace utilisateur
  │                                           │                                  │
  │─── SYN ──────────────────────────────────►│ (Création request_sock)          │
  │◄── SYN-ACK ───────────────────────────────│                                  │
  │─── ACK (Sans données) ───────────────────►│ (Reste en SYN_RECV ; AUCUN réveil│ (En sommeil / Repos)
  │                                           │                                  │
  │... (Délai : 500 ms) ......................│ (Empreinte de ~256 octets)       │ (Zéro cycle CPU perdu)
  │                                           │                                  │
  │─── PSH, ACK (Paquet TLS ClientHello) ────►│──► Promotion en socket complet   │
  │                                           │──► Placement dans Accept Queue   │
  │                                           │──► Réveil EPOLLIN ──────────────►│ (Réveil du thread)
  │                                           │◄── accept4() ────────────────────│
  │                                           │◄── recv() (Données reçues direct)│

Dès réception de cette charge utile, le noyau bascule la connexion vers l'état TCP_ESTABLISHED, instancie les structures mémoire complètes et l'intègre dans la file d'attente d'acceptation. Lorsque le thread utilisateur se réveille et appelle accept(), la charge utile est déjà présente dans le tampon de réception du socket. L'appel ultérieur à recv() délivre immédiatement les données sans temps mort.


Atténuation des attaques : Neutraliser Slowloris et les scans de ports passifs

Sur les infrastructures ouvertes à l'Internet, TCP_DEFER_ACCEPT constitue une barrière de protection native au niveau du noyau face aux attaques par déni de service applicatif et aux reconnaissances de réseau.

1. Absorption de l'épuisement des connexions dans les structures Request Sockets légères

Les attaques de type Slowloris ouvrent massivement des liaisons TCP sans émettre d'en-têtes de niveau applicatif. Sans TCP_DEFER_ACCEPT, un attaquant peut saturer les descripteurs d'un mandataire en utilisant une bande passante dérisoire.

Sous TCP_DEFER_ACCEPT, ces sessions inactives restent piégées dans la table request_sock du système d'exploitation et n'atteignent jamais l'application. Le processus mandataire demeure totalement disponible, préservant sa mémoire et sa puissance de calcul pour les clients réels qui échangent des données authentiques.

2. Purge automatique par expiration et retransmission SYN-ACK par le noyau

Le paramètre numérique fourni à TCP_DEFER_ACCEPT correspond à une durée maximale en secondes. Si un client distant finalise la négociation mais n'envoie aucune information avant ce délai, le noyau réémet un paquet SYN-ACK d'incitation.

Si le client persiste dans son mutisme au-delà du temps imparti, le noyau détruit silencieusement la structure request_sock. La session morte est supprimée de la mémoire sans provoquer de journalisation d'erreur superflue ni solliciter de procédure de nettoyage en espace utilisateur.


Schéma opérationnel : Configurer TCP_DEFER_ACCEPT dans les proxys en production

La mise en œuvre de TCP_DEFER_ACCEPT dans les relais VPN de production réclame un calibrage judicieux de la fenêtre d'attente, adossé à une boucle événementielle non bloquante :

/* Initialisation d'un écouteur réseau renforcé */
int setup_hardened_listener(int port, int defer_timeout) {
    int fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
    
    int optval = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));
    
    /* Activation du report d'acceptation avec temporisation (ex. 15-30 secondes) */
    setsockopt(fd, IPPROTO_TCP, TCP_DEFER_ACCEPT, &defer_timeout, sizeof(defer_timeout));
    
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(port);
    
    bind(fd, (struct sockaddr *)&addr, sizeof(addr));
    listen(fd, 4096);
    return fd;
}

Recommandations d'exploitation :

  • Calibrage temporel : Définir un paramètre defer_timeout compris entre 15 et 30 secondes sur les passerelles publiques. Une temporisation trop agressive (inférieure à 5 secondes) risque de couper indûment des liaisons régulières sur des réseaux mobiles à forte latence.
  • Conformité des flux clients : S'assurer que les protocoles cibles envoient leurs requêtes immédiatement après la poignée de main. Les normes TLS, HTTP, SSH et WireGuard encapsulé sur TCP respectent toutes ce schéma.

Synergies architecturales : TCP Fast Open, SYN Cookies et routage de bordure

TCP_DEFER_ACCEPT s'articule naturellement avec les autres mécanismes d'accélération et de défense du noyau Linux :

  • TCP SYN Cookies : Lors d'inondations SYN massives, le système bascule vers les SYN cookies sans état. Quand le client retourne son acquittement, TCP_DEFER_ACCEPT maintient le report d'allocation jusqu'à l'arrivée des données utiles, prolongeant la protection de la mémoire hôte au-delà du contrôle cryptographique.
  • Confrontation avec TCP Fast Open (TFO) : Tandis que TFO véhicule des données dès le paquet SYN (RFC 7413), TCP_DEFER_ACCEPT optimise les connexions tripartites habituelles où les données surviennent dans le troisième échange. L'association des deux approches assure une réactivité optimale quel que soit le profil des clients.

Chemin de données RiceGuard : Filtrage sans surcoût à la périphérie du réseau

VPNRice incorpore TCP_DEFER_ACCEPT comme composant fondamental de protection périmétrique au sein de ses passerelles RiceGuard. Dans la prise en charge d'un trafic mondial reliant des réseaux d'entreprise et des points de présence distribués, résister aux bruits de fond d'Internet et aux tentatives de blocage est une exigence absolue.

En appliquant le report de connexion sur tous les points d'entrée TCP, les relais RiceGuard assurent que les processus utilisateurs ne se mobilisent que pour des flux légitimes et porteurs de requêtes réelles. Les sessions fantômes, balayages de ports et connexions incomplètes sont neutralisés et détruits directement dans le noyau sans aucun coût de traitement applicatif. Cette défense échelonnée permet de préserver un débit constant, d'optimiser l'usage des ressources serveur et de garantir une confidentialité numérique irréprochable à chaque tunnel.