porta-rs 0.1.1

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

/// Systemd service manager
pub struct SystemdManager;

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

    /// Install the systemd service
    pub fn install(&self, role: &str) -> Result<()> {
        let service_content = self.generate_service(role)?;

        let service_path = PathBuf::from(format!("/etc/systemd/system/porta-{}.service", role));
        fs::write(&service_path, &service_content)
            .context(format!("Failed to write service file to {}", service_path.display()))?;

        // Reload systemd
        Command::new("systemctl")
            .args(["daemon-reload"])
            .status()
            .context("Failed to reload systemd")?;

        tracing::info!("Systemd service installed: {}", service_path.display());
        Ok(())
    }

    /// Enable the service
    pub fn enable(&self, role: &str) -> Result<()> {
        let status = Command::new("systemctl")
            .args(["enable", &format!("porta-{}", role)])
            .status()
            .context("Failed to enable service")?;

        if !status.success() {
            return Err(anyhow::anyhow!("Failed to enable porta-{} service", role));
        }

        tracing::info!("Service porta-{} enabled", role);
        Ok(())
    }

    /// Start the service
    pub fn start(&self, role: &str) -> Result<()> {
        let status = Command::new("systemctl")
            .args(["start", &format!("porta-{}", role)])
            .status()
            .context("Failed to start service")?;

        if !status.success() {
            return Err(anyhow::anyhow!("Failed to start porta-{} service", role));
        }

        tracing::info!("Service porta-{} started", role);
        Ok(())
    }

    /// Stop the service
    pub fn stop(&self, role: &str) -> Result<()> {
        let status = Command::new("systemctl")
            .args(["stop", &format!("porta-{}", role)])
            .status()
            .context("Failed to stop service")?;

        if !status.success() {
            tracing::warn!("Failed to stop porta-{} service", role);
        } else {
            tracing::info!("Service porta-{} stopped", role);
        }

        Ok(())
    }

    /// Restart the service
    pub fn restart(&self, role: &str) -> Result<()> {
        let status = Command::new("systemctl")
            .args(["restart", &format!("porta-{}", role)])
            .status()
            .context("Failed to restart service")?;

        if !status.success() {
            return Err(anyhow::anyhow!("Failed to restart porta-{} service", role));
        }

        tracing::info!("Service porta-{} restarted", role);
        Ok(())
    }

    /// Check if the service is running
    pub fn is_running(&self, role: &str) -> bool {
        Command::new("systemctl")
            .args(["is-active", &format!("porta-{}", role)])
            .output()
            .map(|o| o.status.success())
            .unwrap_or(false)
    }

    /// Get service status
    pub fn status(&self, role: &str) -> Result<String> {
        let output = Command::new("systemctl")
            .args(["status", &format!("porta-{}", role)])
            .output()
            .context("Failed to get service status")?;

        Ok(String::from_utf8_lossy(&output.stdout).to_string())
    }

    /// Generate the service file content
    fn generate_service(&self, role: &str) -> Result<String> {
        let service = format!(
            r#"[Unit]
Description=Porta - Zero-trust CGNAT bypass via WireGuard ({role})
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/local/bin/porta run --role {role}
Restart=always
RestartSec=5
LimitNOFILE=65536

# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/etc/porta /var/lib/porta
PrivateTmp=true

[Install]
WantedBy=multi-user.target
"#,
            role = role
        );

        Ok(service)
    }
}

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