use super::{PortConfig, Protocol};
use crate::deployer::types::{ComposeExec, DeployError, DeployResult};
use std::collections::HashSet;
pub struct UfwManager<'a> {
executor: &'a mut (dyn ComposeExec + Send),
}
impl<'a> UfwManager<'a> {
pub fn new(executor: &'a mut (dyn ComposeExec + Send)) -> Self {
Self { executor }
}
pub async fn ensure_ufw(&mut self) -> DeployResult<()> {
let result = self
.executor
.execute_command("which ufw")
.await
.map_err(|e| DeployError::Firewall(format!("Failed to check UFW: {}", e)))?;
if !result.is_success() {
let install_cmd = "apt-get update && apt-get install -y ufw";
self.executor
.execute_command(install_cmd)
.await
.map_err(|e| DeployError::Firewall(format!("Failed to install UFW: {}", e)))?;
}
let status = self
.executor
.execute_command("ufw status")
.await
.map_err(|e| DeployError::Firewall(format!("Failed to check UFW status: {}", e)))?;
if !status.output.to_stdout_string()?.contains("Status: active") {
self.executor
.execute_command("ufw allow 22/tcp")
.await
.map_err(|e| DeployError::Firewall(format!("Failed to allow SSH: {}", e)))?;
self.executor
.execute_command("echo 'y' | ufw enable")
.await
.map_err(|e| DeployError::Firewall(format!("Failed to enable UFW: {}", e)))?;
}
Ok(())
}
pub async fn configure_ports(&mut self, ports: &[PortConfig]) -> DeployResult<()> {
self.ensure_ufw().await?;
let current_ports = self.get_opened_ports().await?;
for port_config in ports {
if !self.is_port_configured(¤t_ports, port_config) {
self.add_port_rule(port_config).await?;
}
}
Ok(())
}
async fn get_opened_ports(&mut self) -> DeployResult<HashSet<String>> {
let result = self
.executor
.execute_command("ufw status numbered")
.await
.map_err(|e| DeployError::Firewall(format!("Failed to get UFW status: {}", e)))?;
let output = result.output.to_stdout_string()?;
let mut ports = HashSet::new();
for line in output.lines() {
if line.contains("ALLOW") {
if let Some(port_str) = self.extract_port_from_rule(line) {
ports.insert(port_str);
}
}
}
Ok(ports)
}
fn extract_port_from_rule(&self, rule: &str) -> Option<String> {
let parts: Vec<&str> = rule.split_whitespace().collect();
parts
.iter()
.find(|&&p| p.contains('/'))
.map(|&s| s.to_string())
}
fn is_port_configured(&self, current_ports: &HashSet<String>, config: &PortConfig) -> bool {
match config.protocol {
Protocol::Both => {
current_ports.contains(&format!("{}/tcp", config.port))
&& current_ports.contains(&format!("{}/udp", config.port))
}
_ => current_ports.contains(&format!("{}/{}", config.port, config.protocol)),
}
}
async fn add_port_rule(&mut self, config: &PortConfig) -> DeployResult<()> {
let comment = if config.description.is_empty() {
"Managed by DCD".to_string()
} else {
format!("DCD: {}", config.description)
};
match config.protocol {
Protocol::Both => {
self.add_single_port_rule(config.port, "tcp", &comment)
.await?;
self.add_single_port_rule(config.port, "udp", &comment)
.await?;
}
_ => {
self.add_single_port_rule(config.port, &config.protocol.to_string(), &comment)
.await?;
}
}
Ok(())
}
async fn add_single_port_rule(
&mut self,
port: u16,
protocol: &str,
comment: &str,
) -> DeployResult<()> {
let cmd = format!(
"ufw allow {}/{} comment '{}'",
port,
protocol,
comment.replace('\'', "\\'")
);
self.executor.execute_command(&cmd).await.map_err(|e| {
DeployError::Firewall(format!(
"Failed to add port rule {}/{}: {}",
port, protocol, e
))
})?;
Ok(())
}
pub async fn verify_port(&mut self, port: u16, protocol: &Protocol) -> DeployResult<bool> {
if matches!(protocol, Protocol::Tcp | Protocol::Both) {
let cmd = format!("nc -z -v localhost {}", port);
let result = self
.executor
.execute_command(&cmd)
.await
.map_err(|e| DeployError::Firewall(e.to_string()))?;
if !result.is_success() {
return Ok(false);
}
}
if matches!(protocol, Protocol::Udp | Protocol::Both) {
let ports = self.get_opened_ports().await?;
if !ports.contains(&format!("{}/udp", port)) {
return Ok(false);
}
}
Ok(true)
}
}