all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Stefan Hanreich <s.hanreich@proxmox.com>
To: pve-devel@lists.proxmox.com
Cc: Wolfgang Bumiller <w.bumiller@proxmox.com>
Subject: [pve-devel] [PATCH proxmox-firewall v4 1/9] add proxmox-ve-rs crate - move proxmox-ve-config there
Date: Fri, 15 Nov 2024 13:09:29 +0100	[thread overview]
Message-ID: <20241115120937.169342-2-s.hanreich@proxmox.com> (raw)
In-Reply-To: <20241115120937.169342-1-s.hanreich@proxmox.com>

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Reviewed-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
 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 <w.bumiller@proxmox.com>",
-    "Stefan Hanreich <s.hanreich@proxmox.com>",
-    "Proxmox Support Team <support@proxmox.com>",
-]
-description = "Proxmox VE config parsing"
-license = "AGPL-3"
-
-[dependencies]
-log = "0.4"
-anyhow = "1"
-nix = "0.26"
-
-serde = { version = "1", features = [ "derive" ] }
-serde_json = "1"
-serde_plain = "1"
-serde_with = "3"
-
-proxmox-schema = "3.1.2"
-proxmox-sys = "0.6"
-proxmox-sortable-macro = "0.1.3"
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<Options>,
-}
-
-/// default setting for [`Config::is_enabled()`]
-pub const CLUSTER_ENABLED_DEFAULT: bool = false;
-/// default setting for [`Config::ebtables()`]
-pub const CLUSTER_EBTABLES_DEFAULT: bool = false;
-/// default setting for [`Config::default_policy()`]
-pub const CLUSTER_POLICY_IN_DEFAULT: Verdict = Verdict::Drop;
-/// default setting for [`Config::default_policy()`]
-pub const CLUSTER_POLICY_OUT_DEFAULT: Verdict = Verdict::Accept;
-
-impl Config {
-    pub fn parse<R: io::BufRead>(input: R) -> Result<Self, Error> {
-        let parser_config = ParserConfig {
-            guest_iface_names: false,
-            ipset_scope: Some(IpsetScope::Datacenter),
-        };
-
-        Ok(Self {
-            config: super::common::Config::parse(input, &parser_config)?,
-        })
-    }
-
-    pub fn rules(&self) -> &Vec<Rule> {
-        &self.config.rules
-    }
-
-    pub fn groups(&self) -> &BTreeMap<String, Group> {
-        &self.config.groups
-    }
-
-    pub fn ipsets(&self) -> &BTreeMap<String, Ipset> {
-        &self.config.ipsets
-    }
-
-    pub fn alias(&self, name: &str) -> Option<&Alias> {
-        self.config.alias(name)
-    }
-
-    pub fn is_enabled(&self) -> bool {
-        self.config
-            .options
-            .enable
-            .unwrap_or(CLUSTER_ENABLED_DEFAULT)
-    }
-
-    /// returns the ebtables option from the cluster config or [`CLUSTER_EBTABLES_DEFAULT`] if
-    /// unset
-    ///
-    /// this setting is leftover from the old firewall, but has no effect on the nftables firewall
-    pub fn ebtables(&self) -> bool {
-        self.config
-            .options
-            .ebtables
-            .unwrap_or(CLUSTER_EBTABLES_DEFAULT)
-    }
-
-    /// returns policy_in / out or [`CLUSTER_POLICY_IN_DEFAULT`] / [`CLUSTER_POLICY_OUT_DEFAULT`] if
-    /// unset
-    pub fn default_policy(&self, dir: Direction) -> Verdict {
-        match dir {
-            Direction::In => self
-                .config
-                .options
-                .policy_in
-                .unwrap_or(CLUSTER_POLICY_IN_DEFAULT),
-            Direction::Out => self
-                .config
-                .options
-                .policy_out
-                .unwrap_or(CLUSTER_POLICY_OUT_DEFAULT),
-        }
-    }
-
-    /// returns the rate_limit for logs or [`None`] if rate limiting is disabled
-    ///
-    /// If there is no rate limit set, then [`LogRateLimit::default`] is used
-    pub fn log_ratelimit(&self) -> Option<LogRateLimit> {
-        let rate_limit = self
-            .config
-            .options
-            .log_ratelimit
-            .clone()
-            .unwrap_or_default();
-
-        match rate_limit.enabled() {
-            true => Some(rate_limit),
-            false => None,
-        }
-    }
-}
-
-#[derive(Debug, Default, Deserialize)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Options {
-    #[serde(default, with = "serde_option_bool")]
-    enable: Option<bool>,
-
-    #[serde(default, with = "serde_option_bool")]
-    ebtables: Option<bool>,
-
-    #[serde(default, with = "serde_option_log_ratelimit")]
-    log_ratelimit: Option<LogRateLimit>,
-
-    policy_in: Option<Verdict>,
-    policy_out: Option<Verdict>,
-}
-
-#[cfg(test)]
-mod tests {
-    use crate::firewall::types::{
-        address::IpList,
-        alias::{AliasName, AliasScope},
-        ipset::{IpsetAddress, IpsetEntry},
-        log::{LogLevel, LogRateLimitTimescale},
-        rule::{Kind, RuleGroup},
-        rule_match::{
-            Icmpv6, Icmpv6Code, IpAddrMatch, IpMatch, Ports, Protocol, RuleMatch, Tcp, Udp,
-        },
-        Cidr,
-    };
-
-    use super::*;
-
-    #[test]
-    fn test_parse_config() {
-        const CONFIG: &str = r#"
-[OPTIONS]
-enable: 1
-log_ratelimit: 1,rate=10/second,burst=20
-ebtables: 0
-policy_in: REJECT
-policy_out: REJECT
-
-[ALIASES]
-
-another 8.8.8.18
-analias 7.7.0.0/16 # much
-wide cccc::/64
-
-[IPSET a-set]
-
-!5.5.5.5
-1.2.3.4/30
-dc/analias # a comment
-dc/wide
-dddd::/96
-
-[RULES]
-
-GROUP tgr -i eth0 # acomm
-IN ACCEPT -p udp -dport 33 -sport 22 -log warning
-
-[group tgr] # comment for tgr
-
-|OUT ACCEPT -source fe80::1/48 -dest dddd:3:3::9/64 -p icmpv6 -log nolog -icmp-type port-unreachable
-OUT ACCEPT -p tcp -sport 33 -log nolog
-IN BGP(REJECT) -log crit -source 1.2.3.4
-"#;
-
-        let mut config = CONFIG.as_bytes();
-        let config = Config::parse(&mut config).unwrap();
-
-        assert_eq!(
-            config.config.options,
-            Options {
-                ebtables: Some(false),
-                enable: Some(true),
-                log_ratelimit: Some(LogRateLimit::new(
-                    true,
-                    10,
-                    LogRateLimitTimescale::Second,
-                    20
-                )),
-                policy_in: Some(Verdict::Reject),
-                policy_out: Some(Verdict::Reject),
-            }
-        );
-
-        assert_eq!(config.config.aliases.len(), 3);
-
-        assert_eq!(
-            config.config.aliases["another"],
-            Alias::new("another", Cidr::new_v4([8, 8, 8, 18], 32).unwrap(), None),
-        );
-
-        assert_eq!(
-            config.config.aliases["analias"],
-            Alias::new(
-                "analias",
-                Cidr::new_v4([7, 7, 0, 0], 16).unwrap(),
-                "much".to_string()
-            ),
-        );
-
-        assert_eq!(
-            config.config.aliases["wide"],
-            Alias::new(
-                "wide",
-                Cidr::new_v6(
-                    [0xCCCC, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x000],
-                    64
-                )
-                .unwrap(),
-                None
-            ),
-        );
-
-        assert_eq!(config.config.ipsets.len(), 1);
-
-        let mut ipset_elements = vec![
-            IpsetEntry {
-                nomatch: true,
-                address: Cidr::new_v4([5, 5, 5, 5], 32).unwrap().into(),
-                comment: None,
-            },
-            IpsetEntry {
-                nomatch: false,
-                address: Cidr::new_v4([1, 2, 3, 4], 30).unwrap().into(),
-                comment: None,
-            },
-            IpsetEntry {
-                nomatch: false,
-                address: IpsetAddress::Alias(AliasName::new(AliasScope::Datacenter, "analias")),
-                comment: Some("a comment".to_string()),
-            },
-            IpsetEntry {
-                nomatch: false,
-                address: IpsetAddress::Alias(AliasName::new(AliasScope::Datacenter, "wide")),
-                comment: None,
-            },
-            IpsetEntry {
-                nomatch: false,
-                address: Cidr::new_v6([0xdd, 0xdd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 96)
-                    .unwrap()
-                    .into(),
-                comment: None,
-            },
-        ];
-
-        let mut ipset = Ipset::from_parts(IpsetScope::Datacenter, "a-set");
-        ipset.append(&mut ipset_elements);
-
-        assert_eq!(config.config.ipsets["a-set"], ipset,);
-
-        assert_eq!(config.config.rules.len(), 2);
-
-        assert_eq!(
-            config.config.rules[0],
-            Rule {
-                disabled: false,
-                comment: Some("acomm".to_string()),
-                kind: Kind::Group(RuleGroup {
-                    group: "tgr".to_string(),
-                    iface: Some("eth0".to_string()),
-                }),
-            },
-        );
-
-        assert_eq!(
-            config.config.rules[1],
-            Rule {
-                disabled: false,
-                comment: None,
-                kind: Kind::Match(RuleMatch {
-                    dir: Direction::In,
-                    verdict: Verdict::Accept,
-                    proto: Some(Protocol::Udp(Udp::new(Ports::from_u16(22, 33)))),
-                    log: Some(LogLevel::Warning),
-                    ..Default::default()
-                }),
-            },
-        );
-
-        assert_eq!(config.config.groups.len(), 1);
-
-        let entry = &config.config.groups["tgr"];
-        assert_eq!(entry.comment(), Some("comment for tgr"));
-        assert_eq!(entry.rules().len(), 3);
-
-        assert_eq!(
-            entry.rules()[0],
-            Rule {
-                disabled: true,
-                comment: None,
-                kind: Kind::Match(RuleMatch {
-                    dir: Direction::Out,
-                    verdict: Verdict::Accept,
-                    ip: Some(IpMatch {
-                        src: Some(IpAddrMatch::Ip(IpList::from(
-                            Cidr::new_v6(
-                                [0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
-                                48
-                            )
-                            .unwrap()
-                        ))),
-                        dst: Some(IpAddrMatch::Ip(IpList::from(
-                            Cidr::new_v6(
-                                [0xdd, 0xdd, 0, 3, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9],
-                                64
-                            )
-                            .unwrap()
-                        ))),
-                    }),
-                    proto: Some(Protocol::Icmpv6(Icmpv6::new_code(Icmpv6Code::Named(
-                        "port-unreachable"
-                    )))),
-                    log: Some(LogLevel::Nolog),
-                    ..Default::default()
-                }),
-            },
-        );
-        assert_eq!(
-            entry.rules()[1],
-            Rule {
-                disabled: false,
-                comment: None,
-                kind: Kind::Match(RuleMatch {
-                    dir: Direction::Out,
-                    verdict: Verdict::Accept,
-                    proto: Some(Protocol::Tcp(Tcp::new(Ports::from_u16(33, None)))),
-                    log: Some(LogLevel::Nolog),
-                    ..Default::default()
-                }),
-            },
-        );
-
-        assert_eq!(
-            entry.rules()[2],
-            Rule {
-                disabled: false,
-                comment: None,
-                kind: Kind::Match(RuleMatch {
-                    dir: Direction::In,
-                    verdict: Verdict::Reject,
-                    log: Some(LogLevel::Critical),
-                    fw_macro: Some("BGP".to_string()),
-                    ip: Some(IpMatch {
-                        src: Some(IpAddrMatch::Ip(IpList::from(
-                            Cidr::new_v4([1, 2, 3, 4], 32).unwrap()
-                        ))),
-                        dst: None,
-                    }),
-                    ..Default::default()
-                }),
-            },
-        );
-
-        let empty_config = Config::parse("".as_bytes()).expect("empty config is invalid");
-
-        assert_eq!(empty_config.config.options, Options::default());
-        assert!(empty_config.config.rules.is_empty());
-        assert!(empty_config.config.aliases.is_empty());
-        assert!(empty_config.config.ipsets.is_empty());
-        assert!(empty_config.config.groups.is_empty());
-    }
-}
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<O>
-where
-    O: Default + std::fmt::Debug + serde::de::DeserializeOwned,
-{
-    pub(crate) options: O,
-    pub(crate) rules: Vec<Rule>,
-    pub(crate) aliases: BTreeMap<String, Alias>,
-    pub(crate) ipsets: BTreeMap<String, Ipset>,
-    pub(crate) groups: BTreeMap<String, Group>,
-}
-
-enum Sec {
-    None,
-    Options,
-    Aliases,
-    Rules,
-    Ipset(String, Ipset),
-    Group(String, Group),
-}
-
-#[derive(Default)]
-pub struct ParserConfig {
-    /// Network interfaces must be of the form `netX`.
-    pub guest_iface_names: bool,
-    pub ipset_scope: Option<IpsetScope>,
-}
-
-impl<O> Config<O>
-where
-    O: Default + std::fmt::Debug + serde::de::DeserializeOwned,
-{
-    pub fn new() -> Self {
-        Self::default()
-    }
-
-    pub fn parse<R: io::BufRead>(input: R, parser_cfg: &ParserConfig) -> Result<Self, Error> {
-        let mut section = Sec::None;
-
-        let mut this = Self::new();
-        let mut options = HashMap::new();
-
-        for line in input.lines() {
-            let line = line?;
-            let line = line.trim();
-
-            if line.is_empty() || line.starts_with('#') {
-                continue;
-            }
-
-            log::trace!("parsing config line {line}");
-
-            if line.eq_ignore_ascii_case("[OPTIONS]") {
-                this.set_section(&mut section, Sec::Options)?;
-            } else if line.eq_ignore_ascii_case("[ALIASES]") {
-                this.set_section(&mut section, Sec::Aliases)?;
-            } else if line.eq_ignore_ascii_case("[RULES]") {
-                this.set_section(&mut section, Sec::Rules)?;
-            } else if let Some(line) = line.strip_prefix("[IPSET") {
-                let (name, comment) = parse_named_section_tail("ipset", line)?;
-
-                let scope = parser_cfg.ipset_scope.ok_or_else(|| {
-                    format_err!("IPSET in config, but no scope set in parser config")
-                })?;
-
-                let ipset_name = IpsetName::new(scope, name.to_string());
-                let mut ipset = Ipset::new(ipset_name);
-                ipset.comment = comment.map(str::to_owned);
-
-                this.set_section(&mut section, Sec::Ipset(name.to_string(), ipset))?;
-            } else if let Some(line) = line.strip_prefix("[group") {
-                let (name, comment) = parse_named_section_tail("group", line)?;
-                let mut group = Group::new();
-
-                group.set_comment(comment.map(str::to_owned));
-
-                this.set_section(&mut section, Sec::Group(name.to_owned(), group))?;
-            } else if line.starts_with('[') {
-                bail!("invalid section {line:?}");
-            } else {
-                match &mut section {
-                    Sec::None => bail!("config line with no section: {line:?}"),
-                    Sec::Options => Self::parse_option(line, &mut options)?,
-                    Sec::Aliases => this.parse_alias(line)?,
-                    Sec::Rules => this.parse_rule(line, parser_cfg)?,
-                    Sec::Ipset(_name, ipset) => ipset.parse_entry(line)?,
-                    Sec::Group(_name, group) => group.parse_entry(line)?,
-                }
-            }
-        }
-        this.set_section(&mut section, Sec::None)?;
-
-        this.options = O::deserialize(IntoDeserializer::<
-            '_,
-            crate::firewall::parse::SerdeStringError,
-        >::into_deserializer(options))?;
-
-        Ok(this)
-    }
-
-    fn parse_option(line: &str, options: &mut HashMap<String, SomeString>) -> Result<(), Error> {
-        let (key, value) = split_key_value(line)
-            .ok_or_else(|| format_err!("expected colon separated key and value, found {line:?}"))?;
-
-        if options.insert(key.to_string(), value.into()).is_some() {
-            bail!("duplicate option {key:?}");
-        }
-
-        Ok(())
-    }
-
-    fn parse_alias(&mut self, line: &str) -> Result<(), Error> {
-        let alias: Alias = line.parse()?;
-
-        if self
-            .aliases
-            .insert(alias.name().to_string(), alias)
-            .is_some()
-        {
-            bail!("duplicate alias: {line}");
-        }
-
-        Ok(())
-    }
-
-    fn parse_rule(&mut self, line: &str, parser_cfg: &ParserConfig) -> Result<(), Error> {
-        let rule: Rule = line.parse()?;
-
-        if parser_cfg.guest_iface_names {
-            if let Some(iface) = rule.iface() {
-                let _ = iface
-                    .strip_prefix("net")
-                    .ok_or_else(|| {
-                        format_err!("interface name must be of the form \"net<number>\"")
-                    })?
-                    .parse::<u16>()
-                    .map_err(|_| {
-                        format_err!("interface name must be of the form \"net<number>\"")
-                    })?;
-            }
-        }
-
-        self.rules.push(rule);
-        Ok(())
-    }
-
-    fn set_section(&mut self, sec: &mut Sec, to: Sec) -> Result<(), Error> {
-        let prev = std::mem::replace(sec, to);
-
-        match prev {
-            Sec::Ipset(name, ipset) => {
-                if self.ipsets.insert(name.clone(), ipset).is_some() {
-                    bail!("duplicate ipset: {name:?}");
-                }
-            }
-            Sec::Group(name, group) => {
-                if self.groups.insert(name.clone(), group).is_some() {
-                    bail!("duplicate group: {name:?}");
-                }
-            }
-            _ => (),
-        }
-
-        Ok(())
-    }
-
-    pub fn ipsets(&self) -> &BTreeMap<String, Ipset> {
-        &self.ipsets
-    }
-
-    pub fn alias(&self, name: &str) -> Option<&Alias> {
-        self.aliases.get(name)
-    }
-}
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<bool>,
-    pub v6: Option<bool>,
-    pub name: String,
-    pub tcp: Option<u16>,
-    pub udp: Option<u16>,
-}
-
-impl TryFrom<CtHelperMacroJson> for CtHelperMacro {
-    type Error = Error;
-
-    fn try_from(value: CtHelperMacroJson) -> Result<Self, Self::Error> {
-        if value.tcp.is_none() && value.udp.is_none() {
-            bail!("Neither TCP nor UDP port set in CT helper!");
-        }
-
-        let family = match (value.v4, value.v6) {
-            (Some(true), Some(true)) => None,
-            (Some(true), _) => Some(Family::V4),
-            (_, Some(true)) => Some(Family::V6),
-            _ => bail!("Neither v4 nor v6 set in CT Helper Macro!"),
-        };
-
-        let mut ct_helper = CtHelperMacro {
-            family,
-            name: value.name,
-            tcp: None,
-            udp: None,
-        };
-
-        if let Some(dport) = value.tcp {
-            let ports = Ports::from_u16(None, dport);
-            ct_helper.tcp = Some(Tcp::new(ports).into());
-        }
-
-        if let Some(dport) = value.udp {
-            let ports = Ports::from_u16(None, dport);
-            ct_helper.udp = Some(Udp::new(ports).into());
-        }
-
-        Ok(ct_helper)
-    }
-}
-
-#[derive(Clone, Debug, Deserialize)]
-#[serde(try_from = "CtHelperMacroJson")]
-pub struct CtHelperMacro {
-    family: Option<Family>,
-    name: String,
-    tcp: Option<Protocol>,
-    udp: Option<Protocol>,
-}
-
-impl CtHelperMacro {
-    fn helper_name(&self, protocol: &str) -> String {
-        format!("helper-{}-{protocol}", self.name)
-    }
-
-    pub fn tcp_helper_name(&self) -> String {
-        self.helper_name("tcp")
-    }
-
-    pub fn udp_helper_name(&self) -> String {
-        self.helper_name("udp")
-    }
-
-    pub fn family(&self) -> Option<Family> {
-        self.family
-    }
-
-    pub fn name(&self) -> &str {
-        self.name.as_ref()
-    }
-
-    pub fn tcp(&self) -> Option<&Protocol> {
-        self.tcp.as_ref()
-    }
-
-    pub fn udp(&self) -> Option<&Protocol> {
-        self.udp.as_ref()
-    }
-}
-
-fn hashmap() -> &'static HashMap<String, CtHelperMacro> {
-    const MACROS: &str = include_str!("../../resources/ct_helper.json");
-    static HASHMAP: OnceLock<HashMap<String, CtHelperMacro>> = OnceLock::new();
-
-    HASHMAP.get_or_init(|| {
-        let macro_data: Vec<CtHelperMacro> = match serde_json::from_str(MACROS) {
-            Ok(data) => data,
-            Err(err) => {
-                log::error!("could not load data for ct helpers: {err}");
-                Vec::new()
-            }
-        };
-
-        macro_data
-            .into_iter()
-            .map(|elem| (elem.name.clone(), elem))
-            .collect()
-    })
-}
-
-pub fn get_cthelper(name: &str) -> Option<&'static CtHelperMacro> {
-    hashmap().get(name)
-}
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<RuleOptions>,
-}
-
-#[derive(Clone, Debug, Default)]
-pub struct FwMacro {
-    pub _description: &'static str,
-    pub code: Vec<Protocol>,
-}
-
-fn macros() -> &'static HashMap<String, FwMacro> {
-    const MACROS: &str = include_str!("../../resources/macros.json");
-    static HASHMAP: OnceLock<HashMap<String, FwMacro>> = OnceLock::new();
-
-    HASHMAP.get_or_init(|| {
-        let macro_data: HashMap<String, FwMacroData> = match serde_json::from_str(MACROS) {
-            Ok(m) => m,
-            Err(err) => {
-                log::error!("could not load data for macros: {err}");
-                HashMap::new()
-            }
-        };
-
-        let mut macros = HashMap::new();
-
-        'outer: for (name, data) in macro_data {
-            let mut code = Vec::new();
-
-            for c in data.code {
-                match Protocol::from_options(&c) {
-                    Ok(Some(p)) => code.push(p),
-                    Ok(None) => {
-                        continue 'outer;
-                    }
-                    Err(err) => {
-                        log::error!("could not parse data for macro {name}: {err}");
-                        continue 'outer;
-                    }
-                }
-            }
-
-            macros.insert(
-                name,
-                FwMacro {
-                    _description: data.description,
-                    code,
-                },
-            );
-        }
-
-        macros
-    })
-}
-
-pub fn get_macro(name: &str) -> Option<&'static FwMacro> {
-    macros().get(name)
-}
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<bool>,
-
-    #[serde(default, with = "serde_option_bool")]
-    enable: Option<bool>,
-
-    #[serde(default, with = "serde_option_bool")]
-    ipfilter: Option<bool>,
-
-    #[serde(default, with = "serde_option_bool")]
-    ndp: Option<bool>,
-
-    #[serde(default, with = "serde_option_bool")]
-    radv: Option<bool>,
-
-    log_level_in: Option<LogLevel>,
-    log_level_out: Option<LogLevel>,
-
-    #[serde(default, with = "serde_option_bool")]
-    macfilter: Option<bool>,
-
-    #[serde(rename = "policy_in")]
-    policy_in: Option<Verdict>,
-
-    #[serde(rename = "policy_out")]
-    policy_out: Option<Verdict>,
-}
-
-#[derive(Debug)]
-pub struct Config {
-    vmid: Vmid,
-
-    /// The interface prefix: "veth" for containers, "tap" for VMs.
-    iface_prefix: &'static str,
-
-    network_config: NetworkConfig,
-    config: super::common::Config<Options>,
-}
-
-impl Config {
-    pub fn parse<T: io::BufRead, U: io::BufRead>(
-        vmid: &Vmid,
-        iface_prefix: &'static str,
-        firewall_input: T,
-        network_input: U,
-    ) -> Result<Self, Error> {
-        let parser_cfg = super::common::ParserConfig {
-            guest_iface_names: true,
-            ipset_scope: Some(IpsetScope::Guest),
-        };
-
-        let config = super::common::Config::parse(firewall_input, &parser_cfg)?;
-        if !config.groups.is_empty() {
-            bail!("guest firewall config cannot declare groups");
-        }
-
-        let network_config = NetworkConfig::parse(network_input)?;
-
-        Ok(Self {
-            vmid: *vmid,
-            iface_prefix,
-            config,
-            network_config,
-        })
-    }
-
-    pub fn vmid(&self) -> Vmid {
-        self.vmid
-    }
-
-    pub fn alias(&self, name: &AliasName) -> Option<&Alias> {
-        self.config.alias(name.name())
-    }
-
-    pub fn iface_name_by_key(&self, key: &str) -> Result<String, Error> {
-        let index = NetworkConfig::index_from_net_key(key)?;
-        Ok(format!("{}{}i{index}", self.iface_prefix, self.vmid))
-    }
-
-    pub fn iface_name_by_index(&self, index: i64) -> String {
-        format!("{}{}i{index}", self.iface_prefix, self.vmid)
-    }
-
-    /// returns the value of the enabled config key or [`GUEST_ENABLED_DEFAULT`] if unset
-    pub fn is_enabled(&self) -> bool {
-        self.config.options.enable.unwrap_or(GUEST_ENABLED_DEFAULT)
-    }
-
-    pub fn rules(&self) -> &[Rule] {
-        &self.config.rules
-    }
-
-    pub fn log_level(&self, dir: Direction) -> LogLevel {
-        match dir {
-            Direction::In => self.config.options.log_level_in.unwrap_or_default(),
-            Direction::Out => self.config.options.log_level_out.unwrap_or_default(),
-        }
-    }
-
-    /// returns the value of the ndp config key or [`GUEST_ALLOW_NDP_DEFAULT`] if unset
-    pub fn allow_ndp(&self) -> bool {
-        self.config.options.ndp.unwrap_or(GUEST_ALLOW_NDP_DEFAULT)
-    }
-
-    /// returns the value of the dhcp config key or [`GUEST_ALLOW_DHCP_DEFAULT`] if unset
-    pub fn allow_dhcp(&self) -> bool {
-        self.config.options.dhcp.unwrap_or(GUEST_ALLOW_DHCP_DEFAULT)
-    }
-
-    /// returns the value of the radv config key or [`GUEST_ALLOW_RA_DEFAULT`] if unset
-    pub fn allow_ra(&self) -> bool {
-        self.config.options.radv.unwrap_or(GUEST_ALLOW_RA_DEFAULT)
-    }
-
-    /// returns the value of the macfilter config key or [`GUEST_MACFILTER_DEFAULT`] if unset
-    pub fn macfilter(&self) -> bool {
-        self.config
-            .options
-            .macfilter
-            .unwrap_or(GUEST_MACFILTER_DEFAULT)
-    }
-
-    /// returns the value of the ipfilter config key or [`GUEST_IPFILTER_DEFAULT`] if unset
-    pub fn ipfilter(&self) -> bool {
-        self.config
-            .options
-            .ipfilter
-            .unwrap_or(GUEST_IPFILTER_DEFAULT)
-    }
-
-    /// returns the value of the policy_in/out config key or
-    /// [`GUEST_POLICY_IN_DEFAULT`] / [`GUEST_POLICY_OUT_DEFAULT`] if unset
-    pub fn default_policy(&self, dir: Direction) -> Verdict {
-        match dir {
-            Direction::In => self
-                .config
-                .options
-                .policy_in
-                .unwrap_or(GUEST_POLICY_IN_DEFAULT),
-            Direction::Out => self
-                .config
-                .options
-                .policy_out
-                .unwrap_or(GUEST_POLICY_OUT_DEFAULT),
-        }
-    }
-
-    pub fn network_config(&self) -> &NetworkConfig {
-        &self.network_config
-    }
-
-    pub fn ipsets(&self) -> &BTreeMap<String, Ipset> {
-        self.config.ipsets()
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_parse_config() {
-        // most of the stuff is already tested in cluster parsing, only testing
-        // guest specific options here
-        const CONFIG: &str = r#"
-[OPTIONS]
-enable: 1
-dhcp: 1
-ipfilter: 0
-log_level_in: emerg
-log_level_out: crit
-macfilter: 0
-ndp:1
-radv:1
-policy_in: REJECT
-policy_out: REJECT
-"#;
-
-        let config = CONFIG.as_bytes();
-        let network_config: Vec<u8> = Vec::new();
-        let config =
-            Config::parse(&Vmid::new(100), "tap", config, network_config.as_slice()).unwrap();
-
-        assert_eq!(
-            config.config.options,
-            Options {
-                dhcp: Some(true),
-                enable: Some(true),
-                ipfilter: Some(false),
-                ndp: Some(true),
-                radv: Some(true),
-                log_level_in: Some(LogLevel::Emergency),
-                log_level_out: Some(LogLevel::Critical),
-                macfilter: Some(false),
-                policy_in: Some(Verdict::Reject),
-                policy_out: Some(Verdict::Reject),
-            }
-        );
-    }
-}
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<bool>,
-
-    #[serde(default, with = "parse::serde_option_bool")]
-    nftables: Option<bool>,
-
-    log_level_in: Option<LogLevel>,
-    log_level_out: Option<LogLevel>,
-
-    #[serde(default, with = "parse::serde_option_bool")]
-    log_nf_conntrack: Option<bool>,
-    #[serde(default, with = "parse::serde_option_bool")]
-    ndp: Option<bool>,
-
-    #[serde(default, with = "parse::serde_option_bool")]
-    nf_conntrack_allow_invalid: Option<bool>,
-
-    // is Option<Vec<>> for easier deserialization
-    #[serde(default, with = "parse::serde_option_conntrack_helpers")]
-    nf_conntrack_helpers: Option<Vec<String>>,
-
-    #[serde(default, with = "parse::serde_option_number")]
-    nf_conntrack_max: Option<i64>,
-    #[serde(default, with = "parse::serde_option_number")]
-    nf_conntrack_tcp_timeout_established: Option<i64>,
-    #[serde(default, with = "parse::serde_option_number")]
-    nf_conntrack_tcp_timeout_syn_recv: Option<i64>,
-
-    #[serde(default, with = "parse::serde_option_bool")]
-    nosmurfs: Option<bool>,
-
-    #[serde(default, with = "parse::serde_option_bool")]
-    protection_synflood: Option<bool>,
-    #[serde(default, with = "parse::serde_option_number")]
-    protection_synflood_burst: Option<i64>,
-    #[serde(default, with = "parse::serde_option_number")]
-    protection_synflood_rate: Option<i64>,
-
-    smurf_log_level: Option<LogLevel>,
-    tcp_flags_log_level: Option<LogLevel>,
-
-    #[serde(default, with = "parse::serde_option_bool")]
-    tcpflags: Option<bool>,
-}
-
-#[derive(Debug, Default)]
-pub struct Config {
-    pub(crate) config: super::common::Config<Options>,
-}
-
-impl Config {
-    pub fn new() -> Self {
-        Self {
-            config: Default::default(),
-        }
-    }
-
-    pub fn parse<R: io::BufRead>(input: R) -> Result<Self, Error> {
-        let config = super::common::Config::parse(input, &Default::default())?;
-
-        if !config.groups.is_empty() {
-            bail!("host firewall config cannot declare groups");
-        }
-
-        if !config.aliases.is_empty() {
-            bail!("host firewall config cannot declare aliases");
-        }
-
-        if !config.ipsets.is_empty() {
-            bail!("host firewall config cannot declare ipsets");
-        }
-
-        Ok(Self { config })
-    }
-
-    pub fn rules(&self) -> &[Rule] {
-        &self.config.rules
-    }
-
-    pub fn management_ips() -> Result<Vec<Cidr>, Error> {
-        let mut management_cidrs = Vec::new();
-
-        for host_ip in host_ips() {
-            for network_interface_cidr in network_interface_cidrs() {
-                match (host_ip, network_interface_cidr) {
-                    (IpAddr::V4(ip), Cidr::Ipv4(cidr)) => {
-                        if cidr.contains_address(&ip) {
-                            management_cidrs.push(network_interface_cidr);
-                        }
-                    }
-                    (IpAddr::V6(ip), Cidr::Ipv6(cidr)) => {
-                        if cidr.contains_address(&ip) {
-                            management_cidrs.push(network_interface_cidr);
-                        }
-                    }
-                    _ => continue,
-                };
-            }
-        }
-
-        Ok(management_cidrs)
-    }
-
-    pub fn hostname() -> &'static str {
-        nodename()
-    }
-
-    pub fn get_alias(&self, name: &str) -> Option<&Alias> {
-        self.config.alias(name)
-    }
-
-    /// returns value of enabled key or [`HOST_ENABLED_DEFAULT`] if unset
-    pub fn is_enabled(&self) -> bool {
-        self.config.options.enable.unwrap_or(HOST_ENABLED_DEFAULT)
-    }
-
-    /// returns value of nftables key or [`HOST_NFTABLES_DEFAULT`] if unset
-    pub fn nftables(&self) -> bool {
-        self.config
-            .options
-            .nftables
-            .unwrap_or(HOST_NFTABLES_DEFAULT)
-    }
-
-    /// returns value of ndp key or [`HOST_ALLOW_NDP_DEFAULT`] if unset
-    pub fn allow_ndp(&self) -> bool {
-        self.config.options.ndp.unwrap_or(HOST_ALLOW_NDP_DEFAULT)
-    }
-
-    /// returns value of nosmurfs key or [`HOST_BLOCK_SMURFS_DEFAULT`] if unset
-    pub fn block_smurfs(&self) -> bool {
-        self.config
-            .options
-            .nosmurfs
-            .unwrap_or(HOST_BLOCK_SMURFS_DEFAULT)
-    }
-
-    /// returns the log level for the smurf protection rule
-    ///
-    /// If there is no log level set, it returns [`LogLevel::default()`]
-    pub fn block_smurfs_log_level(&self) -> LogLevel {
-        self.config.options.smurf_log_level.unwrap_or_default()
-    }
-
-    /// returns value of protection_synflood key or [`HOST_BLOCK_SYNFLOOD_DEFAULT`] if unset
-    pub fn block_synflood(&self) -> bool {
-        self.config
-            .options
-            .protection_synflood
-            .unwrap_or(HOST_BLOCK_SYNFLOOD_DEFAULT)
-    }
-
-    /// returns value of protection_synflood_rate key or [`HOST_BLOCK_SYNFLOOD_RATE_DEFAULT`] if
-    /// unset
-    pub fn synflood_rate(&self) -> i64 {
-        self.config
-            .options
-            .protection_synflood_rate
-            .unwrap_or(HOST_BLOCK_SYNFLOOD_RATE_DEFAULT)
-    }
-
-    /// returns value of protection_synflood_burst key or [`HOST_BLOCK_SYNFLOOD_BURST_DEFAULT`] if
-    /// unset
-    pub fn synflood_burst(&self) -> i64 {
-        self.config
-            .options
-            .protection_synflood_burst
-            .unwrap_or(HOST_BLOCK_SYNFLOOD_BURST_DEFAULT)
-    }
-
-    /// returns value of tcpflags key or [`HOST_BLOCK_INVALID_TCP_DEFAULT`] if unset
-    pub fn block_invalid_tcp(&self) -> bool {
-        self.config
-            .options
-            .tcpflags
-            .unwrap_or(HOST_BLOCK_INVALID_TCP_DEFAULT)
-    }
-
-    /// returns the log level for the block invalid TCP packets rule
-    ///
-    /// If there is no log level set, it returns [`LogLevel::default()`]
-    pub fn block_invalid_tcp_log_level(&self) -> LogLevel {
-        self.config.options.tcp_flags_log_level.unwrap_or_default()
-    }
-
-    /// returns value of nf_conntrack_allow_invalid key or [`HOST_BLOCK_INVALID_CONNTRACK`] if
-    /// unset
-    pub fn block_invalid_conntrack(&self) -> bool {
-        !self
-            .config
-            .options
-            .nf_conntrack_allow_invalid
-            .unwrap_or(HOST_BLOCK_INVALID_CONNTRACK)
-    }
-
-    pub fn nf_conntrack_max(&self) -> Option<i64> {
-        self.config.options.nf_conntrack_max
-    }
-
-    pub fn nf_conntrack_tcp_timeout_established(&self) -> Option<i64> {
-        self.config.options.nf_conntrack_tcp_timeout_established
-    }
-
-    pub fn nf_conntrack_tcp_timeout_syn_recv(&self) -> Option<i64> {
-        self.config.options.nf_conntrack_tcp_timeout_syn_recv
-    }
-
-    /// returns value of log_nf_conntrack key or [`HOST_LOG_INVALID_CONNTRACK`] if unset
-    pub fn log_nf_conntrack(&self) -> bool {
-        self.config
-            .options
-            .log_nf_conntrack
-            .unwrap_or(HOST_LOG_INVALID_CONNTRACK)
-    }
-
-    pub fn conntrack_helpers(&self) -> Option<&Vec<String>> {
-        self.config.options.nf_conntrack_helpers.as_ref()
-    }
-
-    /// returns the log level for the given direction
-    ///
-    /// If there is no log level set it returns [`LogLevel::default()`]
-    pub fn log_level(&self, dir: Direction) -> LogLevel {
-        match dir {
-            Direction::In => self.config.options.log_level_in.unwrap_or_default(),
-            Direction::Out => self.config.options.log_level_out.unwrap_or_default(),
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use crate::firewall::types::{
-        log::LogLevel,
-        rule::{Kind, RuleGroup, Verdict},
-        rule_match::{Ports, Protocol, RuleMatch, Udp},
-    };
-
-    use super::*;
-
-    #[test]
-    fn test_parse_config() {
-        const CONFIG: &str = r#"
-[OPTIONS]
-enable: 1
-nftables: 1
-log_level_in: debug
-log_level_out: emerg
-log_nf_conntrack: 0
-ndp: 1
-nf_conntrack_allow_invalid: yes
-nf_conntrack_helpers: ftp
-nf_conntrack_max: 44000
-nf_conntrack_tcp_timeout_established: 500000
-nf_conntrack_tcp_timeout_syn_recv: 44
-nosmurfs: no
-protection_synflood: 1
-protection_synflood_burst: 2500
-protection_synflood_rate: 300
-smurf_log_level: notice
-tcp_flags_log_level: nolog
-tcpflags: yes
-
-[RULES]
-
-GROUP tgr -i eth0 # acomm
-IN ACCEPT -p udp -dport 33 -sport 22 -log warning
-
-"#;
-
-        let mut config = CONFIG.as_bytes();
-        let config = Config::parse(&mut config).unwrap();
-
-        assert_eq!(
-            config.config.options,
-            Options {
-                enable: Some(true),
-                nftables: Some(true),
-                log_level_in: Some(LogLevel::Debug),
-                log_level_out: Some(LogLevel::Emergency),
-                log_nf_conntrack: Some(false),
-                ndp: Some(true),
-                nf_conntrack_allow_invalid: Some(true),
-                nf_conntrack_helpers: Some(vec!["ftp".to_string()]),
-                nf_conntrack_max: Some(44000),
-                nf_conntrack_tcp_timeout_established: Some(500000),
-                nf_conntrack_tcp_timeout_syn_recv: Some(44),
-                nosmurfs: Some(false),
-                protection_synflood: Some(true),
-                protection_synflood_burst: Some(2500),
-                protection_synflood_rate: Some(300),
-                smurf_log_level: Some(LogLevel::Notice),
-                tcp_flags_log_level: Some(LogLevel::Nolog),
-                tcpflags: Some(true),
-            }
-        );
-
-        assert_eq!(config.config.rules.len(), 2);
-
-        assert_eq!(
-            config.config.rules[0],
-            Rule {
-                disabled: false,
-                comment: Some("acomm".to_string()),
-                kind: Kind::Group(RuleGroup {
-                    group: "tgr".to_string(),
-                    iface: Some("eth0".to_string()),
-                }),
-            },
-        );
-
-        assert_eq!(
-            config.config.rules[1],
-            Rule {
-                disabled: false,
-                comment: None,
-                kind: Kind::Match(RuleMatch {
-                    dir: Direction::In,
-                    verdict: Verdict::Accept,
-                    proto: Some(Protocol::Udp(Udp::new(Ports::from_u16(22, 33)))),
-                    log: Some(LogLevel::Warning),
-                    ..Default::default()
-                }),
-            },
-        );
-
-        Config::parse("[ALIASES]\ntest 127.0.0.1".as_bytes())
-            .expect_err("host config cannot contain aliases");
-
-        Config::parse("[GROUP test]".as_bytes()).expect_err("host config cannot contain groups");
-
-        Config::parse("[IPSET test]".as_bytes()).expect_err("host config cannot contain ipsets");
-    }
-}
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<bool, Error> {
-    Ok(
-        if value == "0"
-            || value.eq_ignore_ascii_case("false")
-            || value.eq_ignore_ascii_case("off")
-            || value.eq_ignore_ascii_case("no")
-        {
-            false
-        } else if value == "1"
-            || value.eq_ignore_ascii_case("true")
-            || value.eq_ignore_ascii_case("on")
-            || value.eq_ignore_ascii_case("yes")
-        {
-            true
-        } else {
-            bail!("not a boolean: {value:?}");
-        },
-    )
-}
-
-/// Parse the *remainder* of a section line, that is `<whitespace>NAME] #optional comment`.
-/// The `kind` parameter is used for error messages and should be the section type.
-///
-/// Return the name and the optional comment.
-pub fn parse_named_section_tail<'a>(
-    kind: &'static str,
-    line: &'a str,
-) -> Result<(&'a str, Option<&'a str>), Error> {
-    if line.is_empty() || !line.as_bytes()[0].is_ascii_whitespace() {
-        bail!("incomplete {kind} section");
-    }
-
-    let line = line.trim_start();
-    let (name, line) = match_name(line)
-        .ok_or_else(|| format_err!("expected a name for the {kind} at {line:?}"))?;
-
-    let line = line
-        .strip_prefix(']')
-        .ok_or_else(|| format_err!("expected closing ']' in {kind} section header"))?
-        .trim_start();
-
-    Ok(match line.strip_prefix('#') {
-        Some(comment) => (name, Some(comment.trim())),
-        None if !line.is_empty() => bail!("trailing characters after {kind} section: {line:?}"),
-        None => (name, None),
-    })
-}
-
-// parses a number from a string OR number
-pub mod serde_option_number {
-    use std::fmt;
-
-    use serde::de::{Deserializer, Error, Visitor};
-
-    pub fn deserialize<'de, D: Deserializer<'de>>(
-        deserializer: D,
-    ) -> Result<Option<i64>, D::Error> {
-        struct V;
-
-        impl<'de> Visitor<'de> for V {
-            type Value = Option<i64>;
-
-            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
-                f.write_str("a numerical value")
-            }
-
-            fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
-                v.parse().map_err(E::custom).map(Some)
-            }
-
-            fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
-                Ok(None)
-            }
-
-            fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
-            where
-                D: Deserializer<'de>,
-            {
-                deserializer.deserialize_any(self)
-            }
-        }
-
-        deserializer.deserialize_any(V)
-    }
-}
-
-// parses a bool from a string OR bool
-pub mod serde_option_bool {
-    use std::fmt;
-
-    use serde::de::{Deserializer, Error, Visitor};
-
-    pub fn deserialize<'de, D: Deserializer<'de>>(
-        deserializer: D,
-    ) -> Result<Option<bool>, D::Error> {
-        struct V;
-
-        impl<'de> Visitor<'de> for V {
-            type Value = Option<bool>;
-
-            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
-                f.write_str("a boolean-like value")
-            }
-
-            fn visit_bool<E: Error>(self, v: bool) -> Result<Self::Value, E> {
-                Ok(Some(v))
-            }
-
-            fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
-                super::parse_bool(v).map_err(E::custom).map(Some)
-            }
-
-            fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
-                Ok(None)
-            }
-
-            fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
-            where
-                D: Deserializer<'de>,
-            {
-                deserializer.deserialize_any(self)
-            }
-        }
-
-        deserializer.deserialize_any(V)
-    }
-}
-
-// parses a comma_separated list of strings
-pub mod serde_option_conntrack_helpers {
-    use std::fmt;
-
-    use serde::de::{Deserializer, Error, Visitor};
-
-    pub fn deserialize<'de, D: Deserializer<'de>>(
-        deserializer: D,
-    ) -> Result<Option<Vec<String>>, D::Error> {
-        struct V;
-
-        impl<'de> Visitor<'de> for V {
-            type Value = Option<Vec<String>>;
-
-            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
-                f.write_str("A list of conntrack helpers")
-            }
-
-            fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
-                if v.is_empty() {
-                    return Ok(None);
-                }
-
-                Ok(Some(v.split(',').map(String::from).collect()))
-            }
-
-            fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
-                Ok(None)
-            }
-
-            fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
-            where
-                D: Deserializer<'de>,
-            {
-                deserializer.deserialize_any(self)
-            }
-        }
-
-        deserializer.deserialize_any(V)
-    }
-}
-
-// parses a log_ratelimit string: '[enable=]<1|0> [,burst=<integer>] [,rate=<rate>]'
-pub mod serde_option_log_ratelimit {
-    use std::fmt;
-
-    use serde::de::{Deserializer, Error, Visitor};
-
-    use crate::firewall::types::log::LogRateLimit;
-
-    pub fn deserialize<'de, D: Deserializer<'de>>(
-        deserializer: D,
-    ) -> Result<Option<LogRateLimit>, D::Error> {
-        struct V;
-
-        impl<'de> Visitor<'de> for V {
-            type Value = Option<LogRateLimit>;
-
-            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
-                f.write_str("a boolean-like value")
-            }
-
-            fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
-                v.parse().map_err(E::custom).map(Some)
-            }
-
-            fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
-                Ok(None)
-            }
-
-            fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
-            where
-                D: Deserializer<'de>,
-            {
-                deserializer.deserialize_any(self)
-            }
-        }
-
-        deserializer.deserialize_any(V)
-    }
-}
-
-/// `&str` deserializer which also accepts an `Option`.
-///
-/// Serde's `StringDeserializer` does not.
-#[derive(Clone, Copy, Debug)]
-pub struct SomeStrDeserializer<'a, E>(serde::de::value::StrDeserializer<'a, E>);
-
-impl<'de, 'a, E> serde::de::Deserializer<'de> for SomeStrDeserializer<'a, E>
-where
-    E: serde::de::Error,
-{
-    type Error = E;
-
-    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        self.0.deserialize_any(visitor)
-    }
-
-    fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        visitor.visit_some(self.0)
-    }
-
-    fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        self.0.deserialize_str(visitor)
-    }
-
-    fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        self.0.deserialize_string(visitor)
-    }
-
-    fn deserialize_enum<V>(
-        self,
-        _name: &str,
-        _variants: &'static [&'static str],
-        visitor: V,
-    ) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        visitor.visit_enum(self.0)
-    }
-
-    serde::forward_to_deserialize_any! {
-        bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char
-        bytes byte_buf unit unit_struct newtype_struct seq tuple
-        tuple_struct map struct identifier ignored_any
-    }
-}
-
-/// `&str` wrapper which implements `IntoDeserializer` via `SomeStrDeserializer`.
-#[derive(Clone, Debug)]
-pub struct SomeStr<'a>(pub &'a str);
-
-impl<'a> From<&'a str> for SomeStr<'a> {
-    fn from(s: &'a str) -> Self {
-        Self(s)
-    }
-}
-
-impl<'de, 'a, E> serde::de::IntoDeserializer<'de, E> for SomeStr<'a>
-where
-    E: serde::de::Error,
-{
-    type Deserializer = SomeStrDeserializer<'a, E>;
-
-    fn into_deserializer(self) -> Self::Deserializer {
-        SomeStrDeserializer(self.0.into_deserializer())
-    }
-}
-
-/// `String` deserializer which also accepts an `Option`.
-///
-/// Serde's `StringDeserializer` does not.
-#[derive(Clone, Debug)]
-pub struct SomeStringDeserializer<E>(serde::de::value::StringDeserializer<E>);
-
-impl<'de, E> serde::de::Deserializer<'de> for SomeStringDeserializer<E>
-where
-    E: serde::de::Error,
-{
-    type Error = E;
-
-    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        self.0.deserialize_any(visitor)
-    }
-
-    fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        visitor.visit_some(self.0)
-    }
-
-    fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        self.0.deserialize_str(visitor)
-    }
-
-    fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        self.0.deserialize_string(visitor)
-    }
-
-    fn deserialize_enum<V>(
-        self,
-        _name: &str,
-        _variants: &'static [&'static str],
-        visitor: V,
-    ) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        visitor.visit_enum(self.0)
-    }
-
-    serde::forward_to_deserialize_any! {
-        bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char
-        bytes byte_buf unit unit_struct newtype_struct seq tuple
-        tuple_struct map struct identifier ignored_any
-    }
-}
-
-/// `&str` wrapper which implements `IntoDeserializer` via `SomeStringDeserializer`.
-#[derive(Clone, Debug)]
-pub struct SomeString(pub String);
-
-impl From<&str> for SomeString {
-    fn from(s: &str) -> Self {
-        Self::from(s.to_string())
-    }
-}
-
-impl From<String> for SomeString {
-    fn from(s: String) -> Self {
-        Self(s)
-    }
-}
-
-impl<'de, E> serde::de::IntoDeserializer<'de, E> for SomeString
-where
-    E: serde::de::Error,
-{
-    type Deserializer = SomeStringDeserializer<E>;
-
-    fn into_deserializer(self) -> Self::Deserializer {
-        SomeStringDeserializer(self.0.into_deserializer())
-    }
-}
-
-#[derive(Debug)]
-pub struct SerdeStringError(String);
-
-impl std::error::Error for SerdeStringError {}
-
-impl fmt::Display for SerdeStringError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        f.write_str(&self.0)
-    }
-}
-
-impl serde::de::Error for SerdeStringError {
-    fn custom<T: fmt::Display>(msg: T) -> Self {
-        Self(msg.to_string())
-    }
-}
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<String, u16>,
-}
-
-impl NamedPorts {
-    fn new() -> Self {
-        use std::io::BufRead;
-
-        log::trace!("loading /etc/services");
-
-        let mut this = Self::default();
-
-        let file = match std::fs::File::open("/etc/services") {
-            Ok(file) => file,
-            Err(_) => return this,
-        };
-
-        for line in std::io::BufReader::new(file).lines() {
-            let line = match line {
-                Ok(line) => line,
-                Err(_) => break,
-            };
-
-            let line = line.trim_start();
-
-            if line.is_empty() || line.starts_with('#') {
-                continue;
-            }
-
-            let mut parts = line.split_ascii_whitespace();
-
-            let name = match parts.next() {
-                None => continue,
-                Some(name) => name.to_string(),
-            };
-
-            let proto: u16 = match parts.next() {
-                None => continue,
-                Some(proto) => match proto.split('/').next() {
-                    None => continue,
-                    Some(num) => match num.parse() {
-                        Ok(num) => num,
-                        Err(_) => continue,
-                    },
-                },
-            };
-
-            this.ports.insert(name, proto);
-            for alias in parts {
-                if alias.starts_with('#') {
-                    break;
-                }
-                this.ports.insert(alias.to_string(), proto);
-            }
-        }
-
-        this
-    }
-
-    fn find(&self, name: &str) -> Option<u16> {
-        self.ports.get(name).copied()
-    }
-}
-
-fn named_ports() -> &'static NamedPorts {
-    static NAMED_PORTS: OnceLock<NamedPorts> = OnceLock::new();
-
-    NAMED_PORTS.get_or_init(NamedPorts::new)
-}
-
-/// Parse a named port with the help of `/etc/services`.
-pub fn parse_named_port(name: &str) -> Result<u16, Error> {
-    named_ports()
-        .find(name)
-        .ok_or_else(|| format_err!("unknown port name {name:?}"))
-}
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<Ipv4Addr>, mask: u8) -> Result<Self, Error> {
-        Ok(Cidr::Ipv4(Ipv4Cidr::new(addr, mask)?))
-    }
-
-    pub fn new_v6(addr: impl Into<Ipv6Addr>, mask: u8) -> Result<Self, Error> {
-        Ok(Cidr::Ipv6(Ipv6Cidr::new(addr, mask)?))
-    }
-
-    pub const fn family(&self) -> Family {
-        match self {
-            Cidr::Ipv4(_) => Family::V4,
-            Cidr::Ipv6(_) => Family::V6,
-        }
-    }
-
-    pub fn is_ipv4(&self) -> bool {
-        matches!(self, Cidr::Ipv4(_))
-    }
-
-    pub fn is_ipv6(&self) -> bool {
-        matches!(self, Cidr::Ipv6(_))
-    }
-}
-
-impl fmt::Display for Cidr {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self {
-            Self::Ipv4(ip) => f.write_str(ip.to_string().as_str()),
-            Self::Ipv6(ip) => f.write_str(ip.to_string().as_str()),
-        }
-    }
-}
-
-impl std::str::FromStr for Cidr {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Error> {
-        if let Ok(ip) = s.parse::<Ipv4Cidr>() {
-            return Ok(Cidr::Ipv4(ip));
-        }
-
-        if let Ok(ip) = s.parse::<Ipv6Cidr>() {
-            return Ok(Cidr::Ipv6(ip));
-        }
-
-        bail!("invalid ip address or CIDR: {s:?}");
-    }
-}
-
-impl From<Ipv4Cidr> for Cidr {
-    fn from(cidr: Ipv4Cidr) -> Self {
-        Cidr::Ipv4(cidr)
-    }
-}
-
-impl From<Ipv6Cidr> for Cidr {
-    fn from(cidr: Ipv6Cidr) -> Self {
-        Cidr::Ipv6(cidr)
-    }
-}
-
-const IPV4_LENGTH: u8 = 32;
-
-#[derive(Clone, Copy, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Ipv4Cidr {
-    addr: Ipv4Addr,
-    mask: u8,
-}
-
-impl Ipv4Cidr {
-    pub fn new(addr: impl Into<Ipv4Addr>, mask: u8) -> Result<Self, Error> {
-        if mask > 32 {
-            bail!("mask out of range for ipv4 cidr ({mask})");
-        }
-
-        Ok(Self {
-            addr: addr.into(),
-            mask,
-        })
-    }
-
-    pub fn contains_address(&self, other: &Ipv4Addr) -> bool {
-        let bits = u32::from_be_bytes(self.addr.octets());
-        let other_bits = u32::from_be_bytes(other.octets());
-
-        let shift_amount: u32 = IPV4_LENGTH.saturating_sub(self.mask).into();
-
-        bits.checked_shr(shift_amount).unwrap_or(0)
-            == other_bits.checked_shr(shift_amount).unwrap_or(0)
-    }
-
-    pub fn address(&self) -> &Ipv4Addr {
-        &self.addr
-    }
-
-    pub fn mask(&self) -> u8 {
-        self.mask
-    }
-}
-
-impl<T: Into<Ipv4Addr>> From<T> for Ipv4Cidr {
-    fn from(value: T) -> Self {
-        Self {
-            addr: value.into(),
-            mask: 32,
-        }
-    }
-}
-
-impl std::str::FromStr for Ipv4Cidr {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Error> {
-        Ok(match s.find('/') {
-            None => Self {
-                addr: s.parse()?,
-                mask: 32,
-            },
-            Some(pos) => {
-                let mask: u8 = s[(pos + 1)..]
-                    .parse()
-                    .map_err(|_| format_err!("invalid mask in ipv4 cidr: {s:?}"))?;
-
-                Self::new(s[..pos].parse::<Ipv4Addr>()?, mask)?
-            }
-        })
-    }
-}
-
-impl fmt::Display for Ipv4Cidr {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(f, "{}/{}", &self.addr, self.mask)
-    }
-}
-
-const IPV6_LENGTH: u8 = 128;
-
-#[derive(Clone, Copy, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Ipv6Cidr {
-    addr: Ipv6Addr,
-    mask: u8,
-}
-
-impl Ipv6Cidr {
-    pub fn new(addr: impl Into<Ipv6Addr>, mask: u8) -> Result<Self, Error> {
-        if mask > IPV6_LENGTH {
-            bail!("mask out of range for ipv6 cidr");
-        }
-
-        Ok(Self {
-            addr: addr.into(),
-            mask,
-        })
-    }
-
-    pub fn contains_address(&self, other: &Ipv6Addr) -> bool {
-        let bits = u128::from_be_bytes(self.addr.octets());
-        let other_bits = u128::from_be_bytes(other.octets());
-
-        let shift_amount: u32 = IPV6_LENGTH.saturating_sub(self.mask).into();
-
-        bits.checked_shr(shift_amount).unwrap_or(0)
-            == other_bits.checked_shr(shift_amount).unwrap_or(0)
-    }
-
-    pub fn address(&self) -> &Ipv6Addr {
-        &self.addr
-    }
-
-    pub fn mask(&self) -> u8 {
-        self.mask
-    }
-}
-
-impl std::str::FromStr for Ipv6Cidr {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Error> {
-        Ok(match s.find('/') {
-            None => Self {
-                addr: s.parse()?,
-                mask: 128,
-            },
-            Some(pos) => {
-                let mask: u8 = s[(pos + 1)..]
-                    .parse()
-                    .map_err(|_| format_err!("invalid mask in ipv6 cidr: {s:?}"))?;
-
-                Self::new(s[..pos].parse::<Ipv6Addr>()?, mask)?
-            }
-        })
-    }
-}
-
-impl fmt::Display for Ipv6Cidr {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(f, "{}/{}", &self.addr, self.mask)
-    }
-}
-
-impl<T: Into<Ipv6Addr>> From<T> for Ipv6Cidr {
-    fn from(addr: T) -> Self {
-        Self {
-            addr: addr.into(),
-            mask: 128,
-        }
-    }
-}
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum IpEntry {
-    Cidr(Cidr),
-    Range(IpAddr, IpAddr),
-}
-
-impl std::str::FromStr for IpEntry {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Error> {
-        if s.is_empty() {
-            bail!("Empty IP specification!")
-        }
-
-        let entries: Vec<&str> = s
-            .split('-')
-            .take(3) // so we can check whether there are too many
-            .collect();
-
-        match entries.as_slice() {
-            [cidr] => Ok(IpEntry::Cidr(cidr.parse()?)),
-            [beg, end] => {
-                if let Ok(beg) = beg.parse::<Ipv4Addr>() {
-                    if let Ok(end) = end.parse::<Ipv4Addr>() {
-                        if beg < end {
-                            return Ok(IpEntry::Range(beg.into(), end.into()));
-                        }
-
-                        bail!("start address is greater than end address!");
-                    }
-                }
-
-                if let Ok(beg) = beg.parse::<Ipv6Addr>() {
-                    if let Ok(end) = end.parse::<Ipv6Addr>() {
-                        if beg < end {
-                            return Ok(IpEntry::Range(beg.into(), end.into()));
-                        }
-
-                        bail!("start address is greater than end address!");
-                    }
-                }
-
-                bail!("start and end are not valid IP addresses of the same type!")
-            }
-            _ => bail!("Invalid amount of elements in IpEntry!"),
-        }
-    }
-}
-
-impl fmt::Display for IpEntry {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self {
-            Self::Cidr(ip) => write!(f, "{ip}"),
-            Self::Range(beg, end) => write!(f, "{beg}-{end}"),
-        }
-    }
-}
-
-impl IpEntry {
-    fn family(&self) -> Family {
-        match self {
-            Self::Cidr(cidr) => cidr.family(),
-            Self::Range(start, end) => {
-                if start.is_ipv4() && end.is_ipv4() {
-                    return Family::V4;
-                }
-
-                if start.is_ipv6() && end.is_ipv6() {
-                    return Family::V6;
-                }
-
-                // should never be reached due to constructors validating that
-                // start type == end type
-                unreachable!("invalid IP entry")
-            }
-        }
-    }
-}
-
-impl From<Cidr> for IpEntry {
-    fn from(value: Cidr) -> Self {
-        IpEntry::Cidr(value)
-    }
-}
-
-#[derive(Clone, Debug, DeserializeFromStr)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct IpList {
-    // guaranteed to have the same family
-    entries: Vec<IpEntry>,
-    family: Family,
-}
-
-impl Deref for IpList {
-    type Target = Vec<IpEntry>;
-
-    fn deref(&self) -> &Self::Target {
-        &self.entries
-    }
-}
-
-impl<T: Into<IpEntry>> From<T> for IpList {
-    fn from(value: T) -> Self {
-        let entry = value.into();
-
-        Self {
-            family: entry.family(),
-            entries: vec![entry],
-        }
-    }
-}
-
-impl std::str::FromStr for IpList {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Error> {
-        if s.is_empty() {
-            bail!("Empty IP specification!")
-        }
-
-        let mut entries = Vec::new();
-        let mut current_family = None;
-
-        for element in s.split(',') {
-            let entry: IpEntry = element.parse()?;
-
-            if let Some(family) = current_family {
-                if family != entry.family() {
-                    bail!("Incompatible families in IPList!")
-                }
-            } else {
-                current_family = Some(entry.family());
-            }
-
-            entries.push(entry);
-        }
-
-        if entries.is_empty() {
-            bail!("empty ip list")
-        }
-
-        Ok(IpList {
-            entries,
-            family: current_family.unwrap(), // must be set due to length check above
-        })
-    }
-}
-
-impl IpList {
-    pub fn new(entries: Vec<IpEntry>) -> Result<Self, Error> {
-        let family = entries.iter().try_fold(None, |result, entry| {
-            if let Some(family) = result {
-                if entry.family() != family {
-                    bail!("non-matching families in entries list");
-                }
-
-                Ok(Some(family))
-            } else {
-                Ok(Some(entry.family()))
-            }
-        })?;
-
-        if let Some(family) = family {
-            return Ok(Self { entries, family });
-        }
-
-        bail!("no elements in ip list entries");
-    }
-
-    pub fn family(&self) -> Family {
-        self.family
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use std::net::{Ipv4Addr, Ipv6Addr};
-
-    #[test]
-    fn test_v4_cidr() {
-        let mut cidr: Ipv4Cidr = "0.0.0.0/0".parse().expect("valid IPv4 CIDR");
-
-        assert_eq!(cidr.addr, Ipv4Addr::new(0, 0, 0, 0));
-        assert_eq!(cidr.mask, 0);
-
-        assert!(cidr.contains_address(&Ipv4Addr::new(0, 0, 0, 0)));
-        assert!(cidr.contains_address(&Ipv4Addr::new(255, 255, 255, 255)));
-
-        cidr = "192.168.100.1".parse().expect("valid IPv4 CIDR");
-
-        assert_eq!(cidr.addr, Ipv4Addr::new(192, 168, 100, 1));
-        assert_eq!(cidr.mask, 32);
-
-        assert!(cidr.contains_address(&Ipv4Addr::new(192, 168, 100, 1)));
-        assert!(!cidr.contains_address(&Ipv4Addr::new(192, 168, 100, 2)));
-        assert!(!cidr.contains_address(&Ipv4Addr::new(192, 168, 100, 0)));
-
-        cidr = "10.100.5.0/24".parse().expect("valid IPv4 CIDR");
-
-        assert_eq!(cidr.mask, 24);
-
-        assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 0)));
-        assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 1)));
-        assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 100)));
-        assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 255)));
-        assert!(!cidr.contains_address(&Ipv4Addr::new(10, 100, 4, 255)));
-        assert!(!cidr.contains_address(&Ipv4Addr::new(10, 100, 6, 0)));
-
-        "0.0.0.0/-1".parse::<Ipv4Cidr>().unwrap_err();
-        "0.0.0.0/33".parse::<Ipv4Cidr>().unwrap_err();
-        "256.256.256.256/10".parse::<Ipv4Cidr>().unwrap_err();
-
-        "fe80::1/64".parse::<Ipv4Cidr>().unwrap_err();
-        "qweasd".parse::<Ipv4Cidr>().unwrap_err();
-        "".parse::<Ipv4Cidr>().unwrap_err();
-    }
-
-    #[test]
-    fn test_v6_cidr() {
-        let mut cidr: Ipv6Cidr = "abab::1/64".parse().expect("valid IPv6 CIDR");
-
-        assert_eq!(cidr.addr, Ipv6Addr::new(0xABAB, 0, 0, 0, 0, 0, 0, 1));
-        assert_eq!(cidr.mask, 64);
-
-        assert!(cidr.contains_address(&Ipv6Addr::new(0xABAB, 0, 0, 0, 0, 0, 0, 0)));
-        assert!(cidr.contains_address(&Ipv6Addr::new(
-            0xABAB, 0, 0, 0, 0xAAAA, 0xAAAA, 0xAAAA, 0xAAAA
-        )));
-        assert!(cidr.contains_address(&Ipv6Addr::new(
-            0xABAB, 0, 0, 0, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
-        )));
-        assert!(!cidr.contains_address(&Ipv6Addr::new(0xABAB, 0, 0, 1, 0, 0, 0, 0)));
-        assert!(!cidr.contains_address(&Ipv6Addr::new(
-            0xABAA, 0, 0, 0, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
-        )));
-
-        cidr = "eeee::1".parse().expect("valid IPv6 CIDR");
-
-        assert_eq!(cidr.mask, 128);
-
-        assert!(cidr.contains_address(&Ipv6Addr::new(0xEEEE, 0, 0, 0, 0, 0, 0, 1)));
-        assert!(!cidr.contains_address(&Ipv6Addr::new(
-            0xEEED, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
-        )));
-        assert!(!cidr.contains_address(&Ipv6Addr::new(0xEEEE, 0, 0, 0, 0, 0, 0, 0)));
-
-        "eeee::1/-1".parse::<Ipv6Cidr>().unwrap_err();
-        "eeee::1/129".parse::<Ipv6Cidr>().unwrap_err();
-        "gggg::1/64".parse::<Ipv6Cidr>().unwrap_err();
-
-        "192.168.0.1".parse::<Ipv6Cidr>().unwrap_err();
-        "qweasd".parse::<Ipv6Cidr>().unwrap_err();
-        "".parse::<Ipv6Cidr>().unwrap_err();
-    }
-
-    #[test]
-    fn test_parse_ip_entry() {
-        let mut entry: IpEntry = "10.0.0.1".parse().expect("valid IP entry");
-
-        assert_eq!(entry, Cidr::new_v4([10, 0, 0, 1], 32).unwrap().into());
-
-        entry = "10.0.0.0/16".parse().expect("valid IP entry");
-
-        assert_eq!(entry, Cidr::new_v4([10, 0, 0, 0], 16).unwrap().into());
-
-        entry = "192.168.0.1-192.168.99.255"
-            .parse()
-            .expect("valid IP entry");
-
-        assert_eq!(
-            entry,
-            IpEntry::Range([192, 168, 0, 1].into(), [192, 168, 99, 255].into())
-        );
-
-        entry = "fe80::1".parse().expect("valid IP entry");
-
-        assert_eq!(
-            entry,
-            Cidr::new_v6([0xFE80, 0, 0, 0, 0, 0, 0, 1], 128)
-                .unwrap()
-                .into()
-        );
-
-        entry = "fe80::1/48".parse().expect("valid IP entry");
-
-        assert_eq!(
-            entry,
-            Cidr::new_v6([0xFE80, 0, 0, 0, 0, 0, 0, 1], 48)
-                .unwrap()
-                .into()
-        );
-
-        entry = "fd80::1-fd80::ffff".parse().expect("valid IP entry");
-
-        assert_eq!(
-            entry,
-            IpEntry::Range(
-                [0xFD80, 0, 0, 0, 0, 0, 0, 1].into(),
-                [0xFD80, 0, 0, 0, 0, 0, 0, 0xFFFF].into(),
-            )
-        );
-
-        "192.168.100.0-192.168.99.255"
-            .parse::<IpEntry>()
-            .unwrap_err();
-        "192.168.100.0-fe80::1".parse::<IpEntry>().unwrap_err();
-        "192.168.100.0-192.168.200.0/16"
-            .parse::<IpEntry>()
-            .unwrap_err();
-        "192.168.100.0-192.168.200.0-192.168.250.0"
-            .parse::<IpEntry>()
-            .unwrap_err();
-        "qweasd".parse::<IpEntry>().unwrap_err();
-    }
-
-    #[test]
-    fn test_parse_ip_list() {
-        let mut ip_list: IpList = "192.168.0.1,192.168.100.0/24,172.16.0.0-172.32.255.255"
-            .parse()
-            .expect("valid IP list");
-
-        assert_eq!(
-            ip_list,
-            IpList {
-                entries: vec![
-                    IpEntry::Cidr(Cidr::new_v4([192, 168, 0, 1], 32).unwrap()),
-                    IpEntry::Cidr(Cidr::new_v4([192, 168, 100, 0], 24).unwrap()),
-                    IpEntry::Range([172, 16, 0, 0].into(), [172, 32, 255, 255].into()),
-                ],
-                family: Family::V4,
-            }
-        );
-
-        ip_list = "fe80::1/64".parse().expect("valid IP list");
-
-        assert_eq!(
-            ip_list,
-            IpList {
-                entries: vec![IpEntry::Cidr(
-                    Cidr::new_v6([0xFE80, 0, 0, 0, 0, 0, 0, 1], 64).unwrap()
-                ),],
-                family: Family::V6,
-            }
-        );
-
-        "192.168.0.1,fe80::1".parse::<IpList>().unwrap_err();
-
-        "".parse::<IpList>().unwrap_err();
-        "proxmox".parse::<IpList>().unwrap_err();
-    }
-
-    #[test]
-    fn test_construct_ip_list() {
-        let mut ip_list = IpList::new(vec![Cidr::new_v4([10, 0, 0, 0], 8).unwrap().into()])
-            .expect("valid ip list");
-
-        assert_eq!(ip_list.family(), Family::V4);
-
-        ip_list =
-            IpList::new(vec![Cidr::new_v6([0x000; 8], 8).unwrap().into()]).expect("valid ip list");
-
-        assert_eq!(ip_list.family(), Family::V6);
-
-        IpList::new(vec![]).expect_err("empty ip list is invalid");
-
-        IpList::new(vec![
-            Cidr::new_v4([10, 0, 0, 0], 8).unwrap().into(),
-            Cidr::new_v6([0x0000; 8], 8).unwrap().into(),
-        ])
-        .expect_err("cannot mix ip families in ip list");
-    }
-}
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<Self, Self::Err> {
-        Ok(match s {
-            "dc" => AliasScope::Datacenter,
-            "guest" => AliasScope::Guest,
-            _ => bail!("invalid scope for alias: {s}"),
-        })
-    }
-}
-
-impl Display for AliasScope {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.write_str(match self {
-            AliasScope::Datacenter => "dc",
-            AliasScope::Guest => "guest",
-        })
-    }
-}
-
-#[derive(Debug, Clone, DeserializeFromStr)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct AliasName {
-    scope: AliasScope,
-    name: String,
-}
-
-impl Display for AliasName {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.write_fmt(format_args!("{}/{}", self.scope, self.name))
-    }
-}
-
-impl FromStr for AliasName {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        match s.split_once('/') {
-            Some((prefix, name)) if !name.is_empty() => Ok(Self {
-                scope: prefix.parse()?,
-                name: name.to_string(),
-            }),
-            _ => {
-                bail!("Invalid Alias name!")
-            }
-        }
-    }
-}
-
-impl AliasName {
-    pub fn new(scope: AliasScope, name: impl Into<String>) -> Self {
-        Self {
-            scope,
-            name: name.into(),
-        }
-    }
-
-    pub fn name(&self) -> &str {
-        &self.name
-    }
-
-    pub fn scope(&self) -> &AliasScope {
-        &self.scope
-    }
-}
-
-#[derive(Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Alias {
-    name: String,
-    address: Cidr,
-    comment: Option<String>,
-}
-
-impl Alias {
-    pub fn new(
-        name: impl Into<String>,
-        address: impl Into<Cidr>,
-        comment: impl Into<Option<String>>,
-    ) -> Self {
-        Self {
-            name: name.into(),
-            address: address.into(),
-            comment: comment.into(),
-        }
-    }
-
-    pub fn name(&self) -> &str {
-        &self.name
-    }
-
-    pub fn address(&self) -> &Cidr {
-        &self.address
-    }
-
-    pub fn comment(&self) -> Option<&str> {
-        self.comment.as_deref()
-    }
-}
-
-impl FromStr for Alias {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        let (name, line) =
-            match_name(s.trim_start()).ok_or_else(|| format_err!("expected an alias name"))?;
-
-        let (address, line) = match_non_whitespace(line.trim_start())
-            .ok_or_else(|| format_err!("expected a value for alias {name:?}"))?;
-
-        let address: Cidr = address.parse()?;
-
-        let line = line.trim_start();
-
-        let comment = match line.strip_prefix('#') {
-            Some(comment) => Some(comment.trim().to_string()),
-            None if !line.is_empty() => bail!("trailing characters in alias: {line:?}"),
-            None => None,
-        };
-
-        Ok(Alias {
-            name: name.to_string(),
-            address,
-            comment,
-        })
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_parse_alias() {
-        for alias in [
-            "local_network 10.0.0.0/32",
-            "test-_123-___-a---- 10.0.0.1/32",
-        ] {
-            alias.parse::<Alias>().expect("valid alias");
-        }
-
-        for alias in ["-- 10.0.0.1/32", "0asd 10.0.0.1/32", "__test 10.0.0.0/32"] {
-            alias.parse::<Alias>().expect_err("invalid alias");
-        }
-    }
-
-    #[test]
-    fn test_parse_alias_name() {
-        for name in ["dc/proxmox_123", "guest/proxmox-123"] {
-            name.parse::<AliasName>().expect("valid alias name");
-        }
-
-        for name in ["proxmox/proxmox_123", "guests/proxmox-123", "dc/", "/name"] {
-            name.parse::<AliasName>().expect_err("invalid alias name");
-        }
-    }
-}
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<Rule>,
-    comment: Option<String>,
-}
-
-impl Group {
-    pub const fn new() -> Self {
-        Self {
-            rules: Vec::new(),
-            comment: None,
-        }
-    }
-
-    pub fn rules(&self) -> &Vec<Rule> {
-        &self.rules
-    }
-
-    pub fn comment(&self) -> Option<&str> {
-        self.comment.as_deref()
-    }
-
-    pub fn set_comment(&mut self, comment: Option<String>) {
-        self.comment = comment;
-    }
-
-    pub(crate) fn parse_entry(&mut self, line: &str) -> Result<(), Error> {
-        self.rules.push(line.parse()?);
-        Ok(())
-    }
-}
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<Self, Self::Err> {
-        Ok(match s {
-            "+dc" => IpsetScope::Datacenter,
-            "+guest" => IpsetScope::Guest,
-            _ => bail!("invalid scope for ipset: {s}"),
-        })
-    }
-}
-
-impl Display for IpsetScope {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        let prefix = match self {
-            Self::Datacenter => "dc",
-            Self::Guest => "guest",
-        };
-
-        f.write_str(prefix)
-    }
-}
-
-#[derive(Debug, Clone, DeserializeFromStr)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct IpsetName {
-    pub scope: IpsetScope,
-    pub name: String,
-}
-
-impl IpsetName {
-    pub fn new(scope: IpsetScope, name: impl Into<String>) -> Self {
-        Self {
-            scope,
-            name: name.into(),
-        }
-    }
-
-    pub fn name(&self) -> &str {
-        &self.name
-    }
-
-    pub fn scope(&self) -> IpsetScope {
-        self.scope
-    }
-}
-
-impl FromStr for IpsetName {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        match s.split_once('/') {
-            Some((prefix, name)) if !name.is_empty() => Ok(Self {
-                scope: prefix.parse()?,
-                name: name.to_string(),
-            }),
-            _ => {
-                bail!("Invalid IPSet name: {s}")
-            }
-        }
-    }
-}
-
-impl Display for IpsetName {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}/{}", self.scope, self.name)
-    }
-}
-
-#[derive(Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum IpsetAddress {
-    Alias(AliasName),
-    Cidr(Cidr),
-}
-
-impl FromStr for IpsetAddress {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Error> {
-        if let Ok(cidr) = s.parse() {
-            return Ok(IpsetAddress::Cidr(cidr));
-        }
-
-        if let Ok(name) = s.parse() {
-            return Ok(IpsetAddress::Alias(name));
-        }
-
-        bail!("Invalid address in IPSet: {s}")
-    }
-}
-
-impl<T: Into<Cidr>> From<T> for IpsetAddress {
-    fn from(cidr: T) -> Self {
-        IpsetAddress::Cidr(cidr.into())
-    }
-}
-
-#[derive(Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct IpsetEntry {
-    pub nomatch: bool,
-    pub address: IpsetAddress,
-    pub comment: Option<String>,
-}
-
-impl<T: Into<IpsetAddress>> From<T> for IpsetEntry {
-    fn from(value: T) -> Self {
-        Self {
-            nomatch: false,
-            address: value.into(),
-            comment: None,
-        }
-    }
-}
-
-impl FromStr for IpsetEntry {
-    type Err = Error;
-
-    fn from_str(line: &str) -> Result<Self, Error> {
-        let line = line.trim_start();
-
-        let (nomatch, line) = match line.strip_prefix('!') {
-            Some(line) => (true, line),
-            None => (false, line),
-        };
-
-        let (address, line) =
-            match_non_whitespace(line.trim_start()).ok_or_else(|| format_err!("missing value"))?;
-
-        let address: IpsetAddress = address.parse()?;
-        let line = line.trim_start();
-
-        let comment = match line.strip_prefix('#') {
-            Some(comment) => Some(comment.trim().to_string()),
-            None if !line.is_empty() => bail!("trailing characters in ipset entry: {line:?}"),
-            None => None,
-        };
-
-        Ok(Self {
-            nomatch,
-            address,
-            comment,
-        })
-    }
-}
-
-#[derive(Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Ipfilter<'a> {
-    index: i64,
-    ipset: &'a Ipset,
-}
-
-impl Ipfilter<'_> {
-    pub fn index(&self) -> i64 {
-        self.index
-    }
-
-    pub fn ipset(&self) -> &Ipset {
-        self.ipset
-    }
-
-    pub fn name_for_index(index: i64) -> String {
-        format!("ipfilter-net{index}")
-    }
-}
-
-#[derive(Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Ipset {
-    pub name: IpsetName,
-    set: Vec<IpsetEntry>,
-    pub comment: Option<String>,
-}
-
-impl Ipset {
-    pub const fn new(name: IpsetName) -> Self {
-        Self {
-            name,
-            set: Vec::new(),
-            comment: None,
-        }
-    }
-
-    pub fn name(&self) -> &IpsetName {
-        &self.name
-    }
-
-    pub fn from_parts(scope: IpsetScope, name: impl Into<String>) -> Self {
-        Self::new(IpsetName::new(scope, name))
-    }
-
-    pub(crate) fn parse_entry(&mut self, line: &str) -> Result<(), Error> {
-        self.set.push(line.parse()?);
-        Ok(())
-    }
-
-    pub fn ipfilter(&self) -> Option<Ipfilter> {
-        if self.name.scope() != IpsetScope::Guest {
-            return None;
-        }
-
-        let name = self.name.name();
-
-        if let Some(key) = name.strip_prefix("ipfilter-") {
-            let id = NetworkConfig::index_from_net_key(key);
-
-            if let Ok(id) = id {
-                return Some(Ipfilter {
-                    index: id,
-                    ipset: self,
-                });
-            }
-        }
-
-        None
-    }
-}
-
-impl Deref for Ipset {
-    type Target = Vec<IpsetEntry>;
-
-    #[inline]
-    fn deref(&self) -> &Self::Target {
-        &self.set
-    }
-}
-
-impl DerefMut for Ipset {
-    #[inline]
-    fn deref_mut(&mut self) -> &mut Vec<IpsetEntry> {
-        &mut self.set
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_parse_ipset_name() {
-        for test_case in [
-            ("+dc/proxmox-123", IpsetScope::Datacenter, "proxmox-123"),
-            ("+guest/proxmox_123", IpsetScope::Guest, "proxmox_123"),
-        ] {
-            let ipset_name = test_case.0.parse::<IpsetName>().expect("valid ipset name");
-
-            assert_eq!(
-                ipset_name,
-                IpsetName {
-                    scope: test_case.1,
-                    name: test_case.2.to_string(),
-                }
-            )
-        }
-
-        for name in ["+dc/", "+guests/proxmox_123", "guest/proxmox_123"] {
-            name.parse::<IpsetName>().expect_err("invalid ipset name");
-        }
-    }
-
-    #[test]
-    fn test_parse_ipset_address() {
-        let mut ipset_address = "10.0.0.1"
-            .parse::<IpsetAddress>()
-            .expect("valid ipset address");
-        assert!(matches!(ipset_address, IpsetAddress::Cidr(Cidr::Ipv4(..))));
-
-        ipset_address = "fe80::1/64"
-            .parse::<IpsetAddress>()
-            .expect("valid ipset address");
-        assert!(matches!(ipset_address, IpsetAddress::Cidr(Cidr::Ipv6(..))));
-
-        ipset_address = "dc/proxmox-123"
-            .parse::<IpsetAddress>()
-            .expect("valid ipset address");
-        assert!(matches!(ipset_address, IpsetAddress::Alias(..)));
-
-        ipset_address = "guest/proxmox_123"
-            .parse::<IpsetAddress>()
-            .expect("valid ipset address");
-        assert!(matches!(ipset_address, IpsetAddress::Alias(..)));
-    }
-
-    #[test]
-    fn test_ipfilter() {
-        let mut ipset = Ipset::from_parts(IpsetScope::Guest, "ipfilter-net0");
-        ipset.ipfilter().expect("is an ipfilter");
-
-        ipset = Ipset::from_parts(IpsetScope::Guest, "ipfilter-qwe");
-        assert!(ipset.ipfilter().is_none());
-
-        ipset = Ipset::from_parts(IpsetScope::Guest, "proxmox");
-        assert!(ipset.ipfilter().is_none());
-
-        ipset = Ipset::from_parts(IpsetScope::Datacenter, "ipfilter-net0");
-        assert!(ipset.ipfilter().is_none());
-    }
-
-    #[test]
-    fn test_parse_ipset_entry() {
-        let mut entry = "!10.0.0.1 # qweqweasd"
-            .parse::<IpsetEntry>()
-            .expect("valid ipset entry");
-
-        assert_eq!(
-            entry,
-            IpsetEntry {
-                nomatch: true,
-                comment: Some("qweqweasd".to_string()),
-                address: IpsetAddress::Cidr(Cidr::new_v4([10, 0, 0, 1], 32).unwrap())
-            }
-        );
-
-        entry = "fe80::1/48"
-            .parse::<IpsetEntry>()
-            .expect("valid ipset entry");
-
-        assert_eq!(
-            entry,
-            IpsetEntry {
-                nomatch: false,
-                comment: None,
-                address: IpsetAddress::Cidr(
-                    Cidr::new_v6([0xFE80, 0, 0, 0, 0, 0, 0, 1], 48).unwrap()
-                )
-            }
-        )
-    }
-}
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<Self, Error> {
-        match str {
-            "second" => Ok(LogRateLimitTimescale::Second),
-            "minute" => Ok(LogRateLimitTimescale::Minute),
-            "hour" => Ok(LogRateLimitTimescale::Hour),
-            "day" => Ok(LogRateLimitTimescale::Day),
-            _ => bail!("Invalid time scale provided"),
-        }
-    }
-}
-
-#[derive(Debug, Deserialize, Clone)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct LogRateLimit {
-    enabled: bool,
-    rate: i64, // in packets
-    per: LogRateLimitTimescale,
-    burst: i64, // in packets
-}
-
-impl LogRateLimit {
-    pub fn new(enabled: bool, rate: i64, per: LogRateLimitTimescale, burst: i64) -> Self {
-        Self {
-            enabled,
-            rate,
-            per,
-            burst,
-        }
-    }
-
-    pub fn enabled(&self) -> bool {
-        self.enabled
-    }
-
-    pub fn rate(&self) -> i64 {
-        self.rate
-    }
-
-    pub fn burst(&self) -> i64 {
-        self.burst
-    }
-
-    pub fn per(&self) -> LogRateLimitTimescale {
-        self.per
-    }
-}
-
-impl Default for LogRateLimit {
-    fn default() -> Self {
-        Self {
-            enabled: true,
-            rate: 1,
-            burst: 5,
-            per: LogRateLimitTimescale::Second,
-        }
-    }
-}
-
-impl FromStr for LogRateLimit {
-    type Err = Error;
-
-    fn from_str(str: &str) -> Result<Self, Error> {
-        let mut limit = Self::default();
-
-        for element in str.split(',') {
-            match element.split_once('=') {
-                None => {
-                    limit.enabled = parse_bool(element)?;
-                }
-                Some((key, value)) if !key.is_empty() && !value.is_empty() => match key {
-                    "enable" => limit.enabled = parse_bool(value)?,
-                    "burst" => limit.burst = i64::from_str(value)?,
-                    "rate" => match value.split_once('/') {
-                        None => {
-                            limit.rate = i64::from_str(value)?;
-                        }
-                        Some((rate, unit)) => {
-                            if unit.is_empty() {
-                                bail!("empty unit specification")
-                            }
-
-                            limit.rate = i64::from_str(rate)?;
-                            limit.per = LogRateLimitTimescale::from_str(unit)?;
-                        }
-                    },
-                    _ => bail!("Invalid value for Key found in log_ratelimit!"),
-                },
-                _ => bail!("invalid value in log_ratelimit"),
-            }
-        }
-
-        Ok(limit)
-    }
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
-pub enum LogLevel {
-    #[default]
-    Nolog,
-    Emergency,
-    Alert,
-    Critical,
-    Error,
-    Warning,
-    Notice,
-    Info,
-    Debug,
-}
-
-impl std::str::FromStr for LogLevel {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Error> {
-        Ok(match s {
-            "nolog" => LogLevel::Nolog,
-            "emerg" => LogLevel::Emergency,
-            "alert" => LogLevel::Alert,
-            "crit" => LogLevel::Critical,
-            "err" => LogLevel::Error,
-            "warn" => LogLevel::Warning,
-            "warning" => LogLevel::Warning,
-            "notice" => LogLevel::Notice,
-            "info" => LogLevel::Info,
-            "debug" => LogLevel::Debug,
-            _ => bail!("invalid log level {s:?}"),
-        })
-    }
-}
-
-impl fmt::Display for LogLevel {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        f.write_str(match self {
-            LogLevel::Nolog => "nolog",
-            LogLevel::Emergency => "emerg",
-            LogLevel::Alert => "alert",
-            LogLevel::Critical => "crit",
-            LogLevel::Error => "err",
-            LogLevel::Warning => "warn",
-            LogLevel::Notice => "notice",
-            LogLevel::Info => "info",
-            LogLevel::Debug => "debug",
-        })
-    }
-}
-
-serde_plain::derive_deserialize_from_fromstr!(LogLevel, "valid log level");
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_parse_rate_limit() {
-        let mut parsed_rate_limit = "1,burst=123,rate=44"
-            .parse::<LogRateLimit>()
-            .expect("valid rate limit");
-
-        assert_eq!(
-            parsed_rate_limit,
-            LogRateLimit {
-                enabled: true,
-                burst: 123,
-                rate: 44,
-                per: LogRateLimitTimescale::Second,
-            }
-        );
-
-        parsed_rate_limit = "1".parse::<LogRateLimit>().expect("valid rate limit");
-
-        assert_eq!(parsed_rate_limit, LogRateLimit::default());
-
-        parsed_rate_limit = "enable=0,rate=123/hour"
-            .parse::<LogRateLimit>()
-            .expect("valid rate limit");
-
-        assert_eq!(
-            parsed_rate_limit,
-            LogRateLimit {
-                enabled: false,
-                burst: 5,
-                rate: 123,
-                per: LogRateLimitTimescale::Hour,
-            }
-        );
-
-        "2".parse::<LogRateLimit>()
-            .expect_err("invalid value for enable");
-
-        "enabled=0,rate=123"
-            .parse::<LogRateLimit>()
-            .expect_err("invalid key in log ratelimit");
-
-        "enable=0,rate=123,"
-            .parse::<LogRateLimit>()
-            .expect_err("trailing comma in log rate limit specification");
-
-        "enable=0,rate=123/proxmox,"
-            .parse::<LogRateLimit>()
-            .expect_err("invalid unit for rate");
-    }
-}
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<u16, Error> {
-    if let Ok(port) = port.parse::<u16>() {
-        return Ok(port);
-    }
-
-    if let Ok(port) = parse_named_port(port) {
-        return Ok(port);
-    }
-
-    bail!("invalid port specification: {port}")
-}
-
-impl std::str::FromStr for PortEntry {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        Ok(match s.trim().split_once(':') {
-            None => PortEntry::from(parse_port(s)?),
-            Some((first, second)) => {
-                PortEntry::try_from((parse_port(first)?, parse_port(second)?))?
-            }
-        })
-    }
-}
-
-impl From<u16> for PortEntry {
-    fn from(port: u16) -> Self {
-        PortEntry::Port(port)
-    }
-}
-
-impl TryFrom<(u16, u16)> for PortEntry {
-    type Error = Error;
-
-    fn try_from(ports: (u16, u16)) -> Result<Self, Error> {
-        if ports.0 > ports.1 {
-            bail!("start port is greater than end port!");
-        }
-
-        Ok(PortEntry::Range(ports.0, ports.1))
-    }
-}
-
-#[derive(Clone, Debug, DeserializeFromStr)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct PortList(pub(crate) Vec<PortEntry>);
-
-impl FromIterator<PortEntry> for PortList {
-    fn from_iter<T: IntoIterator<Item = PortEntry>>(iter: T) -> Self {
-        Self(iter.into_iter().collect())
-    }
-}
-
-impl<T: Into<PortEntry>> From<T> for PortList {
-    fn from(value: T) -> Self {
-        Self(vec![value.into()])
-    }
-}
-
-impl Deref for PortList {
-    type Target = Vec<PortEntry>;
-
-    fn deref(&self) -> &Self::Target {
-        &self.0
-    }
-}
-
-impl std::str::FromStr for PortList {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Error> {
-        if s.is_empty() {
-            bail!("empty port specification");
-        }
-
-        let mut entries = Vec::new();
-
-        for entry in s.trim().split(',') {
-            entries.push(entry.parse()?);
-        }
-
-        if entries.is_empty() {
-            bail!("invalid empty port list");
-        }
-
-        Ok(Self(entries))
-    }
-}
-
-impl fmt::Display for PortList {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        use fmt::Write;
-        if self.0.len() > 1 {
-            f.write_char('{')?;
-        }
-
-        let mut comma = '\0';
-        for entry in &self.0 {
-            if std::mem::replace(&mut comma, ',') != '\0' {
-                f.write_char(comma)?;
-            }
-            fmt::Display::fmt(entry, f)?;
-        }
-
-        if self.0.len() > 1 {
-            f.write_char('}')?;
-        }
-
-        Ok(())
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_parse_port_entry() {
-        let mut port_entry: PortEntry = "12345".parse().expect("valid port entry");
-        assert_eq!(port_entry, PortEntry::from(12345));
-
-        port_entry = "0:65535".parse().expect("valid port entry");
-        assert_eq!(port_entry, PortEntry::try_from((0, 65535)).unwrap());
-
-        "65536".parse::<PortEntry>().unwrap_err();
-        "100:100000".parse::<PortEntry>().unwrap_err();
-        "qweasd".parse::<PortEntry>().unwrap_err();
-        "".parse::<PortEntry>().unwrap_err();
-    }
-
-    #[test]
-    fn test_parse_port_list() {
-        let mut port_list: PortList = "12345".parse().expect("valid port list");
-        assert_eq!(port_list, PortList::from(12345));
-
-        port_list = "12345,0:65535,1337,ssh:80,https"
-            .parse()
-            .expect("valid port list");
-
-        assert_eq!(
-            port_list,
-            PortList(vec![
-                PortEntry::from(12345),
-                PortEntry::try_from((0, 65535)).unwrap(),
-                PortEntry::from(1337),
-                PortEntry::try_from((22, 80)).unwrap(),
-                PortEntry::from(443),
-            ])
-        );
-
-        "0::1337".parse::<PortList>().unwrap_err();
-        "0:1337,".parse::<PortList>().unwrap_err();
-        "70000".parse::<PortList>().unwrap_err();
-        "qweasd".parse::<PortList>().unwrap_err();
-        "".parse::<PortList>().unwrap_err();
-    }
-}
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<Self, Error> {
-        for (name, dir) in [("IN", Direction::In), ("OUT", Direction::Out)] {
-            if s.eq_ignore_ascii_case(name) {
-                return Ok(dir);
-            }
-        }
-
-        bail!("invalid direction: {s:?}, expect 'IN' or 'OUT'");
-    }
-}
-
-serde_plain::derive_deserialize_from_fromstr!(Direction, "valid packet direction");
-
-impl fmt::Display for Direction {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self {
-            Direction::In => f.write_str("in"),
-            Direction::Out => f.write_str("out"),
-        }
-    }
-}
-
-#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
-pub enum Verdict {
-    Accept,
-    Reject,
-    #[default]
-    Drop,
-}
-
-impl std::str::FromStr for Verdict {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Error> {
-        for (name, verdict) in [
-            ("ACCEPT", Verdict::Accept),
-            ("REJECT", Verdict::Reject),
-            ("DROP", Verdict::Drop),
-        ] {
-            if s.eq_ignore_ascii_case(name) {
-                return Ok(verdict);
-            }
-        }
-        bail!("invalid verdict {s:?}, expected one of 'ACCEPT', 'REJECT' or 'DROP'");
-    }
-}
-
-impl Display for Verdict {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let string = match self {
-            Verdict::Accept => "ACCEPT",
-            Verdict::Drop => "DROP",
-            Verdict::Reject => "REJECT",
-        };
-
-        write!(f, "{string}")
-    }
-}
-
-serde_plain::derive_deserialize_from_fromstr!(Verdict, "valid verdict");
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Rule {
-    pub(crate) disabled: bool,
-    pub(crate) kind: Kind,
-    pub(crate) comment: Option<String>,
-}
-
-impl std::ops::Deref for Rule {
-    type Target = Kind;
-
-    fn deref(&self) -> &Self::Target {
-        &self.kind
-    }
-}
-
-impl std::ops::DerefMut for Rule {
-    fn deref_mut(&mut self) -> &mut Self::Target {
-        &mut self.kind
-    }
-}
-
-impl FromStr for Rule {
-    type Err = Error;
-
-    fn from_str(input: &str) -> Result<Self, Self::Err> {
-        if input.contains(['\n', '\r']) {
-            bail!("rule must not contain any newlines!");
-        }
-
-        let (line, comment) = match input.rsplit_once('#') {
-            Some((line, comment)) if !comment.is_empty() => (line.trim(), Some(comment.trim())),
-            _ => (input.trim(), None),
-        };
-
-        let (disabled, line) = match line.strip_prefix('|') {
-            Some(line) => (true, line.trim_start()),
-            None => (false, line),
-        };
-
-        // todo: case insensitive?
-        let kind = if line.starts_with("GROUP") {
-            Kind::from(line.parse::<RuleGroup>()?)
-        } else {
-            Kind::from(line.parse::<RuleMatch>()?)
-        };
-
-        Ok(Self {
-            disabled,
-            comment: comment.map(str::to_string),
-            kind,
-        })
-    }
-}
-
-impl Rule {
-    pub fn iface(&self) -> Option<&str> {
-        match &self.kind {
-            Kind::Group(group) => group.iface(),
-            Kind::Match(rule) => rule.iface(),
-        }
-    }
-
-    pub fn disabled(&self) -> bool {
-        self.disabled
-    }
-
-    pub fn kind(&self) -> &Kind {
-        &self.kind
-    }
-
-    pub fn comment(&self) -> Option<&str> {
-        self.comment.as_deref()
-    }
-}
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum Kind {
-    Group(RuleGroup),
-    Match(RuleMatch),
-}
-
-impl Kind {
-    pub fn is_group(&self) -> bool {
-        matches!(self, Kind::Group(_))
-    }
-
-    pub fn is_match(&self) -> bool {
-        matches!(self, Kind::Match(_))
-    }
-}
-
-impl From<RuleGroup> for Kind {
-    fn from(value: RuleGroup) -> Self {
-        Kind::Group(value)
-    }
-}
-
-impl From<RuleMatch> for Kind {
-    fn from(value: RuleMatch) -> Self {
-        Kind::Match(value)
-    }
-}
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct RuleGroup {
-    pub(crate) group: String,
-    pub(crate) iface: Option<String>,
-}
-
-impl RuleGroup {
-    pub(crate) fn from_options(group: String, options: RuleOptions) -> Result<Self, Error> {
-        ensure!(
-            options.proto.is_none()
-                && options.dport.is_none()
-                && options.sport.is_none()
-                && options.dest.is_none()
-                && options.source.is_none()
-                && options.log.is_none()
-                && options.icmp_type.is_none(),
-            "only interface parameter is permitted for group rules"
-        );
-
-        Ok(Self {
-            group,
-            iface: options.iface,
-        })
-    }
-
-    pub fn group(&self) -> &str {
-        &self.group
-    }
-
-    pub fn iface(&self) -> Option<&str> {
-        self.iface.as_deref()
-    }
-}
-
-impl FromStr for RuleGroup {
-    type Err = Error;
-
-    fn from_str(input: &str) -> Result<Self, Self::Err> {
-        let (keyword, rest) = match_name(input)
-            .ok_or_else(|| format_err!("expected a leading keyword in rule group"))?;
-
-        if !keyword.eq_ignore_ascii_case("group") {
-            bail!("Expected keyword GROUP")
-        }
-
-        let (name, rest) =
-            match_name(rest.trim()).ok_or_else(|| format_err!("expected a name for rule group"))?;
-
-        let options = rest.trim_start().parse()?;
-
-        Self::from_options(name.to_string(), options)
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use crate::firewall::types::{
-        address::{IpEntry, IpList},
-        alias::{AliasName, AliasScope},
-        ipset::{IpsetName, IpsetScope},
-        log::LogLevel,
-        rule_match::{Icmp, IcmpCode, IpAddrMatch, IpMatch, Ports, Protocol, Udp},
-        Cidr,
-    };
-
-    use super::*;
-
-    #[test]
-    fn test_parse_rule() {
-        let mut rule: Rule = "|GROUP tgr -i eth0 # acomm".parse().expect("valid rule");
-
-        assert_eq!(
-            rule,
-            Rule {
-                disabled: true,
-                comment: Some("acomm".to_string()),
-                kind: Kind::Group(RuleGroup {
-                    group: "tgr".to_string(),
-                    iface: Some("eth0".to_string()),
-                }),
-            },
-        );
-
-        rule = "IN ACCEPT -p udp -dport 33 -sport 22 -log warning"
-            .parse()
-            .expect("valid rule");
-
-        assert_eq!(
-            rule,
-            Rule {
-                disabled: false,
-                comment: None,
-                kind: Kind::Match(RuleMatch {
-                    dir: Direction::In,
-                    verdict: Verdict::Accept,
-                    proto: Some(Udp::new(Ports::from_u16(22, 33)).into()),
-                    log: Some(LogLevel::Warning),
-                    ..Default::default()
-                }),
-            }
-        );
-
-        rule = "IN ACCEPT --proto udp -i eth0".parse().expect("valid rule");
-
-        assert_eq!(
-            rule,
-            Rule {
-                disabled: false,
-                comment: None,
-                kind: Kind::Match(RuleMatch {
-                    dir: Direction::In,
-                    verdict: Verdict::Accept,
-                    proto: Some(Udp::new(Ports::new(None, None)).into()),
-                    iface: Some("eth0".to_string()),
-                    ..Default::default()
-                }),
-            }
-        );
-
-        rule = " OUT DROP \
-          -source 10.0.0.0/24 -dest 20.0.0.0-20.255.255.255,192.168.0.0/16 \
-          -p icmp -log nolog -icmp-type port-unreachable "
-            .parse()
-            .expect("valid rule");
-
-        assert_eq!(
-            rule,
-            Rule {
-                disabled: false,
-                comment: None,
-                kind: Kind::Match(RuleMatch {
-                    dir: Direction::Out,
-                    verdict: Verdict::Drop,
-                    ip: IpMatch::new(
-                        IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 24).unwrap())),
-                        IpAddrMatch::Ip(
-                            IpList::new(vec![
-                                IpEntry::Range([20, 0, 0, 0].into(), [20, 255, 255, 255].into()),
-                                IpEntry::Cidr(Cidr::new_v4([192, 168, 0, 0], 16).unwrap()),
-                            ])
-                            .unwrap()
-                        ),
-                    )
-                    .ok(),
-                    proto: Some(Protocol::Icmp(Icmp::new_code(IcmpCode::Named(
-                        "port-unreachable"
-                    )))),
-                    log: Some(LogLevel::Nolog),
-                    ..Default::default()
-                }),
-            }
-        );
-
-        rule = "IN BGP(ACCEPT) --log crit --iface eth0"
-            .parse()
-            .expect("valid rule");
-
-        assert_eq!(
-            rule,
-            Rule {
-                disabled: false,
-                comment: None,
-                kind: Kind::Match(RuleMatch {
-                    dir: Direction::In,
-                    verdict: Verdict::Accept,
-                    log: Some(LogLevel::Critical),
-                    fw_macro: Some("BGP".to_string()),
-                    iface: Some("eth0".to_string()),
-                    ..Default::default()
-                }),
-            }
-        );
-
-        rule = "IN ACCEPT --source dc/test --dest +dc/test"
-            .parse()
-            .expect("valid rule");
-
-        assert_eq!(
-            rule,
-            Rule {
-                disabled: false,
-                comment: None,
-                kind: Kind::Match(RuleMatch {
-                    dir: Direction::In,
-                    verdict: Verdict::Accept,
-                    ip: Some(
-                        IpMatch::new(
-                            IpAddrMatch::Alias(AliasName::new(AliasScope::Datacenter, "test")),
-                            IpAddrMatch::Set(IpsetName::new(IpsetScope::Datacenter, "test"),),
-                        )
-                        .unwrap()
-                    ),
-                    ..Default::default()
-                }),
-            }
-        );
-
-        rule = "IN REJECT".parse().expect("valid rule");
-
-        assert_eq!(
-            rule,
-            Rule {
-                disabled: false,
-                comment: None,
-                kind: Kind::Match(RuleMatch {
-                    dir: Direction::In,
-                    verdict: Verdict::Reject,
-                    ..Default::default()
-                }),
-            }
-        );
-
-        "IN DROP ---log crit"
-            .parse::<Rule>()
-            .expect_err("too many dashes in option");
-
-        "IN DROP --log --iface eth0"
-            .parse::<Rule>()
-            .expect_err("no value for option");
-
-        "IN DROP --log crit --iface"
-            .parse::<Rule>()
-            .expect_err("no value for option");
-    }
-}
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<String>,
-
-    pub(crate) dport: Option<String>,
-    pub(crate) sport: Option<String>,
-
-    pub(crate) dest: Option<String>,
-    pub(crate) source: Option<String>,
-
-    #[serde(alias = "i")]
-    pub(crate) iface: Option<String>,
-
-    pub(crate) log: Option<LogLevel>,
-    pub(crate) icmp_type: Option<String>,
-}
-
-impl FromStr for RuleOptions {
-    type Err = Error;
-
-    fn from_str(mut line: &str) -> Result<Self, Self::Err> {
-        let mut options = HashMap::new();
-
-        loop {
-            line = line.trim_start();
-
-            if line.is_empty() {
-                break;
-            }
-
-            line = line
-                .strip_prefix('-')
-                .ok_or_else(|| format_err!("expected an option starting with '-'"))?;
-
-            // second dash is optional
-            line = line.strip_prefix('-').unwrap_or(line);
-
-            let param;
-            (param, line) = match_name(line)
-                .ok_or_else(|| format_err!("expected a parameter name after '-'"))?;
-
-            let value;
-            (value, line) = match_non_whitespace(line.trim_start())
-                .ok_or_else(|| format_err!("expected a value for {param:?}"))?;
-
-            if options.insert(param, SomeStr(value)).is_some() {
-                bail!("duplicate option in rule: {param}")
-            }
-        }
-
-        Ok(RuleOptions::deserialize(IntoDeserializer::<
-            '_,
-            crate::firewall::parse::SerdeStringError,
-        >::into_deserializer(
-            options
-        ))?)
-    }
-}
-
-#[derive(Clone, Debug, Default)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct RuleMatch {
-    pub(crate) dir: Direction,
-    pub(crate) verdict: Verdict,
-    pub(crate) fw_macro: Option<String>,
-
-    pub(crate) iface: Option<String>,
-    pub(crate) log: Option<LogLevel>,
-    pub(crate) ip: Option<IpMatch>,
-    pub(crate) proto: Option<Protocol>,
-}
-
-impl RuleMatch {
-    pub(crate) fn from_options(
-        dir: Direction,
-        verdict: Verdict,
-        fw_macro: impl Into<Option<String>>,
-        options: RuleOptions,
-    ) -> Result<Self, Error> {
-        if options.dport.is_some() && options.icmp_type.is_some() {
-            bail!("dport and icmp-type are mutually exclusive");
-        }
-
-        let ip = IpMatch::from_options(&options)?;
-        let proto = Protocol::from_options(&options)?;
-
-        // todo: check protocol & IP Version compatibility
-
-        Ok(Self {
-            dir,
-            verdict,
-            fw_macro: fw_macro.into(),
-            iface: options.iface,
-            log: options.log,
-            ip,
-            proto,
-        })
-    }
-
-    pub fn direction(&self) -> Direction {
-        self.dir
-    }
-
-    pub fn iface(&self) -> Option<&str> {
-        self.iface.as_deref()
-    }
-
-    pub fn verdict(&self) -> Verdict {
-        self.verdict
-    }
-
-    pub fn fw_macro(&self) -> Option<&str> {
-        self.fw_macro.as_deref()
-    }
-
-    pub fn log(&self) -> Option<LogLevel> {
-        self.log
-    }
-
-    pub fn ip(&self) -> Option<&IpMatch> {
-        self.ip.as_ref()
-    }
-
-    pub fn proto(&self) -> Option<&Protocol> {
-        self.proto.as_ref()
-    }
-}
-
-/// Returns `(Macro name, Verdict, RestOfTheLine)`.
-fn parse_action(line: &str) -> Result<(Option<&str>, Verdict, &str), Error> {
-    let (verdict, line) =
-        match_name(line).ok_or_else(|| format_err!("expected a verdict or macro name"))?;
-
-    Ok(if let Some(line) = line.strip_prefix('(') {
-        // <macro>(<verdict>)
-
-        let macro_name = verdict;
-        let (verdict, line) = match_name(line).ok_or_else(|| format_err!("expected a verdict"))?;
-        let line = line
-            .strip_prefix(')')
-            .ok_or_else(|| format_err!("expected closing ')' after verdict"))?;
-
-        let verdict: Verdict = verdict.parse()?;
-
-        (Some(macro_name), verdict, line.trim_start())
-    } else {
-        (None, verdict.parse()?, line.trim_start())
-    })
-}
-
-impl FromStr for RuleMatch {
-    type Err = Error;
-
-    fn from_str(line: &str) -> Result<Self, Self::Err> {
-        let (dir, rest) = match_name(line).ok_or_else(|| format_err!("expected a direction"))?;
-
-        let direction: Direction = dir.parse()?;
-
-        let (fw_macro, verdict, rest) = parse_action(rest.trim_start())?;
-
-        let options: RuleOptions = rest.trim_start().parse()?;
-
-        Self::from_options(direction, verdict, fw_macro.map(str::to_string), options)
-    }
-}
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct IpMatch {
-    pub(crate) src: Option<IpAddrMatch>,
-    pub(crate) dst: Option<IpAddrMatch>,
-}
-
-impl IpMatch {
-    pub fn new(
-        src: impl Into<Option<IpAddrMatch>>,
-        dst: impl Into<Option<IpAddrMatch>>,
-    ) -> Result<Self, Error> {
-        let source = src.into();
-        let dest = dst.into();
-
-        if source.is_none() && dest.is_none() {
-            bail!("either src or dst must be set")
-        }
-
-        if let (Some(IpAddrMatch::Ip(src)), Some(IpAddrMatch::Ip(dst))) = (&source, &dest) {
-            if src.family() != dst.family() {
-                bail!("src and dst family must be equal")
-            }
-        }
-
-        let ip_match = Self {
-            src: source,
-            dst: dest,
-        };
-
-        Ok(ip_match)
-    }
-
-    fn from_options(options: &RuleOptions) -> Result<Option<Self>, Error> {
-        let src = options
-            .source
-            .as_ref()
-            .map(|elem| elem.parse::<IpAddrMatch>())
-            .transpose()?;
-
-        let dst = options
-            .dest
-            .as_ref()
-            .map(|elem| elem.parse::<IpAddrMatch>())
-            .transpose()?;
-
-        if src.is_some() || dst.is_some() {
-            Ok(Some(IpMatch::new(src, dst)?))
-        } else {
-            Ok(None)
-        }
-    }
-
-    pub fn src(&self) -> Option<&IpAddrMatch> {
-        self.src.as_ref()
-    }
-
-    pub fn dst(&self) -> Option<&IpAddrMatch> {
-        self.dst.as_ref()
-    }
-}
-
-#[derive(Clone, Debug, Deserialize)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum IpAddrMatch {
-    Ip(IpList),
-    Set(IpsetName),
-    Alias(AliasName),
-}
-
-impl IpAddrMatch {
-    pub fn family(&self) -> Option<Family> {
-        if let IpAddrMatch::Ip(list) = self {
-            return Some(list.family());
-        }
-
-        None
-    }
-}
-
-impl FromStr for IpAddrMatch {
-    type Err = Error;
-
-    fn from_str(value: &str) -> Result<Self, Error> {
-        if value.is_empty() {
-            bail!("empty IP specification");
-        }
-
-        if let Ok(ip_list) = value.parse() {
-            return Ok(IpAddrMatch::Ip(ip_list));
-        }
-
-        if let Ok(ipset) = value.parse() {
-            return Ok(IpAddrMatch::Set(ipset));
-        }
-
-        if let Ok(name) = value.parse() {
-            return Ok(IpAddrMatch::Alias(name));
-        }
-
-        bail!("invalid IP specification: {value}")
-    }
-}
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum Protocol {
-    Dccp(Ports),
-    Sctp(Sctp),
-    Tcp(Tcp),
-    Udp(Udp),
-    UdpLite(Ports),
-    Icmp(Icmp),
-    Icmpv6(Icmpv6),
-    Named(String),
-    Numeric(u8),
-}
-
-impl Protocol {
-    pub(crate) fn from_options(options: &RuleOptions) -> Result<Option<Self>, Error> {
-        let proto = match options.proto.as_deref() {
-            Some(p) => p,
-            None => return Ok(None),
-        };
-
-        Ok(Some(match proto {
-            "dccp" | "33" => Protocol::Dccp(Ports::from_options(options)?),
-            "sctp" | "132" => Protocol::Sctp(Sctp::from_options(options)?),
-            "tcp" | "6" => Protocol::Tcp(Tcp::from_options(options)?),
-            "udp" | "17" => Protocol::Udp(Udp::from_options(options)?),
-            "udplite" | "136" => Protocol::UdpLite(Ports::from_options(options)?),
-            "icmp" | "1" => Protocol::Icmp(Icmp::from_options(options)?),
-            "ipv6-icmp" | "icmpv6" | "58" => Protocol::Icmpv6(Icmpv6::from_options(options)?),
-            other => match other.parse::<u8>() {
-                Ok(num) => Protocol::Numeric(num),
-                Err(_) => Protocol::Named(other.to_string()),
-            },
-        }))
-    }
-
-    pub fn family(&self) -> Option<Family> {
-        match self {
-            Self::Icmp(_) => Some(Family::V4),
-            Self::Icmpv6(_) => Some(Family::V6),
-            _ => None,
-        }
-    }
-}
-
-#[derive(Clone, Debug, Default)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Udp {
-    ports: Ports,
-}
-
-impl Udp {
-    fn from_options(options: &RuleOptions) -> Result<Self, Error> {
-        Ok(Self {
-            ports: Ports::from_options(options)?,
-        })
-    }
-
-    pub fn new(ports: Ports) -> Self {
-        Self { ports }
-    }
-
-    pub fn ports(&self) -> &Ports {
-        &self.ports
-    }
-}
-
-impl From<Udp> for Protocol {
-    fn from(value: Udp) -> Self {
-        Protocol::Udp(value)
-    }
-}
-
-#[derive(Clone, Debug, Default)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Ports {
-    sport: Option<PortList>,
-    dport: Option<PortList>,
-}
-
-impl Ports {
-    pub fn new(sport: impl Into<Option<PortList>>, dport: impl Into<Option<PortList>>) -> Self {
-        Self {
-            sport: sport.into(),
-            dport: dport.into(),
-        }
-    }
-
-    fn from_options(options: &RuleOptions) -> Result<Self, Error> {
-        Ok(Self {
-            sport: options.sport.as_deref().map(|s| s.parse()).transpose()?,
-            dport: options.dport.as_deref().map(|s| s.parse()).transpose()?,
-        })
-    }
-
-    pub fn from_u16(sport: impl Into<Option<u16>>, dport: impl Into<Option<u16>>) -> Self {
-        Self::new(
-            sport.into().map(PortList::from),
-            dport.into().map(PortList::from),
-        )
-    }
-
-    pub fn sport(&self) -> Option<&PortList> {
-        self.sport.as_ref()
-    }
-
-    pub fn dport(&self) -> Option<&PortList> {
-        self.dport.as_ref()
-    }
-}
-
-#[derive(Clone, Debug, Default)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Tcp {
-    ports: Ports,
-}
-
-impl Tcp {
-    pub fn new(ports: Ports) -> Self {
-        Self { ports }
-    }
-
-    fn from_options(options: &RuleOptions) -> Result<Self, Error> {
-        Ok(Self {
-            ports: Ports::from_options(options)?,
-        })
-    }
-
-    pub fn ports(&self) -> &Ports {
-        &self.ports
-    }
-}
-
-impl From<Tcp> for Protocol {
-    fn from(value: Tcp) -> Self {
-        Protocol::Tcp(value)
-    }
-}
-
-#[derive(Clone, Debug, Default)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Sctp {
-    ports: Ports,
-}
-
-impl Sctp {
-    fn from_options(options: &RuleOptions) -> Result<Self, Error> {
-        Ok(Self {
-            ports: Ports::from_options(options)?,
-        })
-    }
-
-    pub fn ports(&self) -> &Ports {
-        &self.ports
-    }
-}
-
-#[derive(Clone, Debug, Default)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Icmp {
-    ty: Option<IcmpType>,
-    code: Option<IcmpCode>,
-}
-
-impl Icmp {
-    pub fn new_ty(ty: IcmpType) -> Self {
-        Self {
-            ty: Some(ty),
-            ..Default::default()
-        }
-    }
-
-    pub fn new_code(code: IcmpCode) -> Self {
-        Self {
-            code: Some(code),
-            ..Default::default()
-        }
-    }
-
-    fn from_options(options: &RuleOptions) -> Result<Self, Error> {
-        if let Some(ty) = &options.icmp_type {
-            return ty.parse();
-        }
-
-        Ok(Self::default())
-    }
-
-    pub fn ty(&self) -> Option<&IcmpType> {
-        self.ty.as_ref()
-    }
-
-    pub fn code(&self) -> Option<&IcmpCode> {
-        self.code.as_ref()
-    }
-}
-
-impl FromStr for Icmp {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        let mut this = Self::default();
-
-        if let Ok(ty) = s.parse() {
-            this.ty = Some(ty);
-            return Ok(this);
-        }
-
-        if let Ok(code) = s.parse() {
-            this.code = Some(code);
-            return Ok(this);
-        }
-
-        bail!("supplied string is neither a valid icmp type nor code");
-    }
-}
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum IcmpType {
-    Numeric(u8),
-    Named(&'static str),
-    Any,
-}
-
-#[sortable]
-const ICMP_TYPES: [(&str, u8); 15] = sorted!([
-    ("address-mask-reply", 18),
-    ("address-mask-request", 17),
-    ("destination-unreachable", 3),
-    ("echo-reply", 0),
-    ("echo-request", 8),
-    ("info-reply", 16),
-    ("info-request", 15),
-    ("parameter-problem", 12),
-    ("redirect", 5),
-    ("router-advertisement", 9),
-    ("router-solicitation", 10),
-    ("source-quench", 4),
-    ("time-exceeded", 11),
-    ("timestamp-reply", 14),
-    ("timestamp-request", 13),
-]);
-
-impl std::str::FromStr for IcmpType {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Error> {
-        if s.eq_ignore_ascii_case("any") {
-            return Ok(Self::Any);
-        }
-
-        if let Ok(ty) = s.trim().parse::<u8>() {
-            return Ok(Self::Numeric(ty));
-        }
-
-        if let Ok(index) = ICMP_TYPES.binary_search_by(|v| v.0.cmp(s)) {
-            return Ok(Self::Named(ICMP_TYPES[index].0));
-        }
-
-        bail!("{s:?} is not a valid icmp type");
-    }
-}
-
-impl fmt::Display for IcmpType {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self {
-            IcmpType::Numeric(ty) => write!(f, "{ty}"),
-            IcmpType::Named(ty) => write!(f, "{ty}"),
-            IcmpType::Any => write!(f, "any"),
-        }
-    }
-}
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum IcmpCode {
-    Numeric(u8),
-    Named(&'static str),
-}
-
-#[sortable]
-const ICMP_CODES: [(&str, u8); 7] = sorted!([
-    ("admin-prohibited", 13),
-    ("host-prohibited", 10),
-    ("host-unreachable", 1),
-    ("net-prohibited", 9),
-    ("net-unreachable", 0),
-    ("port-unreachable", 3),
-    ("prot-unreachable", 2),
-]);
-
-impl std::str::FromStr for IcmpCode {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Error> {
-        if let Ok(code) = s.trim().parse::<u8>() {
-            return Ok(Self::Numeric(code));
-        }
-
-        if let Ok(index) = ICMP_CODES.binary_search_by(|v| v.0.cmp(s)) {
-            return Ok(Self::Named(ICMP_CODES[index].0));
-        }
-
-        bail!("{s:?} is not a valid icmp code");
-    }
-}
-
-impl fmt::Display for IcmpCode {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self {
-            IcmpCode::Numeric(code) => write!(f, "{code}"),
-            IcmpCode::Named(code) => write!(f, "{code}"),
-        }
-    }
-}
-
-#[derive(Clone, Debug, Default)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Icmpv6 {
-    pub ty: Option<Icmpv6Type>,
-    pub code: Option<Icmpv6Code>,
-}
-
-impl Icmpv6 {
-    pub fn new_ty(ty: Icmpv6Type) -> Self {
-        Self {
-            ty: Some(ty),
-            ..Default::default()
-        }
-    }
-
-    pub fn new_code(code: Icmpv6Code) -> Self {
-        Self {
-            code: Some(code),
-            ..Default::default()
-        }
-    }
-
-    fn from_options(options: &RuleOptions) -> Result<Self, Error> {
-        if let Some(ty) = &options.icmp_type {
-            return ty.parse();
-        }
-
-        Ok(Self::default())
-    }
-
-    pub fn ty(&self) -> Option<&Icmpv6Type> {
-        self.ty.as_ref()
-    }
-
-    pub fn code(&self) -> Option<&Icmpv6Code> {
-        self.code.as_ref()
-    }
-}
-
-impl FromStr for Icmpv6 {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        let mut this = Self::default();
-
-        if let Ok(ty) = s.parse() {
-            this.ty = Some(ty);
-            return Ok(this);
-        }
-
-        if let Ok(code) = s.parse() {
-            this.code = Some(code);
-            return Ok(this);
-        }
-
-        bail!("supplied string is neither a valid icmpv6 type nor code");
-    }
-}
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum Icmpv6Type {
-    Numeric(u8),
-    Named(&'static str),
-    Any,
-}
-
-#[sortable]
-const ICMPV6_TYPES: [(&str, u8); 19] = sorted!([
-    ("destination-unreachable", 1),
-    ("echo-reply", 129),
-    ("echo-request", 128),
-    ("ind-neighbor-advert", 142),
-    ("ind-neighbor-solicit", 141),
-    ("mld-listener-done", 132),
-    ("mld-listener-query", 130),
-    ("mld-listener-reduction", 132),
-    ("mld-listener-report", 131),
-    ("mld2-listener-report", 143),
-    ("nd-neighbor-advert", 136),
-    ("nd-neighbor-solicit", 135),
-    ("nd-redirect", 137),
-    ("nd-router-advert", 134),
-    ("nd-router-solicit", 133),
-    ("packet-too-big", 2),
-    ("parameter-problem", 4),
-    ("router-renumbering", 138),
-    ("time-exceeded", 3),
-]);
-
-impl std::str::FromStr for Icmpv6Type {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Error> {
-        if s.eq_ignore_ascii_case("any") {
-            return Ok(Self::Any);
-        }
-
-        if let Ok(ty) = s.trim().parse::<u8>() {
-            return Ok(Self::Numeric(ty));
-        }
-
-        if let Ok(index) = ICMPV6_TYPES.binary_search_by(|v| v.0.cmp(s)) {
-            return Ok(Self::Named(ICMPV6_TYPES[index].0));
-        }
-
-        bail!("{s:?} is not a valid icmpv6 type");
-    }
-}
-
-impl fmt::Display for Icmpv6Type {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self {
-            Icmpv6Type::Numeric(ty) => write!(f, "{ty}"),
-            Icmpv6Type::Named(ty) => write!(f, "{ty}"),
-            Icmpv6Type::Any => write!(f, "any"),
-        }
-    }
-}
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum Icmpv6Code {
-    Numeric(u8),
-    Named(&'static str),
-}
-
-#[sortable]
-const ICMPV6_CODES: [(&str, u8); 6] = sorted!([
-    ("addr-unreachable", 3),
-    ("admin-prohibited", 1),
-    ("no-route", 0),
-    ("policy-fail", 5),
-    ("port-unreachable", 4),
-    ("reject-route", 6),
-]);
-
-impl std::str::FromStr for Icmpv6Code {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Error> {
-        if let Ok(code) = s.trim().parse::<u8>() {
-            return Ok(Self::Numeric(code));
-        }
-
-        if let Ok(index) = ICMPV6_CODES.binary_search_by(|v| v.0.cmp(s)) {
-            return Ok(Self::Named(ICMPV6_CODES[index].0));
-        }
-
-        bail!("{s:?} is not a valid icmpv6 code");
-    }
-}
-
-impl fmt::Display for Icmpv6Code {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self {
-            Icmpv6Code::Numeric(code) => write!(f, "{code}"),
-            Icmpv6Code::Named(code) => write!(f, "{code}"),
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use crate::firewall::types::{alias::AliasScope::Guest, Cidr};
-
-    use super::*;
-
-    #[test]
-    fn test_parse_action() {
-        assert_eq!(parse_action("REJECT").unwrap(), (None, Verdict::Reject, ""));
-
-        assert_eq!(
-            parse_action("SSH(ACCEPT) qweasd").unwrap(),
-            (Some("SSH"), Verdict::Accept, "qweasd")
-        );
-    }
-
-    #[test]
-    fn test_parse_ip_addr_match() {
-        for input in [
-            "10.0.0.0/8",
-            "10.0.0.0/8,192.168.0.0-192.168.255.255,172.16.0.1",
-            "dc/test",
-            "+guest/proxmox",
-        ] {
-            input.parse::<IpAddrMatch>().expect("valid ip match");
-        }
-
-        for input in [
-            "10.0.0.0/",
-            "10.0.0.0/8,192.168.256.0-192.168.255.255,172.16.0.1",
-            "dcc/test",
-            "+guest/",
-            "",
-        ] {
-            input.parse::<IpAddrMatch>().expect_err("invalid ip match");
-        }
-    }
-
-    #[test]
-    fn test_parse_options() {
-        let mut options: RuleOptions =
-            "-p udp --sport 123 --dport 234 -source 127.0.0.1 --dest 127.0.0.1 -i ens1 --log crit"
-                .parse()
-                .expect("valid option string");
-
-        assert_eq!(
-            options,
-            RuleOptions {
-                proto: Some("udp".to_string()),
-                sport: Some("123".to_string()),
-                dport: Some("234".to_string()),
-                source: Some("127.0.0.1".to_string()),
-                dest: Some("127.0.0.1".to_string()),
-                iface: Some("ens1".to_string()),
-                log: Some(LogLevel::Critical),
-                icmp_type: None,
-            }
-        );
-
-        options = "".parse().expect("valid option string");
-
-        assert_eq!(options, RuleOptions::default(),);
-    }
-
-    #[test]
-    fn test_construct_ip_match() {
-        IpMatch::new(
-            IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 8).unwrap())),
-            IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 8).unwrap())),
-        )
-        .expect("valid ip match");
-
-        IpMatch::new(
-            IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 8).unwrap())),
-            IpAddrMatch::Alias(AliasName::new(Guest, "test")),
-        )
-        .expect("valid ip match");
-
-        IpMatch::new(
-            IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 8).unwrap())),
-            IpAddrMatch::Ip(IpList::from(Cidr::new_v6([0x0000; 8], 8).unwrap())),
-        )
-        .expect_err("cannot mix ip families");
-
-        IpMatch::new(None, None).expect_err("at least one ip must be set");
-    }
-
-    #[test]
-    fn test_from_options() {
-        let mut options = RuleOptions {
-            proto: Some("tcp".to_string()),
-            sport: Some("123".to_string()),
-            dport: Some("234".to_string()),
-            source: Some("192.168.0.1".to_string()),
-            dest: Some("10.0.0.1".to_string()),
-            iface: Some("eth123".to_string()),
-            log: Some(LogLevel::Error),
-            ..Default::default()
-        };
-
-        assert_eq!(
-            Protocol::from_options(&options).unwrap().unwrap(),
-            Protocol::Tcp(Tcp::new(Ports::from_u16(123, 234))),
-        );
-
-        assert_eq!(
-            IpMatch::from_options(&options).unwrap().unwrap(),
-            IpMatch::new(
-                IpAddrMatch::Ip(IpList::from(Cidr::new_v4([192, 168, 0, 1], 32).unwrap()),),
-                IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 1], 32).unwrap()),)
-            )
-            .unwrap(),
-        );
-
-        options = RuleOptions::default();
-
-        assert_eq!(Protocol::from_options(&options).unwrap(), None,);
-
-        assert_eq!(IpMatch::from_options(&options).unwrap(), None,);
-
-        options = RuleOptions {
-            proto: Some("tcp".to_string()),
-            sport: Some("qwe".to_string()),
-            source: Some("qwe".to_string()),
-            ..Default::default()
-        };
-
-        Protocol::from_options(&options).expect_err("invalid source port");
-
-        IpMatch::from_options(&options).expect_err("invalid source address");
-
-        options = RuleOptions {
-            icmp_type: Some("port-unreachable".to_string()),
-            dport: Some("123".to_string()),
-            ..Default::default()
-        };
-
-        RuleMatch::from_options(Direction::In, Verdict::Drop, None, options)
-            .expect_err("cannot mix dport and icmp-type");
-    }
-
-    #[test]
-    fn test_parse_icmp() {
-        let mut icmp: Icmp = "info-request".parse().expect("valid icmp type");
-
-        assert_eq!(
-            icmp,
-            Icmp {
-                ty: Some(IcmpType::Named("info-request")),
-                code: None
-            }
-        );
-
-        icmp = "12".parse().expect("valid icmp type");
-
-        assert_eq!(
-            icmp,
-            Icmp {
-                ty: Some(IcmpType::Numeric(12)),
-                code: None
-            }
-        );
-
-        icmp = "port-unreachable".parse().expect("valid icmp code");
-
-        assert_eq!(
-            icmp,
-            Icmp {
-                ty: None,
-                code: Some(IcmpCode::Named("port-unreachable"))
-            }
-        );
-    }
-
-    #[test]
-    fn test_parse_icmp6() {
-        let mut icmp: Icmpv6 = "echo-reply".parse().expect("valid icmpv6 type");
-
-        assert_eq!(
-            icmp,
-            Icmpv6 {
-                ty: Some(Icmpv6Type::Named("echo-reply")),
-                code: None
-            }
-        );
-
-        icmp = "12".parse().expect("valid icmpv6 type");
-
-        assert_eq!(
-            icmp,
-            Icmpv6 {
-                ty: Some(Icmpv6Type::Numeric(12)),
-                code: None
-            }
-        );
-
-        icmp = "admin-prohibited".parse().expect("valid icmpv6 code");
-
-        assert_eq!(
-            icmp,
-            Icmpv6 {
-                ty: None,
-                code: Some(Icmpv6Code::Named("admin-prohibited"))
-            }
-        );
-    }
-}
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<Vmid, GuestEntry>,
-}
-
-impl From<HashMap<Vmid, GuestEntry>> for GuestMap {
-    fn from(guests: HashMap<Vmid, GuestEntry>) -> Self {
-        Self {
-            guests,
-            _version: Default::default(),
-        }
-    }
-}
-
-impl Deref for GuestMap {
-    type Target = HashMap<Vmid, GuestEntry>;
-
-    fn deref(&self) -> &Self::Target {
-        &self.guests
-    }
-}
-
-impl GuestMap {
-    pub fn new() -> Result<Self, Error> {
-        let data = std::fs::read(VMLIST_CONFIG_PATH)
-            .with_context(|| format!("failed to read guest map from {VMLIST_CONFIG_PATH}"))?;
-
-        serde_json::from_slice(&data).with_context(|| "failed to parse guest map".to_owned())
-    }
-
-    pub fn firewall_config_path(vmid: &Vmid) -> String {
-        format!("/etc/pve/firewall/{}.fw", vmid)
-    }
-
-    /// returns the local configuration path for a given Vmid.
-    ///
-    /// The caller must ensure that the given Vmid exists and is local to the node
-    pub fn config_path(vmid: &Vmid, entry: &GuestEntry) -> String {
-        format!(
-            "/etc/pve/local/{}/{}.conf",
-            entry.ty().config_folder(),
-            vmid
-        )
-    }
-}
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<u32> for Vmid {
-    fn from(value: u32) -> Self {
-        Self::new(value)
-    }
-}
-
-impl fmt::Display for Vmid {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        fmt::Display::fmt(&self.0, f)
-    }
-}
-
-impl FromStr for Vmid {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        Ok(Self(
-            s.parse()
-                .map_err(|_| format_err!("not a valid vmid: {s:?}"))?,
-        ))
-    }
-}
-
-serde_plain::derive_deserialize_from_fromstr!(Vmid, "valid vmid");
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<u8> = LOCAL_PART
-            .iter()
-            .chain(head.iter())
-            .chain(EUI64_MIDDLE_PART.iter())
-            .chain(tail.iter())
-            .copied()
-            .collect();
-
-        // we need to flip the 7th bit of the first eui64 byte
-        eui64_address[8] ^= 0x02;
-
-        Ipv6Addr::from(
-            TryInto::<[u8; 16]>::try_into(eui64_address).expect("is an u8 array with 16 entries"),
-        )
-    }
-}
-
-impl FromStr for MacAddress {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        let split = s.split(':');
-
-        let parsed = split
-            .into_iter()
-            .map(|elem| u8::from_str_radix(elem, 16))
-            .collect::<Result<Vec<u8>, _>>()
-            .map_err(Error::msg)?;
-
-        if parsed.len() != 6 {
-            bail!("Invalid amount of elements in MAC address!");
-        }
-
-        let address = &parsed.as_slice()[0..6];
-        Ok(Self(address.try_into().unwrap()))
-    }
-}
-
-impl Display for MacAddress {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(
-            f,
-            "{:<02X}:{:<02X}:{:<02X}:{:<02X}:{:<02X}:{:<02X}",
-            self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5]
-        )
-    }
-}
-
-#[derive(Debug, Clone, Copy)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum NetworkDeviceModel {
-    VirtIO,
-    Veth,
-    E1000,
-    Vmxnet3,
-    RTL8139,
-}
-
-impl FromStr for NetworkDeviceModel {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        match s {
-            "virtio" => Ok(NetworkDeviceModel::VirtIO),
-            "e1000" => Ok(NetworkDeviceModel::E1000),
-            "rtl8139" => Ok(NetworkDeviceModel::RTL8139),
-            "vmxnet3" => Ok(NetworkDeviceModel::Vmxnet3),
-            "veth" => Ok(NetworkDeviceModel::Veth),
-            _ => bail!("Invalid network device model: {s}"),
-        }
-    }
-}
-
-#[derive(Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct NetworkDevice {
-    model: NetworkDeviceModel,
-    mac_address: MacAddress,
-    firewall: bool,
-    ip: Option<Ipv4Cidr>,
-    ip6: Option<Ipv6Cidr>,
-}
-
-impl NetworkDevice {
-    pub fn model(&self) -> NetworkDeviceModel {
-        self.model
-    }
-
-    pub fn mac_address(&self) -> &MacAddress {
-        &self.mac_address
-    }
-
-    pub fn ip(&self) -> Option<&Ipv4Cidr> {
-        self.ip.as_ref()
-    }
-
-    pub fn ip6(&self) -> Option<&Ipv6Cidr> {
-        self.ip6.as_ref()
-    }
-
-    pub fn has_firewall(&self) -> bool {
-        self.firewall
-    }
-}
-
-impl FromStr for NetworkDevice {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        let (mut ty, mut hwaddr, mut firewall, mut ip, mut ip6) = (None, None, true, None, None);
-
-        for entry in PropertyIterator::new(s) {
-            let (key, value) = entry.unwrap();
-
-            if let Some(key) = key {
-                match key {
-                    "type" | "model" => {
-                        ty = Some(NetworkDeviceModel::from_str(&value)?);
-                    }
-                    "hwaddr" | "macaddr" => {
-                        hwaddr = Some(MacAddress::from_str(&value)?);
-                    }
-                    "firewall" => {
-                        firewall = parse_bool(&value)?;
-                    }
-                    "ip" => {
-                        if value == "dhcp" {
-                            continue;
-                        }
-
-                        ip = Some(Ipv4Cidr::from_str(&value)?);
-                    }
-                    "ip6" => {
-                        if value == "dhcp" || value == "auto" {
-                            continue;
-                        }
-
-                        ip6 = Some(Ipv6Cidr::from_str(&value)?);
-                    }
-                    _ => {
-                        if let Ok(model) = NetworkDeviceModel::from_str(key) {
-                            ty = Some(model);
-                            hwaddr = Some(MacAddress::from_str(&value)?);
-                        }
-                    }
-                }
-            }
-        }
-
-        if let (Some(ty), Some(hwaddr)) = (ty, hwaddr) {
-            return Ok(NetworkDevice {
-                model: ty,
-                mac_address: hwaddr,
-                firewall,
-                ip,
-                ip6,
-            });
-        }
-
-        bail!("No valid network device detected in string {s}");
-    }
-}
-
-#[derive(Debug, Default)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct NetworkConfig {
-    network_devices: HashMap<i64, NetworkDevice>,
-}
-
-impl NetworkConfig {
-    pub fn new() -> Self {
-        Self::default()
-    }
-
-    pub fn index_from_net_key(key: &str) -> Result<i64, Error> {
-        if let Some(digits) = key.strip_prefix("net") {
-            if let Some((digits, rest)) = match_digits(digits) {
-                let index: i64 = digits.parse()?;
-
-                if (0..31).contains(&index) && rest.is_empty() {
-                    return Ok(index);
-                }
-            }
-        }
-
-        bail!("No index found in net key string: {key}")
-    }
-
-    pub fn network_devices(&self) -> &HashMap<i64, NetworkDevice> {
-        &self.network_devices
-    }
-
-    pub fn parse<R: io::BufRead>(input: R) -> Result<Self, Error> {
-        let mut network_devices = HashMap::new();
-
-        for line in input.lines() {
-            let line = line?;
-            let line = line.trim();
-
-            if line.is_empty() || line.starts_with('#') {
-                continue;
-            }
-
-            if line.starts_with('[') {
-                break;
-            }
-
-            if line.starts_with("net") {
-                log::trace!("parsing net config line: {line}");
-
-                if let Some((mut key, mut value)) = line.split_once(':') {
-                    if key.is_empty() || value.is_empty() {
-                        continue;
-                    }
-
-                    key = key.trim();
-                    value = value.trim();
-
-                    if let Ok(index) = Self::index_from_net_key(key) {
-                        let network_device = NetworkDevice::from_str(value)?;
-
-                        let exists = network_devices.insert(index, network_device);
-
-                        if exists.is_some() {
-                            bail!("Duplicated config key detected: {key}");
-                        }
-                    } else {
-                        bail!("Encountered invalid net key in cfg: {key}");
-                    }
-                }
-            }
-        }
-
-        Ok(Self { network_devices })
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_parse_mac_address() {
-        for input in [
-            "aa:aa:aa:11:22:33",
-            "AA:BB:FF:11:22:33",
-            "bc:24:11:AA:bb:Ef",
-        ] {
-            let mac_address = input.parse::<MacAddress>().expect("valid mac address");
-
-            assert_eq!(input.to_uppercase(), mac_address.to_string());
-        }
-
-        for input in [
-            "aa:aa:aa:11:22:33:aa",
-            "AA:BB:FF:11:22",
-            "AA:BB:GG:11:22:33",
-            "AABBGG112233",
-            "",
-        ] {
-            input
-                .parse::<MacAddress>()
-                .expect_err("invalid mac address");
-        }
-    }
-
-    #[test]
-    fn test_eui64_link_local_address() {
-        let mac_address: MacAddress = "BC:24:11:49:8D:75".parse().expect("valid MAC address");
-
-        let link_local_address =
-            Ipv6Addr::from_str("fe80::be24:11ff:fe49:8d75").expect("valid IPv6 address");
-
-        assert_eq!(link_local_address, mac_address.eui64_link_local_address());
-    }
-
-    #[test]
-    fn test_parse_network_device() {
-        let mut network_device: NetworkDevice =
-            "virtio=AA:AA:AA:17:19:81,bridge=public,firewall=1,queues=4"
-                .parse()
-                .expect("valid network configuration");
-
-        assert_eq!(
-            network_device,
-            NetworkDevice {
-                model: NetworkDeviceModel::VirtIO,
-                mac_address: MacAddress([0xAA, 0xAA, 0xAA, 0x17, 0x19, 0x81]),
-                firewall: true,
-                ip: None,
-                ip6: None,
-            }
-        );
-
-        network_device = "model=virtio,macaddr=AA:AA:AA:17:19:81,bridge=public,firewall=1,queues=4"
-            .parse()
-            .expect("valid network configuration");
-
-        assert_eq!(
-            network_device,
-            NetworkDevice {
-                model: NetworkDeviceModel::VirtIO,
-                mac_address: MacAddress([0xAA, 0xAA, 0xAA, 0x17, 0x19, 0x81]),
-                firewall: true,
-                ip: None,
-                ip6: None,
-            }
-        );
-
-        network_device =
-            "name=eth0,bridge=public,firewall=0,hwaddr=AA:AA:AA:E2:3E:24,ip=dhcp,type=veth"
-                .parse()
-                .expect("valid network configuration");
-
-        assert_eq!(
-            network_device,
-            NetworkDevice {
-                model: NetworkDeviceModel::Veth,
-                mac_address: MacAddress([0xAA, 0xAA, 0xAA, 0xE2, 0x3E, 0x24]),
-                firewall: false,
-                ip: None,
-                ip6: None,
-            }
-        );
-
-        "model=virtio"
-            .parse::<NetworkDevice>()
-            .expect_err("invalid network configuration");
-
-        "bridge=public,firewall=0"
-            .parse::<NetworkDevice>()
-            .expect_err("invalid network configuration");
-
-        "".parse::<NetworkDevice>()
-            .expect_err("invalid network configuration");
-
-        "name=eth0,bridge=public,firewall=0,hwaddr=AA:AA:AG:E2:3E:24,ip=dhcp,type=veth"
-            .parse::<NetworkDevice>()
-            .expect_err("invalid network configuration");
-    }
-
-    #[test]
-    fn test_parse_network_confg() {
-        let mut guest_config = "\
-boot: order=scsi0;net0
-cores: 4
-cpu: host
-memory: 8192
-meta: creation-qemu=8.0.2,ctime=1700141675
-name: hoan-sdn
-net0: virtio=AA:BB:CC:F2:FE:75,bridge=public
-numa: 0
-ostype: l26
-parent: uwu
-scsi0: local-lvm:vm-999-disk-0,discard=on,iothread=1,size=32G
-scsihw: virtio-scsi-single
-smbios1: uuid=addb0cc6-0393-4269-a504-1eb46604cb8a
-sockets: 1
-vmgenid: 13bcbb05-3608-4d74-bf4f-d5d20c3538e8
-
-[snapshot]
-boot: order=scsi0;ide2;net0
-cores: 4
-cpu: x86-64-v2-AES
-ide2: NFS-iso:iso/proxmox-ve_8.0-2.iso,media=cdrom,size=1166488K
-memory: 8192
-meta: creation-qemu=8.0.2,ctime=1700141675
-name: test
-net2: virtio=AA:AA:AA:F2:FE:75,bridge=public,firewall=1
-numa: 0
-ostype: l26
-parent: pre-SDN
-scsi0: local-lvm:vm-999-disk-0,discard=on,iothread=1,size=32G
-scsihw: virtio-scsi-single
-smbios1: uuid=addb0cc6-0393-4269-a504-1eb46604cb8a
-snaptime: 1700143513
-sockets: 1
-vmgenid: 706fbe99-d28b-4047-a9cd-3677c859ca8a
-
-[snapshott]
-boot: order=scsi0;ide2;net0
-cores: 4
-cpu: host
-ide2: NFS-iso:iso/proxmox-ve_8.0-2.iso,media=cdrom,size=1166488K
-memory: 8192
-meta: creation-qemu=8.0.2,ctime=1700141675
-name: hoan-sdn
-net0: virtio=AA:AA:FF:F2:FE:75,bridge=public,firewall=0
-numa: 0
-ostype: l26
-parent: SDN
-scsi0: local-lvm:vm-999-disk-0,discard=on,iothread=1,size=32G
-scsihw: virtio-scsi-single
-smbios1: uuid=addb0cc6-0393-4269-a504-1eb46604cb8a
-snaptime: 1700158473
-sockets: 1
-vmgenid: 706fbe99-d28b-4047-a9cd-3677c859ca8a"
-            .as_bytes();
-
-        let mut network_config: NetworkConfig =
-            NetworkConfig::parse(guest_config).expect("valid network configuration");
-
-        assert_eq!(network_config.network_devices().len(), 1);
-
-        assert_eq!(
-            network_config.network_devices()[&0],
-            NetworkDevice {
-                model: NetworkDeviceModel::VirtIO,
-                mac_address: MacAddress([0xAA, 0xBB, 0xCC, 0xF2, 0xFE, 0x75]),
-                firewall: true,
-                ip: None,
-                ip6: None,
-            }
-        );
-
-        guest_config = "\
-arch: amd64
-cores: 1
-features: nesting=1
-hostname: dnsct
-memory: 512
-net0: name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:11,ip=dhcp,type=veth
-net2:   name=eth0,bridge=data,firewall=0,hwaddr=BC:24:11:47:83:12,ip=123.123.123.123/24,type=veth  
-net5: name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:13,ip6=fd80::1/64,type=veth
-ostype: alpine
-rootfs: local-lvm:vm-10001-disk-0,size=1G
-swap: 512
-unprivileged: 1"
-            .as_bytes();
-
-        network_config = NetworkConfig::parse(guest_config).expect("valid network configuration");
-
-        assert_eq!(network_config.network_devices().len(), 3);
-
-        assert_eq!(
-            network_config.network_devices()[&0],
-            NetworkDevice {
-                model: NetworkDeviceModel::Veth,
-                mac_address: MacAddress([0xBC, 0x24, 0x11, 0x47, 0x83, 0x11]),
-                firewall: true,
-                ip: None,
-                ip6: None,
-            }
-        );
-
-        assert_eq!(
-            network_config.network_devices()[&2],
-            NetworkDevice {
-                model: NetworkDeviceModel::Veth,
-                mac_address: MacAddress([0xBC, 0x24, 0x11, 0x47, 0x83, 0x12]),
-                firewall: false,
-                ip: Some(Ipv4Cidr::from_str("123.123.123.123/24").expect("valid ipv4")),
-                ip6: None,
-            }
-        );
-
-        assert_eq!(
-            network_config.network_devices()[&5],
-            NetworkDevice {
-                model: NetworkDeviceModel::Veth,
-                mac_address: MacAddress([0xBC, 0x24, 0x11, 0x47, 0x83, 0x13]),
-                firewall: true,
-                ip: None,
-                ip6: Some(Ipv6Cidr::from_str("fd80::1/64").expect("valid ipv6")),
-            }
-        );
-
-        NetworkConfig::parse(
-            "netqwe: name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:11,ip=dhcp,type=veth"
-                .as_bytes(),
-        )
-        .expect_err("invalid net key");
-
-        NetworkConfig::parse(
-            "net0 name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:11,ip=dhcp,type=veth"
-                .as_bytes(),
-        )
-        .expect_err("invalid net key");
-
-        NetworkConfig::parse(
-            "net33: name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:11,ip=dhcp,type=veth"
-                .as_bytes(),
-        )
-        .expect_err("invalid net key");
-    }
-}
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<IpAddr> {
-    let hostname = nodename();
-
-    log::trace!("resolving hostname");
-
-    format!("{hostname}:0")
-        .to_socket_addrs()
-        .expect("local hostname is resolvable")
-        .map(|addr| addr.ip())
-        .collect()
-}
-
-/// gets a list of all configured CIDRs on all network interfaces of this host
-///
-/// panics if unable to query the current network configuration
-pub fn network_interface_cidrs() -> Vec<Cidr> {
-    use nix::ifaddrs::getifaddrs;
-
-    log::trace!("reading networking interface list");
-
-    let mut cidrs = Vec::new();
-
-    let interfaces = getifaddrs().expect("should be able to query network interfaces");
-
-    for interface in interfaces {
-        if let (Some(address), Some(netmask)) = (interface.address, interface.netmask) {
-            match (address.family(), netmask.family()) {
-                (Some(AddressFamily::Inet), Some(AddressFamily::Inet)) => {
-                    let address = address.as_sockaddr_in().expect("is an IPv4 address").ip();
-
-                    let netmask = netmask
-                        .as_sockaddr_in()
-                        .expect("is an IPv4 address")
-                        .ip()
-                        .count_ones()
-                        .try_into()
-                        .expect("count_ones of u32 is < u8_max");
-
-                    cidrs.push(Cidr::new_v4(address, netmask).expect("netmask is valid"));
-                }
-                (Some(AddressFamily::Inet6), Some(AddressFamily::Inet6)) => {
-                    let address = address.as_sockaddr_in6().expect("is an IPv6 address").ip();
-
-                    let netmask_address =
-                        netmask.as_sockaddr_in6().expect("is an IPv6 address").ip();
-
-                    let netmask = u128::from_be_bytes(netmask_address.octets())
-                        .count_ones()
-                        .try_into()
-                        .expect("count_ones of u128 is < u8_max");
-
-                    cidrs.push(Cidr::new_v6(address, netmask).expect("netmask is valid"));
-                }
-                _ => continue,
-            }
-        }
-    }
-
-    cidrs
-}
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


  reply	other threads:[~2024-11-17 11:25 UTC|newest]

Thread overview: 24+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2024-11-15 12:09 [pve-devel] [PATCH docs/firewall/manager/proxmox{-firewall, -perl-rs} v4 0/9] autogenerate ipsets for sdn objects Stefan Hanreich
2024-11-15 12:09 ` Stefan Hanreich [this message]
2024-11-17 14:08   ` [pve-devel] applied: [PATCH proxmox-firewall v4 1/9] add proxmox-ve-rs crate - move proxmox-ve-config there Thomas Lamprecht
2024-11-15 12:09 ` [pve-devel] [PATCH proxmox-firewall v4 2/9] config: tests: add support for loading sdn and ipam config Stefan Hanreich
2024-11-17 14:08   ` [pve-devel] applied: " Thomas Lamprecht
2024-11-15 12:09 ` [pve-devel] [PATCH proxmox-firewall v4 3/9] ipsets: autogenerate ipsets for vnets and ipam Stefan Hanreich
2024-11-17 14:08   ` [pve-devel] applied: " Thomas Lamprecht
2024-11-15 12:09 ` [pve-devel] [PATCH pve-firewall v4 4/9] add support for loading sdn firewall configuration Stefan Hanreich
2024-11-17 14:57   ` Thomas Lamprecht
2024-11-18  9:22     ` Stefan Hanreich
2024-11-18  9:35     ` Stefan Hanreich
2024-11-15 12:09 ` [pve-devel] [PATCH pve-firewall v4 5/9] nftables: make is_nftables check flag file instead of config Stefan Hanreich
2024-11-17 14:58   ` [pve-devel] applied: " Thomas Lamprecht
2024-11-15 12:09 ` [pve-devel] [PATCH pve-firewall v4 6/9] api: load sdn ipsets Stefan Hanreich
2024-11-17 14:30   ` Thomas Lamprecht
2024-11-18  9:02     ` Stefan Hanreich
2024-11-18 11:38       ` Thomas Lamprecht
2024-11-18 13:23         ` Thomas Lamprecht
2024-11-18 13:32           ` Stefan Hanreich
2024-11-15 12:09 ` [pve-devel] [PATCH proxmox-perl-rs v4 7/9] add PVE::RS::Firewall::SDN module Stefan Hanreich
2024-11-17 14:36   ` [pve-devel] applied: " Thomas Lamprecht
2024-11-15 12:09 ` [pve-devel] [PATCH pve-manager v4 8/9] firewall: add sdn scope to IPRefSelector Stefan Hanreich
2024-11-15 12:09 ` [pve-devel] [PATCH pve-docs v4 9/9] sdn: add documentation for firewall integration Stefan Hanreich
2024-11-18 11:47 ` [pve-devel] [PATCH docs/firewall/manager/proxmox{-firewall, -perl-rs} v4 0/9] autogenerate ipsets for sdn objects Stefan Hanreich

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20241115120937.169342-2-s.hanreich@proxmox.com \
    --to=s.hanreich@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    --cc=w.bumiller@proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal