add proxmox-ve-rs crate - move proxmox-ve-config there

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Reviewed-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
 [ TL: add dependency to d/control and update its version ]
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
This commit is contained in:
Stefan Hanreich 2024-11-15 13:09:29 +01:00 committed by Thomas Lamprecht
parent bea3e651b4
commit aa76920360
32 changed files with 7 additions and 6656 deletions

View File

@ -1,7 +1,9 @@
[workspace] [workspace]
members = [ members = [
"proxmox-ve-config",
"proxmox-nftables", "proxmox-nftables",
"proxmox-firewall", "proxmox-firewall",
] ]
resolver = "2" resolver = "2"
[workspace.dependencies]
proxmox-ve-config = { version = "0.2.0" }

View File

@ -29,7 +29,7 @@ cargo-build:
build: $(BUILDDIR) build: $(BUILDDIR)
$(BUILDDIR): $(BUILDDIR):
rm -rf $@ $@.tmp; mkdir $@.tmp rm -rf $@ $@.tmp; mkdir $@.tmp
cp -a proxmox-firewall proxmox-nftables proxmox-ve-config debian Cargo.toml Makefile defines.mk $@.tmp/ cp -a proxmox-firewall proxmox-nftables debian Cargo.toml Makefile defines.mk $@.tmp/
mv $@.tmp $@ mv $@.tmp $@
.PHONY: deb .PHONY: deb

1
debian/control vendored
View File

@ -13,6 +13,7 @@ Build-Depends: cargo:native,
librust-proxmox-schema-3+default-dev (>= 3.1.2-~~), librust-proxmox-schema-3+default-dev (>= 3.1.2-~~),
librust-proxmox-sortable-macro-dev, librust-proxmox-sortable-macro-dev,
librust-proxmox-sys-dev (>= 0.6~), librust-proxmox-sys-dev (>= 0.6~),
librust-proxmox-ve-config-dev (>= 0.2~),
librust-serde-1+default-dev, librust-serde-1+default-dev,
librust-serde-1+derive-dev, librust-serde-1+derive-dev,
librust-serde-json-1+default-dev, librust-serde-json-1+default-dev,

View File

@ -21,7 +21,7 @@ serde_json = "1"
signal-hook = "0.3" signal-hook = "0.3"
proxmox-nftables = { path = "../proxmox-nftables", features = ["config-ext"] } proxmox-nftables = { path = "../proxmox-nftables", features = ["config-ext"] }
proxmox-ve-config = { path = "../proxmox-ve-config" } proxmox-ve-config = { workspace = true }
[dev-dependencies] [dev-dependencies]
insta = { version = "1.21", features = ["json"] } insta = { version = "1.21", features = ["json"] }

View File

@ -22,4 +22,4 @@ serde = { version = "1", features = [ "derive" ] }
serde_json = "1" serde_json = "1"
serde_plain = "1" serde_plain = "1"
proxmox-ve-config = { path = "../proxmox-ve-config", optional = true } proxmox-ve-config = { workspace = true, optional = true }

View File

@ -1,25 +0,0 @@
[package]
name = "proxmox-ve-config"
version = "0.1.0"
edition = "2021"
authors = [
"Wolfgang Bumiller <w.bumiller@proxmox.com>",
"Stefan Hanreich <s.hanreich@proxmox.com>",
"Proxmox Support Team <support@proxmox.com>",
]
description = "Proxmox VE config parsing"
license = "AGPL-3"
[dependencies]
log = "0.4"
anyhow = "1"
nix = "0.26"
serde = { version = "1", features = [ "derive" ] }
serde_json = "1"
serde_plain = "1"
serde_with = "3"
proxmox-schema = "3.1.2"
proxmox-sys = "0.6"
proxmox-sortable-macro = "0.1.3"

View File

@ -1,52 +0,0 @@
[
{
"name": "amanda",
"v4": true,
"v6": true,
"udp": 10080
},
{
"name": "ftp",
"v4": true,
"v6": true,
"tcp": 21
} ,
{
"name": "irc",
"v4": true,
"tcp": 6667
},
{
"name": "netbios-ns",
"v4": true,
"udp": 137
},
{
"name": "pptp",
"v4": true,
"tcp": 1723
},
{
"name": "sane",
"v4": true,
"v6": true,
"tcp": 6566
},
{
"name": "sip",
"v4": true,
"v6": true,
"udp": 5060
},
{
"name": "snmp",
"v4": true,
"udp": 161
},
{
"name": "tftp",
"v4": true,
"v6": true,
"udp": 69
}
]

View File

@ -1,923 +0,0 @@
{
"Amanda": {
"code": [
{
"dport": "10080",
"proto": "udp"
},
{
"dport": "10080",
"proto": "tcp"
}
],
"desc": "Amanda Backup"
},
"Auth": {
"code": [
{
"dport": "113",
"proto": "tcp"
}
],
"desc": "Auth (identd) traffic"
},
"BGP": {
"code": [
{
"dport": "179",
"proto": "tcp"
}
],
"desc": "Border Gateway Protocol traffic"
},
"BitTorrent": {
"code": [
{
"dport": "6881:6889",
"proto": "tcp"
},
{
"dport": "6881",
"proto": "udp"
}
],
"desc": "BitTorrent traffic for BitTorrent 3.1 and earlier"
},
"BitTorrent32": {
"code": [
{
"dport": "6881:6999",
"proto": "tcp"
},
{
"dport": "6881",
"proto": "udp"
}
],
"desc": "BitTorrent traffic for BitTorrent 3.2 and later"
},
"CVS": {
"code": [
{
"dport": "2401",
"proto": "tcp"
}
],
"desc": "Concurrent Versions System pserver traffic"
},
"Ceph": {
"code": [
{
"dport": "6789",
"proto": "tcp"
},
{
"dport": "3300",
"proto": "tcp"
},
{
"dport": "6800:7300",
"proto": "tcp"
}
],
"desc": "Ceph Storage Cluster traffic (Ceph Monitors, OSD & MDS Daemons)"
},
"Citrix": {
"code": [
{
"dport": "1494",
"proto": "tcp"
},
{
"dport": "1604",
"proto": "udp"
},
{
"dport": "2598",
"proto": "tcp"
}
],
"desc": "Citrix/ICA traffic (ICA, ICA Browser, CGP)"
},
"DAAP": {
"code": [
{
"dport": "3689",
"proto": "tcp"
},
{
"dport": "3689",
"proto": "udp"
}
],
"desc": "Digital Audio Access Protocol traffic (iTunes, Rythmbox daemons)"
},
"DCC": {
"code": [
{
"dport": "6277",
"proto": "tcp"
}
],
"desc": "Distributed Checksum Clearinghouse spam filtering mechanism"
},
"DHCPfwd": {
"code": [
{
"dport": "67:68",
"proto": "udp",
"sport": "67:68"
}
],
"desc": "Forwarded DHCP traffic"
},
"DHCPv6": {
"code": [
{
"dport": "546:547",
"proto": "udp",
"sport": "546:547"
}
],
"desc": "DHCPv6 traffic"
},
"DNS": {
"code": [
{
"dport": "53",
"proto": "udp"
},
{
"dport": "53",
"proto": "tcp"
}
],
"desc": "Domain Name System traffic (upd and tcp)"
},
"Distcc": {
"code": [
{
"dport": "3632",
"proto": "tcp"
}
],
"desc": "Distributed Compiler service"
},
"FTP": {
"code": [
{
"dport": "21",
"proto": "tcp"
}
],
"desc": "File Transfer Protocol"
},
"Finger": {
"code": [
{
"dport": "79",
"proto": "tcp"
}
],
"desc": "Finger protocol (RFC 742)"
},
"GNUnet": {
"code": [
{
"dport": "2086",
"proto": "tcp"
},
{
"dport": "2086",
"proto": "udp"
},
{
"dport": "1080",
"proto": "tcp"
},
{
"dport": "1080",
"proto": "udp"
}
],
"desc": "GNUnet secure peer-to-peer networking traffic"
},
"GRE": {
"code": [
{
"proto": "47"
}
],
"desc": "Generic Routing Encapsulation tunneling protocol"
},
"Git": {
"code": [
{
"dport": "9418",
"proto": "tcp"
}
],
"desc": "Git distributed revision control traffic"
},
"HKP": {
"code": [
{
"dport": "11371",
"proto": "tcp"
}
],
"desc": "OpenPGP HTTP key server protocol traffic"
},
"HTTP": {
"code": [
{
"dport": "80",
"proto": "tcp"
}
],
"desc": "Hypertext Transfer Protocol (WWW)"
},
"HTTPS": {
"code": [
{
"dport": "443",
"proto": "tcp"
}
],
"desc": "Hypertext Transfer Protocol (WWW) over SSL"
},
"HTTP/3": {
"code": [
{
"dport": "443",
"proto": "udp"
}
],
"desc": "Hypertext Transfer Protocol v3"
},
"ICPV2": {
"code": [
{
"dport": "3130",
"proto": "udp"
}
],
"desc": "Internet Cache Protocol V2 (Squid) traffic"
},
"ICQ": {
"code": [
{
"dport": "5190",
"proto": "tcp"
}
],
"desc": "AOL Instant Messenger traffic"
},
"IMAP": {
"code": [
{
"dport": "143",
"proto": "tcp"
}
],
"desc": "Internet Message Access Protocol"
},
"IMAPS": {
"code": [
{
"dport": "993",
"proto": "tcp"
}
],
"desc": "Internet Message Access Protocol over SSL"
},
"IPIP": {
"code": [
{
"proto": "94"
}
],
"desc": "IPIP capsulation traffic"
},
"IPsec": {
"code": [
{
"dport": "500",
"proto": "udp",
"sport": "500"
},
{
"proto": "50"
}
],
"desc": "IPsec traffic"
},
"IPsecah": {
"code": [
{
"dport": "500",
"proto": "udp",
"sport": "500"
},
{
"proto": "51"
}
],
"desc": "IPsec authentication (AH) traffic"
},
"IPsecnat": {
"code": [
{
"dport": "500",
"proto": "udp"
},
{
"dport": "4500",
"proto": "udp"
},
{
"proto": "50"
}
],
"desc": "IPsec traffic and Nat-Traversal"
},
"IRC": {
"code": [
{
"dport": "6667",
"proto": "tcp"
}
],
"desc": "Internet Relay Chat traffic"
},
"Jetdirect": {
"code": [
{
"dport": "9100",
"proto": "tcp"
}
],
"desc": "HP Jetdirect printing"
},
"L2TP": {
"code": [
{
"dport": "1701",
"proto": "udp"
}
],
"desc": "Layer 2 Tunneling Protocol traffic"
},
"LDAP": {
"code": [
{
"dport": "389",
"proto": "tcp"
}
],
"desc": "Lightweight Directory Access Protocol traffic"
},
"LDAPS": {
"code": [
{
"dport": "636",
"proto": "tcp"
}
],
"desc": "Secure Lightweight Directory Access Protocol traffic"
},
"MDNS": {
"code": [
{
"dport": "5353",
"proto": "udp"
}
],
"desc": "Multicast DNS"
},
"MSNP": {
"code": [
{
"dport": "1863",
"proto": "tcp"
}
],
"desc": "Microsoft Notification Protocol"
},
"MSSQL": {
"code": [
{
"dport": "1433",
"proto": "tcp"
}
],
"desc": "Microsoft SQL Server"
},
"Mail": {
"code": [
{
"dport": "25",
"proto": "tcp"
},
{
"dport": "465",
"proto": "tcp"
},
{
"dport": "587",
"proto": "tcp"
}
],
"desc": "Mail traffic (SMTP, SMTPS, Submission)"
},
"Munin": {
"code": [
{
"dport": "4949",
"proto": "tcp"
}
],
"desc": "Munin networked resource monitoring traffic"
},
"MySQL": {
"code": [
{
"dport": "3306",
"proto": "tcp"
}
],
"desc": "MySQL server"
},
"NNTP": {
"code": [
{
"dport": "119",
"proto": "tcp"
}
],
"desc": "NNTP traffic (Usenet)."
},
"NNTPS": {
"code": [
{
"dport": "563",
"proto": "tcp"
}
],
"desc": "Encrypted NNTP traffic (Usenet)"
},
"NTP": {
"code": [
{
"dport": "123",
"proto": "udp"
}
],
"desc": "Network Time Protocol (ntpd)"
},
"NeighborDiscovery": {
"code": [
{
"dport": "nd-router-solicit",
"proto": "icmpv6"
},
{
"dport": "nd-router-advert",
"proto": "icmpv6"
},
{
"dport": "nd-neighbor-solicit",
"proto": "icmpv6"
},
{
"dport": "nd-neighbor-advert",
"proto": "icmpv6"
}
],
"desc": "IPv6 neighbor solicitation, neighbor and router advertisement"
},
"OSPF": {
"code": [
{
"proto": "89"
}
],
"desc": "OSPF multicast traffic"
},
"OpenVPN": {
"code": [
{
"dport": "1194",
"proto": "udp"
}
],
"desc": "OpenVPN traffic"
},
"PBS": {
"code": [
{
"dport": "8007",
"proto": "tcp"
}
],
"desc": "Proxmox Backup Server"
},
"PCA": {
"code": [
{
"dport": "5632",
"proto": "udp"
},
{
"dport": "5631",
"proto": "tcp"
}
],
"desc": "Symantec PCAnywere (tm)"
},
"PMG": {
"code": [
{
"dport": "8006",
"proto": "tcp"
}
],
"desc": "Proxmox Mail Gateway web interface"
},
"POP3": {
"code": [
{
"dport": "110",
"proto": "tcp"
}
],
"desc": "POP3 traffic"
},
"POP3S": {
"code": [
{
"dport": "995",
"proto": "tcp"
}
],
"desc": "Encrypted POP3 traffic"
},
"PPtP": {
"code": [
{
"proto": "47"
},
{
"dport": "1723",
"proto": "tcp"
}
],
"desc": "Point-to-Point Tunneling Protocol"
},
"Ping": {
"code": [
{
"dport": "echo-request",
"proto": "icmp"
}
],
"desc": "ICMP echo request"
},
"PostgreSQL": {
"code": [
{
"dport": "5432",
"proto": "tcp"
}
],
"desc": "PostgreSQL server"
},
"Printer": {
"code": [
{
"dport": "515",
"proto": "tcp"
}
],
"desc": "Line Printer protocol printing"
},
"RDP": {
"code": [
{
"dport": "3389",
"proto": "tcp"
}
],
"desc": "Microsoft Remote Desktop Protocol traffic"
},
"RIP": {
"code": [
{
"dport": "520",
"proto": "udp"
}
],
"desc": "Routing Information Protocol (bidirectional)"
},
"RNDC": {
"code": [
{
"dport": "953",
"proto": "tcp"
}
],
"desc": "BIND remote management protocol"
},
"Razor": {
"code": [
{
"dport": "2703",
"proto": "tcp"
}
],
"desc": "Razor Antispam System"
},
"Rdate": {
"code": [
{
"dport": "37",
"proto": "tcp"
}
],
"desc": "Remote time retrieval (rdate)"
},
"Rsync": {
"code": [
{
"dport": "873",
"proto": "tcp"
}
],
"desc": "Rsync server"
},
"SANE": {
"code": [
{
"dport": "6566",
"proto": "tcp"
}
],
"desc": "SANE network scanning"
},
"SMB": {
"code": [
{
"dport": "135,445",
"proto": "udp"
},
{
"dport": "137:139",
"proto": "udp"
},
{
"dport": "1024:65535",
"proto": "udp",
"sport": "137"
},
{
"dport": "135,139,445",
"proto": "tcp"
}
],
"desc": "Microsoft SMB traffic"
},
"SMBswat": {
"code": [
{
"dport": "901",
"proto": "tcp"
}
],
"desc": "Samba Web Administration Tool"
},
"SMTP": {
"code": [
{
"dport": "25",
"proto": "tcp"
}
],
"desc": "Simple Mail Transfer Protocol"
},
"SMTPS": {
"code": [
{
"dport": "465",
"proto": "tcp"
}
],
"desc": "Encrypted Simple Mail Transfer Protocol"
},
"SNMP": {
"code": [
{
"dport": "161:162",
"proto": "udp"
},
{
"dport": "161",
"proto": "tcp"
}
],
"desc": "Simple Network Management Protocol"
},
"SPAMD": {
"code": [
{
"dport": "783",
"proto": "tcp"
}
],
"desc": "Spam Assassin SPAMD traffic"
},
"SPICEproxy": {
"code": [
{
"dport": "3128",
"proto": "tcp"
}
],
"desc": "Proxmox VE SPICE display proxy traffic"
},
"SSH": {
"code": [
{
"dport": "22",
"proto": "tcp"
}
],
"desc": "Secure shell traffic"
},
"SVN": {
"code": [
{
"dport": "3690",
"proto": "tcp"
}
],
"desc": "Subversion server (svnserve)"
},
"SixXS": {
"code": [
{
"dport": "3874",
"proto": "tcp"
},
{
"dport": "3740",
"proto": "udp"
},
{
"proto": "41"
},
{
"dport": "5072,8374",
"proto": "udp"
}
],
"desc": "SixXS IPv6 Deployment and Tunnel Broker"
},
"Squid": {
"code": [
{
"dport": "3128",
"proto": "tcp"
}
],
"desc": "Squid web proxy traffic"
},
"Submission": {
"code": [
{
"dport": "587",
"proto": "tcp"
}
],
"desc": "Mail message submission traffic"
},
"Syslog": {
"code": [
{
"dport": "514",
"proto": "udp"
},
{
"dport": "514",
"proto": "tcp"
}
],
"desc": "Syslog protocol (RFC 5424) traffic"
},
"TFTP": {
"code": [
{
"dport": "69",
"proto": "udp"
}
],
"desc": "Trivial File Transfer Protocol traffic"
},
"Telnet": {
"code": [
{
"dport": "23",
"proto": "tcp"
}
],
"desc": "Telnet traffic"
},
"Telnets": {
"code": [
{
"dport": "992",
"proto": "tcp"
}
],
"desc": "Telnet over SSL"
},
"Time": {
"code": [
{
"dport": "37",
"proto": "tcp"
}
],
"desc": "RFC 868 Time protocol"
},
"Trcrt": {
"code": [
{
"dport": "33434:33524",
"proto": "udp"
},
{
"dport": "echo-request",
"proto": "icmp"
}
],
"desc": "Traceroute (for up to 30 hops) traffic"
},
"VNC": {
"code": [
{
"dport": "5900:5999",
"proto": "tcp"
}
],
"desc": "VNC traffic for VNC display's 0 - 99"
},
"VNCL": {
"code": [
{
"dport": "5500",
"proto": "tcp"
}
],
"desc": "VNC traffic from Vncservers to Vncviewers in listen mode"
},
"Web": {
"code": [
{
"dport": "80",
"proto": "tcp"
},
{
"dport": "443",
"proto": "tcp"
}
],
"desc": "WWW traffic (HTTP and HTTPS)"
},
"Webcache": {
"code": [
{
"dport": "8080",
"proto": "tcp"
}
],
"desc": "Web Cache/Proxy traffic (port 8080)"
},
"Webmin": {
"code": [
{
"dport": "10000",
"proto": "tcp"
}
],
"desc": "Webmin traffic"
},
"Whois": {
"code": [
{
"dport": "43",
"proto": "tcp"
}
],
"desc": "Whois (nicname, RFC 3912) traffic"
}
}

