herolib-mos 0.3.13

Mycelium Operating System (MOS) - Network and VM abstraction layer
Documentation
use super::error::{NetworkError, Result};
use super::namespace::NetNamespace;
use super::types::Route;
use std::process::Command;

/// Add a route
pub fn add_route(route: &Route, ns: Option<&NetNamespace>) -> Result<()> {
    // Detect if this is an IPv6 route
    let is_ipv6 =
        route.gateway.is_ipv6() || route.destination.as_ref().map_or(false, |d| d.is_ipv6());

    let mut args = vec![];

    // Add -6 flag for IPv6 routes
    if is_ipv6 {
        args.push("-6");
    }

    args.push("route");
    args.push("add");

    // Destination
    let dest_str;
    if let Some(dest) = &route.destination {
        dest_str = dest.to_string();
        args.push(&dest_str);
    } else {
        args.push("default");
    }

    // Gateway
    args.push("via");
    let gw_str = route.gateway.to_string();
    args.push(&gw_str);

    // Interface (optional)
    if let Some(ref iface) = route.interface {
        args.push("dev");
        args.push(iface);
    }

    let output = if let Some(namespace) = ns {
        let mut ns_args = vec!["netns", "exec", &namespace.name, "ip"];
        ns_args.extend_from_slice(&args);
        Command::new("ip")
            .args(&ns_args)
            .output()
            .map_err(|e| NetworkError::Route(format!("Failed to add route: {}", e)))?
    } else {
        Command::new("ip")
            .args(&args)
            .output()
            .map_err(|e| NetworkError::Route(format!("Failed to add route: {}", e)))?
    };

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        // Ignore "File exists" error (route already exists)
        if !stderr.contains("File exists") {
            return Err(NetworkError::Route(format!(
                "Failed to add route: {}",
                stderr
            )));
        }
    }

    Ok(())
}

/// Delete a route
pub fn delete_route(route: &Route, ns: Option<&NetNamespace>) -> Result<()> {
    // Detect if this is an IPv6 route
    let is_ipv6 =
        route.gateway.is_ipv6() || route.destination.as_ref().map_or(false, |d| d.is_ipv6());

    let mut args = vec![];

    // Add -6 flag for IPv6 routes
    if is_ipv6 {
        args.push("-6");
    }

    args.push("route");
    args.push("del");

    // Destination
    let dest_str;
    if let Some(dest) = &route.destination {
        dest_str = dest.to_string();
        args.push(&dest_str);
    } else {
        args.push("default");
    }

    // Gateway
    args.push("via");
    let gw_str = route.gateway.to_string();
    args.push(&gw_str);

    // Interface (optional)
    if let Some(ref iface) = route.interface {
        args.push("dev");
        args.push(iface);
    }

    let output = if let Some(namespace) = ns {
        let mut ns_args = vec!["netns", "exec", &namespace.name, "ip"];
        ns_args.extend_from_slice(&args);
        Command::new("ip")
            .args(&ns_args)
            .output()
            .map_err(|e| NetworkError::Route(format!("Failed to delete route: {}", e)))?
    } else {
        Command::new("ip")
            .args(&args)
            .output()
            .map_err(|e| NetworkError::Route(format!("Failed to delete route: {}", e)))?
    };

    if !output.status.success() {
        return Err(NetworkError::Route(format!(
            "Failed to delete route: {}",
            String::from_utf8_lossy(&output.stderr)
        )));
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::Route;
    use ipnetwork::IpNetwork;
    use std::net::IpAddr;
    use std::str::FromStr;

    #[test]
    fn test_route_with_all_fields() {
        let route = Route {
            destination: Some(IpNetwork::from_str("10.0.0.0/24").unwrap()),
            gateway: IpAddr::from_str("10.0.0.1").unwrap(),
            interface: Some("eth0".to_string()),
        };

        assert!(route.destination.is_some());
        assert_eq!(route.gateway.to_string(), "10.0.0.1");
        assert_eq!(route.interface, Some("eth0".to_string()));
    }

    #[test]
    fn test_default_route() {
        let route = Route {
            destination: None,
            gateway: IpAddr::from_str("192.168.1.1").unwrap(),
            interface: None,
        };

        assert!(route.destination.is_none());
        assert_eq!(route.gateway.to_string(), "192.168.1.1");
    }

    #[test]
    fn test_ipv6_route() {
        let route = Route {
            destination: Some(IpNetwork::from_str("2001:db8::/64").unwrap()),
            gateway: IpAddr::from_str("2001:db8::1").unwrap(),
            interface: Some("eth0".to_string()),
        };

        assert!(route.destination.is_some());
        assert!(route.gateway.to_string().contains("2001:db8"));
    }
}