porta-rs 0.1.1

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

use crate::config::Protocol;

/// UFW rule manager
pub struct UfwManager;

impl UfwManager {
    pub fn new() -> Self {
        Self
    }

    /// Add a UFW rule to allow traffic on a port
    pub fn allow_port(&self, port: u16, protocol: &Protocol) -> Result<()> {
        let proto_str = match protocol {
            Protocol::Tcp => "tcp",
            Protocol::Udp => "udp",
        };

        let status = Command::new("ufw")
            .args(["allow", &format!("{}/{}", port, proto_str)])
            .status()
            .context("Failed to execute 'ufw allow'")?;

        if !status.success() {
            return Err(anyhow::anyhow!(
                "Failed to add UFW rule for port {} {}",
                port,
                proto_str
            ));
        }

        tracing::info!("UFW rule added: allow {} {}", port, proto_str);
        Ok(())
    }

    /// Remove a UFW rule
    pub fn deny_port(&self, port: u16, protocol: &Protocol) -> Result<()> {
        let proto_str = match protocol {
            Protocol::Tcp => "tcp",
            Protocol::Udp => "udp",
        };

        let status = Command::new("ufw")
            .args(["delete", "allow", &format!("{}/{}", port, proto_str)])
            .status()
            .context("Failed to execute 'ufw delete allow'")?;

        if !status.success() {
            tracing::warn!(
                "Failed to remove UFW rule for port {} {} (may not exist)",
                port,
                proto_str
            );
        } else {
            tracing::info!("UFW rule removed: allow {} {}", port, proto_str);
        }

        Ok(())
    }

    /// Check if UFW is active
    pub fn is_active(&self) -> bool {
        Command::new("ufw")
            .args(["status"])
            .output()
            .map(|o| {
                let stdout = String::from_utf8_lossy(&o.stdout);
                stdout.contains("active")
            })
            .unwrap_or(false)
    }

    /// Enable UFW
    pub fn enable(&self) -> Result<()> {
        let status = Command::new("ufw")
            .args(["--force", "enable"])
            .status()
            .context("Failed to enable UFW")?;

        if !status.success() {
            return Err(anyhow::anyhow!("Failed to enable UFW"));
        }

        tracing::info!("UFW enabled");
        Ok(())
    }

    /// Reload UFW rules
    pub fn reload(&self) -> Result<()> {
        let status = Command::new("ufw")
            .args(["reload"])
            .status()
            .context("Failed to reload UFW")?;

        if !status.success() {
            return Err(anyhow::anyhow!("Failed to reload UFW"));
        }

        tracing::info!("UFW reloaded");
        Ok(())
    }
}

impl Default for UfwManager {
    fn default() -> Self {
        Self::new()
    }
}