View File

@ -1,374 +0,0 @@
use std::collections::BTreeMap;
use std::io;
use anyhow::Error;
use serde::Deserialize;
use crate::firewall::common::ParserConfig;
use crate::firewall::types::ipset::{Ipset, IpsetScope};
use crate::firewall::types::log::LogRateLimit;
use crate::firewall::types::rule::{Direction, Verdict};
use crate::firewall::types::{Alias, Group, Rule};
use crate::firewall::parse::{serde_option_bool, serde_option_log_ratelimit};
#[derive(Debug, Default)]
pub struct Config {
pub(crate) config: super::common::Config<Options>,
}
/// default setting for [`Config::is_enabled()`]
pub const CLUSTER_ENABLED_DEFAULT: bool = false;
/// default setting for [`Config::ebtables()`]
pub const CLUSTER_EBTABLES_DEFAULT: bool = false;
/// default setting for [`Config::default_policy()`]
pub const CLUSTER_POLICY_IN_DEFAULT: Verdict = Verdict::Drop;
/// default setting for [`Config::default_policy()`]
pub const CLUSTER_POLICY_OUT_DEFAULT: Verdict = Verdict::Accept;
impl Config {
pub fn parse<R: io::BufRead>(input: R) -> Result<Self, Error> {
let parser_config = ParserConfig {
guest_iface_names: false,
ipset_scope: Some(IpsetScope::Datacenter),
};
Ok(Self {
config: super::common::Config::parse(input, &parser_config)?,
})
}
pub fn rules(&self) -> &Vec<Rule> {
&self.config.rules
}
pub fn groups(&self) -> &BTreeMap<String, Group> {
&self.config.groups
}
pub fn ipsets(&self) -> &BTreeMap<String, Ipset> {
&self.config.ipsets
}
pub fn alias(&self, name: &str) -> Option<&Alias> {
self.config.alias(name)
}
pub fn is_enabled(&self) -> bool {
self.config
.options
.enable
.unwrap_or(CLUSTER_ENABLED_DEFAULT)
}
/// returns the ebtables option from the cluster config or [`CLUSTER_EBTABLES_DEFAULT`] if
/// unset
///
/// this setting is leftover from the old firewall, but has no effect on the nftables firewall
pub fn ebtables(&self) -> bool {
self.config
.options
.ebtables
.unwrap_or(CLUSTER_EBTABLES_DEFAULT)
}
/// returns policy_in / out or [`CLUSTER_POLICY_IN_DEFAULT`] / [`CLUSTER_POLICY_OUT_DEFAULT`] if
/// unset
pub fn default_policy(&self, dir: Direction) -> Verdict {
match dir {
Direction::In => self
.config
.options
.policy_in
.unwrap_or(CLUSTER_POLICY_IN_DEFAULT),
Direction::Out => self
.config
.options
.policy_out
.unwrap_or(CLUSTER_POLICY_OUT_DEFAULT),
}
}
/// returns the rate_limit for logs or [`None`] if rate limiting is disabled
///
/// If there is no rate limit set, then [`LogRateLimit::default`] is used
pub fn log_ratelimit(&self) -> Option<LogRateLimit> {
let rate_limit = self
.config
.options
.log_ratelimit
.clone()
.unwrap_or_default();
match rate_limit.enabled() {
true => Some(rate_limit),
false => None,
}
}
}
#[derive(Debug, Default, Deserialize)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Options {
#[serde(default, with = "serde_option_bool")]
enable: Option<bool>,
#[serde(default, with = "serde_option_bool")]
ebtables: Option<bool>,
#[serde(default, with = "serde_option_log_ratelimit")]
log_ratelimit: Option<LogRateLimit>,
policy_in: Option<Verdict>,
policy_out: Option<Verdict>,
}
#[cfg(test)]
mod tests {
use crate::firewall::types::{
address::IpList,
alias::{AliasName, AliasScope},
ipset::{IpsetAddress, IpsetEntry},
log::{LogLevel, LogRateLimitTimescale},
rule::{Kind, RuleGroup},
rule_match::{
Icmpv6, Icmpv6Code, IpAddrMatch, IpMatch, Ports, Protocol, RuleMatch, Tcp, Udp,
},
Cidr,
};
use super::*;
#[test]
fn test_parse_config() {
const CONFIG: &str = r#"
[OPTIONS]
enable: 1
log_ratelimit: 1,rate=10/second,burst=20
ebtables: 0
policy_in: REJECT
policy_out: REJECT
[ALIASES]
another 8.8.8.18
analias 7.7.0.0/16 # much
wide cccc::/64
[IPSET a-set]
!5.5.5.5
1.2.3.4/30
dc/analias # a comment
dc/wide
dddd::/96
[RULES]
GROUP tgr -i eth0 # acomm
IN ACCEPT -p udp -dport 33 -sport 22 -log warning
[group tgr] # comment for tgr
|OUT ACCEPT -source fe80::1/48 -dest dddd:3:3::9/64 -p icmpv6 -log nolog -icmp-type port-unreachable
OUT ACCEPT -p tcp -sport 33 -log nolog
IN BGP(REJECT) -log crit -source 1.2.3.4
"#;
let mut config = CONFIG.as_bytes();
let config = Config::parse(&mut config).unwrap();
assert_eq!(
config.config.options,
Options {
ebtables: Some(false),
enable: Some(true),
log_ratelimit: Some(LogRateLimit::new(
true,
10,
LogRateLimitTimescale::Second,
20
)),
policy_in: Some(Verdict::Reject),
policy_out: Some(Verdict::Reject),
}
);
assert_eq!(config.config.aliases.len(), 3);
assert_eq!(
config.config.aliases["another"],
Alias::new("another", Cidr::new_v4([8, 8, 8, 18], 32).unwrap(), None),
);
assert_eq!(
config.config.aliases["analias"],
Alias::new(
"analias",
Cidr::new_v4([7, 7, 0, 0], 16).unwrap(),
"much".to_string()
),
);
assert_eq!(
config.config.aliases["wide"],
Alias::new(
"wide",
Cidr::new_v6(
[0xCCCC, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x000],
64
)
.unwrap(),
None
),
);
assert_eq!(config.config.ipsets.len(), 1);
let mut ipset_elements = vec![
IpsetEntry {
nomatch: true,
address: Cidr::new_v4([5, 5, 5, 5], 32).unwrap().into(),
comment: None,
},
IpsetEntry {
nomatch: false,
address: Cidr::new_v4([1, 2, 3, 4], 30).unwrap().into(),
comment: None,
},
IpsetEntry {
nomatch: false,
address: IpsetAddress::Alias(AliasName::new(AliasScope::Datacenter, "analias")),
comment: Some("a comment".to_string()),
},
IpsetEntry {
nomatch: false,
address: IpsetAddress::Alias(AliasName::new(AliasScope::Datacenter, "wide")),
comment: None,
},
IpsetEntry {
nomatch: false,
address: Cidr::new_v6([0xdd, 0xdd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 96)
.unwrap()
.into(),
comment: None,
},
];
let mut ipset = Ipset::from_parts(IpsetScope::Datacenter, "a-set");
ipset.append(&mut ipset_elements);
assert_eq!(config.config.ipsets["a-set"], ipset,);
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()
}),
},
);
assert_eq!(config.config.groups.len(), 1);
let entry = &config.config.groups["tgr"];
assert_eq!(entry.comment(), Some("comment for tgr"));
assert_eq!(entry.rules().len(), 3);
assert_eq!(
entry.rules()[0],
Rule {
disabled: true,
comment: None,
kind: Kind::Match(RuleMatch {
dir: Direction::Out,
verdict: Verdict::Accept,
ip: Some(IpMatch {
src: Some(IpAddrMatch::Ip(IpList::from(
Cidr::new_v6(
[0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
48
)
.unwrap()
))),
dst: Some(IpAddrMatch::Ip(IpList::from(
Cidr::new_v6(
[0xdd, 0xdd, 0, 3, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9],
64
)
.unwrap()
))),
}),
proto: Some(Protocol::Icmpv6(Icmpv6::new_code(Icmpv6Code::Named(
"port-unreachable"
)))),
log: Some(LogLevel::Nolog),
..Default::default()
}),
},
);
assert_eq!(
entry.rules()[1],
Rule {
disabled: false,
comment: None,
kind: Kind::Match(RuleMatch {
dir: Direction::Out,
verdict: Verdict::Accept,
proto: Some(Protocol::Tcp(Tcp::new(Ports::from_u16(33, None)))),
log: Some(LogLevel::Nolog),
..Default::default()
}),
},
);
assert_eq!(
entry.rules()[2],
Rule {
disabled: false,
comment: None,
kind: Kind::Match(RuleMatch {
dir: Direction::In,
verdict: Verdict::Reject,
log: Some(LogLevel::Critical),
fw_macro: Some("BGP".to_string()),
ip: Some(IpMatch {
src: Some(IpAddrMatch::Ip(IpList::from(
Cidr::new_v4([1, 2, 3, 4], 32).unwrap()
))),
dst: None,
}),
..Default::default()
}),
},
);
let empty_config = Config::parse("".as_bytes()).expect("empty config is invalid");
assert_eq!(empty_config.config.options, Options::default());
assert!(empty_config.config.rules.is_empty());
assert!(empty_config.config.aliases.is_empty());
assert!(empty_config.config.ipsets.is_empty());
assert!(empty_config.config.groups.is_empty());
}
}

View File

@ -1,184 +0,0 @@
use std::collections::{BTreeMap, HashMap};
use std::io;
use anyhow::{bail, format_err, Error};
use serde::de::IntoDeserializer;
use crate::firewall::parse::{parse_named_section_tail, split_key_value, SomeString};
use crate::firewall::types::ipset::{IpsetName, IpsetScope};
use crate::firewall::types::{Alias, Group, Ipset, Rule};
#[derive(Debug, Default)]
pub struct Config<O>
where
O: Default + std::fmt::Debug + serde::de::DeserializeOwned,
{
pub(crate) options: O,
pub(crate) rules: Vec<Rule>,
pub(crate) aliases: BTreeMap<String, Alias>,
pub(crate) ipsets: BTreeMap<String, Ipset>,
pub(crate) groups: BTreeMap<String, Group>,
}
enum Sec {
None,
Options,
Aliases,
Rules,
Ipset(String, Ipset),
Group(String, Group),
}
#[derive(Default)]
pub struct ParserConfig {
/// Network interfaces must be of the form `netX`.
pub guest_iface_names: bool,
pub ipset_scope: Option<IpsetScope>,
}
impl<O> Config<O>
where
O: Default + std::fmt::Debug + serde::de::DeserializeOwned,
{
pub fn new() -> Self {
Self::default()
}
pub fn parse<R: io::BufRead>(input: R, parser_cfg: &ParserConfig) -> Result<Self, Error> {
let mut section = Sec::None;
let mut this = Self::new();
let mut options = HashMap::new();
for line in input.lines() {
let line = line?;
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
log::trace!("parsing config line {line}");
if line.eq_ignore_ascii_case("[OPTIONS]") {
this.set_section(&mut section, Sec::Options)?;
} else if line.eq_ignore_ascii_case("[ALIASES]") {
this.set_section(&mut section, Sec::Aliases)?;
} else if line.eq_ignore_ascii_case("[RULES]") {
this.set_section(&mut section, Sec::Rules)?;
} else if let Some(line) = line.strip_prefix("[IPSET") {
let (name, comment) = parse_named_section_tail("ipset", line)?;
let scope = parser_cfg.ipset_scope.ok_or_else(|| {
format_err!("IPSET in config, but no scope set in parser config")
})?;
let ipset_name = IpsetName::new(scope, name.to_string());
let mut ipset = Ipset::new(ipset_name);
ipset.comment = comment.map(str::to_owned);
this.set_section(&mut section, Sec::Ipset(name.to_string(), ipset))?;
} else if let Some(line) = line.strip_prefix("[group") {
let (name, comment) = parse_named_section_tail("group", line)?;
let mut group = Group::new();
group.set_comment(comment.map(str::to_owned));
this.set_section(&mut section, Sec::Group(name.to_owned(), group))?;
} else if line.starts_with('[') {
bail!("invalid section {line:?}");
} else {
match &mut section {
Sec::None => bail!("config line with no section: {line:?}"),
Sec::Options => Self::parse_option(line, &mut options)?,
Sec::Aliases => this.parse_alias(line)?,
Sec::Rules => this.parse_rule(line, parser_cfg)?,
Sec::Ipset(_name, ipset) => ipset.parse_entry(line)?,
Sec::Group(_name, group) => group.parse_entry(line)?,
}
}
}
this.set_section(&mut section, Sec::None)?;
this.options = O::deserialize(IntoDeserializer::<
'_,
crate::firewall::parse::SerdeStringError,
>::into_deserializer(options))?;
Ok(this)
}
fn parse_option(line: &str, options: &mut HashMap<String, SomeString>) -> Result<(), Error> {
let (key, value) = split_key_value(line)
.ok_or_else(|| format_err!("expected colon separated key and value, found {line:?}"))?;
if options.insert(key.to_string(), value.into()).is_some() {
bail!("duplicate option {key:?}");
}
Ok(())
}
fn parse_alias(&mut self, line: &str) -> Result<(), Error> {
let alias: Alias = line.parse()?;
if self
.aliases
.insert(alias.name().to_string(), alias)
.is_some()
{
bail!("duplicate alias: {line}");
}
Ok(())
}
fn parse_rule(&mut self, line: &str, parser_cfg: &ParserConfig) -> Result<(), Error> {
let rule: Rule = line.parse()?;
if parser_cfg.guest_iface_names {
if let Some(iface) = rule.iface() {
let _ = iface
.strip_prefix("net")
.ok_or_else(|| {
format_err!("interface name must be of the form \"net<number>\"")
})?
.parse::<u16>()
.map_err(|_| {
format_err!("interface name must be of the form \"net<number>\"")
})?;
}
}
self.rules.push(rule);
Ok(())
}
fn set_section(&mut self, sec: &mut Sec, to: Sec) -> Result<(), Error> {
let prev = std::mem::replace(sec, to);
match prev {
Sec::Ipset(name, ipset) => {
if self.ipsets.insert(name.clone(), ipset).is_some() {
bail!("duplicate ipset: {name:?}");
}
}
Sec::Group(name, group) => {
if self.groups.insert(name.clone(), group).is_some() {
bail!("duplicate group: {name:?}");
}
}
_ => (),
}
Ok(())
}
pub fn ipsets(&self) -> &BTreeMap<String, Ipset> {
&self.ipsets
}
pub fn alias(&self, name: &str) -> Option<&Alias> {
self.aliases.get(name)
}
}

View File

@ -1,115 +0,0 @@
use anyhow::{bail, Error};
use serde::Deserialize;
use std::collections::HashMap;
use std::sync::OnceLock;
use crate::firewall::types::address::Family;
use crate::firewall::types::rule_match::{Ports, Protocol, Tcp, Udp};
#[derive(Clone, Debug, Deserialize)]
pub struct CtHelperMacroJson {
pub v4: Option<bool>,
pub v6: Option<bool>,
pub name: String,
pub tcp: Option<u16>,
pub udp: Option<u16>,
}
impl TryFrom<CtHelperMacroJson> for CtHelperMacro {
type Error = Error;
fn try_from(value: CtHelperMacroJson) -> Result<Self, Self::Error> {
if value.tcp.is_none() && value.udp.is_none() {
bail!("Neither TCP nor UDP port set in CT helper!");
}
let family = match (value.v4, value.v6) {
(Some(true), Some(true)) => None,
(Some(true), _) => Some(Family::V4),
(_, Some(true)) => Some(Family::V6),
_ => bail!("Neither v4 nor v6 set in CT Helper Macro!"),
};
let mut ct_helper = CtHelperMacro {
family,
name: value.name,
tcp: None,
udp: None,
};
if let Some(dport) = value.tcp {
let ports = Ports::from_u16(None, dport);
ct_helper.tcp = Some(Tcp::new(ports).into());
}
if let Some(dport) = value.udp {
let ports = Ports::from_u16(None, dport);
ct_helper.udp = Some(Udp::new(ports).into());
}
Ok(ct_helper)
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(try_from = "CtHelperMacroJson")]
pub struct CtHelperMacro {
family: Option<Family>,
name: String,
tcp: Option<Protocol>,
udp: Option<Protocol>,
}
impl CtHelperMacro {
fn helper_name(&self, protocol: &str) -> String {
format!("helper-{}-{protocol}", self.name)
}
pub fn tcp_helper_name(&self) -> String {
self.helper_name("tcp")
}
pub fn udp_helper_name(&self) -> String {
self.helper_name("udp")
}
pub fn family(&self) -> Option<Family> {
self.family
}
pub fn name(&self) -> &str {
self.name.as_ref()
}
pub fn tcp(&self) -> Option<&Protocol> {
self.tcp.as_ref()
}
pub fn udp(&self) -> Option<&Protocol> {
self.udp.as_ref()
}
}
fn hashmap() -> &'static HashMap<String, CtHelperMacro> {
const MACROS: &str = include_str!("../../resources/ct_helper.json");
static HASHMAP: OnceLock<HashMap<String, CtHelperMacro>> = OnceLock::new();
HASHMAP.get_or_init(|| {
let macro_data: Vec<CtHelperMacro> = match serde_json::from_str(MACROS) {
Ok(data) => data,
Err(err) => {
log::error!("could not load data for ct helpers: {err}");
Vec::new()
}
};
macro_data
.into_iter()
.map(|elem| (elem.name.clone(), elem))
.collect()
})
}
pub fn get_cthelper(name: &str) -> Option<&'static CtHelperMacro> {
hashmap().get(name)
}

View File

@ -1,69 +0,0 @@
use std::collections::HashMap;
use serde::Deserialize;
use std::sync::OnceLock;
use crate::firewall::types::rule_match::Protocol;
use super::types::rule_match::RuleOptions;
#[derive(Clone, Debug, Default, Deserialize)]
struct FwMacroData {
#[serde(rename = "desc")]
pub description: &'static str,
pub code: Vec<RuleOptions>,
}
#[derive(Clone, Debug, Default)]
pub struct FwMacro {
pub _description: &'static str,
pub code: Vec<Protocol>,
}
fn macros() -> &'static HashMap<String, FwMacro> {
const MACROS: &str = include_str!("../../resources/macros.json");
static HASHMAP: OnceLock<HashMap<String, FwMacro>> = OnceLock::new();
HASHMAP.get_or_init(|| {
let macro_data: HashMap<String, FwMacroData> = match serde_json::from_str(MACROS) {
Ok(m) => m,
Err(err) => {
log::error!("could not load data for macros: {err}");
HashMap::new()
}
};
let mut macros = HashMap::new();
'outer: for (name, data) in macro_data {
let mut code = Vec::new();
for c in data.code {
match Protocol::from_options(&c) {
Ok(Some(p)) => code.push(p),
Ok(None) => {
continue 'outer;
}
Err(err) => {
log::error!("could not parse data for macro {name}: {err}");
continue 'outer;
}
}
}
macros.insert(
name,
FwMacro {
_description: data.description,
code,
},
);
}
macros
})
}
pub fn get_macro(name: &str) -> Option<&'static FwMacro> {
macros().get(name)
}

View File

@ -1,237 +0,0 @@
use std::collections::BTreeMap;
use std::io;
use crate::guest::types::Vmid;
use crate::guest::vm::NetworkConfig;
use crate::firewall::types::alias::{Alias, AliasName};
use crate::firewall::types::ipset::IpsetScope;
use crate::firewall::types::log::LogLevel;
use crate::firewall::types::rule::{Direction, Rule, Verdict};
use crate::firewall::types::Ipset;
use anyhow::{bail, Error};
use serde::Deserialize;
use crate::firewall::parse::serde_option_bool;
/// default return value for [`Config::is_enabled()`]
pub const GUEST_ENABLED_DEFAULT: bool = false;
/// default return value for [`Config::allow_ndp()`]
pub const GUEST_ALLOW_NDP_DEFAULT: bool = true;
/// default return value for [`Config::allow_dhcp()`]
pub const GUEST_ALLOW_DHCP_DEFAULT: bool = true;
/// default return value for [`Config::allow_ra()`]
pub const GUEST_ALLOW_RA_DEFAULT: bool = false;
/// default return value for [`Config::macfilter()`]
pub const GUEST_MACFILTER_DEFAULT: bool = true;
/// default return value for [`Config::ipfilter()`]
pub const GUEST_IPFILTER_DEFAULT: bool = false;
/// default return value for [`Config::default_policy()`]
pub const GUEST_POLICY_IN_DEFAULT: Verdict = Verdict::Drop;
/// default return value for [`Config::default_policy()`]
pub const GUEST_POLICY_OUT_DEFAULT: Verdict = Verdict::Accept;
#[derive(Debug, Default, Deserialize)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Options {
#[serde(default, with = "serde_option_bool")]
dhcp: Option<bool>,
#[serde(default, with = "serde_option_bool")]
enable: Option<bool>,
#[serde(default, with = "serde_option_bool")]
ipfilter: Option<bool>,
#[serde(default, with = "serde_option_bool")]
ndp: Option<bool>,
#[serde(default, with = "serde_option_bool")]
radv: Option<bool>,
log_level_in: Option<LogLevel>,
log_level_out: Option<LogLevel>,
#[serde(default, with = "serde_option_bool")]
macfilter: Option<bool>,
#[serde(rename = "policy_in")]
policy_in: Option<Verdict>,
#[serde(rename = "policy_out")]
policy_out: Option<Verdict>,
}
#[derive(Debug)]
pub struct Config {
vmid: Vmid,
/// The interface prefix: "veth" for containers, "tap" for VMs.
iface_prefix: &'static str,
network_config: NetworkConfig,
config: super::common::Config<Options>,
}
impl Config {
pub fn parse<T: io::BufRead, U: io::BufRead>(
vmid: &Vmid,
iface_prefix: &'static str,
firewall_input: T,
network_input: U,
) -> Result<Self, Error> {
let parser_cfg = super::common::ParserConfig {
guest_iface_names: true,
ipset_scope: Some(IpsetScope::Guest),
};
let config = super::common::Config::parse(firewall_input, &parser_cfg)?;
if !config.groups.is_empty() {
bail!("guest firewall config cannot declare groups");
}
let network_config = NetworkConfig::parse(network_input)?;
Ok(Self {
vmid: *vmid,
iface_prefix,
config,
network_config,
})
}
pub fn vmid(&self) -> Vmid {
self.vmid
}
pub fn alias(&self, name: &AliasName) -> Option<&Alias> {
self.config.alias(name.name())
}
pub fn iface_name_by_key(&self, key: &str) -> Result<String, Error> {
let index = NetworkConfig::index_from_net_key(key)?;
Ok(format!("{}{}i{index}", self.iface_prefix, self.vmid))
}
pub fn iface_name_by_index(&self, index: i64) -> String {
format!("{}{}i{index}", self.iface_prefix, self.vmid)
}
/// returns the value of the enabled config key or [`GUEST_ENABLED_DEFAULT`] if unset
pub fn is_enabled(&self) -> bool {
self.config.options.enable.unwrap_or(GUEST_ENABLED_DEFAULT)
}
pub fn rules(&self) -> &[Rule] {
&self.config.rules
}
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(),
}
}
/// returns the value of the ndp config key or [`GUEST_ALLOW_NDP_DEFAULT`] if unset
pub fn allow_ndp(&self) -> bool {
self.config.options.ndp.unwrap_or(GUEST_ALLOW_NDP_DEFAULT)
}
/// returns the value of the dhcp config key or [`GUEST_ALLOW_DHCP_DEFAULT`] if unset
pub fn allow_dhcp(&self) -> bool {
self.config.options.dhcp.unwrap_or(GUEST_ALLOW_DHCP_DEFAULT)
}
/// returns the value of the radv config key or [`GUEST_ALLOW_RA_DEFAULT`] if unset
pub fn allow_ra(&self) -> bool {
self.config.options.radv.unwrap_or(GUEST_ALLOW_RA_DEFAULT)
}
/// returns the value of the macfilter config key or [`GUEST_MACFILTER_DEFAULT`] if unset
pub fn macfilter(&self) -> bool {
self.config
.options
.macfilter
.unwrap_or(GUEST_MACFILTER_DEFAULT)
}
/// returns the value of the ipfilter config key or [`GUEST_IPFILTER_DEFAULT`] if unset
pub fn ipfilter(&self) -> bool {
self.config
.options
.ipfilter
.unwrap_or(GUEST_IPFILTER_DEFAULT)
}
/// returns the value of the policy_in/out config key or
/// [`GUEST_POLICY_IN_DEFAULT`] / [`GUEST_POLICY_OUT_DEFAULT`] if unset
pub fn default_policy(&self, dir: Direction) -> Verdict {
match dir {
Direction::In => self
.config
.options
.policy_in
.unwrap_or(GUEST_POLICY_IN_DEFAULT),
Direction::Out => self
.config
.options
.policy_out
.unwrap_or(GUEST_POLICY_OUT_DEFAULT),
}
}
pub fn network_config(&self) -> &NetworkConfig {
&self.network_config
}
pub fn ipsets(&self) -> &BTreeMap<String, Ipset> {
self.config.ipsets()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_config() {
// most of the stuff is already tested in cluster parsing, only testing
// guest specific options here
const CONFIG: &str = r#"
[OPTIONS]
enable: 1
dhcp: 1
ipfilter: 0
log_level_in: emerg
log_level_out: crit
macfilter: 0
ndp:1
radv:1
policy_in: REJECT
policy_out: REJECT
"#;
let config = CONFIG.as_bytes();
let network_config: Vec<u8> = Vec::new();
let config =
Config::parse(&Vmid::new(100), "tap", config, network_config.as_slice()).unwrap();
assert_eq!(
config.config.options,
Options {
dhcp: Some(true),
enable: Some(true),
ipfilter: Some(false),
ndp: Some(true),
radv: Some(true),
log_level_in: Some(LogLevel::Emergency),
log_level_out: Some(LogLevel::Critical),
macfilter: Some(false),
policy_in: Some(Verdict::Reject),
policy_out: Some(Verdict::Reject),
}
);
}
}

View File

@ -1,372 +0,0 @@
use std::io;
use std::net::IpAddr;
use anyhow::{bail, Error};
use serde::Deserialize;
use crate::host::utils::{host_ips, network_interface_cidrs};
use proxmox_sys::nodename;
use crate::firewall::parse;
use crate::firewall::types::log::LogLevel;
use crate::firewall::types::rule::Direction;
use crate::firewall::types::{Alias, Cidr, Rule};
/// default setting for the enabled key
pub const HOST_ENABLED_DEFAULT: bool = true;
/// default setting for the nftables key
pub const HOST_NFTABLES_DEFAULT: bool = false;
/// default return value for [`Config::allow_ndp()`]
pub const HOST_ALLOW_NDP_DEFAULT: bool = true;
/// default return value for [`Config::block_smurfs()`]
pub const HOST_BLOCK_SMURFS_DEFAULT: bool = true;
/// default return value for [`Config::block_synflood()`]
pub const HOST_BLOCK_SYNFLOOD_DEFAULT: bool = false;
/// default rate limit for synflood rule (packets / second)
pub const HOST_BLOCK_SYNFLOOD_RATE_DEFAULT: i64 = 200;
/// default rate limit for synflood rule (packets / second)
pub const HOST_BLOCK_SYNFLOOD_BURST_DEFAULT: i64 = 1000;
/// default return value for [`Config::block_invalid_tcp()`]
pub const HOST_BLOCK_INVALID_TCP_DEFAULT: bool = false;
/// default return value for [`Config::block_invalid_conntrack()`]
pub const HOST_BLOCK_INVALID_CONNTRACK: bool = false;
/// default setting for logging of invalid conntrack entries
pub const HOST_LOG_INVALID_CONNTRACK: bool = false;
#[derive(Debug, Default, Deserialize)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Options {
#[serde(default, with = "parse::serde_option_bool")]
enable: Option<bool>,
#[serde(default, with = "parse::serde_option_bool")]
nftables: Option<bool>,
log_level_in: Option<LogLevel>,
log_level_out: Option<LogLevel>,
#[serde(default, with = "parse::serde_option_bool")]
log_nf_conntrack: Option<bool>,
#[serde(default, with = "parse::serde_option_bool")]
ndp: Option<bool>,
#[serde(default, with = "parse::serde_option_bool")]
nf_conntrack_allow_invalid: Option<bool>,
// is Option<Vec<>> for easier deserialization
#[serde(default, with = "parse::serde_option_conntrack_helpers")]
nf_conntrack_helpers: Option<Vec<String>>,
#[serde(default, with = "parse::serde_option_number")]
nf_conntrack_max: Option<i64>,
#[serde(default, with = "parse::serde_option_number")]
nf_conntrack_tcp_timeout_established: Option<i64>,
#[serde(default, with = "parse::serde_option_number")]
nf_conntrack_tcp_timeout_syn_recv: Option<i64>,
#[serde(default, with = "parse::serde_option_bool")]
nosmurfs: Option<bool>,
#[serde(default, with = "parse::serde_option_bool")]
protection_synflood: Option<bool>,
#[serde(default, with = "parse::serde_option_number")]
protection_synflood_burst: Option<i64>,
#[serde(default, with = "parse::serde_option_number")]
protection_synflood_rate: Option<i64>,
smurf_log_level: Option<LogLevel>,
tcp_flags_log_level: Option<LogLevel>,
#[serde(default, with = "parse::serde_option_bool")]
tcpflags: Option<bool>,
}
#[derive(Debug, Default)]
pub struct Config {
pub(crate) config: super::common::Config<Options>,
}
impl Config {
pub fn new() -> Self {
Self {
config: Default::default(),
}
}
pub fn parse<R: io::BufRead>(input: R) -> Result<Self, Error> {
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<Vec<Cidr>, 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);
}
}
(IpAddr::V6(ip), Cidr::Ipv6(cidr)) => {
if cidr.contains_address(&ip) {
management_cidrs.push(network_interface_cidr);
}
}
_ => continue,
};
}
}
Ok(management_cidrs)
}
pub fn hostname() -> &'static str {
nodename()
}
pub fn get_alias(&self, name: &str) -> Option<&Alias> {
self.config.alias(name)
}
/// returns value of enabled key or [`HOST_ENABLED_DEFAULT`] if unset
pub fn is_enabled(&self) -> bool {
self.config.options.enable.unwrap_or(HOST_ENABLED_DEFAULT)
}
/// returns value of nftables key or [`HOST_NFTABLES_DEFAULT`] if unset
pub fn nftables(&self) -> bool {
self.config
.options
.nftables
.unwrap_or(HOST_NFTABLES_DEFAULT)
}
/// returns value of ndp key or [`HOST_ALLOW_NDP_DEFAULT`] if unset
pub fn allow_ndp(&self) -> bool {
self.config.options.ndp.unwrap_or(HOST_ALLOW_NDP_DEFAULT)
}
/// returns value of nosmurfs key or [`HOST_BLOCK_SMURFS_DEFAULT`] if unset
pub fn block_smurfs(&self) -> bool {
self.config
.options
.nosmurfs
.unwrap_or(HOST_BLOCK_SMURFS_DEFAULT)
}
/// returns the log level for the smurf protection rule
///
/// If there is no log level set, it returns [`LogLevel::default()`]
pub fn block_smurfs_log_level(&self) -> LogLevel {
self.config.options.smurf_log_level.unwrap_or_default()
}
/// returns value of protection_synflood key or [`HOST_BLOCK_SYNFLOOD_DEFAULT`] if unset
pub fn block_synflood(&self) -> bool {
self.config
.options
.protection_synflood
.unwrap_or(HOST_BLOCK_SYNFLOOD_DEFAULT)
}
/// returns value of protection_synflood_rate key or [`HOST_BLOCK_SYNFLOOD_RATE_DEFAULT`] if
/// unset
pub fn synflood_rate(&self) -> i64 {
self.config
.options
.protection_synflood_rate
.unwrap_or(HOST_BLOCK_SYNFLOOD_RATE_DEFAULT)
}
/// returns value of protection_synflood_burst key or [`HOST_BLOCK_SYNFLOOD_BURST_DEFAULT`] if
/// unset
pub fn synflood_burst(&self) -> i64 {
self.config
.options
.protection_synflood_burst
.unwrap_or(HOST_BLOCK_SYNFLOOD_BURST_DEFAULT)
}
/// returns value of tcpflags key or [`HOST_BLOCK_INVALID_TCP_DEFAULT`] if unset
pub fn block_invalid_tcp(&self) -> bool {
self.config
.options
.tcpflags
.unwrap_or(HOST_BLOCK_INVALID_TCP_DEFAULT)
}
/// returns the log level for the block invalid TCP packets rule
///
/// If there is no log level set, it returns [`LogLevel::default()`]
pub fn block_invalid_tcp_log_level(&self) -> LogLevel {
self.config.options.tcp_flags_log_level.unwrap_or_default()
}
/// returns value of nf_conntrack_allow_invalid key or [`HOST_BLOCK_INVALID_CONNTRACK`] if
/// unset
pub fn block_invalid_conntrack(&self) -> bool {
!self
.config
.options
.nf_conntrack_allow_invalid
.unwrap_or(HOST_BLOCK_INVALID_CONNTRACK)
}
pub fn nf_conntrack_max(&self) -> Option<i64> {
self.config.options.nf_conntrack_max
}
pub fn nf_conntrack_tcp_timeout_established(&self) -> Option<i64> {
self.config.options.nf_conntrack_tcp_timeout_established
}
pub fn nf_conntrack_tcp_timeout_syn_recv(&self) -> Option<i64> {
self.config.options.nf_conntrack_tcp_timeout_syn_recv
}
/// returns value of log_nf_conntrack key or [`HOST_LOG_INVALID_CONNTRACK`] if unset
pub fn log_nf_conntrack(&self) -> bool {
self.config
.options
.log_nf_conntrack
.unwrap_or(HOST_LOG_INVALID_CONNTRACK)
}
pub fn conntrack_helpers(&self) -> Option<&Vec<String>> {
self.config.options.nf_conntrack_helpers.as_ref()
}
/// returns the log level for the given direction
///
/// If there is no log level set it returns [`LogLevel::default()`]
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");
}
}

View File

@ -1,10 +0,0 @@
pub mod cluster;
pub mod common;
pub mod ct_helper;
pub mod fw_macros;
pub mod guest;
pub mod host;
pub mod ports;
pub mod types;
pub(crate) mod parse;

View File

