[pve-devel] [PATCH proxmox-firewall v4 1/9] add proxmox-ve-rs crate - move proxmox-ve-config there

Stefan Hanreich s.hanreich at proxmox.com
Fri Nov 15 13:09:29 CET 2024


Signed-off-by: Stefan Hanreich <s.hanreich at proxmox.com>
Reviewed-by: Wolfgang Bumiller <w.bumiller at 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 at proxmox.com>",
-    "Stefan Hanreich <s.hanreich at proxmox.com>",
-    "Proxmox Support Team <support at 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 at 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




More information about the pve-devel mailing list