use std::process::Command;
use tracing::{debug, info, warn};
use starpod_core::{Result, StarpodError};
const NS_NAME: &str = "starpod-ns";
const VETH_HOST: &str = "sp-veth0";
const VETH_CHILD: &str = "sp-veth1";
const HOST_IP: &str = "10.200.1.1";
const CHILD_IP: &str = "10.200.1.2";
const SUBNET: &str = "10.200.1.0/24";
pub struct NamespaceHandle {
pub ns_path: String,
}
impl NamespaceHandle {
pub fn pre_exec_fn(&self) -> Box<dyn Fn() -> std::io::Result<()> + Send + Sync> {
let ns_path = self.ns_path.clone();
Box::new(move || {
use std::os::unix::io::AsRawFd;
let file = std::fs::File::open(&ns_path).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to open netns {ns_path}: {e}"),
)
})?;
nix::sched::setns(file.as_raw_fd(), nix::sched::CloneFlags::CLONE_NEWNET).map_err(
|e| std::io::Error::new(std::io::ErrorKind::Other, format!("setns failed: {e}")),
)?;
Ok(())
})
}
}
impl Drop for NamespaceHandle {
fn drop(&mut self) {
debug!("Cleaning up network namespace {NS_NAME}");
let _ = run_cmd("ip", &["netns", "delete", NS_NAME]);
}
}
pub fn create_namespace(proxy_port: u16) -> Result<NamespaceHandle> {
let _ = run_cmd("ip", &["netns", "delete", NS_NAME]);
run_cmd("ip", &["netns", "add", NS_NAME])
.map_err(|e| StarpodError::Proxy(format!("Failed to create netns: {e}")))?;
info!("Created network namespace {NS_NAME}");
run_cmd(
"ip",
&[
"link", "add", VETH_HOST, "type", "veth", "peer", "name", VETH_CHILD,
],
)
.map_err(|e| StarpodError::Proxy(format!("Failed to create veth pair: {e}")))?;
run_cmd("ip", &["link", "set", VETH_CHILD, "netns", NS_NAME])
.map_err(|e| StarpodError::Proxy(format!("Failed to move veth to netns: {e}")))?;
run_cmd(
"ip",
&["addr", "add", &format!("{HOST_IP}/24"), "dev", VETH_HOST],
)
.map_err(|e| StarpodError::Proxy(format!("Failed to configure host veth: {e}")))?;
run_cmd("ip", &["link", "set", VETH_HOST, "up"])
.map_err(|e| StarpodError::Proxy(format!("Failed to bring up host veth: {e}")))?;
run_ns_cmd(&format!("{CHILD_IP}/24"), "addr", &["add"], VETH_CHILD)
.map_err(|e| StarpodError::Proxy(format!("Failed to configure child veth: {e}")))?;
run_cmd(
"ip",
&[
"netns", "exec", NS_NAME, "ip", "link", "set", VETH_CHILD, "up",
],
)
.map_err(|e| StarpodError::Proxy(format!("Failed to bring up child veth: {e}")))?;
run_cmd(
"ip",
&["netns", "exec", NS_NAME, "ip", "link", "set", "lo", "up"],
)
.map_err(|e| StarpodError::Proxy(format!("Failed to bring up child loopback: {e}")))?;
run_cmd(
"ip",
&[
"netns", "exec", NS_NAME, "ip", "route", "add", "default", "via", HOST_IP,
],
)
.map_err(|e| StarpodError::Proxy(format!("Failed to set default route: {e}")))?;
let _ = std::fs::write("/proc/sys/net/ipv4/ip_forward", "1");
run_cmd(
"iptables",
&[
"-t",
"nat",
"-A",
"POSTROUTING",
"-s",
SUBNET,
"-j",
"MASQUERADE",
],
)
.map_err(|e| StarpodError::Proxy(format!("Failed to set up NAT: {e}")))?;
let proxy_dest = format!("{HOST_IP}:{proxy_port}");
for port in &["80", "443"] {
run_cmd(
"iptables",
&[
"-t",
"nat",
"-A",
"PREROUTING",
"-s",
SUBNET,
"-p",
"tcp",
"--dport",
port,
"-j",
"DNAT",
"--to-destination",
&proxy_dest,
],
)
.map_err(|e| StarpodError::Proxy(format!("Failed to set up DNAT for port {port}: {e}")))?;
}
let ns_path = format!("/var/run/netns/{NS_NAME}");
info!(
ns_path = %ns_path,
proxy_port = proxy_port,
"Network namespace isolation active"
);
Ok(NamespaceHandle { ns_path })
}
fn run_cmd(program: &str, args: &[&str]) -> std::result::Result<(), String> {
let output = Command::new(program)
.args(args)
.output()
.map_err(|e| format!("{program}: {e}"))?;
if output.status.success() {
Ok(())
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(format!(
"{program} {} exited {}: {}",
args.join(" "),
output.status,
stderr.trim()
))
}
}
fn run_ns_cmd(
addr: &str,
subcmd: &str,
args: &[&str],
dev: &str,
) -> std::result::Result<(), String> {
let mut cmd_args = vec!["netns", "exec", NS_NAME, "ip", subcmd];
cmd_args.extend_from_slice(args);
cmd_args.push(addr);
cmd_args.push("dev");
cmd_args.push(dev);
run_cmd("ip", &cmd_args)
}