Fundierte Artikel über Technologien, die das Kommende formen.

IRCv3: Lektionen in abwärtskompatiblem Protokolldesign

IRCv3 modernisiert ein Protokoll von 1988, ohne es zu brechen. Lektionen zu Capability-Negotiation, Message Tags und Protokollevolution.

Ein altes Telegrafengerät, verbunden über ein leuchtendes Kabel mit einem modernen Smartphone auf einem Schreibtisch
Der Trick von IRCv3: Das Wire-Format von 1988 funktioniert immer noch, aber moderne Clients bekommen ein modernes Erlebnis.

1988 schrieb ein finnischer Universitätsstudent ein Chat-Protokoll, das auf ein paar Seiten passte. Knapp vier Jahrzehnte später transportiert dasselbe Protokoll – IRC – immer noch Millionen Nachrichten am Tag, und die IRCv3-Arbeitsgruppe bringt uns allen still und leise bei, wie man ein Protokoll weiterentwickelt, ohne die installierte Basis in Schutt und Asche zu legen. Wenn du schon mal versucht hast, eine API zu versionieren, auf die fremde Leute angewiesen sind, ist IRCv3 die Fallstudie, von der du dir etwas abschauen solltest.

Ich baue seit Jahren immer wieder Bots und kleine Clients für IRC, und was mich immer wieder überrascht, ist, wie modern sich das Erlebnis anfühlen kann, wenn beide Seiten die neueren Erweiterungen sprechen. Vom Server wiedergegebene Historie. Echte Authentifizierung. Nachrichtenzeitstempel, die einen Bouncer überleben. Und trotzdem kann ein Client von 1995 immer noch auf denselben Server verbinden und chatten. Diese Kombination passiert nicht zufällig. Sie ist das Ergebnis einer Reihe bewusster Designentscheidungen und einiger schmerzhafter Fehler, die die Community umschiffen musste.

Die Ursünde: Fähigkeiten, nach denen man nicht fragen kann

Das IRC-Kernprotokoll, wie es in RFC 1459 und später RFC 2812 festgehalten wurde, hatte keinen Mechanismus, mit dem ein Client einen Server fragen konnte, was er unterstützt. Gar keinen. Du hast dich verbunden, NICK und USER geschickt und dann geraten. Willst du wissen, ob der Server einen bestimmten Modus oder eine Erweiterung unterstützt? Schick ihn und schau, ob ein Fehler zurückkommt. Das klingt harmlos, bis du eine Client-Bibliothek pflegst, die das Verhalten für fünf verschiedene Serverimplementierungen einzeln abfangen muss, die jeweils die mehrdeutigen Stellen der Spezifikation etwas anders interpretieren.

Der Fehlermodus ist nicht theoretisch. Entwickler schrieben Clients, die Features ermittelten, indem sie Befehle abfeuerten und Fehler-Numerics parsten – ein fragiles Durcheinander, das jedes Mal kaputtging, wenn ein Server einen Fehlertext änderte. Bots traten Channels mit falschen Annahmen bei und fluteten die Logs mit Fehlermeldungen. Das ist die Lücke zwischen Spezifikation und Implementierung in freier Wildbahn: Wenn die Spezifikation keinen Entdeckungsmechanismus vorsieht, erfindet jeder Implementierer einen eigenen, und zwar schlecht.

IRCv3s Lösung war Capability-Negotiation, und sie ist so simpel, dass es fast peinlich ist. Während der Verbindungsregistrierung schickt der Client CAP LS, und der Server antwortet mit einer Liste benannter Fähigkeiten, die er unterstützt, etwa sasl, server-time oder message-tags. Der Client schickt dann CAP REQ mit genau der Teilmenge, die er möchte, der Server bestätigt, und beide Seiten arbeiten mit einem gemeinsamen, expliziten Verständnis davon, was die Sitzung unterstützt. Ein Legacy-Client, der nie CAP sendet, bekommt einfach das alte Verhalten. Ein Legacy-Server, der CAP nicht kennt, antwortet mit einem Fehler, den der Client gefahrlos ignorieren und auf den Fallback zurückfallen kann.

