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, 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,
}
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 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,
};
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<()> {
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 _ = std::process::Command::new("ip")
.args(["link", "del", &veth_host])
.status();
let _ = std::process::Command::new("ip")
.args(["netns", "del", &state.ns_name])
.status();
let dir = crate::paths::sandbox_dir(id);
if dir.exists() {
std::fs::remove_dir_all(&dir)?;
}
Ok(())
}