Skip to main content
Blog
Blog Learning

Corriger la gestion des timeouts TLS dans le service Edge de cside

Comment cside a amélioré la fiabilité TLS du service Edge en Rust en sortant le travail de handshake du chemin d'acceptation d'Axum vers des tâches Tokio bornées.

Jun 16, 2026 6 min read
Visualisation abstraite bleu foncé de connexions réseau traversant une passerelle circulaire

Chez cside, nous accordons une grande importance à la sûreté mémoire et à la performance. Tous nos services centraux sont écrits en Rust, y compris le service Edge.

L'Edge se situe à une frontière sensible. Il doit être rapide, sûr et résilient, car il fait partie du chemin où nous collectons des signaux de haute qualité pour des produits comme la détection de VPN et la détection de bot.

Pour de nombreuses applications web, la configuration TLS la plus simple consiste à laisser un répartiteur de charge cloud terminer TLS et transmettre du HTTP en clair à l'application. C'est un bon comportement par défaut lorsque l'application n'a besoin que de la requête HTTP finale.

L'Edge a un rôle différent.

Certains signaux de détection existent avant que la requête ne devienne du HTTP ordinaire, à la couche 4 (la couche transport) plutôt qu'à la couche 7. Si TLS est entièrement terminé avant que le trafic n'atteigne l'Edge, ces signaux ne sont plus disponibles sous la même forme. Pour cette partie de la plateforme, l'Edge doit donc opérer à la couche 4 et gérer le chemin TLS directement.

Cela donne à cside une meilleure qualité de signal pour la détection, mais cela signifie aussi que l'Edge doit gérer lui-même le comportement réel de TLS.

Cet article porte sur une petite correction de fiabilité dans ce chemin : sortir le travail de handshake TLS du chemin d'acceptation d'Axum pour le placer dans des tâches Tokio bornées.

Ce qui a commencé à échouer

Le correctif en lui-même n'était pas volumineux, mais comprendre la panne a demandé quelques recherches.

L'Edge était en bonne santé. Les certificats se chargeaient. Le port était joignable. La plupart du trafic se comportait normalement.

Mais sous une charge plus élevée, certaines vérifications HTTPS et connexions clientes semblaient se bloquer ou expirer.

Au premier abord, cela peut ressembler à un problème de TLS. En pratique, le schéma important était plus précis : certains clients ouvraient une connexion mais ne terminaient pas le handshake TLS.

C'est normal sur un service exposé à internet. Les points d'accès publics voient des connexions incomplètes en permanence :

le client se connecte, puis n'envoie rien
le client démarre TLS, puis disparaît
le client démarre TLS, puis se fige

Ces échecs n'étaient pas la partie surprenante.

La partie surprenante, c'était l'ampleur de l'impact qu'un seul handshake incomplet pouvait avoir sur le trafic sain à proximité.

Avant le correctif

Avant le correctif, une partie du chemin TLS de l'Edge faisait trop de travail en une seule étape.

Conceptuellement, son comportement était le suivant :

accepter une connexion
terminer le travail TLS pour cette connexion
puis accepter la connexion suivante

En Rust simplifié, la forme ressemblait à peu près à ceci :

impl axum::serve::Listener for EdgeTlsListener {
  async fn accept(&mut self) -> (TlsStream, SocketAddr) {
    let (mut tcp_stream, addr) = self.tcp_listener.accept().await;

    let hello = read_tls_hello(&mut tcp_stream).await;
    let tls_stream = complete_tls_handshake(hello, tcp_stream).await;

    (tls_stream, addr)
  }
}

Ce code est facile à appréhender, mais il place l'intégralité du handshake TLS à l'intérieur de accept().

Pour Axum, accept() est la porte d'entrée. S'il est occupé à attendre une connexion, le serveur ne reçoit pas la prochaine connexion terminée depuis ce listener.

Cela paraît simple, mais cela crée du head-of-line blocking.

Si une connexion démarrait TLS puis se figeait, l'Edge attendait le timeout de cette connexion avant de passer à la suite. Pendant cette attente, des connexions saines pouvaient se retrouver retardées derrière elle.

Le problème n'était pas :

TLS ne peut pas fonctionner

C'était :

un seul handshake TLS incomplet peut retarder des handshakes sains ultérieurs

Cette distinction était importante.

Augmenter le timeout n'aurait pas résolu le problème. Cela aurait simplement fait tenir la file plus longtemps au chemin lent.

Le correctif

Le correctif a consisté à séparer l'acceptation d'une connexion de la finalisation de son handshake TLS.

L'Edge accepte désormais rapidement les nouvelles connexions et traite chaque handshake TLS indépendamment. Un handshake figé ou incomplet peut toujours expirer, mais il n'empêche plus des connexions saines ultérieures de progresser.

Conceptuellement, le nouveau flux ressemble à ceci :

accepter rapidement les connexions
traiter chaque handshake TLS indépendamment
ne renvoyer au traitement des requêtes que les connexions sécurisées terminées

La nouvelle forme utilise des tâches Tokio ainsi qu'un channel de connexions sécurisées terminées :

let (ready_tx, ready_rx) = tokio::sync::mpsc::channel(limit);

tokio::spawn(async move {
  loop {
    let (tcp_stream, addr) = tcp_listener.accept().await;
    let ready_tx = ready_tx.clone();

    tokio::spawn(async move {
      if let Some(tls_stream) = finish_tls(tcp_stream).await {
        let _ = ready_tx.send((tls_stream, addr)).await;
      }
    });
  }
});

