use std::process::{Command, Stdio};
use crate::driver::VmError;
fn validate_interface_name(name: &str) -> Result<(), VmError> {
if name.is_empty() || name.len() > 15 {
return Err(VmError::InvalidConfig(format!(
"interface name must be 1-15 characters, got {} characters",
name.len()
)));
}
if !name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
return Err(VmError::InvalidConfig(format!(
"interface name '{}' contains invalid characters (only alphanumeric, hyphen, underscore allowed)",
name
)));
}
Ok(())
}
pub fn ensure_bridge(name: &str, gateway_ip: &str, subnet_cidr: &str) -> Result<(), VmError> {
validate_interface_name(name)?;
if !link_exists(name) {
run_ip(&["link", "add", name, "type", "bridge"])?;
tracing::info!(bridge = %name, "created bridge");
}
run_ip(&["link", "set", name, "up"])?;
let addr_output = Command::new("ip")
.args(["addr", "show", "dev", name])
.output()
.map_err(|e| VmError::Hypervisor(format!("failed to check bridge address: {}", e)))?;
let addr_str = String::from_utf8_lossy(&addr_output.stdout);
let cidr = format!(
"{}/{}",
gateway_ip,
subnet_cidr.split('/').next_back().unwrap_or("24")
);
if !addr_str.contains(&cidr) {
run_ip(&["addr", "add", &cidr, "dev", name])?;
}
std::fs::write("/proc/sys/net/ipv4/ip_forward", "1").map_err(|e| {
VmError::Hypervisor(format!(
"failed to enable IPv4 forwarding for bridge '{}': {}",
name, e
))
})?;
setup_nat(name, subnet_cidr)?;
tracing::info!(bridge = %name, gateway = %gateway_ip, "bridge ready");
Ok(())
}
pub fn create_tap(name: &str) -> Result<(), VmError> {
validate_interface_name(name)?;
run_ip(&["tuntap", "add", "dev", name, "mode", "tap"])?;
run_ip(&["link", "set", name, "up"])?;
tracing::debug!(tap = %name, "TAP device created");
Ok(())
}
pub fn add_to_bridge(tap: &str, bridge: &str) -> Result<(), VmError> {
validate_interface_name(tap)?;
validate_interface_name(bridge)?;
run_ip(&["link", "set", tap, "master", bridge])?;
tracing::debug!(tap = %tap, bridge = %bridge, "TAP added to bridge");
Ok(())
}
pub fn delete_tap(name: &str) {
let status = Command::new("ip")
.args(["tuntap", "del", "dev", name, "mode", "tap"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
match status {
Ok(s) if s.success() => tracing::debug!(tap = %name, "TAP device deleted"),
_ => tracing::debug!(tap = %name, "TAP device cleanup (may not exist)"),
}
}
pub fn delete_bridge(name: &str) {
if let Err(e) = Command::new("ip")
.args(["link", "set", name, "down"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
{
tracing::warn!(bridge = %name, "failed to bring bridge down: {}", e);
}
match Command::new("ip")
.args(["link", "del", name, "type", "bridge"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
{
Ok(s) if s.success() => {
tracing::info!(bridge = %name, "bridge deleted");
}
Ok(s) => {
tracing::warn!(bridge = %name, exit = %s, "bridge deletion failed (may not exist)");
}
Err(e) => {
tracing::error!(bridge = %name, "failed to run ip command for bridge deletion: {}", e);
}
}
}
fn setup_nat(bridge: &str, subnet: &str) -> Result<(), VmError> {
let check = Command::new("iptables")
.args([
"-t",
"nat",
"-C",
"POSTROUTING",
"-s",
subnet,
"!",
"-o",
bridge,
"-j",
"MASQUERADE",
])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
if let Ok(s) = check {
if s.success() {
return Ok(()); }
}
let status = Command::new("iptables")
.args([
"-t",
"nat",
"-A",
"POSTROUTING",
"-s",
subnet,
"!",
"-o",
bridge,
"-j",
"MASQUERADE",
])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map_err(|e| VmError::Hypervisor(format!("failed to add iptables NAT rule: {}", e)))?;
if !status.success() {
return Err(VmError::Hypervisor(format!(
"iptables MASQUERADE rule failed (exit {}). Are you running as root?",
status
)));
}
Ok(())
}
fn link_exists(name: &str) -> bool {
match Command::new("ip")
.args(["link", "show", "dev", name])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
{
Ok(s) => s.success(),
Err(e) => {
tracing::warn!(link = %name, "failed to check if link exists: {}", e);
false
}
}
}
fn run_ip(args: &[&str]) -> Result<(), VmError> {
let output = Command::new("ip")
.args(args)
.stdout(Stdio::null())
.stderr(Stdio::piped())
.output()
.map_err(|e| VmError::Hypervisor(format!("failed to run ip {}: {}", args.join(" "), e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("File exists") {
return Ok(());
}
return Err(VmError::Hypervisor(format!(
"ip {} failed: {}",
args.join(" "),
stderr.trim()
)));
}
Ok(())
}