Spec couche 1 — air-socket (sockets bas niveau + résolution de noms enfichable)
Spécification technique — Version 1.0. Couche 1 « Primitives système ».
Position et méthode
air-socket fournit les sockets typés (AirTcpSocket/Listener,
AirUdpSocket, AirUnixStream/Listener/Datagram), les adresses, et un
framework de résolution de noms enfichable. (Netlink retiré — cf. Section 3.) S’appuie sur air-base-lib
(AirError) et consomme la famille net de la couche 0 (socket/bind/listen/
connect/accept4/send/recv/sendmsg/recvmsg/shutdown/sockopts) + fs (lecture
/etc/hosts, /etc/resolv.conf). API Rust d’abord (ABI C différée).
Décision de couche : synchrone. connect/accept/read/write bloquent. La stack réseau asynchrone (io_uring) et le framework de connexion sémantique (
AirConnection, HTTP, TLS de session…) relèvent de la couche 2 ; la résolution async/annulable aussi (lancer le resolver synchrone sur un thread, ou un client DNS non-bloquant côté reactor).
Renommage validé : la crate sockets bas niveau s’appelle
air-socket(et nonair-network, réservé au framework réseau couche 2).
Carte de consommation couche 0 (dette doc) : sockets →
net(tout) ; résolution →fs(/etc/hosts,/etc/resolv.conf) +net(requêtes DNS UDP/TCP).
Section 1 — Adresses
#![allow(unused)]
fn main() {
/// Adresse IP (v4 ou v6). Couche **valeur** : parsing/affichage purs, **sans libc**
/// (`std::net::IpAddr` est pur-Rust ; seul `getaddrinfo` est libc, qu'on n'utilise
/// PAS). Type Air pour la surface uniforme / ABI C future ; enrobe `std::net` en interne.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AirIpAddress { V4([u8; 4]), V6([u8; 16]) }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AirSocketAddress { pub ip: AirIpAddress, pub port: u16 /* + scope_id v6 */ }
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AirUnixAddress { /* path (AirPath) OU abstract (octets, Linux) */ }
impl AirIpAddress {
pub fn parse(s: &str) -> AirResult<Self>; // via std::net (pur Rust)
pub fn is_v6(&self) -> bool;
}
impl AirSocketAddress { pub fn parse(s: &str) -> AirResult<Self>; /* "host:port" / "[v6]:port" */ }
}
Décision. Les valeurs d’adresse réutilisent std::net (parsing/octets/
affichage — pur Rust, zéro libc) ; on les enrobe en types Air (ABI C future).
La conversion vers air-sys-syscall::net::SocketAddr (sérialisation sockaddr)
fait le pont vers les syscalls couche 0.
Section 2 — Résolution de noms enfichable et ordonnée (le cœur conceptuel)
Décision de fond (override macro-archi). La macro-architecture disait « resolver système, pas de DNS Air spécifique ». Air change d’avis : pas de dépendance à la libc/NSS, et Air doit pouvoir offrir d’autres piles de résolution que le DNS. On bâtit donc un framework de résolution enfichable, où plusieurs sources (piles) sont consultées dans un ordre configurable ; notre propre client DNS est l’une d’elles (un fallback parmi d’autres). (À réconcilier dans la macro-archi — item passe-2.)
#![allow(unused)]
fn main() {
/// Une **source** de résolution (pile) : DNS, /etc/hosts, mDNS, custom…
pub trait AirNameResolutionSource: Send + Sync {
fn name(&self) -> &str;
/// Résout un nom → IPs. `Ok(vec![])` = « pas trouvé ici » (passer à la source
/// suivante) ; `Err` = erreur dure (stoppe la chaîne).
fn resolve(&self, hostname: &str) -> AirResult<Vec<AirIpAddress>>;
}
/// Résolveur : consulte une **liste ordonnée** de sources jusqu'au premier résultat.
pub struct AirNameResolver { /* Vec<Box<dyn AirNameResolutionSource>> */ }
impl AirNameResolver {
/// Défaut Air : `[hosts file, DNS]` (ordre par défaut, **configurable**).
pub fn system_default() -> AirResult<Self>;
pub fn with_sources(sources: Vec<Box<dyn AirNameResolutionSource>>) -> Self;
/// Ajoute / ordonne des piles (push = priorité la plus basse).
pub fn push_source(&mut self, source: Box<dyn AirNameResolutionSource>);
/// Résout, **IPv6 d'abord, IPv4 en fallback** (cf. décision ci-dessous) :
/// l'ordre des adresses rendues place les V6 avant les V4.
pub fn resolve(&self, hostname: &str) -> AirResult<Vec<AirIpAddress>>;
pub fn resolve_socket(&self, host: &str, port: u16) -> AirResult<Vec<AirSocketAddress>>;
}
}
Sources fournies
#![allow(unused)]
fn main() {
/// Pile `/etc/hosts` (parsing pur ; lecture via `fs`).
pub struct AirHostsFileSource { /* entrées chargées */ }
impl AirHostsFileSource { pub fn load() -> AirResult<Self>; }
/// **Client DNS Air** (RFC 1035 ; A + AAAA), **sans libc**. Nameservers depuis
/// `/etc/resolv.conf` (ou fournis). UDP, **repli TCP** sur réponse tronquée (bit TC).
pub struct AirDnsSource { /* nameservers: Vec<AirSocketAddress>, timeout, retries */ }
impl AirDnsSource {
pub fn from_resolv_conf() -> AirResult<Self>;
pub fn with_nameservers(servers: Vec<AirSocketAddress>) -> Self;
}
// (mDNS, LLMNR, sources custom : piles futures implémentant le trait.)
}
Décision : IPv6-first, IPv4-fallback (politique Air). Le résolveur interroge AAAA puis A, et ordonne les résultats V6 avant V4. Les helpers de connexion (
AirTcpSocket::connect_to_host) tentent donc l’IPv6 d’abord, l’IPv4 ensuite. (À ce niveau de couche, c’est surtout l’ordre des adresses rendues ; une vraie stratégie « Happy Eyeballs » concurrente relève de la couche 2.)
⚠️ Sécurité — parsing de réponses DNS = données externes hostiles. Le décodage des messages DNS (RFC 1035 : compression de noms par pointeurs, RR de longueur variable) est une surface de vulnérabilité classique. → fuzzing obligatoire (
cargo-fuzz) sur le parseur DNS (Principe 3) ; slicing viaget, bornes sur la décompression de noms (anti-boucle de pointeurs),checked_*. Zéro panique, zéro OOB sur toute entrée.
Section 3 — Sockets typés
#![allow(unused)]
fn main() {
// --- TCP ---
pub struct AirTcpSocket { /* OwnedFd connecté */ }
pub struct AirTcpListener { /* OwnedFd en écoute */ }
impl AirTcpSocket {
pub fn connect(addr: &AirSocketAddress) -> AirResult<Self>;
/// Résout `host` (résolveur fourni ou défaut) puis connecte **IPv6 d'abord**.
pub fn connect_to_host(host: &str, port: u16, resolver: &AirNameResolver) -> AirResult<Self>;
pub fn read(&mut self, buffer: &mut [u8]) -> AirResult<usize>;
pub fn write(&mut self, data: &[u8]) -> AirResult<usize>;
pub fn shutdown(&self, how: AirShutdown) -> AirResult<()>;
pub fn peer_address(&self) -> AirResult<AirSocketAddress>;
pub fn set_nodelay(&self, on: bool) -> AirResult<()>; // options typées (cf. family-net)
pub fn set_keepalive(&self, on: bool) -> AirResult<()>;
pub fn as_fd(&self) -> BorrowedFd<'_>; // intégration event loop (couche 2)
}
impl AirTcpListener {
pub fn bind(addr: &AirSocketAddress) -> AirResult<Self>; // SO_REUSEADDR par défaut ? cf. décision
pub fn listen(addr: &AirSocketAddress, backlog: u32) -> AirResult<Self>;
pub fn accept(&self) -> AirResult<(AirTcpSocket, AirSocketAddress)>; // CLOEXEC
pub fn local_address(&self) -> AirResult<AirSocketAddress>;
}
// --- UDP ---
pub struct AirUdpSocket { /* OwnedFd */ }
impl AirUdpSocket {
pub fn bind(addr: &AirSocketAddress) -> AirResult<Self>;
pub fn send_to(&self, data: &[u8], dest: &AirSocketAddress) -> AirResult<usize>;
pub fn recv_from(&self, buffer: &mut [u8]) -> AirResult<(usize, AirSocketAddress)>;
pub fn connect(&self, addr: &AirSocketAddress) -> AirResult<()>; // pour send/recv
}
// --- Unix (stream + datagram, path & abstract) — signatures FIGÉES (PASSE 2) ---
pub struct AirUnixStream { /* OwnedFd */ }
impl AirUnixStream {
pub fn connect(addr: &AirUnixAddress) -> AirResult<Self>;
pub fn read(&mut self, buffer: &mut [u8]) -> AirResult<usize>;
pub fn write(&mut self, data: &[u8]) -> AirResult<usize>;
/// Passage de FD (capability AirCom, ADR-001) via sendmsg/recvmsg + SCM_RIGHTS.
pub fn send_fd(&self, fd: BorrowedFd<'_>) -> AirResult<()>;
pub fn recv_fd(&self) -> AirResult<OwnedFd>; // reçu en CLOEXEC
pub fn shutdown(&self, how: AirShutdown) -> AirResult<()>;
pub fn as_fd(&self) -> BorrowedFd<'_>;
}
pub struct AirUnixListener { /* OwnedFd */ }
impl AirUnixListener {
pub fn bind(addr: &AirUnixAddress) -> AirResult<Self>;
pub fn accept(&self) -> AirResult<(AirUnixStream, AirUnixAddress)>; // pair souvent non lié
pub fn as_fd(&self) -> BorrowedFd<'_>;
}
pub struct AirUnixDatagram { /* OwnedFd */ }
impl AirUnixDatagram {
pub fn bind(addr: &AirUnixAddress) -> AirResult<Self>;
pub fn send_to(&self, data: &[u8], dest: &AirUnixAddress) -> AirResult<usize>;
pub fn recv_from(&self, buffer: &mut [u8]) -> AirResult<(usize, AirUnixAddress)>;
pub fn as_fd(&self) -> BorrowedFd<'_>;
}
// --- Netlink : RETIRÉ d'air-socket (décision 2026-06-15) ---
// La couche 0 scellée n'expose pas de socket netlink GÉNÉRIQUE (seul un socket
// uevent privé existe dans `device`). Netlink relève de la **configuration kernel**
// (plan de contrôle réseau), un concern distinct → crate `air-netlink`
// (**couche 1**, tranché ADR-079 ; révise la mention « couche 2 » de 2026-06-15),
// qui exige l'additif couche 0 `AF_NETLINK`/`sockaddr_nl` (`couche-0-v1.11`, RFC).
pub enum AirShutdown { Read, Write, Both }
}
Décisions sockets.
- CLOEXEC + (NOSIGNAL sur send) par défaut — hérité des invariants
netcouche 0. - Options typées (
set_nodelay/set_keepalive/set_reuseaddr…) — pas d’APIsetsockoptgénérique (ADR-021 conv. 3), via les options defamily-net. as_fd()exposé partout → la couche 2 enregistrera ces FD dans l’event loop io_uring (les façades synchrones ici, l’async là-haut).- Passage de FD Unix réutilise
sendmsg/recvmsg+SCM_RIGHTSdenet— c’est la matérialisation des capabilities AirCom (ADR-001).
Récapitulatif air-socket
| Domaine | API | Couche 0 / dép |
|---|---|---|
| Adresses | AirIpAddress, AirSocketAddress, AirUnixAddress | std::net (valeur, pur Rust) → pont net::SocketAddr |
| Résolution | AirNameResolver, AirNameResolutionSource, AirHostsFileSource, AirDnsSource | fs (hosts/resolv.conf), net (DNS UDP/TCP) |
| TCP/UDP | AirTcpSocket/Listener, AirUdpSocket | net |
| Unix | AirUnixStream/Listener/Datagram (+ passage de FD SCM_RIGHTS) | net |
| Netlink | retiré → crate air-netlink (couche 1, ADR-079 ; additif couche 0 AF_NETLINK requis) | — |
Différé : async + framework de connexion sémantique (couche 2) ; résolution annulable/concurrente (couche 2) ; mDNS/LLMNR et autres piles ; ABI C.
Dépendances (règle des 80 %, ADR-024)
air-base-lib,air-sys-syscall::net/::fs(couche 0),std::net(types adresse purs, sansgetaddrinfo— donc sans libc de résolution). Aucune dépendance externe (pas de crate DNS tierce : le client DNS est maison).
Stratégie de tests
- Couverture 100 % lignes + branches (Principe 1).
- Résolution :
AirHostsFileSource(fixtures/etc/hosts) ; ordre des piles (une pile renvoie vide → la suivante répond ; erreur dure → stoppe) ; IPv6-first (AAAA avant A dans l’ordre rendu) ;AirDnsSourcecontre un serveur DNS de test (réponses A/AAAA, troncature → repli TCP). - Fuzzing (cargo-fuzz) — obligatoire : parseur de réponses DNS (RFC 1035,
compression de noms : anti-boucle de pointeurs, bornes) et parseur
/etc/hosts. Zéro panique/OOB sur données hostiles. - Property-based : parsing d’adresses (
"[::1]:80", IPv4-mapped…), round-trip. - Intégration (loopback v4 et v6, AF_UNIX) : TCP connect/accept/echo, UDP
send/recv, Unix stream + passage de FD (envoyer un FD, le recevoir = test cœur
AirCom),
connect_to_host(résout via hosts puis connecte v6-first). - Doctests.
Décisions de fond
- Synchrone ; async + framework de connexion = couche 2.
- Résolution enfichable et ordonnée (override macro-archi) — sources DNS/hosts/…, ordre configurable ; client DNS maison, sans libc ; Air peut offrir d’autres piles que le DNS.
- IPv6-first, IPv4-fallback (politique Air) — ordre des adresses + ordre de connexion des helpers.
- Parsing DNS = surface hostile → fuzzing obligatoire (sécurité, Principe 3).
- Adresses sur
std::net(pur Rust, pasgetaddrinfo/libc) ; pont versnetcouche 0 pour les syscalls. - Options socket typées (ADR-021 c.3) ; CLOEXEC/NOSIGNAL par défaut ;
as_fdpour l’event loop couche 2.
Additifs planifiés — support air-mdns (descellement additif, ADR-080)
Statut.
air-socketest scellée (couche 1). Les deux additifs ci-dessous sont instruits pour la specair-mdns(mDNS/DNS-SD) ; ils entrent par descellement additif (délégation ADR-065, neutres non-sécurité) tranché par ADR-080. Purement additifs (aucune signature existante modifiée), zéro-C, zérounsafe.
A.1 — Multicast UDP typé (sur AirUdpSocket)
#![allow(unused)]
fn main() {
impl AirUdpSocket {
pub fn join_multicast_v4(&self, group: Ipv4Addr, interface: Ipv4Addr) -> AirResult<()>;
pub fn join_multicast_v6(&self, group: Ipv6Addr, interface_index: u32) -> AirResult<()>;
pub fn leave_multicast_v4(&self, group: Ipv4Addr, interface: Ipv4Addr) -> AirResult<()>;
pub fn leave_multicast_v6(&self, group: Ipv6Addr, interface_index: u32) -> AirResult<()>;
pub fn set_multicast_loop_v4(&self, on: bool) -> AirResult<()>;
pub fn set_multicast_ttl_v4(&self, ttl: u32) -> AirResult<()>;
pub fn set_multicast_hops_v6(&self, hops: u32) -> AirResult<()>;
pub fn set_multicast_interface_v4(&self, interface: Ipv4Addr) -> AirResult<()>;
}
}
- Fonctions dédiées typées (jamais d’
setsockoptbrut exposé, ADR-021 conv. 3). - Requiert l’additif couche 0
couche-0-v1.12:setsockoptstructuré (setsockopt_bytes, ADR-080) —IP_ADD_MEMBERSHIP/IPV6_ADD_MEMBERSHIPprennent unip_mreq/ipv6_mreq(structure), hors dusetsockoptentier d’ADR-071.
A.2 — Codec DNS exposé + étendu (SRV/TXT/PTR)
#![allow(unused)]
fn main() {
pub mod dns {
/// Codec de noms DNS **public** (encodage/décodage, **compression suivie AVEC
/// borne** — invariant anti-boucle déjà éprouvé/fuzzé). Réutilisé par air-mdns.
pub fn encode_name(out: &mut Vec<u8>, name: &DnsName<'_>);
pub fn read_name(msg: &[u8], offset: usize) -> AirResult<(DnsName<'static>, usize)>;
/// Types d'enregistrement ajoutés (A/AAAA existants) : DNS-SD.
pub enum ResourceRecord<'a> { A(Ipv4Addr), Aaaa(Ipv6Addr),
Ptr(DnsName<'a>), Srv { priority: u16, weight: u16, port: u16, target: DnsName<'a> },
Txt(TxtPairs<'a>) }
pub fn parse_records(message: &[u8]) -> AirResult<RecordIter<'_>>;
}
}
- Promeut le codec de noms (aujourd’hui privé,
skip_name/read_u16…) en module public sans-IO + ajoute SRV/TXT/PTR. Le résolveur A/AAAA existant est inchangé (même codec sous-jacent). Re-fuzz (fuzz_dns_parseétendu aux records SRV/TXT/PTR).
Travail à reprendre
- Additifs
air-mdns(ci-dessus, ADR-080) : multicast UDP typé (couche-0-v1.12setsockoptstructuré) + codec DNS public SRV/TXT/PTR. Instruits, non actifs avant descellement. - Piles de résolution supplémentaires : mDNS (→
air-mdns), LLMNR, sources custom. - Résolution async/annulable et framework de connexion (
AirConnection, HTTP, TLS) — couche 2. - ABI C d’
air-socket; autres crates couche 1 (air-crypto,air-device).
Licence du document : MPL 2.0
Statut : Spécification technique d’air-socket (couche 1). API Rust ; ABI C
différée. Synchrone ; résolution maison enfichable (DNS sans libc) ; IPv6-first.