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 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 non air-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 via get, 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 net couche 0.
  • Options typées (set_nodelay/set_keepalive/set_reuseaddr…) — pas d’API setsockopt générique (ADR-021 conv. 3), via les options de family-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_RIGHTS de net — c’est la matérialisation des capabilities AirCom (ADR-001).

Récapitulatif air-socket

DomaineAPICouche 0 / dép
AdressesAirIpAddress, AirSocketAddress, AirUnixAddressstd::net (valeur, pur Rust) → pont net::SocketAddr
RésolutionAirNameResolver, AirNameResolutionSource, AirHostsFileSource, AirDnsSourcefs (hosts/resolv.conf), net (DNS UDP/TCP)
TCP/UDPAirTcpSocket/Listener, AirUdpSocketnet
UnixAirUnixStream/Listener/Datagram (+ passage de FD SCM_RIGHTS)net
Netlinkretiré → 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, sans getaddrinfo — 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) ; AirDnsSource contre 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

  1. Synchrone ; async + framework de connexion = couche 2.
  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.
  3. IPv6-first, IPv4-fallback (politique Air) — ordre des adresses + ordre de connexion des helpers.
  4. Parsing DNS = surface hostile → fuzzing obligatoire (sécurité, Principe 3).
  5. Adresses sur std::net (pur Rust, pas getaddrinfo/libc) ; pont vers net couche 0 pour les syscalls.
  6. Options socket typées (ADR-021 c.3) ; CLOEXEC/NOSIGNAL par défaut ; as_fd pour l’event loop couche 2.

Additifs planifiés — support air-mdns (descellement additif, ADR-080)

Statut. air-socket est scellée (couche 1). Les deux additifs ci-dessous sont instruits pour la spec air-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éro unsafe.

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’setsockopt brut exposé, ADR-021 conv. 3).
  • Requiert l’additif couche 0 couche-0-v1.12 : setsockopt structuré (setsockopt_bytes, ADR-080) — IP_ADD_MEMBERSHIP/IPV6_ADD_MEMBERSHIP prennent un ip_mreq/ipv6_mreq (structure), hors du setsockopt entier 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.12 setsockopt structuré) + 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.