Des articles approfondis sur les technologies qui façonnent l'avenir.

IRCv3 : leçons de design de protocole rétrocompatible

IRCv3 modernise un protocole de 1988 sans le casser. Négociation de capacités, message tags et évolution rétrocompatible expliquées en détail.

Un vieux télégraphe relié par un câble lumineux à un smartphone moderne posé sur un bureau
Le tour de force d'IRCv3 : le format de 1988 fonctionne toujours, mais les clients modernes profitent d'une expérience moderne.

En 1988, un étudiant finlandais a écrit un protocole de chat tenant sur quelques pages. Presque quatre décennies plus tard, ce même protocole, IRC, transporte encore des millions de messages par jour, et le groupe de travail IRCv3 nous apprend discrètement à faire évoluer un protocole sans ruiner la base installée. Si vous avez déjà essayé de versionner une API dont dépendent des inconnus, IRCv3 est l'étude de cas à piquer sans vergogne.

Je construis des bots et des clients expérimentaux pour IRC depuis des années, et ce qui continue de me surprendre, c'est à quel point l'expérience peut paraître moderne quand les deux extrémités parlent les extensions récentes. Historique rejoué par le serveur. Vraie authentification. Horodatages qui survivent à un bouncer. Et pourtant, un client écrit en 1995 peut toujours se connecter au même serveur et discuter tranquillement. Cette combinaison n'arrive pas par hasard : c'est le résultat d'un ensemble de choix de conception délibérés, et de quelques erreurs douloureuses que la communauté a dû contourner.

Le péché originel : des capacités impossibles à interroger

Le protocole IRC de base, tel que codifié dans la RFC 1459 puis la RFC 2812, n'avait aucun mécanisme permettant à un client de demander à un serveur ce qu'il supporte. Aucun. Vous vous connectiez, envoyiez NICK et USER, puis vous deviniez. Vous voulez savoir si le serveur gère tel mode ou telle extension ? Envoyez-le et voyez si une erreur arrive. Cela semble pittoresque jusqu'au jour où vous maintenez une bibliothèque cliente obligée de traiter à part le comportement de cinq implémentations de serveurs, chacune interprétant légèrement différemment les parties ambiguës de la spécification.

Le problème n'est pas théorique. Des développeurs écrivaient des clients qui sondaient les fonctionnalités en envoyant des commandes et en analysant les numériques d'erreur, un bricolage fragile qui cassait dès qu'un serveur modifiait une chaîne d'erreur. Les bots rejoignaient des canaux avec de mauvaises hypothèses et inondaient les logs d'échecs. C'est l'écart entre spécification et implémentation dans son milieu naturel : quand la spec ne fournit pas de mécanisme de découverte, chaque implémenteur en invente un, et mal.

La solution d'IRCv3 était la négociation de capacités, et elle est si simple que c'en est presque gênant. Pendant l'enregistrement de la connexion, le client envoie CAP LS et le serveur répond avec la liste des capacités nommées qu'il supporte, par exemple sasl, server-time ou message-tags. Le client envoie alors CAP REQ avec exactement le sous-ensemble souhaité, le serveur acquitte, et les deux parties poursuivent avec une compréhension partagée et explicite de ce que supporte la session. Un client historique qui n'envoie jamais CAP obtient simplement l'ancien comportement. Un serveur historique qui ne comprend pas CAP renvoie une erreur que le client peut ignorer sans risque et à laquelle il peut revenir.

-> CAP LS 302
<- CAP * LS :sasl=PLAIN,EXTERNAL server-time message-tags echo-message
-> CAP REQ :sasl server-time
<- CAP * ACK :sasl server-time
-> AUTHENTICATE PLAIN
<- AUTHENTICATE +
-> AUTHENTICATE dXNlcgB1c2VyAHBhc3N3b3Jk
<- 900 jane jane!jane@host jane :You are now logged in

La leçon dépasse largement IRC. Si vous concevez aujourd'hui un protocole ou une API, intégrez le mécanisme de négociation dès la version un, même s'il n'y a encore rien à négocier. Le coût se résume à un échange de messages ; le bénéfice, c'est que chaque future fonctionnalité devient optionnelle au lieu d'être un changement cassant.

Leçon : l'extensibilité vaut mieux que les nouvelles commandes

La deuxième grande idée d'IRCv3, ce sont les message tags, et elle vient d'une vraie erreur : on essayait sans cesse d'ajouter des fonctionnalités en définissant de nouvelles commandes ou en entassant de la sémantique dans les paramètres existants. Le format de message d'IRC est réputé pour sa concision : un préfixe, une commande et une liste de paramètres finaux. Il n'y avait nulle part où accrocher des métadonnées. Donc, quand on voulait des horodatages sur l'historique rejoué, ou indiquer quel compte avait envoyé un message, la réponse à l'ancienne consistait à inventer un nouveau numérique ou à tripoter le texte final. Les deux approches cassaient les anciens clients ou obligeaient chaque analyseur à grossir de cas particuliers.

La solution a été d'ajouter une nouvelle section facultative au début du message : @clé=valeur;clé2=valeur2 avant le préfixe. Point crucial : les serveurs n'envoient des tags qu'aux clients ayant négocié la capacité message-tags, donc les anciens clients ne voient jamais un octet qu'ils ne savent pas analyser. Voici à quoi ressemble réellement un message horodaté sur le fil :

@time=2024-03-14T09:26:00.000Z;account=jane :jane!jane@host PRIVMSG #rust :borrow checker finally clicked for me

C'est le même geste architectural que les en-têtes HTTP, les métadonnées gRPC ou la tolérance aux champs inconnus de protobuf : réserver un point d'extension générique, et les fonctionnalités cessent de nécessiter une chirurgie du protocole. L'extension server-time, qui règle le vieux problème du bouncer où tous les messages rejoués semblent venir d'arriver à l'instant, n'est littéralement qu'un tag. Le brouillon +reply, qui permet aux clients de chaîner les réponses à des messages précis, en est un autre. Pas de nouvelle commande. Pas de nouveau numérique. Pas de journée J de migration.

J'ai moi-même fait l'erreur inverse. Sur un système interne de pub/sub, il y a quelques années, nous avons ajouté une fonctionnalité en surchargeant un champ JSON avec un préfixe magique, parce que modifier le format de l'enveloppe « demandait trop de travail ». Huit mois plus tard, nous maintenions une expression régulière dans trois langages pour déballer la magie, et le parseur d'une équipe traitait la chaîne magique comme une donnée littérale, ce qui a produit un incident mémorable. Un champ de métadonnées bête et explicite aurait coûté une journée. La chaîne magique nous a coûté des trimestres. Les message tags d'IRCv3 sont exactement ce que ce système aurait dû avoir dès le départ.

Des enveloppes sur une chaîne de convoyage, certaines portant de petites étiquettes colorées de métadonnées
Message tags : des métadonnées facultatives attachées à chaque message, invisibles pour les clients qui ne les ont pas demandées.

Leçon : en finir avec le mot de passe en clair comme contournement

Demandez à quiconque utilisait un client IRC dans les années 2000 comment fonctionnait la connexion : vous tapiez /msg NickServ IDENTIFY motdepasse. Vos identifiants partaient comme un message de chat ordinaire, visibles dans les journaux, parfois collés dans un canal public quand votre client se trompait de commande, un rite de passage si courant qu'il est devenu un mème. L'authentification avait été ajoutée au-dessus du protocole, parce que celui-ci n'avait aucune notion de la chose.

IRCv3 a corrigé cela proprement en standardisant l'authentification SASL pendant l'enregistrement. Au lieu de discuter avec un bot après la connexion, le client négocie la capacité sasl et s'authentifie dans le cadre de la poignée de main, avant même d'avoir un pseudo visible par qui que ce soit. Le serveur sait qui vous êtes dès le premier message, ce qui entraîne tout le reste : l'extension account-tag peut attacher le nom de votre compte à chaque message, les bots peuvent prendre des décisions d'autorisation sans leurs propres heuristiques d'analyse de NickServ, et les clients peuvent construire des parcours d'inscription qui n'exigent pas de lire un mur de texte NOTICE.

