porta-rs 0.1.1

Zero-trust CGNAT bypass via WireGuard tunnel
use anyhow::{Context, Result};

use crate::config::Role;

/// WireGuard interface manager
pub struct InterfaceManager {
    name: String,
}

impl InterfaceManager {
    pub fn new(name: &str) -> Self {
        Self {
            name: name.to_string(),
        }
    }

    /// Create and configure the WireGuard interface
    pub fn create(&self, role: Role, ip: &str, listen_port: u16) -> Result<()> {
        self.create_interface()?;
        self.configure_ip(ip)?;

        match role {
            Role::Server => {
                self.set_listen_port(listen_port)?;
            }
            Role::Client => {
                // Client doesn't listen, it connects
            }
        }

        self.bring_up()?;

        Ok(())
    }

    /// Create the WireGuard interface using ip link
    fn create_interface(&self) -> Result<()> {
        // Check if interface already exists
        if self.interface_exists() {
            tracing::info!("Interface {} already exists, skipping creation", self.name);
            return Ok(());
        }

        let status = std::process::Command::new("ip")
            .args(["link", "add", &self.name, "type", "wireguard"])
            .status()
            .context("Failed to execute 'ip link add'")?;

        if !status.success() {
            return Err(anyhow::anyhow!(
                "Failed to create WireGuard interface '{}'. Check if wireguard module is loaded.",
                self.name
            ));
        }

        tracing::info!("Created WireGuard interface: {}", self.name);
        Ok(())
    }

    /// Configure IP address on the interface
    fn configure_ip(&self, ip: &str) -> Result<()> {
        // Remove existing IPs first
        let _ = std::process::Command::new("ip")
            .args(["addr", "flush", "dev", &self.name])
            .status();

        let status = std::process::Command::new("ip")
            .args(["addr", "add", ip, "dev", &self.name])
            .status()
            .context("Failed to execute 'ip addr add'")?;

        if !status.success() {
            return Err(anyhow::anyhow!(
                "Failed to configure IP {} on interface {}",
                ip,
                self.name
            ));
        }

        tracing::info!("Configured IP {} on interface {}", ip, self.name);
        Ok(())
    }

    /// Set the listening port for the interface
    fn set_listen_port(&self, port: u16) -> Result<()> {
        let status = std::process::Command::new("wg")
            .args(["set", &self.name, "listen-port", &port.to_string()])
            .status()
            .context("Failed to execute 'wg set listen-port'")?;

        if !status.success() {
            return Err(anyhow::anyhow!(
                "Failed to set listen port {} on interface {}",
                port,
                self.name
            ));
        }

        tracing::info!("Set listen port {} on interface {}", port, self.name);
        Ok(())
    }

    /// Set private key on the interface
    pub fn set_private_key(&self, private_key: &str) -> Result<()> {
        let status = std::process::Command::new("wg")
            .args(["set", &self.name, "private-key", private_key])
            .status()
            .context("Failed to execute 'wg set private-key'")?;

        if !status.success() {
            return Err(anyhow::anyhow!(
                "Failed to set private key on interface {}",
                self.name
            ));
        }

        tracing::info!("Set private key on interface {}", self.name);
        Ok(())
    }

    /// Add a peer to the interface
    pub fn add_peer(&self, public_key: &str, endpoint: Option<&str>, allowed_ips: &[&str]) -> Result<()> {
        let mut args = vec![
            "set".to_string(),
            self.name.clone(),
            "peer".to_string(),
            public_key.to_string(),
        ];

        if let Some(ep) = endpoint {
            args.push("endpoint".to_string());
            args.push(ep.to_string());
        }

        if !allowed_ips.is_empty() {
            let joined = allowed_ips.join(",");
            args.push("allowed-ips".to_string());
            args.push(joined);
        }

        let status = std::process::Command::new("wg")
            .args(&args)
            .status()
            .context("Failed to execute 'wg set peer'")?;

        if !status.success() {
            return Err(anyhow::anyhow!(
                "Failed to add peer {} to interface {}",
                public_key,
                self.name
            ));
        }

        tracing::info!("Added peer {} to interface {}", public_key, self.name);
        Ok(())
    }

    /// Remove a peer from the interface
    pub fn remove_peer(&self, public_key: &str) -> Result<()> {
        let status = std::process::Command::new("wg")
            .args(["set", &self.name, "peer", public_key, "remove"])
            .status()
            .context("Failed to execute 'wg set peer remove'")?;

        if !status.success() {
            return Err(anyhow::anyhow!(
                "Failed to remove peer {} from interface {}",
                public_key,
                self.name
            ));
        }

        tracing::info!("Removed peer {} from interface {}", public_key, self.name);
        Ok(())
    }

    /// Bring the interface up
    fn bring_up(&self) -> Result<()> {
        let status = std::process::Command::new("ip")
            .args(["link", "set", &self.name, "up"])
            .status()
            .context("Failed to execute 'ip link set up'")?;

        if !status.success() {
            return Err(anyhow::anyhow!(
                "Failed to bring up interface {}",
                self.name
            ));
        }

        tracing::info!("Brought up interface: {}", self.name);
        Ok(())
    }

    /// Bring the interface down
    pub fn bring_down(&self) -> Result<()> {
        let status = std::process::Command::new("ip")
            .args(["link", "set", &self.name, "down"])
            .status()
            .context("Failed to execute 'ip link set down'")?;

        if !status.success() {
            tracing::warn!("Failed to bring down interface {}", self.name);
        } else {
            tracing::info!("Brought down interface: {}", self.name);
        }

        Ok(())
    }

    /// Delete the interface
    pub fn delete(&self) -> Result<()> {
        self.bring_down()?;

        let status = std::process::Command::new("ip")
            .args(["link", "delete", &self.name])
            .status()
            .context("Failed to execute 'ip link delete'")?;

        if !status.success() {
            tracing::warn!("Failed to delete interface {}", self.name);
        } else {
            tracing::info!("Deleted interface: {}", self.name);
        }

        Ok(())
    }

    /// Check if the interface exists
    fn interface_exists(&self) -> bool {
        std::process::Command::new("ip")
            .args(["link", "show", &self.name])
            .status()
            .map(|s| s.success())
            .unwrap_or(false)
    }

    /// Get interface status (up/down)
    pub fn is_up(&self) -> bool {
        std::process::Command::new("ip")
            .args(["link", "show", &self.name])
            .output()
            .map(|o| {
                let stdout = String::from_utf8_lossy(&o.stdout);
                stdout.contains("state UP")
            })
            .unwrap_or(false)
    }

    /// Add a route for traffic to flow through the tunnel
    pub fn add_route(&self, destination: &str, gateway: &str) -> Result<()> {
        let status = std::process::Command::new("ip")
            .args(["route", "add", destination, "via", gateway, "dev", &self.name])
            .status()
            .context("Failed to execute 'ip route add'")?;

        if !status.success() {
            tracing::warn!("Failed to add route {} via {}", destination, gateway);
        }

        Ok(())
    }
}