use std::net::{IpAddr, Ipv4Addr}; use std::os::fd::AsRawFd; use anyhow::{anyhow, bail, Context, Result}; use futures_util::TryStreamExt; use rtnetlink::{new_connection, Handle, LinkUnspec, LinkVeth, NetworkNamespace}; use crate::audit; use crate::naming::{veth_names, VethNames}; async fn find_link_index(handle: &Handle, name: &str) -> Result { let mut links = handle.link().get().match_name(name.to_string()).execute(); match links .try_next() .await .with_context(|| format!("recherche de l'interface {name}"))? { Some(link) => Ok(link.header.index), None => bail!("interface {name} introuvable"), } } /// Crée un service isolé : un network namespace + une paire veth qui le /// relie au namespace racine. Labo pédagogique un-seul-poste : ce n'est /// jamais branché sur le trafic réel de tripoli-edge/tripoli-api, qui /// continuent de tourner nativement sur l'hôte. pub async fn create(service_name: &str, subnet_id: u8) -> Result<()> { let VethNames { host, peer } = veth_names(service_name); let ns_path = format!("/run/netns/{service_name}"); NetworkNamespace::add(service_name.to_string()) .await .map_err(|e| anyhow!("création du namespace : {e}"))?; audit::log("namespace_created", service_name, &ns_path, "ok"); let (connection, handle, _) = new_connection().context("connexion netlink (namespace racine)")?; tokio::spawn(connection); handle .link() .add(LinkVeth::new(&host, &peer).build()) .execute() .await .with_context(|| format!("création de la paire veth {host}/{peer}"))?; // L'index du pair doit être résolu AVANT de le déplacer dans le // namespace : une fois déplacé, il n'est plus visible depuis le // handle netlink de la racine. let peer_index = find_link_index(&handle, &peer).await?; let host_index = find_link_index(&handle, &host).await?; let ns_file = std::fs::File::open(&ns_path) .with_context(|| format!("ouverture de {ns_path}"))?; handle .link() .set(LinkUnspec::new_with_index(peer_index).setns_by_fd(ns_file.as_raw_fd()).build()) .execute() .await .context("déplacement de l'interface pair dans le namespace")?; let host_ip = Ipv4Addr::new(10, 200, subnet_id, 1); let peer_ip = Ipv4Addr::new(10, 200, subnet_id, 2); handle .address() .add(host_index, IpAddr::V4(host_ip), 30) .execute() .await .context("adresse côté hôte")?; handle .link() .set(LinkUnspec::new_with_index(host_index).up().build()) .execute() .await .context("activation de l'interface hôte")?; audit::log( "veth_created", &format!("{host}<->{peer}"), &format!("hôte {host_ip}/30, namespace {service_name}"), "ok", ); configure_inside_namespace(ns_path.clone(), peer.clone(), peer_ip) .context("configuration à l'intérieur du namespace")?; audit::log( "namespace_interface_configured", service_name, &format!("{peer} {peer_ip}/30"), "ok", ); println!("service '{service_name}' prêt :"); println!(" hôte : {host} {host_ip}/30 (namespace racine)"); println!(" ns : {peer} {peer_ip}/30 (namespace {service_name}, {ns_path})"); Ok(()) } /// Entre dans le namespace cible pour y configurer l'adresse et activer /// l'interface - et `lo`, qui démarre toujours down dans un namespace /// neuf. Volontairement un `std::thread::spawn` dédié, jamais réutilisé /// (pas `tokio::task::spawn_blocking`, dont les threads sont mis en /// pool et réutilisés) : `setns` change la namespace RÉSEAU du thread /// appelant pour toute sa durée de vie, et laisser un thread du pool /// tokio « coincé » dans un namespace ferait planter n'importe quelle /// tâche bloquante ultérieure qui tomberait dessus. fn configure_inside_namespace(ns_path: String, if_name: String, ip: Ipv4Addr) -> Result<()> { let handle = std::thread::spawn(move || -> Result<()> { let ns_file = std::fs::File::open(&ns_path).with_context(|| format!("ouverture de {ns_path}"))?; nix::sched::setns(&ns_file, nix::sched::CloneFlags::CLONE_NEWNET) .map_err(|e| anyhow!("setns : {e}"))?; let rt = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .context("runtime tokio dédié à ce thread")?; rt.block_on(async move { let (connection, handle, _) = new_connection().context("connexion netlink (dans le namespace)")?; tokio::spawn(connection); if let Ok(lo_index) = find_link_index(&handle, "lo").await { handle .link() .set(LinkUnspec::new_with_index(lo_index).up().build()) .execute() .await .context("activation de lo dans le namespace")?; } let if_index = find_link_index(&handle, &if_name).await?; handle .address() .add(if_index, IpAddr::V4(ip), 30) .execute() .await .context("adresse côté namespace")?; handle .link() .set(LinkUnspec::new_with_index(if_index).up().build()) .execute() .await .context("activation de l'interface dans le namespace")?; Ok(()) }) }); handle .join() .map_err(|_| anyhow!("le thread de configuration a paniqué"))? } /// Supprime la paire veth (la suppression d'un bout supprime /// automatiquement l'autre, même dans un namespace différent - c'est /// une propriété du noyau, pas quelque chose que ce code doit gérer), /// puis le namespace lui-même. pub async fn destroy(service_name: &str) -> Result<()> { let VethNames { host, .. } = veth_names(service_name); let (connection, handle, _) = new_connection().context("connexion netlink")?; tokio::spawn(connection); match find_link_index(&handle, &host).await { Ok(index) => { handle .link() .del(index) .execute() .await .with_context(|| format!("suppression de {host}"))?; audit::log("veth_deleted", &host, service_name, "ok"); } Err(_) => { audit::log("veth_deleted", &host, service_name, "absente, rien à faire"); } } NetworkNamespace::del(service_name.to_string()) .await .map_err(|e| anyhow!("suppression du namespace : {e}"))?; audit::log("namespace_deleted", service_name, "", "ok"); println!("service '{service_name}' détruit"); Ok(()) } pub async fn list() -> Result<()> { let mut names = Vec::new(); if let Ok(entries) = std::fs::read_dir("/run/netns") { for entry in entries.flatten() { if let Some(name) = entry.file_name().to_str() { names.push(name.to_string()); } } } if names.is_empty() { println!("aucun namespace tripoli-netctl actif"); return Ok(()); } let (connection, handle, _) = new_connection().context("connexion netlink")?; tokio::spawn(connection); for name in names { let VethNames { host, .. } = veth_names(&name); match find_link_index(&handle, &host).await { Ok(_) => println!("{name} : veth {host} présente côté hôte"), Err(_) => { println!("{name} : namespace présent, pas de veth {host} associée") } } } Ok(()) }