-> 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

Die Lektion geht weit über IRC hinaus. Wenn du heute irgendein Protokoll oder eine API entwirfst, bau den Verhandlungsmechanismus schon in Version eins ein, auch wenn es noch nichts zu verhandeln gibt. Die Kosten sind ein einziger Nachrichtenaustausch; der Gewinn ist, dass jedes künftige Feature opt-in ist statt eine Breaking Change.

Lektion: Erweiterbarkeit schlägt neue Befehle

Die zweite große Idee in IRCv3 sind Message Tags, und sie entstand aus einem echten Fehler: Leute versuchten immer wieder, Features durch neue Befehle oder durch das Hineinpressen von Semantik in bestehende Nachrichtenparameter zu ergänzen. Das IRC-Wire-Format ist berüchtigt knapp: ein Präfix, ein Befehl und eine Liste von Trailing-Parametern. Es gab keinen Ort, an dem man Metadaten unterbringen konnte. Wollte jemand also Zeitstempel für wiedergegebene Historie oder markieren, welcher Account eine Nachricht geschickt hat, lautete die altmodische Antwort, ein neues Numeric zu erfinden oder den Trailing-Text zu verbiegen. Beide Ansätze brechen ältere Clients oder zwingen jeden Parser, Sonderfälle anzuhäufen.

Die Lösung war, am Anfang der Nachricht einen neuen, optionalen Abschnitt hinzuzufügen: @key=value;key2=value2 vor dem Präfix. Entscheidend ist, dass Server Tags nur an Clients senden, die die Capability message-tags ausgehandelt haben, sodass alte Clients nie ein Byte zu sehen bekommen, das sie nicht parsen können. So sieht eine zeitgestempelte Nachricht auf dem Draht tatsächlich aus:

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

Das ist derselbe architektonische Schritt wie bei HTTP-Headern, gRPC-Metadaten oder der Toleranz von Protobuf gegenüber unbekannten Feldern: Man reserviert einen generischen Erweiterungspunkt, und Features müssen nicht mehr am Protokollkern operieren. Die Erweiterung server-time, die das uralte Bouncer-Problem löst, bei dem wiedergegebene Nachrichten alle so aussehen, als wären sie gerade eben eingetroffen, besteht buchstäblich aus einem einzigen Tag. Das +reply-Draft, mit dem Clients Antworten auf bestimmte Nachrichten verketten können, ist ein weiterer Tag. Keine neuen Befehle. Keine neuen Numerics. Kein Flag-Day für die Versionierung.

Ich habe selbst den gegenteiligen Fehler gemacht. In einem internen Pub/Sub-System haben wir vor Jahren ein Feature hinzugefügt, indem wir ein JSON-Feld mit einem magischen Präfix überladen haben, weil das Ändern des Envelope-Formats "zu viel Arbeit" war. Acht Monate später pflegten wir einen Regex in drei Sprachen, um die Magie wieder auszupacken, und das Parsing eines Teams behandelte den magischen String als Literaldaten, was einen denkwürdigen Vorfall auslöste. Ein simples, explizites Metadatenfeld hätte einen Tag gekostet. Der magische String kostete uns Quartale. Die Message Tags von IRCv3 sind genau das, was dieses System von Anfang an hätte haben sollen.

Umschläge auf einem Förderband, einige mit kleinen, bunten Metadaten-Aufklebern
Message Tags: optionale Metadaten pro Nachricht, unsichtbar für Clients, die nie danach gefragt haben.

Lektion: Schluss mit dem Passwort-im-Klartext-Workaround

