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 BA6341FF348 for ; Wed, 17 Apr 2024 15:54:41 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 388B2869A; Wed, 17 Apr 2024 15:54:42 +0200 (CEST) From: Stefan Hanreich To: pve-devel@lists.proxmox.com Date: Wed, 17 Apr 2024 15:53:28 +0200 Message-Id: <20240417135404.573490-4-s.hanreich@proxmox.com> X-Mailer: git-send-email 2.39.2 In-Reply-To: <20240417135404.573490-1-s.hanreich@proxmox.com> References: <20240417135404.573490-1-s.hanreich@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.332 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 URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [ports.rs, mod.rs, port.rs] Subject: [pve-devel] [PATCH proxmox-firewall v2 03/39] config: firewall: add types for ports X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox VE development discussion Cc: Wolfgang Bumiller Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pve-devel-bounces@lists.proxmox.com Sender: "pve-devel" Adds types for all kinds of port-related values in the firewall config as well as FromStr implementations for parsing them from the config. Also adds a helper for parsing the named ports from `/etc/services`. Reviewed-by: Lukas Wagner Reviewed-by: Max Carrara Co-authored-by: Wolfgang Bumiller Signed-off-by: Stefan Hanreich --- proxmox-ve-config/src/firewall/mod.rs | 1 + proxmox-ve-config/src/firewall/ports.rs | 80 ++++++++ proxmox-ve-config/src/firewall/types/mod.rs | 1 + proxmox-ve-config/src/firewall/types/port.rs | 181 +++++++++++++++++++ 4 files changed, 263 insertions(+) create mode 100644 proxmox-ve-config/src/firewall/ports.rs create mode 100644 proxmox-ve-config/src/firewall/types/port.rs diff --git a/proxmox-ve-config/src/firewall/mod.rs b/proxmox-ve-config/src/firewall/mod.rs index cd40856..a9f65bf 100644 --- a/proxmox-ve-config/src/firewall/mod.rs +++ b/proxmox-ve-config/src/firewall/mod.rs @@ -1 +1,2 @@ +pub mod ports; pub mod types; diff --git a/proxmox-ve-config/src/firewall/ports.rs b/proxmox-ve-config/src/firewall/ports.rs new file mode 100644 index 0000000..9d5d1be --- /dev/null +++ b/proxmox-ve-config/src/firewall/ports.rs @@ -0,0 +1,80 @@ +use anyhow::{format_err, Error}; +use std::sync::OnceLock; + +#[derive(Default)] +struct NamedPorts { + ports: std::collections::HashMap, +} + +impl NamedPorts { + fn new() -> Self { + use std::io::BufRead; + + log::trace!("loading /etc/services"); + + let mut this = Self::default(); + + let file = match std::fs::File::open("/etc/services") { + Ok(file) => file, + Err(_) => return this, + }; + + for line in std::io::BufReader::new(file).lines() { + let line = match line { + Ok(line) => line, + Err(_) => break, + }; + + let line = line.trim_start(); + + if line.is_empty() || line.starts_with('#') { + continue; + } + + let mut parts = line.split_ascii_whitespace(); + + let name = match parts.next() { + None => continue, + Some(name) => name.to_string(), + }; + + let proto: u16 = match parts.next() { + None => continue, + Some(proto) => match proto.split('/').next() { + None => continue, + Some(num) => match num.parse() { + Ok(num) => num, + Err(_) => continue, + }, + }, + }; + + this.ports.insert(name, proto); + for alias in parts { + if alias.starts_with('#') { + break; + } + this.ports.insert(alias.to_string(), proto); + } + } + + this + } + + fn find(&self, name: &str) -> Option { + self.ports.get(name).copied() + } +} + +fn named_ports() -> &'static NamedPorts { + static NAMED_PORTS: OnceLock = OnceLock::new(); + + NAMED_PORTS.get_or_init(NamedPorts::new) +} + +/// Parse a named port with the help of `/etc/services`. +pub fn parse_named_port(name: &str) -> Result { + named_ports() + .find(name) + .ok_or_else(|| format_err!("unknown port name {name:?}")) +} diff --git a/proxmox-ve-config/src/firewall/types/mod.rs b/proxmox-ve-config/src/firewall/types/mod.rs index de534b4..b740e5d 100644 --- a/proxmox-ve-config/src/firewall/types/mod.rs +++ b/proxmox-ve-config/src/firewall/types/mod.rs @@ -1,3 +1,4 @@ pub mod address; +pub mod port; pub use address::Cidr; diff --git a/proxmox-ve-config/src/firewall/types/port.rs b/proxmox-ve-config/src/firewall/types/port.rs new file mode 100644 index 0000000..c1252d9 --- /dev/null +++ b/proxmox-ve-config/src/firewall/types/port.rs @@ -0,0 +1,181 @@ +use std::fmt; +use std::ops::Deref; + +use anyhow::{bail, Error}; +use serde_with::DeserializeFromStr; + +use crate::firewall::ports::parse_named_port; + +#[derive(Clone, Debug)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub enum PortEntry { + Port(u16), + Range(u16, u16), +} + +impl fmt::Display for PortEntry { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Port(p) => write!(f, "{p}"), + Self::Range(beg, end) => write!(f, "{beg}-{end}"), + } + } +} + +fn parse_port(port: &str) -> Result { + if let Ok(port) = port.parse::() { + return Ok(port); + } + + if let Ok(port) = parse_named_port(port) { + return Ok(port); + } + + bail!("invalid port specification: {port}") +} + +impl std::str::FromStr for PortEntry { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(match s.trim().split_once(':') { + None => PortEntry::from(parse_port(s)?), + Some((first, second)) => { + PortEntry::try_from((parse_port(first)?, parse_port(second)?))? + } + }) + } +} + +impl From for PortEntry { + fn from(port: u16) -> Self { + PortEntry::Port(port) + } +} + +impl TryFrom<(u16, u16)> for PortEntry { + type Error = Error; + + fn try_from(ports: (u16, u16)) -> Result { + if ports.0 > ports.1 { + bail!("start port is greater than end port!"); + } + + Ok(PortEntry::Range(ports.0, ports.1)) + } +} + +#[derive(Clone, Debug, DeserializeFromStr)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub struct PortList(pub(crate) Vec); + +impl FromIterator for PortList { + fn from_iter>(iter: T) -> Self { + Self(iter.into_iter().collect()) + } +} + +impl> From for PortList { + fn from(value: T) -> Self { + Self(vec![value.into()]) + } +} + +impl Deref for PortList { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::str::FromStr for PortList { + type Err = Error; + + fn from_str(s: &str) -> Result { + if s.is_empty() { + bail!("empty port specification"); + } + + let mut entries = Vec::new(); + + for entry in s.trim().split(',') { + entries.push(entry.parse()?); + } + + if entries.is_empty() { + bail!("invalid empty port list"); + } + + Ok(Self(entries)) + } +} + +impl fmt::Display for PortList { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use fmt::Write; + if self.0.len() > 1 { + f.write_char('{')?; + } + + let mut comma = '\0'; + for entry in &self.0 { + if std::mem::replace(&mut comma, ',') != '\0' { + f.write_char(comma)?; + } + fmt::Display::fmt(entry, f)?; + } + + if self.0.len() > 1 { + f.write_char('}')?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_port_entry() { + let mut port_entry: PortEntry = "12345".parse().expect("valid port entry"); + assert_eq!(port_entry, PortEntry::from(12345)); + + port_entry = "0:65535".parse().expect("valid port entry"); + assert_eq!(port_entry, PortEntry::try_from((0, 65535)).unwrap()); + + "65536".parse::().unwrap_err(); + "100:100000".parse::().unwrap_err(); + "qweasd".parse::().unwrap_err(); + "".parse::().unwrap_err(); + } + + #[test] + fn test_parse_port_list() { + let mut port_list: PortList = "12345".parse().expect("valid port list"); + assert_eq!(port_list, PortList::from(12345)); + + port_list = "12345,0:65535,1337,ssh:80,https" + .parse() + .expect("valid port list"); + + assert_eq!( + port_list, + PortList(vec![ + PortEntry::from(12345), + PortEntry::try_from((0, 65535)).unwrap(), + PortEntry::from(1337), + PortEntry::try_from((22, 80)).unwrap(), + PortEntry::from(443), + ]) + ); + + "0::1337".parse::().unwrap_err(); + "0:1337,".parse::().unwrap_err(); + "70000".parse::().unwrap_err(); + "qweasd".parse::().unwrap_err(); + "".parse::().unwrap_err(); + } +} -- 2.39.2 _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel