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-network (substrat de connexion ≈ Network.framework)

Spécification technique — Version 0.1 (proposition). Couche 2 « Frameworks système ».

Cadrage. air-network vise la couverture fonctionnelle des API réseau d’Apple (Network.framework) sans jamais copier leur API — surface en vocabulaire Air idiomatique (Rust), leur doc servant de checklist et de nord de conception (note de stratégie, décision BDFL 2026-06-13). Les équivalents Apple sont cités entre parenthèses au seul titre de checklist de couverture.

Position et méthode

air-network est le substrat de connexion de couche 2 : le framework qui compose transport (air-socket, L1), sécurité (air-tls, L2) et transports avancés (air-quic, L2, différé) au-dessus du runtime async io_uring (air-runtime, L1, ADR-038), et sur lequel se branchent les protocoles applicatifs (air-http, air-ssh). C’est le point réservé par air-socket (spec air-socket : « air-network, réservé au framework réseau couche 2 ») et le « substrat » de la note réseau §2.5.

Ce que air-network n’est PAS. Ce n’est pas un protocole filaire : il n’a pas de Framer/Codec/StateMachine propres (ceux-ci vivent dans les crates de protocole — air-tls, air-quic — selon le patron sans-IO 9-composants). air-network orchestre : il déclare une pile, résout un endpoint, pilote l’I/O (le seul étage qui touche sockets + horloge), surveille le chemin réseau, et expose une connexion sémantique de haut niveau. Il consomme les cœurs sans-IO ; il n’en réimplémente aucun.

Périmètre fonctionnel (checklist Network.framework, vocabulaire Air)

Concept Air≈ AppleRéalisation
Connexion sémantique bidirectionnelleNWConnectionAirConnection (async, sur air-runtime)
Écouteur de connexions entrantesNWListenerAirListener
Découverte de serviceNWBrowser / BonjourAirBrowser (sur air-mdns, différé)
Conscience du chemin réseauNWPath / NWPathMonitorAirNetworkPath / AirNetworkPathMonitor (netlink + sysfs)
Paramètres + pile de protocoles déclaratifsNWParameters / NWProtocolStackAirParameters / AirProtocolStack
TLS intégré par défaut(défaut Apple)AirParameters::secure() (sécurité-d’abord)
Endpoint (hôte/port/service/unix)NWEndpointAirEndpoint

Dépendances

  • Crates tierces externes : ZÉRO hors l’exception TLS déléguée à air-tls (ADR-042 : rustls+aws-lc-rs en production, ou la contingence maison pur Rust). air-network lui-même n’ajoute aucune dépendance C ni tierce → son propre check-c-surface est vert.
  • Crates Air (couches inférieures) :
    • air-socket (L1) — transports TCP/UDP/Unix + résolveur de noms enfichable ;
    • air-tls (L2) — sécurité de session (composée par défaut) ;
    • air-quic (L2, différé) — transport QUIC (débloque HTTP/3) ;
    • air-runtime (L1) — exécution async io_uring (buffers possédés, ADR-038) ;
    • air-device (L1) — énumération des interfaces réseau (sysfs) pour AirNetworkPath ;
    • air-mdns (L2, différé) — découverte de service pour AirBrowser ;
    • air-base-core (L1)AirError/AirResult (ADR-019), AirDuration.
  • Zéro unsafe exposé ; parsing/validation défensifs (Principe 3/4).

Dépendance — surveillance du chemin (AirNetworkPathMonitor). Observer les changements de lien/route/adresse exige rtnetlink (RTNLGRP_LINK/IPV4_IFADDR/ IPV6_IFADDR, RTM_NEWROUTE…). Tranché (ADR-079, décision BDFL 2026-07-07) : une crate air-netlink (couche 1) — transport netlink générique sans-IO (spec) — fournit rtnetlink ; AirNetworkPathMonitor consomme son RtnetlinkClient. Requiert l’additif couche 0 AF_NETLINK/sockaddr_nl (couche-0-v1.11).

Articulation avec le patron sans-IO (où air-network se place)

La note réseau §2.5 grave l’empilement :

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-network est le substrat transverse où ces piles se déclarent et se pilotent : il détient le pilote I/O commun (composant §2.1 de la note) qui câble les cœurs sans-IO empilés au transport réel air-socket + à l’horloge réelle air-runtime. Un cœur N consomme l’API du cœur N-1, pas son I/O ; le seul I/O réel vit ici, au bas de la pile.

Inventaire des objets à créer — ~20 objets

A. Surface publique — 16 objets

