Spec couche 1 — air-netlink (transport netlink générique, patron sans-IO)
Spécification technique — Version 0.1 (proposition). Couche 1 « Primitives système ». Tranchée par ADR-079.
Cadrage.
air-netlinkest un transport netlink générique de couche 1 (pair d’air-socket) : message framing, codec d’attributs TLV (rtattr/nlattr), corrélation requête/réponse/dump. Maison, pur Rust, zéro-C, zérounsafe. Il ne connaît aucune famille en propre :rtnetlink(routes/adresses/liens),nl80211(wifi),nftables,genetlinkse posent au-dessus de lui. Suit le patron sans-IO 9-composants (note réseau §2.2).
Position et méthode
Netlink est un protocole filaire kernel↔userspace : socket(AF_NETLINK, …) +
messages nlmsghdr porteurs d’attributs TLV. C’est un plan de contrôle (on
interroge/modifie l’état du noyau : routes, adresses, liens…), distinct du transport
de données d’air-socket. Comme air-socket::dns (sans-IO, fuzzé), le cœur de
air-netlink est pur (parse/sérialise/corrèle), le pilote I/O (le socket réel)
étant mince.
Premier consommateur : air-network::AirNetworkPathMonitor (L2) — énumérer et
surveiller interfaces/adresses/routes via rtnetlink. Autres consommateurs prévus :
daemons WireGuard/DHCP (L5), air-config (projection de routes), wifi (nl80211).
Dépendances
- Tierces externes : ZÉRO. Entièrement maison (netlink n’a pas de crypto).
- Couche 0 (après descellement
couche-0-v1.11, ADR-079) :SocketDomain::Netlink(AF_NETLINK = 16) — additif à l’enum typé ;SocketAddrNetlink(sockaddr_nl, 12 o) — additif pourbind;- réutilise tel quel
socket/bind/sendmsg/recvmsggénériques déjà présents (air-sys-syscall::net) — aucun nouveau syscall.
air-base-core(L1) —AirError/AirResult(ADR-019).- Zéro
unsafeexposé ; parsing TLV borné (get(), longueurs validées, alignementNLMSG_ALIGN/RTA_ALIGNvérifié — jamais de lecture hors-borne ni de boucle non bornée).
air-deviceinchangé (ADR-079) : son socketNETLINK_KOBJECT_UEVENTreste spécifique. Migration future surair-netlinkpossible, non requise.
Anatomie : les 9 composants appliqués à netlink
| # | Composant | Réalisation air-netlink | Pur ? |
|---|---|---|---|
| 1 | Framer | MessageFramer — délimite le flux en messages nlmsghdr (len/type/flags/seq/pid), gère le multipart (NLM_F_MULTI → NLMSG_DONE) et l’alignement | ✅ |
| 2 | Codec | MessageCodec (en-tête) + AttributeCodec (TLV rtattr/nlattr) — la surface hostile, décodage borné + imbrication (NLA_F_NESTED) | ✅ |
| 3 | StateMachine | RequestStateMachine — corrélation par numéro de séquence, sémantique dump (NLM_F_DUMP → multipart), ACK/erreur (NLMSG_ERROR, code errno) | ✅ |
| 4 | Handshaker | // NO HANDSHAKE : netlink n'a ni auth ni accord de clé. | — (absent voulu) |
| 5 | Flow Controller | // NO FLOW CONTROL : socket noyau. ENOBUFS (buffer plein) = signalé à l'appelant (resync dump), pas de fenêtrage. | — (absent voulu) |
| 6 | Multiplexer | // MINIMAL : abonnement à des groupes multicast (RTNLGRP_*) sur le socket ; pas de streams applicatifs. | ✅ (minimal) |
| 7 | Timer Manager | // MINIMAL : timeout de requête optionnel (horloge injectée). Pas de retransmission (kernel fiable en unicast). | ✅ (horloge injectée) |
| 8 | Session Context | NetlinkSession — compteur de séquence, nl_pid du socket, groupes abonnés, requêtes en attente | ✅ |
| 9 | Extension hooks | NetlinkFamily + modules de famille typés (rtnetlink, …) construits au-dessus du cœur générique | ✅ |
3 composants absents/minimaux marqués (Handshaker/Flow absents ; Mux/Timer minimaux) — l’absence est intentionnelle et visible (note §2.2).
Inventaire des objets — ~15 objets
A. Surface publique générique — ~9
| # | Objet | Rôle |
|---|---|---|
| 1 | NetlinkSocket | RAII — possède le fd AF_NETLINK ; open/bind/send/recv (pilote I/O) |
| 2 | NetlinkFamily | enum/protocole (Route, Generic, Netfilter…) au socket() |
| 3 | NetlinkGroup | groupe multicast à joindre (abonnement aux notifications) |
| 4 | NetlinkMessage<'a> | en-tête + payload (vue empruntée, zéro-copie) |
| 5 | NetlinkHeader | nlmsghdr typé (type, flags, seq, pid) |
| 6 | NetlinkAttribute<'a> | attribut TLV décodé (type + valeur bornée) |
| 7 | NetlinkRequest (+ Builder) | construit un message (flags REQUEST/ACK/DUMP) |
| 8 | DumpIter<'a> | itérateur borné sur une réponse multipart (dump) |
| 9 | NetlinkError | enum (ADR-019) — mappe NLMSG_ERROR (errno) + erreurs de décodage |
B. Cœur sans-IO — ~4
MessageFramer, MessageCodec + AttributeCodec (TLV), RequestStateMachine,
NetlinkSession.
C. Module rtnetlink (premier consommateur intégré) — ~2 + valeurs
RtnetlinkClient (helpers typés : list_links/list_addresses/list_routes en dump,
subscribe(link|addr|route)) + RtLink/RtAddress/RtRoute (vues typées des
attributs). (Les autres familles — nl80211, nftables — viendront en modules séparés.)
Codecs (bytes ↔ messages/attributs)
#![allow(unused)]
fn main() {
/// Lecteur borné + alignement netlink (NLMSG_ALIGN/RTA_ALIGN) — jamais d'indexation
/// paniquante, jamais de boucle sur une longueur mensongère (Principe 3).
pub struct NetlinkReader<'a> { /* … */ }
impl<'a> NetlinkReader<'a> {
pub fn header(&mut self) -> Result<NetlinkHeader, DecodeError>; // 16 o, len validée
pub fn attributes(&mut self) -> AttributeIter<'a>; // TLV bornés, alignés
pub fn nested(&self, attr: &NetlinkAttribute<'a>) -> AttributeIter<'a>; // NLA_F_NESTED
}
/// Attribut TLV : [ len(u16) ][ type(u16) ][ value… aligné 4 ].
pub struct NetlinkAttribute<'a> { pub kind: u16, pub value: &'a [u8] }
// AttributeCodec::decode : borne value par len ; refuse len < 4 ou débordant le message.
}
Décision — cœur générique, familles au-dessus. Le codec ne connaît pas la sémantique des attributs (
RTA_DST,IFLA_IFNAME…) : il rend des(type, bytes)bornés. Les modules de famille (rtnetlink) interprètent. ⇒ la surface hostile (décodage TLV) est une seule fois, fuzzée une seule fois, réutilisée partout.
Pilote I/O & machine à états (multi-thread, mono-propriétaire)
#![allow(unused)]
fn main() {
impl NetlinkSocket {
pub fn open(family: NetlinkFamily) -> AirResult<Self>; // socket(AF_NETLINK,…) RAII
pub fn bind(&self, groups: &[NetlinkGroup]) -> AirResult<()>; // sockaddr_nl
pub fn send(&self, request: &NetlinkRequest) -> AirResult<u32>; // rend le seq
pub fn recv<'b>(&self, buf: &'b mut [u8]) -> AirResult<NetlinkReader<'b>>;
}
}
- Cœur sans-IO testable sans socket : on lui jette des octets → messages/
attributs/erreurs (fuzz). Le pilote (
NetlinkSocket) est le seul contact couche 0. - Thread model :
NetlinkSocketSend/!Sync(un fd, un propriétaire, zéro verrou) — comme les objets fd d’air-socket. Plusieurs sockets = plusieurs threads, indépendants.
Sécurité
- Surface hostile = messages du noyau (attributs TLV en boucle). Défenses : toutes
les longueurs validées (
len ≥ 4,len ≤ reste du message), alignement vérifié, imbrication bornée (profondeur max surNLA_F_NESTED, anti-explosion), jamais d’allocation dictée par une longueur non vérifiée. Fuzz obligatoire surAttributeCodec/MessageFramer. - Authentification kernel : ne traiter que les messages dont
nl_pid == 0(émanant du noyau) pour les notifications — même discipline que le socket uevent d’air-device. - Privilèges : les opérations d’écriture (ajout de route/adresse) exigent
CAP_NET_ADMIN— l’appelant (daemon) porte le privilège ;air-netlinkn’élève rien.
Stratégie de tests
- Couverture 100 % (couche 1) lignes + branches.
- Cœur sans-IO : unitaires + property-based (round-trip
decode∘encode = idsur messages valides) + fuzz (fuzz_netlink_attributes,fuzz_netlink_message: octets hostiles → zéro panic, borne d’imbrication respectée). RequestStateMachine: dump multipart (→NLMSG_DONE),NLMSG_ERROR(errno mappé), corrélation de séquence.- Pilote I/O : test réel
AF_NETLINK/NETLINK_ROUTEsur la machine (dump des liens → au moinsloprésent), abonnement àRTNLGRP_LINK(interface synthétique up/down si root, sinon test décodeur). - Note fuzz : la surface de parsing d’octets externes est le cœur → fuzz couvert.
Récapitulatif & décisions
| Famille | Objets |
|---|---|
| A. Surface publique générique | ~9 |
| B. Cœur sans-IO | ~4 |
C. Module rtnetlink | ~2 + valeurs |
| Total | ~15 (3 composants absents/minimaux marqués) |
- Transport netlink générique, couche 1 (ADR-079, option B) — pair d’
air-socket, réutilisable par tous les consommateurs (path monitor, WireGuard, DHCP, config, wifi). - Cœur générique, familles au-dessus — la surface hostile TLV décodée une fois, fuzzée une fois.
- Zéro-C, zéro
unsafe, zéro tierce ; RAII pour le fd ;Send/!Sync. - Additif couche 0 modéré (
AF_NETLINK+sockaddr_nl) — syscalls génériques déjà présents, aucun nouveau syscall. air-deviceinchangé ; migration future possible, non requise.
Travail à reprendre
- Additif couche 0
couche-0-v1.11:SocketDomain::Netlink+SocketAddrNetlink(RFC de descellement, ADR-079) — prérequis d’implémentation. - Consommateur : câbler
air-network::AirNetworkPathMonitorsurRtnetlinkClient. - Familles supplémentaires :
nl80211(état/signal wifi → enrichitAirInterfaceType::WiFi),nftables(futurair-firewall),genetlink. - Migration du socket uevent d’
air-devicesurair-netlink(non bloquante). - ABI C différée.
Licence du document : MPL 2.0
Statut : Spécification air-netlink (couche 1) v0.1 — transport netlink générique
sans-IO, tranché par ADR-079 (option B, couche 1). Additif couche 0 AF_NETLINK
requis. Implémentation à suivre.