Résumé
- En TLS 1.3,
close_notifyest une déclaration authentifiée et directionnelle : son émetteur n’enverra plus de messages TLS sur cette connexion. L’alerte ne ferme pas l’autre sens et n’acquitte ni requête, ni réponse, ni effet durable. - Un EOF de transport arrivé sans cette alerte laisse une ambiguïté de troncature. Il ne peut être assimilé à une fin propre que si le protocole applicatif possède une règle indépendante de complétude et que l’application l’a effectivement vérifiée.
- Une décision de reprise ou d’attribution exige quatre frontières distinctes : fin du message applicatif, alertes TLS envoyée et reçue, terminaison du transport, puis résultat durable. Le booléen « fermé » n’a pas cette autorité.
Le journal disait « succès »
La chronologie de l’incident paraissait rassurante. La réponse HTTP de succès avait été écrite, le wrapper TLS avait exécuté son arrêt local et le journal portait shutdown=success. Les opérateurs ont d’abord lu ce champ comme la preuve d’une transaction accomplie.
Le serveur avait toutefois construit sa réponse avant le commit définitif. Il a envoyé les enregistrements correspondants, puis close_notify. Quelques millisecondes plus tard, une contrainte d’unicité a rejeté l’écriture en base. Le client possédait une fin TLS authentique ; il ne possédait aucun identifiant de commit.
La panne de raisonnement vient de là. L’alerte ordonnait la fin du flux TLS du serveur après les données que celui-ci avait choisi d’émettre. Elle ne décrivait ni la consommation de ces données par l’application distante, ni l’état d’un registre métier situé derrière le processus.
Une connexion a deux fins
TLS 1.3 ferme les directions séparément. Celui qui envoie close_notify promet de ne plus émettre de message TLS sur cette connexion. Les données reçues après cette alerte doivent être ignorées. En revanche, son côté lecture peut rester actif : le pair peut encore avoir un dernier message à transmettre avant de fermer à son tour.
Chaque partie doit envoyer l’alerte avant de fermer son écriture, sauf si elle a déjà émis une alerte d’erreur. TLS 1.3 n’exige plus la réponse immédiate qui, dans des versions anciennes, pouvait supprimer des écritures encore en attente. Une fermeture régulière n’est donc pas un instant global, mais deux décisions ordonnées.
TCP offre lui aussi une demi-fermeture, avec une autre valeur probante. Un FIN borne un sens du flot d’octets. Il n’est pas une alerte TLS authentifiée, ne désigne pas le dernier enregistrement complet et ne connaît pas la structure du message applicatif.
L’EOF n’est pas l’alerte
Lorsque le transport disparaît avant close_notify, le destinataire ne sait pas si toutes les données prévues par l’émetteur lui sont parvenues. Cette absence ne prouve pas une attaque : un processus peut tomber, un proxy peut expirer ou un pair ancien peut fermer seulement TCP. Elle signifie que la borne cryptographique la plus forte manque.
Il faut aussi distinguer l’alerte fatale, le reset, le délai dépassé, l’EOF inattendu, l’alerte de fermeture reçue et l’essai local d’envoi. Les réunir sous « déconnecté » détruit l’information nécessaire à l’enquête.
Le registre IANA attribue la valeur 0 à close_notify. Cette référence permet d’interpréter le code d’alerte. Elle ne prouve pas qu’un processus de production l’a émis, que le pair l’a reçu, ni que les octets précédents formaient une réponse complète.
La complétude appartient au protocole supérieur
Une alerte reçue après plusieurs enregistrements protégés prouve leur ordre avant la fin TLS de l’émetteur. TLS voit toutefois leur contenu comme une suite opaque. Il ignore si elle contient un document entier, un délimiteur terminal, un accusé, un reçu de paiement ou la moitié d’une structure.
Le protocole applicatif doit donc fournir sa propre borne : longueur annoncée, chunk terminal, FIN de flux, code final, identifiant de requête, acquittement ou inscription durable. Ces éléments ne sont pas interchangeables. Une réponse complète peut encore annoncer une opération finalement refusée ; une opération peut aussi être commise sans que la réponse parvienne au client.
Les buffers ajoutent une frontière. Accepter un appel d’écriture peut seulement placer des octets dans TLS ou dans un BIO. En mode non bloquant, l’alerte peut être marquée comme tentée avant sa sortie effective sur le réseau. Ni l’un ni l’autre n’établit un commit en base.
Lire les retours OpenSSL comme des états
SSL_shutdown() ne renvoie pas un simple verdict. Un résultat 0 signifie normalement que l’alerte locale a été envoyée tandis que celle du pair reste attendue. Ce n’est ni une erreur, ni la preuve d’une fermeture bidirectionnelle. Le résultat 1 indique que les deux alertes ont été envoyée et reçue.
Le premier passage ferme l’écriture TLS, pas la lecture, et laisse le socket TCP ouvert. OpenSSL recommande de continuer à lire afin de traiter les dernières données et les messages postérieurs au handshake. Relancer l’arrêt sans vider les données applicatives en attente peut échouer.
Les indicateurs internes doivent rester nommés. L’état « sent » suit la tentative d’envoi ; l’état « received » suit l’alerte du pair. Le mode quiet marque localement la connexion fermée sans émettre l’alerte et n’est pas conforme. Depuis OpenSSL 3.0, l’EOF inattendu est une erreur significative. L’option qui l’ignore n’est défendable que si l’application détecte elle-même toute troncature.
GnuTLS fait un choix analogue entre GNUTLS_SHUT_WR, qui ferme l’émission, et GNUTLS_SHUT_RDWR, qui attend aussi le pair. BoringSSL conserve lui aussi des états de lecture et d’écriture séparés. Ces API rendent la distinction possible ; elles ne garantissent pas que la télémétrie de la flotte la conserve.
HTTP rend l’erreur visible
HTTP/1.1 possède des règles de complétude. Avec Content-Length, tous les octets annoncés doivent être reçus. Avec le codage chunked, le chunk nul terminal est indispensable. Si l’un manque, la fermeture de connexion ne transforme pas le message en réponse complète.
Le cas délimité uniquement par la fermeture est plus fragile. Sa borne dépend de la fin de connexion ; une fin TLS incomplète rend donc la réponse incomplète. RFC 9112 préfère les longueurs ou encodages explicites précisément parce qu’une panne réseau peut imiter une fin normale.
À l’inverse, un message dont la longueur ou le chunk terminal a déjà été validé garde cette preuve même si l’alerte du pair manque ensuite. C’est le seul fondement sérieux pour une politique de compatibilité face à l’EOF : chaque message utilisé doit posséder une borne supérieure vérifiée.
Une fermeture multiplexée ne nomme pas la requête
HTTP/2 place de nombreux flux sur une connexion TLS. close_notify n’indique pas lesquels ont commencé à être traités. GOAWAY fournit une limite de numéro de flux : les identifiants supérieurs peuvent être retentés, tandis que ceux situés dans la plage restent potentiellement exécutés.
Sans GOAWAY, un POST non idempotent en vol demeure ambigu, même si la fin de connexion semble régulière. HTTP/3 garde cette idée au-dessus de QUIC : sa propre trame GOAWAY borne les requêtes acceptées. Une fermeture de connexion ne fabrique jamais un acquittement par requête.
Retenter tout ce qui précédait une fermeture « propre » peut doubler un paiement ou un message. Ne rien retenter peut perdre le travail. La décision dépend des méthodes, clés d’idempotence, recherches de résultat et preuves métier, non de l’adjectif appliqué au socket.
QUIC exige un vocabulaire différent
QUIC transporte les octets du handshake TLS mais protège ses données applicatives avec ses propres mécanismes. Les alertes TLS y deviennent des erreurs de connexion, tandis que QUIC dispose de CONNECTION_CLOSE, FIN de flux, reset, fermeture et drainage. Les sémantiques de warning de close_notify ne s’y transposent pas.
HTTP/3 ajoute GOAWAY parce qu’une fermeture QUIC reste incapable d’indiquer seule quelles requêtes ont été acceptées. Les mesures doivent donc nommer le porteur exact : alerte TLS, FIN ou RST TCP, FIN de flux QUIC, CONNECTION_CLOSE, délai inactif, GOAWAY HTTP ou acquittement applicatif.
Construire un registre des fins
Pour chaque connexion, conserver le rôle de l’extrémité, la direction, la version TLS, l’identifiant de corrélation et le dernier message ou flux complet. Enregistrer la règle de cadrage, les tailles attendue et reçue, l’identifiant de requête, la clé d’idempotence et la classe de reprise.
Côté TLS, distinguer l’alerte locale mise en file, effectivement expédiée et reçue par le pair ; garder la suite des retours de bibliothèque, les états lecture/écriture, les buffers restants et la classification exacte des erreurs. Documenter les modes quiet, l’EOF ignoré, les versions, kTLS et les proxys.
Côté transport, séparer FIN, RST, EOF, délai et demi-fermeture. Côté application, conserver l’acquittement, l’identifiant de commit, l’horodatage durable et l’autorité qui les produit. La causalité doit pouvoir être reconstruite, pas seulement le dernier état affiché.
Les tests négatifs doivent couper TCP avant l’alerte, supprimer le chunk final, fermer après une trame complète mais avant le commit, observer le retour 0, laisser des données du pair en attente, activer quiet shutdown, alterner la politique d’EOF et terminer HTTP/2 ou HTTP/3 avec une opération non idempotente en vol.
Sources
- https://www.rfc-editor.org/rfc/rfc9846.html
- https://www.rfc-editor.org/rfc/rfc9293.html
- https://www.rfc-editor.org/rfc/rfc9112.html
- https://www.rfc-editor.org/rfc/rfc9113.html
- https://www.rfc-editor.org/rfc/rfc9000.html
- https://www.rfc-editor.org/rfc/rfc9001.html
- https://www.rfc-editor.org/rfc/rfc9114.html
- https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml
- https://docs.openssl.org/3.5/man3/SSL_shutdown/
- https://docs.openssl.org/master/man3/SSL_get_error/
- https://docs.openssl.org/master/man3/SSL_CTX_set_options/
- https://www.gnutls.org/manual/html_node/Core-TLS-API.html
- https://boringssl.googlesource.com/boringssl/+/refs/heads/main/ssl/ssl_lib.cc
- https://heng.lu/running-code-primary-the-patch-needed-to-preserve-the-internet-original-design/
Briefing des membres
Contexte approfondi du profil
Connectez-vous avec le bon niveau d'adhésion pour débloquer le briefing complet et les notes de source.
Réservé à Strategic Circle
Strategic Circle
Ouvert à tous les lecteurs. Débloquez les briefings de profil après adhésion et connexion.
Rejoindre Strategic CircleRéservé aux membres de Leadership Alliance
Leadership Alliance
Réservé aux propriétaires et dirigeants qualifiés d'actifs IP ; connectez-vous pour débloquer les briefings Alliance.
Rejoindre Leadership Alliance