Spec couche 2 — air-quic (QUIC v1 pur Rust, patron sans-IO)
Spécification technique — Version 0.1 (proposition). Couche 2 « Frameworks système ».
Cadrage.
air-quicimplémente QUIC v1 (RFC 9000 transport + RFC 9001 TLS + RFC 9002 loss/congestion), transport deair-http(HTTP/3) et futur socle de transports Air. Maison, pur Rust, zéro-C, zérounsafe; primitives viaair-crypto, handshake TLS 1.3 viaair-tls. Suit à l’identique le patron sans-IO 9-composants (note réseau §2.2).
Position et méthode
QUIC est un protocole filaire (contrairement à air-network qui l’orchestre) :
il a donc son cœur sans-IO (les 9 composants) + un pilote I/O mince sur
air-socket (UDP) et air-runtime (async). QUIC est le premier protocole Air à
exercer les 9 composants (TLS en laissait 2 absents) : il a du flow control et du
multiplexage de streams natifs.
Empilement (note §2.5) :
air-http (h3) → air-quic → air-socket (UDP) [async: air-runtime]
└─ handshake TLS 1.3 via air-tls (CRYPTO frames), AEAD via air-crypto
Décision structurante — QUIC ≠ TLS-sur-TCP. QUIC n’utilise pas le record layer de TLS. Il transporte les messages de handshake TLS 1.3 dans ses propres frames CRYPTO, et protège ses paquets avec son propre AEAD (clés dérivées des secrets TLS via HKDF-Expand-Label, labels QUIC, RFC 9001 §5).
air-quicréutilise donc le cœur handshake d’air-tls(HandshakeStateMachine+KeySchedule+TranscriptHash), pas sonRecordCodec. ⇒ Exigence surair-tls: exposer son handshake découplé du record layer (accommodation à graver dans la specair-tls; cf. « Travail à reprendre »).
Périmètre de sécurité — moderne seul
- QUIC v1 (RFC 9000) uniquement ; négociation de version (RFC 9368) pour détecter/refuser proprement, pas pour parler des versions obsolètes.
- TLS 1.3 obligatoire (RFC 9001) — QUIC n’a pas de mode legacy ; hérite du
périmètre
air-tls(AEAD only, FS). - Retry + Address Validation (anti-amplification, RFC 9000 §8) ; Stateless Reset.
- 0-RTT : optionnel, désactivé par défaut (anti-rejeu, comme
air-tls).
Dépendances
- Tierces externes : ZÉRO hors l’exception TLS déléguée à
air-tls(ADR-042). - Crates Air :
air-tls(L2, handshake TLS 1.3),air-crypto(L1, AEAD de protection de paquet — AES-GCM/ChaCha20 + HKDF ; + header protection via le modulelowlevel—AirAesBlock/AirChaCha20, additif ADR-081),air-socket(L1, UDP),air-runtime(L1, async + timers),air-base-core(L1). - Zéro
unsafeexposé ; parsing défensif intégral (varints bornés,get()).
Anatomie : les 9 composants appliqués à QUIC v1
| # | Composant | Réalisation air-quic | Pur ? |
|---|---|---|---|
| 1 | Framer | PacketFramer — délimite un datagramme UDP en paquets QUIC (coalescés), en-têtes long/short, extraction du numéro de paquet (protégé) | ✅ |
| 2 | Codec | PacketCodec (en-têtes), FrameCodec (STREAM/ACK/CRYPTO/MAX_DATA/RESET_STREAM/CONNECTION_CLOSE/PATH_CHALLENGE/NEW_CONNECTION_ID…), TransportParamCodec — varints RFC 9000 §16 bornés | ✅ |
| 3 | StateMachine | ConnectionStateMachine — Initial → Handshaking → Established → Closing → Draining (RFC 9000 §10) | ✅ |
| 4 | Handshaker | QuicHandshake — pilote le handshake air-tls via frames CRYPTO ; dérive les clés de paquet (Initial/Handshake/1-RTT), key update | ✅ + air-tls/air-crypto |
| 5 | Flow Controller | FlowController — fenêtrage connexion (MAX_DATA) et par stream (MAX_STREAM_DATA) + limites de streams (MAX_STREAMS) | ✅ |
| 6 | Multiplexer | StreamMultiplexer — streams bidi/uni, IDs RFC 9000 §2.1 (sans collision/ABA), isolation inter-streams, ordre par stream | ✅ |
| 7 | Timer Manager | LossDetection + CongestionController — RTT, PTO, retransmission, NewReno (RFC 9002) ; idle timeout, ACK delay — horloge injectée | ✅ (horloge injectée) |
| 8 | Session Context | ConnectionSecrets/KeySchedule + PacketProtection (AEAD) + HeaderProtection ; connection IDs, transport params ; zeroize | ✅ + air-crypto |
| 9 | Extension hooks | TransportParameters + registre d’extensions (datagram RFC 9221, ack-frequency…) ; extension inconnue ignorée gracieusement | ✅ |
Inventaire des objets — ~32 objets
A. Surface publique — ~13
| # | Objet | Rôle |
|---|---|---|
| 1 | QuicClientConfig (+ Builder) | politique client (config air-tls + transport params) — Arc, immuable |
| 2 | QuicServerConfig (+ Builder) | politique serveur |
| 3 | QuicEndpoint | possède un socket UDP, démultiplexe N connexions par Connection ID |
| 4 | QuicConnection | machine à états d’une connexion (Send/!Sync) |
| 5 | QuicConnectionState | enum d’état |
| 6 | SendStream | moitié émettrice d’un stream (flow-controlée) |
| 7 | RecvStream | moitié réceptrice (réordonnancement) |
| 8 | StreamId | newtype (bidi/uni, initiateur) |
| 9 | TransportParameters (+ Builder) | idle timeout, max_data, max_streams, max_udp_payload… |
| 10 | ConnectionId | newtype (routage/migration) |
| 11 | QuicError | enum (ADR-019) — transport + applicatif |
| 12 | ConnectionClose | frame de fermeture (code + raison) |
| 13 | Datagram | RFC 9221 (datagramme non fiable) — optionnel |
B. Cœur sans-IO — ~14 (les 9 composants + sous-objets)
PacketFramer, PacketCodec, FrameCodec, TransportParamCodec,
ConnectionStateMachine, QuicHandshake, FlowController, StreamMultiplexer,
LossDetection, CongestionController, KeySchedule, PacketProtection,
HeaderProtection, ExtensionRegistry.
C. Enums wire — ~5
PacketType (Initial/0-RTT/Handshake/Retry/1-RTT), FrameType, QuicVersion,
StreamKind (bidi/uni × client/server-initiated), EncryptionLevel
(Initial/Handshake/1-RTT).
Codecs (bytes ↔ frames)
#![allow(unused)]
fn main() {
/// Lecteur borné + varints QUIC (RFC 9000 §16) — jamais d'indexation paniquante.
pub struct QuicReader<'a> { /* … */ }
impl<'a> QuicReader<'a> {
pub fn varint(&mut self) -> Result<u64, DecodeError>; // 1/2/4/8 octets préfixés
pub fn take(&mut self, n: usize) -> Result<&'a [u8], DecodeError>;
}
pub enum QuicFrame<'a> {
Padding, Ping,
Ack(AckRanges<'a>),
Crypto { offset: u64, data: &'a [u8] }, // porte le handshake TLS
Stream { id: StreamId, offset: u64, fin: bool, data: &'a [u8] },
MaxData(u64), MaxStreamData { id: StreamId, max: u64 }, MaxStreams { kind: StreamKind, max: u64 },
ResetStream { id: StreamId, code: u64, final_size: u64 },
NewConnectionId { /* … */ }, RetireConnectionId(u64),
PathChallenge([u8; 8]), PathResponse([u8; 8]),
ConnectionClose(ConnectionClose),
// OMIS : rien de legacy (QUIC v1 n'a pas d'héritage).
}
// FrameCodec::decode borne toutes les longueurs ; frame inconnue en niveau interdit → CONNECTION_CLOSE(PROTOCOL_VIOLATION).
}
Protection de paquet (RFC 9001 §5). HeaderProtection (masque dérivé via
lowlevel, ADR-081) + PacketProtection (lowlevel::AirAeadExplicitNonce, ADR-082 :
nonce = IV ⊕ numéro de paquet, déterministe — pas la façade à nonce aléatoire). Clés dérivées
par KeySchedule des secrets TLS (air-tls), labels QUIC (quic key/quic iv/
quic hp).
Machine à états — multi-thread (mono-propriétaire, sans-IO)
#![allow(unused)]
fn main() {
pub enum QuicEvent {
Connected { alpn: Option<AlpnProtocol> },
StreamOpened(StreamId), StreamData(StreamId), StreamFinished(StreamId),
DatagramReceived, ConnectionClosed(ConnectionClose), WantsTransmit,
}
impl QuicConnection {
/// SANS-IO : ingère un datagramme UDP entrant → événements + transitions.
pub fn recv_datagram(&mut self, now: AirInstant, packet: &[u8]) -> Result<Vec<QuicEvent>, QuicError>;
/// SANS-IO : récupère le prochain datagramme à émettre (respecte congestion + PMTU).
pub fn poll_transmit(&mut self, now: AirInstant) -> Option<QuicDatagram>;
/// SANS-IO : prochaine échéance (PTO, idle, ACK). Horloge injectée → test déterministe.
pub fn poll_timeout(&self) -> Option<AirInstant>;
pub fn handle_timeout(&mut self, now: AirInstant) -> Result<Vec<QuicEvent>, QuicError>;
// Streams :
pub fn open_stream(&mut self, kind: StreamKind) -> Result<StreamId, QuicError>;
pub fn send(&mut self, id: StreamId, data: &[u8], fin: bool) -> Result<usize, QuicError>; // flow-controlé
pub fn read(&mut self, id: StreamId, out: &mut [u8]) -> Result<usize, QuicError>;
pub fn close(&mut self, code: u64, reason: &[u8]);
}
}
Thread model : QuicConnection Send/!Sync (mono-propriétaire, zéro verrou) ;
QuicEndpoint route les datagrammes vers la bonne connexion par Connection ID ; les
configs sont Arc Send/Sync. Un endpoint par cœur (thread-per-core, ADR-038/039).
Pilote I/O (io/) = seul contact air-socket (UDP recvmmsg/sendmmsg) + horloge.
Décision de fond à trancher — maison intégral vs distillation quinn-proto
La note réseau §3 l’a explicitement laissée ouverte. Deux options, à arbitrer (BDFL) :
- (A) Maison intégral — cohérent avec l’ethos (zéro-C, possession totale, auditabilité, 9-composants), mais QUIC est vaste (loss recovery, congestion, multi-stream, migration) → effort important, historique de bugs subtils.
- (B) Distillation
quinn-proto—quinn-protoest déjà un cœur sans-IO Rust pur, memory-safe, éprouvé (interop large). Modèleaudit-dlmalloc: s’inspirer / distiller plutôt que vendorer en boîte noire ou réinventer. Règle des 80 % (ADR-024) à évaluer.Recommandation : cadrer par un audit
quinn-proto(comme l’audit s2n-tls pourair-tls) avant de trancher — mesurer surface, deps, part réutilisable, puis décider maison vs distillation. Cette spec décrit la cible fonctionnelle (les 9 composants, l’API) indépendamment du chemin retenu.
Stratégie de tests
- Couverture couche 2 > 90 % (viser 100 % cœur sans-IO).
- Par composant : Framer/Codec fuzzés (varints/longueurs hostiles → zéro panic) ; StateMachine property/model-based ; FlowController property (jamais dépasser la fenêtre, pas de deadlock) ; Multiplexer (IDs sans ABA, isolation) ; Timers virtual clock (PTO tire à l’instant prévu).
- Vecteurs RFC 9001 (sample packet protection) + interop vs
ngtcp2/quiche(client ET serveur), comme les KATair-crypto. - Pilote I/O : loopback UDP réel.
Récapitulatif & décisions
| Famille | Objets |
|---|---|
| A. Surface publique | ~13 |
| B. Cœur sans-IO (9 comp.) | ~14 |
| C. Enums wire | ~5 |
| Total | ~32 |
- QUIC v1 moderne seul (RFC 9000/9001/9002), TLS 1.3 obligatoire.
- Handshake TLS via
air-tls(frames CRYPTO), AEAD paquet viaair-crypto— jamais le record layer TLS. - 9 composants tous exercés (flow control + mux natifs).
- Zéro-C, zéro
unsafe, primitives déléguées ; async 1ʳᵉ classe (ADR-038), connexionSend/!Sync. - Chemin d’implémentation (maison vs
quinn-proto) à trancher après audit.
Travail à reprendre
- Audit
quinn-proto(surface/deps/80 %) → décision maison vs distillation. - Accommodation
air-tls: exposer le cœur handshake découplé du record layer (requis parair-quic) — à graver dans la specair-tls. - Congestion avancée (CUBIC/BBR) après NewReno v1.
air-http(h3) consommateur ; migration de connexion (multi-path) ; ABI C différée.
Licence du document : MPL 2.0
Statut : Spécification air-quic (couche 2) v0.1 — QUIC v1 pur Rust, patron sans-IO,
handshake via air-tls. Chemin d’implémentation à trancher (audit quinn-proto).
Implémentation à suivre.