diff --git a/src/sandbox/manager.rs b/src/sandbox/manager.rs index d9c1f3d..7e50aab 100644 --- a/src/sandbox/manager.rs +++ b/src/sandbox/manager.rs @@ -18,7 +18,7 @@ use std::sync::atomic::{AtomicU16, Ordering}; use std::sync::Arc; use std::time::{Duration, Instant, SystemTime}; -use anyhow::{bail, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use serde::Serialize; use tokio::sync::RwLock; use tracing::{debug, error, info, warn}; @@ -289,6 +289,13 @@ impl SandboxManager { ) { Ok(info) => Some(info), Err(e) => { + if config.network != network::NetworkMode::None { + cleanup_failed_create(&sandbox_id, init_pid); + return Err(anyhow!(e).context(format!( + "failed to initialize network for sandbox '{}'", + sandbox_id + ))); + } warn!(sandbox = sandbox_id, error = %e, "network setup failed"); None } @@ -1141,6 +1148,22 @@ fn spawn_init_process( Ok(init_pid) } +fn cleanup_failed_create(sandbox_id: &str, init_pid: i32) { + let _ = nix::sys::signal::kill( + nix::unistd::Pid::from_raw(init_pid), + nix::sys::signal::Signal::SIGKILL, + ); + std::thread::sleep(Duration::from_millis(100)); + + if let Ok(cgroup) = CgroupManager::create(sandbox_id) { + let _ = cgroup.kill_all(); + std::thread::sleep(Duration::from_millis(50)); + let _ = cgroup.cleanup(); + } + + let _ = cleanup_rootfs(sandbox_id); +} + /// Cleans up all resources for a sandbox. fn destroy_sandbox_resources(sandbox: &ManagedSandbox) { // Kill opencode serve process if running. diff --git a/src/sandbox/network.rs b/src/sandbox/network.rs index 078ebda..76926e6 100644 --- a/src/sandbox/network.rs +++ b/src/sandbox/network.rs @@ -35,6 +35,7 @@ //! ``` use std::fs; +use std::hash::{DefaultHasher, Hash, Hasher}; use std::path::{Path, PathBuf}; use std::process::Command; @@ -143,10 +144,10 @@ pub fn setup_network(sandbox_id: &str, mode: &NetworkMode, child_pid: Pid) -> Re ensure_bridge(&bridge, DEFAULT_GATEWAY)?; let ip = allocate_ip(sandbox_id)?; - let veth_host = format!("veth-{}", &sandbox_id[..sandbox_id.len().min(6)]); + let veth_host = format!("veth-{}", veth_suffix(sandbox_id)); let veth_sandbox = "eth0"; - setup_veth_pair( + if let Err(e) = setup_veth_pair( sandbox_id, &veth_host, veth_sandbox, @@ -154,7 +155,10 @@ pub fn setup_network(sandbox_id: &str, mode: &NetworkMode, child_pid: Pid) -> Re &ip, DEFAULT_GATEWAY, child_pid, - )?; + ) { + let _ = release_ip(sandbox_id); + return Err(e); + } Ok(NetworkInfo { mode: mode.clone(), @@ -170,10 +174,10 @@ pub fn setup_network(sandbox_id: &str, mode: &NetworkMode, child_pid: Pid) -> Re ensure_bridge(&bridge, gateway)?; let ip = allocate_ip(sandbox_id)?; - let veth_host = format!("veth-{}", &sandbox_id[..sandbox_id.len().min(6)]); + let veth_host = format!("veth-{}", veth_suffix(sandbox_id)); let veth_sandbox = "eth0"; - setup_veth_pair( + if let Err(e) = setup_veth_pair( sandbox_id, &veth_host, veth_sandbox, @@ -181,7 +185,10 @@ pub fn setup_network(sandbox_id: &str, mode: &NetworkMode, child_pid: Pid) -> Re &ip, gateway, child_pid, - )?; + ) { + let _ = release_ip(sandbox_id); + return Err(e); + } Ok(NetworkInfo { mode: mode.clone(), @@ -259,8 +266,34 @@ fn ensure_bridge(name: &str, gateway: &str) -> Result<()> { run_cmd("ip", &["link", "set", name, "up"]) .with_context(|| format!("failed to bring up bridge {name}"))?; + // Ensure the bridge has the expected gateway IP even when it already exists. + let bridge_has_gateway = run_cmd( + "ip", + &[ + "-4", + "addr", + "show", + "dev", + name, + "to", + &format!("{gateway}/16"), + ], + ) + .is_ok(); + if !bridge_has_gateway { + run_cmd( + "ip", + &["addr", "add", &format!("{gateway}/16"), "dev", name], + ) + .with_context(|| { + format!("failed to assign missing gateway IP {gateway}/16 on bridge {name}") + })?; + } + // Always ensure IP forwarding is enabled — required for NAT. - let _ = fs::write("/proc/sys/net/ipv4/ip_forward", "1"); + fs::write("/proc/sys/net/ipv4/ip_forward", "1").with_context(|| { + "failed to enable net.ipv4.ip_forward (required for sandbox internet access)" + })?; // Always ensure the MASQUERADE rule exists. Use -C to check first so we // do not pile up duplicate rules on every sandbox start. @@ -291,7 +324,8 @@ fn ensure_bridge(name: &str, gateway: &str) -> Result<()> { "-j", "MASQUERADE", ]; - let _ = run_cmd("iptables", &add); + run_cmd("iptables", &add) + .with_context(|| "failed to add MASQUERADE rule for sandbox subnet")?; } Ok(()) @@ -312,7 +346,10 @@ fn setup_veth_pair( // Use a temporary name for the sandbox-side veth to avoid conflicts // (e.g., "eth0" already exists on the host inside Docker). - let veth_tmp = format!("vp-{}", &sandbox_id[..sandbox_id.len().min(6)]); + let veth_tmp = format!("vp-{}", veth_suffix(sandbox_id)); + + // If a stale interface with the same generated name exists, remove it first. + let _ = run_cmd("ip", &["link", "del", veth_host]); // Create the veth pair on the host with a temporary peer name. run_cmd( @@ -411,6 +448,13 @@ fn setup_veth_pair( Ok(()) } +fn veth_suffix(sandbox_id: &str) -> String { + let mut hasher = DefaultHasher::new(); + sandbox_id.hash(&mut hasher); + let hex = format!("{:016x}", hasher.finish()); + hex[..8].to_string() +} + /// Cleans up networking resources for a sandbox. /// /// Deletes the host-side veth interface (which automatically removes the peer)