/ crates / acdc-config / src / firewall.rs
firewall.rs
  1  //! Firewall configuration.
  2  
  3  use acdc_core::{config::NodeInstance, Error, Result};
  4  use acdc_tui::output;
  5  use std::process::Command;
  6  use tracing::debug;
  7  
  8  /// Configure firewall rules for a node instance.
  9  pub fn configure(instance: &NodeInstance) -> Result<()> {
 10      if !has_ufw() {
 11          output::info(
 12              "Firewall",
 13              "UFW not available, skipping firewall configuration",
 14          );
 15          return Ok(());
 16      }
 17  
 18      output::status("Configuring firewall rules...");
 19  
 20      let ports = instance.ports.all();
 21      for port in ports {
 22          allow_port(port)?;
 23      }
 24  
 25      // Always allow SSH
 26      allow_port(22)?;
 27  
 28      // Radicle P2P port
 29      allow_port(8776)?;
 30  
 31      output::success("Firewall rules configured");
 32      Ok(())
 33  }
 34  
 35  /// Check if ufw is available.
 36  pub fn has_ufw() -> bool {
 37      Command::new("which")
 38          .arg("ufw")
 39          .output()
 40          .map(|o| o.status.success())
 41          .unwrap_or(false)
 42  }
 43  
 44  /// Check if ufw is enabled.
 45  pub fn is_ufw_enabled() -> bool {
 46      Command::new("sudo")
 47          .args(["ufw", "status"])
 48          .output()
 49          .map(|o| {
 50              let stdout = String::from_utf8_lossy(&o.stdout);
 51              stdout.contains("Status: active")
 52          })
 53          .unwrap_or(false)
 54  }
 55  
 56  /// Allow a port through the firewall.
 57  pub fn allow_port(port: u16) -> Result<()> {
 58      debug!("Allowing port {} through firewall", port);
 59  
 60      let output = Command::new("sudo")
 61          .args(["ufw", "allow", &port.to_string()])
 62          .output()
 63          .map_err(Error::Io)?;
 64  
 65      if !output.status.success() {
 66          let stderr = String::from_utf8_lossy(&output.stderr);
 67          return Err(Error::Config(format!(
 68              "Failed to allow port {}: {}",
 69              port, stderr
 70          )));
 71      }
 72  
 73      Ok(())
 74  }
 75  
 76  /// Deny a port through the firewall.
 77  pub fn deny_port(port: u16) -> Result<()> {
 78      debug!("Denying port {} through firewall", port);
 79  
 80      let output = Command::new("sudo")
 81          .args(["ufw", "deny", &port.to_string()])
 82          .output()
 83          .map_err(Error::Io)?;
 84  
 85      if !output.status.success() {
 86          let stderr = String::from_utf8_lossy(&output.stderr);
 87          return Err(Error::Config(format!(
 88              "Failed to deny port {}: {}",
 89              port, stderr
 90          )));
 91      }
 92  
 93      Ok(())
 94  }
 95  
 96  /// Delete a firewall rule for a port.
 97  pub fn delete_rule(port: u16) -> Result<()> {
 98      debug!("Deleting firewall rule for port {}", port);
 99  
100      let output = Command::new("sudo")
101          .args(["ufw", "delete", "allow", &port.to_string()])
102          .output()
103          .map_err(Error::Io)?;
104  
105      if !output.status.success() {
106          // Non-fatal, rule might not exist
107          debug!("Could not delete rule for port {}", port);
108      }
109  
110      Ok(())
111  }
112  
113  /// Remove firewall rules for a node instance.
114  pub fn remove(instance: &NodeInstance) -> Result<()> {
115      if !has_ufw() {
116          return Ok(());
117      }
118  
119      output::status("Removing firewall rules...");
120  
121      let ports = instance.ports.all();
122      for port in ports {
123          delete_rule(port)?;
124      }
125  
126      output::success("Firewall rules removed");
127      Ok(())
128  }
129  
130  /// Print current firewall status.
131  pub fn print_status() -> Result<()> {
132      if !has_ufw() {
133          output::info("Firewall", "UFW not installed");
134          return Ok(());
135      }
136  
137      let output = Command::new("sudo")
138          .args(["ufw", "status", "verbose"])
139          .output()
140          .map_err(Error::Io)?;
141  
142      let stdout = String::from_utf8_lossy(&output.stdout);
143      println!("{}", stdout);
144  
145      Ok(())
146  }
147  
148  /// Enable the firewall.
149  pub fn enable() -> Result<()> {
150      output::status("Enabling firewall...");
151  
152      let output = Command::new("sudo")
153          .args(["ufw", "--force", "enable"])
154          .output()
155          .map_err(Error::Io)?;
156  
157      if !output.status.success() {
158          let stderr = String::from_utf8_lossy(&output.stderr);
159          return Err(Error::Config(format!(
160              "Failed to enable firewall: {}",
161              stderr
162          )));
163      }
164  
165      output::success("Firewall enabled");
166      Ok(())
167  }
168  
169  /// Disable the firewall.
170  pub fn disable() -> Result<()> {
171      output::status("Disabling firewall...");
172  
173      let output = Command::new("sudo")
174          .args(["ufw", "disable"])
175          .output()
176          .map_err(Error::Io)?;
177  
178      if !output.status.success() {
179          let stderr = String::from_utf8_lossy(&output.stderr);
180          return Err(Error::Config(format!(
181              "Failed to disable firewall: {}",
182              stderr
183          )));
184      }
185  
186      output::success("Firewall disabled");
187      Ok(())
188  }