Frag jemanden, der in den 2000ern einen IRC-Client benutzt hat, wie das Login funktionierte, und du bekommst dieselbe Antwort: /msg NickServ IDENTIFY passwort. Deine Zugangsdaten gingen als ganz normale Chatnachricht raus, waren in Logs sichtbar und landeten gelegentlich in einem öffentlichen Channel, wenn dein Client den Befehl verhunzte. Das war ein Initiationsritus, der so verbreitet war, dass er zum Meme wurde. Authentifizierung wurde auf der Anwendungsebene angeflanscht, weil das Protokoll dafür kein Konzept hatte.

IRCv3 hat das ordentlich behoben, indem es SASL-Authentifizierung während der Registrierung standardisiert hat. Statt nach dem Verbinden mit einem Bot zu chatten, handelt der Client die Capability sasl aus und authentifiziert sich als Teil des Handshakes, bevor er einen Nickname hat, den irgendjemand sehen kann. Der Server weiß ab der ersten Nachricht, wer du bist, was sich auf alles andere auswirkt: Die Erweiterung account-tag kann deinen Accountnamen an jede Nachricht hängen, Bots können Berechtigungsentscheidungen treffen, ohne eigene NickServ-Parsing-Heuristiken, und Clients können Registrierungsabläufe bauen, die nicht darin bestehen, eine Wand aus NOTICE-Text zu lesen.

Die allgemeinere Lektion: Wird ein Workaround universell, hört er auf, ein Workaround zu sein, und wird zu deinem De-facto-Protokoll, ohne die gleiche Designsorgfalt. NickServ stand in keinem RFC, hat aber zwei Jahrzehnte Client-Verhalten geprägt, einschließlich seiner Fehlermodi. Das Standardisieren dessen, was die Leute ohnehin schon taten (beim Verbinden authentifizieren), mit einem echten Mechanismus (SASL, das PLAIN, EXTERNAL für Client-Zertifikate und Platz für mehr mitbringt), hat Folklore in Infrastruktur verwandelt. Wenn deine Nutzer alle denselben umständlichen Tanz aufführen, um etwas zu erledigen, ist das dein nächster Spezifikationspunkt und keine FAQ für den Support.

Lektion: Historie ist ein Protokollproblem, kein Client-Problem

Die meiste Zeit seines Lebens war IRC streng live: Wenn dein Client nicht verbunden war, als eine Nachricht gesendet wurde, existierte sie für dich nie. Die Antwort der Community war der Bouncer, ein dauerhaft verbundener Proxy (ZNC ist der Klassiker), der auf einem VPS lief, deine Sitzung hielt und verpasste Nachrichten wieder abspielte. Bouncer funktionierten, aber sie waren ein weiteres Infrastrukturteil, das betrieben werden musste, ein weiteres Credential, das man verwalten musste, und das Wiedergabeerlebnis war miserabel: Alles kam mit dem Zeitpunkt der Wiedergabe gestempelt an und stand außer Reihenfolge zu allem, was du live gesehen hattest.

IRCv3 ist das in Schichten angegangen. Zuerst machte server-time wiedergegebene Historie nutzbar, indem es den echten Empfangszeitstempel mitlieferte. Dann gab das chathistory-Draft Clients einen Standardweg, Zeitbereiche der Historie anzufordern, und Server wie Ergo begannen, Channel-Historie direkt zu speichern. Das Ergebnis ist, wie viele Leute bemerkt haben, die einen kleinen Ergo-Server für Freunde aufgesetzt haben, verblüffend: Verbinde dich nach einer Woche Offline-Zeit vom Handy, und der Server spielt alles mit korrekten Zeitstempeln ab, ohne Bouncer und ohne VPS-Babysitting. Es ist der größte Sprung bei der Nutzungsqualität, den IRC je hatte, und er kam vom Protokoll, nicht von einem cleveren Client-Hack.

