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::VethPair;
use ipnetwork::IpNetwork;
use std::process::Command;

/// Create a veth pair
pub fn create_veth_pair(
    local_name: &str,
    peer_name: &str,
    mtu: u32,
    prefix: &str,
) -> Result<VethPair> {
    let peer_full_name = if prefix.is_empty() {
        peer_name.to_string()
    } else {
        format!("{}-{}", prefix, peer_name)
    };

    let output = Command::new("ip")
        .args([
            "link",
            "add",
            local_name,
            "type",
            "veth",
            "peer",
            "name",
            &peer_full_name,
        ])
        .output()
        .map_err(|e| NetworkError::Interface(format!("Failed to create veth pair: {}", e)))?;

    if !output.status.success() {
        return Err(NetworkError::Interface(format!(
            "Failed to create veth pair: {}",
            String::from_utf8_lossy(&output.stderr)
        )));
    }

    // Set MTU on both ends
    for iface in &[local_name, &peer_full_name] {
        let output = Command::new("ip")
            .args(["link", "set", "dev", iface, "mtu", &mtu.to_string()])
            .output()
            .map_err(|e| NetworkError::Interface(format!("Failed to set MTU: {}", e)))?;

        if !output.status.success() {
            return Err(NetworkError::Interface(format!(
                "Failed to set MTU for {}: {}",
                iface,
                String::from_utf8_lossy(&output.stderr)
            )));
        }
    }

    Ok(VethPair {
        local_name: local_name.to_string(),
        peer_name: peer_full_name,
        mtu,
    })
}

/// Move an interface to a network namespace
pub fn move_to_namespace(interface: &str, ns: &NetNamespace) -> Result<()> {
    let output = Command::new("ip")
        .args(["link", "set", interface, "netns", &ns.name])
        .output()
        .map_err(|e| {
            NetworkError::Interface(format!("Failed to move interface to namespace: {}", e))
        })?;

    if !output.status.success() {
        return Err(NetworkError::Interface(format!(
            "Failed to move interface {} to namespace: {}",
            interface,
            String::from_utf8_lossy(&output.stderr)
        )));
    }

    Ok(())
}

/// Check if an interface exists
pub fn exists(name: &str, ns: Option<&NetNamespace>) -> Result<bool> {
    let output = if let Some(namespace) = ns {
        Command::new("ip")
            .args(["netns", "exec", &namespace.name, "ip", "link", "show", name])
            .output()
            .map_err(|e| NetworkError::Interface(format!("Failed to check interface: {}", e)))?
    } else {
        Command::new("ip")
            .args(["link", "show", name])
            .output()
            .map_err(|e| NetworkError::Interface(format!("Failed to check interface: {}", e)))?
    };

    Ok(output.status.success())
}

/// Set an interface up
pub fn set_up(name: &str, ns: Option<&NetNamespace>) -> Result<()> {
    let output = if let Some(namespace) = ns {
        Command::new("ip")
            .args([
                "netns",
                "exec",
                &namespace.name,
                "ip",
                "link",
                "set",
                "dev",
                name,
                "up",
            ])
            .output()
            .map_err(|e| NetworkError::Interface(format!("Failed to set interface up: {}", e)))?
    } else {
        Command::new("ip")
            .args(["link", "set", "dev", name, "up"])
            .output()
            .map_err(|e| NetworkError::Interface(format!("Failed to set interface up: {}", e)))?
    };

    if !output.status.success() {
        return Err(NetworkError::Interface(format!(
            "Failed to set interface {} up: {}",
            name,
            String::from_utf8_lossy(&output.stderr)
        )));
    }

    Ok(())
}