#Objet Air≈ AppleNature
1AirParametersNWParameterspile déclarée (transport + sécurité + options)
2AirParametersBuilderbuilder (secure()/plaintext()/with_tls…)
3AirProtocolStackNWProtocolStackpile ordonnée transport→sécurité→app
4AirTransport(choix transport)enum Tcp / Udp / Quic
5AirEndpointNWEndpointenum HostPort / Service / Unix
6AirConnectionNWConnectionconnexion async (état, send/receive)
7AirConnectionStateNWConnection.Stateenum Setup/Preparing/Ready/Failed/Cancelled
8AirContentContextNWConnection.ContentContextmétadonnées de message (final, expedited…)
9AirListenerNWListeneraccepteur de connexions entrantes
10AirBrowserNWBrowserdécouverte de service (mDNS)
11AirDiscoveredServiceNWBrowser.Resultservice découvert (nom, endpoint, txt)
12AirServiceType(_http._tcp…)newtype type de service DNS-SD
13AirNetworkPathNWPathinstantané du chemin réseau
14AirNetworkPathStatusNWPath.Statusenum Satisfied/Unsatisfied/RequiresConnection
15AirInterfaceTypeNWInterface.InterfaceTypeenum Wired/WiFi/Cellular/Loopback/Vpn/Other
16AirNetworkPathMonitorNWPathMonitorobserve les changements de chemin

B. Traits & valeurs — ~4 objets

#ObjetRôle
17AirNetworkErrorenum d’erreur (ADR-019) — résolution, transport, sécurité, chemin
18AirNetworkPathEventenum d’événement du monitor (PathChanged, BetterPathAvailable, Expensive…)
19AirProtocolOptionstrait — options par protocole (marqueur composé dans AirProtocolStack)
20AirConnectionGroup(optionnel) groupe de connexions partageant des paramètres (≈ NWConnectionGroup, multicast/UDP) — différé

Paramètres & pile déclarative

#![allow(unused)]
fn main() {
/// Déclaration d'une pile réseau. **Sécurité-d'abord** : `secure()` est le défaut
/// idiomatique ; `plaintext()` est explicite (jamais de clair par accident).
pub struct AirParameters { stack: AirProtocolStack, /* options */ }

impl AirParametersBuilder {
    /// TCP + TLS (défaut recommandé). `tls` = config `air-tls` (client ou serveur).
    pub fn secure(tls: Arc<air_tls::TlsClientConfig>) -> Self;
    /// QUIC (TLS 1.3 intégré, RFC 9000) — débloque HTTP/3. (air-quic différé.)
    pub fn quic(tls: Arc<air_tls::TlsClientConfig>) -> Self;
    /// UDP/TCP en clair — usage explicite (DNS, découverte, protocoles non chiffrés).
    pub fn plaintext(transport: AirTransport) -> Self;
    pub fn with_alpn(self, protocols: &[air_tls::AlpnProtocol]) -> Self;
    pub fn with_service_class(self, class: AirServiceClass) -> Self;  // best-effort/interactive…
    pub fn prohibit_expensive(self, yes: bool) -> Self;               // refuse liens onéreux
    pub fn prohibit_constrained(self, yes: bool) -> Self;             // refuse liens restreints
    pub fn require_interface_type(self, t: AirInterfaceType) -> Self; // épingle wifi/wired…
    pub fn build(self) -> AirResult<AirParameters>;                   // valide en amont (Principe 4)
}
}

Décision — sécurité par défaut. Le constructeur idiomatique est secure() (TLS composé). Le clair exige plaintext() explicite. On refuse le piège Apple/POSIX du « clair par omission ». Aligné air-tls (TLS 1.3-first) + ethos Air.

Endpoint

#![allow(unused)]
fn main() {
pub enum AirEndpoint {
    HostPort { host: AirHostName, port: u16 },     // résolu via air-socket (IPv6-first)
    Service  { name: AirServiceName, ty: AirServiceType, domain: AirDomain }, // mDNS/DNS-SD
    Unix     { path: air_base_core::AirPath /* chemin socket (FS) */ },
}
// AirHostName : newtype validé (IDNA/ASCII, jamais d'octets arbitraires en nom d'hôte).
}

Connexion (async, sur air-runtime)