Es gibt ein Gegenargument, das man ernst nehmen sollte, und tägliche IRC-Nutzer bringen es vor: Der Großteil des wahrgenommenen Ruckelns liegt auf der Client-Seite, und mit einem guten Client und einem Bouncer bekommt man heute schon ein gutes Erlebnis. Stimmt, aber das Argument beweist den Punkt aus der anderen Richtung. Jeder Client-Autor hat Scrollback-Zusammensetzung, Highlight-Tracking und Deduplizierung bei der Wiedergabe unabhängig voneinander neu implementiert, jeweils mit leicht unterschiedlichen Bugs. Den schwierigen Teil ins Protokoll und in den Server zu verlagern heißt, dass er einmal richtig umgesetzt wird und jeder Client davon profitiert. Dieselbe Entscheidung treffen wir in der Webentwicklung ständig: Invarianten in die Plattform verlagern (denk daran, wie viel App-Code verschwand, als Browser nativen fetch bekamen oder TLS zur Grundausstattung wurde), statt tausend Codebasen das Problem halb lösen zu lassen.

Lektion: Optionale Features erzeugen auseinanderlaufende Realitäten

Nun zu dem Fehler, den IRCv3 noch nicht vollständig gelöst hat, und der für alle, die erweiterbare Systeme entwerfen, am relevantesten ist. Weil jede Erweiterung optional ist und unabhängig ausgehandelt wird, können zwei Nutzer im selben Channel buchstäblich verschiedene Gespräche sehen. Der Client des einen zeigt Nachrichten mit +reply-Tags als verkettete Antworten an; der andere zeigt dieselben Nachrichten als flache, kontextlose Zeilen. Ein Nutzer sieht Emoji-Reaktionen als saubere Glyphen an einer Nachricht; ein anderer sieht gar nichts oder eine kryptische Ersatzdarstellung. Die Support-Matrix von IRCv3 ist ein Raster aus Ja und Nein, und dein Erlebnis hängt davon ab, in welcher Spalte du lebst.

Das ist die Schattenseite der eleganten Degradierung. Abwärtskompatibilität garantiert, dass nichts bricht, aber im Modell ist nichts garantiert, das Kohärenz sicherstellt. Slack und Discord haben, was man sonst von ihnen halten mag, dieses Problem nie: Die Plattform kontrolliert beide Enden eng genug, dass eine Reaktion für alle eine Reaktion ist. IRCv3 hat Evolution über Kontrolle gestellt, und der Preis ist eine dauerhaft ungleichmäßige Feature-Landschaft, in der Draft-Spezifikationen manchmal jahrelang Entwürfe bleiben.

Wenn du etwas Erweiterbares baust, plane das von Tag eins ein. Einige Gegenmaßnahmen, auf die IRCv3 und ähnliche Ökosysteme gekommen sind:

  • Degradierungen bewusst entwerfen. Ein Reply-Tag sollte auf eine lesbare zitierte Zeile zurückfallen, nicht verschwinden. Ist der Fallback unbrauchbar, ist das Feature noch nicht reif genug, um es optional auszuliefern.
  • Die Support-Matrix veröffentlichen und ehrlich pflegen. Die Client- und Server-Tabellen von IRCv3 lassen Nutzer informierte Entscheidungen treffen; Schweigen führt dazu, dass die Schuld der falschen Software zugeschoben wird.
  • Eine Baseline definieren und sie fördern. Die nummerierten Releases (3.1, 3.2) bündeln Capabilities, damit Clients auf „3.2-Unterstützung“ abzielen können, statt auf elf einzelne Flags. Gib deinem Ökosystem einen Boden, auf den es konvergieren kann.
  • Drafts ablaufen lassen. Ein Draft, der in Servern ausgeliefert wird, aber nie finalisiert wird (die Channel-Historie hat mit diesem Schicksal geliebäugelt), ist schlimmer als gar kein Draft, weil er Verhalten forkt, ohne sich festzulegen.

Eine Variante davon sahen wir im Artikel warum kollaboratives Bearbeiten schwieriger ist, als du denkst: Sobald Beteiligte uneinig sein können, wie der gemeinsame Zustand aussieht, brauchst du explizite Konvergenzregeln, nicht nur gute Absichten. Optional-by-default ist eine gute Strategie für Capabilities, aber nutzersichtbare Semantik braucht einen Plan für die Leute, die auf der anderen Seite der Matrix feststecken.

Zwei stilisierte Fenster zeigen denselben Stream, eines voller Threads und Symbole, eines schlicht
Optionale Erweiterungen bedeuten, dass zwei Nutzer denselben Channel beobachten und verschiedene Gespräche sehen können.

Was das für deine nächste API oder dein nächstes Protokoll bedeutet

Lässt man die Chat-Nostalgie beiseite, ist IRCv3 ein kompaktes Playbook für die Weiterentwicklung von allem, was ein Wire-Format hat. Hier ist die Checkliste, die ich jedem Team mitgeben würde, das ein Protokoll, ein Event-Schema oder auch nur eine öffentliche REST-API startet:

  1. Verhandlung zuerst ausliefern. Capability-Austausch in Version eins, auch mit einer leeren Capability-Liste. Discovery nachträglich einzubauen ist zehnmal schwieriger.
  2. Einen Erweiterungspunkt reservieren. Tags, Header, Metadaten-Maps: ein generischer Slot, an dem künftige Features hängen können, ohne die Kern-Grammatik anzufassen.
  3. Neues Verhalten pro Verbindung opt-in machen. Alte Clients müssen exakt das alte Verhalten Byte für Byte erhalten. IRCv3-Server senden nie einen Tag an einen Client, der nicht danach gefragt hat.
  4. Die Folklore standardisieren. Welchen Hack deine Nutzer auch immer übernommen haben (NickServ-artige Authentifizierung, Bouncer-artige Wiedergabe), das ist eine Spezifikation, die darauf wartet, ordentlich geschrieben zu werden.
  5. Die Support-Matrix einplanen. Optionale Features laufen auseinander; entscheide, wie degradierte Erlebnisse aussehen, und veröffentliche, wer was unterstützt.
  6. Fortschritt in benannte Meilensteine bündeln. Einzelne Flags sind für Implementierer; nummerierte Releases sind für Menschen.

Was ich wirklich beeindruckend finde, ist die Zurückhaltung. IRCv3 hat nicht End-to-End-Verschlüsselung, einen Federation-Neuentwurf oder irgendein großes Neuschreiben angestrebt. Es hat sich die kleinen, scharfen Probleme herausgepickt (Discovery, Metadaten, Authentifizierung, Zeit) und sie so gelöst, dass ein Client von 1995 sie einfach ignorieren kann. Deshalb ist IRC praktisch der einzige Überlebende seiner Generation von Internetprotokollen, der weitgehend so läuft, wie es entworfen wurde, und deshalb kann ein Wochenendprojekt heute einer Gruppe von Freunden Historien-Sync, TLS und echte Accounts auf einem 5-Dollar-VPS verschaffen.

Abwärtskompatibilität ist nicht die Abwesenheit von Veränderung. Sie ist die Disziplin, jede Änderung so zu gestalten, dass die Vergangenheit sie gefahrlos ignorieren kann.

Wenn du das nächste Mal versucht bist, eine API zu versionieren, indem du den Pfad von /v1 auf /v2 hochzählst und jeden Konsumenten zur Migration zwingst, denk an das Protokoll, das einen Client aus der Modem-Ära und eine moderne Mobile-App im selben Channel zusammenbringt. Verhandle Capabilities. Häng Metadaten an, statt Befehle zu erfinden. Halte den Boden stabil und lass die Decke in ihrem eigenen Tempo steigen. IRCv3 beweist, dass man ein Protokoll haben kann, das seine Älteren respektiert und trotzdem neue Tricks lernt. Und wenn ein Chat-System von 1988 das schafft, hat dein drei Jahre alter Microservice keine Ausrede.