use serde::{Deserialize, Serialize};
use std::io;
use std::path::PathBuf;
#[derive(Debug)]
pub enum SandboxError {
NotFound(String),
NotRunning(String),
Io(io::Error),
}
impl std::fmt::Display for SandboxError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SandboxError::NotFound(msg) => write!(f, "sandbox not found: {}", msg),
SandboxError::NotRunning(id) => {
write!(f, "sandbox '{}' is not running (pause process dead)", id)
}
SandboxError::Io(e) => write!(f, "sandbox I/O error: {}", e),
}
}
}
impl std::error::Error for SandboxError {}
impl From<io::Error> for SandboxError {
fn from(e: io::Error) -> Self {
SandboxError::Io(e)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum NsMode {
#[default]
Pod,
Container,
Node,
}
impl NsMode {
pub fn from_cri(mode: i32) -> Self {
match mode {
2 => NsMode::Node,
1 => NsMode::Container,
_ => NsMode::Pod,
}
}
pub fn is_host(self) -> bool {
matches!(self, NsMode::Node)
}
}
fn default_pid_mode() -> NsMode {
NsMode::Container
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct NamespaceModes {
#[serde(default)]
pub network: NsMode,
#[serde(default = "default_pid_mode")]
pub pid: NsMode,
#[serde(default)]
pub ipc: NsMode,
}
impl Default for NamespaceModes {
fn default() -> Self {
NamespaceModes {
network: NsMode::Pod,
pid: default_pid_mode(),
ipc: NsMode::Pod,
}
}
}
impl NamespaceModes {
pub fn host_network(&self) -> bool {
self.network.is_host()
}
pub fn host_ipc(&self) -> bool {
self.ipc.is_host()
}
pub fn host_pid(&self) -> bool {
self.pid.is_host()
}
pub fn shared_pid(&self) -> bool {
matches!(self.pid, NsMode::Pod)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SandboxState {
pub id: String,
pub name: Option<String>,
pub pause_pid: i32,
pub ns_name: String,
pub veth_host: String,
pub container_ip: String,
#[serde(default)]
pub namespaces: NamespaceModes,
}
impl SandboxState {
pub fn load(id: &str) -> io::Result<Self> {
let path = crate::paths::sandbox_dir(id).join("state.json");
let data = std::fs::read_to_string(&path)
.map_err(|e| io::Error::other(format!("sandbox '{}' not found: {}", id, e)))?;
serde_json::from_str(&data).map_err(|e| io::Error::other(e.to_string()))
}
pub fn save(&self) -> io::Result<()> {
let dir = crate::paths::sandbox_dir(&self.id);
std::fs::create_dir_all(&dir)?;
let json =
serde_json::to_string_pretty(self).map_err(|e| io::Error::other(e.to_string()))?;
std::fs::write(dir.join("state.json"), json)
}
pub fn net_ns_path(&self) -> PathBuf {
PathBuf::from(format!("/run/netns/{}", self.ns_name))
}
pub fn ipc_ns_path(&self) -> PathBuf {
PathBuf::from(format!("/proc/{}/ns/ipc", self.pause_pid))
}
pub fn uts_ns_path(&self) -> PathBuf {
PathBuf::from(format!("/proc/{}/ns/uts", self.pause_pid))
}
pub fn pid_ns_path(&self) -> PathBuf {
PathBuf::from(format!("/proc/{}/ns/pid", self.pause_pid))
}
pub fn is_alive(&self) -> bool {
if self.pause_pid <= 0 {
return false;
}
unsafe { libc::kill(self.pause_pid, 0) == 0 }
}
}
pub fn list_sandboxes() -> Vec<SandboxState> {
let dir = crate::paths::sandboxes_dir();
let Ok(entries) = std::fs::read_dir(&dir) else {
return Vec::new();
};
let mut sandboxes = Vec::new();
for entry in entries.flatten() {
let state_file = entry.path().join("state.json");
if state_file.exists() {
if let Ok(data) = std::fs::read_to_string(&state_file) {
if let Ok(s) = serde_json::from_str::<SandboxState>(&data) {
sandboxes.push(s);
}
}
}
}
sandboxes.sort_by(|a, b| a.id.cmp(&b.id));
sandboxes
}
pub fn generate_sandbox_id() -> String {
let mut buf = [0u8; 8];
if let Ok(mut f) = std::fs::File::open("/dev/urandom") {
use std::io::Read;
let _ = f.read_exact(&mut buf);
} else {
let pid = unsafe { libc::getpid() } as u64;
let t = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.subsec_nanos() as u64;
let v = pid ^ (t << 32) ^ t;
buf.copy_from_slice(&v.to_le_bytes());
}
buf.iter().map(|b| format!("{:02x}", b)).collect()
}
pub fn create_sandbox(name: Option<&str>) -> io::Result<SandboxState> {
if unsafe { libc::getuid() } != 0 {
return Err(io::Error::other(
"sandbox create requires root (bridge networking needs CAP_NET_ADMIN)",
));
}
let id = generate_sandbox_id();
let ns_name = format!("psb-{}", &id[..8]);
let dir = crate::paths::sandbox_dir(&id);
std::fs::create_dir_all(&dir)?;
let net_setup = crate::network::setup_bridge_network(&ns_name, "pelagos0", false, vec![])
.map_err(|e| io::Error::other(format!("sandbox bridge network setup failed: {}", e)))?;
let container_ip = net_setup.container_ip.to_string();
let veth_host = net_setup.veth_host.clone();
let exe = std::env::current_exe()
.map_err(|e| io::Error::other(format!("cannot find current executable: {}", e)))?;
let child = std::process::Command::new(&exe)
.args(["sandbox", "__pause__", &ns_name])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.map_err(|e| io::Error::other(format!("failed to spawn pause process: {}", e)))?;
let pause_pid = child.id() as i32;
std::mem::forget(child);
std::thread::sleep(std::time::Duration::from_millis(50));
let state = SandboxState {
id: id.clone(),
name: name.map(|s| s.to_string()),
pause_pid,
ns_name: ns_name.clone(),
veth_host,
container_ip,
namespaces: NamespaceModes::default(),
};
state.save()?;
std::fs::write(
crate::paths::sandbox_pid_file(&id),
format!("{}", pause_pid),
)?;
std::fs::write(crate::paths::sandbox_ns_name_file(&id), &ns_name)?;
if let Some(n) = name {
std::fs::write(crate::paths::sandbox_name_file(&id), n)?;
}
std::mem::forget(net_setup);
Ok(state)
}
pub fn remove_sandbox(id: &str) -> io::Result<()> {
if id.is_empty() || id.contains('/') || id == "." || id == ".." {
return Err(io::Error::other(format!("invalid sandbox id '{}'", id)));
}
let state = SandboxState::load(id)?;
if state.is_alive() {
unsafe { libc::kill(state.pause_pid, libc::SIGTERM) };
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
while state.is_alive() && std::time::Instant::now() < deadline {
std::thread::sleep(std::time::Duration::from_millis(50));
}
if state.is_alive() {
unsafe { libc::kill(state.pause_pid, libc::SIGKILL) };
}
}
let veth_host = state.veth_host.clone();
let _ = crate::netlink::link_del(&veth_host);
let _ = crate::netlink::netns_del(&state.ns_name);
let dir = crate::paths::sandbox_dir(id);
if dir.exists() {
crate::paths::guarded_remove_dir_all(&dir)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_nsmode_from_cri() {
assert_eq!(NsMode::from_cri(0), NsMode::Pod);
assert_eq!(NsMode::from_cri(1), NsMode::Container);
assert_eq!(NsMode::from_cri(2), NsMode::Node);
assert_eq!(NsMode::from_cri(99), NsMode::Pod);
assert!(NsMode::Node.is_host());
assert!(!NsMode::Pod.is_host());
assert!(!NsMode::Container.is_host());
}
#[test]
fn test_namespace_modes_host_helpers() {
let m = NamespaceModes {
network: NsMode::Node,
pid: NsMode::Pod,
ipc: NsMode::Node,
};
assert!(m.host_network());
assert!(!m.host_pid());
assert!(m.host_ipc());
assert!(m.shared_pid()); let d = NamespaceModes::default();
assert!(!d.host_network() && !d.host_pid() && !d.host_ipc());
assert!(!d.shared_pid());
assert!(matches!(d.pid, NsMode::Container));
let shared = NamespaceModes {
pid: NsMode::Pod,
..Default::default()
};
assert!(shared.shared_pid() && !shared.host_pid());
}
#[test]
fn test_namespace_modes_serde_contract() {
let m = NamespaceModes {
network: NsMode::Node,
pid: NsMode::Container,
ipc: NsMode::Pod,
};
let json = serde_json::to_string(&m).unwrap();
assert_eq!(json, r#"{"network":"Node","pid":"Container","ipc":"Pod"}"#);
let back: NamespaceModes = serde_json::from_str(&json).unwrap();
assert!(back.host_network() && !back.host_pid() && !back.host_ipc());
let legacy: SandboxState = serde_json::from_str(
r#"{"id":"x","name":null,"pause_pid":1,"ns_name":"n","veth_host":"","container_ip":"1.2.3.4"}"#,
)
.unwrap();
assert!(!legacy.namespaces.host_network());
assert!(!legacy.namespaces.shared_pid());
assert!(matches!(legacy.namespaces.pid, NsMode::Container));
let shared = NamespaceModes {
pid: NsMode::Pod,
..Default::default()
};
let back: NamespaceModes =
serde_json::from_str(&serde_json::to_string(&shared).unwrap()).unwrap();
assert!(back.shared_pid());
}
}