Phase 4 : tripoli-netctl, labo network namespaces + veth (rtnetlink)
- CLI (clap) : create/destroy/list. Network namespace créé via rtnetlink::NetworkNamespace (fork + unshare(CLONE_NEWNET) + bind-mount, équivalent de `ip netns add`), paire veth créée dans la racine, un bout déplacé dans le namespace (setns_by_fd) et adressé depuis un thread OS dédié qui fait setns() - jamais un thread du pool spawn_blocking de tokio, qui serait réutilisé pour d'autres tâches et resterait coincé dans le namespace. - Journal d'audit JSON (stderr + /var/log/tripoli-netctl/audit.log) à chaque opération. - Documenté explicitement comme labo pédagogique un-seul-poste : tripoli-edge et tripoli-api restent nativement sur l'hôte, ce projet ne pilote aucun trafic de production. - Testé en conditions réelles sur le serveur : création, adressage des deux côtés (dont lo, down par défaut dans un netns neuf), ping traversant effectivement la veth, puis destruction propre (veth + namespace) vérifiée par absence des interfaces et du namespace. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
546734ad77
commit
991f4dd04f
9 changed files with 688 additions and 2 deletions
238
Cargo.lock
generated
238
Cargo.lock
generated
|
|
@ -26,6 +26,62 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstream"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"anstyle-parse",
|
||||||
|
"anstyle-query",
|
||||||
|
"anstyle-wincon",
|
||||||
|
"colorchoice",
|
||||||
|
"is_terminal_polyfill",
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle"
|
||||||
|
version = "1.0.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-parse"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||||
|
dependencies = [
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-query"
|
||||||
|
version = "1.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-wincon"
|
||||||
|
version = "3.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"once_cell_polyfill",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.104"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "argon2"
|
name = "argon2"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
|
|
@ -208,6 +264,12 @@ version = "1.0.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.45"
|
version = "0.4.45"
|
||||||
|
|
@ -222,6 +284,52 @@ dependencies = [
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap"
|
||||||
|
version = "4.6.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
|
||||||
|
dependencies = [
|
||||||
|
"clap_builder",
|
||||||
|
"clap_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_builder"
|
||||||
|
version = "4.6.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
|
||||||
|
dependencies = [
|
||||||
|
"anstream",
|
||||||
|
"anstyle",
|
||||||
|
"clap_lex",
|
||||||
|
"strsim",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_derive"
|
||||||
|
version = "4.6.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_lex"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorchoice"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const-oid"
|
name = "const-oid"
|
||||||
version = "0.9.6"
|
version = "0.9.6"
|
||||||
|
|
@ -891,6 +999,12 @@ version = "2.12.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0"
|
checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is_terminal_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
|
|
@ -1074,6 +1188,77 @@ version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d"
|
checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "netlink-packet-core"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2d6e2daf9a8c2e21302714706860a0e6be02e03d17294ecc66b354ea7d4059dd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "netlink-packet-route"
|
||||||
|
version = "0.33.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59d48ffc8b73a506e1ff0878528c72668f2bfbc3d875b6be890a96be5d0d5576"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"netlink-packet-core",
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "netlink-proto"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "97e03ae6247c6cdb499e8f14e98c72b83954d85822b687056e3a21150b438f2b"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"netlink-packet-core",
|
||||||
|
"netlink-sys",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "netlink-sys"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c99d38e00b420df49e940fe0826e667c3dad82ac68eb4c2bbf754c1c14a83a10"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.30.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cfg-if",
|
||||||
|
"cfg_aliases",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.31.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cfg-if",
|
||||||
|
"cfg_aliases",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nu-ansi-term"
|
name = "nu-ansi-term"
|
||||||
version = "0.50.3"
|
version = "0.50.3"
|
||||||
|
|
@ -1140,6 +1325,12 @@ version = "1.21.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking"
|
name = "parking"
|
||||||
version = "2.2.1"
|
version = "2.2.1"
|
||||||
|
|
@ -1381,6 +1572,24 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rtnetlink"
|
||||||
|
version = "0.23.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "90f60a3ee149005c5819e03e3d73ba4a470bc18b52b0c8422cfe1bea2cae63c6"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"netlink-packet-core",
|
||||||
|
"netlink-packet-route",
|
||||||
|
"netlink-proto",
|
||||||
|
"netlink-sys",
|
||||||
|
"nix 0.30.1",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.23.45"
|
version = "0.23.45"
|
||||||
|
|
@ -1821,6 +2030,12 @@ dependencies = [
|
||||||
"unicode-properties",
|
"unicode-properties",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
|
|
@ -2171,6 +2386,23 @@ dependencies = [
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tripoli-netctl"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"chrono",
|
||||||
|
"clap",
|
||||||
|
"futures-util",
|
||||||
|
"nix 0.31.3",
|
||||||
|
"rtnetlink",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "try-lock"
|
name = "try-lock"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
|
|
@ -2240,6 +2472,12 @@ version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.26.1"
|
version = "1.26.1"
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = ["tripoli-edge", "tripoli-api"]
|
members = ["tripoli-edge", "tripoli-api", "tripoli-netctl"]
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ tenté, non simulé.
|
||||||
| OpenTofu (serveur + DNS) | Code écrit — `tofu/` ; serveur en donnée en lecture seule (jamais recréé/détruit), DNS géré via le provider OVH. `plan`/`apply` pas encore exécutés (nécessitent des identifiants OVH/Hetzner non fournis) |
|
| OpenTofu (serveur + DNS) | Code écrit — `tofu/` ; serveur en donnée en lecture seule (jamais recréé/détruit), DNS géré via le provider OVH. `plan`/`apply` pas encore exécutés (nécessitent des identifiants OVH/Hetzner non fournis) |
|
||||||
| `tripoli-edge` (Rust, Phase 2) | Fait et déployé en prod — `tripoli-edge/`, service systemd, derrière Caddy, vérifié via `https://tripoli.cloud/healthz` et `/metrics` |
|
| `tripoli-edge` (Rust, Phase 2) | Fait et déployé en prod — `tripoli-edge/`, service systemd, derrière Caddy, vérifié via `https://tripoli.cloud/healthz` et `/metrics` |
|
||||||
| `tripoli-api` (Rust, Phase 3) | Fait et déployé en prod — `tripoli-api/`, auth par session (Argon2), upload/download, PostgreSQL dédié (Docker), vérifié via `https://tripoli.cloud/api/...` |
|
| `tripoli-api` (Rust, Phase 3) | Fait et déployé en prod — `tripoli-api/`, auth par session (Argon2), upload/download, PostgreSQL dédié (Docker), vérifié via `https://tripoli.cloud/api/...` |
|
||||||
| `tripoli-netctl` (Rust, Phase 4) | Pas commencé |
|
| `tripoli-netctl` (Rust, Phase 4) | Fait — `tripoli-netctl/`, labo pédagogique (netns + veth via `rtnetlink`), testé sur le serveur (création, ping traversant la veth, destruction propre), **volontairement pas branché sur le trafic de prod** |
|
||||||
| Observabilité dédiée (Phase 5) | Pas commencé (une stack Prometheus/Grafana existe déjà sur le serveur, partagée avec les autres projets) |
|
| Observabilité dédiée (Phase 5) | Pas commencé (une stack Prometheus/Grafana existe déjà sur le serveur, partagée avec les autres projets) |
|
||||||
| Bird/BGP (Phase 6, bonus) | Pas commencé |
|
| Bird/BGP (Phase 6, bonus) | Pas commencé |
|
||||||
|
|
||||||
|
|
|
||||||
17
tripoli-netctl/Cargo.toml
Normal file
17
tripoli-netctl/Cargo.toml
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
[package]
|
||||||
|
name = "tripoli-netctl"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "time"] }
|
||||||
|
rtnetlink = "0.23"
|
||||||
|
nix = { version = "0.31", features = ["sched", "user"] }
|
||||||
|
futures-util = "0.3"
|
||||||
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
anyhow = "1"
|
||||||
84
tripoli-netctl/README.md
Normal file
84
tripoli-netctl/README.md
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
# tripoli-netctl
|
||||||
|
|
||||||
|
Labo pédagogique un-seul-poste sur les network namespaces Linux et la
|
||||||
|
crate `rtnetlink`. **Ne pilote aucun service de production** :
|
||||||
|
tripoli-edge et tripoli-api continuent de tourner nativement sur l'hôte,
|
||||||
|
dans le namespace racine. Ce n'est pas un CNI, pas un orchestrateur -
|
||||||
|
juste une démonstration, sur une seule machine, de ce qui se passe sous
|
||||||
|
le capot quand un outil comme Docker ou Kubernetes isole un service en
|
||||||
|
réseau.
|
||||||
|
|
||||||
|
## Ce que fait `create <name>`
|
||||||
|
|
||||||
|
1. Crée un network namespace nommé `<name>` (`rtnetlink::NetworkNamespace::add`,
|
||||||
|
équivalent de `ip netns add` - fork + `unshare(CLONE_NEWNET)` +
|
||||||
|
bind-mount de `/proc/self/ns/net` sur `/run/netns/<name>`).
|
||||||
|
2. Crée une paire veth (`tv-<name>h` / `tv-<name>p`, noms tronqués à 6
|
||||||
|
caractères alphanumériques pour respecter la limite de 15 caractères
|
||||||
|
des interfaces Linux).
|
||||||
|
3. Déplace `tv-<name>p` dans le namespace créé (`setns_by_fd`).
|
||||||
|
4. Adresse le côté hôte (`10.200.<subnet-id>.1/30`) et l'active.
|
||||||
|
5. Depuis un thread OS dédié qui fait `setns()` vers le namespace cible,
|
||||||
|
adresse le côté namespace (`10.200.<subnet-id>.2/30`), l'active, et
|
||||||
|
active `lo` (qui démarre toujours *down* dans un namespace neuf -
|
||||||
|
détail facile à oublier, source classique de "ça ne marche pas" dans
|
||||||
|
ce genre de labo).
|
||||||
|
|
||||||
|
Chaque étape est journalisée en JSON sur stderr et dans
|
||||||
|
`/var/log/tripoli-netctl/audit.log`.
|
||||||
|
|
||||||
|
## Pourquoi un thread dédié, pas `tokio::task::spawn_blocking`
|
||||||
|
|
||||||
|
`setns()` change la namespace réseau du **thread appelant**, pour toute
|
||||||
|
sa durée de vie. Les threads de `spawn_blocking` sont mis en pool et
|
||||||
|
réutilisés par tokio pour d'autres tâches bloquantes : y laisser un
|
||||||
|
thread « coincé » dans un namespace réseau casserait silencieusement
|
||||||
|
n'importe quelle tâche bloquante ultérieure qui tomberait dessus. Un
|
||||||
|
`std::thread::spawn` dédié, jamais réutilisé, élimine le risque.
|
||||||
|
|
||||||
|
## `destroy <name>`
|
||||||
|
|
||||||
|
Supprime le côté hôte de la veth - le noyau supprime automatiquement le
|
||||||
|
pair, même déplacé dans un autre namespace, dès qu'un des deux bouts
|
||||||
|
disparaît - puis démonte/supprime le fichier de namespace.
|
||||||
|
|
||||||
|
## `list`
|
||||||
|
|
||||||
|
Énumère `/run/netns` et vérifie, pour chacun, la présence de la veth
|
||||||
|
hôte associée.
|
||||||
|
|
||||||
|
## Utilisation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ./tripoli-netctl create demo --subnet-id 5
|
||||||
|
sudo ./tripoli-netctl list
|
||||||
|
sudo ./tripoli-netctl destroy demo
|
||||||
|
```
|
||||||
|
|
||||||
|
Root est requis (namespaces + netlink privilégié) - vérifié explicitement
|
||||||
|
au démarrage plutôt que de laisser échouer la première opération avec un
|
||||||
|
message obscur.
|
||||||
|
|
||||||
|
## Limites assumées (v1)
|
||||||
|
|
||||||
|
- Pas d'allocation automatique de sous-réseau : `--subnet-id` est
|
||||||
|
explicite, une collision entre deux services donnerait une erreur
|
||||||
|
claire plutôt qu'une allocation silencieuse qui la masquerait.
|
||||||
|
- IPv4 uniquement.
|
||||||
|
- Un seul veth par service - pas de topologie plus complexe (bridge,
|
||||||
|
plusieurs interfaces).
|
||||||
|
|
||||||
|
## Vérifié
|
||||||
|
|
||||||
|
```
|
||||||
|
$ sudo tripoli-netctl create demo1 --subnet-id 5
|
||||||
|
service 'demo1' prêt :
|
||||||
|
hôte : tv-demo1h 10.200.5.1/30 (namespace racine)
|
||||||
|
ns : tv-demo1p 10.200.5.2/30 (namespace demo1, /run/netns/demo1)
|
||||||
|
|
||||||
|
$ ping -c 2 10.200.5.2 # depuis la racine, vers le namespace
|
||||||
|
2 packets transmitted, 2 received, 0% packet loss
|
||||||
|
|
||||||
|
$ sudo tripoli-netctl destroy demo1
|
||||||
|
service 'demo1' détruit # veth et namespace bien supprimés, vérifié
|
||||||
|
```
|
||||||
46
tripoli-netctl/src/audit.rs
Normal file
46
tripoli-netctl/src/audit.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use chrono::Utc;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
const AUDIT_LOG_PATH: &str = "/var/log/tripoli-netctl/audit.log";
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct AuditEntry<'a> {
|
||||||
|
timestamp: chrono::DateTime<Utc>,
|
||||||
|
operation: &'a str,
|
||||||
|
target: &'a str,
|
||||||
|
detail: &'a str,
|
||||||
|
result: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Une ligne JSON par opération, faite à la main (pas de logger
|
||||||
|
/// structuré générique) : ce fichier n'a qu'un rôle, tracer qui a fait
|
||||||
|
/// quoi sur les namespaces réseau de cette machine, et doit rester
|
||||||
|
/// lisible tel quel avec `jq` sans configuration.
|
||||||
|
pub fn log(operation: &str, target: &str, detail: &str, result: &str) {
|
||||||
|
let entry = AuditEntry {
|
||||||
|
timestamp: Utc::now(),
|
||||||
|
operation,
|
||||||
|
target,
|
||||||
|
detail,
|
||||||
|
result,
|
||||||
|
};
|
||||||
|
|
||||||
|
let line = serde_json::to_string(&entry).expect("sérialisation toujours valide");
|
||||||
|
eprintln!("{line}");
|
||||||
|
|
||||||
|
if let Some(parent) = std::path::Path::new(AUDIT_LOG_PATH).parent() {
|
||||||
|
if std::fs::create_dir_all(parent).is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(mut file) = std::fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(AUDIT_LOG_PATH)
|
||||||
|
{
|
||||||
|
let _ = writeln!(file, "{line}");
|
||||||
|
}
|
||||||
|
}
|
||||||
56
tripoli-netctl/src/main.rs
Normal file
56
tripoli-netctl/src/main.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
mod audit;
|
||||||
|
mod naming;
|
||||||
|
mod netns_ops;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
|
/// tripoli-netctl - labo pédagogique un-seul-poste sur les network
|
||||||
|
/// namespaces Linux et la crate `rtnetlink`. Ne pilote AUCUN service de
|
||||||
|
/// production : tripoli-edge et tripoli-api continuent de tourner
|
||||||
|
/// nativement sur l'hôte, dans le namespace racine.
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "tripoli-netctl", version, about)]
|
||||||
|
struct Cli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Command,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum Command {
|
||||||
|
/// Crée un network namespace nommé, relié à la racine par une paire veth.
|
||||||
|
Create {
|
||||||
|
/// Nom du service (aussi le nom du namespace).
|
||||||
|
name: String,
|
||||||
|
/// Détermine le sous-réseau point-à-point utilisé : 10.200.<id>.0/30.
|
||||||
|
/// Pas d'allocation automatique - un labo n'a pas besoin de cette
|
||||||
|
/// complexité, et une collision explicite vaut mieux qu'une
|
||||||
|
/// allocation silencieuse qui masquerait une erreur d'usage.
|
||||||
|
#[arg(long, default_value_t = 1)]
|
||||||
|
subnet_id: u8,
|
||||||
|
},
|
||||||
|
/// Détruit un service précédemment créé (veth + namespace).
|
||||||
|
Destroy { name: String },
|
||||||
|
/// Liste les namespaces actifs et leur veth associée côté hôte.
|
||||||
|
List,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
||||||
|
.init();
|
||||||
|
|
||||||
|
if !nix::unistd::geteuid().is_root() {
|
||||||
|
eprintln!("tripoli-netctl doit tourner en root (network namespaces, netlink privilégié)");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
match cli.command {
|
||||||
|
Command::Create { name, subnet_id } => netns_ops::create(&name, subnet_id).await,
|
||||||
|
Command::Destroy { name } => netns_ops::destroy(&name).await,
|
||||||
|
Command::List => netns_ops::list().await,
|
||||||
|
}
|
||||||
|
}
|
||||||
22
tripoli-netctl/src/naming.rs
Normal file
22
tripoli-netctl/src/naming.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
/// Linux limite les noms d'interface à 15 caractères (IFNAMSIZ - 1). On
|
||||||
|
/// dérive un nom court et déterministe à partir du nom de service, pour
|
||||||
|
/// que `create`/`destroy` retrouvent toujours la même paire veth sans
|
||||||
|
/// avoir à la stocker séparément.
|
||||||
|
pub struct VethNames {
|
||||||
|
pub host: String,
|
||||||
|
pub peer: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn veth_names(service_name: &str) -> VethNames {
|
||||||
|
let short: String = service_name
|
||||||
|
.chars()
|
||||||
|
.filter(|c| c.is_ascii_alphanumeric())
|
||||||
|
.map(|c| c.to_ascii_lowercase())
|
||||||
|
.take(6)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
VethNames {
|
||||||
|
host: format!("tv-{short}h"),
|
||||||
|
peer: format!("tv-{short}p"),
|
||||||
|
}
|
||||||
|
}
|
||||||
223
tripoli-netctl/src/netns_ops.rs
Normal file
223
tripoli-netctl/src/netns_ops.rs
Normal file
|
|
@ -0,0 +1,223 @@
|
||||||
|
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(())
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue