From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 83AFE1FF168 for ; Tue, 12 Nov 2024 13:36:55 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 6DBB228B68; Tue, 12 Nov 2024 13:36:55 +0100 (CET) From: Stefan Hanreich To: pve-devel@lists.proxmox.com Date: Tue, 12 Nov 2024 13:25:55 +0100 Message-Id: <20241112122602.88598-18-s.hanreich@proxmox.com> X-Mailer: git-send-email 2.39.5 In-Reply-To: <20241112122602.88598-1-s.hanreich@proxmox.com> References: <20241112122602.88598-1-s.hanreich@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.241 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_LAZY_DOMAIN_SECURITY 1 Sending domain does not have any anti-forgery methods RDNS_NONE 0.793 Delivered to internal network by a host with no rDNS SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_NONE 0.001 SPF: sender does not publish an SPF Record X-Mailman-Approved-At: Tue, 12 Nov 2024 13:36:53 +0100 Subject: [pve-devel] [PATCH proxmox-firewall v3 17/24] add proxmox-ve-rs crate - move proxmox-ve-config there X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox VE development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pve-devel-bounces@lists.proxmox.com Sender: "pve-devel" Signed-off-by: Stefan Hanreich --- Cargo.toml | 4 +- Makefile | 2 +- proxmox-firewall/Cargo.toml | 2 +- proxmox-nftables/Cargo.toml | 2 +- proxmox-ve-config/Cargo.toml | 25 - proxmox-ve-config/resources/ct_helper.json | 52 - proxmox-ve-config/resources/macros.json | 923 ----------------- proxmox-ve-config/src/firewall/cluster.rs | 374 ------- proxmox-ve-config/src/firewall/common.rs | 184 ---- proxmox-ve-config/src/firewall/ct_helper.rs | 115 --- proxmox-ve-config/src/firewall/fw_macros.rs | 69 -- proxmox-ve-config/src/firewall/guest.rs | 237 ----- proxmox-ve-config/src/firewall/host.rs | 372 ------- proxmox-ve-config/src/firewall/mod.rs | 10 - proxmox-ve-config/src/firewall/parse.rs | 494 --------- proxmox-ve-config/src/firewall/ports.rs | 80 -- .../src/firewall/types/address.rs | 615 ----------- proxmox-ve-config/src/firewall/types/alias.rs | 174 ---- proxmox-ve-config/src/firewall/types/group.rs | 36 - proxmox-ve-config/src/firewall/types/ipset.rs | 349 ------- proxmox-ve-config/src/firewall/types/log.rs | 222 ---- proxmox-ve-config/src/firewall/types/mod.rs | 14 - proxmox-ve-config/src/firewall/types/port.rs | 181 ---- proxmox-ve-config/src/firewall/types/rule.rs | 412 -------- .../src/firewall/types/rule_match.rs | 977 ------------------ proxmox-ve-config/src/guest/mod.rs | 115 --- proxmox-ve-config/src/guest/types.rs | 38 - proxmox-ve-config/src/guest/vm.rs | 510 --------- proxmox-ve-config/src/host/mod.rs | 1 - proxmox-ve-config/src/host/utils.rs | 70 -- proxmox-ve-config/src/lib.rs | 3 - 31 files changed, 6 insertions(+), 6656 deletions(-) delete mode 100644 proxmox-ve-config/Cargo.toml delete mode 100644 proxmox-ve-config/resources/ct_helper.json delete mode 100644 proxmox-ve-config/resources/macros.json delete mode 100644 proxmox-ve-config/src/firewall/cluster.rs delete mode 100644 proxmox-ve-config/src/firewall/common.rs delete mode 100644 proxmox-ve-config/src/firewall/ct_helper.rs delete mode 100644 proxmox-ve-config/src/firewall/fw_macros.rs delete mode 100644 proxmox-ve-config/src/firewall/guest.rs delete mode 100644 proxmox-ve-config/src/firewall/host.rs delete mode 100644 proxmox-ve-config/src/firewall/mod.rs delete mode 100644 proxmox-ve-config/src/firewall/parse.rs delete mode 100644 proxmox-ve-config/src/firewall/ports.rs delete mode 100644 proxmox-ve-config/src/firewall/types/address.rs delete mode 100644 proxmox-ve-config/src/firewall/types/alias.rs delete mode 100644 proxmox-ve-config/src/firewall/types/group.rs delete mode 100644 proxmox-ve-config/src/firewall/types/ipset.rs delete mode 100644 proxmox-ve-config/src/firewall/types/log.rs delete mode 100644 proxmox-ve-config/src/firewall/types/mod.rs delete mode 100644 proxmox-ve-config/src/firewall/types/port.rs delete mode 100644 proxmox-ve-config/src/firewall/types/rule.rs delete mode 100644 proxmox-ve-config/src/firewall/types/rule_match.rs delete mode 100644 proxmox-ve-config/src/guest/mod.rs delete mode 100644 proxmox-ve-config/src/guest/types.rs delete mode 100644 proxmox-ve-config/src/guest/vm.rs delete mode 100644 proxmox-ve-config/src/host/mod.rs delete mode 100644 proxmox-ve-config/src/host/utils.rs delete mode 100644 proxmox-ve-config/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 1fbc2e0..3894d9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,9 @@ [workspace] members = [ - "proxmox-ve-config", "proxmox-nftables", "proxmox-firewall", ] resolver = "2" + +[workspace.dependencies] +proxmox-ve-config = { version = "0.1.0" } diff --git a/Makefile b/Makefile index e49e58f..a134423 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ cargo-build: build: $(BUILDDIR) $(BUILDDIR): 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 $@ .PHONY: deb diff --git a/proxmox-firewall/Cargo.toml b/proxmox-firewall/Cargo.toml index 6cb1b09..c2adcac 100644 --- a/proxmox-firewall/Cargo.toml +++ b/proxmox-firewall/Cargo.toml @@ -21,7 +21,7 @@ serde_json = "1" signal-hook = "0.3" proxmox-nftables = { path = "../proxmox-nftables", features = ["config-ext"] } -proxmox-ve-config = { path = "../proxmox-ve-config" } +proxmox-ve-config = { workspace = true } [dev-dependencies] insta = { version = "1.21", features = ["json"] } diff --git a/proxmox-nftables/Cargo.toml b/proxmox-nftables/Cargo.toml index e84509d..4ff6f41 100644 --- a/proxmox-nftables/Cargo.toml +++ b/proxmox-nftables/Cargo.toml @@ -22,4 +22,4 @@ serde = { version = "1", features = [ "derive" ] } serde_json = "1" serde_plain = "1" -proxmox-ve-config = { path = "../proxmox-ve-config", optional = true } +proxmox-ve-config = { workspace = true, optional = true } diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml deleted file mode 100644 index 0239c08..0000000 --- a/proxmox-ve-config/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "proxmox-ve-config" -version = "0.1.0" -edition = "2021" -authors = [ - "Wolfgang Bumiller ", - "Stefan Hanreich ", - "Proxmox Support Team ", -] -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" diff --git a/proxmox-ve-config/resources/ct_helper.json b/proxmox-ve-config/resources/ct_helper.json deleted file mode 100644 index 5e70a3a..0000000 --- a/proxmox-ve-config/resources/ct_helper.json +++ /dev/null @@ -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 - } -] diff --git a/proxmox-ve-config/resources/macros.json b/proxmox-ve-config/resources/macros.json deleted file mode 100644 index 2fcc0fb..0000000 --- a/proxmox-ve-config/resources/macros.json +++ /dev/null @@ -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" - } -} diff --git a/proxmox-ve-config/src/firewall/cluster.rs b/proxmox-ve-config/src/firewall/cluster.rs deleted file mode 100644 index 223124b..0000000 --- a/proxmox-ve-config/src/firewall/cluster.rs +++ /dev/null @@ -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, -} - -/// 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(input: R) -> Result { - 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 { - &self.config.rules - } - - pub fn groups(&self) -> &BTreeMap { - &self.config.groups - } - - pub fn ipsets(&self) -> &BTreeMap { - &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 { - 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, - - #[serde(default, with = "serde_option_bool")] - ebtables: Option, - - #[serde(default, with = "serde_option_log_ratelimit")] - log_ratelimit: Option, - - policy_in: Option, - policy_out: Option, -} - -#[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()); - } -} diff --git a/proxmox-ve-config/src/firewall/common.rs b/proxmox-ve-config/src/firewall/common.rs deleted file mode 100644 index a08f19c..0000000 --- a/proxmox-ve-config/src/firewall/common.rs +++ /dev/null @@ -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 -where - O: Default + std::fmt::Debug + serde::de::DeserializeOwned, -{ - pub(crate) options: O, - pub(crate) rules: Vec, - pub(crate) aliases: BTreeMap, - pub(crate) ipsets: BTreeMap, - pub(crate) groups: BTreeMap, -} - -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, -} - -impl Config -where - O: Default + std::fmt::Debug + serde::de::DeserializeOwned, -{ - pub fn new() -> Self { - Self::default() - } - - pub fn parse(input: R, parser_cfg: &ParserConfig) -> Result { - 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) -> 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\"") - })? - .parse::() - .map_err(|_| { - format_err!("interface name must be of the form \"net\"") - })?; - } - } - - 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 { - &self.ipsets - } - - pub fn alias(&self, name: &str) -> Option<&Alias> { - self.aliases.get(name) - } -} diff --git a/proxmox-ve-config/src/firewall/ct_helper.rs b/proxmox-ve-config/src/firewall/ct_helper.rs deleted file mode 100644 index 40e4fee..0000000 --- a/proxmox-ve-config/src/firewall/ct_helper.rs +++ /dev/null @@ -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, - pub v6: Option, - pub name: String, - pub tcp: Option, - pub udp: Option, -} - -impl TryFrom for CtHelperMacro { - type Error = Error; - - fn try_from(value: CtHelperMacroJson) -> Result { - 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, - name: String, - tcp: Option, - udp: Option, -} - -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 { - 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 { - const MACROS: &str = include_str!("../../resources/ct_helper.json"); - static HASHMAP: OnceLock> = OnceLock::new(); - - HASHMAP.get_or_init(|| { - let macro_data: Vec = 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) -} diff --git a/proxmox-ve-config/src/firewall/fw_macros.rs b/proxmox-ve-config/src/firewall/fw_macros.rs deleted file mode 100644 index 5fa8dab..0000000 --- a/proxmox-ve-config/src/firewall/fw_macros.rs +++ /dev/null @@ -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, -} - -#[derive(Clone, Debug, Default)] -pub struct FwMacro { - pub _description: &'static str, - pub code: Vec, -} - -fn macros() -> &'static HashMap { - const MACROS: &str = include_str!("../../resources/macros.json"); - static HASHMAP: OnceLock> = OnceLock::new(); - - HASHMAP.get_or_init(|| { - let macro_data: HashMap = 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) -} diff --git a/proxmox-ve-config/src/firewall/guest.rs b/proxmox-ve-config/src/firewall/guest.rs deleted file mode 100644 index c7e282f..0000000 --- a/proxmox-ve-config/src/firewall/guest.rs +++ /dev/null @@ -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, - - #[serde(default, with = "serde_option_bool")] - enable: Option, - - #[serde(default, with = "serde_option_bool")] - ipfilter: Option, - - #[serde(default, with = "serde_option_bool")] - ndp: Option, - - #[serde(default, with = "serde_option_bool")] - radv: Option, - - log_level_in: Option, - log_level_out: Option, - - #[serde(default, with = "serde_option_bool")] - macfilter: Option, - - #[serde(rename = "policy_in")] - policy_in: Option, - - #[serde(rename = "policy_out")] - policy_out: Option, -} - -#[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, -} - -impl Config { - pub fn parse( - vmid: &Vmid, - iface_prefix: &'static str, - firewall_input: T, - network_input: U, - ) -> Result { - 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 { - 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 { - 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 = 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), - } - ); - } -} diff --git a/proxmox-ve-config/src/firewall/host.rs b/proxmox-ve-config/src/firewall/host.rs deleted file mode 100644 index 3de6fad..0000000 --- a/proxmox-ve-config/src/firewall/host.rs +++ /dev/null @@ -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, - - #[serde(default, with = "parse::serde_option_bool")] - nftables: Option, - - log_level_in: Option, - log_level_out: Option, - - #[serde(default, with = "parse::serde_option_bool")] - log_nf_conntrack: Option, - #[serde(default, with = "parse::serde_option_bool")] - ndp: Option, - - #[serde(default, with = "parse::serde_option_bool")] - nf_conntrack_allow_invalid: Option, - - // is Option> for easier deserialization - #[serde(default, with = "parse::serde_option_conntrack_helpers")] - nf_conntrack_helpers: Option>, - - #[serde(default, with = "parse::serde_option_number")] - nf_conntrack_max: Option, - #[serde(default, with = "parse::serde_option_number")] - nf_conntrack_tcp_timeout_established: Option, - #[serde(default, with = "parse::serde_option_number")] - nf_conntrack_tcp_timeout_syn_recv: Option, - - #[serde(default, with = "parse::serde_option_bool")] - nosmurfs: Option, - - #[serde(default, with = "parse::serde_option_bool")] - protection_synflood: Option, - #[serde(default, with = "parse::serde_option_number")] - protection_synflood_burst: Option, - #[serde(default, with = "parse::serde_option_number")] - protection_synflood_rate: Option, - - smurf_log_level: Option, - tcp_flags_log_level: Option, - - #[serde(default, with = "parse::serde_option_bool")] - tcpflags: Option, -} - -#[derive(Debug, Default)] -pub struct Config { - pub(crate) config: super::common::Config, -} - -impl Config { - pub fn new() -> Self { - Self { - config: Default::default(), - } - } - - pub fn parse(input: R) -> Result { - let config = super::common::Config::parse(input, &Default::default())?; - - if !config.groups.is_empty() { - bail!("host firewall config cannot declare groups"); - } - - if !config.aliases.is_empty() { - bail!("host firewall config cannot declare aliases"); - } - - if !config.ipsets.is_empty() { - bail!("host firewall config cannot declare ipsets"); - } - - Ok(Self { config }) - } - - pub fn rules(&self) -> &[Rule] { - &self.config.rules - } - - pub fn management_ips() -> Result, Error> { - let mut management_cidrs = Vec::new(); - - for host_ip in host_ips() { - for network_interface_cidr in network_interface_cidrs() { - match (host_ip, network_interface_cidr) { - (IpAddr::V4(ip), Cidr::Ipv4(cidr)) => { - if cidr.contains_address(&ip) { - management_cidrs.push(network_interface_cidr); - } - } - (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 { - self.config.options.nf_conntrack_max - } - - pub fn nf_conntrack_tcp_timeout_established(&self) -> Option { - self.config.options.nf_conntrack_tcp_timeout_established - } - - pub fn nf_conntrack_tcp_timeout_syn_recv(&self) -> Option { - self.config.options.nf_conntrack_tcp_timeout_syn_recv - } - - /// 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> { - 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"); - } -} diff --git a/proxmox-ve-config/src/firewall/mod.rs b/proxmox-ve-config/src/firewall/mod.rs deleted file mode 100644 index 2cf57e2..0000000 --- a/proxmox-ve-config/src/firewall/mod.rs +++ /dev/null @@ -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; diff --git a/proxmox-ve-config/src/firewall/parse.rs b/proxmox-ve-config/src/firewall/parse.rs deleted file mode 100644 index 7bf00c0..0000000 --- a/proxmox-ve-config/src/firewall/parse.rs +++ /dev/null @@ -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 { - 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 `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, D::Error> { - struct V; - - impl<'de> Visitor<'de> for V { - type Value = Option; - - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str("a numerical value") - } - - fn visit_str(self, v: &str) -> Result { - v.parse().map_err(E::custom).map(Some) - } - - fn visit_none(self) -> Result { - Ok(None) - } - - fn visit_some(self, deserializer: D) -> Result - 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, D::Error> { - struct V; - - impl<'de> Visitor<'de> for V { - type Value = Option; - - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str("a boolean-like value") - } - - fn visit_bool(self, v: bool) -> Result { - Ok(Some(v)) - } - - fn visit_str(self, v: &str) -> Result { - super::parse_bool(v).map_err(E::custom).map(Some) - } - - fn visit_none(self) -> Result { - Ok(None) - } - - fn visit_some(self, deserializer: D) -> Result - 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>, D::Error> { - struct V; - - impl<'de> Visitor<'de> for V { - type Value = Option>; - - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str("A list of conntrack helpers") - } - - fn visit_str(self, v: &str) -> Result { - if v.is_empty() { - return Ok(None); - } - - Ok(Some(v.split(',').map(String::from).collect())) - } - - fn visit_none(self) -> Result { - Ok(None) - } - - fn visit_some(self, deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_any(self) - } - } - - deserializer.deserialize_any(V) - } -} - -// parses a log_ratelimit string: '[enable=]<1|0> [,burst=] [,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, D::Error> { - struct V; - - impl<'de> Visitor<'de> for V { - type Value = Option; - - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str("a boolean-like value") - } - - fn visit_str(self, v: &str) -> Result { - v.parse().map_err(E::custom).map(Some) - } - - fn visit_none(self) -> Result { - Ok(None) - } - - fn visit_some(self, deserializer: D) -> Result - 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(self, visitor: V) -> Result - where - V: serde::de::Visitor<'de>, - { - self.0.deserialize_any(visitor) - } - - fn deserialize_option(self, visitor: V) -> Result - where - V: serde::de::Visitor<'de>, - { - visitor.visit_some(self.0) - } - - fn deserialize_str(self, visitor: V) -> Result - where - V: serde::de::Visitor<'de>, - { - self.0.deserialize_str(visitor) - } - - fn deserialize_string(self, visitor: V) -> Result - where - V: serde::de::Visitor<'de>, - { - self.0.deserialize_string(visitor) - } - - fn deserialize_enum( - self, - _name: &str, - _variants: &'static [&'static str], - visitor: V, - ) -> Result - 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(serde::de::value::StringDeserializer); - -impl<'de, E> serde::de::Deserializer<'de> for SomeStringDeserializer -where - E: serde::de::Error, -{ - type Error = E; - - fn deserialize_any(self, visitor: V) -> Result - where - V: serde::de::Visitor<'de>, - { - self.0.deserialize_any(visitor) - } - - fn deserialize_option(self, visitor: V) -> Result - where - V: serde::de::Visitor<'de>, - { - visitor.visit_some(self.0) - } - - fn deserialize_str(self, visitor: V) -> Result - where - V: serde::de::Visitor<'de>, - { - self.0.deserialize_str(visitor) - } - - fn deserialize_string(self, visitor: V) -> Result - where - V: serde::de::Visitor<'de>, - { - self.0.deserialize_string(visitor) - } - - fn deserialize_enum( - self, - _name: &str, - _variants: &'static [&'static str], - visitor: V, - ) -> Result - 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 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; - - 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(msg: T) -> Self { - Self(msg.to_string()) - } -} diff --git a/proxmox-ve-config/src/firewall/ports.rs b/proxmox-ve-config/src/firewall/ports.rs deleted file mode 100644 index 9d5d1be..0000000 --- a/proxmox-ve-config/src/firewall/ports.rs +++ /dev/null @@ -1,80 +0,0 @@ -use anyhow::{format_err, Error}; -use std::sync::OnceLock; - -#[derive(Default)] -struct NamedPorts { - ports: std::collections::HashMap, -} - -impl NamedPorts { - fn new() -> Self { - use std::io::BufRead; - - log::trace!("loading /etc/services"); - - let mut this = Self::default(); - - let file = match std::fs::File::open("/etc/services") { - Ok(file) => file, - Err(_) => return this, - }; - - for line in std::io::BufReader::new(file).lines() { - let line = match line { - Ok(line) => line, - Err(_) => break, - }; - - let line = line.trim_start(); - - if line.is_empty() || line.starts_with('#') { - continue; - } - - let mut parts = line.split_ascii_whitespace(); - - let name = match parts.next() { - None => continue, - Some(name) => name.to_string(), - }; - - let proto: u16 = match parts.next() { - None => continue, - Some(proto) => match proto.split('/').next() { - None => continue, - Some(num) => match num.parse() { - Ok(num) => num, - Err(_) => continue, - }, - }, - }; - - this.ports.insert(name, proto); - for alias in parts { - if alias.starts_with('#') { - break; - } - this.ports.insert(alias.to_string(), proto); - } - } - - this - } - - fn find(&self, name: &str) -> Option { - self.ports.get(name).copied() - } -} - -fn named_ports() -> &'static NamedPorts { - static NAMED_PORTS: OnceLock = OnceLock::new(); - - NAMED_PORTS.get_or_init(NamedPorts::new) -} - -/// Parse a named port with the help of `/etc/services`. -pub fn parse_named_port(name: &str) -> Result { - named_ports() - .find(name) - .ok_or_else(|| format_err!("unknown port name {name:?}")) -} diff --git a/proxmox-ve-config/src/firewall/types/address.rs b/proxmox-ve-config/src/firewall/types/address.rs deleted file mode 100644 index e48ac1b..0000000 --- a/proxmox-ve-config/src/firewall/types/address.rs +++ /dev/null @@ -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, mask: u8) -> Result { - Ok(Cidr::Ipv4(Ipv4Cidr::new(addr, mask)?)) - } - - pub fn new_v6(addr: impl Into, mask: u8) -> Result { - 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 { - if let Ok(ip) = s.parse::() { - return Ok(Cidr::Ipv4(ip)); - } - - if let Ok(ip) = s.parse::() { - return Ok(Cidr::Ipv6(ip)); - } - - bail!("invalid ip address or CIDR: {s:?}"); - } -} - -impl From for Cidr { - fn from(cidr: Ipv4Cidr) -> Self { - Cidr::Ipv4(cidr) - } -} - -impl From 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, mask: u8) -> Result { - 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> From 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 { - 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::()?, 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, mask: u8) -> Result { - 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 { - 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::()?, mask)? - } - }) - } -} - -impl fmt::Display for Ipv6Cidr { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}/{}", &self.addr, self.mask) - } -} - -impl> From 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 { - 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::() { - if let Ok(end) = end.parse::() { - 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::() { - if let Ok(end) = end.parse::() { - 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 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, - family: Family, -} - -impl Deref for IpList { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.entries - } -} - -impl> From 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 { - 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) -> Result { - 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::().unwrap_err(); - "0.0.0.0/33".parse::().unwrap_err(); - "256.256.256.256/10".parse::().unwrap_err(); - - "fe80::1/64".parse::().unwrap_err(); - "qweasd".parse::().unwrap_err(); - "".parse::().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::().unwrap_err(); - "eeee::1/129".parse::().unwrap_err(); - "gggg::1/64".parse::().unwrap_err(); - - "192.168.0.1".parse::().unwrap_err(); - "qweasd".parse::().unwrap_err(); - "".parse::().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::() - .unwrap_err(); - "192.168.100.0-fe80::1".parse::().unwrap_err(); - "192.168.100.0-192.168.200.0/16" - .parse::() - .unwrap_err(); - "192.168.100.0-192.168.200.0-192.168.250.0" - .parse::() - .unwrap_err(); - "qweasd".parse::().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::().unwrap_err(); - - "".parse::().unwrap_err(); - "proxmox".parse::().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"); - } -} diff --git a/proxmox-ve-config/src/firewall/types/alias.rs b/proxmox-ve-config/src/firewall/types/alias.rs deleted file mode 100644 index e6aa30d..0000000 --- a/proxmox-ve-config/src/firewall/types/alias.rs +++ /dev/null @@ -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 { - 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 { - 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) -> 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, -} - -impl Alias { - pub fn new( - name: impl Into, - address: impl Into, - comment: impl Into>, - ) -> 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 { - 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::().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::().expect_err("invalid alias"); - } - } - - #[test] - fn test_parse_alias_name() { - for name in ["dc/proxmox_123", "guest/proxmox-123"] { - name.parse::().expect("valid alias name"); - } - - for name in ["proxmox/proxmox_123", "guests/proxmox-123", "dc/", "/name"] { - name.parse::().expect_err("invalid alias name"); - } - } -} diff --git a/proxmox-ve-config/src/firewall/types/group.rs b/proxmox-ve-config/src/firewall/types/group.rs deleted file mode 100644 index 7455268..0000000 --- a/proxmox-ve-config/src/firewall/types/group.rs +++ /dev/null @@ -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, - comment: Option, -} - -impl Group { - pub const fn new() -> Self { - Self { - rules: Vec::new(), - comment: None, - } - } - - pub fn rules(&self) -> &Vec { - &self.rules - } - - pub fn comment(&self) -> Option<&str> { - self.comment.as_deref() - } - - pub fn set_comment(&mut self, comment: Option) { - self.comment = comment; - } - - pub(crate) fn parse_entry(&mut self, line: &str) -> Result<(), Error> { - self.rules.push(line.parse()?); - Ok(()) - } -} diff --git a/proxmox-ve-config/src/firewall/types/ipset.rs b/proxmox-ve-config/src/firewall/types/ipset.rs deleted file mode 100644 index c1af642..0000000 --- a/proxmox-ve-config/src/firewall/types/ipset.rs +++ /dev/null @@ -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 { - 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) -> 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 { - 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 { - 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> From 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, -} - -impl> From 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 { - 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, - pub comment: Option, -} - -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) -> 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 { - 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; - - #[inline] - fn deref(&self) -> &Self::Target { - &self.set - } -} - -impl DerefMut for Ipset { - #[inline] - fn deref_mut(&mut self) -> &mut Vec { - &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::().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::().expect_err("invalid ipset name"); - } - } - - #[test] - fn test_parse_ipset_address() { - let mut ipset_address = "10.0.0.1" - .parse::() - .expect("valid ipset address"); - assert!(matches!(ipset_address, IpsetAddress::Cidr(Cidr::Ipv4(..)))); - - ipset_address = "fe80::1/64" - .parse::() - .expect("valid ipset address"); - assert!(matches!(ipset_address, IpsetAddress::Cidr(Cidr::Ipv6(..)))); - - ipset_address = "dc/proxmox-123" - .parse::() - .expect("valid ipset address"); - assert!(matches!(ipset_address, IpsetAddress::Alias(..))); - - ipset_address = "guest/proxmox_123" - .parse::() - .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::() - .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::() - .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() - ) - } - ) - } -} diff --git a/proxmox-ve-config/src/firewall/types/log.rs b/proxmox-ve-config/src/firewall/types/log.rs deleted file mode 100644 index 72344e4..0000000 --- a/proxmox-ve-config/src/firewall/types/log.rs +++ /dev/null @@ -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 { - 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 { - 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 { - 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::() - .expect("valid rate limit"); - - assert_eq!( - parsed_rate_limit, - LogRateLimit { - enabled: true, - burst: 123, - rate: 44, - per: LogRateLimitTimescale::Second, - } - ); - - parsed_rate_limit = "1".parse::().expect("valid rate limit"); - - assert_eq!(parsed_rate_limit, LogRateLimit::default()); - - parsed_rate_limit = "enable=0,rate=123/hour" - .parse::() - .expect("valid rate limit"); - - assert_eq!( - parsed_rate_limit, - LogRateLimit { - enabled: false, - burst: 5, - rate: 123, - per: LogRateLimitTimescale::Hour, - } - ); - - "2".parse::() - .expect_err("invalid value for enable"); - - "enabled=0,rate=123" - .parse::() - .expect_err("invalid key in log ratelimit"); - - "enable=0,rate=123," - .parse::() - .expect_err("trailing comma in log rate limit specification"); - - "enable=0,rate=123/proxmox," - .parse::() - .expect_err("invalid unit for rate"); - } -} diff --git a/proxmox-ve-config/src/firewall/types/mod.rs b/proxmox-ve-config/src/firewall/types/mod.rs deleted file mode 100644 index 8fd551e..0000000 --- a/proxmox-ve-config/src/firewall/types/mod.rs +++ /dev/null @@ -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; diff --git a/proxmox-ve-config/src/firewall/types/port.rs b/proxmox-ve-config/src/firewall/types/port.rs deleted file mode 100644 index c1252d9..0000000 --- a/proxmox-ve-config/src/firewall/types/port.rs +++ /dev/null @@ -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 { - if let Ok(port) = port.parse::() { - return Ok(port); - } - - if let Ok(port) = parse_named_port(port) { - return Ok(port); - } - - bail!("invalid port specification: {port}") -} - -impl std::str::FromStr for PortEntry { - type Err = Error; - - fn from_str(s: &str) -> Result { - Ok(match s.trim().split_once(':') { - None => PortEntry::from(parse_port(s)?), - Some((first, second)) => { - PortEntry::try_from((parse_port(first)?, parse_port(second)?))? - } - }) - } -} - -impl From for PortEntry { - fn from(port: u16) -> Self { - PortEntry::Port(port) - } -} - -impl TryFrom<(u16, u16)> for PortEntry { - type Error = Error; - - fn try_from(ports: (u16, u16)) -> Result { - if ports.0 > ports.1 { - bail!("start port is greater than end port!"); - } - - Ok(PortEntry::Range(ports.0, ports.1)) - } -} - -#[derive(Clone, Debug, DeserializeFromStr)] -#[cfg_attr(test, derive(Eq, PartialEq))] -pub struct PortList(pub(crate) Vec); - -impl FromIterator for PortList { - fn from_iter>(iter: T) -> Self { - Self(iter.into_iter().collect()) - } -} - -impl> From for PortList { - fn from(value: T) -> Self { - Self(vec![value.into()]) - } -} - -impl Deref for PortList { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl std::str::FromStr for PortList { - type Err = Error; - - fn from_str(s: &str) -> Result { - if s.is_empty() { - bail!("empty port specification"); - } - - let mut entries = Vec::new(); - - for entry in s.trim().split(',') { - entries.push(entry.parse()?); - } - - if entries.is_empty() { - bail!("invalid empty port list"); - } - - Ok(Self(entries)) - } -} - -impl fmt::Display for PortList { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use fmt::Write; - if self.0.len() > 1 { - f.write_char('{')?; - } - - let mut comma = '\0'; - for entry in &self.0 { - if std::mem::replace(&mut comma, ',') != '\0' { - f.write_char(comma)?; - } - fmt::Display::fmt(entry, f)?; - } - - if self.0.len() > 1 { - f.write_char('}')?; - } - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_port_entry() { - let mut port_entry: PortEntry = "12345".parse().expect("valid port entry"); - assert_eq!(port_entry, PortEntry::from(12345)); - - port_entry = "0:65535".parse().expect("valid port entry"); - assert_eq!(port_entry, PortEntry::try_from((0, 65535)).unwrap()); - - "65536".parse::().unwrap_err(); - "100:100000".parse::().unwrap_err(); - "qweasd".parse::().unwrap_err(); - "".parse::().unwrap_err(); - } - - #[test] - fn test_parse_port_list() { - let mut port_list: PortList = "12345".parse().expect("valid port list"); - assert_eq!(port_list, PortList::from(12345)); - - port_list = "12345,0:65535,1337,ssh:80,https" - .parse() - .expect("valid port list"); - - assert_eq!( - port_list, - PortList(vec![ - PortEntry::from(12345), - PortEntry::try_from((0, 65535)).unwrap(), - PortEntry::from(1337), - PortEntry::try_from((22, 80)).unwrap(), - PortEntry::from(443), - ]) - ); - - "0::1337".parse::().unwrap_err(); - "0:1337,".parse::().unwrap_err(); - "70000".parse::().unwrap_err(); - "qweasd".parse::().unwrap_err(); - "".parse::().unwrap_err(); - } -} diff --git a/proxmox-ve-config/src/firewall/types/rule.rs b/proxmox-ve-config/src/firewall/types/rule.rs deleted file mode 100644 index 20deb3a..0000000 --- a/proxmox-ve-config/src/firewall/types/rule.rs +++ /dev/null @@ -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 { - 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 { - 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, -} - -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 { - 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::()?) - } else { - Kind::from(line.parse::()?) - }; - - 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 for Kind { - fn from(value: RuleGroup) -> Self { - Kind::Group(value) - } -} - -impl From 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, -} - -impl RuleGroup { - pub(crate) fn from_options(group: String, options: RuleOptions) -> Result { - 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 { - 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::() - .expect_err("too many dashes in option"); - - "IN DROP --log --iface eth0" - .parse::() - .expect_err("no value for option"); - - "IN DROP --log crit --iface" - .parse::() - .expect_err("no value for option"); - } -} diff --git a/proxmox-ve-config/src/firewall/types/rule_match.rs b/proxmox-ve-config/src/firewall/types/rule_match.rs deleted file mode 100644 index 94d8624..0000000 --- a/proxmox-ve-config/src/firewall/types/rule_match.rs +++ /dev/null @@ -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, - - pub(crate) dport: Option, - pub(crate) sport: Option, - - pub(crate) dest: Option, - pub(crate) source: Option, - - #[serde(alias = "i")] - pub(crate) iface: Option, - - pub(crate) log: Option, - pub(crate) icmp_type: Option, -} - -impl FromStr for RuleOptions { - type Err = Error; - - fn from_str(mut line: &str) -> Result { - 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, - - pub(crate) iface: Option, - pub(crate) log: Option, - pub(crate) ip: Option, - pub(crate) proto: Option, -} - -impl RuleMatch { - pub(crate) fn from_options( - dir: Direction, - verdict: Verdict, - fw_macro: impl Into>, - options: RuleOptions, - ) -> Result { - 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 { - 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('(') { - // () - - 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 { - 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, - pub(crate) dst: Option, -} - -impl IpMatch { - pub fn new( - src: impl Into>, - dst: impl Into>, - ) -> Result { - 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, Error> { - let src = options - .source - .as_ref() - .map(|elem| elem.parse::()) - .transpose()?; - - let dst = options - .dest - .as_ref() - .map(|elem| elem.parse::()) - .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 { - if let IpAddrMatch::Ip(list) = self { - return Some(list.family()); - } - - None - } -} - -impl FromStr for IpAddrMatch { - type Err = Error; - - fn from_str(value: &str) -> Result { - 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, 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::() { - Ok(num) => Protocol::Numeric(num), - Err(_) => Protocol::Named(other.to_string()), - }, - })) - } - - pub fn family(&self) -> Option { - 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 { - Ok(Self { - ports: Ports::from_options(options)?, - }) - } - - pub fn new(ports: Ports) -> Self { - Self { ports } - } - - pub fn ports(&self) -> &Ports { - &self.ports - } -} - -impl From 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, - dport: Option, -} - -impl Ports { - pub fn new(sport: impl Into>, dport: impl Into>) -> Self { - Self { - sport: sport.into(), - dport: dport.into(), - } - } - - fn from_options(options: &RuleOptions) -> Result { - 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>, dport: impl Into>) -> 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 { - Ok(Self { - ports: Ports::from_options(options)?, - }) - } - - pub fn ports(&self) -> &Ports { - &self.ports - } -} - -impl From 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 { - 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, - code: Option, -} - -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 { - 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 { - 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 { - if s.eq_ignore_ascii_case("any") { - return Ok(Self::Any); - } - - if let Ok(ty) = s.trim().parse::() { - 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 { - if let Ok(code) = s.trim().parse::() { - 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, - pub code: Option, -} - -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 { - 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 { - 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 { - if s.eq_ignore_ascii_case("any") { - return Ok(Self::Any); - } - - if let Ok(ty) = s.trim().parse::() { - 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 { - if let Ok(code) = s.trim().parse::() { - 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::().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::().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")) - } - ); - } -} diff --git a/proxmox-ve-config/src/guest/mod.rs b/proxmox-ve-config/src/guest/mod.rs deleted file mode 100644 index 74fd8ab..0000000 --- a/proxmox-ve-config/src/guest/mod.rs +++ /dev/null @@ -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, -} - -impl From> for GuestMap { - fn from(guests: HashMap) -> Self { - Self { - guests, - _version: Default::default(), - } - } -} - -impl Deref for GuestMap { - type Target = HashMap; - - fn deref(&self) -> &Self::Target { - &self.guests - } -} - -impl GuestMap { - pub fn new() -> Result { - 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 - ) - } -} diff --git a/proxmox-ve-config/src/guest/types.rs b/proxmox-ve-config/src/guest/types.rs deleted file mode 100644 index 217c537..0000000 --- a/proxmox-ve-config/src/guest/types.rs +++ /dev/null @@ -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 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 { - Ok(Self( - s.parse() - .map_err(|_| format_err!("not a valid vmid: {s:?}"))?, - )) - } -} - -serde_plain::derive_deserialize_from_fromstr!(Vmid, "valid vmid"); diff --git a/proxmox-ve-config/src/guest/vm.rs b/proxmox-ve-config/src/guest/vm.rs deleted file mode 100644 index 5b5866a..0000000 --- a/proxmox-ve-config/src/guest/vm.rs +++ /dev/null @@ -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 = 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 { - let split = s.split(':'); - - let parsed = split - .into_iter() - .map(|elem| u8::from_str_radix(elem, 16)) - .collect::, _>>() - .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 { - 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, - ip6: Option, -} - -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 { - 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, -} - -impl NetworkConfig { - pub fn new() -> Self { - Self::default() - } - - pub fn index_from_net_key(key: &str) -> Result { - 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 { - &self.network_devices - } - - pub fn parse(input: R) -> Result { - 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::().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::() - .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::() - .expect_err("invalid network configuration"); - - "bridge=public,firewall=0" - .parse::() - .expect_err("invalid network configuration"); - - "".parse::() - .expect_err("invalid network configuration"); - - "name=eth0,bridge=public,firewall=0,hwaddr=AA:AA:AG:E2:3E:24,ip=dhcp,type=veth" - .parse::() - .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"); - } -} diff --git a/proxmox-ve-config/src/host/mod.rs b/proxmox-ve-config/src/host/mod.rs deleted file mode 100644 index b5614dd..0000000 --- a/proxmox-ve-config/src/host/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod utils; diff --git a/proxmox-ve-config/src/host/utils.rs b/proxmox-ve-config/src/host/utils.rs deleted file mode 100644 index b1dc8e9..0000000 --- a/proxmox-ve-config/src/host/utils.rs +++ /dev/null @@ -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 { - 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 { - 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 -} diff --git a/proxmox-ve-config/src/lib.rs b/proxmox-ve-config/src/lib.rs deleted file mode 100644 index 856b14f..0000000 --- a/proxmox-ve-config/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod firewall; -pub mod guest; -pub mod host; -- 2.39.5 _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel