diff --git a/Cargo.lock b/Cargo.lock index 7f9f255..4fed820 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -241,6 +241,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "edera-check" version = "0.2.18" @@ -256,8 +262,10 @@ dependencies = [ "flate2", "futures", "log", - "nix", + "nftables", + "nix 0.31.2", "procfs", + "rtnetlink", "sysinfo", "tar", "tokio", @@ -461,6 +469,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + [[package]] name = "jiff" version = "0.2.23" @@ -560,6 +574,81 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "netlink-packet-core" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4" +dependencies = [ + "paste", +] + +[[package]] +name = "netlink-packet-route" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8919612f6028ab4eacbbfe1234a9a43e3722c6e0915e7ff519066991905092" +dependencies = [ + "bitflags", + "libc", + "log", + "netlink-packet-core", +] + +[[package]] +name = "netlink-proto" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128" +dependencies = [ + "bytes", + "futures", + "log", + "netlink-packet-core", + "netlink-sys", + "thiserror 2.0.18", +] + +[[package]] +name = "netlink-sys" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" +dependencies = [ + "bytes", + "futures-util", + "libc", + "log", + "tokio", +] + +[[package]] +name = "nftables" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c57e7343eed9e9330e084eef12651b15be3c8ed7825915a0ffa33736b852bed" +dependencies = [ + "schemars", + "serde", + "serde_json", + "serde_path_to_error", + "strum", + "strum_macros", + "thiserror 2.0.18", +] + +[[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.2" @@ -644,6 +733,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -725,6 +820,26 @@ dependencies = [ "bitflags", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "regex" version = "1.12.3" @@ -754,6 +869,24 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +[[package]] +name = "rtnetlink" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc19f84f710fa2f337617f9bc0400260a94224bde7bae28fd8879f3771ca5784" +dependencies = [ + "futures-channel", + "futures-util", + "log", + "netlink-packet-core", + "netlink-packet-route", + "netlink-proto", + "netlink-sys", + "nix 0.30.1", + "thiserror 1.0.69", + "tokio", +] + [[package]] name = "rustix" version = "1.1.3" @@ -773,12 +906,47 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -799,6 +967,41 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "shlex" version = "1.3.0" @@ -849,6 +1052,24 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "syn" version = "2.0.117" @@ -885,6 +1106,46 @@ dependencies = [ "xattr", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio" version = "1.52.1" @@ -1162,3 +1423,9 @@ dependencies = [ "libc", "rustix", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 0259a63..4a46d43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,3 +32,5 @@ clap = { version = "4.6", features = ["derive"] } console = "0.16" bytesize = "2.3" dmidecode = "1.0" +nftables = "0.6" +rtnetlink = "0.21" diff --git a/src/helpers/host_executor.rs b/src/helpers/host_executor.rs index b46698a..76b41ea 100644 --- a/src/helpers/host_executor.rs +++ b/src/helpers/host_executor.rs @@ -66,6 +66,7 @@ impl HostNamespaceExecutor { Builder::new_multi_thread() .thread_name(format!("pid-{target_pid}-executor-threadset")) + .enable_io() .build() .expect("could not spawn pid-{target_pid} threadset") }) diff --git a/src/main.rs b/src/main.rs index fc2fc80..93cefe4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,6 @@ use log::debug; use nix::unistd::Uid; use std::{ fs, - fs::File, path::{Path, PathBuf}, process, }; @@ -141,24 +140,30 @@ async fn main() -> Result<()> { async fn create_gzip_from(base_path: PathBuf, host_executor: HostNamespaceExecutor) -> Result<()> { let mut archive_path = base_path.clone(); archive_path.set_extension("tar.gz"); - let tar_gz = File::create(&archive_path) - .with_context(|| format!("failed to create {}", archive_path.display()))?; - let enc = GzEncoder::new(tar_gz, Compression::default()); - let mut tar = tar::Builder::new(enc); - tar.append_dir_all(".", &base_path) - .context("failed to append to tar {}")?; - tar.into_inner().context("failed to finish tar")?; let container_tarfile = archive_path.to_string_lossy().to_string(); - let targz_content = std::fs::read(&container_tarfile).expect("could not read tar"); + let targz_content = tokio::task::spawn_blocking(move || -> Result> { + let mut buf = Vec::new(); + { + let enc = GzEncoder::new(&mut buf, Compression::default()); + let mut tar = tar::Builder::new(enc); + tar.append_dir_all(".", &base_path) + .context("failed to append to tar")?; + tar.into_inner() + .context("failed to finish tar")? + .finish() + .context("failed to finish gzip encoder")?; + } + std::fs::remove_dir_all(&base_path).with_context(|| { + format!("failed to remove results directory {}", base_path.display()) + })?; + Ok(buf) + }) + .await??; debug!("Read {} bytes of tar", targz_content.len()); - // Remove the source directory after tar creation - std::fs::remove_dir_all(&base_path) - .with_context(|| format!("failed to remove results directory {}", base_path.display()))?; let copy_to_host: JoinHandle<()> = host_executor.spawn_in_host_ns(async move { - // Write tar.gz to host tokio::fs::write(&container_tarfile, targz_content) .await .expect("could not write tar to host"); diff --git a/src/recorders/common.rs b/src/recorders/common.rs index 1545764..5a37874 100644 --- a/src/recorders/common.rs +++ b/src/recorders/common.rs @@ -30,7 +30,12 @@ impl CommonSystemRecorder { let output = match self .host_executor - .spawn_in_host_ns(async move { Command::new(cmd).args(tool_args).output() }) + .spawn_in_host_ns(async move { + tokio::process::Command::new(cmd) + .args(tool_args) + .output() + .await + }) .await { Ok(output) => output, @@ -317,6 +322,145 @@ impl CommonSystemRecorder { .expect("/proc/self/mountinfo not found") } + /// Records network link state for all interfaces. + /// + /// Manual equivalent: + /// ```sh + /// ip -d link show + /// ``` + pub async fn record_links(&self) -> CheckResult { + const NAME: &str = "Captured network links"; + match self + .host_executor + .spawn_in_host_ns(async { + use futures::TryStreamExt as _; + let (connection, handle, _) = + rtnetlink::new_connection().map_err(|e| e.to_string())?; + tokio::spawn(connection); + let links: Vec<_> = handle + .link() + .get() + .execute() + .try_collect() + .await + .map_err(|e: rtnetlink::Error| e.to_string())?; + Ok::( + links + .iter() + .map(|l| format!("{l:#?}")) + .collect::>() + .join("\n\n"), + ) + }) + .await + { + Ok(Ok(text)) => CheckResult::new_with_output(NAME, Passed, Some(text)), + Ok(Err(e)) => CheckResult::new(NAME, Skipped(e)), + Err(e) => CheckResult::new(NAME, Skipped(e.to_string())), + } + } + + /// Records IPv4 and IPv6 routes from all routing tables. + /// + /// Manual equivalent: + /// ```sh + /// ip route show && ip -6 route show + /// ``` + pub async fn record_routes(&self) -> CheckResult { + const NAME: &str = "Captured network routes"; + match self + .host_executor + .spawn_in_host_ns(async { + use futures::TryStreamExt as _; + let (connection, handle, _) = + rtnetlink::new_connection().map_err(|e| e.to_string())?; + tokio::spawn(connection); + // AF_UNSPEC (the default) returns all routes for all families, + // including both IPv4 and IPv6. + let routes: Vec<_> = handle + .route() + .get(rtnetlink::packet_route::route::RouteMessage::default()) + .execute() + .try_collect() + .await + .map_err(|e: rtnetlink::Error| e.to_string())?; + Ok::( + routes + .iter() + .map(|r| format!("{r:#?}")) + .collect::>() + .join("\n\n"), + ) + }) + .await + { + Ok(Ok(text)) => CheckResult::new_with_output(NAME, Passed, Some(text)), + Ok(Err(e)) => CheckResult::new(NAME, Skipped(e)), + Err(e) => CheckResult::new(NAME, Skipped(e.to_string())), + } + } + + /// Records neighbour (ARP/NDP) table entries for all interfaces. + /// + /// Manual equivalent: + /// ```sh + /// ip neigh show + /// ``` + pub async fn record_neighbours(&self) -> CheckResult { + const NAME: &str = "Captured network neighbours"; + match self + .host_executor + .spawn_in_host_ns(async { + use futures::TryStreamExt as _; + let (connection, handle, _) = + rtnetlink::new_connection().map_err(|e| e.to_string())?; + tokio::spawn(connection); + let neighbours: Vec<_> = handle + .neighbours() + .get() + .execute() + .try_collect() + .await + .map_err(|e: rtnetlink::Error| e.to_string())?; + Ok::( + neighbours + .iter() + .map(|n| format!("{n:#?}")) + .collect::>() + .join("\n\n"), + ) + }) + .await + { + Ok(Ok(text)) => CheckResult::new_with_output(NAME, Passed, Some(text)), + Ok(Err(e)) => CheckResult::new(NAME, Skipped(e)), + Err(e) => CheckResult::new(NAME, Skipped(e.to_string())), + } + } + + /// Records the complete nftables ruleset as JSON, covering all address families + /// (ip, ip6, inet, arp, bridge, netdev). + /// + /// Manual equivalent: + /// ```sh + /// nft -j list ruleset + /// ``` + pub async fn record_nftables_ruleset(&self) -> CheckResult { + const NAME: &str = "Captured nftables ruleset"; + + match self + .host_executor + .spawn_in_host_ns(async { + nftables::helper::get_current_ruleset_raw(None::<&str>, std::iter::empty::<&str>()) + }) + .await + { + Ok(Ok(json)) => CheckResult::new_with_output(NAME, Passed, Some(json)), + Ok(Err(e)) => CheckResult::new(NAME, Skipped(e.to_string())), + Err(e) => CheckResult::new(NAME, Skipped(e.to_string())), + } + } + async fn current_kernel_version(&self) -> Result { self.host_executor .spawn_in_host_ns(async { diff --git a/src/recorders/postinstall/system.rs b/src/recorders/postinstall/system.rs index bafc4f7..8e1acc1 100644 --- a/src/recorders/postinstall/system.rs +++ b/src/recorders/postinstall/system.rs @@ -39,6 +39,10 @@ impl SystemRecorder { self.common.record_slabinfo().boxed(), self.common.record_mounts().boxed(), self.common.record_mountinfo().boxed(), + self.common.record_nftables_ruleset().boxed(), + self.common.record_links().boxed(), + self.common.record_routes().boxed(), + self.common.record_neighbours().boxed(), self.record_hv_console().boxed(), self.record_hv_debug_info().boxed(), self.record_daemon_logs().boxed(), diff --git a/src/recorders/preinstall/system.rs b/src/recorders/preinstall/system.rs index afa2965..bbed5ad 100644 --- a/src/recorders/preinstall/system.rs +++ b/src/recorders/preinstall/system.rs @@ -38,6 +38,10 @@ impl SystemRecorder { self.common.record_slabinfo().boxed(), self.common.record_mounts().boxed(), self.common.record_mountinfo().boxed(), + self.common.record_nftables_ruleset().boxed(), + self.common.record_links().boxed(), + self.common.record_routes().boxed(), + self.common.record_neighbours().boxed(), ]) .await;