diff --git a/container-tool/Cargo.lock b/container-tool/Cargo.lock new file mode 100644 index 0000000..2221182 --- /dev/null +++ b/container-tool/Cargo.lock @@ -0,0 +1,527 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "container-tool" +version = "0.1.0" +dependencies = [ + "futures-util", + "rtnetlink", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[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.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ec2f5b6839be2a19d7fa5aab5bc444380f6311c2b693551cb80f45caaa7b5ef" +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.17", +] + +[[package]] +name = "netlink-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23" +dependencies = [ + "bytes", + "futures", + "libc", + "log", + "tokio", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rtnetlink" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08fd15aa4c64c34d0b3178e45ec6dad313a9f02b193376d501668a7950264bb7" +dependencies = [ + "futures", + "log", + "netlink-packet-core", + "netlink-packet-route", + "netlink-proto", + "netlink-sys", + "nix", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "syn" +version = "2.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[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.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[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.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" diff --git a/container-tool/Cargo.toml b/container-tool/Cargo.toml new file mode 100644 index 0000000..94ecc2e --- /dev/null +++ b/container-tool/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "container-tool" +version = "0.1.0" +edition = "2024" + +[dependencies] +futures-util = "0.3.31" +rtnetlink = "0.18.1" +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.145" +tokio = { version = "1.48.0", features = ["macros", "rt", "signal"] } + +[[bin]] +name = "container-tool" +path = "src/main.rs" diff --git a/container-tool/src/main.rs b/container-tool/src/main.rs new file mode 100644 index 0000000..4086896 --- /dev/null +++ b/container-tool/src/main.rs @@ -0,0 +1,687 @@ +mod netlink_commands; + +use serde::{Deserialize, Serialize}; +use std::{ + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + os::unix::fs::PermissionsExt, + path::PathBuf, + process::Command, + str::FromStr, +}; +use tokio::signal::unix::{SignalKind, signal}; + +use crate::netlink_commands::{ + ip_addr_add, ip_link_add_to_bridge, ip_link_del, ip_link_get_index, ip_link_set_name, + ip_link_up, ip_route_add, ip_route_add_default_via_addr, +}; + +// TODO: read global and instance specific config from json + +fn main() { + let command = std::env::args().nth(1).expect("No command provided"); + let config_file = std::env::args().nth(2).expect("No config file provided"); + let config_data = std::fs::read_to_string(config_file).expect("Failed to read config file"); + let config: Config = serde_json::from_str(&config_data).expect("Failed to parse config file"); + + match command.as_str() { + // Prepare the networking on the host side + "prepare-host-networking" => prepare_host_networking(config), + + // Set up directories and files in the container root directory + "prepare-rootdir" => prepare_rootdir(config), + + // Set up the networking inside the container (needs to be running) + "prepare-networking-in-container" => unimplemented!(), + + // Run the container + "run-container" => unimplemented!(), + + // Teardown the networking on the host side + "teardown-host-networking" => teardown_host_networking(config), + + // Reload the container (e.g., after a configuration change) + "reload-container" => reload_container(config), + _ => panic!("Unknown command: {}", command), + } +} + +#[derive(Debug, Serialize, Deserialize)] +struct Config { + instance_name: String, + + root_dir: PathBuf, + + ephemeral: bool, + private_network: bool, + private_users: Option, + personality: Option, + + host_bridge: Option, + host_address: Option, + local_address: Option, + host_address6: Option, + local_address6: Option, + + #[serde(default)] + port: Vec, + + #[serde(default)] + interfaces: Vec, + + #[serde(default)] + macvlans: Vec, + + #[serde(default)] + extra_veths: Vec, + + #[serde(default)] + bind_mounts: Vec, + + #[serde(default)] + additional_capabilities: Vec, + + #[serde(default)] + tmpfs: Vec, + + #[serde(default)] + extra_nspawn_args: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +struct Veth { + name: String, + host_bridge: Option, + host_address: Option, + local_address: Option, + host_address6: Option, + local_address6: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +struct BindMount { + source: String, + target: String, + options: Vec, +} + +impl ToString for BindMount { + fn to_string(&self) -> String { + if !self.options.is_empty() { + format!("{}:{}:{}", self.source, self.target, self.options.join(",")) + } else { + format!("{}:{}", self.source, self.target) + } + } +} + +impl FromStr for BindMount { + type Err = String; + + fn from_str(s: &str) -> Result { + let parts: Vec<&str> = s.split(':').collect(); + match parts.as_slice() { + [path] => Ok(BindMount { + source: path.to_string(), + target: path.to_string(), + options: vec![], + }), + [source, target] => Ok(BindMount { + source: source.to_string(), + target: target.to_string(), + options: vec![], + }), + [source, target, options] => Ok(BindMount { + source: source.to_string(), + target: target.to_string(), + options: options.split(',').map(|s| s.to_string()).collect(), + }), + _ => Err(format!("Invalid bind mount format: {}", s)), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +struct Port { + protocol: String, // "tcp" or "udp" + host_port: u16, + container_port: u16, +} + +impl FromStr for Port { + type Err = String; + + fn from_str(s: &str) -> Result { + let parts: Vec<&str> = s.split(':').collect(); + match parts[..] { + [port] => { + let port_num: u16 = port + .parse() + .map_err(|_| format!("Invalid port number: {}", port))?; + Ok(Port { + protocol: "tcp".to_string(), + host_port: port_num, + container_port: port_num, + }) + } + [protocol, port] if ["tcp", "udp"].contains(&protocol) => { + let port_num: u16 = port + .parse() + .map_err(|_| format!("Invalid port number: {}", port))?; + Ok(Port { + protocol: protocol.to_string(), + host_port: port_num, + container_port: port_num, + }) + } + [host_port, container_port] => { + let host_port_num: u16 = host_port + .parse() + .map_err(|_| format!("Invalid host port number: {}", host_port))?; + let container_port_num: u16 = container_port + .parse() + .map_err(|_| format!("Invalid container port number: {}", container_port))?; + Ok(Port { + protocol: "tcp".to_string(), + host_port: host_port_num, + container_port: container_port_num, + }) + } + [protocol, host_port, container_port] if ["tcp", "udp"].contains(&protocol) => { + let host_port_num: u16 = host_port + .parse() + .map_err(|_| format!("Invalid host port number: {}", host_port))?; + let container_port_num: u16 = container_port + .parse() + .map_err(|_| format!("Invalid container port number: {}", container_port))?; + Ok(Port { + protocol: protocol.to_string(), + host_port: host_port_num, + container_port: container_port_num, + }) + } + _ => Err(format!("Invalid port format: {}", s)), + } + } +} + +impl ToString for Port { + fn to_string(&self) -> String { + format!( + "{}:{}:{}", + self.protocol, self.host_port, self.container_port + ) + } +} + +fn prepare_host_networking(config: Config) { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("Failed to create Tokio runtime") + .block_on(async { + let (netlink_conn, netlink_handle, _) = + rtnetlink::new_connection().expect("Failed to create netlink connection"); + tokio::spawn(netlink_conn); + if (config.host_address.is_some() + || config.local_address.is_some() + || config.host_address6.is_some() + || config.local_address6.is_some()) + && config.host_bridge.is_none() + { + let if_host = format!("ve-{}", config.instance_name); + let if_index = ip_link_get_index(&netlink_handle, &if_host) + .await + .expect(format!("Failed to get index for interface {}", if_host).as_str()); + + ip_link_up(&netlink_handle, if_index) + .await + .expect(format!("Failed to bring up interface {}", if_host).as_str()); + + if let Some(addr) = &config.host_address { + ip_addr_add(&netlink_handle, if_index, IpAddr::V4(*addr)) + .await + .expect("Failed to add host address"); + } + + if let Some(addr6) = &config.host_address6 { + ip_addr_add(&netlink_handle, if_index, IpAddr::V6(*addr6)) + .await + .expect("Failed to add host address6"); + } + + if let Some(addr) = &config.local_address { + ip_route_add(&netlink_handle, if_index, IpAddr::V4(*addr)) + .await + .expect("Failed to add local address"); + } + + if let Some(addr6) = &config.local_address6 { + ip_route_add(&netlink_handle, if_index, IpAddr::V6(*addr6)) + .await + .expect("Failed to add local address6"); + } + } + + for veth in &config.extra_veths { + let if_name = &veth.name; + let if_index = ip_link_get_index(&netlink_handle, if_name) + .await + .expect(format!("Failed to get index for interface {}", if_name).as_str()); + + if let Some(host_bridge) = &veth.host_bridge { + let bridge_index = ip_link_get_index(&netlink_handle, host_bridge) + .await + .expect(format!("Failed to get index for bridge {}", host_bridge).as_str()); + + ip_link_add_to_bridge(&netlink_handle, if_index, bridge_index) + .await + .expect( + format!( + "Failed to add interface {} to bridge {}", + if_name, host_bridge + ) + .as_str(), + ); + } else { + ip_link_up(&netlink_handle, if_index) + .await + .expect(format!("Failed to bring up interface {}", if_name).as_str()); + + if let Some(addr) = &veth.host_address { + let ip_addr = IpAddr::from_str(addr) + .expect(format!("Invalid IP address: {}", addr).as_str()); + ip_addr_add(&netlink_handle, if_index, ip_addr) + .await + .expect("Failed to add veth host address"); + } + + if let Some(addr6) = &veth.host_address6 { + let ip_addr6 = IpAddr::from_str(addr6) + .expect(format!("Invalid IP address: {}", addr6).as_str()); + ip_addr_add(&netlink_handle, if_index, ip_addr6) + .await + .expect("Failed to add veth host address6"); + } + + if let Some(addr) = &veth.local_address { + let ip_addr = IpAddr::from_str(addr) + .expect(format!("Invalid IP address: {}", addr).as_str()); + ip_route_add(&netlink_handle, if_index, ip_addr) + .await + .expect("Failed to add veth local address"); + } + + if let Some(addr6) = &veth.local_address6 { + let ip_addr6 = IpAddr::from_str(addr6) + .expect(format!("Invalid IP address: {}", addr6).as_str()); + ip_route_add(&netlink_handle, if_index, ip_addr6) + .await + .expect("Failed to add veth local address6"); + } + } + } + }); +} + +fn prepare_rootdir(config: Config) { + for (dir, mode) in [ + ("/etc", 0o755), + ("/var/lib", 0o755), + ("/var/lib/private", 0o700), + ("/root", 0o700), + ] { + let path = config.root_dir.join(dir.strip_prefix('/').unwrap()); + std::fs::create_dir_all(&path) + .expect(format!("Failed to create directory {}", path.display()).as_str()); + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode)) + .expect(format!("Failed to set permissions for directory {}", path.display()).as_str()); + } + + if !config.root_dir.join("etc/os-release").exists() { + std::fs::File::create(config.root_dir.join("etc/os-release")) + .expect("Failed to create /etc/os-release"); + } + + if !config.root_dir.join("etc/machine-id").exists() { + std::fs::File::create(config.root_dir.join("etc/machine-id")) + .expect("Failed to create /etc/machine-id"); + } +} + +// TODO: make this into a separate executable that is copied into the container. +// It can still use the config file to get the necessary parameters. + +fn prepare_networking_in_container(config: Config) { + // # The container's init script, a small wrapper around the regular + // # NixOS stage-2 init script. + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("Failed to create Tokio runtime") + .block_on(async { + tokio::spawn(async { + const SIGRTMIN: i32 = 34; // Typically 34 on Linux systems + let mut signal = signal(SignalKind::from_raw(SIGRTMIN)) + .expect("Failed to set up signal handler"); + signal.recv().await; + std::process::exit(0); + }); + + let (netlink_conn, netlink_handle, _) = + rtnetlink::new_connection().expect("Failed to create netlink connection"); + tokio::spawn(netlink_conn); + + // # Initialise the container side of the veth pair. + if config.host_address.is_some() + || config.local_address.is_some() + || config.host_address6.is_some() + || config.local_address6.is_some() + || config.host_bridge.is_some() + { + let if_index = ip_link_get_index(&netlink_handle, "host0") + .await + .expect("Failed to get index for interface host0"); + + ip_link_set_name(&netlink_handle, if_index, "eth0") + .await + .expect("Failed to rename interface host0 to eth0"); + ip_link_up(&netlink_handle, if_index) + .await + .expect("Failed to bring up interface eth0"); + + if let Some(addr) = &config.local_address { + ip_addr_add(&netlink_handle, if_index, IpAddr::V4(*addr)) + .await + .expect("Failed to add local address"); + } + if let Some(addr6) = &config.local_address6 { + ip_addr_add(&netlink_handle, if_index, IpAddr::V6(*addr6)) + .await + .expect("Failed to add local address6"); + } + if let Some(addr) = &config.host_address { + ip_route_add(&netlink_handle, if_index, IpAddr::V4(*addr)) + .await + .expect("Failed to add host address"); + ip_route_add_default_via_addr(&netlink_handle, if_index, IpAddr::V4(*addr)) + .await + .expect("Failed to add default route via host address"); + } + if let Some(addr6) = &config.host_address6 { + ip_route_add(&netlink_handle, if_index, IpAddr::V6(*addr6)) + .await + .expect("Failed to add host address6"); + ip_route_add_default_via_addr(&netlink_handle, if_index, IpAddr::V6(*addr6)) + .await + .expect("Failed to add default route via host address6"); + } + } + + // renderExtraVeth = ( + // name: cfg: '' + // echo "Bringing ${name} up" + // ip link set dev ${name} up + // ${optionalString (cfg.localAddress != null) '' + // echo "Setting ip for ${name}" + // ip addr add ${cfg.localAddress} dev ${name} + // ''} + // ${optionalString (cfg.localAddress6 != null) '' + // echo "Setting ip6 for ${name}" + // ip -6 addr add ${cfg.localAddress6} dev ${name} + // ''} + // ${optionalString (cfg.hostAddress != null) '' + // echo "Setting route to host for ${name}" + // ip route add ${cfg.hostAddress} dev ${name} + // ''} + // ${optionalString (cfg.hostAddress6 != null) '' + // echo "Setting route6 to host for ${name}" + // ip -6 route add ${cfg.hostAddress6} dev ${name} + // ''} + // '' + // ); + + // ${concatStringsSep "\n" (mapAttrsToList renderExtraVeth cfg.extraVeths)} + + // # Start the regular stage 2 script. + // # We source instead of exec to not lose an early stop signal, which is + // # also the only _reliable_ shutdown signal we have since early stop + // # does not execute ExecStop* commands. + // set +e + // . "$1" + // '' + }); +} + +fn run_container(config: Config) { + // let command = Command::new("systemd-nspawn"); + + let mut args = vec![ + "--keep-unit".to_string(), + "-M".to_string(), + config.instance_name.clone(), + "-D".to_string(), + config.root_dir.to_str().unwrap().to_string(), + "--notify-ready=yes".to_string(), + "--resolv-conf=copy-host".to_string(), + "--kill-signal=SIGRTMIN+3".to_string(), + ]; + + if config.private_network { + args.push("--private-network".to_string()); + } + if let Some(private_users) = &config.private_users { + args.push(format!("--private-users={}", private_users)); + } + + // NIX_BIND_OPT="" + // if [ -n "$PRIVATE_USERS" ]; then + // extraFlags+=("--private-users=$PRIVATE_USERS") + // if [[ + // "$PRIVATE_USERS" = "pick" + // || ("$PRIVATE_USERS" =~ ^[[:digit:]]+$ && "$PRIVATE_USERS" -gt 0) + // ]]; then + // # when user namespacing is enabled, we use `idmap` mount option so that + // # bind mounts under /nix get proper owner (and not nobody/nogroup). + // NIX_BIND_OPT=":idmap" + // fi + // fi + + if config.host_address.is_some() + || config.local_address.is_some() + || config.host_address6.is_some() + || config.local_address6.is_some() + { + args.push("--network-veth".to_string()); + } + + for port in &config.port { + args.push(format!("--port={}", port.to_string())); + } + + if let Some(host_bridge) = &config.host_bridge { + args.push(format!("--network-bridge={}", host_bridge)); + } + + if let Some(network_namespace_path) = &config.host_bridge { + args.push(format!( + "--network-namespace-path={}", + network_namespace_path + )); + } + + for veth in &config.extra_veths { + args.push(format!("--network-veth-extra={}", veth.name)); + } + + for iface in &config.interfaces { + args.push(format!("--network-interface={}", iface)); + } + + for iface in &config.macvlans { + args.push(format!("--network-macvlan={}", iface)); + } + + if let Some(personality) = &config.personality { + args.push(format!("--personality={}", personality)); + } + + // --bind-ro=/nix/store:/nix/store$NIX_BIND_OPT \ + // --bind-ro=/nix/var/nix/db:/nix/var/nix/db$NIX_BIND_OPT \ + // --bind-ro=/nix/var/nix/daemon-socket:/nix/var/nix/daemon-socket$NIX_BIND_OPT \ + // --bind="/nix/var/nix/profiles/per-container/$INSTANCE:/nix/var/nix/profiles$NIX_BIND_OPT" \ + // --bind="/nix/var/nix/gcroots/per-container/$INSTANCE:/nix/var/nix/gcroots$NIX_BIND_OPT" \ + + for bind_mount in &config.bind_mounts { + args.push(format!("--bind={}", bind_mount.to_string())); + } + + if !config.ephemeral { + args.push("--link-journal=try-guest".to_string()); + } + + if config.ephemeral { + args.push("--ephemeral".to_string()); + } + + if !config.additional_capabilities.is_empty() { + args.push(format!( + "--capability={}", + config.additional_capabilities.join(",") + )); + } + + for tmpfs in &config.tmpfs { + args.push(format!("--tmpfs={}", tmpfs)); + } + + for extra_arg in &config.extra_nspawn_args { + args.push(extra_arg.clone()); + } + + unimplemented!(); + // declare -a extraFlags + + // export SYSTEMD_NSPAWN_UNIFIED_HIERARCHY=1 + + // # Run systemd-nspawn without startup notification (we'll + // # wait for the container systemd to signal readiness) + // # Kill signal handling means systemd-nspawn will pass a system-halt signal + // # to the container systemd when it receives SIGTERM for container shutdown; + // # containerInit and stage2 have to handle this as well. + // # TODO: fix shellcheck issue properly + // # shellcheck disable=SC2086 + // exec ${config.systemd.package}/bin/systemd-nspawn \ + // --keep-unit \ + // -M "$INSTANCE" -D "$root" "''${extraFlags[@]}" \ + // --notify-ready=yes \ + // --resolv-conf=copy-host \ + // --kill-signal=SIGRTMIN+3 \ + // --bind-ro=/nix/store:/nix/store$NIX_BIND_OPT \ + // --bind-ro=/nix/var/nix/db:/nix/var/nix/db$NIX_BIND_OPT \ + // --bind-ro=/nix/var/nix/daemon-socket:/nix/var/nix/daemon-socket$NIX_BIND_OPT \ + // --bind="/nix/var/nix/profiles/per-container/$INSTANCE:/nix/var/nix/profiles$NIX_BIND_OPT" \ + // --bind="/nix/var/nix/gcroots/per-container/$INSTANCE:/nix/var/nix/gcroots$NIX_BIND_OPT" \ + // ${optionalString (!cfg.ephemeral) "--link-journal=try-guest"} \ + // --setenv PRIVATE_NETWORK="$PRIVATE_NETWORK" \ + // --setenv PRIVATE_USERS="$PRIVATE_USERS" \ + // --setenv HOST_BRIDGE="$HOST_BRIDGE" \ + // --setenv HOST_ADDRESS="$HOST_ADDRESS" \ + // --setenv LOCAL_ADDRESS="$LOCAL_ADDRESS" \ + // --setenv HOST_ADDRESS6="$HOST_ADDRESS6" \ + // --setenv LOCAL_ADDRESS6="$LOCAL_ADDRESS6" \ + // --setenv HOST_PORT="$HOST_PORT" \ + // --setenv PATH="$PATH" \ + // ${optionalString cfg.ephemeral "--ephemeral"} \ + // ${ + // optionalString ( + // cfg.additionalCapabilities != null && cfg.additionalCapabilities != [ ] + // ) ''--capability="${concatStringsSep "," cfg.additionalCapabilities}"'' + // } \ + // ${ + // optionalString ( + // cfg.tmpfs != null && cfg.tmpfs != [ ] + // ) ''--tmpfs=${concatStringsSep " --tmpfs=" cfg.tmpfs}'' + // } \ + // $EXTRA_NSPAWN_FLAGS \ + // ${containerInit cfg} "''${SYSTEM_PATH:-/nix/var/nix/profiles/system}/init" +} + +fn teardown_host_networking(config: Config) { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("Failed to create Tokio runtime") + .block_on(async { + let (netlink_conn, netlink_handle, _) = + rtnetlink::new_connection().expect("Failed to create netlink connection"); + + tokio::spawn(netlink_conn); + + if config.host_address.is_some() + || config.local_address.is_some() + || config.host_address6.is_some() + || config.local_address6.is_some() + { + let ve_if_host = format!("ve-{}", config.instance_name); + let ve_if_index = ip_link_get_index(&netlink_handle, &ve_if_host).await; + if let Some(ve_if_index) = ve_if_index { + ip_link_del(&netlink_handle, ve_if_index) + .await + // TODO: should these expects block the whole teardown? + .expect(format!("Failed to delete interface {}", ve_if_host).as_str()); + } + + let vb_if_host = format!("vb-{}", config.instance_name); + let vb_if_index = ip_link_get_index(&netlink_handle, &ve_if_host).await; + if let Some(vb_if_index) = vb_if_index { + ip_link_del(&netlink_handle, vb_if_index) + .await + .expect(format!("Failed to delete interface {}", vb_if_host).as_str()); + } + } + + for veth in &config.extra_veths { + let if_index = ip_link_get_index(&netlink_handle, &veth.name).await; + if let Some(if_index) = if_index { + ip_link_del(&netlink_handle, if_index) + .await + .expect(format!("Failed to delete interface {}", veth.name).as_str()); + } + } + }); +} + +// # Run a command in the container. +// sub runInContainer { +// my @args = @_; +// my $leader = getLeader; +// exec($nsenter, "--all", "-t", $leader, "--", @args); +// die "cannot run ‘nsenter’: $!\n"; +// } +// +// from 'run' command: +// runInContainer("@su@", "root", "-l", "-c", "exec " . $s); + +fn run_in_container(config: Config, script: &str) { + Command::new("systemd-run") + .args(&[ + "--machine", + &config.instance_name, + "--wait", + "--quiet", + "--", + "/bin/sh", + "-c", + script, + ]) + .status() + .expect("Failed to run command in container"); +} + +// from reload script: +// ${nixos-container}/bin/nixos-container run "$INSTANCE" -- \ +// bash --login -c "''${SYSTEM_PATH:-/nix/var/nix/profiles/system}/bin/switch-to-configuration test" + +fn reload_container(config: Config) { + unimplemented!(); +} diff --git a/container-tool/src/netlink_commands.rs b/container-tool/src/netlink_commands.rs new file mode 100644 index 0000000..0a14c28 --- /dev/null +++ b/container-tool/src/netlink_commands.rs @@ -0,0 +1,169 @@ +use futures_util::TryStreamExt; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use rtnetlink::{ + AddressMessageBuilder, Error, Handle, LinkMessageBuilder, LinkUnspec, RouteMessageBuilder, +}; + +const DEFAULT_PREFIX_LEN_V4: u8 = 24; +const DEFAULT_PREFIX_LEN_V6: u8 = 64; + +pub async fn ip_link_get_index(handle: &Handle, if_name: &str) -> Option { + let mut links = handle + .link() + .get() + .match_name(if_name.to_string()) + .execute(); + + while let Some(msg) = links.try_next().await.ok()? { + return Some(msg.header.index); + } + + None +} + +pub async fn ip_link_up(handle: &Handle, if_index: u32) -> Result<(), Error> { + handle + .link() + .set(LinkUnspec::new_with_index(if_index).up().build()) + .execute() + .await +} + +pub async fn ip_link_down(handle: &Handle, if_index: u32) -> Result<(), Error> { + handle + .link() + .set(LinkUnspec::new_with_index(if_index).down().build()) + .execute() + .await +} + +pub async fn ip_link_del(handle: &Handle, if_index: u32) -> Result<(), Error> { + handle + .link() + .del(if_index) + .execute() + .await +} + +pub async fn ip_link_add_to_bridge( + handle: &Handle, + if_index: u32, + bridge_index: u32, +) -> Result<(), Error> { + let message = LinkMessageBuilder::::new() + .index(if_index) + .controller(bridge_index) + .build(); + + handle.link().set(message).execute().await +} + +pub async fn ip_link_set_name( + handle: &Handle, + if_index: u32, + new_name: &str, +) -> Result<(), Error> { + handle + .link() + .set(LinkUnspec::new_with_index(if_index).name(new_name.to_owned()).build()) + .execute() + .await +} + +pub async fn ip_addr_add(handle: &Handle, if_index: u32, addr: IpAddr) -> Result<(), Error> { + handle + .address() + .add( + if_index, + addr, + if addr.is_ipv4() { + DEFAULT_PREFIX_LEN_V4 + } else { + DEFAULT_PREFIX_LEN_V6 + }, + ) + .execute() + .await +} + +pub async fn ip_addr_del(handle: &Handle, if_index: u32, addr: IpAddr) -> Result<(), Error> { + let message = match addr { + IpAddr::V4(ipv4) => AddressMessageBuilder::::new() + .index(if_index) + .address(ipv4, DEFAULT_PREFIX_LEN_V4) + .build(), + + IpAddr::V6(ipv6) => AddressMessageBuilder::::new() + .index(if_index) + .address(ipv6, DEFAULT_PREFIX_LEN_V6) + .build(), + }; + + handle.address().del(message).execute().await +} + +pub async fn ip_route_add(handle: &Handle, if_index: u32, addr: IpAddr) -> Result<(), Error> { + match addr { + IpAddr::V4(ipv4) => { + let message = RouteMessageBuilder::::new() + .destination_prefix(ipv4, DEFAULT_PREFIX_LEN_V4) + .output_interface(if_index) + .build(); + + handle.route().add(message).execute().await + } + IpAddr::V6(ipv6) => { + let message = RouteMessageBuilder::::new() + .destination_prefix(ipv6, DEFAULT_PREFIX_LEN_V6) + .output_interface(if_index) + .build(); + + handle.route().add(message).execute().await + } + } +} + +pub async fn ip_route_add_default_via_addr(handle: &Handle, if_index: u32, addr: IpAddr) -> Result<(), Error> { + match addr { + IpAddr::V4(ipv4) => { + let message = RouteMessageBuilder::::new() + .destination_prefix(Ipv4Addr::UNSPECIFIED, 0) + .gateway(ipv4) + .output_interface(if_index) + .build(); + + handle.route().add(message).execute().await + } + IpAddr::V6(ipv6) => { + let message = RouteMessageBuilder::::new() + .destination_prefix(Ipv6Addr::UNSPECIFIED, 0) + .gateway(ipv6) + .output_interface(if_index) + .build(); + + handle.route().add(message).execute().await + } + } +} + +pub async fn ip_route_del(handle: &Handle, if_index: u32, addr: IpAddr) -> Result<(), Error> { + match addr { + IpAddr::V4(ipv4) => { + let message = RouteMessageBuilder::::new() + .destination_prefix(ipv4, DEFAULT_PREFIX_LEN_V4) + .output_interface(if_index) + .build(); + + handle.route().del(message).execute().await + } + IpAddr::V6(ipv6) => { + let message = RouteMessageBuilder::::new() + .destination_prefix(ipv6, DEFAULT_PREFIX_LEN_V6) + .output_interface(if_index) + .build(); + + handle.route().del(message).execute().await + } + } +} diff --git a/flake.lock b/flake.lock index b4c8ee1..2e7ef83 100644 --- a/flake.lock +++ b/flake.lock @@ -17,7 +17,28 @@ }, "root": { "inputs": { - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1763606317, + "narHash": "sha256-lsq4Urmb9Iyg2zyg2yG6oMQk9yuaoIgy+jgvYM4guxA=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "a5615abaf30cfaef2e32f1ff9bd5ca94e2911371", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" } } }, diff --git a/flake.nix b/flake.nix index a948219..382e147 100644 --- a/flake.nix +++ b/flake.nix @@ -1,7 +1,12 @@ { - inputs.nixpkgs.url = "nixpkgs/nixos-unstable"; + inputs = { + nixpkgs.url = "nixpkgs/nixos-unstable"; - outputs = { self, nixpkgs }: let + rust-overlay.url = "github:oxalica/rust-overlay"; + rust-overlay.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = { self, nixpkgs, rust-overlay }: let inherit (nixpkgs) lib; systems = [ @@ -9,9 +14,21 @@ "aarch64-linux" ]; - forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system nixpkgs.legacyPackages.${system}); + forAllSystems = f: nixpkgs.lib.genAttrs systems (system: let + pkgs = import nixpkgs { + inherit system; + overlays = [ + (import rust-overlay) + ]; + }; + + rust-bin = rust-overlay.lib.mkRustBin { } pkgs.buildPackages; + toolchain = rust-bin.stable.latest.default.override { + extensions = [ "rust-src" ]; + }; + in f system pkgs toolchain); in { - apps = forAllSystems (system: pkgs: { + apps = forAllSystems (system: pkgs: _: { default = self.apps.${system}.vm; vm = { type = "app"; @@ -19,9 +36,47 @@ }; }); + devShell = forAllSystems (system: pkgs: toolchain: pkgs.mkShell { + nativeBuildInputs = with pkgs; [ + toolchain + cargo-edit + ]; + + RUST_SRC_PATH = "${toolchain}/lib/rustlib/src/rust/library"; + }); + + overlays = { + default = self.overlays.container-tool; + container-tool = final: prev: { + inherit (self.packages.${prev.stdenv.hostPlatform.system}) container-tool; + }; + }; + + packages = forAllSystems (system: pkgs: _: { + default = self.packages.${system}.container-tool; + container-tool = let + cargoToml = builtins.fromTOML (builtins.readFile ./container-tool/Cargo.toml); + in pkgs.callPackage ({ + lib, + rustPlatform, + }: + rustPlatform.buildRustPackage { + pname = cargoToml.package.name; + version = cargoToml.package.version; + src = pkgs.lib.cleanSource ./container-tool; + + cargoLock.lockFile = ./container-tool/Cargo.lock; + meta = with lib; { + license = licenses.mit; + platforms = platforms.linux ++ platforms.darwin; + mainProgram = (lib.head (cargoToml.bin)).name; + }; + }) { }; + }); + nixosModules.default = ./modules/user-jails.nix; - nixosConfigurations = lib.mapAttrs' (n: v: lib.nameValuePair "vm-${n}" v) (forAllSystems (system: pkgs: + nixosConfigurations = lib.mapAttrs' (n: v: lib.nameValuePair "vm-${n}" v) (forAllSystems (system: pkgs: _: lib.nixosSystem { inherit system pkgs; modules = [