From: Stefan Hanreich <s.hanreich@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH proxmox-firewall] firewall: improve error handling of firewall
Date: Thu, 25 Apr 2024 19:23:07 +0200 [thread overview]
Message-ID: <20240425172307.654151-1-s.hanreich@proxmox.com> (raw)
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@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
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
next reply other threads:[~2024-04-25 17:23 UTC|newest]
Thread overview: 2+ messages / expand[flat|nested] mbox.gz Atom feed top
2024-04-25 17:23 Stefan Hanreich [this message]
2024-04-25 17:32 ` [pve-devel] applied: " Thomas Lamprecht
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20240425172307.654151-1-s.hanreich@proxmox.com \
--to=s.hanreich@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox