Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ADR-091 — Motif d’architecture réseau sans-IO : anatomie canonique à 9 composants, obligatoire

Statut : Accepté (2026-07-11, décision BDFL). Rend NORMATIVE la §2 de la note de travail reseau-architecture-crates-fr.md (jusqu’ici « exploratoire, non engageant »). Toute implémentation réseau maison d’Air DOIT s’y conformer.

Catégorie : Architecture (réseau, couches 1–2, fondateur). Toute évolution structurante passe par un RFC (ADR-015). S’appuie sur ADR-019 (erreurs), ADR-038 (sync-first / async opt-in), ADR-024 (dépendances 80 %). Encadre ADR-042 (TLS) et ADR-043 (SSH).

Contexte

Le portage std sur Air safe est bouclé (ADR-088) ; la phase suivante est la couche 2 — runloop air-async → framework réseau Air → air-sshd (la démonstration de la couche 2). Avant d’écrire ces protocoles à état (SSH, TLS, QUIC, HTTP), il faut graver comment on architecture le code réseau, pour une raison de fond :

Le motif sans-IO est le SEUL qui permette de tester l’ensemble de la pile réseau indépendamment du réseau. On fuzz et on property-teste le cœur d’un protocole en lui jetant des octets arbitraires, sans aucun setup réseau — c’est la surface hostile (le réseau est la première surface de brèches), isolée et directement testable.

La note reseau-architecture-crates-fr.md a mûri ce motif (§2) ; il est déjà éprouvé — le client DNS d’air-socket (codec pur crate::dns fuzzé, séparé du pilote I/O resolver.rs) en est le précurseur. Il devient normatif ici, avant que SSH/TLS/QUIC ne s’écrivent, pour que toute la pile réseau naisse sur la même anatomie testable.

Décision

