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).
Additif réalisé (2026-07-31, ADR-116 inc.1) — identité du pair Unix.
#![allow(unused)]
fn main() {
impl AirUnixSeqpacket {
/// `{pid, uid, gid}` du pair, tels que le noyau les a FIGÉS à la connexion.
pub fn peer_credentials(&self) -> AirResult<UnixCredentials>; // SO_PEERCRED
}
}
Sur SO_PEERCRED, le pair ne déclare pas son identité : le noyau la constate,
au moment de connect/socketpair, et un client ne peut donc pas mentir. C’est la
seule façon d’authentifier un appelant local sans protocole applicatif, et c’est ce
qui rend acceptable un service qui signe à la demande avec des secrets — air-agent
(ADR-116 §1 : seul le propriétaire
pilote son agent). Le verrouillage 0600 du chemin du socket reste de la défense en
profondeur ; c’est ce contrôle-ci qui fait foi. Rendu d’abord sur le seul
AirUnixSeqpacket (le besoin établi ; cf. l’additif suivant) ; les SCM_CREDENTIALS
ancillaires (SO_PASSCRED, transmis par message — propriété d’un message, pas
d’une connexion) restent hors périmètre.
Additif réalisé (2026-07-31, ADR-116 inc.3b) — identité du pair sur un FLUX.
#![allow(unused)]
fn main() {
impl AirUnixStream {
/// Idem sur SOCK_STREAM — même garantie noyau, même usage.
pub fn peer_credentials(&self) -> AirResult<UnixCredentials>; // SO_PEERCRED
}
}
Strictement le pendant flux du précédent, et il est requis par le transport, pas
par un goût d’API : le protocole natif d’air-agent circule en SOCK_SEQPACKET, mais
le protocole ssh-agent d’OpenSSH (SSH_AUTH_SOCK, celui que parlent ssh(1) et
git) circule sur un SOCK_STREAM — imposé par OpenSSH. Servir ce protocole sans
pouvoir constater l’uid de l’appelant reviendrait à ouvrir un oracle de signature au
premier processus venu ; le contrôle d’appelant doit donc exister des deux côtés du
choix de transport, sans quoi c’est le transport qui déciderait de la politique de
sécurité. Une ligne de délégation, zéro sémantique nouvelle.
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.