[pve-devel] [PATCH proxmox-firewall] firewall: improve error handling of firewall

Stefan Hanreich s.hanreich at proxmox.com
Thu Apr 25 19:23:07 CEST 2024


Error handling of the firewall binary should now be much more robust
on configuration errors. Instead of panicking in some cases it should
now log an error.

Signed-off-by: Stefan Hanreich <s.hanreich at proxmox.com>
---
 proxmox-firewall/src/bin/proxmox-firewall.rs |   7 +-
 proxmox-firewall/src/config.rs               | 239 +++++++++----------
 proxmox-firewall/src/firewall.rs             |   7 +-
 proxmox-firewall/tests/integration_tests.rs  |  51 ++--
 4 files changed, 155 insertions(+), 149 deletions(-)

diff --git a/proxmox-firewall/src/bin/proxmox-firewall.rs b/proxmox-firewall/src/bin/proxmox-firewall.rs
index 4e07993..b61007d 100644
--- a/proxmox-firewall/src/bin/proxmox-firewall.rs
+++ b/proxmox-firewall/src/bin/proxmox-firewall.rs
@@ -5,6 +5,7 @@ use std::time::{Duration, Instant};
 
 use anyhow::{Context, Error};
 
+use proxmox_firewall::config::{FirewallConfig, PveFirewallConfigLoader, PveNftConfigLoader};
 use proxmox_firewall::firewall::Firewall;
 use proxmox_nftables::{client::NftError, NftClient};
 
