Add new port specification types for firewall rules: - FirewallPortListEntry: single numeric port, numeric port range or named service - FirewallPortList: comma-separated list of port entries
Includes comprehensive parsing with validation and unit tests. Signed-off-by: Dietmar Maurer <[email protected]> --- proxmox-firewall-api-types/Cargo.toml | 1 + proxmox-firewall-api-types/src/lib.rs | 5 + proxmox-firewall-api-types/src/port.rs | 173 +++++++++++++++++++++++++ 3 files changed, 179 insertions(+) create mode 100644 proxmox-firewall-api-types/src/port.rs diff --git a/proxmox-firewall-api-types/Cargo.toml b/proxmox-firewall-api-types/Cargo.toml index 3122d813..97b477b8 100644 --- a/proxmox-firewall-api-types/Cargo.toml +++ b/proxmox-firewall-api-types/Cargo.toml @@ -15,6 +15,7 @@ enum-fallback = ["dep:proxmox-fixed-string"] [dependencies] anyhow.workspace = true regex.workspace = true +const_format.workspace = true serde = { workspace = true, features = [ "derive" ] } serde_plain = { workspace = true } diff --git a/proxmox-firewall-api-types/src/lib.rs b/proxmox-firewall-api-types/src/lib.rs index 993115d8..b099be0c 100644 --- a/proxmox-firewall-api-types/src/lib.rs +++ b/proxmox-firewall-api-types/src/lib.rs @@ -20,3 +20,8 @@ pub use node_options::FirewallNodeOptions; mod firewall_ref; pub use firewall_ref::{FirewallRef, FirewallRefType}; + +mod port; +pub use port::{ + FirewallPortList, FirewallPortListEntry, FIREWALL_DPORT_API_SCHEMA, FIREWALL_SPORT_API_SCHEMA, +}; diff --git a/proxmox-firewall-api-types/src/port.rs b/proxmox-firewall-api-types/src/port.rs new file mode 100644 index 00000000..46989ba4 --- /dev/null +++ b/proxmox-firewall-api-types/src/port.rs @@ -0,0 +1,173 @@ +use std::fmt::Display; +use std::str::FromStr; + +use anyhow::{bail, Error}; +use const_format::concatcp; +use proxmox_schema::{ApiStringFormat, Schema, StringSchema, UpdaterType}; + +#[derive(Clone, Debug, PartialEq)] +/// Single entry in a TCP/UDP port list. +/// +/// Can be a named service, a numeric port or a port range. +pub enum FirewallPortListEntry { + Named(String), + Numeric(u16), + Range(u16, u16), +} + +impl FromStr for FirewallPortListEntry { + type Err = Error; + fn from_str(s: &str) -> Result<Self, Self::Err> { + Ok(match s.trim().split_once(':') { + None => { + if s.is_empty() { + bail!("empty port specification"); + } + if s.find(|c: char| !(c.is_digit(10))).is_some() { + // Note: arbitrary length limit, longer than anything in /etc/services + if s.len() < 256 { + if s.contains(|c: char| !(c.is_ascii_alphanumeric() || c == '-')) { + bail!("invalid characters in port name"); + } + FirewallPortListEntry::Named(s.to_string()) + } else { + bail!("port name too long"); + } + } else { + let port = s.parse::<u16>()?; + FirewallPortListEntry::Numeric(port) + } + } + Some((first, second)) => { + let first = first.parse::<u16>()?; + let second = second.parse::<u16>()?; + if first > second { + bail!("invalid port range: start port greater than end port") + } + FirewallPortListEntry::Range(first, second) + } + }) + } +} + +impl Display for FirewallPortListEntry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + FirewallPortListEntry::Named(name) => write!(f, "{}", name), + FirewallPortListEntry::Numeric(number) => write!(f, "{}", number), + FirewallPortListEntry::Range(first, second) => write!(f, "{}:{}", first, second), + } + } +} + +#[derive(Clone, Debug, PartialEq)] +/// TCP/UDP port list. +pub struct FirewallPortList(pub Vec<FirewallPortListEntry>); + +const PORT_FORMAT_DESCRIPTION: &'static str = r#"You can use service names or simple numbers (0-65535), +as defined in '/etc/services'. Port ranges can be specified with '\d+:\d+', +for example '80:85', and you can use comma separated list to match several ports or ranges."#; + +/// API schema for firewall source port list. +pub const FIREWALL_SPORT_API_SCHEMA: Schema = StringSchema::new(concatcp!( + "Restrict TCP/UDP source port. ", + PORT_FORMAT_DESCRIPTION +)) +.format(&ApiStringFormat::VerifyFn(verify_firewall_port_list)) +.schema(); + +/// API schema for firewall destination port list. +pub const FIREWALL_DPORT_API_SCHEMA: Schema = StringSchema::new(concatcp!( + "Restrict TCP/UDP destination port. ", + PORT_FORMAT_DESCRIPTION +)) +.format(&ApiStringFormat::VerifyFn(verify_firewall_port_list)) +.schema(); + +serde_plain::derive_deserialize_from_fromstr!(FirewallPortList, "valid port list"); +serde_plain::derive_serialize_from_display!(FirewallPortList); + +impl FromStr for FirewallPortList { + type Err = Error; + fn from_str(s: &str) -> Result<Self, Self::Err> { + let mut res = Vec::new(); + for part in s.split(',') { + let entry = FirewallPortListEntry::from_str(part.trim())?; + res.push(entry); + } + Ok(FirewallPortList(res)) + } +} + +impl Display for FirewallPortList { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for (i, entry) in self.0.iter().enumerate() { + if i > 0 { + write!(f, ",")?; + } + write!(f, "{}", entry)?; + } + Ok(()) + } +} + +fn verify_firewall_port_list(s: &str) -> Result<(), Error> { + FirewallPortList::from_str(s)?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_port_entry() { + let mut port_entry: FirewallPortListEntry = "12345".parse().expect("valid port entry"); + assert_eq!(port_entry, FirewallPortListEntry::Numeric(12345)); + + port_entry = "0:65535".parse().expect("valid port entry"); + assert_eq!(port_entry, FirewallPortListEntry::Range(0, 65535)); + + "ssh:80".parse::<FirewallPortListEntry>().unwrap_err(); + "65536".parse::<FirewallPortListEntry>().unwrap_err(); + "100:80".parse::<FirewallPortListEntry>().unwrap_err(); + "100:100000".parse::<FirewallPortListEntry>().unwrap_err(); + "any-name".parse::<FirewallPortListEntry>().unwrap(); + "TOS-network-unreachable" + .parse::<FirewallPortListEntry>() + .unwrap(); + "no_underscores" + .parse::<FirewallPortListEntry>() + .unwrap_err(); + "imap2".parse::<FirewallPortListEntry>().unwrap(); + "".parse::<FirewallPortListEntry>().unwrap_err(); + } + + #[test] + fn test_parse_port_list() { + let mut port_list = FirewallPortList::from_str("12345").expect("valid port list"); + assert_eq!( + port_list, + FirewallPortList(vec![FirewallPortListEntry::Numeric(12345)]) + ); + + port_list = + FirewallPortList::from_str("12345,0:65535,1337,https").expect("valid port list"); + + assert_eq!( + port_list, + FirewallPortList(vec![ + FirewallPortListEntry::from_str("12345").unwrap(), + FirewallPortListEntry::from_str("0:65535").unwrap(), + FirewallPortListEntry::from_str("1337").unwrap(), + FirewallPortListEntry::from_str("https").unwrap(), + ]) + ); + + FirewallPortList::from_str("0::1337").unwrap_err(); + FirewallPortList::from_str("0:1337,").unwrap_err(); + FirewallPortList::from_str("70000").unwrap_err(); + FirewallPortList::from_str("ssh:80").unwrap_err(); + FirewallPortList::from_str("").unwrap_err(); + } +} -- 2.47.3
