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).

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

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.