@ -1,494 +0,0 @@
use std::fmt;
use anyhow::{bail, format_err, Error};
const NAME_SPECIAL_CHARACTERS: [u8; 2] = [b'-', b'_'];
/// Parses out a "name" which can be alphanumeric and include dashes.
///
/// Returns `None` if the name part would be empty.
///
/// Returns a tuple with the name and the remainder (not trimmed).
///
/// # Examples
/// ```ignore
/// assert_eq!(match_name("some-name someremainder"), Some(("some-name", " someremainder")));
/// assert_eq!(match_name("some-name@someremainder"), Some(("some-name", "@someremainder")));
/// assert_eq!(match_name(""), None);
/// assert_eq!(match_name(" someremainder"), None);
/// ```
pub fn match_name(line: &str) -> Option<(&str, &str)> {
if !line.starts_with(|c: char| c.is_ascii_alphabetic()) {
return None;
}
let end = line
.as_bytes()
.iter()
.position(|&b| !(b.is_ascii_alphanumeric() || NAME_SPECIAL_CHARACTERS.contains(&b)));
let (name, rest) = match end {
Some(end) => line.split_at(end),
None => (line, ""),
};
if name.is_empty() {
None
} else {
Some((name, rest))
}
}
/// Parses up to the next whitespace character or end of the string.
///
/// Returns `None` if the non-whitespace part would be empty.
///
/// Returns a tuple containing the parsed section and the *trimmed* remainder.
pub fn match_non_whitespace(line: &str) -> Option<(&str, &str)> {
let (text, rest) = line
.as_bytes()
.iter()
.position(|&b| b.is_ascii_whitespace())
.map(|pos| {
let (a, b) = line.split_at(pos);
(a, b.trim_start())
})
.unwrap_or((line, ""));
if text.is_empty() {
None
} else {
Some((text, rest))
}
}
/// parses out all digits and returns the remainder
///
/// returns [`None`] if the digit part would be empty
///
/// Returns a tuple with the digits and the remainder (not trimmed).
pub fn match_digits(line: &str) -> Option<(&str, &str)> {
let split_position = line.as_bytes().iter().position(|&b| !b.is_ascii_digit());
let (digits, rest) = match split_position {
Some(pos) => line.split_at(pos),
None => (line, ""),
};
if !digits.is_empty() {
return Some((digits, rest));
}
None
}
/// Separate a `key: value` line, trimming whitespace.
///
/// Returns `None` if the `key` would be empty.
pub fn split_key_value(line: &str) -> Option<(&str, &str)> {
line.split_once(':')
.map(|(key, value)| (key.trim(), value.trim()))
}
/// Parse a boolean.
///
/// values that parse as [`false`]: 0, false, off, no
/// values that parse as [`true`]: 1, true, on, yes
///
/// # Examples
/// ```ignore
/// assert_eq!(parse_bool("false"), Ok(false));
/// assert_eq!(parse_bool("on"), Ok(true));
/// assert!(parse_bool("proxmox").is_err());
/// ```
pub fn parse_bool(value: &str) -> Result<bool, Error> {
Ok(
if value == "0"
|| value.eq_ignore_ascii_case("false")
|| value.eq_ignore_ascii_case("off")
|| value.eq_ignore_ascii_case("no")
{
false
} else if value == "1"
|| value.eq_ignore_ascii_case("true")
|| value.eq_ignore_ascii_case("on")
|| value.eq_ignore_ascii_case("yes")
{
true
} else {
bail!("not a boolean: {value:?}");
},
)
}
/// Parse the *remainder* of a section line, that is `<whitespace>NAME] #optional comment`.
/// The `kind` parameter is used for error messages and should be the section type.
///
/// Return the name and the optional comment.
pub fn parse_named_section_tail<'a>(
kind: &'static str,
line: &'a str,
) -> Result<(&'a str, Option<&'a str>), Error> {
if line.is_empty() || !line.as_bytes()[0].is_ascii_whitespace() {
bail!("incomplete {kind} section");
}
let line = line.trim_start();
let (name, line) = match_name(line)
.ok_or_else(|| format_err!("expected a name for the {kind} at {line:?}"))?;
let line = line
.strip_prefix(']')
.ok_or_else(|| format_err!("expected closing ']' in {kind} section header"))?
.trim_start();
Ok(match line.strip_prefix('#') {
Some(comment) => (name, Some(comment.trim())),
None if !line.is_empty() => bail!("trailing characters after {kind} section: {line:?}"),
None => (name, None),
})
}
// parses a number from a string OR number
pub mod serde_option_number {
use std::fmt;
use serde::de::{Deserializer, Error, Visitor};
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<i64>, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = Option<i64>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a numerical value")
}
fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
v.parse().map_err(E::custom).map(Some)
}
fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(self)
}
}
deserializer.deserialize_any(V)
}
}
// parses a bool from a string OR bool
pub mod serde_option_bool {
use std::fmt;
use serde::de::{Deserializer, Error, Visitor};
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<bool>, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = Option<bool>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a boolean-like value")
}
fn visit_bool<E: Error>(self, v: bool) -> Result<Self::Value, E> {
Ok(Some(v))
}
fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
super::parse_bool(v).map_err(E::custom).map(Some)
}
fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(self)
}
}
deserializer.deserialize_any(V)
}
}
// parses a comma_separated list of strings
pub mod serde_option_conntrack_helpers {
use std::fmt;
use serde::de::{Deserializer, Error, Visitor};
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<Vec<String>>, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = Option<Vec<String>>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("A list of conntrack helpers")
}
fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
if v.is_empty() {
return Ok(None);
}
Ok(Some(v.split(',').map(String::from).collect()))
}
fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(self)
}
}
deserializer.deserialize_any(V)
}
}
// parses a log_ratelimit string: '[enable=]<1|0> [,burst=<integer>] [,rate=<rate>]'
pub mod serde_option_log_ratelimit {
use std::fmt;
use serde::de::{Deserializer, Error, Visitor};
use crate::firewall::types::log::LogRateLimit;
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<LogRateLimit>, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = Option<LogRateLimit>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a boolean-like value")
}
fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
v.parse().map_err(E::custom).map(Some)
}
fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(self)
}
}
deserializer.deserialize_any(V)
}
}
/// `&str` deserializer which also accepts an `Option`.
///
/// Serde's `StringDeserializer` does not.
#[derive(Clone, Copy, Debug)]
pub struct SomeStrDeserializer<'a, E>(serde::de::value::StrDeserializer<'a, E>);
impl<'de, 'a, E> serde::de::Deserializer<'de> for SomeStrDeserializer<'a, E>
where
E: serde::de::Error,
{
type Error = E;
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
self.0.deserialize_any(visitor)
}
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
visitor.visit_some(self.0)
}
fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
self.0.deserialize_str(visitor)
}
fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
self.0.deserialize_string(visitor)
}
fn deserialize_enum<V>(
self,
_name: &str,
_variants: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
visitor.visit_enum(self.0)
}
serde::forward_to_deserialize_any! {
bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char
bytes byte_buf unit unit_struct newtype_struct seq tuple
tuple_struct map struct identifier ignored_any
}
}
/// `&str` wrapper which implements `IntoDeserializer` via `SomeStrDeserializer`.
#[derive(Clone, Debug)]
pub struct SomeStr<'a>(pub &'a str);
impl<'a> From<&'a str> for SomeStr<'a> {
fn from(s: &'a str) -> Self {
Self(s)
}
}
impl<'de, 'a, E> serde::de::IntoDeserializer<'de, E> for SomeStr<'a>
where
E: serde::de::Error,
{
type Deserializer = SomeStrDeserializer<'a, E>;
fn into_deserializer(self) -> Self::Deserializer {
SomeStrDeserializer(self.0.into_deserializer())
}
}
/// `String` deserializer which also accepts an `Option`.
///
/// Serde's `StringDeserializer` does not.
#[derive(Clone, Debug)]
pub struct SomeStringDeserializer<E>(serde::de::value::StringDeserializer<E>);
impl<'de, E> serde::de::Deserializer<'de> for SomeStringDeserializer<E>
where
E: serde::de::Error,
{
type Error = E;
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
self.0.deserialize_any(visitor)
}
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
visitor.visit_some(self.0)
}
fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
self.0.deserialize_str(visitor)
}
fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
self.0.deserialize_string(visitor)
}
fn deserialize_enum<V>(
self,
_name: &str,
_variants: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
visitor.visit_enum(self.0)
}
serde::forward_to_deserialize_any! {
bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char
bytes byte_buf unit unit_struct newtype_struct seq tuple
tuple_struct map struct identifier ignored_any
}
}
/// `&str` wrapper which implements `IntoDeserializer` via `SomeStringDeserializer`.
#[derive(Clone, Debug)]
pub struct SomeString(pub String);
impl From<&str> for SomeString {
fn from(s: &str) -> Self {
Self::from(s.to_string())
}
}
impl From<String> for SomeString {
fn from(s: String) -> Self {
Self(s)
}
}
impl<'de, E> serde::de::IntoDeserializer<'de, E> for SomeString
where
E: serde::de::Error,
{
type Deserializer = SomeStringDeserializer<E>;
fn into_deserializer(self) -> Self::Deserializer {
SomeStringDeserializer(self.0.into_deserializer())
}
}
#[derive(Debug)]
pub struct SerdeStringError(String);
impl std::error::Error for SerdeStringError {}
impl fmt::Display for SerdeStringError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(&self.0)
}
}
impl serde::de::Error for SerdeStringError {
fn custom<T: fmt::Display>(msg: T) -> Self {
Self(msg.to_string())
}
}

View File

@ -1,80 +0,0 @@
use anyhow::{format_err, Error};
use std::sync::OnceLock;
#[derive(Default)]
struct NamedPorts {
ports: std::collections::HashMap<String, u16>,
}
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<u16> {
self.ports.get(name).copied()
}
}
fn named_ports() -> &'static NamedPorts {
static NAMED_PORTS: OnceLock<NamedPorts> = 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<u16, Error> {
named_ports()
.find(name)
.ok_or_else(|| format_err!("unknown port name {name:?}"))
}

View File

@ -1,615 +0,0 @@
use std::fmt;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::ops::Deref;
use anyhow::{bail, format_err, Error};
use serde_with::DeserializeFromStr;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Family {
V4,
V6,
}
impl fmt::Display for Family {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Family::V4 => f.write_str("Ipv4"),
Family::V6 => f.write_str("Ipv6"),
}
}
}
#[derive(Clone, Copy, Debug)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub enum Cidr {
Ipv4(Ipv4Cidr),
Ipv6(Ipv6Cidr),
}
impl Cidr {
pub fn new_v4(addr: impl Into<Ipv4Addr>, mask: u8) -> Result<Self, Error> {
Ok(Cidr::Ipv4(Ipv4Cidr::new(addr, mask)?))
}
pub fn new_v6(addr: impl Into<Ipv6Addr>, mask: u8) -> Result<Self, Error> {
Ok(Cidr::Ipv6(Ipv6Cidr::new(addr, mask)?))
}
pub const fn family(&self) -> Family {
match self {
Cidr::Ipv4(_) => Family::V4,
Cidr::Ipv6(_) => Family::V6,
}
}
pub fn is_ipv4(&self) -> bool {
matches!(self, Cidr::Ipv4(_))
}
pub fn is_ipv6(&self) -> bool {
matches!(self, Cidr::Ipv6(_))
}
}
impl fmt::Display for Cidr {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Ipv4(ip) => f.write_str(ip.to_string().as_str()),
Self::Ipv6(ip) => f.write_str(ip.to_string().as_str()),
}
}
}
impl std::str::FromStr for Cidr {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
if let Ok(ip) = s.parse::<Ipv4Cidr>() {
return Ok(Cidr::Ipv4(ip));
}
if let Ok(ip) = s.parse::<Ipv6Cidr>() {
return Ok(Cidr::Ipv6(ip));
}
bail!("invalid ip address or CIDR: {s:?}");
}
}
impl From<Ipv4Cidr> for Cidr {
fn from(cidr: Ipv4Cidr) -> Self {
Cidr::Ipv4(cidr)
}
}
impl From<Ipv6Cidr> for Cidr {
fn from(cidr: Ipv6Cidr) -> Self {
Cidr::Ipv6(cidr)
}
}
const IPV4_LENGTH: u8 = 32;
#[derive(Clone, Copy, Debug)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Ipv4Cidr {
addr: Ipv4Addr,
mask: u8,
}
impl Ipv4Cidr {
pub fn new(addr: impl Into<Ipv4Addr>, mask: u8) -> Result<Self, Error> {
if mask > 32 {
bail!("mask out of range for ipv4 cidr ({mask})");
}
Ok(Self {
addr: addr.into(),
mask,
})
}
pub fn contains_address(&self, other: &Ipv4Addr) -> bool {
let bits = u32::from_be_bytes(self.addr.octets());
let other_bits = u32::from_be_bytes(other.octets());
let shift_amount: u32 = IPV4_LENGTH.saturating_sub(self.mask).into();
bits.checked_shr(shift_amount).unwrap_or(0)
== other_bits.checked_shr(shift_amount).unwrap_or(0)
}
pub fn address(&self) -> &Ipv4Addr {
&self.addr
}
pub fn mask(&self) -> u8 {
self.mask
}
}
impl<T: Into<Ipv4Addr>> From<T> for Ipv4Cidr {
fn from(value: T) -> Self {
Self {
addr: value.into(),
mask: 32,
}
}
}
impl std::str::FromStr for Ipv4Cidr {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
Ok(match s.find('/') {
None => Self {
addr: s.parse()?,
mask: 32,
},
Some(pos) => {
let mask: u8 = s[(pos + 1)..]
.parse()
.map_err(|_| format_err!("invalid mask in ipv4 cidr: {s:?}"))?;
Self::new(s[..pos].parse::<Ipv4Addr>()?, mask)?
}
})
}
}
impl fmt::Display for Ipv4Cidr {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}/{}", &self.addr, self.mask)
}
}
const IPV6_LENGTH: u8 = 128;
#[derive(Clone, Copy, Debug)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Ipv6Cidr {
addr: Ipv6Addr,
mask: u8,
}
impl Ipv6Cidr {
pub fn new(addr: impl Into<Ipv6Addr>, mask: u8) -> Result<Self, Error> {
if mask > IPV6_LENGTH {
bail!("mask out of range for ipv6 cidr");
}
Ok(Self {
addr: addr.into(),
mask,
})
}
pub fn contains_address(&self, other: &Ipv6Addr) -> bool {
let bits = u128::from_be_bytes(self.addr.octets());
let other_bits = u128::from_be_bytes(other.octets());
let shift_amount: u32 = IPV6_LENGTH.saturating_sub(self.mask).into();
bits.checked_shr(shift_amount).unwrap_or(0)
== other_bits.checked_shr(shift_amount).unwrap_or(0)
}
pub fn address(&self) -> &Ipv6Addr {
&self.addr
}
pub fn mask(&self) -> u8 {
self.mask
}
}
impl std::str::FromStr for Ipv6Cidr {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
Ok(match s.find('/') {
None => Self {
addr: s.parse()?,
mask: 128,
},
Some(pos) => {
let mask: u8 = s[(pos + 1)..]
.parse()
.map_err(|_| format_err!("invalid mask in ipv6 cidr: {s:?}"))?;
Self::new(s[..pos].parse::<Ipv6Addr>()?, mask)?
}
})
}
}
impl fmt::Display for Ipv6Cidr {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}/{}", &self.addr, self.mask)
}
}
impl<T: Into<Ipv6Addr>> From<T> for Ipv6Cidr {
fn from(addr: T) -> Self {
Self {
addr: addr.into(),
mask: 128,
}
}
}
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub enum IpEntry {
Cidr(Cidr),
Range(IpAddr, IpAddr),
}
impl std::str::FromStr for IpEntry {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
if s.is_empty() {
bail!("Empty IP specification!")
}
let entries: Vec<&str> = s
.split('-')
.take(3) // so we can check whether there are too many
.collect();
match entries.as_slice() {
[cidr] => Ok(IpEntry::Cidr(cidr.parse()?)),
[beg, end] => {
if let Ok(beg) = beg.parse::<Ipv4Addr>() {
if let Ok(end) = end.parse::<Ipv4Addr>() {
if beg < end {
return Ok(IpEntry::Range(beg.into(), end.into()));
}
bail!("start address is greater than end address!");
}
}
if let Ok(beg) = beg.parse::<Ipv6Addr>() {
if let Ok(end) = end.parse::<Ipv6Addr>() {
if beg < end {
return Ok(IpEntry::Range(beg.into(), end.into()));
}
bail!("start address is greater than end address!");
}
}
bail!("start and end are not valid IP addresses of the same type!")
}
_ => bail!("Invalid amount of elements in IpEntry!"),
}
}
}
impl fmt::Display for IpEntry {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Cidr(ip) => write!(f, "{ip}"),
Self::Range(beg, end) => write!(f, "{beg}-{end}"),
}
}
}
impl IpEntry {
fn family(&self) -> Family {
match self {
Self::Cidr(cidr) => cidr.family(),
Self::Range(start, end) => {
if start.is_ipv4() && end.is_ipv4() {
return Family::V4;
}
if start.is_ipv6() && end.is_ipv6() {
return Family::V6;
}
// should never be reached due to constructors validating that
// start type == end type
unreachable!("invalid IP entry")
}
}
}
}
impl From<Cidr> for IpEntry {
fn from(value: Cidr) -> Self {
IpEntry::Cidr(value)
}
}
#[derive(Clone, Debug, DeserializeFromStr)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct IpList {
// guaranteed to have the same family
entries: Vec<IpEntry>,
family: Family,
}
impl Deref for IpList {
type Target = Vec<IpEntry>;
fn deref(&self) -> &Self::Target {
&self.entries
}
}
impl<T: Into<IpEntry>> From<T> for IpList {
fn from(value: T) -> Self {
let entry = value.into();
Self {
family: entry.family(),
entries: vec![entry],
}
}
}
impl std::str::FromStr for IpList {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
if s.is_empty() {
bail!("Empty IP specification!")
}
let mut entries = Vec::new();
let mut current_family = None;
for element in s.split(',') {
let entry: IpEntry = element.parse()?;
if let Some(family) = current_family {
if family != entry.family() {
bail!("Incompatible families in IPList!")
}
} else {
current_family = Some(entry.family());
}
entries.push(entry);
}
if entries.is_empty() {
bail!("empty ip list")
}
Ok(IpList {
entries,
family: current_family.unwrap(), // must be set due to length check above
})
}
}
impl IpList {
pub fn new(entries: Vec<IpEntry>) -> Result<Self, Error> {
let family = entries.iter().try_fold(None, |result, entry| {
if let Some(family) = result {
if entry.family() != family {
bail!("non-matching families in entries list");
}
Ok(Some(family))
} else {
Ok(Some(entry.family()))
}
})?;
if let Some(family) = family {
return Ok(Self { entries, family });
}
bail!("no elements in ip list entries");
}
pub fn family(&self) -> Family {
self.family
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{Ipv4Addr, Ipv6Addr};
#[test]
fn test_v4_cidr() {
let mut cidr: Ipv4Cidr = "0.0.0.0/0".parse().expect("valid IPv4 CIDR");
assert_eq!(cidr.addr, Ipv4Addr::new(0, 0, 0, 0));
assert_eq!(cidr.mask, 0);
assert!(cidr.contains_address(&Ipv4Addr::new(0, 0, 0, 0)));
assert!(cidr.contains_address(&Ipv4Addr::new(255, 255, 255, 255)));
cidr = "192.168.100.1".parse().expect("valid IPv4 CIDR");
assert_eq!(cidr.addr, Ipv4Addr::new(192, 168, 100, 1));
assert_eq!(cidr.mask, 32);
assert!(cidr.contains_address(&Ipv4Addr::new(192, 168, 100, 1)));
assert!(!cidr.contains_address(&Ipv4Addr::new(192, 168, 100, 2)));
assert!(!cidr.contains_address(&Ipv4Addr::new(192, 168, 100, 0)));
cidr = "10.100.5.0/24".parse().expect("valid IPv4 CIDR");
assert_eq!(cidr.mask, 24);
assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 0)));
assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 1)));
assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 100)));
assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 255)));
assert!(!cidr.contains_address(&Ipv4Addr::new(10, 100, 4, 255)));
assert!(!cidr.contains_address(&Ipv4Addr::new(10, 100, 6, 0)));
"0.0.0.0/-1".parse::<Ipv4Cidr>().unwrap_err();
"0.0.0.0/33".parse::<Ipv4Cidr>().unwrap_err();
"256.256.256.256/10".parse::<Ipv4Cidr>().unwrap_err();
"fe80::1/64".parse::<Ipv4Cidr>().unwrap_err();
"qweasd".parse::<Ipv4Cidr>().unwrap_err();
"".parse::<Ipv4Cidr>().unwrap_err();
}
#[test]
fn test_v6_cidr() {
let mut cidr: Ipv6Cidr = "abab::1/64".parse().expect("valid IPv6 CIDR");
assert_eq!(cidr.addr, Ipv6Addr::new(0xABAB, 0, 0, 0, 0, 0, 0, 1));
assert_eq!(cidr.mask, 64);
assert!(cidr.contains_address(&Ipv6Addr::new(0xABAB, 0, 0, 0, 0, 0, 0, 0)));
assert!(cidr.contains_address(&Ipv6Addr::new(
0xABAB, 0, 0, 0, 0xAAAA, 0xAAAA, 0xAAAA, 0xAAAA
)));
assert!(cidr.contains_address(&Ipv6Addr::new(
0xABAB, 0, 0, 0, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
)));
assert!(!cidr.contains_address(&Ipv6Addr::new(0xABAB, 0, 0, 1, 0, 0, 0, 0)));
assert!(!cidr.contains_address(&Ipv6Addr::new(
0xABAA, 0, 0, 0, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
)));
cidr = "eeee::1".parse().expect("valid IPv6 CIDR");
assert_eq!(cidr.mask, 128);
assert!(cidr.contains_address(&Ipv6Addr::new(0xEEEE, 0, 0, 0, 0, 0, 0, 1)));
assert!(!cidr.contains_address(&Ipv6Addr::new(
0xEEED, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
)));
assert!(!cidr.contains_address(&Ipv6Addr::new(0xEEEE, 0, 0, 0, 0, 0, 0, 0)));
"eeee::1/-1".parse::<Ipv6Cidr>().unwrap_err();
"eeee::1/129".parse::<Ipv6Cidr>().unwrap_err();
"gggg::1/64".parse::<Ipv6Cidr>().unwrap_err();
"192.168.0.1".parse::<Ipv6Cidr>().unwrap_err();
"qweasd".parse::<Ipv6Cidr>().unwrap_err();
"".parse::<Ipv6Cidr>().unwrap_err();
}
#[test]
fn test_parse_ip_entry() {
let mut entry: IpEntry = "10.0.0.1".parse().expect("valid IP entry");
assert_eq!(entry, Cidr::new_v4([10, 0, 0, 1], 32).unwrap().into());
entry = "10.0.0.0/16".parse().expect("valid IP entry");
assert_eq!(entry, Cidr::new_v4([10, 0, 0, 0], 16).unwrap().into());
entry = "192.168.0.1-192.168.99.255"
.parse()
.expect("valid IP entry");
assert_eq!(
entry,
IpEntry::Range([192, 168, 0, 1].into(), [192, 168, 99, 255].into())
);
entry = "fe80::1".parse().expect("valid IP entry");
assert_eq!(
entry,
Cidr::new_v6([0xFE80, 0, 0, 0, 0, 0, 0, 1], 128)
.unwrap()
.into()
);
entry = "fe80::1/48".parse().expect("valid IP entry");
assert_eq!(
entry,
Cidr::new_v6([0xFE80, 0, 0, 0, 0, 0, 0, 1], 48)
.unwrap()
.into()
);
entry = "fd80::1-fd80::ffff".parse().expect("valid IP entry");
assert_eq!(
entry,
IpEntry::Range(
[0xFD80, 0, 0, 0, 0, 0, 0, 1].into(),
[0xFD80, 0, 0, 0, 0, 0, 0, 0xFFFF].into(),
)
);
"192.168.100.0-192.168.99.255"
.parse::<IpEntry>()
.unwrap_err();
"192.168.100.0-fe80::1".parse::<IpEntry>().unwrap_err();
"192.168.100.0-192.168.200.0/16"
.parse::<IpEntry>()
.unwrap_err();
"192.168.100.0-192.168.200.0-192.168.250.0"
.parse::<IpEntry>()
.unwrap_err();
"qweasd".parse::<IpEntry>().unwrap_err();
}
#[test]
fn test_parse_ip_list() {
let mut ip_list: IpList = "192.168.0.1,192.168.100.0/24,172.16.0.0-172.32.255.255"
.parse()
.expect("valid IP list");
assert_eq!(
ip_list,
IpList {
entries: vec![
IpEntry::Cidr(Cidr::new_v4([192, 168, 0, 1], 32).unwrap()),
IpEntry::Cidr(Cidr::new_v4([192, 168, 100, 0], 24).unwrap()),
IpEntry::Range([172, 16, 0, 0].into(), [172, 32, 255, 255].into()),
],
family: Family::V4,
}
);
ip_list = "fe80::1/64".parse().expect("valid IP list");
assert_eq!(
ip_list,
IpList {
entries: vec![IpEntry::Cidr(
Cidr::new_v6([0xFE80, 0, 0, 0, 0, 0, 0, 1], 64).unwrap()
),],
family: Family::V6,
}
);
"192.168.0.1,fe80::1".parse::<IpList>().unwrap_err();
"".parse::<IpList>().unwrap_err();
"proxmox".parse::<IpList>().unwrap_err();
}
#[test]
fn test_construct_ip_list() {
let mut ip_list = IpList::new(vec![Cidr::new_v4([10, 0, 0, 0], 8).unwrap().into()])
.expect("valid ip list");
assert_eq!(ip_list.family(), Family::V4);
ip_list =
IpList::new(vec![Cidr::new_v6([0x000; 8], 8).unwrap().into()]).expect("valid ip list");
assert_eq!(ip_list.family(), Family::V6);
IpList::new(vec![]).expect_err("empty ip list is invalid");
IpList::new(vec![
Cidr::new_v4([10, 0, 0, 0], 8).unwrap().into(),
Cidr::new_v6([0x0000; 8], 8).unwrap().into(),
])
.expect_err("cannot mix ip families in ip list");
}
}