Tout crate réseau maison d’Air suit le motif sans-IO et l’anatomie canonique à 9 composants ci-dessous. C’est obligatoire, uniforme, et vérifié en revue.

  1. Séparation sans-IO (règle absolue). Un protocole se décompose en deux étages nets :

    • Cœur sans-IO (pur) — machine à états octets entrants → événements + octets sortants. Aucun socket, aucun async, aucune horloge, aucune allocation cachée. Pur calcul (parse, sérialise, transitions, erreurs protocolaires). Vise no_std + alloc.
    • Pilote I/O (mince) — la seule partie qui touche le monde extérieur : câble le cœur à air-socket (synchrone) et/ou air-runtime/air-async (asynchrone). Reste fin.
  2. Anatomie canonique — les 9 composants, uniformes dans CHAQUE crate. Un lecteur doit retrouver les mêmes cases d’un crate à l’autre (« on sait toujours où regarder ») ; chaque composant est testable en isolation. 8 des 9 sont purs (le cœur sans-IO) :

    #ComposantFrontièrePur ?
    1Framerdélimite le flux en trames
    2Codec(dé)sérialise le contenu d’une trame ↔ messages typés
    3StateMachinetransitions du protocole (légalité, événements, émissions)
    4Handshakerétablissement (kex/handshake) — sécurité-critique✅ + air-crypto
    5Flow Controllerback-pressure / fenêtrage
    6Multiplexermultiplexage de flux/canaux
    7Timer Manageréchéances (timeouts/retransmission/keepalive)horloge injectée
    8Session Contextétat/params/secrets par connexion (état partagé explicite)
    9Extension hookspoints d’extension (ALPN, extensions TLS, négo algos…)

    Communication par événements/messages typés (pas d’état mutable partagé), sauf le Session Context (#8) qui est l’état partagé explicite, passé par référence. Chaque composant expose une interface étroite (trait).

  3. Absence intentionnelle, jamais un trou. Tous les protocoles n’exercent pas les 9 (ex. un client NTP : Framer + Codec + StateMachine minimale + Timer). Un composant absent par conception est marqué explicitement (type unité / commentaire // NO MULTIPLEXING: …) — l’absence est visible et intentionnelle. Ainsi « je retrouve toujours les mêmes cases » reste vrai.

  4. Structure de crate type. air-<proto>/src/proto/ = le cœur sans-IO (un fichier par composant : framer.rs/codec.rs/state.rs/handshake.rs/flow.rs/mux.rs/timers.rs/ session.rs/ext.rs + mod.rs assemblant feed(&[u8]) -> Events / poll_transmit() -> Option<&[u8]> / handle_timeout(now), pilotés sans I/O) ; src/io/ = le pilote (seul endroit qui touche sockets + horloge réelle) ; fuzz/ = une cible de fuzz par composant parseur.

  5. Régime de test drastique — par composant, en isolation (la première ligne).

    • Fuzz obligatoire sur chaque parseur (Framer/Codec/Handshaker) : octets hostiles → jamais de panic, jamais de lecture hors-borne, jamais d’alloc non bornée, jamais de trame malformée émise.
    • StateMachine : property + model-based (transitions illégales rejetées ; couverture état×événement). Timer : virtual clock déterministejamais de sleep réel en test.
    • Session Context : zeroize des secrets au drop, zéro fuite inter-sessions.
    • Pilote I/O : intégration loopback réelle. Interop/conformité : vecteurs rejoués contre les implémentations de référence (comme les KAT d’air-crypto).
    • Couverture 100 % (couches fondatrices).
  6. Neutralité sync/async (ADR-038). Le même cœur sans-IO se pilote en synchrone (boucle bloquante sur air-socket) ou en asynchrone (air-runtime/air-async). On ne se verrouille pas sur l’async ; le choix vit dans le pilote, pas dans le cœur.

  7. Composabilité — cœurs sans-IO empilés. Les protocoles s’empilent par composition de cœurs : le pilote d’un cœur N consomme l’API du cœur N−1 (pas son I/O). L’I/O réel n’existe qu’au bas de la pile (air-http/h3 → air-quic → air-tls → air-socket/UDP).

  8. Règles de codage (héritées des Principes et des couches 0/1). Parsing défensif (Principe 3 : get() jamais l’indexation, &[u8] vs &str strict, checked_*, anti-boucle) ; aucune fn unsafe exposée (// SAFETY: sur tout bloc interne) ; erreurs AirError (ADR-019, protocolaires dans le cœur / transport dans le pilote) ; back-pressure explicite ; zéro-C (check-c-surface) sauf l’exception TLS nommée et étroite d’ADR-042 (le C ne touche que les primitives aws-lc-rs, jamais le protocole ni le parseur).

  9. Portée. Obligatoire pour toute implémentation réseau maison : air-tls, air-quic, air-http, air-ssh/air-sshd, air-mdns, WireGuard, DHCP, NTP, etc. air-socket (L1, sockets BSD) et le client DNS (déjà sans-IO) sont conformes / précurseurs. Un nouveau crate réseau qui n’exhibe pas les 9 composants (ou leurs absences marquées) ne passe pas la revue.

Conséquences

  • Sécurité par construction : la surface hostile n°1 (parseurs réseau) est isolée, fuzzée, testée sans réseau. L’uniformité (« on sait où regarder ») + l’isolation stricte sont nos deux leviers de sécurité réseau.
  • Testabilité déterministe : le cœur est une fonction ; pas de réseau ni d’horloge réelle dans les tests unitaires/property/model-based. La pile entière est testable indépendamment.
  • no_std-friendly : le cœur pur peut viser no_std + alloc ; le couplage std/OS reste dans le pilote — cohérent avec l’ethos couches 0/1.
  • Prérequis de la couche 2 : ce motif conditionne air-async (le pilote async), le framework réseau air-network, et air-sshd. On l’écrit avant eux.
  • La note reste l’élaboration tactique : liste des protocoles (§1), points durs (§3), ordre de priorité (§4) demeurent exploratoires dans la note ; seule la §2 (le motif) devient normative ici. Les choix par protocole (granularité air-http, périmètre SSH v1, vendoring QUIC…) restent ouverts, tranchés par specs/ADR au fil de l’eau.
  • Précédent industriel assumé : quinn-proto/h2 (Rust), hyper-h2/quic-go — le sans-IO est l’état de l’art pour séparer état protocolaire et I/O.

Alternatives rejetées

  • Stack monolithique async (cœur + I/O mêlés) : rejeté — non fuzzable sans réseau (la surface hostile n’est pas isolable), verrouille l’async (contre ADR-038), non composable, et rend la couverture 100 % illusoire.
  • Architecture ad hoc par protocole : rejeté — perd l’uniformité « on sait toujours où regarder », multiplie les revues, dilue la discipline de fuzz sur la surface la plus dangereuse.
  • Réutiliser des piles réseau C existantes (OpenSSL/…): rejeté — viole le zéro-C sur la surface hostile n°1 (memory-unsafety là où c’est le plus grave) ; l’unique exception C est TLS-primitives via aws-lc-rs (ADR-042), étroite et nommée, jamais le protocole/parseur.
  • Laisser la note « exploratoire » : rejeté — SSH/TLS/QUIC vont s’écrire ; sans motif normatif avant, ils divergeraient, et le levier de test/sécurité serait perdu au moment où il compte le plus.