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

Note de travail — Protocoles réseau d’Air & architecture des crates réseau purs

Statut : la §2 (motif sans-IO + anatomie à 9 composants) est désormais NORMATIVE via ADR-091 (2026-07-11) — obligatoire pour toute impl réseau maison. Le reste de ce document (§1 liste des protocoles, §3 points durs, §4 ordre de priorité) demeure DOCUMENT DE TRAVAIL exploratoire (tranché par specs/ADR au fil de l’eau ; cf. docs/notes/api-reseau-strategie-fr.md pour le cadrage air-network). Objets : (1) lister les protocoles réseau qu’Air doit implémenter, (2) [→ gravé ADR-091] comment on architecture le code de ces crates réseau « purs » (maison, zéro-C, fuzzés).

1. Périmètre & tiers

Reprend la décision de périmètre connectivité (discutée 2026-06-24/25) :

  • Desktop + serveur = tier-1. Mobile = tier-2 éventuel (téléphonie hors socle).
  • Écartés du socle : SIP, WebRTC, téléphonie (→ tier-2 mobile, ADR ultérieur).

Protocoles retenus (tableau de travail)

ProtocoleCoucheTierDépend deCrate cibleTransport / framingParseur hostile → fuzzÉtat
TCP / UDP / UnixL11couche 0 netair-socketsockets BSD(adresses)✅ livré
DNS (client)L11air-socketair-socket::resolverUDP + repli TCP, RFC 1035fuzz_dns_parse✅ livré
TLSL21rustls + aws-lc-rs + rustls-webpki, air-socketair-tls (intègre rustls — ADR-042)TLS 1.3 (rustls ; provider aws-lc-rs, exception C étroite)✅ (intégration + interop)à spécifier
QUICL21air-tls, air-socket (UDP), air-runtimeair-quicQUIC (RFC 9000), TLS 1.3 intégré✅ (paquets/frames)à spécifier
HTTP 1/2/3L21air-tls (h1/h2), air-quic (h3), air-runtimeair-http (h1/h2/h3 = modules internes)h1=TCP, h2=TCP/TLS, h3=QUIC✅ (parseurs h1/h2/h3)à spécifier
SSH (+ scp/sftp)L21air-crypto, air-socket, air-runtimeair-ssh (MAISON, différé — ADR-043 ; OpenSSH système en incubation ; client avant serveur)RFC 4251+, SFTP, strict-kex anti-Terrapin✅ (Handshaker/Framer)différé (ADR-043)
mDNS / DNS-SDL2/L51air-socket (UDP multicast)air-mdns (ou daemon)RFC 6762/6763à spécifier
WireGuard (VPN)L2/L51air-crypto, air-socket, couche 0daemon + crateUDP, Noise✅ (handshake)à spécifier
DHCP (client)L51air-socketdaemon .airserviceUDP broadcast, RA/SLAACà spécifier
NTP / SNTPL51air-socketdaemon .airserviceUDPà spécifier
WebSocketL21air-httpmodule d’air-httpupgrade h1/h2dérivé
WebDAV / IPPL2+1air-httpconsommateurs HTTPsur HTTP(héritée)dérivé
SMB2(interop seulement)écarté tier-1

Note SMB (décision BDFL 2026-06-25)

SMB n’est PAS tier-1 (tier-2 au plus). Justification : conceptuellement, SMB n’apporte rien qu’un daemon HTTP/3 (interrogé par un client distant adéquat) ne puisse faire — tout ce que fait SMB (partage de fichiers, énumération, verrous, notifications) est transposable sur un transport HTTP/3. Donc le partage de fichiers Air-natif passera par HTTP/3 (daemon couche 5 + client), pas par l’implémentation du protocole SMB.

⚠️ Réserve honnête : SMB-tier-2 ne resterait justifié que pour l’interop avec l’existant (monter un partage Windows/NAS SMB déjà déployé). Ce besoin d’interop est réel côté desktop, mais différé ; il ne conditionne pas le socle.


2. Architecture du code des crates réseau purs (LE cœur de cette note)

Objectif : un patron d’architecture unique pour tous les crates réseau maison, qui maximise testabilité, fuzzabilité, zéro-C, et la cohérence avec ADR-038 (synchrone-first / async opt-in) et la pile Air (air-socket L1, air-runtime, air-network L2).

2.1 Principe directeur : sans-IO (séparer le protocole de l’I/O)

Chaque protocole se décompose en deux étages nets :

  1. Cœur sans-IO (pur) — une machine à états du protocole : octets entrants → événements + octets sortants. Aucun socket, aucun async, aucune horloge, aucune allocation cachée. C’est une bibliothèque de pur calcul (parse, sérialise, transitions d’état, gestion des erreurs protocolaires).
  2. Pilote I/O (mince) — câble le cœur sans-IO au transport réel : air-socket (synchrone) et/ou air-runtime (asynchrone). C’est la seule partie qui touche le monde extérieur ; elle reste fine.

Pourquoi ce patron (et pas un stack monolithique async) :

  • Fuzzabilité triviale (Principe 3) : on fuzz le cœur en lui jetant des octets arbitraires — zéro setup réseau. C’est la surface hostile, isolée et directement testable. (Déjà éprouvé : le parseur DNS d’air-socket est sans-IO et fuzzé.)
  • Testabilité déterministe : pas de réseau dans les tests unitaires/property ; le cœur est une fonction.
  • Neutre sync/async (ADR-038) : le même cœur se pilote en synchrone (boucle bloquante sur air-socket) ou en asynchrone (air-runtime, modèle buffers possédés). On ne se verrouille pas sur l’async.
  • Composable : les protocoles s’empilent par composition de cœurs sans-IO — un cœur HTTP/3 pilote un cœur QUIC qui pilote un cœur TLS, etc. (cf. §2.4).
  • no_std-friendly : le cœur pur peut viser no_std + alloc (cohérent ethos couche 0), le couplage std restant dans le pilote I/O.

(Référence d’industrie : le patron « sans-IO » — hyper-h2 (Python), quinn-proto/h2 (Rust), quic-go — sépare état protocolaire et I/O exactement ainsi.)

2.2 Anatomie canonique : les 9 composants (DÉCISION — uniforme dans CHAQUE crate)

Règle gravée. Tout crate réseau Air expose la même anatomie : un lecteur/ auditeur doit retrouver ces 9 composants à l’identique d’un crate à l’autre. Le réseau est la première surface de brèches ; cette uniformité (« on sait toujours où regarder ») + l’isolation stricte (chaque composant testable indépendamment) sont nos deux leviers de sécurité. 8 des 9 composants sont purs (sans-IO) ; ils constituent le cœur sans-IO de §2.1. Le pilote I/O (§2.1) les enveloppe.

#ComposantResponsabilité (frontière nette)Pur ?Régime de test drastique
1FramerDélimite le flux d’octets en trames (longueurs/délimiteurs/records) : « où commence/finit une trame »fuzz (longueurs tronquées/géantes/malicieuses) ; property : jamais de lecture hors borne, jamais d’alloc non bornée
2Codec(Dé)sérialise le contenu d’une trame ↔ messages typés (≠ Framer)fuzz octets→message (jamais de panic) ; round-trip decode∘encode = id sur valide ; bytes vs &str
3StateMachineTransitions du protocole : légalité, événement déclenché, quoi émettreproperty + model-based (transitions illégales rejetées, zéro panic) ; couverture état×événement
4HandshakerÉtablissement (TLS handshake / QUIC / SSH kex / Noise) — sécurité-critique (auth, accord de clés)✅ (logique) + air-cryptoKAT/vecteurs de conformité + négatifs (downgrade, handshake malformé) + interop
5Flow ControllerBack-pressure / fenêtrage (crédits, windows)property (jamais dépasser la fenêtre, pas de deadlock, comptabilité des crédits équilibrée) ; adversarial (pair hors-fenêtre → rejet)
6MultiplexerMultiplexage de flux/canaux sur une connexion (streams h2/QUIC, canaux SSH)property (IDs sans collision/ABA, isolation inter-streams, équité) ; fuzz entrelacement de trames
7Timer ManagerÉchéances (timeouts, retransmission, keepalive)via horloge injectéevirtual clock déterministe (l’échéance tire exactement quand prévu) ; jamais de sleep réel en test
8Session ContextÉtat/params négociés/secrets par connexion — état partagé explicite (lu/écrit par les autres)cycle de vie ; zeroize des secrets au drop (cf. air-crypto) ; zéro fuite inter-sessions
9Extension hooksPoints d’extension (extensions TLS, ALPN, upgrade HTTP, négo d’algos SSH) — trait/registrehook invoqué correctement ; extension inconnue gérée gracieusement (ignorée/rejetée selon spec) ; aucun hook ne viole les invariants du cœur

Communication & frontières. Les composants communiquent 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) → testable en isolation avec des entrées simulées, sans réseau.

Uniformité même en l’absence d’un composant. Tous les protocoles n’exercent pas les 9 (ex. un client NTP : Framer + Codec + StateMachine minimale + Timer ; pas de Multiplexer ni Flow Controller ni Handshaker). Règle : un composant absent par conception est marqué explicitement (type unité / commentaire « NO MULTIPLEXING: … ») — l’absence est intentionnelle et visible, jamais un trou. Ainsi « je retrouve toujours les mêmes cases » reste vrai.

2.3 Structure interne type d’un crate air-<proto>

air-<proto>/
├── src/
│   ├── proto/        — CŒUR SANS-IO (= les 9 composants, §2.2), pur/fuzzable
│   │   ├── framer.rs        (1. Framer)
│   │   ├── codec.rs         (2. Codec)
│   │   ├── state.rs         (3. StateMachine)
│   │   ├── handshake.rs     (4. Handshaker — consomme air-crypto)
│   │   ├── flow.rs          (5. Flow Controller)
│   │   ├── mux.rs           (6. Multiplexer)
│   │   ├── timers.rs        (7. Timer Manager — horloge injectée)
│   │   ├── session.rs       (8. Session Context)
│   │   ├── ext.rs           (9. Extension hooks)
│   │   └── mod.rs           (assemble : `feed(&[u8]) -> Events`, `poll_transmit() -> Option<&[u8]>`,
│   │                         `handle_timeout(now)`, pilotés SANS I/O)
│   ├── io/           — PILOTE : câble proto/ à air-socket (sync) / air-runtime (async) ;
│   │                   SEUL endroit qui touche sockets + horloge réelle
│   ├── error.rs      — AirError (ADR-019) ; erreurs protocolaires (cœur) vs transport (pilote)
│   └── lib.rs        — surface publique ergonomique
└── fuzz/             — UNE cible de fuzz par composant parseur (Framer, Codec, Handshaker…)

2.4 Règles de codage (héritées des Principes & des couches 0/1)

  • Zéro-C, maison : aucune dépendance C/*-sys (check-c-surface) — sauf l’exception TLS gravée par ADR-042 : air-tls = rustls (protocole pur Rust, memory-safe) + aws-lc-rs (provider crypto AWS-LC, asm formellement vérifié + FIPS) + rustls-webpki (X.509 pur Rust). Exception C NOMMÉE et ÉTROITE : le C ne concerne que les primitives (aws-lc-sys), jamais le protocole ni le parseur X.509 (qui restent memory-safe). ring reste banni. air-tls maison (sur le patron sans-IO ci-dessous) gardé en contingence si rustls devient un passif. (Choix motivé : la combinaison la plus sûre disponible — memory-safety + vérif formelle + 1.3 ; cf. ADR-042 pour l’analyse.)
  • Parsing défensif (Principe 3) : get() (jamais d’indexation paniquante), distinction stricte &[u8] vs &str, checked_*/saturating_*, pas de suivi de pointeur/longueur non borné (anti-boucle, comme le DNS sans compression).
  • Aucune fn unsafe exposée ; // SAFETY: sur tout bloc interne.
  • Erreurs : AirError (ADR-019) ; le cœur rend des erreurs protocolaires typées, le pilote mappe les erreurs transport.
  • Buffers & back-pressure : intégration avec air-runtime (buffers possédés) et air-memory (arenas/pools) pour éviter le churn d’allocation ; back-pressure explicite (reactive streams, cohérent AirCom).
  • Couverture 100 % (couches fondatrices) ; fuzz obligatoire sur chaque parseur ; interop (cf. §2.5).

2.5 Composition / empilement

Les protocoles s’empilent ; les cœurs sans-IO se composent sans que chacun réinvente la gestion de connexion :

air-http (h3)  →  air-quic  →  air-tls 1.3  →  air-socket (UDP)   [async: air-runtime]
air-http (h2)  →  air-tls 1.3  →  air-socket (TCP)
air-http (h1)  →  air-socket (TCP)
air-ssh        →  air-crypto + air-socket (TCP)
  • air-network (L2) = le substrat où ces crates se branchent : framework de connexion / listener / découverte / path-awareness (≈ Network.framework, cf. note api-reseau-strategie-fr.md). Il compose transport + TLS/QUIC ; les protocoles applicatifs (HTTP/SSH) sont au-dessus.
  • Le pilote I/O d’un cœur N consomme l’API du cœur N-1 (pas son I/O) : empilement sans-IO sur sans-IO, l’I/O réel n’existe qu’au bas de la pile.

2.6 Stratégie de test (transverse aux crates réseau)

Rappel : le régime de test par composant (les 9, §2.2) est la première ligne — chaque composant fuzzé/property-testé en isolation. Ci-dessous = le niveau intégration/système, en complément.

  • Cœur sans-IO : unitaires + property-based + fuzz (octets hostiles → jamais de panic, jamais de trame malformée émise).
  • Pilote I/O : intégration loopback réelle (comme air-socket).
  • Interop / conformité : tester contre les implémentations de référence (air-http vs curl/nginx ; air-ssh vs OpenSSH ; air-quic vs ngtcp2/quiche) — vecteurs de conformité rejoués, comme les KAT d’air-crypto.

3. Points durs / à trancher (à remplir)

  • TLS provider zéro-C : rustls + RustCrypto provider — valider l’exception 80 %, vérifier la couverture des suites nécessaires (TLS 1.3 d’abord ; TLS 1.2 ?).
  • QUIC : ampleur (congestion, loss recovery, multi-stream) — vendoring partiel d’un cœur existant (quinn-proto) vs maison intégral ? (règle des 80 % / ADR-024.)
  • Ampleur SSH (algorithmes, kex, sous-système SFTP) — périmètre v1.
  • Granularité : confirmer air-http unique (h1/h2/h3 internes) vs crates séparés.
  • Daemons plomberie (DHCP/NTP/mDNS) : crate-lib + .airservice couche 5, ou tout-en-un ? Articulation avec air-config/air-network.
  • Async vs sync par protocole : lesquels exposent une API sync de 1ère classe (ADR-038) en plus de l’async ?

4. Ordre de priorité (proposé, à valider)

  1. air-tls (socle de tout le sécurisé ; débloque HTTP/QUIC).
  2. air-quic (débloque HTTP/3).
  3. air-http (h1 → h2 → h3).
  4. air-ssh (+ sftp).
  5. Plomberie : DHCP, NTP, mDNS (daemons couche 5).
  6. WireGuard.
  7. (tier-2) SMB interop — si/quand justifié.

Prérequis transverses : air-network (substrat L2) + air-runtime (async) à implémenter avant/en parallèle des premiers protocoles.


Licence du document : MPL 2.0 — document de travail, exploratoire (non engageant).