La leçon plus large : quand un contournement devient universel, il cesse d'être un contournement et devient votre protocole de fait, sans aucun des soins de conception. NickServ ne figurait dans aucune RFC, mais il a façonné deux décennies de comportements clients, y compris leurs modes de défaillance. Standardiser ce que les gens faisaient déjà (s'authentifier à la connexion) avec un vrai mécanisme (SASL, qui apporte PLAIN, EXTERNAL pour les certificats clients, et de la place pour d'autres) a transformé le folklore en infrastructure. Si vos utilisateurs exécutent tous la même danse maladroite pour accomplir une tâche, c'est votre prochain élément de spécification, pas une FAQ de support.

Leçon : l'historique est un problème de protocole, pas de client

Pendant la majeure partie de la vie d'IRC, le protocole était strictement en direct : si votre client n'était pas connecté au moment de l'envoi, le message n'existait tout simplement pas pour vous. La réponse de la communauté a été le bouncer, un proxy connecté en permanence (ZNC en est l'exemple classique) qui tournait sur un VPS, gardait votre session et rejouait ce que vous aviez manqué. Les bouncers fonctionnaient, mais c'était une infrastructure de plus à faire tourner, un identifiant de plus à gérer, et l'expérience de rejeu était exécrable : tout arrivait estampillé à l'heure du rejeu, dans le désordre par rapport à ce que vous aviez vu en direct.

IRCv3 a attaqué le problème par couches. D'abord, server-time a rendu l'historique rejoué exploitable en transportant l'horodatage réel de réception. Ensuite, le brouillon chathistory a donné aux clients un moyen standard de demander des plages d'historique, et des serveurs comme Ergo ont commencé à stocker directement l'historique des canaux. Le résultat, comme beaucoup l'ont découvert en montant un petit serveur Ergo pour leurs amis, est saisissant : vous vous connectez depuis votre téléphone après une semaine hors ligne, et le serveur rejoue tout avec les bons horodatages, sans bouncer ni VPS à surveiller. C'est le plus gros bond de confort d'utilisation qu'IRC ait jamais connu, et il vient du protocole, pas d'une astuce de client.

Il existe un contre-argument qui mérite d'être pris au sérieux, et les utilisateurs quotidiens d'IRC le font : la plupart des aspérités perçues d'IRC sont côté client, et l'on peut obtenir une bonne expérience aujourd'hui avec un client correct et un bouncer. C'est vrai, mais cet argument prouve le point dans l'autre sens. Chaque auteur de client a réimplémenté indépendamment la recomposition du défilement, le suivi des mentions et la déduplication au rejeu, avec des bogues légèrement différents. Déplacer la partie difficile dans le protocole et le serveur signifie qu'elle n'est implémentée qu'une fois, correctement, et que chaque client en profite. Nous faisons ce même choix en développement web en permanence : pousser les invariants dans la plateforme (pensez à la quantité de code applicatif qui a disparu quand les navigateurs ont intégré fetch, ou quand TLS est devenu incontournable) plutôt que de laisser mille codebases résoudre le problème à moitié.

Leçon : les fonctionnalités facultatives créent des réalités divergentes

Voici maintenant l'erreur qu'IRCv3 n'a pas entièrement résolue, et c'est celle qui concerne le plus quiconque conçoit des systèmes extensibles. Comme chaque extension est facultative et négociée indépendamment, deux utilisateurs du même canal peuvent littéralement voir deux conversations différentes. Un client reçoit des messages avec des tags +reply et les affiche en réponses imbriquées ; un autre montre ces mêmes messages sous forme de lignes plates, sans contexte. Un utilisateur voit les réactions emoji sous forme de jolis symboles attachés à un message ; un autre ne voit rien du tout, ou un repli cryptique. La matrice de support des clients IRCv3 est une grille de oui et de non, et votre expérience dépend de la colonne où vous vous trouvez.

C'est le revers de la dégradation gracieuse. La rétrocompatibilité garantit que rien ne casse, mais rien dans le modèle ne garantit la cohérence. Slack et Discord, quoi qu'on pense par ailleurs, n'ont jamais ce problème : la plateforme contrôle les deux extrémités assez étroitement pour qu'une réaction soit une réaction pour tout le monde. IRCv3 a choisi l'évolution plutôt que le contrôle, et le prix à payer est un paysage fonctionnel durablement inégal, où les spécifications en brouillon restent parfois en brouillon pendant des années.

Si vous construisez quelque chose d'extensible, prévoyez-le dès le premier jour. Voici quelques parades qu'IRCv3 et des écosystèmes similaires ont retenues :

  • Concevez les dégradations délibérément. Un tag de réponse doit retomber sur une citation lisible, pas disparaître. Si le repli est inutilisable, la fonctionnalité n'est pas prête à être livrée en option.
  • Publiez la matrice de support et gardez-la honnête. Les tableaux clients et serveurs d'IRCv3 permettent aux utilisateurs de faire des choix éclairés ; le silence attire le blâme sur le mauvais logiciel.
  • Définissez une base commune et faites-la progresser. Les versions numérotées (3.1, 3.2) regroupent des capacités pour que les clients visent « le support 3.2 » plutôt que onze drapeaux individuels. Donnez à votre écosystème un plancher vers lequel converger.
  • Faites expirer vos brouillons. Un brouillon livré dans les serveurs mais jamais finalisé (l'historique des canaux a flirté avec ce sort) est pire que pas de brouillon du tout, car il fait diverger le comportement sans jamais s'engager.

Nous avons vu une version de cela dans l'article sur pourquoi l'édition collaborative est plus difficile qu'on le pense : dès que des participants peuvent être en désaccord sur la forme de l'état partagé, il faut des règles de convergence explicites, pas seulement de bonnes intentions. Faire des capacités des options par défaut est une bonne stratégie, mais la sémantique visible par l'utilisateur doit prévoir un plan pour ceux qui se retrouvent du mauvais côté de la matrice.

Deux fenêtres stylisées affichant le même flux, l'une riche en fils de discussion et en symboles, l'autre épurée
Les extensions facultatives signifient que deux utilisateurs peuvent suivre le même canal et voir des conversations différentes.

Ce que cela implique pour votre prochaine API ou ce prochain protocole

Une fois la nostalgie du chat écartée, IRCv3 est un guide compact pour faire évoluer n'importe quoi doté d'un format sur le fil. Voici la liste de contrôle que je remettrais à toute équipe qui démarre un protocole, un schéma d'événements, ou même une API REST publique :

  1. Livrez la négociation en premier. Un échange de capacités dès la version un, même avec une liste vide. Ajouter la découverte après coup est dix fois plus difficile.
  2. Réservez un point d'extension. Tags, en-têtes, tables de métadonnées : un emplacement générique auquel les futures fonctionnalités peuvent s'accrocher sans toucher à la grammaire de base.
  3. Rendez les nouveaux comportements optionnels par connexion. Les anciens clients doivent recevoir exactement l'ancien comportement, octet pour octet. Les serveurs IRCv3 n'envoient jamais un tag à un client qui ne l'a pas demandé.
  4. Standardisez le folklore. Quel que soit le bricolage sur lequel vos utilisateurs ont convergé (authentification façon NickServ, rejeu façon bouncer), c'est une spécification qui attend d'être écrite proprement.
  5. Planifiez la matrice de support. Les fonctionnalités optionnelles divergent ; décidez à quoi ressemblent les expériences dégradées et publiez qui supporte quoi.
  6. Regroupez les progrès en jalons nommés. Les drapeaux individuels sont pour les implémenteurs ; les versions numérotées sont pour les humains.

Ce qui me frappe vraiment, c'est la retenue. IRCv3 n'a couru après ni le chiffrement de bout en bout, ni une refonte de la fédération, ni une grande réécriture. Il a choisi les problèmes petits et tranchants (découverte, métadonnées, authentification, temps) et les a résolus de façon qu'un client de 1995 puisse les ignorer. C'est pour cela qu'IRC est pratiquement le seul survivant de sa génération de protocoles internet à tourner encore largement tel qu'il a été conçu, et pourquoi un projet de week-end aujourd'hui peut offrir à un groupe d'amis la synchronisation de l'historique, TLS et de vrais comptes sur un VPS à 5 dollars.

La rétrocompatibilité n'est pas l'absence de changement. C'est la discipline qui consiste à faire de chaque changement quelque chose que le passé peut ignorer sans risque.

La prochaine fois que vous serez tenté de versionner une API en passant du chemin /v1 à /v2 et en forçant chaque consommateur à migrer, pensez au protocole qui a permis à un client de l'ère du modem et à une application mobile moderne de partager le même canal. Négociez les capacités. Attachez des métadonnées au lieu d'inventer des commandes. Gardez le plancher solide et laissez le plafond monter à son propre rythme. IRCv3 prouve qu'on peut avoir un protocole qui respecte ses aînés tout en apprenant de nouveaux tours, et si un système de chat de 1988 y parvient, votre microservice de trois ans n'a aucune excuse.