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

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-quic implémente QUIC v1 (RFC 9000 transport + RFC 9001 TLS + RFC 9002 loss/congestion), transport de air-http (HTTP/3) et futur socle de transports Air. Maison, pur Rust, zéro-C, zéro unsafe ; primitives via air-crypto, handshake TLS 1.3 via air-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-quic réutilise donc le cœur handshake d’air-tls (HandshakeStateMachine + KeySchedule + TranscriptHash), pas son RecordCodec. ⇒ Exigence sur air-tls : exposer son handshake découplé du record layer (accommodation à graver dans la spec air-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 module lowlevelAirAesBlock/AirChaCha20, additif ADR-081), air-socket (L1, UDP), air-runtime (L1, async + timers), air-base-core (L1).
  • Zéro unsafe exposé ; parsing défensif intégral (varints bornés, get()).

Anatomie : les 9 composants appliqués à QUIC v1

#ComposantRéalisation air-quicPur ?
1FramerPacketFramer — délimite un datagramme UDP en paquets QUIC (coalescés), en-têtes long/short, extraction du numéro de paquet (protégé)
2CodecPacketCodec (en-têtes), FrameCodec (STREAM/ACK/CRYPTO/MAX_DATA/RESET_STREAM/CONNECTION_CLOSE/PATH_CHALLENGE/NEW_CONNECTION_ID…), TransportParamCodecvarints RFC 9000 §16 bornés
3StateMachineConnectionStateMachineInitial → Handshaking → Established → Closing → Draining (RFC 9000 §10)
4HandshakerQuicHandshake — 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
5Flow ControllerFlowController — fenêtrage connexion (MAX_DATA) et par stream (MAX_STREAM_DATA) + limites de streams (MAX_STREAMS)
6MultiplexerStreamMultiplexer — streams bidi/uni, IDs RFC 9000 §2.1 (sans collision/ABA), isolation inter-streams, ordre par stream
7Timer ManagerLossDetection + CongestionController — RTT, PTO, retransmission, NewReno (RFC 9002) ; idle timeout, ACK delay — horloge injectée✅ (horloge injectée)
8Session ContextConnectionSecrets/KeySchedule + PacketProtection (AEAD) + HeaderProtection ; connection IDs, transport params ; zeroize✅ + air-crypto
9Extension hooksTransportParameters + registre d’extensions (datagram RFC 9221, ack-frequency…) ; extension inconnue ignorée gracieusement

Inventaire des objets — ~32 objets

A. Surface publique — ~13

#ObjetRôle
1QuicClientConfig (+ Builder)politique client (config air-tls + transport params) — Arc, immuable
2QuicServerConfig (+ Builder)politique serveur
3QuicEndpointpossède un socket UDP, démultiplexe N connexions par Connection ID
4QuicConnectionmachine à états d’une connexion (Send/!Sync)
5QuicConnectionStateenum d’état
6SendStreammoitié émettrice d’un stream (flow-controlée)
7RecvStreammoitié réceptrice (réordonnancement)
8StreamIdnewtype (bidi/uni, initiateur)
9TransportParameters (+ Builder)idle timeout, max_data, max_streams, max_udp_payload…
10ConnectionIdnewtype (routage/migration)
11QuicErrorenum (ADR-019) — transport + applicatif
12ConnectionCloseframe de fermeture (code + raison)
13DatagramRFC 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-protoquinn-proto est déjà un cœur sans-IO Rust pur, memory-safe, éprouvé (interop large). Modèle audit-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 pour air-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 KAT air-crypto.
  • Pilote I/O : loopback UDP réel.

Récapitulatif & décisions

FamilleObjets
A. Surface publique~13
B. Cœur sans-IO (9 comp.)~14
C. Enums wire~5
Total~32
  1. QUIC v1 moderne seul (RFC 9000/9001/9002), TLS 1.3 obligatoire.
  2. Handshake TLS via air-tls (frames CRYPTO), AEAD paquet via air-crypto — jamais le record layer TLS.
  3. 9 composants tous exercés (flow control + mux natifs).
  4. Zéro-C, zéro unsafe, primitives déléguées ; async 1ʳᵉ classe (ADR-038), connexion Send/!Sync.
  5. 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 par air-quic) — à graver dans la spec air-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.