tftsr-devops_investigation/src-tauri/src/proxmox/firewall.rs
Shaun Arman c3298b2d77
All checks were successful
Test / frontend-typecheck (pull_request) Successful in 1m51s
Test / frontend-tests (pull_request) Successful in 1m44s
PR Review Automation / review (pull_request) Successful in 6m43s
Test / rust-fmt-check (pull_request) Successful in 12m17s
Test / rust-clippy (pull_request) Successful in 14m1s
Test / rust-tests (pull_request) Successful in 15m36s
fix(fmt): apply cargo fmt to resolve CI formatting check failure
2026-06-21 16:18:23 -05:00

352 lines
10 KiB
Rust

// Firewall management module
// Provides operations for managing Proxmox firewall
use serde::{Deserialize, Serialize};
/// Firewall rule
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FirewallRule {
pub rule_num: u32,
pub action: String,
pub protocol: String,
pub source: String,
pub destination: String,
pub port: Option<String>,
pub enabled: bool,
}
/// Firewall status
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FirewallStatus {
pub enabled: bool,
pub rules: Vec<FirewallRule>,
pub rule_count: u32,
}
/// List firewall rules — returns normalized Vec<FirewallRule> using correct PVE field names.
/// PVE uses: pos (position), proto (protocol), enable (0/1 integer), src (source), dest (destination).
pub async fn list_firewall_rules(
client: &crate::proxmox::client::ProxmoxClient,
node: &str,
ticket: &str,
) -> Result<Vec<FirewallRule>, String> {
let path = format!("nodes/{}/firewall/rules", node);
let response: serde_json::Value = client
.get(&path, Some(ticket))
.await
.map_err(|e| format!("Failed to list firewall rules: {}", e))?;
let rules = response.as_array().ok_or("Invalid response format")?;
let rule_list: Vec<FirewallRule> = rules
.iter()
.filter_map(|rule| {
// PVE uses "pos" for the rule position number
let rule_num = rule.get("pos").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
let action = rule.get("action").and_then(|v| v.as_str())?.to_string();
// PVE uses "proto" not "protocol"
let protocol = rule
.get("proto")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
// source and dest are optional fields
let source = rule
.get("source")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let destination = rule
.get("dest")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let port = rule
.get("dport")
.or_else(|| rule.get("sport"))
.and_then(|p| p.as_str())
.map(|s| s.to_string());
// PVE uses "enable" as integer (1=enabled, 0=disabled), not "enabled" bool
let enabled = rule
.get("enable")
.and_then(|e| e.as_i64().map(|n| n != 0).or_else(|| e.as_bool()))
.unwrap_or(true);
Some(FirewallRule {
rule_num,
action,
protocol,
source,
destination,
port,
enabled,
})
})
.collect();
Ok(rule_list)
}
/// Add firewall rule — uses correct PVE API field names (proto, enable, dest).
/// `rule.rule_num` is intentionally not sent: PVE assigns the position (pos) automatically
/// on creation. rule_num is only used for update/delete operations on existing rules.
pub async fn add_rule(
client: &crate::proxmox::client::ProxmoxClient,
node: &str,
rule: &FirewallRule,
ticket: &str,
) -> Result<(), String> {
let path = format!("nodes/{}/firewall/rules", node);
let mut config = serde_json::json!({
"action": rule.action,
"type": "in",
"enable": if rule.enabled { 1 } else { 0 }
});
// Only include optional fields when non-empty
if !rule.protocol.is_empty() {
config["proto"] = serde_json::Value::String(rule.protocol.clone());
}
if !rule.source.is_empty() {
config["source"] = serde_json::Value::String(rule.source.clone());
}
if !rule.destination.is_empty() {
config["dest"] = serde_json::Value::String(rule.destination.clone());
}
if let Some(ref port) = rule.port {
if !port.is_empty() {
config["dport"] = serde_json::Value::String(port.clone());
}
}
let _response: serde_json::Value = client
.post(&path, &config, Some(ticket))
.await
.map_err(|e| format!("Failed to add firewall rule: {}", e))?;
Ok(())
}
/// Delete firewall rule
pub async fn delete_rule(
client: &crate::proxmox::client::ProxmoxClient,
node: &str,
rule_num: u32,
ticket: &str,
) -> Result<(), String> {
let path = format!("nodes/{}/firewall/rules/{}", node, rule_num);
let _response: serde_json::Value = client
.delete(&path, Some(ticket))
.await
.map_err(|e| format!("Failed to delete firewall rule {}: {}", rule_num, e))?;
Ok(())
}
/// Update firewall rule
pub async fn update_rule(
client: &crate::proxmox::client::ProxmoxClient,
node: &str,
rule_num: u32,
rule: &FirewallRule,
ticket: &str,
) -> Result<(), String> {
let path = format!("nodes/{}/firewall/rules/{}", node, rule_num);
let config = serde_json::json!({
"action": rule.action,
"protocol": rule.protocol,
"source": rule.source,
"dest": rule.destination,
"dport": rule.port,
"enabled": rule.enabled
});
let _response: serde_json::Value = client
.put(&path, &config, Some(ticket))
.await
.map_err(|e| format!("Failed to update firewall rule {}: {}", rule_num, e))?;
Ok(())
}
/// Enable firewall
pub async fn enable_firewall(
client: &crate::proxmox::client::ProxmoxClient,
node: &str,
ticket: &str,
) -> Result<(), String> {
let path = format!("nodes/{}/firewall/options", node);
let config = serde_json::json!({
"enabled": true
});
let _response: serde_json::Value = client
.put(&path, &config, Some(ticket))
.await
.map_err(|e| format!("Failed to enable firewall: {}", e))?;
Ok(())
}
/// Disable firewall
pub async fn disable_firewall(
client: &crate::proxmox::client::ProxmoxClient,
node: &str,
ticket: &str,
) -> Result<(), String> {
let path = format!("nodes/{}/firewall/options", node);
let config = serde_json::json!({
"enabled": false
});
let _response: serde_json::Value = client
.put(&path, &config, Some(ticket))
.await
.map_err(|e| format!("Failed to disable firewall: {}", e))?;
Ok(())
}
/// Get firewall status
pub async fn get_firewall_status(
client: &crate::proxmox::client::ProxmoxClient,
node: &str,
ticket: &str,
) -> Result<FirewallStatus, String> {
let path = format!("nodes/{}/firewall/rules", node);
let rules_response: serde_json::Value = client
.get(&path, Some(ticket))
.await
.map_err(|e| format!("Failed to get firewall rules: {}", e))?;
let enabled_path = format!("nodes/{}/firewall/options", node);
let options_response: serde_json::Value = client
.get(&enabled_path, Some(ticket))
.await
.map_err(|e| format!("Failed to get firewall options: {}", e))?;
let enabled = options_response
.get("enabled")
.and_then(|e| e.as_bool())
.unwrap_or(false);
let rules: Vec<FirewallRule> = rules_response
.as_array()
.unwrap_or(&Vec::new())
.iter()
.filter_map(|rule| {
let rule_num = rule.get("pos").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
let action = rule.get("action").and_then(|v| v.as_str())?.to_string();
let protocol = rule
.get("proto")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let source = rule
.get("source")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let destination = rule
.get("dest")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let port = rule
.get("dport")
.or_else(|| rule.get("sport"))
.and_then(|p| p.as_str())
.map(|s| s.to_string());
let enabled = rule
.get("enable")
.and_then(|e| e.as_i64().map(|n| n != 0).or_else(|| e.as_bool()))
.unwrap_or(true);
Some(FirewallRule {
rule_num,
action,
protocol,
source,
destination,
port,
enabled,
})
})
.collect();
let rule_count = rules.len() as u32;
Ok(FirewallStatus {
enabled,
rules,
rule_count,
})
}
/// Get firewall zone configuration
pub async fn get_firewall_zone(
client: &crate::proxmox::client::ProxmoxClient,
node: &str,
zone: &str,
ticket: &str,
) -> Result<serde_json::Value, String> {
let path = format!("nodes/{}/firewall/zones/{}", node, zone);
client
.get(&path, Some(ticket))
.await
.map_err(|e| format!("Failed to get firewall zone {}: {}", zone, e))
}
/// List firewall zones
pub async fn list_firewall_zones(
client: &crate::proxmox::client::ProxmoxClient,
node: &str,
ticket: &str,
) -> Result<Vec<serde_json::Value>, String> {
let path = format!("nodes/{}/firewall/zones", node);
let response: serde_json::Value = client
.get(&path, Some(ticket))
.await
.map_err(|e| format!("Failed to list firewall zones: {}", e))?;
if let Some(zones) = response.as_array() {
Ok(zones.to_vec())
} else {
Err("Invalid response format".to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_firewall_rule_serialization() {
let rule = FirewallRule {
rule_num: 1,
action: "ACCEPT".to_string(),
protocol: "tcp".to_string(),
source: "10.0.0.0/8".to_string(),
destination: "any".to_string(),
port: Some("443".to_string()),
enabled: true,
};
let json = serde_json::to_string(&rule).unwrap();
let deserialized: FirewallRule = serde_json::from_str(&json).unwrap();
assert_eq!(rule.action, deserialized.action);
assert_eq!(rule.enabled, deserialized.enabled);
}
#[test]
fn test_firewall_status_serialization() {
let status = FirewallStatus {
enabled: true,
rules: vec![],
rule_count: 0,
};
let json = serde_json::to_string(&status).unwrap();
let deserialized: FirewallStatus = serde_json::from_str(&json).unwrap();
assert_eq!(status.enabled, deserialized.enabled);
}
}