Spec couche 2 — air-network (substrat de connexion ≈ Network.framework)
Spécification technique — Version 0.1 (proposition). Couche 2 « Frameworks système ».
Cadrage.
air-networkvise 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 | ≈ Apple | Réalisation |
|---|---|---|
| Connexion sémantique bidirectionnelle | NWConnection | AirConnection (async, sur air-runtime) |
| Écouteur de connexions entrantes | NWListener | AirListener |
| Découverte de service | NWBrowser / Bonjour | AirBrowser (sur air-mdns, différé) |
| Conscience du chemin réseau | NWPath / NWPathMonitor | AirNetworkPath / AirNetworkPathMonitor (netlink + sysfs) |
| Paramètres + pile de protocoles déclaratifs | NWParameters / NWProtocolStack | AirParameters / AirProtocolStack |
| TLS intégré par défaut | (défaut Apple) | AirParameters::secure() (sécurité-d’abord) |
| Endpoint (hôte/port/service/unix) | NWEndpoint | AirEndpoint |
Dépendances
- Crates tierces externes : ZÉRO hors l’exception TLS déléguée à
air-tls(ADR-042 :rustls+aws-lc-rsen production, ou la contingence maison pur Rust).air-networklui-même n’ajoute aucune dépendance C ni tierce → son proprecheck-c-surfaceest 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) pourAirNetworkPath;air-mdns(L2, différé) — découverte de service pourAirBrowser;air-base-core(L1) —AirError/AirResult(ADR-019),AirDuration.
- Zéro
unsafeexposé ; 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 crateair-netlink(couche 1) — transport netlink générique sans-IO (spec) — fournitrtnetlink;AirNetworkPathMonitorconsomme sonRtnetlinkClient. Requiert l’additif couche 0AF_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 | ≈ Apple | Nature |
|---|---|---|---|
| 1 | AirParameters | NWParameters | pile déclarée (transport + sécurité + options) |
| 2 | AirParametersBuilder | — | builder (secure()/plaintext()/with_tls…) |
| 3 | AirProtocolStack | NWProtocolStack | pile ordonnée transport→sécurité→app |
| 4 | AirTransport | (choix transport) | enum Tcp / Udp / Quic |
| 5 | AirEndpoint | NWEndpoint | enum HostPort / Service / Unix |
| 6 | AirConnection | NWConnection | connexion async (état, send/receive) |
| 7 | AirConnectionState | NWConnection.State | enum Setup/Preparing/Ready/Failed/Cancelled |
| 8 | AirContentContext | NWConnection.ContentContext | métadonnées de message (final, expedited…) |
| 9 | AirListener | NWListener | accepteur de connexions entrantes |
| 10 | AirBrowser | NWBrowser | découverte de service (mDNS) |
| 11 | AirDiscoveredService | NWBrowser.Result | service découvert (nom, endpoint, txt) |
| 12 | AirServiceType | (_http._tcp…) | newtype type de service DNS-SD |
| 13 | AirNetworkPath | NWPath | instantané du chemin réseau |
| 14 | AirNetworkPathStatus | NWPath.Status | enum Satisfied/Unsatisfied/RequiresConnection |
| 15 | AirInterfaceType | NWInterface.InterfaceType | enum Wired/WiFi/Cellular/Loopback/Vpn/Other |
| 16 | AirNetworkPathMonitor | NWPathMonitor | observe les changements de chemin |
B. Traits & valeurs — ~4 objets
| # | Objet | Rôle |
|---|---|---|
| 17 | AirNetworkError | enum d’erreur (ADR-019) — résolution, transport, sécurité, chemin |
| 18 | AirNetworkPathEvent | enum d’événement du monitor (PathChanged, BetterPathAvailable, Expensive…) |
| 19 | AirProtocolOptions | trait — options par protocole (marqueur composé dans AirProtocolStack) |
| 20 | AirConnectionGroup | (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 exigeplaintext()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 surair-socketsynchrone.
É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/constrainedsur Linux (honnêteté d’implémentation). Linux n’a pas de bit noyau « lien onéreux/restreint » (contrairement à iOS). On les dérive :
- Type d’interface (
air-device/sysfs) :wwan/cellular⇒expensivepar défaut ;loopback/wired⇒ ni l’un ni l’autre.- Politique administrateur (
air-config) : drapeau metered par interface (override explicite).- 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ésArcentre connexions (immuables aprèsbuild), commeair-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 immuableSend + Syncclonable.- 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 (
AirHostNameIDNA, ports typés) — la validation du nom d’hôte pour le certificat est portée parair-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-mdnsrestent maison zéro-C.
Stratégie de tests
- Couverture : couche 2, cible > 90 % (CLAUDE.md).
- Composition : tests que
AirParametersdé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) :AirListener↔AirConnectionbout-en-bout, sécurisé, avec transfert + back-pressure + shutdown propre. - Path-awareness déterministe :
AirNetworkPathMonitorpiloté par des événements netlink simulés (interface up/down, route ajoutée/retirée) — virtual clock, pas de vrai réseau ; vérifierexpensive/constraineddérivés + override de politique. - Découverte :
AirBrowser/advertisesur boucle mDNS locale (quandair-mdnsatterrit). - Interop :
AirConnectionvs serveurs de référence (OpenSSL/nginx) viaair-tls.
Récapitulatif
| Famille | Objets | Rôle |
|---|---|---|
| A. Surface publique | 16 | Parameters/Stack/Endpoint/Connection/Listener/Browser/Path/Monitor |
| B. Traits & valeurs | ~4 | Error, PathEvent, ProtocolOptions, (ConnectionGroup différé) |
| Total | ~20 | substrat de composition (pas un protocole filaire) |
Décisions de fond
- Substrat de composition, pas un protocole —
air-networkorchestre et pilote l’I/O ; les cœurs sans-IO (Framer/Codec/StateMachine) vivent dansair-tls/air-quic(note réseau §2.5). Pas de 10ᵉ composant. - Couverture Apple, vocabulaire Air — Network.framework = checklist, jamais copie
d’API (note
api-reseau-strategie). - Sécurité par défaut —
secure()(TLS composé) idiomatique ; clair explicite. - Async 1ʳᵉ classe (ADR-038) sur
air-runtime, façade bloquante de confort ; connexionSend/!Sync,ConfigArcpartagé (thread-per-core). - Path-awareness honnête sur Linux —
expensive/constraineddérivés (interface + politique + indices), jamais un bit noyau inexistant ; dérivation isolée et testable. - Zéro-C propre — la seule exception C est celle, déléguée/nommée, d’
air-tls(ADR-042) ;air-networkn’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 0AF_NETLINK(couche-0-v1.11) puis le câblage deAirNetworkPathMonitorsurRtnetlinkClient.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. noteapi-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.