224 lines
7.7 KiB
Rust
224 lines
7.7 KiB
Rust
|
|
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<u32> {
|
||
|
|
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(())
|
||
|
|
}
|