use std::fs::OpenOptions; use std::io::Write; use std::net::SocketAddr; use std::net::TcpStream; use std::path::PathBuf; use std::process::ExitCode; use std::time::Duration; use serde::Serialize; #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct NamespaceReport { user_namespace: String, mount_namespace: String, pid_namespace: String, network_namespace: String, ipc_namespace: String, uts_namespace: String, proc_pid_one_namespace: Option, proc_file_system_type: String, overflow_uid: String, overflow_gid: String, effective_uid: u32, effective_gid: u32, effective_capabilities: String, permitted_capabilities: String, inheritable_capabilities: String, ambient_capabilities: String, no_new_privileges: u32, seccomp_mode: u32, connect_succeeded: Option, allowed_write_succeeded: Option, denied_read_succeeded: Option, denied_write_succeeded: Option, } fn main() -> Result> { let mut connect = None; let mut allowed_write = None; let mut denied_write = None; let mut exit_code = 0; let mut args = std::env::args_os().skip(1); while let Some(arg) = args.next() { match arg.to_str() { Some("--connect") => { connect = Some( args.next() .ok_or("--connect requires an address")? .into_string() .map_err(|_| "connect address must be valid UTF-8")?, ); } Some("--allowed-write") => { allowed_write = Some(PathBuf::from( args.next().ok_or("--allowed-write requires a path")?, )); } Some("--denied-write") => { denied_write = Some(PathBuf::from( args.next().ok_or("--denied-write requires a path")?, )); } Some("--exit-code") => { exit_code = args .next() .ok_or("--exit-code requires a value")? .to_str() .ok_or("--exit-code must be valid UTF-8")? .parse::()?; } _ => return Err(format!("unexpected argument: {}", arg.to_string_lossy()).into()), } } let status = std::fs::read_to_string("/proc/self/status")?; let denied_read_succeeded = denied_write .as_deref() .map(|path| std::fs::read(path).is_ok()); let report = NamespaceReport { user_namespace: namespace("user")?, mount_namespace: namespace("mnt")?, pid_namespace: namespace("pid")?, network_namespace: namespace("net")?, ipc_namespace: namespace("ipc")?, uts_namespace: namespace("uts")?, proc_pid_one_namespace: std::fs::read_link("/proc/1/ns/pid") .ok() .map(|path| path.to_string_lossy().into_owned()), proc_file_system_type: proc_file_system_type()?, overflow_uid: std::fs::read_to_string("/proc/sys/kernel/overflowuid")? .trim() .to_string(), overflow_gid: std::fs::read_to_string("/proc/sys/kernel/overflowgid")? .trim() .to_string(), effective_uid: status_id(&status, "Uid")?, effective_gid: status_id(&status, "Gid")?, effective_capabilities: status_field(&status, "CapEff")?.to_string(), permitted_capabilities: status_field(&status, "CapPrm")?.to_string(), inheritable_capabilities: status_field(&status, "CapInh")?.to_string(), ambient_capabilities: status_field(&status, "CapAmb")?.to_string(), no_new_privileges: status_number(&status, "NoNewPrivs")?, seccomp_mode: status_number(&status, "Seccomp")?, connect_succeeded: connect .map(|address| connect_to(address.as_str())) .transpose()?, allowed_write_succeeded: allowed_write.map(|path| append_probe(path.as_path())), denied_read_succeeded, denied_write_succeeded: denied_write.map(|path| append_probe(path.as_path())), }; println!("{}", serde_json::to_string(&report)?); std::io::stdout().flush()?; Ok(ExitCode::from(exit_code)) } fn namespace(name: &str) -> std::io::Result { std::fs::read_link(format!("/proc/self/ns/{name}")) .map(|path| path.to_string_lossy().into_owned()) } fn proc_file_system_type() -> Result> { let mountinfo = std::fs::read_to_string("/proc/self/mountinfo")?; for line in mountinfo.lines() { let Some((mount, file_system)) = line.split_once(" - ") else { continue; }; if mount.split_whitespace().nth(4) == Some("/proc") { return file_system .split_whitespace() .next() .map(str::to_string) .ok_or_else(|| "proc mount is missing its filesystem type".into()); } } Err("/proc is not present in mountinfo".into()) } fn status_field<'a>(status: &'a str, name: &str) -> Result<&'a str, Box> { status .lines() .find_map(|line| line.strip_prefix(&format!("{name}:"))) .map(str::trim) .ok_or_else(|| format!("/proc/self/status is missing {name}").into()) } fn status_number(status: &str, name: &str) -> Result> { status_field(status, name)? .split_whitespace() .next() .ok_or_else(|| format!("/proc/self/status field {name} is empty"))? .parse() .map_err(Into::into) } fn status_id(status: &str, name: &str) -> Result> { status_field(status, name)? .split_whitespace() .nth(1) .ok_or_else(|| format!("/proc/self/status field {name} has no effective ID"))? .parse() .map_err(Into::into) } fn connect_to(address: &str) -> Result> { let address: SocketAddr = address.parse()?; Ok(TcpStream::connect_timeout(&address, Duration::from_millis(500)).is_ok()) } fn append_probe(path: &std::path::Path) -> bool { OpenOptions::new() .append(true) .open(path) .and_then(|mut file| file.write_all(b"changed")) .is_ok() }