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.mdpour le cadrageair-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)
| Protocole | Couche | Tier | Dépend de | Crate cible | Transport / framing | Parseur hostile → fuzz | État |
|---|---|---|---|---|---|---|---|
| TCP / UDP / Unix | L1 | 1 | couche 0 net | air-socket | sockets BSD | (adresses) | ✅ livré |
| DNS (client) | L1 | 1 | air-socket | air-socket::resolver | UDP + repli TCP, RFC 1035 | ✅ fuzz_dns_parse | ✅ livré |
| TLS | L2 | 1 | rustls + aws-lc-rs + rustls-webpki, air-socket | air-tls (intègre rustls — ADR-042) | TLS 1.3 (rustls ; provider aws-lc-rs, exception C étroite) | ✅ (intégration + interop) | à spécifier |
| QUIC | L2 | 1 | air-tls, air-socket (UDP), air-runtime | air-quic | QUIC (RFC 9000), TLS 1.3 intégré | ✅ (paquets/frames) | à spécifier |
| HTTP 1/2/3 | L2 | 1 | air-tls (h1/h2), air-quic (h3), air-runtime | air-http (h1/h2/h3 = modules internes) | h1=TCP, h2=TCP/TLS, h3=QUIC | ✅ (parseurs h1/h2/h3) | à spécifier |
| SSH (+ scp/sftp) | L2 | 1 | air-crypto, air-socket, air-runtime | air-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-SD | L2/L5 | 1 | air-socket (UDP multicast) | air-mdns (ou daemon) | RFC 6762/6763 | ✅ | à spécifier |
| WireGuard (VPN) | L2/L5 | 1 | air-crypto, air-socket, couche 0 | daemon + crate | UDP, Noise | ✅ (handshake) | à spécifier |
| DHCP (client) | L5 | 1 | air-socket | daemon .airservice | UDP broadcast, RA/SLAAC | ✅ | à spécifier |
| NTP / SNTP | L5 | 1 | air-socket | daemon .airservice | UDP | ✅ | à spécifier |
| WebSocket | L2 | 1 | air-http | module d’air-http | upgrade h1/h2 | ✅ | dérivé |
| WebDAV / IPP | L2+ | 1 | air-http | consommateurs HTTP | sur HTTP | (héritée) | dérivé |
| — | 2 | — | (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 :
- 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). - 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 viserno_std + alloc(cohérent ethos couche 0), le couplagestdrestant 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.
| # | Composant | Responsabilité (frontière nette) | Pur ? | Régime de test drastique |
|---|---|---|---|---|
| 1 | Framer | Dé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 |
| 2 | Codec | (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 |
| 3 | StateMachine | Transitions du protocole : légalité, événement déclenché, quoi émettre | ✅ | property + model-based (transitions illégales rejetées, zéro panic) ; couverture état×événement |
| 4 | Handshaker | Établissement (TLS handshake / QUIC / SSH kex / Noise) — sécurité-critique (auth, accord de clés) | ✅ (logique) + air-crypto | KAT/vecteurs de conformité + négatifs (downgrade, handshake malformé) + interop |
| 5 | Flow Controller | Back-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) |
| 6 | Multiplexer | Multiplexage de flux/canaux sur une connexion (streams h2/QUIC, canaux SSH) | ✅ | property (IDs sans collision/ABA, isolation inter-streams, équité) ; fuzz entrelacement de trames |
| 7 | Timer Manager | Échéances (timeouts, retransmission, keepalive) | ✅ via horloge injectée | virtual clock déterministe (l’échéance tire exactement quand prévu) ; jamais de sleep réel en test |
| 8 | Session 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 |
| 9 | Extension hooks | Points d’extension (extensions TLS, ALPN, upgrade HTTP, négo d’algos SSH) — trait/registre | ✅ | hook 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).ringreste banni.air-tlsmaison (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
unsafeexposé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. noteapi-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-httpvs curl/nginx ;air-sshvs OpenSSH ;air-quicvs 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-httpunique (h1/h2/h3 internes) vs crates séparés. - Daemons plomberie (DHCP/NTP/mDNS) : crate-lib +
.airservicecouche 5, ou tout-en-un ? Articulation avecair-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)
air-tls(socle de tout le sécurisé ; débloque HTTP/QUIC).air-quic(débloque HTTP/3).air-http(h1 → h2 → h3).air-ssh(+ sftp).- Plomberie : DHCP, NTP, mDNS (daemons couche 5).
- WireGuard.
- (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).