#![allow(unused)]
fn main() {
pub enum AirConnectionState { Setup, Preparing, Ready, Failed(AirNetworkError), Cancelled }

impl AirConnection {
    /// Prépare une connexion sortante (résolution + handshake transport + TLS).
    /// Async : ne bloque pas ; l'état passe Setup→Preparing→Ready.
    pub async fn establish(endpoint: AirEndpoint, params: AirParameters) -> AirResult<Self>;

    pub fn state(&self) -> AirConnectionState;
    /// Notifie les transitions d'état (Ready, Failed…) — modèle événementiel.
    pub async fn await_ready(&mut self) -> AirResult<()>;

    /// Envoi ordonné. `context` porte les métadonnées (message final, expedited).
    pub async fn send(&mut self, data: &[u8], context: AirContentContext) -> AirResult<()>;
    /// Réception (buffers possédés, ADR-028 : zéro discard silencieux, ADR-032).
    pub async fn receive(&mut self, max: usize) -> AirResult<(Vec<u8>, AirContentContext)>;

    /// Le chemin réseau **de cette connexion** (interface courante, coût).
    pub fn current_path(&self) -> AirNetworkPath;

    pub async fn shutdown(&mut self) -> AirResult<()>;   // close_notify TLS + FIN
    pub fn cancel(&mut self);                            // abandon immédiat
}
}

Neutralité sync/async (ADR-038). L’API de 1ʳᵉ classe est async (une connexion sémantique est intrinsèquement événementielle). Une façade bloquante de confort (AirConnection::establish_blocking, send_blocking…) est fournie pour les usages one-shot/CLI, pilotée par le même cœur sur air-socket synchrone.

Écouteur & découverte

#![allow(unused)]
fn main() {
impl AirListener {
    pub async fn bind(endpoint: AirEndpoint, params: AirParameters) -> AirResult<Self>;
    /// Accepte la prochaine connexion entrante (déjà sécurisée si la pile le déclare).
    pub async fn accept(&mut self) -> AirResult<AirConnection>;
    pub fn local_endpoint(&self) -> AirEndpoint;
    /// Publie ce service en mDNS/DNS-SD (≈ NWListener.service). air-mdns, différé.
    pub fn advertise(&mut self, name: AirServiceName, ty: AirServiceType) -> AirResult<()>;
}

impl AirBrowser {                                   // ≈ NWBrowser (Bonjour), air-mdns
    pub async fn start(ty: AirServiceType, domain: AirDomain) -> AirResult<Self>;
    pub async fn next(&mut self) -> AirResult<AirBrowseChange>;  // Added/Removed service
}
}

Conscience du chemin réseau (path-awareness)

#![allow(unused)]
fn main() {
pub enum AirNetworkPathStatus { Satisfied, Unsatisfied, RequiresConnection }
pub enum AirInterfaceType { Wired, WiFi, Cellular, Loopback, Vpn, Other }

pub struct AirNetworkPath { /* snapshot immuable */ }
impl AirNetworkPath {
    pub fn status(&self) -> AirNetworkPathStatus;
    pub fn primary_interface(&self) -> Option<AirInterfaceType>;
    pub fn supports_ipv4(&self) -> bool;
    pub fn supports_ipv6(&self) -> bool;
    pub fn is_expensive(&self) -> bool;     // voir décision ci-dessous
    pub fn is_constrained(&self) -> bool;   // voir décision ci-dessous
}

impl AirNetworkPathMonitor {
    pub async fn start() -> AirResult<Self>;
    /// Prochain changement de chemin (up/down, nouvelle route, meilleur chemin).
    pub async fn next_event(&mut self) -> AirResult<AirNetworkPathEvent>;
    pub fn current(&self) -> AirNetworkPath;
}
}

Décision — expensive/constrained sur Linux (honnêteté d’implémentation). Linux n’a pas de bit noyau « lien onéreux/restreint » (contrairement à iOS). On les dérive :

  1. Type d’interface (air-device/sysfs) : wwan/cellularexpensive par défaut ; loopback/wired ⇒ ni l’un ni l’autre.
  2. Politique administrateur (air-config) : drapeau metered par interface (override explicite).
  3. Indices systemd-networkd / NetworkManager s’ils sont présents (best-effort).

is_expensive()/is_constrained() renvoient donc une heuristique documentée + surchargeable par politique, jamais un bit noyau. Cette dérivation est isolée et testable ; son absence de source noyau est explicite, pas masquée.

Réalisation Linux du monitor : énumération des interfaces via air-device (sysfs) + abonnement rtnetlink aux événements lien/adresse/route (dépendance air-netlink à résoudre, cf. supra). RequiresConnection ≈ portail captif / lien monté sans route par défaut.

