use anyhow::{Context, Result};
use std::fs;
use std::path::PathBuf;
use std::process::Command;
pub struct SystemdManager;
impl SystemdManager {
pub fn new() -> Self {
Self
}
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()))?;
Command::new("systemctl")
.args(["daemon-reload"])
.status()
.context("Failed to reload systemd")?;
tracing::info!("Systemd service installed: {}", service_path.display());
Ok(())
}
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(())
}
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(())
}
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(())
}
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(())
}
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)
}
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())
}
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()
}
}