/// Set IP address on an interface
pub fn set_addr(name: &str, addr: &IpNetwork, ns: Option<&NetNamespace>) -> Result<()> {
    let addr_str = addr.to_string();

    let output = if let Some(namespace) = ns {
        Command::new("ip")
            .args([
                "netns",
                "exec",
                &namespace.name,
                "ip",
                "addr",
                "add",
                &addr_str,
                "dev",
                name,
            ])
            .output()
            .map_err(|e| NetworkError::Interface(format!("Failed to set IP address: {}", e)))?
    } else {
        Command::new("ip")
            .args(["addr", "add", &addr_str, "dev", name])
            .output()
            .map_err(|e| NetworkError::Interface(format!("Failed to set IP address: {}", e)))?
    };

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        // Ignore "File exists" error (address already set)
        if !stderr.contains("File exists") {
            return Err(NetworkError::Interface(format!(
                "Failed to set IP address {} on {}: {}",
                addr_str, name, stderr
            )));
        }
    }

    Ok(())
}

/// Attach an interface to a bridge
pub fn attach_to_bridge(interface: &str, bridge: &str) -> Result<()> {
    let output = Command::new("ip")
        .args(["link", "set", interface, "master", bridge])
        .output()
        .map_err(|e| {
            NetworkError::Interface(format!("Failed to attach interface to bridge: {}", e))
        })?;

    if !output.status.success() {
        return Err(NetworkError::Interface(format!(
            "Failed to attach interface {} to bridge {}: {}",
            interface,
            bridge,
            String::from_utf8_lossy(&output.stderr)
        )));
    }

    // Bring the interface up
    set_up(interface, None)?;

    Ok(())
}

/// Delete an interface
pub fn delete_interface(name: &str) -> Result<()> {
    let output = Command::new("ip")
        .args(["link", "delete", name])
        .output()
        .map_err(|e| NetworkError::Interface(format!("Failed to delete interface: {}", e)))?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        // Ignore "Cannot find device" error (interface already deleted)
        if !stderr.contains("Cannot find device") {
            return Err(NetworkError::Interface(format!(
                "Failed to delete interface {}: {}",
                name, stderr
            )));
        }
    }

    Ok(())
}

/// Enable IPv6 forwarding
pub fn enable_ipv6_forwarding(ns: Option<&NetNamespace>) -> Result<()> {
    let output = if let Some(namespace) = ns {
        Command::new("ip")
            .args([
                "netns",
                "exec",
                &namespace.name,
                "sysctl",
                "-w",
                "net.ipv6.conf.all.forwarding=1",
            ])
            .output()
            .map_err(|e| {
                NetworkError::Interface(format!("Failed to enable IPv6 forwarding: {}", e))
            })?
    } else {
        Command::new("sysctl")
            .args(["-w", "net.ipv6.conf.all.forwarding=1"])
            .output()
            .map_err(|e| {
                NetworkError::Interface(format!("Failed to enable IPv6 forwarding: {}", e))
            })?
    };

    if !output.status.success() {
        return Err(NetworkError::Interface(format!(
            "Failed to enable IPv6 forwarding: {}",
            String::from_utf8_lossy(&output.stderr)
        )));
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_veth_pair_struct() {
        let veth = VethPair {
            local_name: "veth0".to_string(),
            peer_name: "veth1".to_string(),
            mtu: 1500,
        };

        assert_eq!(veth.local_name, "veth0");
        assert_eq!(veth.peer_name, "veth1");
        assert_eq!(veth.mtu, 1500);
    }

    #[test]
    fn test_veth_pair_with_prefix() {
        let _local = "public";
        let peer = "br0";
        let prefix = "test";

        let expected_peer = format!("{}-{}", prefix, peer);
        assert_eq!(expected_peer, "test-br0");
    }

    #[test]
    fn test_veth_pair_without_prefix() {
        let _local = "public";
        let peer = "br0";
        let prefix = "";

        let expected_peer = if prefix.is_empty() {
            peer.to_string()
        } else {
            format!("{}-{}", prefix, peer)
        };
        assert_eq!(expected_peer, "br0");
    }

    #[test]
    fn test_mtu_values() {
        let standard_mtu = 1500u32;
        let jumbo_mtu = 9000u32;

        assert_eq!(standard_mtu, 1500);
        assert_eq!(jumbo_mtu, 9000);
    }
}