Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion src/sandbox/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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.
Expand Down
62 changes: 53 additions & 9 deletions src/sandbox/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
//! ```

use std::fs;
use std::hash::{DefaultHasher, Hash, Hasher};
use std::path::{Path, PathBuf};
use std::process::Command;

Expand Down Expand Up @@ -143,18 +144,21 @@ 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,
&bridge,
&ip,
DEFAULT_GATEWAY,
child_pid,
)?;
) {
let _ = release_ip(sandbox_id);
return Err(e);
}

Ok(NetworkInfo {
mode: mode.clone(),
Expand All @@ -170,18 +174,21 @@ 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,
&bridge,
&ip,
gateway,
child_pid,
)?;
) {
let _ = release_ip(sandbox_id);
return Err(e);
}

Ok(NetworkInfo {
mode: mode.clone(),
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(())
Expand All @@ -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(
Expand Down Expand Up @@ -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)
Expand Down
Loading