Le listener exposé à Axum devient alors beaucoup plus petit :

impl axum::serve::Listener for EdgeTlsListener {
  async fn accept(&mut self) -> (TlsStream, SocketAddr) {
    self.ready_rx
      .recv()
      .await
      .expect("TLS accept loop terminated")
  }
}

L'élément important, c'est la frontière : accept() ne réalise plus lui-même le travail lent de handshake. Il reçoit des handshakes qui sont déjà terminés.

Le mode de défaillance est ainsi passé de ceci :

un handshake figé
-> retarde la connexion suivante

à ceci :

un handshake figé
-> expire de façon indépendante
-> les connexions saines continuent

C'est là l'amélioration de fiabilité importante.

Le correctif n'a pas supprimé les timeouts. Les timeouts restent nécessaires. Un handshake incomplet ne devrait pas vivre éternellement.

Le correctif a changé l'endroit où le timeout est payé. Une mauvaise connexion paie désormais son propre timeout au lieu de le faire payer aux autres connexions.

Garder le tout borné

Il y a une seconde partie importante dans ce correctif.

Si chaque nouvelle connexion peut créer un travail illimité, le service devient réactif mais pas sûr sous pression. L'Edge borne donc aussi le nombre de handshakes TLS pouvant être en cours simultanément.

La version simplifiée ressemble à ceci :

let permits = Arc::new(tokio::sync::Semaphore::new(limit));

loop {
  let (tcp_stream, addr) = tcp_listener.accept().await;
  let permit = permits.clone().acquire_owned().await.expect("TLS semaphore closed");
  let ready_tx = ready_tx.clone();

  tokio::spawn(async move {
    let _permit = permit;

    if let Some(tls_stream) = finish_tls(tcp_stream).await {
      let _ = ready_tx.send((tls_stream, addr)).await;
    }
  });
}

Le permit appartient à la tâche. Lorsque la tâche se termine, Rust libère le permit et restitue de la capacité au sémaphore. Cela maintient la borne de concurrence liée à la durée de vie du travail de handshake réel.

Cela nous donne les deux propriétés que nous voulions :

les handshakes lents ne bloquent pas les handshakes sains

et :

les handshakes lents ne peuvent pas créer de travail illimité

C'est le genre de compromis qui nous tient à cœur dans l'Edge : améliorer la fiabilité sans renoncer à une utilisation prévisible des ressources.

Rendre les défaillances visibles

Nous avons aussi resserré un chemin de défaillance interne.

Si la partie de l'Edge chargée d'accepter les connexions sécurisées s'arrête un jour de façon inattendue, le service ne devrait pas attendre silencieusement pour toujours. Les blocages silencieux sont difficiles à exploiter et difficiles à appréhender.

Le channel rend cet état explicite. Si tous les expéditeurs ont disparu, la réception depuis le channel renvoie None. Cela ne devrait pas être traité comme une période d'inactivité normale ; le corps final de accept() remplace donc le précédent .expect() par une défaillance explicite et journalisée :

self.ready_rx.recv().await.unwrap_or_else(|| {
  tracing::error!("TLS accept loop terminated");
  panic!("TLS accept loop terminated")
})

Le chemin de défaillance devient désormais visible immédiatement au lieu de se transformer en une attente cachée.

Cela ne change rien au trafic client normal, mais cela rend le système plus facile à faire confiance pendant les incidents.

Ce que nous avons appris

La principale leçon, c'est que les timeouts ne suffisent pas si le timeout est payé au mauvais endroit.

Un timeout autour du travail TLS semble raisonnable. Mais si une connexion lente peut forcer des connexions sans rapport à attendre derrière elle, le timeout devient une souffrance partagée.

Le meilleur modèle est :

accepter rapidement
isoler le travail lent
borner la concurrence
rendre visibles les défaillances inattendues

Une autre leçon, c'est que les services exposés à internet devraient considérer les connexions incomplètes comme normales. Les clients se déconnectent. Les health checks réessaient. Les réseaux vacillent. Certains handshakes ne se terminent jamais.

L'Edge ne devrait pas présumer qu'internet est bien rangé.

Avant le correctif :

un handshake incomplet
-> le trafic sain à proximité peut attendre

Après le correctif :

un handshake incomplet
-> timeout isolé
-> le trafic sain continue

Le correctif final n'a pas fait disparaître les mauvaises connexions.

Il a fait en sorte que l'Edge les gère au bon endroit, avec les bonnes bornes, tout en préservant les garanties de performance et de sûreté mémoire que nous attendons de nos services en Rust.

Taym Haddadi
Software Engineer

Software engineer at cside, working on the Rust services behind the cside Edge.

FAQ

Frequently Asked Questions

Certains produits de détection de cside reposent sur des signaux de niveau connexion qui ne sont disponibles qu'avant qu'une requête ne soit aplatie en HTTP ordinaire par une terminaison TLS managée. L'Edge traite ce chemin directement afin que ces signaux puissent être utilisés de façon sûre et cohérente.

Non. La gestion de TLS reste à l'intérieur du service Edge. Le changement a amélioré la manière dont les handshakes incomplets sont isolés, afin qu'ils n'affectent pas le trafic sain.

Surveillez et sécurisez vos scripts tiers

Gain full visibility and control over every script delivered to your users to enhance site security and performance.

Commencez gratuitement, ou essayez Business avec un essai de 14 jours.

cside Interface du tableau de bord affichant la surveillance des scripts et les analyses de sécurité
Related Articles
Réserver une démonstration