From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id E709690958 for ; Tue, 2 Apr 2024 19:17:02 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id C8A5CA550 for ; Tue, 2 Apr 2024 19:16:32 +0200 (CEST) Received: from lana.proxmox.com (unknown [94.136.29.99]) by firstgate.proxmox.com (Proxmox) with ESMTP for ; Tue, 2 Apr 2024 19:16:30 +0200 (CEST) Received: by lana.proxmox.com (Postfix, from userid 10043) id BA0272C3268; Tue, 2 Apr 2024 19:16:30 +0200 (CEST) From: Stefan Hanreich To: pve-devel@lists.proxmox.com Cc: Stefan Hanreich , Wolfgang Bumiller Date: Tue, 2 Apr 2024 19:15:55 +0200 Message-Id: <20240402171629.536804-4-s.hanreich@proxmox.com> X-Mailer: git-send-email 2.39.2 In-Reply-To: <20240402171629.536804-1-s.hanreich@proxmox.com> References: <20240402171629.536804-1-s.hanreich@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.340 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. [mod.rs, ports.rs, port.rs] Subject: [pve-devel] [PATCH proxmox-firewall 03/37] 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: , X-List-Received-Date: Tue, 02 Apr 2024 17:17:02 -0000 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`. 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 | 78 ++++++++ proxmox-ve-config/src/firewall/types/mod.rs | 1 + proxmox-ve-config/src/firewall/types/port.rs | 181 +++++++++++++++++++ 4 files changed, 261 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..96527f1 --- /dev/null +++ b/proxmox-ve-config/src/firewall/ports.rs @@ -0,0 +1,78 @@ +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; + + 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