From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 787EC1FF142 for ; Mon, 16 Feb 2026 11:45:02 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 77E1ACFF6; Mon, 16 Feb 2026 11:44:59 +0100 (CET) From: Dietmar Maurer To: pve-devel@lists.proxmox.com Subject: [RFC proxmox 04/22] firewall-api-types: add logging types Date: Mon, 16 Feb 2026 11:43:42 +0100 Message-ID: <20260216104401.3959270-5-dietmar@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260216104401.3959270-1-dietmar@proxmox.com> References: <20260216104401.3959270-1-dietmar@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.575 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_LAZY_DOMAIN_SECURITY 1 Sending domain does not have any anti-forgery methods RDNS_NONE 0.793 Delivered to internal network by a host with no rDNS SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_NONE 0.001 SPF: sender does not publish an SPF Record Message-ID-Hash: J7BW3HUZSWOYHTZP5E7A6FHFPM6OFNXT X-Message-ID-Hash: J7BW3HUZSWOYHTZP5E7A6FHFPM6OFNXT X-MailFrom: dietmar@zilli.proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: This adds several firewall logging-related types: - FirewallLogLevel: enum for syslog-style log levels (emerg to nolog) - FirewallLogRateLimit: configuration for rate-limiting log messages - FirewallPacketRate: packet rate representation (e.g., '100/second') - FirewallPacketRateTimescale: time units for rate limiting Includes comprehensive tests for parsing, display, and roundtrip serialization. Signed-off-by: Dietmar Maurer --- proxmox-firewall-api-types/src/lib.rs | 5 + proxmox-firewall-api-types/src/log.rs | 312 ++++++++++++++++++++++++++ 2 files changed, 317 insertions(+) create mode 100644 proxmox-firewall-api-types/src/log.rs diff --git a/proxmox-firewall-api-types/src/lib.rs b/proxmox-firewall-api-types/src/lib.rs index b8004c76..d9ff4548 100644 --- a/proxmox-firewall-api-types/src/lib.rs +++ b/proxmox-firewall-api-types/src/lib.rs @@ -1,2 +1,7 @@ +mod log; +pub use log::{ + FirewallLogLevel, FirewallLogRateLimit, FirewallPacketRate, FirewallPacketRateTimescale, +}; + mod policy; pub use policy::{FirewallFWPolicy, FirewallIOPolicy}; diff --git a/proxmox-firewall-api-types/src/log.rs b/proxmox-firewall-api-types/src/log.rs new file mode 100644 index 00000000..fb2df49e --- /dev/null +++ b/proxmox-firewall-api-types/src/log.rs @@ -0,0 +1,312 @@ +use std::fmt; +use std::str::FromStr; + +use anyhow::{bail, Error}; +use serde::{Deserialize, Serialize}; + +use proxmox_schema::api; + +#[cfg(feature = "enum-fallback")] +use proxmox_fixed_string::FixedString; + +/// Firewall log rate limit time scales. +#[derive(Copy, Clone, Default, PartialEq)] +#[cfg_attr(test, derive(Debug))] +pub enum FirewallPacketRateTimescale { + /// second + #[default] + Second, + /// minute + Minute, + /// hour + Hour, + /// day + Day, + #[cfg(feature = "enum-fallback")] + /// Unknown variants for forward compatibility. + UnknownEnumValue(FixedString), +} + +impl FromStr for FirewallPacketRateTimescale { + type Err = Error; + + fn from_str(str: &str) -> Result { + match str { + "second" => Ok(FirewallPacketRateTimescale::Second), + "minute" => Ok(FirewallPacketRateTimescale::Minute), + "hour" => Ok(FirewallPacketRateTimescale::Hour), + "day" => Ok(FirewallPacketRateTimescale::Day), + "" => bail!("empty time scale specification"), + #[cfg(not(feature = "enum-fallback"))] + _ => bail!("Invalid time scale provided"), + #[cfg(feature = "enum-fallback")] + other => Ok(FirewallPacketRateTimescale::UnknownEnumValue( + FixedString::from_str(other)?, + )), + } + } +} + +impl fmt::Display for FirewallPacketRateTimescale { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + FirewallPacketRateTimescale::Second => write!(f, "second"), + FirewallPacketRateTimescale::Minute => write!(f, "minute"), + FirewallPacketRateTimescale::Hour => write!(f, "hour"), + FirewallPacketRateTimescale::Day => write!(f, "day"), + #[cfg(feature = "enum-fallback")] + &FirewallPacketRateTimescale::UnknownEnumValue(scale) => scale.fmt(f), + } + } +} + +/// Packet rate for log rate limiting. +#[derive(Copy, Clone, PartialEq)] +#[cfg_attr(test, derive(Debug))] +pub struct FirewallPacketRate { + /// Number of packets + pub packets: u64, + /// Time scale for the rate + pub timescale: FirewallPacketRateTimescale, +} + +serde_plain::derive_deserialize_from_fromstr!(FirewallPacketRate, "valid packet rate"); +serde_plain::derive_serialize_from_display!(FirewallPacketRate); + +impl fmt::Display for FirewallPacketRate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}/{}", self.packets, self.timescale) + } +} + +impl FromStr for FirewallPacketRate { + type Err = Error; + + fn from_str(str: &str) -> Result { + match str.split_once('/') { + None => Ok(FirewallPacketRate { + packets: u64::from_str(str)?, + timescale: FirewallPacketRateTimescale::default(), + }), + Some((rate, unit)) => Ok(FirewallPacketRate { + packets: u64::from_str(rate)?, + timescale: FirewallPacketRateTimescale::from_str(unit)?, + }), + } + } +} + +#[api( + default_key: "enable", + properties: { + burst: { + default: 5, + minimum: 0, + optional: true, + type: Integer, + }, + enable: { + default: true, + }, + rate: { + default: "1/second", + optional: true, + type: String, + }, + }, +)] +/// Firewall log rate limit configuration. +#[derive(Deserialize, Serialize, Clone, PartialEq)] +#[cfg_attr(test, derive(Debug))] +pub struct FirewallLogRateLimit { + /// Initial burst of packages which will always get logged before the rate + /// is applied + #[serde(deserialize_with = "proxmox_serde::perl::deserialize_u64")] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub burst: Option, + + /// Enable or disable log rate limiting + #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")] + pub enable: bool, + + /// Frequency with which the burst bucket gets refilled + #[serde(default, skip_serializing_if = "Option::is_none")] + pub rate: Option, +} + +#[api] +/// Firewall log levels. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub enum FirewallLogLevel { + #[serde(rename = "emerg")] + /// emerg. + Emergency, + #[serde(rename = "alert")] + /// alert. + Alert, + #[serde(rename = "crit")] + /// crit. + Critical, + #[serde(rename = "err")] + /// err. + Error, + #[serde(rename = "warning")] + /// warning. + Warning, + #[serde(rename = "notice")] + /// notice. + Notice, + #[serde(rename = "info")] + /// info. + Info, + #[serde(rename = "debug")] + /// debug. + Debug, + #[serde(rename = "nolog")] + #[default] + /// nolog. + Nolog, +} + +serde_plain::derive_display_from_serialize!(FirewallLogLevel); +serde_plain::derive_fromstr_from_deserialize!(FirewallLogLevel); + +#[cfg(test)] +mod tests { + use proxmox_schema::property_string::PropertyString; + + use super::*; + + #[test] + fn test_parse_rate_limit() { + let mut parsed_rate_limit: PropertyString = + serde_plain::from_str("1,burst=123,rate=44").expect("valid rate limit"); + + assert_eq!( + parsed_rate_limit.into_inner(), + FirewallLogRateLimit { + enable: true, + burst: Some(123), + rate: Some(FirewallPacketRate { + packets: 44, + timescale: FirewallPacketRateTimescale::Second, + }), + } + ); + + parsed_rate_limit = serde_plain::from_str("1").expect("valid rate limit"); + + assert_eq!( + parsed_rate_limit.into_inner(), + FirewallLogRateLimit { + enable: true, + burst: None, + rate: None + } + ); + + parsed_rate_limit = + serde_plain::from_str("enable=0,rate=123/hour").expect("valid rate limit"); + + assert_eq!( + parsed_rate_limit.into_inner(), + FirewallLogRateLimit { + enable: false, + burst: None, + rate: Some(FirewallPacketRate { + packets: 123, + timescale: FirewallPacketRateTimescale::Hour, + }), + } + ); + + serde_plain::from_str::>("2") + .expect_err("invalid value for enable"); + + serde_plain::from_str::>("enabled=0,rate=123") + .expect_err("invalid key in log ratelimit"); + + #[cfg(not(feature = "enum-fallback"))] + serde_plain::from_str::>("enable=0,rate=123/proxmox,") + .expect_err("invalid unit for rate"); + } + + #[test] + fn test_packet_rate_parse() { + // Test parsing with all timescales + let rate: FirewallPacketRate = "100/second".parse().expect("valid rate"); + assert_eq!(rate.packets, 100); + assert_eq!(rate.timescale, FirewallPacketRateTimescale::Second); + + let rate: FirewallPacketRate = "50/minute".parse().expect("valid rate"); + assert_eq!(rate.packets, 50); + assert_eq!(rate.timescale, FirewallPacketRateTimescale::Minute); + + let rate: FirewallPacketRate = "10/hour".parse().expect("valid rate"); + assert_eq!(rate.packets, 10); + assert_eq!(rate.timescale, FirewallPacketRateTimescale::Hour); + + let rate: FirewallPacketRate = "1/day".parse().expect("valid rate"); + assert_eq!(rate.packets, 1); + assert_eq!(rate.timescale, FirewallPacketRateTimescale::Day); + + // Test default timescale when no unit specified + let rate: FirewallPacketRate = "42".parse().expect("valid rate without unit"); + assert_eq!(rate.packets, 42); + assert_eq!(rate.timescale, FirewallPacketRateTimescale::Second); + } + + #[test] + fn test_packet_rate_display() { + let rate = FirewallPacketRate { + packets: 100, + timescale: FirewallPacketRateTimescale::Second, + }; + assert_eq!(rate.to_string(), "100/second"); + + let rate = FirewallPacketRate { + packets: 5, + timescale: FirewallPacketRateTimescale::Hour, + }; + assert_eq!(rate.to_string(), "5/hour"); + } + + #[test] + fn test_packet_rate_roundtrip() { + let original = FirewallPacketRate { + packets: 123, + timescale: FirewallPacketRateTimescale::Minute, + }; + let serialized = original.to_string(); + let parsed: FirewallPacketRate = serialized.parse().expect("roundtrip parse"); + assert_eq!(original, parsed); + } + + #[test] + fn test_packet_rate_parse_errors() { + // Empty timescale + "100/" + .parse::() + .expect_err("empty timescale"); + + // Invalid timescale + #[cfg(not(feature = "enum-fallback"))] + "100/invalid" + .parse::() + .expect_err("invalid timescale"); + #[cfg(feature = "enum-fallback")] + "100/invalid" + .parse::() + .expect("valid timescale (enum fallback feature)"); + + // Invalid packet count + "abc/second" + .parse::() + .expect_err("invalid packet count"); + + // Negative number + "-5/second" + .parse::() + .expect_err("negative packet count"); + } +} -- 2.47.3