@@ -24,7 +25,9 @@ fn remove_firewall() -> Result<(), std::io::Error> {
 }
 
 fn handle_firewall() -> Result<(), Error> {
-    let firewall = Firewall::new();
+    let config = FirewallConfig::new(&PveFirewallConfigLoader::new(), &PveNftConfigLoader::new())?;
+
+    let firewall = Firewall::new(config);
 
     if !firewall.is_enabled() {
         return remove_firewall().with_context(|| "could not remove firewall tables".to_string());
@@ -84,7 +87,7 @@ fn main() -> Result<(), std::io::Error> {
         let start = Instant::now();
 
         if let Err(error) = handle_firewall() {
-            log::error!("error creating firewall rules: {error}");
+            log::error!("error updating firewall rules: {error}");
         }
 
         let duration = start.elapsed();
diff --git a/proxmox-firewall/src/config.rs b/proxmox-firewall/src/config.rs
index 2cf3e39..5bd2512 100644
--- a/proxmox-firewall/src/config.rs
+++ b/proxmox-firewall/src/config.rs
@@ -2,9 +2,8 @@ use std::collections::BTreeMap;
 use std::default::Default;
 use std::fs::File;
 use std::io::{self, BufReader};
-use std::sync::OnceLock;
 
-use anyhow::Error;
+use anyhow::{format_err, Context, Error};
 
 use proxmox_ve_config::firewall::cluster::Config as ClusterConfig;
 use proxmox_ve_config::firewall::guest::Config as GuestConfig;
@@ -19,15 +18,19 @@ use proxmox_nftables::types::ListChain;
 use proxmox_nftables::NftClient;
 
 pub trait FirewallConfigLoader {
-    fn cluster(&self) -> Option<Box<dyn io::BufRead>>;
-    fn host(&self) -> Option<Box<dyn io::BufRead>>;
-    fn guest_list(&self) -> GuestMap;
-    fn guest_config(&self, vmid: &Vmid, guest: &GuestEntry) -> Option<Box<dyn io::BufRead>>;
-    fn guest_firewall_config(&self, vmid: &Vmid) -> Option<Box<dyn io::BufRead>>;
+    fn cluster(&self) -> Result<Option<Box<dyn io::BufRead>>, Error>;
+    fn host(&self) -> Result<Option<Box<dyn io::BufRead>>, Error>;
+    fn guest_list(&self) -> Result<GuestMap, Error>;
+    fn guest_config(
+        &self,
+        vmid: &Vmid,
+        guest: &GuestEntry,
+    ) -> Result<Option<Box<dyn io::BufRead>>, Error>;
+    fn guest_firewall_config(&self, vmid: &Vmid) -> Result<Option<Box<dyn io::BufRead>>, Error>;
 }
 
 #[derive(Default)]
-struct PveFirewallConfigLoader {}
+pub struct PveFirewallConfigLoader {}
 
 impl PveFirewallConfigLoader {
     pub fn new() -> Self {
@@ -56,69 +59,70 @@ const CLUSTER_CONFIG_PATH: &str = "/etc/pve/firewall/cluster.fw";
 const HOST_CONFIG_PATH: &str = "/etc/pve/local/host.fw";
 
 impl FirewallConfigLoader for PveFirewallConfigLoader {
-    fn cluster(&self) -> Option<Box<dyn io::BufRead>> {
+    fn cluster(&self) -> Result<Option<Box<dyn io::BufRead>>, Error> {
         log::info!("loading cluster config");
 
-        let fd =
-            open_config_file(CLUSTER_CONFIG_PATH).expect("able to read cluster firewall config");
+        let fd = open_config_file(CLUSTER_CONFIG_PATH)?;
 
         if let Some(file) = fd {
             let buf_reader = Box::new(BufReader::new(file)) as Box<dyn io::BufRead>;
-            return Some(buf_reader);
+            return Ok(Some(buf_reader));
         }
 
-        None
+        Ok(None)
     }
 
-    fn host(&self) -> Option<Box<dyn io::BufRead>> {
+    fn host(&self) -> Result<Option<Box<dyn io::BufRead>>, Error> {
         log::info!("loading host config");
 
-        let fd = open_config_file(HOST_CONFIG_PATH).expect("able to read host firewall config");
+        let fd = open_config_file(HOST_CONFIG_PATH)?;
 
         if let Some(file) = fd {
             let buf_reader = Box::new(BufReader::new(file)) as Box<dyn io::BufRead>;
-            return Some(buf_reader);
+            return Ok(Some(buf_reader));
         }
 
-        None
+        Ok(None)
     }
 
-    fn guest_list(&self) -> GuestMap {
+    fn guest_list(&self) -> Result<GuestMap, Error> {
         log::info!("loading vmlist");
-        GuestMap::new().expect("able to read vmlist")
+        GuestMap::new()
     }
 
-    fn guest_config(&self, vmid: &Vmid, entry: &GuestEntry) -> Option<Box<dyn io::BufRead>> {
+    fn guest_config(
+        &self,
+        vmid: &Vmid,
+        entry: &GuestEntry,
+    ) -> Result<Option<Box<dyn io::BufRead>>, Error> {
         log::info!("loading guest #{vmid} config");
 
-        let fd = open_config_file(&GuestMap::config_path(vmid, entry))
-            .expect("able to read guest config");
+        let fd = open_config_file(&GuestMap::config_path(vmid, entry))?;
 
         if let Some(file) = fd {
             let buf_reader = Box::new(BufReader::new(file)) as Box<dyn io::BufRead>;
-            return Some(buf_reader);
+            return Ok(Some(buf_reader));
         }
 
-        None
+        Ok(None)
     }
 
-    fn guest_firewall_config(&self, vmid: &Vmid) -> Option<Box<dyn io::BufRead>> {
+    fn guest_firewall_config(&self, vmid: &Vmid) -> Result<Option<Box<dyn io::BufRead>>, Error> {
         log::info!("loading guest #{vmid} firewall config");
 
-        let fd = open_config_file(&GuestMap::firewall_config_path(vmid))
-            .expect("able to read guest firewall config");
+        let fd = open_config_file(&GuestMap::firewall_config_path(vmid))?;
 
         if let Some(file) = fd {
             let buf_reader = Box::new(BufReader::new(file)) as Box<dyn io::BufRead>;
-            return Some(buf_reader);
+            return Ok(Some(buf_reader));
         }
 
-        None
+        Ok(None)
     }
 }
 
 pub trait NftConfigLoader {
-    fn chains(&self) -> CommandOutput;
+    fn chains(&self) -> Result<Option<CommandOutput>, Error>;
 }
 
 #[derive(Debug, Default)]
@@ -131,131 +135,122 @@ impl PveNftConfigLoader {
 }
 
 impl NftConfigLoader for PveNftConfigLoader {
-    fn chains(&self) -> CommandOutput {
+    fn chains(&self) -> Result<Option<CommandOutput>, Error> {
         log::info!("querying nftables config for chains");
 
         let commands = Commands::new(vec![List::chains()]);
 
         NftClient::run_json_commands(&commands)
-            .expect("can query chains in nftables")
-            .expect("nft returned output")
+            .with_context(|| "unable to query nft chains".to_string())
     }
 }
 
 pub struct FirewallConfig {
-    firewall_loader: Box<dyn FirewallConfigLoader>,
-    nft_loader: Box<dyn NftConfigLoader>,
-    cluster_config: OnceLock<ClusterConfig>,
-    host_config: OnceLock<HostConfig>,
-    guest_config: OnceLock<BTreeMap<Vmid, GuestConfig>>,
-    nft_config: OnceLock<BTreeMap<String, ListChain>>,
-}
-
-impl Default for FirewallConfig {
-    fn default() -> Self {
-        Self {
-            firewall_loader: Box::new(PveFirewallConfigLoader::new()),
-            nft_loader: Box::new(PveNftConfigLoader::new()),
-            cluster_config: OnceLock::new(),
-            host_config: OnceLock::new(),
-            guest_config: OnceLock::new(),
-            nft_config: OnceLock::new(),
-        }
-    }
+    cluster_config: ClusterConfig,
+    host_config: HostConfig,
+    guest_config: BTreeMap<Vmid, GuestConfig>,
+    nft_config: BTreeMap<String, ListChain>,
 }
 
 impl FirewallConfig {
-    pub fn new(
-        firewall_loader: Box<dyn FirewallConfigLoader>,
-        nft_loader: Box<dyn NftConfigLoader>,
-    ) -> Self {
-        Self {
-            firewall_loader,
-            nft_loader,
-            cluster_config: OnceLock::new(),
-            host_config: OnceLock::new(),
-            guest_config: OnceLock::new(),
-            nft_config: OnceLock::new(),
+    fn parse_cluster(firewall_loader: &dyn FirewallConfigLoader) -> Result<ClusterConfig, Error> {
+        match firewall_loader.cluster()? {
+            Some(data) => ClusterConfig::parse(data),
+            None => {
+                log::info!("no cluster config found, falling back to default");
+                Ok(ClusterConfig::default())
+            }
         }
     }
 
-    pub fn cluster(&self) -> &ClusterConfig {
-        self.cluster_config.get_or_init(|| {
-            let raw_config = self.firewall_loader.cluster();
-
-            match raw_config {
-                Some(data) => ClusterConfig::parse(data).expect("cluster firewall config is valid"),
-                None => {
-                    log::info!("no cluster config found, falling back to default");
-                    ClusterConfig::default()
-                }
+    fn parse_host(firewall_loader: &dyn FirewallConfigLoader) -> Result<HostConfig, Error> {
+        match firewall_loader.host()? {
+            Some(data) => HostConfig::parse(data),
+            None => {
+                log::info!("no host config found, falling back to default");
+                Ok(HostConfig::default())
             }
-        })
+        }
     }
 
-    pub fn host(&self) -> &HostConfig {
-        self.host_config.get_or_init(|| {
-            let raw_config = self.firewall_loader.host();
-
-            match raw_config {
-                Some(data) => HostConfig::parse(data).expect("host firewall config is valid"),
-                None => {
-                    log::info!("no host config found, falling back to default");
-                    HostConfig::default()
-                }
+    pub fn parse_guests(
+        firewall_loader: &dyn FirewallConfigLoader,
+    ) -> Result<BTreeMap<Vmid, GuestConfig>, Error> {
+        let mut guests = BTreeMap::new();
+
+        for (vmid, entry) in firewall_loader.guest_list()?.iter() {
+            if !entry.is_local() {
+                log::debug!("guest #{vmid} is not local, skipping");
+                continue;
             }
-        })
-    }
 
-    pub fn guests(&self) -> &BTreeMap<Vmid, GuestConfig> {
-        self.guest_config.get_or_init(|| {
-            let mut guests = BTreeMap::new();
+            let raw_firewall_config = firewall_loader.guest_firewall_config(vmid)?;
 
-            for (vmid, entry) in self.firewall_loader.guest_list().iter() {
-                if !entry.is_local() {
-                    log::debug!("guest #{vmid} is not local, skipping");
-                    continue;
-                }
+            if let Some(raw_firewall_config) = raw_firewall_config {
+                log::debug!("found firewall config for #{vmid}, loading guest config");
 
-                let raw_firewall_config = self.firewall_loader.guest_firewall_config(vmid);
+                let raw_config = firewall_loader
+                    .guest_config(vmid, entry)?
+                    .ok_or_else(|| format_err!("could not load guest config for #{vmid}"))?;
 
-                if let Some(raw_firewall_config) = raw_firewall_config {
-                    log::debug!("found firewall config for #{vmid}, loading guest config");
+                let config = GuestConfig::parse(
+                    vmid,
+                    entry.ty().iface_prefix(),
+                    raw_firewall_config,
+                    raw_config,
+                )?;
 
-                    let raw_config = self
-                        .firewall_loader
-                        .guest_config(vmid, entry)
-                        .expect("guest config exists if firewall config exists");
+                guests.insert(*vmid, config);
+            }
+        }
 
-                    let config = GuestConfig::parse(
-                        vmid,
-                        entry.ty().iface_prefix(),
-                        raw_firewall_config,
-                        raw_config,
-                    )
-                    .expect("guest config is valid");
+        Ok(guests)
+    }
 
-                    guests.insert(*vmid, config);
-                }
+    pub fn parse_nft(
+        nft_loader: &dyn NftConfigLoader,
+    ) -> Result<BTreeMap<String, ListChain>, Error> {
+        let output = nft_loader
+            .chains()?
+            .ok_or_else(|| format_err!("no command output from nft query"))?;
+
+        let mut chains = BTreeMap::new();
+
+        for element in &output.nftables {
+            if let ListOutput::Chain(chain) = element {
+                chains.insert(chain.name().to_owned(), chain.clone());
             }
+        }
+
+        Ok(chains)
+    }
 
-            guests
+    pub fn new(
+        firewall_loader: &dyn FirewallConfigLoader,
+        nft_loader: &dyn NftConfigLoader,
+    ) -> Result<Self, Error> {
+        Ok(Self {
+            cluster_config: Self::parse_cluster(firewall_loader)?,
+            host_config: Self::parse_host(firewall_loader)?,
+            guest_config: Self::parse_guests(firewall_loader)?,
+            nft_config: Self::parse_nft(nft_loader)?,
         })
     }
 
-    pub fn nft_chains(&self) -> &BTreeMap<String, ListChain> {
-        self.nft_config.get_or_init(|| {
-            let output = self.nft_loader.chains();
-            let mut chains = BTreeMap::new();
+    pub fn cluster(&self) -> &ClusterConfig {
+        &self.cluster_config
+    }
 
-            for element in &output.nftables {
-                if let ListOutput::Chain(chain) = element {
-                    chains.insert(chain.name().to_owned(), chain.clone());
-                }
-            }
+    pub fn host(&self) -> &HostConfig {
+        &self.host_config
+    }
 
-            chains
-        })
+    pub fn guests(&self) -> &BTreeMap<Vmid, GuestConfig> {
+        &self.guest_config
+    }
+
+    pub fn nft_chains(&self) -> &BTreeMap<String, ListChain> {
+        &self.nft_config
     }
 
     pub fn is_enabled(&self) -> bool {
diff --git a/proxmox-firewall/src/firewall.rs b/proxmox-firewall/src/firewall.rs
index 509e295..41b1df2 100644
--- a/proxmox-firewall/src/firewall.rs
+++ b/proxmox-firewall/src/firewall.rs
@@ -41,7 +41,6 @@ static NF_CONNTRACK_TCP_TIMEOUT_SYN_RECV: &str =
     "/proc/sys/net/netfilter/nf_conntrack_tcp_timeout_syn_recv";
 static LOG_CONNTRACK_FILE: &str = "/var/lib/pve-firewall/log_nf_conntrack";
 
-#[derive(Default)]
 pub struct Firewall {
     config: FirewallConfig,
 }
@@ -53,10 +52,8 @@ impl From<FirewallConfig> for Firewall {
 }
 
 impl Firewall {
-    pub fn new() -> Self {
-        Self {
-            ..Default::default()
-        }
+    pub fn new(config: FirewallConfig) -> Self {
+        Self { config }
     }
 
     pub fn is_enabled(&self) -> bool {
diff --git a/proxmox-firewall/tests/integration_tests.rs b/proxmox-firewall/tests/integration_tests.rs
index 860c78d..e9baffe 100644
--- a/proxmox-firewall/tests/integration_tests.rs
+++ b/proxmox-firewall/tests/integration_tests.rs
@@ -1,3 +1,4 @@
+use anyhow::{Context, Error};
 use std::collections::HashMap;
 
 use proxmox_firewall::config::{FirewallConfig, FirewallConfigLoader, NftConfigLoader};
@@ -16,15 +17,15 @@ impl MockFirewallConfigLoader {
 }
 
 impl FirewallConfigLoader for MockFirewallConfigLoader {
-    fn cluster(&self) -> Option<Box<dyn std::io::BufRead>> {
-        Some(Box::new(include_str!("input/cluster.fw").as_bytes()))
+    fn cluster(&self) -> Result<Option<Box<dyn std::io::BufRead>>, Error> {
+        Ok(Some(Box::new(include_str!("input/cluster.fw").as_bytes())))
     }
 
-    fn host(&self) -> Option<Box<dyn std::io::BufRead>> {
-        Some(Box::new(include_str!("input/host.fw").as_bytes()))
+    fn host(&self) -> Result<Option<Box<dyn std::io::BufRead>>, Error> {
+        Ok(Some(Box::new(include_str!("input/host.fw").as_bytes())))
     }
 
-    fn guest_list(&self) -> GuestMap {
+    fn guest_list(&self) -> Result<GuestMap, Error> {
         let hostname = nodename().to_string();
 
         let mut map = HashMap::new();
@@ -35,31 +36,38 @@ impl FirewallConfigLoader for MockFirewallConfigLoader {
         let entry = GuestEntry::new(hostname, GuestType::Ct);
         map.insert(100.into(), entry);
 
-        GuestMap::from(map)
+        Ok(GuestMap::from(map))
     }
 
-    fn guest_config(&self, vmid: &Vmid, _guest: &GuestEntry) -> Option<Box<dyn std::io::BufRead>> {
+    fn guest_config(
+        &self,
+        vmid: &Vmid,
+        _guest: &GuestEntry,
+    ) -> Result<Option<Box<dyn std::io::BufRead>>, Error> {
         if *vmid == Vmid::new(101) {
-            return Some(Box::new(include_str!("input/101.conf").as_bytes()));
+            return Ok(Some(Box::new(include_str!("input/101.conf").as_bytes())));
         }
 
         if *vmid == Vmid::new(100) {
-            return Some(Box::new(include_str!("input/100.conf").as_bytes()));
+            return Ok(Some(Box::new(include_str!("input/100.conf").as_bytes())));
         }
 
-        None
+        Ok(None)
     }
 
-    fn guest_firewall_config(&self, vmid: &Vmid) -> Option<Box<dyn std::io::BufRead>> {
+    fn guest_firewall_config(
+        &self,
+        vmid: &Vmid,
+    ) -> Result<Option<Box<dyn std::io::BufRead>>, Error> {
         if *vmid == Vmid::new(101) {
-            return Some(Box::new(include_str!("input/101.fw").as_bytes()));
+            return Ok(Some(Box::new(include_str!("input/101.fw").as_bytes())));
         }
 
         if *vmid == Vmid::new(100) {
-            return Some(Box::new(include_str!("input/100.fw").as_bytes()));
+            return Ok(Some(Box::new(include_str!("input/100.fw").as_bytes())));
         }
 
-        None
+        Ok(None)
     }
 }
 
@@ -72,19 +80,22 @@ impl MockNftConfigLoader {
 }
 
 impl NftConfigLoader for MockNftConfigLoader {
-    fn chains(&self) -> CommandOutput {
-        serde_json::from_str(include_str!("input/chains.json")).expect("valid chains.json")
+    fn chains(&self) -> Result<Option<CommandOutput>, Error> {
+        serde_json::from_str::<CommandOutput>(include_str!("input/chains.json"))
+            .map(Some)
+            .with_context(|| "invalid chains.json".to_string())
     }
 }
 
 #[test]
 fn test_firewall() {
     let firewall_config = FirewallConfig::new(
-        Box::new(MockFirewallConfigLoader::new()),
-        Box::new(MockNftConfigLoader::new()),
-    );
+        &MockFirewallConfigLoader::new(),
+        &MockNftConfigLoader::new(),
+    )
+    .expect("valid mock configuration");
 
-    let firewall = Firewall::from(firewall_config);
+    let firewall = Firewall::new(firewall_config);
 
     insta::assert_json_snapshot!(firewall.full_host_fw().expect("firewall can be generated"));
 }
-- 
2.39.2




More information about the pve-devel mailing list