Modèle multi-thread & async

  • AirParameters / configs : Send + Sync, partagés Arc entre connexions (immuables après build), comme air-tls.
  • AirConnection / AirListener : Send, !Sync — une connexion pilotée par une tâche à la fois (aucun verrou), scalabilité par N connexions indépendantes sur un exécuteur thread-per-core (ADR-038/039, sans work-stealing). Le key schedule TLS sous-jacent l’impose déjà (air-tls).
  • AirNetworkPathMonitor : source d’événements partageable prudemment (un lecteur) ; l’état de chemin (AirNetworkPath) est un snapshot immuable Send + Sync clonable.
  • Buffers possédés (ADR-028) de bout en bout ; back-pressure explicite ; zéro discard silencieux (ADR-032) sur receive.

Sécurité

  • TLS par défaut (secure()), clair explicite (plaintext()).
  • Endpoints validés en amont (AirHostName IDNA, ports typés) — la validation du nom d’hôte pour le certificat est portée par air-tls (ServerName, RFC 6125).
  • prohibit_expensive/prohibit_constrained/require_interface_type : la pile peut refuser un chemin non conforme à la politique (ex. ne jamais synchroniser sur cellulaire) — décision de sécurité/coût côté appelant, exprimée déclarativement.
  • Aucune surface C propre ; la seule surface C possible est celle, déléguée et nommée, d’air-tls (ADR-042). air-quic/air-mdns restent maison zéro-C.

Stratégie de tests

  • Couverture : couche 2, cible > 90 % (CLAUDE.md).
  • Composition : tests que AirParameters déclare correctement la pile (TCP+TLS, QUIC, clair) et que le pilote I/O câble les bons cœurs.
  • Loopback réel (comme air-socket/air-tls) : AirListenerAirConnection bout-en-bout, sécurisé, avec transfert + back-pressure + shutdown propre.
  • Path-awareness déterministe : AirNetworkPathMonitor piloté par des événements netlink simulés (interface up/down, route ajoutée/retirée) — virtual clock, pas de vrai réseau ; vérifier expensive/constrained dérivés + override de politique.
  • Découverte : AirBrowser/advertise sur boucle mDNS locale (quand air-mdns atterrit).
  • Interop : AirConnection vs serveurs de référence (OpenSSL/nginx) via air-tls.

Récapitulatif

FamilleObjetsRôle
A. Surface publique16Parameters/Stack/Endpoint/Connection/Listener/Browser/Path/Monitor
B. Traits & valeurs~4Error, PathEvent, ProtocolOptions, (ConnectionGroup différé)
Total~20substrat de composition (pas un protocole filaire)

Décisions de fond

  1. Substrat de composition, pas un protocoleair-network orchestre et pilote l’I/O ; les cœurs sans-IO (Framer/Codec/StateMachine) vivent dans air-tls/air-quic (note réseau §2.5). Pas de 10ᵉ composant.
  2. Couverture Apple, vocabulaire Air — Network.framework = checklist, jamais copie d’API (note api-reseau-strategie).
  3. Sécurité par défautsecure() (TLS composé) idiomatique ; clair explicite.
  4. Async 1ʳᵉ classe (ADR-038) sur air-runtime, façade bloquante de confort ; connexion Send/!Sync, Config Arc partagé (thread-per-core).
  5. Path-awareness honnête sur Linuxexpensive/constrained dérivés (interface + politique + indices), jamais un bit noyau inexistant ; dérivation isolée et testable.
  6. Zéro-C propre — la seule exception C est celle, déléguée/nommée, d’air-tls (ADR-042) ; air-network n’en ajoute aucune.

Travail à reprendre

  • air-netlink (L1)tranché (ADR-079, option B couche 1) : spec écrite (docs/specs/layer-1/air-netlink.md) ; reste l’additif couche 0 AF_NETLINK (couche-0-v1.11) puis le câblage de AirNetworkPathMonitor sur RtnetlinkClient.
  • air-quic (L2) — transport QUIC (débloque HTTP/3) ; AirParameters::quic.
  • air-mdns (L2) — découverte/publication de service ; AirBrowser/advertise.
  • AirConnectionGroup (multicast/UDP en groupe, ≈ NWConnectionGroup) — différé.
  • Équivalent URLSession (client HTTP/URL-loading haut niveau) au-dessus d’air-network — couche à confirmer (couche 4 élargie ?), cf. note api-reseau-strategie.
  • ABI C d’air-network : différée.

Licence du document : MPL 2.0 Statut : Spécification air-network (couche 2) v0.1 — substrat de connexion, couverture fonctionnelle Network.framework en vocabulaire Air. Compose air-socket (L1) + air-tls (L2) + air-runtime (L1). Implémentation à suivre.