This adds new types for representing firewall address matches: - FirewallAddressMatch: enum for IP lists, ipset references, or alias references - FirewallAddressList: validated list of address entries with consistent address family - FirewallAddressEntry: enum for CIDR or IP range entries
The implementation includes: - Proper encapsulation with constructor and accessor methods - Address family validation in FirewallAddressList::new() - FromStr implementations for parsing address specifications - Integration with existing FirewallIpsetReference and FirewallAliasReference types Signed-off-by: Dietmar Maurer <[email protected]> --- proxmox-firewall-api-types/Cargo.toml | 1 + proxmox-firewall-api-types/src/address.rs | 225 ++++++++++++++++++++++ proxmox-firewall-api-types/src/lib.rs | 3 + 3 files changed, 229 insertions(+) create mode 100644 proxmox-firewall-api-types/src/address.rs diff --git a/proxmox-firewall-api-types/Cargo.toml b/proxmox-firewall-api-types/Cargo.toml index 97b477b8..8b77b522 100644 --- a/proxmox-firewall-api-types/Cargo.toml +++ b/proxmox-firewall-api-types/Cargo.toml @@ -22,3 +22,4 @@ serde_plain = { workspace = true } proxmox-schema = { workspace = true, features = ["api-macro"] } proxmox-serde = { workspace = true, features = ["perl"] } proxmox-fixed-string = { workspace = true, optional = true } +proxmox-network-types = { workspace = true, features = [ "api-types" ] } diff --git a/proxmox-firewall-api-types/src/address.rs b/proxmox-firewall-api-types/src/address.rs new file mode 100644 index 00000000..46166352 --- /dev/null +++ b/proxmox-firewall-api-types/src/address.rs @@ -0,0 +1,225 @@ +use std::fmt; +use std::str::FromStr; + +use super::{FirewallAliasReference, FirewallIpsetReference}; + +use anyhow::{bail, Error}; +use proxmox_network_types::ip_address::{Cidr, Family, IpRange}; +use proxmox_schema::{ApiStringFormat, ApiType, Schema, StringSchema}; + +/// A match for source or destination address. +#[derive(Clone, Debug, PartialEq)] +pub enum FirewallAddressMatch { + /// IP address list match. + Ip(FirewallAddressList), + /// IP set match. + Ipset(FirewallIpsetReference), + /// Alias match. + Alias(FirewallAliasReference), +} + +impl ApiType for FirewallAddressMatch { + const API_SCHEMA: Schema = StringSchema::new( + r#"Restrict source or destination packet address. + This can refer to a single IP address, + an IP set ('+ipsetname') or an IP alias definition. You can also specify + an address range like '20.34.101.207-201.3.9.99', or a list of IP + addresses and networks (entries are separated by comma). Please do not + mix IPv4 and IPv6 addresses inside such lists."#, + ) + .format(&ApiStringFormat::VerifyFn(verify_firewall_address_match)) + .max_length(512) + .schema(); +} + +fn verify_firewall_address_match(s: &str) -> Result<(), Error> { + FirewallAddressMatch::from_str(s).map(|_| ()) +} + +serde_plain::derive_deserialize_from_fromstr!(FirewallAddressMatch, "valid firewall address match"); +serde_plain::derive_serialize_from_display!(FirewallAddressMatch); + +/// A firewall address entry (CIDR or Range). +#[derive(Clone, Debug, PartialEq)] +pub enum FirewallAddressEntry { + /// CIDR notation. + Cidr(Cidr), + /// IP range. + Range(IpRange), +} + +impl FirewallAddressEntry { + /// Get the address family of the entry. + pub fn family(&self) -> Family { + match self { + Self::Cidr(cidr) => cidr.family(), + Self::Range(range) => range.family(), + } + } +} + +impl fmt::Display for FirewallAddressEntry { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Cidr(ip) => ip.fmt(f), + Self::Range(range) => range.fmt(f), + } + } +} + +impl FromStr for FirewallAddressEntry { + type Err = Error; + + fn from_str(s: &str) -> Result<Self, Error> { + if let Ok(cidr) = s.parse() { + return Ok(FirewallAddressEntry::Cidr(cidr)); + } + + if let Ok(range) = s.parse() { + return Ok(FirewallAddressEntry::Range(range)); + } + + bail!("Invalid IP entry: {s}"); + } +} + +/// A list of firewall address entries. +#[derive(Clone, Debug, PartialEq)] +pub struct FirewallAddressList { + // guaranteed to have the same family + entries: Vec<FirewallAddressEntry>, + family: Family, +} + +impl FirewallAddressList { + /// Creates a new address list from a vector of entries. + /// + /// Validates that all entries have the same address family. + pub fn new(entries: Vec<FirewallAddressEntry>) -> Result<Self, Error> { + if entries.is_empty() { + bail!("empty address list"); + } + + let family = entries[0].family(); + + for entry in &entries[1..] { + if entry.family() != family { + bail!("address list entries must have the same address family"); + } + } + + Ok(Self { entries, family }) + } + + /// Returns the entries of the address list. + pub fn entries(&self) -> &[FirewallAddressEntry] { + &self.entries + } + + /// Returns the address family of the address list. + pub fn family(&self) -> Family { + self.family + } +} + +impl fmt::Display for FirewallAddressMatch { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Ip(list) => { + for (i, entry) in list.entries.iter().enumerate() { + if i > 0 { + write!(f, ",")?; + } + entry.fmt(f)?; + } + Ok(()) + } + Self::Ipset(reference) => reference.fmt(f), + Self::Alias(reference) => reference.fmt(f), + } + } +} + +impl FromStr for FirewallAddressMatch { + type Err = Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let s = s.trim(); + + if s.is_empty() { + bail!("empty firewall address specification"); + } + + if s.starts_with('+') { + return Ok(FirewallAddressMatch::Ipset( + s.parse::<FirewallIpsetReference>()?, + )); + } + + if let Ok(alias_ref) = s.parse::<FirewallAliasReference>() { + return Ok(FirewallAddressMatch::Alias(alias_ref)); + } + + let mut entries = Vec::new(); + + for element in s.split(',') { + let entry: FirewallAddressEntry = element.parse()?; + entries.push(entry); + } + + Ok(Self::Ip(FirewallAddressList::new(entries)?)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_ip_addr_match() { + for input in [ + "10.0.0.0/8", + "10.0.0.0/8,192.168.0.0-192.168.255.255,172.16.0.1", + "dc/test", + "+guest/proxmox", + ] { + input + .parse::<FirewallAddressMatch>() + .expect("valid ip match"); + } + + for input in [ + "10.0.0.0/", + "10.0.0.0/8,192.168.256.0-192.168.255.255,172.16.0.1", + "dc/test!invalid", + "+guest/", + "", + ] { + input + .parse::<FirewallAddressMatch>() + .expect_err("invalid ip match"); + } + } + + #[test] + fn test_firewall_address_list_mixed_families() { + let entries = vec![ + "10.0.0.1/32".parse().unwrap(), + "fe80::1/128".parse().unwrap(), + ]; + + assert!(FirewallAddressList::new(entries).is_err()); + } + + #[test] + fn test_firewall_address_list_valid() { + let entries = vec![ + "10.0.0.1/32".parse().unwrap(), + "192.168.1.1/32".parse().unwrap(), + ]; + + let list = FirewallAddressList::new(entries).expect("valid list"); + assert_eq!(list.family(), Family::V4); + assert_eq!(list.entries().len(), 2); + } +} diff --git a/proxmox-firewall-api-types/src/lib.rs b/proxmox-firewall-api-types/src/lib.rs index 8fae5042..c6f00250 100644 --- a/proxmox-firewall-api-types/src/lib.rs +++ b/proxmox-firewall-api-types/src/lib.rs @@ -1,3 +1,6 @@ +mod address; +pub use address::{FirewallAddressEntry, FirewallAddressList, FirewallAddressMatch}; + mod alias; pub use alias::{FirewallAliasReference, FirewallAliasScope}; -- 2.47.3
