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 3432B90BA1 for ; Tue, 2 Apr 2024 19:17:15 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 84B42A807 for ; Tue, 2 Apr 2024 19:16:40 +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:35 +0200 (CEST) Received: by lana.proxmox.com (Postfix, from userid 10043) id 27B422C3492; Tue, 2 Apr 2024 19:16:31 +0200 (CEST) From: Stefan Hanreich To: pve-devel@lists.proxmox.com Cc: Stefan Hanreich , Wolfgang Bumiller Date: Tue, 2 Apr 2024 19:16:05 +0200 Message-Id: <20240402171629.536804-14-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.328 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 Subject: [pve-devel] [PATCH proxmox-firewall 13/37] config: firewall: add host specific config + option types 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:15 -0000 Co-authored-by: Wolfgang Bumiller Signed-off-by: Stefan Hanreich --- proxmox-ve-config/src/firewall/host.rs | 309 +++++++++++++++++++++++++ proxmox-ve-config/src/firewall/mod.rs | 1 + 2 files changed, 310 insertions(+) create mode 100644 proxmox-ve-config/src/firewall/host.rs diff --git a/proxmox-ve-config/src/firewall/host.rs b/proxmox-ve-config/src/firewall/host.rs new file mode 100644 index 0000000..3e47bfa --- /dev/null +++ b/proxmox-ve-config/src/firewall/host.rs @@ -0,0 +1,309 @@ +use std::io; +use std::net::IpAddr; + +use anyhow::{bail, Error}; +use serde::Deserialize; + +use crate::host::utils::{host_ips, hostname, network_interface_cidrs}; + +use crate::firewall::parse; +use crate::firewall::types::log::LogLevel; +use crate::firewall::types::rule::Direction; +use crate::firewall::types::{Alias, Cidr, Rule}; + +#[derive(Debug, Default, Deserialize)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub struct Options { + #[serde(default, with = "parse::serde_option_bool")] + enable: Option, + + #[serde(default, with = "parse::serde_option_bool")] + nftables: Option, + + log_level_in: Option, + log_level_out: Option, + + #[serde(default, with = "parse::serde_option_bool")] + log_nf_conntrack: Option, + #[serde(default, with = "parse::serde_option_bool")] + ndp: Option, + + #[serde(default, with = "parse::serde_option_bool")] + nf_conntrack_allow_invalid: Option, + + #[serde(default, with = "parse::serde_option_conntrack_helpers")] + nf_conntrack_helpers: Option>, + + #[serde(default, with = "parse::serde_option_number")] + nf_conntrack_max: Option, + #[serde(default, with = "parse::serde_option_number")] + nf_conntrack_tcp_timeout_established: Option, + #[serde(default, with = "parse::serde_option_number")] + nf_conntrack_tcp_timeout_syn_recv: Option, + + #[serde(default, with = "parse::serde_option_bool")] + nosmurfs: Option, + + #[serde(default, with = "parse::serde_option_bool")] + protection_synflood: Option, + #[serde(default, with = "parse::serde_option_number")] + protection_synflood_burst: Option, + #[serde(default, with = "parse::serde_option_number")] + protection_synflood_rate: Option, + + smurf_log_level: Option, + tcp_flags_log_level: Option, + + #[serde(default, with = "parse::serde_option_bool")] + tcpflags: Option, +} + +#[derive(Debug, Default)] +pub struct Config { + pub(crate) config: super::common::Config, +} + +impl Config { + pub fn new() -> Self { + Self { + config: Default::default(), + } + } + + pub fn parse(input: R) -> Result { + let config = super::common::Config::parse(input, &Default::default())?; + + if !config.groups.is_empty() { + bail!("host firewall config cannot declare groups"); + } + + if !config.aliases.is_empty() { + bail!("host firewall config cannot declare aliases"); + } + + if !config.ipsets.is_empty() { + bail!("host firewall config cannot declare ipsets"); + } + + Ok(Self { config }) + } + + pub fn rules(&self) -> &[Rule] { + &self.config.rules + } + + pub fn management_ips() -> Result, Error> { + let mut management_cidrs = Vec::new(); + + for host_ip in host_ips() { + for network_interface_cidr in network_interface_cidrs() { + match (host_ip, network_interface_cidr) { + (IpAddr::V4(ip), Cidr::Ipv4(cidr)) => { + if cidr.contains_address(ip) { + management_cidrs.push(network_interface_cidr.clone()); + } + } + (IpAddr::V6(ip), Cidr::Ipv6(cidr)) => { + if cidr.contains_address(ip) { + management_cidrs.push(network_interface_cidr.clone()); + } + } + _ => continue, + }; + } + } + + Ok(management_cidrs) + } + + pub fn hostname() -> &'static str { + hostname() + } + + pub fn get_alias(&self, name: &str) -> Option<&Alias> { + self.config.alias(name) + } + + pub fn is_enabled(&self) -> bool { + self.config.options.enable.unwrap_or(true) + } + + pub fn nftables(&self) -> bool { + self.config.options.nftables.unwrap_or(false) + } + + pub fn allow_ndp(&self) -> bool { + self.config.options.ndp.unwrap_or(true) + } + + pub fn block_smurfs(&self) -> bool { + self.config.options.nosmurfs.unwrap_or(true) + } + + pub fn block_smurfs_log_level(&self) -> LogLevel { + self.config.options.smurf_log_level.unwrap_or_default() + } + + pub fn block_synflood(&self) -> bool { + self.config.options.protection_synflood.unwrap_or(false) + } + + pub fn synflood_rate(&self) -> i64 { + self.config.options.protection_synflood_rate.unwrap_or(200) + } + + pub fn synflood_burst(&self) -> i64 { + self.config + .options + .protection_synflood_burst + .unwrap_or(1000) + } + + pub fn block_invalid_tcp(&self) -> bool { + self.config.options.tcpflags.unwrap_or(false) + } + + pub fn block_invalid_tcp_log_level(&self) -> LogLevel { + self.config.options.tcp_flags_log_level.unwrap_or_default() + } + + pub fn block_invalid_conntrack(&self) -> bool { + !self + .config + .options + .nf_conntrack_allow_invalid + .unwrap_or(false) + } + + pub fn nf_conntrack_max(&self) -> Option { + self.config.options.nf_conntrack_max + } + + pub fn nf_conntrack_tcp_timeout_established(&self) -> Option { + self.config.options.nf_conntrack_tcp_timeout_established + } + + pub fn nf_conntrack_tcp_timeout_syn_recv(&self) -> Option { + self.config.options.nf_conntrack_tcp_timeout_syn_recv + } + + pub fn log_nf_conntrack(&self) -> bool { + self.config.options.log_nf_conntrack.unwrap_or(false) + } + + pub fn conntrack_helpers(&self) -> Option<&Vec> { + self.config.options.nf_conntrack_helpers.as_ref() + } + + pub fn log_level(&self, dir: Direction) -> LogLevel { + match dir { + Direction::In => self.config.options.log_level_in.unwrap_or_default(), + Direction::Out => self.config.options.log_level_out.unwrap_or_default(), + } + } +} + +#[cfg(test)] +mod tests { + use crate::firewall::types::{ + log::LogLevel, + rule::{Kind, RuleGroup, Verdict}, + rule_match::{Ports, Protocol, RuleMatch, Udp}, + }; + + use super::*; + + #[test] + fn test_parse_config() { + const CONFIG: &str = r#" +[OPTIONS] +enable: 1 +nftables: 1 +log_level_in: debug +log_level_out: emerg +log_nf_conntrack: 0 +ndp: 1 +nf_conntrack_allow_invalid: yes +nf_conntrack_helpers: ftp +nf_conntrack_max: 44000 +nf_conntrack_tcp_timeout_established: 500000 +nf_conntrack_tcp_timeout_syn_recv: 44 +nosmurfs: no +protection_synflood: 1 +protection_synflood_burst: 2500 +protection_synflood_rate: 300 +smurf_log_level: notice +tcp_flags_log_level: nolog +tcpflags: yes + +[RULES] + +GROUP tgr -i eth0 # acomm +IN ACCEPT -p udp -dport 33 -sport 22 -log warning + +"#; + + let mut config = CONFIG.as_bytes(); + let config = Config::parse(&mut config).unwrap(); + + assert_eq!( + config.config.options, + Options { + enable: Some(true), + nftables: Some(true), + log_level_in: Some(LogLevel::Debug), + log_level_out: Some(LogLevel::Emergency), + log_nf_conntrack: Some(false), + ndp: Some(true), + nf_conntrack_allow_invalid: Some(true), + nf_conntrack_helpers: Some(vec!["ftp".to_string()]), + nf_conntrack_max: Some(44000), + nf_conntrack_tcp_timeout_established: Some(500000), + nf_conntrack_tcp_timeout_syn_recv: Some(44), + nosmurfs: Some(false), + protection_synflood: Some(true), + protection_synflood_burst: Some(2500), + protection_synflood_rate: Some(300), + smurf_log_level: Some(LogLevel::Notice), + tcp_flags_log_level: Some(LogLevel::Nolog), + tcpflags: Some(true), + } + ); + + assert_eq!(config.config.rules.len(), 2); + + assert_eq!( + config.config.rules[0], + Rule { + disabled: false, + comment: Some("acomm".to_string()), + kind: Kind::Group(RuleGroup { + group: "tgr".to_string(), + iface: Some("eth0".to_string()), + }), + }, + ); + + assert_eq!( + config.config.rules[1], + Rule { + disabled: false, + comment: None, + kind: Kind::Match(RuleMatch { + dir: Direction::In, + verdict: Verdict::Accept, + proto: Some(Protocol::Udp(Udp::new(Ports::from_u16(22, 33)))), + log: Some(LogLevel::Warning), + ..Default::default() + }), + }, + ); + + Config::parse("[ALIASES]\ntest 127.0.0.1".as_bytes()) + .expect_err("host config cannot contain aliases"); + + Config::parse("[GROUP test]".as_bytes()).expect_err("host config cannot contain groups"); + + Config::parse("[IPSET test]".as_bytes()).expect_err("host config cannot contain ipsets"); + } +} diff --git a/proxmox-ve-config/src/firewall/mod.rs b/proxmox-ve-config/src/firewall/mod.rs index 82689c3..85fe6c4 100644 --- a/proxmox-ve-config/src/firewall/mod.rs +++ b/proxmox-ve-config/src/firewall/mod.rs @@ -1,5 +1,6 @@ pub mod cluster; pub mod common; +pub mod host; pub mod ports; pub mod types; -- 2.39.2