View File

@ -1,174 +0,0 @@
use std::fmt::Display;
use std::str::FromStr;
use anyhow::{bail, format_err, Error};
use serde_with::DeserializeFromStr;
use crate::firewall::parse::{match_name, match_non_whitespace};
use crate::firewall::types::address::Cidr;
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub enum AliasScope {
Datacenter,
Guest,
}
impl FromStr for AliasScope {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"dc" => AliasScope::Datacenter,
"guest" => AliasScope::Guest,
_ => bail!("invalid scope for alias: {s}"),
})
}
}
impl Display for AliasScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
AliasScope::Datacenter => "dc",
AliasScope::Guest => "guest",
})
}
}
#[derive(Debug, Clone, DeserializeFromStr)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct AliasName {
scope: AliasScope,
name: String,
}
impl Display for AliasName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}/{}", self.scope, self.name))
}
}
impl FromStr for AliasName {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.split_once('/') {
Some((prefix, name)) if !name.is_empty() => Ok(Self {
scope: prefix.parse()?,
name: name.to_string(),
}),
_ => {
bail!("Invalid Alias name!")
}
}
}
}
impl AliasName {
pub fn new(scope: AliasScope, name: impl Into<String>) -> Self {
Self {
scope,
name: name.into(),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn scope(&self) -> &AliasScope {
&self.scope
}
}
#[derive(Debug)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Alias {
name: String,
address: Cidr,
comment: Option<String>,
}
impl Alias {
pub fn new(
name: impl Into<String>,
address: impl Into<Cidr>,
comment: impl Into<Option<String>>,
) -> Self {
Self {
name: name.into(),
address: address.into(),
comment: comment.into(),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn address(&self) -> &Cidr {
&self.address
}
pub fn comment(&self) -> Option<&str> {
self.comment.as_deref()
}
}
impl FromStr for Alias {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (name, line) =
match_name(s.trim_start()).ok_or_else(|| format_err!("expected an alias name"))?;
let (address, line) = match_non_whitespace(line.trim_start())
.ok_or_else(|| format_err!("expected a value for alias {name:?}"))?;
let address: Cidr = address.parse()?;
let line = line.trim_start();
let comment = match line.strip_prefix('#') {
Some(comment) => Some(comment.trim().to_string()),
None if !line.is_empty() => bail!("trailing characters in alias: {line:?}"),
None => None,
};
Ok(Alias {
name: name.to_string(),
address,
comment,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_alias() {
for alias in [
"local_network 10.0.0.0/32",
"test-_123-___-a---- 10.0.0.1/32",
] {
alias.parse::<Alias>().expect("valid alias");
}
for alias in ["-- 10.0.0.1/32", "0asd 10.0.0.1/32", "__test 10.0.0.0/32"] {
alias.parse::<Alias>().expect_err("invalid alias");
}
}
#[test]
fn test_parse_alias_name() {
for name in ["dc/proxmox_123", "guest/proxmox-123"] {
name.parse::<AliasName>().expect("valid alias name");
}
for name in ["proxmox/proxmox_123", "guests/proxmox-123", "dc/", "/name"] {
name.parse::<AliasName>().expect_err("invalid alias name");
}
}
}

View File

@ -1,36 +0,0 @@
use anyhow::Error;
use crate::firewall::types::Rule;
#[derive(Debug)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Group {
rules: Vec<Rule>,
comment: Option<String>,
}
impl Group {
pub const fn new() -> Self {
Self {
rules: Vec::new(),
comment: None,
}
}
pub fn rules(&self) -> &Vec<Rule> {
&self.rules
}
pub fn comment(&self) -> Option<&str> {
self.comment.as_deref()
}
pub fn set_comment(&mut self, comment: Option<String>) {
self.comment = comment;
}
pub(crate) fn parse_entry(&mut self, line: &str) -> Result<(), Error> {
self.rules.push(line.parse()?);
Ok(())
}
}

View File

@ -1,349 +0,0 @@
use core::fmt::Display;
use std::ops::{Deref, DerefMut};
use std::str::FromStr;
use anyhow::{bail, format_err, Error};
use serde_with::DeserializeFromStr;
use crate::firewall::parse::match_non_whitespace;
use crate::firewall::types::address::Cidr;
use crate::firewall::types::alias::AliasName;
use crate::guest::vm::NetworkConfig;
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum IpsetScope {
Datacenter,
Guest,
}
impl FromStr for IpsetScope {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"+dc" => IpsetScope::Datacenter,
"+guest" => IpsetScope::Guest,
_ => bail!("invalid scope for ipset: {s}"),
})
}
}
impl Display for IpsetScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let prefix = match self {
Self::Datacenter => "dc",
Self::Guest => "guest",
};
f.write_str(prefix)
}
}
#[derive(Debug, Clone, DeserializeFromStr)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct IpsetName {
pub scope: IpsetScope,
pub name: String,
}
impl IpsetName {
pub fn new(scope: IpsetScope, name: impl Into<String>) -> Self {
Self {
scope,
name: name.into(),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn scope(&self) -> IpsetScope {
self.scope
}
}
impl FromStr for IpsetName {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.split_once('/') {
Some((prefix, name)) if !name.is_empty() => Ok(Self {
scope: prefix.parse()?,
name: name.to_string(),
}),
_ => {
bail!("Invalid IPSet name: {s}")
}
}
}
}
impl Display for IpsetName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}/{}", self.scope, self.name)
}
}
#[derive(Debug)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub enum IpsetAddress {
Alias(AliasName),
Cidr(Cidr),
}
impl FromStr for IpsetAddress {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
if let Ok(cidr) = s.parse() {
return Ok(IpsetAddress::Cidr(cidr));
}
if let Ok(name) = s.parse() {
return Ok(IpsetAddress::Alias(name));
}
bail!("Invalid address in IPSet: {s}")
}
}
impl<T: Into<Cidr>> From<T> for IpsetAddress {
fn from(cidr: T) -> Self {
IpsetAddress::Cidr(cidr.into())
}
}
#[derive(Debug)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct IpsetEntry {
pub nomatch: bool,
pub address: IpsetAddress,
pub comment: Option<String>,
}
impl<T: Into<IpsetAddress>> From<T> for IpsetEntry {
fn from(value: T) -> Self {
Self {
nomatch: false,
address: value.into(),
comment: None,
}
}
}
impl FromStr for IpsetEntry {
type Err = Error;
fn from_str(line: &str) -> Result<Self, Error> {
let line = line.trim_start();
let (nomatch, line) = match line.strip_prefix('!') {
Some(line) => (true, line),
None => (false, line),
};
let (address, line) =
match_non_whitespace(line.trim_start()).ok_or_else(|| format_err!("missing value"))?;
let address: IpsetAddress = address.parse()?;
let line = line.trim_start();
let comment = match line.strip_prefix('#') {
Some(comment) => Some(comment.trim().to_string()),
None if !line.is_empty() => bail!("trailing characters in ipset entry: {line:?}"),
None => None,
};
Ok(Self {
nomatch,
address,
comment,
})
}
}
#[derive(Debug)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Ipfilter<'a> {
index: i64,
ipset: &'a Ipset,
}
impl Ipfilter<'_> {
pub fn index(&self) -> i64 {
self.index
}
pub fn ipset(&self) -> &Ipset {
self.ipset
}
pub fn name_for_index(index: i64) -> String {
format!("ipfilter-net{index}")
}
}
#[derive(Debug)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Ipset {
pub name: IpsetName,
set: Vec<IpsetEntry>,
pub comment: Option<String>,
}
impl Ipset {
pub const fn new(name: IpsetName) -> Self {
Self {
name,
set: Vec::new(),
comment: None,
}
}
pub fn name(&self) -> &IpsetName {
&self.name
}
pub fn from_parts(scope: IpsetScope, name: impl Into<String>) -> Self {
Self::new(IpsetName::new(scope, name))
}
pub(crate) fn parse_entry(&mut self, line: &str) -> Result<(), Error> {
self.set.push(line.parse()?);
Ok(())
}
pub fn ipfilter(&self) -> Option<Ipfilter> {
if self.name.scope() != IpsetScope::Guest {
return None;
}
let name = self.name.name();
if let Some(key) = name.strip_prefix("ipfilter-") {
let id = NetworkConfig::index_from_net_key(key);
if let Ok(id) = id {
return Some(Ipfilter {
index: id,
ipset: self,
});
}
}
None
}
}
impl Deref for Ipset {
type Target = Vec<IpsetEntry>;
#[inline]
fn deref(&self) -> &Self::Target {
&self.set
}
}
impl DerefMut for Ipset {
#[inline]
fn deref_mut(&mut self) -> &mut Vec<IpsetEntry> {
&mut self.set
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_ipset_name() {
for test_case in [
("+dc/proxmox-123", IpsetScope::Datacenter, "proxmox-123"),
("+guest/proxmox_123", IpsetScope::Guest, "proxmox_123"),
] {
let ipset_name = test_case.0.parse::<IpsetName>().expect("valid ipset name");
assert_eq!(
ipset_name,
IpsetName {
scope: test_case.1,
name: test_case.2.to_string(),
}
)
}
for name in ["+dc/", "+guests/proxmox_123", "guest/proxmox_123"] {
name.parse::<IpsetName>().expect_err("invalid ipset name");
}
}
#[test]
fn test_parse_ipset_address() {
let mut ipset_address = "10.0.0.1"
.parse::<IpsetAddress>()
.expect("valid ipset address");
assert!(matches!(ipset_address, IpsetAddress::Cidr(Cidr::Ipv4(..))));
ipset_address = "fe80::1/64"
.parse::<IpsetAddress>()
.expect("valid ipset address");
assert!(matches!(ipset_address, IpsetAddress::Cidr(Cidr::Ipv6(..))));
ipset_address = "dc/proxmox-123"
.parse::<IpsetAddress>()
.expect("valid ipset address");
assert!(matches!(ipset_address, IpsetAddress::Alias(..)));
ipset_address = "guest/proxmox_123"
.parse::<IpsetAddress>()
.expect("valid ipset address");
assert!(matches!(ipset_address, IpsetAddress::Alias(..)));
}
#[test]
fn test_ipfilter() {
let mut ipset = Ipset::from_parts(IpsetScope::Guest, "ipfilter-net0");
ipset.ipfilter().expect("is an ipfilter");
ipset = Ipset::from_parts(IpsetScope::Guest, "ipfilter-qwe");
assert!(ipset.ipfilter().is_none());
ipset = Ipset::from_parts(IpsetScope::Guest, "proxmox");
assert!(ipset.ipfilter().is_none());
ipset = Ipset::from_parts(IpsetScope::Datacenter, "ipfilter-net0");
assert!(ipset.ipfilter().is_none());
}
#[test]
fn test_parse_ipset_entry() {
let mut entry = "!10.0.0.1 # qweqweasd"
.parse::<IpsetEntry>()
.expect("valid ipset entry");
assert_eq!(
entry,
IpsetEntry {
nomatch: true,
comment: Some("qweqweasd".to_string()),
address: IpsetAddress::Cidr(Cidr::new_v4([10, 0, 0, 1], 32).unwrap())
}
);
entry = "fe80::1/48"
.parse::<IpsetEntry>()
.expect("valid ipset entry");
assert_eq!(
entry,
IpsetEntry {
nomatch: false,
comment: None,
address: IpsetAddress::Cidr(
Cidr::new_v6([0xFE80, 0, 0, 0, 0, 0, 0, 1], 48).unwrap()
)
}
)
}
}

View File

@ -1,222 +0,0 @@
use std::fmt;
use std::str::FromStr;
use crate::firewall::parse::parse_bool;
use anyhow::{bail, Error};
use serde::{Deserialize, Serialize};
#[derive(Copy, Clone, Debug, Deserialize, Serialize, Default)]
#[cfg_attr(test, derive(Eq, PartialEq))]
#[serde(rename_all = "lowercase")]
pub enum LogRateLimitTimescale {
#[default]
Second,
Minute,
Hour,
Day,
}
impl FromStr for LogRateLimitTimescale {
type Err = Error;
fn from_str(str: &str) -> Result<Self, Error> {
match str {
"second" => Ok(LogRateLimitTimescale::Second),
"minute" => Ok(LogRateLimitTimescale::Minute),
"hour" => Ok(LogRateLimitTimescale::Hour),
"day" => Ok(LogRateLimitTimescale::Day),
_ => bail!("Invalid time scale provided"),
}
}
}
#[derive(Debug, Deserialize, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct LogRateLimit {
enabled: bool,
rate: i64, // in packets
per: LogRateLimitTimescale,
burst: i64, // in packets
}
impl LogRateLimit {
pub fn new(enabled: bool, rate: i64, per: LogRateLimitTimescale, burst: i64) -> Self {
Self {
enabled,
rate,
per,
burst,
}
}
pub fn enabled(&self) -> bool {
self.enabled
}
pub fn rate(&self) -> i64 {
self.rate
}
pub fn burst(&self) -> i64 {
self.burst
}
pub fn per(&self) -> LogRateLimitTimescale {
self.per
}
}
impl Default for LogRateLimit {
fn default() -> Self {
Self {
enabled: true,
rate: 1,
burst: 5,
per: LogRateLimitTimescale::Second,
}
}
}
impl FromStr for LogRateLimit {
type Err = Error;
fn from_str(str: &str) -> Result<Self, Error> {
let mut limit = Self::default();
for element in str.split(',') {
match element.split_once('=') {
None => {
limit.enabled = parse_bool(element)?;
}
Some((key, value)) if !key.is_empty() && !value.is_empty() => match key {
"enable" => limit.enabled = parse_bool(value)?,
"burst" => limit.burst = i64::from_str(value)?,
"rate" => match value.split_once('/') {
None => {
limit.rate = i64::from_str(value)?;
}
Some((rate, unit)) => {
if unit.is_empty() {
bail!("empty unit specification")
}
limit.rate = i64::from_str(rate)?;
limit.per = LogRateLimitTimescale::from_str(unit)?;
}
},
_ => bail!("Invalid value for Key found in log_ratelimit!"),
},
_ => bail!("invalid value in log_ratelimit"),
}
}
Ok(limit)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
pub enum LogLevel {
#[default]
Nolog,
Emergency,
Alert,
Critical,
Error,
Warning,
Notice,
Info,
Debug,
}
impl std::str::FromStr for LogLevel {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
Ok(match s {
"nolog" => LogLevel::Nolog,
"emerg" => LogLevel::Emergency,
"alert" => LogLevel::Alert,
"crit" => LogLevel::Critical,
"err" => LogLevel::Error,
"warn" => LogLevel::Warning,
"warning" => LogLevel::Warning,
"notice" => LogLevel::Notice,
"info" => LogLevel::Info,
"debug" => LogLevel::Debug,
_ => bail!("invalid log level {s:?}"),
})
}
}
impl fmt::Display for LogLevel {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(match self {
LogLevel::Nolog => "nolog",
LogLevel::Emergency => "emerg",
LogLevel::Alert => "alert",
LogLevel::Critical => "crit",
LogLevel::Error => "err",
LogLevel::Warning => "warn",
LogLevel::Notice => "notice",
LogLevel::Info => "info",
LogLevel::Debug => "debug",
})
}
}
serde_plain::derive_deserialize_from_fromstr!(LogLevel, "valid log level");
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_rate_limit() {
let mut parsed_rate_limit = "1,burst=123,rate=44"
.parse::<LogRateLimit>()
.expect("valid rate limit");
assert_eq!(
parsed_rate_limit,
LogRateLimit {
enabled: true,
burst: 123,
rate: 44,
per: LogRateLimitTimescale::Second,
}
);
parsed_rate_limit = "1".parse::<LogRateLimit>().expect("valid rate limit");
assert_eq!(parsed_rate_limit, LogRateLimit::default());
parsed_rate_limit = "enable=0,rate=123/hour"
.parse::<LogRateLimit>()
.expect("valid rate limit");
assert_eq!(
parsed_rate_limit,
LogRateLimit {
enabled: false,
burst: 5,
rate: 123,
per: LogRateLimitTimescale::Hour,
}
);
"2".parse::<LogRateLimit>()
.expect_err("invalid value for enable");
"enabled=0,rate=123"
.parse::<LogRateLimit>()
.expect_err("invalid key in log ratelimit");
"enable=0,rate=123,"
.parse::<LogRateLimit>()
.expect_err("trailing comma in log rate limit specification");
"enable=0,rate=123/proxmox,"
.parse::<LogRateLimit>()
.expect_err("invalid unit for rate");
}
}

View File

@ -1,14 +0,0 @@
pub mod address;
pub mod alias;
pub mod group;
pub mod ipset;
pub mod log;
pub mod port;
pub mod rule;
pub mod rule_match;
pub use address::Cidr;
pub use alias::Alias;
pub use group::Group;
pub use ipset::Ipset;
pub use rule::Rule;

View File

@ -1,181 +0,0 @@
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<u16, Error> {
if let Ok(port) = port.parse::<u16>() {
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<Self, Self::Err> {
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<u16> 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<Self, Error> {
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<PortEntry>);
impl FromIterator<PortEntry> for PortList {
fn from_iter<T: IntoIterator<Item = PortEntry>>(iter: T) -> Self {
Self(iter.into_iter().collect())
}
}
impl<T: Into<PortEntry>> From<T> for PortList {
fn from(value: T) -> Self {
Self(vec![value.into()])
}
}
impl Deref for PortList {
type Target = Vec<PortEntry>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::str::FromStr for PortList {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
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::<PortEntry>().unwrap_err();
"100:100000".parse::<PortEntry>().unwrap_err();
"qweasd".parse::<PortEntry>().unwrap_err();
"".parse::<PortEntry>().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::<PortList>().unwrap_err();
"0:1337,".parse::<PortList>().unwrap_err();
"70000".parse::<PortList>().unwrap_err();
"qweasd".parse::<PortList>().unwrap_err();
"".parse::<PortList>().unwrap_err();
}
}

View File

@ -1,412 +0,0 @@
use core::fmt::Display;
use std::fmt;
use std::str::FromStr;
use anyhow::{bail, ensure, format_err, Error};
use crate::firewall::parse::match_name;
use crate::firewall::types::rule_match::RuleMatch;
use crate::firewall::types::rule_match::RuleOptions;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum Direction {
#[default]
In,
Out,
}
impl std::str::FromStr for Direction {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
for (name, dir) in [("IN", Direction::In), ("OUT", Direction::Out)] {
if s.eq_ignore_ascii_case(name) {
return Ok(dir);
}
}
bail!("invalid direction: {s:?}, expect 'IN' or 'OUT'");
}
}
serde_plain::derive_deserialize_from_fromstr!(Direction, "valid packet direction");
impl fmt::Display for Direction {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Direction::In => f.write_str("in"),
Direction::Out => f.write_str("out"),
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum Verdict {
Accept,
Reject,
#[default]
Drop,
}
impl std::str::FromStr for Verdict {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
for (name, verdict) in [
("ACCEPT", Verdict::Accept),
("REJECT", Verdict::Reject),
("DROP", Verdict::Drop),
] {
if s.eq_ignore_ascii_case(name) {
return Ok(verdict);
}
}
bail!("invalid verdict {s:?}, expected one of 'ACCEPT', 'REJECT' or 'DROP'");
}
}
impl Display for Verdict {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let string = match self {
Verdict::Accept => "ACCEPT",
Verdict::Drop => "DROP",
Verdict::Reject => "REJECT",
};
write!(f, "{string}")
}
}
serde_plain::derive_deserialize_from_fromstr!(Verdict, "valid verdict");
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Rule {
pub(crate) disabled: bool,
pub(crate) kind: Kind,
pub(crate) comment: Option<String>,
}
impl std::ops::Deref for Rule {
type Target = Kind;
fn deref(&self) -> &Self::Target {
&self.kind
}
}
impl std::ops::DerefMut for Rule {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.kind
}
}
impl FromStr for Rule {
type Err = Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
if input.contains(['\n', '\r']) {
bail!("rule must not contain any newlines!");
}
let (line, comment) = match input.rsplit_once('#') {
Some((line, comment)) if !comment.is_empty() => (line.trim(), Some(comment.trim())),
_ => (input.trim(), None),
};
let (disabled, line) = match line.strip_prefix('|') {
Some(line) => (true, line.trim_start()),
None => (false, line),
};
// todo: case insensitive?
let kind = if line.starts_with("GROUP") {
Kind::from(line.parse::<RuleGroup>()?)
} else {
Kind::from(line.parse::<RuleMatch>()?)
};
Ok(Self {
disabled,
comment: comment.map(str::to_string),
kind,
})
}
}
impl Rule {
pub fn iface(&self) -> Option<&str> {
match &self.kind {
Kind::Group(group) => group.iface(),
Kind::Match(rule) => rule.iface(),
}
}
pub fn disabled(&self) -> bool {
self.disabled
}
pub fn kind(&self) -> &Kind {
&self.kind
}
pub fn comment(&self) -> Option<&str> {
self.comment.as_deref()
}
}
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub enum Kind {
Group(RuleGroup),
Match(RuleMatch),
}
impl Kind {
pub fn is_group(&self) -> bool {
matches!(self, Kind::Group(_))
}
pub fn is_match(&self) -> bool {
matches!(self, Kind::Match(_))
}
}
impl From<RuleGroup> for Kind {
fn from(value: RuleGroup) -> Self {
Kind::Group(value)
}
}
impl From<RuleMatch> for Kind {
fn from(value: RuleMatch) -> Self {
Kind::Match(value)
}
}
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct RuleGroup {
pub(crate) group: String,
pub(crate) iface: Option<String>,
}
impl RuleGroup {
pub(crate) fn from_options(group: String, options: RuleOptions) -> Result<Self, Error> {
ensure!(
options.proto.is_none()
&& options.dport.is_none()
&& options.sport.is_none()
&& options.dest.is_none()
&& options.source.is_none()
&& options.log.is_none()
&& options.icmp_type.is_none(),
"only interface parameter is permitted for group rules"
);
Ok(Self {
group,
iface: options.iface,
})
}
pub fn group(&self) -> &str {
&self.group
}
pub fn iface(&self) -> Option<&str> {
self.iface.as_deref()
}
}
impl FromStr for RuleGroup {
type Err = Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let (keyword, rest) = match_name(input)
.ok_or_else(|| format_err!("expected a leading keyword in rule group"))?;
if !keyword.eq_ignore_ascii_case("group") {
bail!("Expected keyword GROUP")
}
let (name, rest) =
match_name(rest.trim()).ok_or_else(|| format_err!("expected a name for rule group"))?;
let options = rest.trim_start().parse()?;
Self::from_options(name.to_string(), options)
}
}
#[cfg(test)]
mod tests {
use crate::firewall::types::{
address::{IpEntry, IpList},
alias::{AliasName, AliasScope},
ipset::{IpsetName, IpsetScope},
log::LogLevel,
rule_match::{Icmp, IcmpCode, IpAddrMatch, IpMatch, Ports, Protocol, Udp},
Cidr,
};
use super::*;
#[test]
fn test_parse_rule() {
let mut rule: Rule = "|GROUP tgr -i eth0 # acomm".parse().expect("valid rule");
assert_eq!(
rule,
Rule {
disabled: true,
comment: Some("acomm".to_string()),
kind: Kind::Group(RuleGroup {
group: "tgr".to_string(),
iface: Some("eth0".to_string()),
}),
},
);
rule = "IN ACCEPT -p udp -dport 33 -sport 22 -log warning"
.parse()
.expect("valid rule");
assert_eq!(
rule,
Rule {
disabled: false,
comment: None,
kind: Kind::Match(RuleMatch {
dir: Direction::In,
verdict: Verdict::Accept,
proto: Some(Udp::new(Ports::from_u16(22, 33)).into()),
log: Some(LogLevel::Warning),
..Default::default()
}),
}
);
rule = "IN ACCEPT --proto udp -i eth0".parse().expect("valid rule");
assert_eq!(
rule,
Rule {
disabled: false,
comment: None,
kind: Kind::Match(RuleMatch {
dir: Direction::In,
verdict: Verdict::Accept,
proto: Some(Udp::new(Ports::new(None, None)).into()),
iface: Some("eth0".to_string()),
..Default::default()
}),
}
);
rule = " OUT DROP \
-source 10.0.0.0/24 -dest 20.0.0.0-20.255.255.255,192.168.0.0/16 \
-p icmp -log nolog -icmp-type port-unreachable "
.parse()
.expect("valid rule");
assert_eq!(
rule,
Rule {
disabled: false,
comment: None,
kind: Kind::Match(RuleMatch {
dir: Direction::Out,
verdict: Verdict::Drop,
ip: IpMatch::new(
IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 24).unwrap())),
IpAddrMatch::Ip(
IpList::new(vec![
IpEntry::Range([20, 0, 0, 0].into(), [20, 255, 255, 255].into()),
IpEntry::Cidr(Cidr::new_v4([192, 168, 0, 0], 16).unwrap()),
])
.unwrap()
),
)
.ok(),
proto: Some(Protocol::Icmp(Icmp::new_code(IcmpCode::Named(
"port-unreachable"
)))),
log: Some(LogLevel::Nolog),
..Default::default()
}),
}
);
rule = "IN BGP(ACCEPT) --log crit --iface eth0"
.parse()
.expect("valid rule");
assert_eq!(
rule,
Rule {
disabled: false,
comment: None,
kind: Kind::Match(RuleMatch {
dir: Direction::In,
verdict: Verdict::Accept,
log: Some(LogLevel::Critical),
fw_macro: Some("BGP".to_string()),
iface: Some("eth0".to_string()),
..Default::default()
}),
}
);
rule = "IN ACCEPT --source dc/test --dest +dc/test"
.parse()
.expect("valid rule");
assert_eq!(
rule,
Rule {
disabled: false,
comment: None,
kind: Kind::Match(RuleMatch {
dir: Direction::In,
verdict: Verdict::Accept,
ip: Some(
IpMatch::new(
IpAddrMatch::Alias(AliasName::new(AliasScope::Datacenter, "test")),
IpAddrMatch::Set(IpsetName::new(IpsetScope::Datacenter, "test"),),
)
.unwrap()
),
..Default::default()
}),
}
);
rule = "IN REJECT".parse().expect("valid rule");
assert_eq!(
rule,
Rule {
disabled: false,
comment: None,
kind: Kind::Match(RuleMatch {
dir: Direction::In,
verdict: Verdict::Reject,
..Default::default()
}),
}
);
"IN DROP ---log crit"
.parse::<Rule>()
.expect_err("too many dashes in option");
"IN DROP --log --iface eth0"
.parse::<Rule>()
.expect_err("no value for option");
"IN DROP --log crit --iface"
.parse::<Rule>()
.expect_err("no value for option");
}
}

View File

@ -1,977 +0,0 @@
use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
use serde::Deserialize;
use anyhow::{bail, format_err, Error};
use serde::de::IntoDeserializer;
use proxmox_sortable_macro::sortable;
use crate::firewall::parse::{match_name, match_non_whitespace, SomeStr};
use crate::firewall::types::address::{Family, IpList};
use crate::firewall::types::alias::AliasName;
use crate::firewall::types::ipset::IpsetName;
use crate::firewall::types::log::LogLevel;
use crate::firewall::types::port::PortList;
use crate::firewall::types::rule::{Direction, Verdict};
#[derive(Clone, Debug, Default, Deserialize)]
#[cfg_attr(test, derive(Eq, PartialEq))]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub(crate) struct RuleOptions {
#[serde(alias = "p")]
pub(crate) proto: Option<String>,
pub(crate) dport: Option<String>,
pub(crate) sport: Option<String>,
pub(crate) dest: Option<String>,
pub(crate) source: Option<String>,
#[serde(alias = "i")]
pub(crate) iface: Option<String>,
pub(crate) log: Option<LogLevel>,
pub(crate) icmp_type: Option<String>,
}
impl FromStr for RuleOptions {
type Err = Error;
fn from_str(mut line: &str) -> Result<Self, Self::Err> {
let mut options = HashMap::new();
loop {
line = line.trim_start();
if line.is_empty() {
break;
}
line = line
.strip_prefix('-')
.ok_or_else(|| format_err!("expected an option starting with '-'"))?;
// second dash is optional
line = line.strip_prefix('-').unwrap_or(line);
let param;
(param, line) = match_name(line)
.ok_or_else(|| format_err!("expected a parameter name after '-'"))?;
let value;
(value, line) = match_non_whitespace(line.trim_start())
.ok_or_else(|| format_err!("expected a value for {param:?}"))?;
if options.insert(param, SomeStr(value)).is_some() {
bail!("duplicate option in rule: {param}")
}
}
Ok(RuleOptions::deserialize(IntoDeserializer::<
'_,
crate::firewall::parse::SerdeStringError,
>::into_deserializer(
options
))?)
}
}
#[derive(Clone, Debug, Default)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct RuleMatch {
pub(crate) dir: Direction,
pub(crate) verdict: Verdict,
pub(crate) fw_macro: Option<String>,
pub(crate) iface: Option<String>,
pub(crate) log: Option<LogLevel>,
pub(crate) ip: Option<IpMatch>,
pub(crate) proto: Option<Protocol>,
}
impl RuleMatch {
pub(crate) fn from_options(
dir: Direction,
verdict: Verdict,
fw_macro: impl Into<Option<String>>,
options: RuleOptions,
) -> Result<Self, Error> {
if options.dport.is_some() && options.icmp_type.is_some() {
bail!("dport and icmp-type are mutually exclusive");
}
let ip = IpMatch::from_options(&options)?;
let proto = Protocol::from_options(&options)?;
// todo: check protocol & IP Version compatibility
Ok(Self {
dir,
verdict,
fw_macro: fw_macro.into(),
iface: options.iface,
log: options.log,
ip,
proto,
})
}
pub fn direction(&self) -> Direction {
self.dir
}
pub fn iface(&self) -> Option<&str> {
self.iface.as_deref()
}
pub fn verdict(&self) -> Verdict {
self.verdict
}
pub fn fw_macro(&self) -> Option<&str> {
self.fw_macro.as_deref()
}
pub fn log(&self) -> Option<LogLevel> {
self.log
}
pub fn ip(&self) -> Option<&IpMatch> {
self.ip.as_ref()
}
pub fn proto(&self) -> Option<&Protocol> {
self.proto.as_ref()
}
}
/// Returns `(Macro name, Verdict, RestOfTheLine)`.
fn parse_action(line: &str) -> Result<(Option<&str>, Verdict, &str), Error> {
let (verdict, line) =
match_name(line).ok_or_else(|| format_err!("expected a verdict or macro name"))?;
Ok(if let Some(line) = line.strip_prefix('(') {
// <macro>(<verdict>)
let macro_name = verdict;
let (verdict, line) = match_name(line).ok_or_else(|| format_err!("expected a verdict"))?;
let line = line
.strip_prefix(')')
.ok_or_else(|| format_err!("expected closing ')' after verdict"))?;
let verdict: Verdict = verdict.parse()?;
(Some(macro_name), verdict, line.trim_start())
} else {
(None, verdict.parse()?, line.trim_start())
})
}
impl FromStr for RuleMatch {
type Err = Error;
fn from_str(line: &str) -> Result<Self, Self::Err> {
let (dir, rest) = match_name(line).ok_or_else(|| format_err!("expected a direction"))?;
let direction: Direction = dir.parse()?;
let (fw_macro, verdict, rest) = parse_action(rest.trim_start())?;
let options: RuleOptions = rest.trim_start().parse()?;
Self::from_options(direction, verdict, fw_macro.map(str::to_string), options)
}
}
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct IpMatch {
pub(crate) src: Option<IpAddrMatch>,
pub(crate) dst: Option<IpAddrMatch>,
}
impl IpMatch {
pub fn new(
src: impl Into<Option<IpAddrMatch>>,
dst: impl Into<Option<IpAddrMatch>>,
) -> Result<Self, Error> {
let source = src.into();
let dest = dst.into();
if source.is_none() && dest.is_none() {
bail!("either src or dst must be set")
}
if let (Some(IpAddrMatch::Ip(src)), Some(IpAddrMatch::Ip(dst))) = (&source, &dest) {
if src.family() != dst.family() {
bail!("src and dst family must be equal")
}
}
let ip_match = Self {
src: source,
dst: dest,
};
Ok(ip_match)
}
fn from_options(options: &RuleOptions) -> Result<Option<Self>, Error> {
let src = options
.source
.as_ref()
.map(|elem| elem.parse::<IpAddrMatch>())
.transpose()?;
let dst = options
.dest
.as_ref()
.map(|elem| elem.parse::<IpAddrMatch>())
.transpose()?;
if src.is_some() || dst.is_some() {
Ok(Some(IpMatch::new(src, dst)?))
} else {
Ok(None)
}
}
pub fn src(&self) -> Option<&IpAddrMatch> {
self.src.as_ref()
}
pub fn dst(&self) -> Option<&IpAddrMatch> {
self.dst.as_ref()
}
}
#[derive(Clone, Debug, Deserialize)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub enum IpAddrMatch {
Ip(IpList),
Set(IpsetName),
Alias(AliasName),
}
impl IpAddrMatch {
pub fn family(&self) -> Option<Family> {
if let IpAddrMatch::Ip(list) = self {
return Some(list.family());
}
None
}
}
impl FromStr for IpAddrMatch {
type Err = Error;
fn from_str(value: &str) -> Result<Self, Error> {
if value.is_empty() {
bail!("empty IP specification");
}
if let Ok(ip_list) = value.parse() {
return Ok(IpAddrMatch::Ip(ip_list));
}
if let Ok(ipset) = value.parse() {
return Ok(IpAddrMatch::Set(ipset));
}
if let Ok(name) = value.parse() {
return Ok(IpAddrMatch::Alias(name));
}
bail!("invalid IP specification: {value}")
}
}
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub enum Protocol {
Dccp(Ports),
Sctp(Sctp),
Tcp(Tcp),
Udp(Udp),
UdpLite(Ports),
Icmp(Icmp),
Icmpv6(Icmpv6),
Named(String),
Numeric(u8),
}
impl Protocol {
pub(crate) fn from_options(options: &RuleOptions) -> Result<Option<Self>, Error> {
let proto = match options.proto.as_deref() {
Some(p) => p,
None => return Ok(None),
};
Ok(Some(match proto {
"dccp" | "33" => Protocol::Dccp(Ports::from_options(options)?),
"sctp" | "132" => Protocol::Sctp(Sctp::from_options(options)?),
"tcp" | "6" => Protocol::Tcp(Tcp::from_options(options)?),
"udp" | "17" => Protocol::Udp(Udp::from_options(options)?),
"udplite" | "136" => Protocol::UdpLite(Ports::from_options(options)?),
"icmp" | "1" => Protocol::Icmp(Icmp::from_options(options)?),
"ipv6-icmp" | "icmpv6" | "58" => Protocol::Icmpv6(Icmpv6::from_options(options)?),
other => match other.parse::<u8>() {
Ok(num) => Protocol::Numeric(num),
Err(_) => Protocol::Named(other.to_string()),
},
}))
}
pub fn family(&self) -> Option<Family> {
match self {
Self::Icmp(_) => Some(Family::V4),
Self::Icmpv6(_) => Some(Family::V6),
_ => None,
}
}
}
#[derive(Clone, Debug, Default)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Udp {
ports: Ports,
}
impl Udp {
fn from_options(options: &RuleOptions) -> Result<Self, Error> {
Ok(Self {
ports: Ports::from_options(options)?,
})
}
pub fn new(ports: Ports) -> Self {
Self { ports }
}
pub fn ports(&self) -> &Ports {
&self.ports
}
}
impl From<Udp> for Protocol {
fn from(value: Udp) -> Self {
Protocol::Udp(value)
}
}
#[derive(Clone, Debug, Default)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Ports {
sport: Option<PortList>,
dport: Option<PortList>,
}
impl Ports {
pub fn new(sport: impl Into<Option<PortList>>, dport: impl Into<Option<PortList>>) -> Self {
Self {
sport: sport.into(),
dport: dport.into(),
}
}
fn from_options(options: &RuleOptions) -> Result<Self, Error> {
Ok(Self {
sport: options.sport.as_deref().map(|s| s.parse()).transpose()?,
dport: options.dport.as_deref().map(|s| s.parse()).transpose()?,
})
}
pub fn from_u16(sport: impl Into<Option<u16>>, dport: impl Into<Option<u16>>) -> Self {
Self::new(
sport.into().map(PortList::from),
dport.into().map(PortList::from),
)
}
pub fn sport(&self) -> Option<&PortList> {
self.sport.as_ref()
}
pub fn dport(&self) -> Option<&PortList> {
self.dport.as_ref()
}
}
#[derive(Clone, Debug, Default)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Tcp {
ports: Ports,
}
impl Tcp {
pub fn new(ports: Ports) -> Self {
Self { ports }
}
fn from_options(options: &RuleOptions) -> Result<Self, Error> {
Ok(Self {
ports: Ports::from_options(options)?,
})
}
pub fn ports(&self) -> &Ports {
&self.ports
}
}
impl From<Tcp> for Protocol {
fn from(value: Tcp) -> Self {
Protocol::Tcp(value)
}
}
#[derive(Clone, Debug, Default)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Sctp {
ports: Ports,
}
impl Sctp {
fn from_options(options: &RuleOptions) -> Result<Self, Error> {
Ok(Self {
ports: Ports::from_options(options)?,
})
}
pub fn ports(&self) -> &Ports {
&self.ports
}
}
#[derive(Clone, Debug, Default)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Icmp {
ty: Option<IcmpType>,
code: Option<IcmpCode>,
}
impl Icmp {
pub fn new_ty(ty: IcmpType) -> Self {
Self {
ty: Some(ty),
..Default::default()
}
}
pub fn new_code(code: IcmpCode) -> Self {
Self {
code: Some(code),
..Default::default()
}
}
fn from_options(options: &RuleOptions) -> Result<Self, Error> {
if let Some(ty) = &options.icmp_type {
return ty.parse();
}
Ok(Self::default())
}
pub fn ty(&self) -> Option<&IcmpType> {
self.ty.as_ref()
}
pub fn code(&self) -> Option<&IcmpCode> {
self.code.as_ref()
}
}
impl FromStr for Icmp {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut this = Self::default();
if let Ok(ty) = s.parse() {
this.ty = Some(ty);
return Ok(this);
}
if let Ok(code) = s.parse() {
this.code = Some(code);
return Ok(this);
}
bail!("supplied string is neither a valid icmp type nor code");
}
}
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub enum IcmpType {
Numeric(u8),
Named(&'static str),
Any,
}
#[sortable]
const ICMP_TYPES: [(&str, u8); 15] = sorted!([
("address-mask-reply", 18),
("address-mask-request", 17),
("destination-unreachable", 3),
("echo-reply", 0),
("echo-request", 8),
("info-reply", 16),
("info-request", 15),
("parameter-problem", 12),
("redirect", 5),
("router-advertisement", 9),
("router-solicitation", 10),
("source-quench", 4),
("time-exceeded", 11),
("timestamp-reply", 14),
("timestamp-request", 13),
]);
impl std::str::FromStr for IcmpType {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
if s.eq_ignore_ascii_case("any") {
return Ok(Self::Any);
}
if let Ok(ty) = s.trim().parse::<u8>() {
return Ok(Self::Numeric(ty));
}
if let Ok(index) = ICMP_TYPES.binary_search_by(|v| v.0.cmp(s)) {
return Ok(Self::Named(ICMP_TYPES[index].0));
}
bail!("{s:?} is not a valid icmp type");
}
}
impl fmt::Display for IcmpType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
IcmpType::Numeric(ty) => write!(f, "{ty}"),
IcmpType::Named(ty) => write!(f, "{ty}"),
IcmpType::Any => write!(f, "any"),
}
}
}
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub enum IcmpCode {
Numeric(u8),
Named(&'static str),
}
#[sortable]
const ICMP_CODES: [(&str, u8); 7] = sorted!([
("admin-prohibited", 13),
("host-prohibited", 10),
("host-unreachable", 1),
("net-prohibited", 9),
("net-unreachable", 0),
("port-unreachable", 3),
("prot-unreachable", 2),
]);
impl std::str::FromStr for IcmpCode {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
if let Ok(code) = s.trim().parse::<u8>() {
return Ok(Self::Numeric(code));
}
if let Ok(index) = ICMP_CODES.binary_search_by(|v| v.0.cmp(s)) {
return Ok(Self::Named(ICMP_CODES[index].0));
}
bail!("{s:?} is not a valid icmp code");
}
}
impl fmt::Display for IcmpCode {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
IcmpCode::Numeric(code) => write!(f, "{code}"),
IcmpCode::Named(code) => write!(f, "{code}"),
}
}
}
#[derive(Clone, Debug, Default)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Icmpv6 {
pub ty: Option<Icmpv6Type>,
pub code: Option<Icmpv6Code>,
}
impl Icmpv6 {
pub fn new_ty(ty: Icmpv6Type) -> Self {
Self {
ty: Some(ty),
..Default::default()
}
}
pub fn new_code(code: Icmpv6Code) -> Self {
Self {
code: Some(code),
..Default::default()
}
}
fn from_options(options: &RuleOptions) -> Result<Self, Error> {
if let Some(ty) = &options.icmp_type {
return ty.parse();
}
Ok(Self::default())
}
pub fn ty(&self) -> Option<&Icmpv6Type> {
self.ty.as_ref()
}
pub fn code(&self) -> Option<&Icmpv6Code> {
self.code.as_ref()
}
}
impl FromStr for Icmpv6 {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut this = Self::default();
if let Ok(ty) = s.parse() {
this.ty = Some(ty);
return Ok(this);
}
if let Ok(code) = s.parse() {
this.code = Some(code);
return Ok(this);
}
bail!("supplied string is neither a valid icmpv6 type nor code");
}
}
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub enum Icmpv6Type {
Numeric(u8),
Named(&'static str),
Any,
}
#[sortable]
const ICMPV6_TYPES: [(&str, u8); 19] = sorted!([
("destination-unreachable", 1),
("echo-reply", 129),
("echo-request", 128),
("ind-neighbor-advert", 142),
("ind-neighbor-solicit", 141),
("mld-listener-done", 132),
("mld-listener-query", 130),
("mld-listener-reduction", 132),
("mld-listener-report", 131),
("mld2-listener-report", 143),
("nd-neighbor-advert", 136),
("nd-neighbor-solicit", 135),
("nd-redirect", 137),
("nd-router-advert", 134),
("nd-router-solicit", 133),
("packet-too-big", 2),
("parameter-problem", 4),
("router-renumbering", 138),
("time-exceeded", 3),
]);
impl std::str::FromStr for Icmpv6Type {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
if s.eq_ignore_ascii_case("any") {
return Ok(Self::Any);
}
if let Ok(ty) = s.trim().parse::<u8>() {
return Ok(Self::Numeric(ty));
}
if let Ok(index) = ICMPV6_TYPES.binary_search_by(|v| v.0.cmp(s)) {
return Ok(Self::Named(ICMPV6_TYPES[index].0));
}
bail!("{s:?} is not a valid icmpv6 type");
}
}
impl fmt::Display for Icmpv6Type {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Icmpv6Type::Numeric(ty) => write!(f, "{ty}"),
Icmpv6Type::Named(ty) => write!(f, "{ty}"),
Icmpv6Type::Any => write!(f, "any"),
}
}
}
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub enum Icmpv6Code {
Numeric(u8),
Named(&'static str),
}
#[sortable]
const ICMPV6_CODES: [(&str, u8); 6] = sorted!([
("addr-unreachable", 3),
("admin-prohibited", 1),
("no-route", 0),
("policy-fail", 5),
("port-unreachable", 4),
("reject-route", 6),
]);
impl std::str::FromStr for Icmpv6Code {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
if let Ok(code) = s.trim().parse::<u8>() {
return Ok(Self::Numeric(code));
}
if let Ok(index) = ICMPV6_CODES.binary_search_by(|v| v.0.cmp(s)) {
return Ok(Self::Named(ICMPV6_CODES[index].0));
}
bail!("{s:?} is not a valid icmpv6 code");
}
}
impl fmt::Display for Icmpv6Code {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Icmpv6Code::Numeric(code) => write!(f, "{code}"),
Icmpv6Code::Named(code) => write!(f, "{code}"),
}
}
}
#[cfg(test)]
mod tests {
use crate::firewall::types::{alias::AliasScope::Guest, Cidr};
use super::*;
#[test]
fn test_parse_action() {
assert_eq!(parse_action("REJECT").unwrap(), (None, Verdict::Reject, ""));
assert_eq!(
parse_action("SSH(ACCEPT) qweasd").unwrap(),
(Some("SSH"), Verdict::Accept, "qweasd")
);
}
#[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::<IpAddrMatch>().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",
"dcc/test",
"+guest/",
"",
] {
input.parse::<IpAddrMatch>().expect_err("invalid ip match");
}
}
#[test]
fn test_parse_options() {
let mut options: RuleOptions =
"-p udp --sport 123 --dport 234 -source 127.0.0.1 --dest 127.0.0.1 -i ens1 --log crit"
.parse()
.expect("valid option string");
assert_eq!(
options,
RuleOptions {
proto: Some("udp".to_string()),
sport: Some("123".to_string()),
dport: Some("234".to_string()),
source: Some("127.0.0.1".to_string()),
dest: Some("127.0.0.1".to_string()),
iface: Some("ens1".to_string()),
log: Some(LogLevel::Critical),
icmp_type: None,
}
);
options = "".parse().expect("valid option string");
assert_eq!(options, RuleOptions::default(),);
}
#[test]
fn test_construct_ip_match() {
IpMatch::new(
IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 8).unwrap())),
IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 8).unwrap())),
)
.expect("valid ip match");
IpMatch::new(
IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 8).unwrap())),
IpAddrMatch::Alias(AliasName::new(Guest, "test")),
)
.expect("valid ip match");
IpMatch::new(
IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 8).unwrap())),
IpAddrMatch::Ip(IpList::from(Cidr::new_v6([0x0000; 8], 8).unwrap())),
)
.expect_err("cannot mix ip families");
IpMatch::new(None, None).expect_err("at least one ip must be set");
}
#[test]
fn test_from_options() {
let mut options = RuleOptions {
proto: Some("tcp".to_string()),
sport: Some("123".to_string()),
dport: Some("234".to_string()),
source: Some("192.168.0.1".to_string()),
dest: Some("10.0.0.1".to_string()),
iface: Some("eth123".to_string()),
log: Some(LogLevel::Error),
..Default::default()
};
assert_eq!(
Protocol::from_options(&options).unwrap().unwrap(),
Protocol::Tcp(Tcp::new(Ports::from_u16(123, 234))),
);
assert_eq!(
IpMatch::from_options(&options).unwrap().unwrap(),
IpMatch::new(
IpAddrMatch::Ip(IpList::from(Cidr::new_v4([192, 168, 0, 1], 32).unwrap()),),
IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 1], 32).unwrap()),)
)
.unwrap(),
);
options = RuleOptions::default();
assert_eq!(Protocol::from_options(&options).unwrap(), None,);
assert_eq!(IpMatch::from_options(&options).unwrap(), None,);
options = RuleOptions {
proto: Some("tcp".to_string()),
sport: Some("qwe".to_string()),
source: Some("qwe".to_string()),
..Default::default()
};
Protocol::from_options(&options).expect_err("invalid source port");
IpMatch::from_options(&options).expect_err("invalid source address");
options = RuleOptions {
icmp_type: Some("port-unreachable".to_string()),
dport: Some("123".to_string()),
..Default::default()
};
RuleMatch::from_options(Direction::In, Verdict::Drop, None, options)
.expect_err("cannot mix dport and icmp-type");
}
#[test]
fn test_parse_icmp() {
let mut icmp: Icmp = "info-request".parse().expect("valid icmp type");
assert_eq!(
icmp,
Icmp {
ty: Some(IcmpType::Named("info-request")),
code: None
}
);
icmp = "12".parse().expect("valid icmp type");
assert_eq!(
icmp,
Icmp {
ty: Some(IcmpType::Numeric(12)),
code: None
}
);
icmp = "port-unreachable".parse().expect("valid icmp code");
assert_eq!(
icmp,
Icmp {
ty: None,
code: Some(IcmpCode::Named("port-unreachable"))
}
);
}
#[test]
fn test_parse_icmp6() {
let mut icmp: Icmpv6 = "echo-reply".parse().expect("valid icmpv6 type");
assert_eq!(
icmp,
Icmpv6 {
ty: Some(Icmpv6Type::Named("echo-reply")),
code: None
}
);
icmp = "12".parse().expect("valid icmpv6 type");
assert_eq!(
icmp,
Icmpv6 {
ty: Some(Icmpv6Type::Numeric(12)),
code: None
}
);
icmp = "admin-prohibited".parse().expect("valid icmpv6 code");
assert_eq!(
icmp,
Icmpv6 {
ty: None,
code: Some(Icmpv6Code::Named("admin-prohibited"))
}
);
}
}

View File

@ -1,115 +0,0 @@
use core::ops::Deref;
use std::collections::HashMap;
use anyhow::{Context, Error};
use serde::Deserialize;
use proxmox_sys::nodename;
use types::Vmid;
pub mod types;
pub mod vm;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize)]
pub enum GuestType {
#[serde(rename = "qemu")]
Vm,
#[serde(rename = "lxc")]
Ct,
}
impl GuestType {
pub fn iface_prefix(self) -> &'static str {
match self {
GuestType::Vm => "tap",
GuestType::Ct => "veth",
}
}
fn config_folder(&self) -> &'static str {
match self {
GuestType::Vm => "qemu-server",
GuestType::Ct => "lxc",
}
}
}
#[derive(Deserialize)]
pub struct GuestEntry {
node: String,
#[serde(rename = "type")]
ty: GuestType,
#[serde(rename = "version")]
_version: usize,
}
impl GuestEntry {
pub fn new(node: String, ty: GuestType) -> Self {
Self {
node,
ty,
_version: Default::default(),
}
}
pub fn is_local(&self) -> bool {
nodename() == self.node
}
pub fn ty(&self) -> &GuestType {
&self.ty
}
}
const VMLIST_CONFIG_PATH: &str = "/etc/pve/.vmlist";
#[derive(Deserialize)]
pub struct GuestMap {
#[serde(rename = "version")]
_version: usize,
#[serde(rename = "ids", default)]
guests: HashMap<Vmid, GuestEntry>,
}
impl From<HashMap<Vmid, GuestEntry>> for GuestMap {
fn from(guests: HashMap<Vmid, GuestEntry>) -> Self {
Self {
guests,
_version: Default::default(),
}
}
}
impl Deref for GuestMap {
type Target = HashMap<Vmid, GuestEntry>;
fn deref(&self) -> &Self::Target {
&self.guests
}
}
impl GuestMap {
pub fn new() -> Result<Self, Error> {
let data = std::fs::read(VMLIST_CONFIG_PATH)
.with_context(|| format!("failed to read guest map from {VMLIST_CONFIG_PATH}"))?;
serde_json::from_slice(&data).with_context(|| "failed to parse guest map".to_owned())
}
pub fn firewall_config_path(vmid: &Vmid) -> String {
format!("/etc/pve/firewall/{}.fw", vmid)
}
/// returns the local configuration path for a given Vmid.
///
/// The caller must ensure that the given Vmid exists and is local to the node
pub fn config_path(vmid: &Vmid, entry: &GuestEntry) -> String {
format!(
"/etc/pve/local/{}/{}.conf",
entry.ty().config_folder(),
vmid
)
}
}

View File

@ -1,38 +0,0 @@
use std::fmt;
use std::str::FromStr;
use anyhow::{format_err, Error};
#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)]
pub struct Vmid(u32);
impl Vmid {
pub fn new(id: u32) -> Self {
Vmid(id)
}
}
impl From<u32> for Vmid {
fn from(value: u32) -> Self {
Self::new(value)
}
}
impl fmt::Display for Vmid {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl FromStr for Vmid {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(
s.parse()
.map_err(|_| format_err!("not a valid vmid: {s:?}"))?,
))
}
}
serde_plain::derive_deserialize_from_fromstr!(Vmid, "valid vmid");

View File

@ -1,510 +0,0 @@
use anyhow::{bail, Error};
use core::fmt::Display;
use std::io;
use std::str::FromStr;
use std::{collections::HashMap, net::Ipv6Addr};
use proxmox_schema::property_string::PropertyIterator;
use crate::firewall::parse::{match_digits, parse_bool};
use crate::firewall::types::address::{Ipv4Cidr, Ipv6Cidr};
#[derive(Debug)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct MacAddress([u8; 6]);
static LOCAL_PART: [u8; 8] = [0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
static EUI64_MIDDLE_PART: [u8; 2] = [0xFF, 0xFE];
impl MacAddress {
/// generates a link local IPv6-address according to RFC 4291 (Appendix A)
pub fn eui64_link_local_address(&self) -> Ipv6Addr {
let head = &self.0[..3];
let tail = &self.0[3..];
let mut eui64_address: Vec<u8> = LOCAL_PART
.iter()
.chain(head.iter())
.chain(EUI64_MIDDLE_PART.iter())
.chain(tail.iter())
.copied()
.collect();
// we need to flip the 7th bit of the first eui64 byte
eui64_address[8] ^= 0x02;
Ipv6Addr::from(
TryInto::<[u8; 16]>::try_into(eui64_address).expect("is an u8 array with 16 entries"),
)
}
}
impl FromStr for MacAddress {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let split = s.split(':');
let parsed = split
.into_iter()
.map(|elem| u8::from_str_radix(elem, 16))
.collect::<Result<Vec<u8>, _>>()
.map_err(Error::msg)?;
if parsed.len() != 6 {
bail!("Invalid amount of elements in MAC address!");
}
let address = &parsed.as_slice()[0..6];
Ok(Self(address.try_into().unwrap()))
}
}
impl Display for MacAddress {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{:<02X}:{:<02X}:{:<02X}:{:<02X}:{:<02X}:{:<02X}",
self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5]
)
}
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub enum NetworkDeviceModel {
VirtIO,
Veth,
E1000,
Vmxnet3,
RTL8139,
}
impl FromStr for NetworkDeviceModel {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"virtio" => Ok(NetworkDeviceModel::VirtIO),
"e1000" => Ok(NetworkDeviceModel::E1000),
"rtl8139" => Ok(NetworkDeviceModel::RTL8139),
"vmxnet3" => Ok(NetworkDeviceModel::Vmxnet3),
"veth" => Ok(NetworkDeviceModel::Veth),
_ => bail!("Invalid network device model: {s}"),
}
}
}
#[derive(Debug)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct NetworkDevice {
model: NetworkDeviceModel,
mac_address: MacAddress,
firewall: bool,
ip: Option<Ipv4Cidr>,
ip6: Option<Ipv6Cidr>,
}
impl NetworkDevice {
pub fn model(&self) -> NetworkDeviceModel {
self.model
}
pub fn mac_address(&self) -> &MacAddress {
&self.mac_address
}
pub fn ip(&self) -> Option<&Ipv4Cidr> {
self.ip.as_ref()
}
pub fn ip6(&self) -> Option<&Ipv6Cidr> {
self.ip6.as_ref()
}
pub fn has_firewall(&self) -> bool {
self.firewall
}
}
impl FromStr for NetworkDevice {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (mut ty, mut hwaddr, mut firewall, mut ip, mut ip6) = (None, None, true, None, None);
for entry in PropertyIterator::new(s) {
let (key, value) = entry.unwrap();
if let Some(key) = key {
match key {
"type" | "model" => {
ty = Some(NetworkDeviceModel::from_str(&value)?);
}
"hwaddr" | "macaddr" => {
hwaddr = Some(MacAddress::from_str(&value)?);
}
"firewall" => {
firewall = parse_bool(&value)?;
}
"ip" => {
if value == "dhcp" {
continue;
}
ip = Some(Ipv4Cidr::from_str(&value)?);
}
"ip6" => {
if value == "dhcp" || value == "auto" {
continue;
}
ip6 = Some(Ipv6Cidr::from_str(&value)?);
}
_ => {
if let Ok(model) = NetworkDeviceModel::from_str(key) {
ty = Some(model);
hwaddr = Some(MacAddress::from_str(&value)?);
}
}
}
}
}
if let (Some(ty), Some(hwaddr)) = (ty, hwaddr) {
return Ok(NetworkDevice {
model: ty,
mac_address: hwaddr,
firewall,
ip,
ip6,
});
}
bail!("No valid network device detected in string {s}");
}
}
#[derive(Debug, Default)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct NetworkConfig {
network_devices: HashMap<i64, NetworkDevice>,
}
impl NetworkConfig {
pub fn new() -> Self {
Self::default()
}
pub fn index_from_net_key(key: &str) -> Result<i64, Error> {
if let Some(digits) = key.strip_prefix("net") {
if let Some((digits, rest)) = match_digits(digits) {
let index: i64 = digits.parse()?;
if (0..31).contains(&index) && rest.is_empty() {
return Ok(index);
}
}
}
bail!("No index found in net key string: {key}")
}
pub fn network_devices(&self) -> &HashMap<i64, NetworkDevice> {
&self.network_devices
}
pub fn parse<R: io::BufRead>(input: R) -> Result<Self, Error> {
let mut network_devices = HashMap::new();
for line in input.lines() {
let line = line?;
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if line.starts_with('[') {
break;
}
if line.starts_with("net") {
log::trace!("parsing net config line: {line}");
if let Some((mut key, mut value)) = line.split_once(':') {
if key.is_empty() || value.is_empty() {
continue;
}
key = key.trim();
value = value.trim();
if let Ok(index) = Self::index_from_net_key(key) {
let network_device = NetworkDevice::from_str(value)?;
let exists = network_devices.insert(index, network_device);
if exists.is_some() {
bail!("Duplicated config key detected: {key}");
}
} else {
bail!("Encountered invalid net key in cfg: {key}");
}
}
}
}
Ok(Self { network_devices })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_mac_address() {
for input in [
"aa:aa:aa:11:22:33",
"AA:BB:FF:11:22:33",
"bc:24:11:AA:bb:Ef",
] {
let mac_address = input.parse::<MacAddress>().expect("valid mac address");
assert_eq!(input.to_uppercase(), mac_address.to_string());
}
for input in [
"aa:aa:aa:11:22:33:aa",
"AA:BB:FF:11:22",
"AA:BB:GG:11:22:33",
"AABBGG112233",
"",
] {
input
.parse::<MacAddress>()
.expect_err("invalid mac address");
}
}
#[test]
fn test_eui64_link_local_address() {
let mac_address: MacAddress = "BC:24:11:49:8D:75".parse().expect("valid MAC address");
let link_local_address =
Ipv6Addr::from_str("fe80::be24:11ff:fe49:8d75").expect("valid IPv6 address");
assert_eq!(link_local_address, mac_address.eui64_link_local_address());
}
#[test]
fn test_parse_network_device() {
let mut network_device: NetworkDevice =
"virtio=AA:AA:AA:17:19:81,bridge=public,firewall=1,queues=4"
.parse()
.expect("valid network configuration");
assert_eq!(
network_device,
NetworkDevice {
model: NetworkDeviceModel::VirtIO,
mac_address: MacAddress([0xAA, 0xAA, 0xAA, 0x17, 0x19, 0x81]),
firewall: true,
ip: None,
ip6: None,
}
);
network_device = "model=virtio,macaddr=AA:AA:AA:17:19:81,bridge=public,firewall=1,queues=4"
.parse()
.expect("valid network configuration");
assert_eq!(
network_device,
NetworkDevice {
model: NetworkDeviceModel::VirtIO,
mac_address: MacAddress([0xAA, 0xAA, 0xAA, 0x17, 0x19, 0x81]),
firewall: true,
ip: None,
ip6: None,
}
);
network_device =
"name=eth0,bridge=public,firewall=0,hwaddr=AA:AA:AA:E2:3E:24,ip=dhcp,type=veth"
.parse()
.expect("valid network configuration");
assert_eq!(
network_device,
NetworkDevice {
model: NetworkDeviceModel::Veth,
mac_address: MacAddress([0xAA, 0xAA, 0xAA, 0xE2, 0x3E, 0x24]),
firewall: false,
ip: None,
ip6: None,
}
);
"model=virtio"
.parse::<NetworkDevice>()
.expect_err("invalid network configuration");
"bridge=public,firewall=0"
.parse::<NetworkDevice>()
.expect_err("invalid network configuration");
"".parse::<NetworkDevice>()
.expect_err("invalid network configuration");
"name=eth0,bridge=public,firewall=0,hwaddr=AA:AA:AG:E2:3E:24,ip=dhcp,type=veth"
.parse::<NetworkDevice>()
.expect_err("invalid network configuration");
}
#[test]
fn test_parse_network_confg() {
let mut guest_config = "\
boot: order=scsi0;net0
cores: 4
cpu: host
memory: 8192
meta: creation-qemu=8.0.2,ctime=1700141675
name: hoan-sdn
net0: virtio=AA:BB:CC:F2:FE:75,bridge=public
numa: 0
ostype: l26
parent: uwu
scsi0: local-lvm:vm-999-disk-0,discard=on,iothread=1,size=32G
scsihw: virtio-scsi-single
smbios1: uuid=addb0cc6-0393-4269-a504-1eb46604cb8a
sockets: 1
vmgenid: 13bcbb05-3608-4d74-bf4f-d5d20c3538e8
[snapshot]
boot: order=scsi0;ide2;net0
cores: 4
cpu: x86-64-v2-AES
ide2: NFS-iso:iso/proxmox-ve_8.0-2.iso,media=cdrom,size=1166488K
memory: 8192
meta: creation-qemu=8.0.2,ctime=1700141675
name: test
net2: virtio=AA:AA:AA:F2:FE:75,bridge=public,firewall=1
numa: 0
ostype: l26
parent: pre-SDN
scsi0: local-lvm:vm-999-disk-0,discard=on,iothread=1,size=32G
scsihw: virtio-scsi-single
smbios1: uuid=addb0cc6-0393-4269-a504-1eb46604cb8a
snaptime: 1700143513
sockets: 1
vmgenid: 706fbe99-d28b-4047-a9cd-3677c859ca8a
[snapshott]
boot: order=scsi0;ide2;net0
cores: 4
cpu: host
ide2: NFS-iso:iso/proxmox-ve_8.0-2.iso,media=cdrom,size=1166488K
memory: 8192
meta: creation-qemu=8.0.2,ctime=1700141675
name: hoan-sdn
net0: virtio=AA:AA:FF:F2:FE:75,bridge=public,firewall=0
numa: 0
ostype: l26
parent: SDN
scsi0: local-lvm:vm-999-disk-0,discard=on,iothread=1,size=32G
scsihw: virtio-scsi-single
smbios1: uuid=addb0cc6-0393-4269-a504-1eb46604cb8a
snaptime: 1700158473
sockets: 1
vmgenid: 706fbe99-d28b-4047-a9cd-3677c859ca8a"
.as_bytes();
let mut network_config: NetworkConfig =
NetworkConfig::parse(guest_config).expect("valid network configuration");
assert_eq!(network_config.network_devices().len(), 1);
assert_eq!(
network_config.network_devices()[&0],
NetworkDevice {
model: NetworkDeviceModel::VirtIO,
mac_address: MacAddress([0xAA, 0xBB, 0xCC, 0xF2, 0xFE, 0x75]),
firewall: true,
ip: None,
ip6: None,
}
);
guest_config = "\
arch: amd64
cores: 1
features: nesting=1
hostname: dnsct
memory: 512
net0: name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:11,ip=dhcp,type=veth
net2: name=eth0,bridge=data,firewall=0,hwaddr=BC:24:11:47:83:12,ip=123.123.123.123/24,type=veth
net5: name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:13,ip6=fd80::1/64,type=veth
ostype: alpine
rootfs: local-lvm:vm-10001-disk-0,size=1G
swap: 512
unprivileged: 1"
.as_bytes();
network_config = NetworkConfig::parse(guest_config).expect("valid network configuration");
assert_eq!(network_config.network_devices().len(), 3);
assert_eq!(
network_config.network_devices()[&0],
NetworkDevice {
model: NetworkDeviceModel::Veth,
mac_address: MacAddress([0xBC, 0x24, 0x11, 0x47, 0x83, 0x11]),
firewall: true,
ip: None,
ip6: None,
}
);
assert_eq!(
network_config.network_devices()[&2],
NetworkDevice {
model: NetworkDeviceModel::Veth,
mac_address: MacAddress([0xBC, 0x24, 0x11, 0x47, 0x83, 0x12]),
firewall: false,
ip: Some(Ipv4Cidr::from_str("123.123.123.123/24").expect("valid ipv4")),
ip6: None,
}
);
assert_eq!(
network_config.network_devices()[&5],
NetworkDevice {
model: NetworkDeviceModel::Veth,
mac_address: MacAddress([0xBC, 0x24, 0x11, 0x47, 0x83, 0x13]),
firewall: true,
ip: None,
ip6: Some(Ipv6Cidr::from_str("fd80::1/64").expect("valid ipv6")),
}
);
NetworkConfig::parse(
"netqwe: name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:11,ip=dhcp,type=veth"
.as_bytes(),
)
.expect_err("invalid net key");
NetworkConfig::parse(
"net0 name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:11,ip=dhcp,type=veth"
.as_bytes(),
)
.expect_err("invalid net key");
NetworkConfig::parse(
"net33: name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:11,ip=dhcp,type=veth"
.as_bytes(),
)
.expect_err("invalid net key");
}
}

View File

@ -1 +0,0 @@
pub mod utils;

View File

@ -1,70 +0,0 @@
use std::net::{IpAddr, ToSocketAddrs};
use crate::firewall::types::Cidr;
use nix::sys::socket::{AddressFamily, SockaddrLike};
use proxmox_sys::nodename;
/// gets a list of IPs that the hostname of this node resolves to
///
/// panics if the local hostname is not resolvable
pub fn host_ips() -> Vec<IpAddr> {
let hostname = nodename();
log::trace!("resolving hostname");
format!("{hostname}:0")
.to_socket_addrs()
.expect("local hostname is resolvable")
.map(|addr| addr.ip())
.collect()
}
/// gets a list of all configured CIDRs on all network interfaces of this host
///
/// panics if unable to query the current network configuration
pub fn network_interface_cidrs() -> Vec<Cidr> {
use nix::ifaddrs::getifaddrs;
log::trace!("reading networking interface list");
let mut cidrs = Vec::new();
let interfaces = getifaddrs().expect("should be able to query network interfaces");
for interface in interfaces {
if let (Some(address), Some(netmask)) = (interface.address, interface.netmask) {
match (address.family(), netmask.family()) {
(Some(AddressFamily::Inet), Some(AddressFamily::Inet)) => {
let address = address.as_sockaddr_in().expect("is an IPv4 address").ip();
let netmask = netmask
.as_sockaddr_in()
.expect("is an IPv4 address")
.ip()
.count_ones()
.try_into()
.expect("count_ones of u32 is < u8_max");
cidrs.push(Cidr::new_v4(address, netmask).expect("netmask is valid"));
}
(Some(AddressFamily::Inet6), Some(AddressFamily::Inet6)) => {
let address = address.as_sockaddr_in6().expect("is an IPv6 address").ip();
let netmask_address =
netmask.as_sockaddr_in6().expect("is an IPv6 address").ip();
let netmask = u128::from_be_bytes(netmask_address.octets())
.count_ones()
.try_into()
.expect("count_ones of u128 is < u8_max");
cidrs.push(Cidr::new_v6(address, netmask).expect("netmask is valid"));
}
_ => continue,
}
}
}
cidrs
}

View File

@ -1,3 +0,0 @@
pub mod firewall;
pub mod guest;
pub mod host;