container-tool: init

This commit is contained in:
2025-11-20 22:59:47 +09:00
parent fe15f8a9cc
commit 09a39017f9
6 changed files with 1480 additions and 6 deletions

527
container-tool/Cargo.lock generated Normal file
View File

@@ -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"

15
container-tool/Cargo.toml Normal file
View File

@@ -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"

687
container-tool/src/main.rs Normal file
View File

@@ -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<String>,
personality: Option<String>,
host_bridge: Option<String>,
host_address: Option<Ipv4Addr>,
local_address: Option<Ipv4Addr>,
host_address6: Option<Ipv6Addr>,
local_address6: Option<Ipv6Addr>,
#[serde(default)]
port: Vec<Port>,
#[serde(default)]
interfaces: Vec<String>,
#[serde(default)]
macvlans: Vec<String>,
#[serde(default)]
extra_veths: Vec<Veth>,
#[serde(default)]
bind_mounts: Vec<BindMount>,
#[serde(default)]
additional_capabilities: Vec<String>,
#[serde(default)]
tmpfs: Vec<String>,
#[serde(default)]
extra_nspawn_args: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct Veth {
name: String,
host_bridge: Option<String>,
host_address: Option<String>,
local_address: Option<String>,
host_address6: Option<String>,
local_address6: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct BindMount {
source: String,
target: String,
options: Vec<String>,
}
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<Self, Self::Err> {
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<Self, Self::Err> {
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!();
}

View File

@@ -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<u32> {
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::<LinkUnspec>::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::<Ipv4Addr>::new()
.index(if_index)
.address(ipv4, DEFAULT_PREFIX_LEN_V4)
.build(),
IpAddr::V6(ipv6) => AddressMessageBuilder::<Ipv6Addr>::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::<Ipv4Addr>::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::<Ipv6Addr>::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::<Ipv4Addr>::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::<Ipv6Addr>::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::<Ipv4Addr>::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::<Ipv6Addr>::new()
.destination_prefix(ipv6, DEFAULT_PREFIX_LEN_V6)
.output_interface(if_index)
.build();
handle.route().del(message).execute().await
}
}
}

23
flake.lock generated
View File

@@ -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"
}
}
},

View File

@@ -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 = [