public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v3 00/24] autogenerate ipsets for sdn objects
@ 2024-11-12 12:25 Stefan Hanreich
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 01/24] debian: add files for packaging Stefan Hanreich
                   ` (23 more replies)
  0 siblings, 24 replies; 26+ messages in thread
From: Stefan Hanreich @ 2024-11-12 12:25 UTC (permalink / raw)
  To: pve-devel

This patch series adds support for autogenerating ipsets for SDN objects. It
autogenerates ipsets for every VNet as follows:

* ipset containing all IP ranges of the VNet
* ipset containing all gateways of the VNet
* ipset containing all IP ranges of the subnet - except gateways
* ipset containing all dhcp ranges of the vnet

Additionally it generates an IPSet for every guest that has one or more IPAM
entries in the pve IPAM.

Those can then be used in the cluster / host / guest firewalls. Firewall rules
automatically update on changes of the SDN / IPAM configuration. This patch
series works for the old firewall as well as the new firewall.

The ipsets in nftables currently get generated as named ipsets in every table,
this means that the `nft list ruleset` output can get quite crowded for large
SDN configurations or large IPAM databases. Another option would be to only
include them as anonymous IPsets in the rules, which would make the nft output
far less crowded but this way would use more memory when making extensive use of
the sdn ipsets, since everytime it is used in a rule we create an entirely new
ipset.

The base for proxmox-ve-rs (which is a filtered version of the proxmox-firewall
repository can be found here:)

staff/s.hanreich/proxmox-ve-rs.git master

Dependencies:
* proxmox-perl-rs and proxmox-firewall depend on proxmox-ve-rs
* pve-firewall depends on proxmox-perl-rs
* pve-manager depends on pve-firewall

Changes from v2:
* rename end in IpRange to last to avoid confusion - thanks @Wolfgang
* bump Rust to 1.82 - thanks @Wolfgang
* improvements to the code generating IPSets - thanks @Wolfgang
* implement AsRef<str> for SDN name types - thanks @Wolfgang
* improve docstrings (proper capitalization and punctuation) - thanks @Wolfgang
* included a patch that removes proxmox-ve-config from proxmox-firewall

Changes from RFC:
* added documentation
* added separate SDN scope for IPSets
* rustfmt fixes

proxmox-ve-rs:

Stefan Hanreich (16):
  debian: add files for packaging
  firewall: add sdn scope for ipsets
  firewall: add ip range types
  firewall: address: use new iprange type for ip entries
  ipset: add range variant to addresses
  iprange: add methods for converting an ip range to cidrs
  ipset: address: add helper methods
  firewall: guest: derive traits according to rust api guidelines
  common: add allowlist
  sdn: add name types
  sdn: add ipam module
  sdn: ipam: add method for generating ipsets
  sdn: add config module
  sdn: config: add method for generating ipsets
  tests: add sdn config tests
  tests: add ipam tests

 .cargo/config.toml                            |    5 +
 .gitignore                                    |    8 +
 Cargo.toml                                    |   17 +
 Makefile                                      |   69 +
 build.sh                                      |   35 +
 bump.sh                                       |   44 +
 proxmox-ve-config/Cargo.toml                  |   19 +-
 proxmox-ve-config/debian/changelog            |    5 +
 proxmox-ve-config/debian/control              |   46 +
 proxmox-ve-config/debian/copyright            |   19 +
 proxmox-ve-config/debian/debcargo.toml        |    4 +
 proxmox-ve-config/src/common/mod.rs           |   31 +
 .../src/firewall/types/address.rs             | 1171 ++++++++++++++++-
 proxmox-ve-config/src/firewall/types/alias.rs |    4 +-
 proxmox-ve-config/src/firewall/types/ipset.rs |   32 +-
 proxmox-ve-config/src/firewall/types/rule.rs  |    6 +-
 proxmox-ve-config/src/guest/types.rs          |    7 +-
 proxmox-ve-config/src/guest/vm.rs             |   11 +-
 proxmox-ve-config/src/lib.rs                  |    2 +
 proxmox-ve-config/src/sdn/config.rs           |  640 +++++++++
 proxmox-ve-config/src/sdn/ipam.rs             |  368 ++++++
 proxmox-ve-config/src/sdn/mod.rs              |  251 ++++
 proxmox-ve-config/tests/sdn/main.rs           |  189 +++
 proxmox-ve-config/tests/sdn/resources/ipam.db |   26 +
 .../tests/sdn/resources/running-config.json   |   54 +
 25 files changed, 2976 insertions(+), 87 deletions(-)
 create mode 100644 .cargo/config.toml
 create mode 100644 .gitignore
 create mode 100644 Cargo.toml
 create mode 100644 Makefile
 create mode 100755 build.sh
 create mode 100755 bump.sh
 create mode 100644 proxmox-ve-config/debian/changelog
 create mode 100644 proxmox-ve-config/debian/control
 create mode 100644 proxmox-ve-config/debian/copyright
 create mode 100644 proxmox-ve-config/debian/debcargo.toml
 create mode 100644 proxmox-ve-config/src/common/mod.rs
 create mode 100644 proxmox-ve-config/src/sdn/config.rs
 create mode 100644 proxmox-ve-config/src/sdn/ipam.rs
 create mode 100644 proxmox-ve-config/src/sdn/mod.rs
 create mode 100644 proxmox-ve-config/tests/sdn/main.rs
 create mode 100644 proxmox-ve-config/tests/sdn/resources/ipam.db
 create mode 100644 proxmox-ve-config/tests/sdn/resources/running-config.json


proxmox-firewall:

Stefan Hanreich (3):
  add proxmox-ve-rs crate - move proxmox-ve-config there
  config: tests: add support for loading sdn and ipam config
  ipsets: autogenerate ipsets for vnets and ipam

 Cargo.toml                                    |    4 +-
 Makefile                                      |    2 +-
 proxmox-firewall/Cargo.toml                   |    2 +-
 proxmox-firewall/src/config.rs                |   69 +
 proxmox-firewall/src/firewall.rs              |   22 +-
 proxmox-firewall/src/object.rs                |   41 +-
 .../tests/input/.running-config.json          |   45 +
 proxmox-firewall/tests/input/ipam.db          |   32 +
 proxmox-firewall/tests/integration_tests.rs   |   10 +
 .../integration_tests__firewall.snap          | 1288 +++++++++++++++++
 proxmox-nftables/Cargo.toml                   |    2 +-
 proxmox-nftables/src/expression.rs            |   17 +-
 proxmox-nftables/src/types.rs                 |    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 -
 40 files changed, 1517 insertions(+), 6671 deletions(-)
 create mode 100644 proxmox-firewall/tests/input/.running-config.json
 create mode 100644 proxmox-firewall/tests/input/ipam.db
 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


pve-firewall:

Stefan Hanreich (2):
  add support for loading sdn firewall configuration
  api: load sdn ipsets

 src/PVE/API2/Firewall/Cluster.pm |  8 +++--
 src/PVE/API2/Firewall/Rules.pm   | 12 ++++---
 src/PVE/API2/Firewall/VM.pm      |  3 +-
 src/PVE/Firewall.pm              | 59 ++++++++++++++++++++++++++++----
 4 files changed, 67 insertions(+), 15 deletions(-)


proxmox-perl-rs:

Stefan Hanreich (1):
  add PVE::RS::Firewall::SDN module

 pve-rs/Cargo.toml          |   1 +
 pve-rs/Makefile            |   1 +
 pve-rs/src/firewall/mod.rs |   1 +
 pve-rs/src/firewall/sdn.rs | 130 +++++++++++++++++++++++++++++++++++++
 pve-rs/src/lib.rs          |   1 +
 5 files changed, 134 insertions(+)
 create mode 100644 pve-rs/src/firewall/mod.rs
 create mode 100644 pve-rs/src/firewall/sdn.rs


pve-manager:

Stefan Hanreich (1):
  firewall: add sdn scope to IPRefSelector

 www/manager6/form/IPRefSelector.js | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)


pve-docs:

Stefan Hanreich (1):
  sdn: add documentation for firewall integration

 pvesdn.adoc | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 92 insertions(+)


Summary over all repositories:
  76 files changed, 4793 insertions(+), 6774 deletions(-)

-- 
Generated by git-murpp 0.6.0

_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v3 01/24] debian: add files for packaging
  2024-11-12 12:25 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v3 00/24] autogenerate ipsets for sdn objects Stefan Hanreich
@ 2024-11-12 12:25 ` Stefan Hanreich
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 02/24] firewall: add sdn scope for ipsets Stefan Hanreich
                   ` (22 subsequent siblings)
  23 siblings, 0 replies; 26+ messages in thread
From: Stefan Hanreich @ 2024-11-12 12:25 UTC (permalink / raw)
  To: pve-devel

Since we now have a standalone repository for Proxmox VE related
crates, add the required files for packaging the crates contained in
this repository.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 .cargo/config.toml                     |  5 ++
 .gitignore                             |  8 +++
 Cargo.toml                             | 17 +++++++
 Makefile                               | 69 ++++++++++++++++++++++++++
 build.sh                               | 35 +++++++++++++
 bump.sh                                | 44 ++++++++++++++++
 proxmox-ve-config/Cargo.toml           | 19 +++----
 proxmox-ve-config/debian/changelog     |  5 ++
 proxmox-ve-config/debian/control       | 46 +++++++++++++++++
 proxmox-ve-config/debian/copyright     | 19 +++++++
 proxmox-ve-config/debian/debcargo.toml |  4 ++
 11 files changed, 260 insertions(+), 11 deletions(-)
 create mode 100644 .cargo/config.toml
 create mode 100644 .gitignore
 create mode 100644 Cargo.toml
 create mode 100644 Makefile
 create mode 100755 build.sh
 create mode 100755 bump.sh
 create mode 100644 proxmox-ve-config/debian/changelog
 create mode 100644 proxmox-ve-config/debian/control
 create mode 100644 proxmox-ve-config/debian/copyright
 create mode 100644 proxmox-ve-config/debian/debcargo.toml

diff --git a/.cargo/config.toml b/.cargo/config.toml
new file mode 100644
index 0000000..3b5b6e4
--- /dev/null
+++ b/.cargo/config.toml
@@ -0,0 +1,5 @@
+[source]
+[source.debian-packages]
+directory = "/usr/share/cargo/registry"
+[source.crates-io]
+replace-with = "debian-packages"
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d72b68b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+/target
+/*/target
+Cargo.lock
+**/*.rs.bk
+/*.buildinfo
+/*.changes
+/build
+/*-deb
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..dc7f312
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,17 @@
+[workspace]
+members = [
+    "proxmox-ve-config",
+]
+exclude = [
+    "build",
+]
+resolver = "2"
+
+[workspace.package]
+authors = ["Proxmox Support Team <support@proxmox.com>"]
+edition = "2021"
+license = "AGPL-3"
+homepage = "https://proxmox.com"
+exclude = [ "debian" ]
+rust-version = "1.82"
+
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..0da9b74
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,69 @@
+# Shortcut for common operations:
+
+CRATES != echo proxmox-*/Cargo.toml | sed -e 's|/Cargo.toml||g'
+
+# By default we just run checks:
+.PHONY: all
+all: check
+
+.PHONY: deb
+deb: $(foreach c,$(CRATES), $c-deb)
+	echo $(foreach c,$(CRATES), $c-deb)
+	lintian build/*.deb
+
+.PHONY: dsc
+dsc: $(foreach c,$(CRATES), $c-dsc)
+	echo $(foreach c,$(CRATES), $c-dsc)
+	lintian build/*.dsc
+
+.PHONY: autopkgtest
+autopkgtest: $(foreach c,$(CRATES), $c-autopkgtest)
+
+.PHONY: dinstall
+dinstall:
+	$(MAKE) clean
+	$(MAKE) deb
+	sudo -k dpkg -i build/librust-*.deb
+
+%-deb:
+	./build.sh $*
+	touch $@
+
+%-dsc:
+	BUILDCMD='dpkg-buildpackage -S -us -uc -d' ./build.sh $*
+	touch $@
+
+%-autopkgtest:
+	autopkgtest build/$* build/*.deb -- null
+	touch $@
+
+.PHONY: check
+check:
+	cargo test
+
+# Prints a diff between the current code and the one rustfmt would produce
+.PHONY: fmt
+fmt:
+	cargo +nightly fmt -- --check
+
+# Doc without dependencies
+.PHONY: doc
+doc:
+	cargo doc --no-deps
+
+.PHONY: clean
+clean:
+	cargo clean
+	rm -rf build/
+	rm -f -- *-deb *-dsc *-autopkgtest *.build *.buildinfo *.changes
+
+.PHONY: update
+update:
+	cargo update
+
+%-upload: %-deb
+	cd build; \
+	    dcmd --deb rust-$*_*.changes \
+	    | grep -v '.changes$$' \
+	    | tar -cf "$@.tar" -T-; \
+	    cat "$@.tar" | ssh -X repoman@repo.proxmox.com upload --product devel --dist bookworm
diff --git a/build.sh b/build.sh
new file mode 100755
index 0000000..39a8302
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,35 @@
+#!/bin/sh
+
+set -eux
+
+export CARGO=/usr/bin/cargo
+export RUSTC=/usr/bin/rustc
+
+CRATE=$1
+BUILDCMD=${BUILDCMD:-"dpkg-buildpackage -b -uc -us"}
+
+mkdir -p build
+echo system >build/rust-toolchain
+rm -rf "build/${CRATE}"
+
+CONTROL="$PWD/${CRATE}/debian/control"
+
+if [ -e "$CONTROL" ]; then
+    # check but only warn, debcargo fails anyway if crates are missing
+    dpkg-checkbuilddeps $PWD/${CRATE}/debian/control || true
+    # rm -f "$PWD/${CRATE}/debian/control"
+fi
+
+debcargo package \
+    --config "$PWD/${CRATE}/debian/debcargo.toml" \
+    --changelog-ready \
+    --no-overlay-write-back \
+    --directory "$PWD/build/${CRATE}" \
+    "${CRATE}" \
+    "$(dpkg-parsechangelog -l "${CRATE}/debian/changelog" -SVersion | sed -e 's/-.*//')"
+
+cd "build/${CRATE}"
+rm -f debian/source/format.debcargo.hint
+${BUILDCMD}
+
+cp debian/control "$CONTROL"
diff --git a/bump.sh b/bump.sh
new file mode 100755
index 0000000..08ad119
--- /dev/null
+++ b/bump.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+
+package=$1
+
+if [[ -z "$package" ]]; then
+	echo "USAGE:"
+	echo -e "\t bump.sh <crate> [patch|minor|major|<version>]"
+	echo ""
+	echo "Defaults to bumping patch version by 1"
+	exit 0
+fi
+
+cargo_set_version="$(command -v cargo-set-version)"
+if [[ -z "$cargo_set_version" || ! -x "$cargo_set_version" ]]; then
+	echo 'bump.sh requires "cargo set-version", provided by "cargo-edit".'
+	exit 1
+fi
+
+if [[ ! -e "$package/Cargo.toml" ]]; then
+	echo "Invalid crate '$package'"
+	exit 1
+fi
+
+version=$2
+if [[ -z "$version" ]]; then
+	version="patch"
+fi
+
+case "$version" in
+	patch|minor|major)
+		bump="--bump"
+		;;
+	*)
+		bump=
+		;;
+esac
+
+cargo_toml="$package/Cargo.toml"
+changelog="$package/debian/changelog"
+
+cargo set-version -p "$package" $bump "$version"
+version="$(cargo metadata --format-version=1 | jq ".packages[] | select(.name == \"$package\").version" | sed -e 's/\"//g')"
+DEBFULLNAME="Proxmox Support Team" DEBEMAIL="support@proxmox.com" dch --no-conf --changelog "$changelog" --newversion "$version-1" --distribution stable
+git commit --edit -sm "bump $package to $version-1" Cargo.toml "$cargo_toml" "$changelog"
diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml
index cc689c8..8639d7c 100644
--- a/proxmox-ve-config/Cargo.toml
+++ b/proxmox-ve-config/Cargo.toml
@@ -1,25 +1,22 @@
 [package]
 name = "proxmox-ve-config"
 version = "0.1.0"
-edition = "2021"
-authors = [
-    "Wolfgang Bumiller <w.bumiller@proxmox.com>",
-    "Stefan Hanreich <s.hanreich@proxmox.com>",
-    "Proxmox Support Team <support@proxmox.com>",
-]
-description = "Proxmox VE config parsing"
-license = "AGPL-3"
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+exclude.workspace = true
 
 [dependencies]
 log = "0.4"
 anyhow = "1"
 nix = "0.26"
+thiserror = "1.0.59"
 
 serde = { version = "1", features = [ "derive" ] }
 serde_json = "1"
 serde_plain = "1"
-serde_with = "2.3.3"
+serde_with = "3"
 
-proxmox-schema = "3.1.0"
-proxmox-sys = "0.5.3"
+proxmox-schema = "3.1.2"
+proxmox-sys = "0.6.4"
 proxmox-sortable-macro = "0.1.3"
diff --git a/proxmox-ve-config/debian/changelog b/proxmox-ve-config/debian/changelog
new file mode 100644
index 0000000..0dfd399
--- /dev/null
+++ b/proxmox-ve-config/debian/changelog
@@ -0,0 +1,5 @@
+proxmox-ve-config (0.1.0) UNRELEASED; urgency=medium
+
+  * Initial release.
+
+ -- Proxmox Support Team <support@proxmox.com>  Mon, 03 Jun 2024 10:51:11 +0200
diff --git a/proxmox-ve-config/debian/control b/proxmox-ve-config/debian/control
new file mode 100644
index 0000000..f5a9993
--- /dev/null
+++ b/proxmox-ve-config/debian/control
@@ -0,0 +1,46 @@
+Source: proxmox-ve-config
+Section: rust
+Priority: optional
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Build-Depends: cargo:native,
+               debhelper-compat (= 13),
+               dh-sequence-cargo,
+               librust-anyhow-1+default-dev,
+               librust-log-0.4+default-dev (>= 0.4.17-~~),
+               librust-nix-0.26+default-dev (>= 0.26.1-~~),
+               librust-thiserror-dev (>= 1.0.59-~~),
+               librust-proxmox-schema-3+default-dev,
+               librust-proxmox-sortable-macro-dev,
+               librust-proxmox-sys-dev,
+               librust-serde-1+default-dev,
+               librust-serde-1+derive-dev,
+               librust-serde-json-1+default-dev,
+               librust-serde-plain-1+default-dev,
+               librust-serde-with+default-dev,
+               libstd-rust-dev,
+               netbase,
+               python3,
+               rustc:native (>= 1.82),
+Standards-Version: 4.7.0
+Homepage: https://www.proxmox.com
+
+Package: librust-proxmox-ve-config-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-anyhow-1+default-dev,
+ librust-log-0.4+default-dev (>= 0.4.17-~~),
+ librust-nix-0.26+default-dev (>= 0.26.1-~~),
+ librust-thiserror-dev (>= 1.0.59-~~),
+ librust-proxmox-schema-3+default-dev,
+ librust-proxmox-sortable-macro-dev,
+ librust-proxmox-sys-dev,
+ librust-serde-1+default-dev,
+ librust-serde-1+derive-dev,
+ librust-serde-json-1+default-dev,
+ librust-serde-plain-1+default-dev,
+ librust-serde-with+default-dev,
+ libstd-rust-dev,
+Description: Library for reading and writing the configuration files of Proxmox
+ Virtual Enviroment.
diff --git a/proxmox-ve-config/debian/copyright b/proxmox-ve-config/debian/copyright
new file mode 100644
index 0000000..2d3374f
--- /dev/null
+++ b/proxmox-ve-config/debian/copyright
@@ -0,0 +1,19 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+
+Files:
+ *
+Copyright: 2019 - 2024 Proxmox Server Solutions GmbH <support@proxmox.com>
+License: AGPL-3.0-or-later
+ This program is free software: you can redistribute it and/or modify it under
+ the terms of the GNU Affero General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or (at your option) any
+ later version.
+ .
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ details.
+ .
+ You should have received a copy of the GNU Affero General Public License along
+ with this program. If not, see <https://www.gnu.org/licenses/>.
+
diff --git a/proxmox-ve-config/debian/debcargo.toml b/proxmox-ve-config/debian/debcargo.toml
new file mode 100644
index 0000000..27510eb
--- /dev/null
+++ b/proxmox-ve-config/debian/debcargo.toml
@@ -0,0 +1,4 @@
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support@proxmox.com>"
+
-- 
2.39.5


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v3 02/24] firewall: add sdn scope for ipsets
  2024-11-12 12:25 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v3 00/24] autogenerate ipsets for sdn objects Stefan Hanreich
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 01/24] debian: add files for packaging Stefan Hanreich
@ 2024-11-12 12:25 ` Stefan Hanreich
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 03/24] firewall: add ip range types Stefan Hanreich
                   ` (21 subsequent siblings)
  23 siblings, 0 replies; 26+ messages in thread
From: Stefan Hanreich @ 2024-11-12 12:25 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/src/firewall/types/ipset.rs | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/proxmox-ve-config/src/firewall/types/ipset.rs b/proxmox-ve-config/src/firewall/types/ipset.rs
index c1af642..6fbdca8 100644
--- a/proxmox-ve-config/src/firewall/types/ipset.rs
+++ b/proxmox-ve-config/src/firewall/types/ipset.rs
@@ -14,6 +14,7 @@ use crate::guest::vm::NetworkConfig;
 pub enum IpsetScope {
     Datacenter,
     Guest,
+    Sdn,
 }
 
 impl FromStr for IpsetScope {
@@ -23,6 +24,7 @@ impl FromStr for IpsetScope {
         Ok(match s {
             "+dc" => IpsetScope::Datacenter,
             "+guest" => IpsetScope::Guest,
+            "+sdn" => IpsetScope::Sdn,
             _ => bail!("invalid scope for ipset: {s}"),
         })
     }
@@ -33,6 +35,7 @@ impl Display for IpsetScope {
         let prefix = match self {
             Self::Datacenter => "dc",
             Self::Guest => "guest",
+            Self::Sdn => "sdn",
         };
 
         f.write_str(prefix)
-- 
2.39.5


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v3 03/24] firewall: add ip range types
  2024-11-12 12:25 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v3 00/24] autogenerate ipsets for sdn objects Stefan Hanreich
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 01/24] debian: add files for packaging Stefan Hanreich
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 02/24] firewall: add sdn scope for ipsets Stefan Hanreich
@ 2024-11-12 12:25 ` Stefan Hanreich
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 04/24] firewall: address: use new iprange type for ip entries Stefan Hanreich
                   ` (20 subsequent siblings)
  23 siblings, 0 replies; 26+ messages in thread
From: Stefan Hanreich @ 2024-11-12 12:25 UTC (permalink / raw)
  To: pve-devel

Currently we are using tuples to represent IP ranges which is
suboptimal. Validation logic and invariant checking needs to happen at
every site using the IP range rather than having a unified struct for
enforcing those invariants.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 .../src/firewall/types/address.rs             | 230 +++++++++++++++++-
 1 file changed, 228 insertions(+), 2 deletions(-)

diff --git a/proxmox-ve-config/src/firewall/types/address.rs b/proxmox-ve-config/src/firewall/types/address.rs
index e48ac1b..f7bde51 100644
--- a/proxmox-ve-config/src/firewall/types/address.rs
+++ b/proxmox-ve-config/src/firewall/types/address.rs
@@ -1,9 +1,9 @@
-use std::fmt;
+use std::fmt::{self, Display};
 use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
 use std::ops::Deref;
 
 use anyhow::{bail, format_err, Error};
-use serde_with::DeserializeFromStr;
+use serde_with::{DeserializeFromStr, SerializeDisplay};
 
 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
 pub enum Family {
@@ -239,6 +239,202 @@ impl<T: Into<Ipv6Addr>> From<T> for Ipv6Cidr {
     }
 }
 
+#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
+pub enum IpRangeError {
+    MismatchedFamilies,
+    StartGreaterThanLast,
+    InvalidFormat,
+}
+
+impl std::error::Error for IpRangeError {}
+
+impl Display for IpRangeError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.write_str(match self {
+            IpRangeError::MismatchedFamilies => "mismatched ip address families",
+            IpRangeError::StartGreaterThanLast => "start is greater than last",
+            IpRangeError::InvalidFormat => "invalid ip range format",
+        })
+    }
+}
+
+/// Represents a range of IPv4 or IPv6 addresses.
+///
+/// For more information see [`AddressRange`]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, SerializeDisplay, DeserializeFromStr)]
+pub enum IpRange {
+    V4(AddressRange<Ipv4Addr>),
+    V6(AddressRange<Ipv6Addr>),
+}
+
+impl IpRange {
+    /// Returns the family of the IpRange.
+    pub fn family(&self) -> Family {
+        match self {
+            IpRange::V4(_) => Family::V4,
+            IpRange::V6(_) => Family::V6,
+        }
+    }
+
+    /// Creates a new [`IpRange`] from two [`IpAddr`].
+    ///
+    /// # Errors
+    ///
+    /// This function will return an error if start and last IP address are not from the same family.
+    pub fn new(start: impl Into<IpAddr>, last: impl Into<IpAddr>) -> Result<Self, IpRangeError> {
+        match (start.into(), last.into()) {
+            (IpAddr::V4(start), IpAddr::V4(last)) => Self::new_v4(start, last),
+            (IpAddr::V6(start), IpAddr::V6(last)) => Self::new_v6(start, last),
+            _ => Err(IpRangeError::MismatchedFamilies),
+        }
+    }
+
+    /// construct a new Ipv4 Range
+    pub fn new_v4(
+        start: impl Into<Ipv4Addr>,
+        last: impl Into<Ipv4Addr>,
+    ) -> Result<Self, IpRangeError> {
+        Ok(IpRange::V4(AddressRange::new_v4(start, last)?))
+    }
+
+    pub fn new_v6(
+        start: impl Into<Ipv6Addr>,
+        last: impl Into<Ipv6Addr>,
+    ) -> Result<Self, IpRangeError> {
+        Ok(IpRange::V6(AddressRange::new_v6(start, last)?))
+    }
+}
+
+impl std::str::FromStr for IpRange {
+    type Err = IpRangeError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if let Ok(range) = s.parse() {
+            return Ok(IpRange::V4(range));
+        }
+
+        if let Ok(range) = s.parse() {
+            return Ok(IpRange::V6(range));
+        }
+
+        Err(IpRangeError::InvalidFormat)
+    }
+}
+
+impl fmt::Display for IpRange {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            IpRange::V4(range) => range.fmt(f),
+            IpRange::V6(range) => range.fmt(f),
+        }
+    }
+}
+
+/// Represents a range of IP addresses from start to last.
+///
+/// This type is for encapsulation purposes for the [`IpRange`] enum and should be instantiated via
+/// that enum.
+///
+/// # Invariants
+///
+/// * start and last have the same IP address family
+/// * start is less than or equal to last
+///
+/// # Textual representation
+///
+/// Two IP addresses separated by a hyphen, e.g.: `127.0.0.1-127.0.0.255`
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct AddressRange<T> {
+    start: T,
+    last: T,
+}
+
+impl AddressRange<Ipv4Addr> {
+    pub(crate) fn new_v4(
+        start: impl Into<Ipv4Addr>,
+        last: impl Into<Ipv4Addr>,
+    ) -> Result<AddressRange<Ipv4Addr>, IpRangeError> {
+        let (start, last) = (start.into(), last.into());
+
+        if start > last {
+            return Err(IpRangeError::StartGreaterThanLast);
+        }
+
+        Ok(Self { start, last })
+    }
+}
+
+impl AddressRange<Ipv6Addr> {
+    pub(crate) fn new_v6(
+        start: impl Into<Ipv6Addr>,
+        last: impl Into<Ipv6Addr>,
+    ) -> Result<AddressRange<Ipv6Addr>, IpRangeError> {
+        let (start, last) = (start.into(), last.into());
+
+        if start > last {
+            return Err(IpRangeError::StartGreaterThanLast);
+        }
+
+        Ok(Self { start, last })
+    }
+}
+
+impl<T> AddressRange<T> {
+    pub fn start(&self) -> &T {
+        &self.start
+    }
+
+    pub fn last(&self) -> &T {
+        &self.last
+    }
+}
+
+impl std::str::FromStr for AddressRange<Ipv4Addr> {
+    type Err = IpRangeError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if let Some((start, last)) = s.split_once('-') {
+            let start_address = start
+                .parse::<Ipv4Addr>()
+                .map_err(|_| IpRangeError::InvalidFormat)?;
+
+            let last_address = last
+                .parse::<Ipv4Addr>()
+                .map_err(|_| IpRangeError::InvalidFormat)?;
+
+            return Self::new_v4(start_address, last_address);
+        }
+
+        Err(IpRangeError::InvalidFormat)
+    }
+}
+
+impl std::str::FromStr for AddressRange<Ipv6Addr> {
+    type Err = IpRangeError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if let Some((start, last)) = s.split_once('-') {
+            let start_address = start
+                .parse::<Ipv6Addr>()
+                .map_err(|_| IpRangeError::InvalidFormat)?;
+
+            let last_address = last
+                .parse::<Ipv6Addr>()
+                .map_err(|_| IpRangeError::InvalidFormat)?;
+
+            return Self::new_v6(start_address, last_address);
+        }
+
+        Err(IpRangeError::InvalidFormat)
+    }
+}
+
+impl<T: fmt::Display> fmt::Display for AddressRange<T> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}-{}", self.start, self.last)
+    }
+}
+
 #[derive(Clone, Debug)]
 #[cfg_attr(test, derive(Eq, PartialEq))]
 pub enum IpEntry {
@@ -612,4 +808,34 @@ mod tests {
         ])
         .expect_err("cannot mix ip families in ip list");
     }
+
+    #[test]
+    fn test_ip_range() {
+        IpRange::new([10, 0, 0, 2], [10, 0, 0, 1]).unwrap_err();
+
+        IpRange::new(
+            [0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x1000],
+            [0x2001, 0x0db8, 0, 0, 0, 0, 0, 0],
+        )
+        .unwrap_err();
+
+        let v4_range = IpRange::new([10, 0, 0, 0], [10, 0, 0, 100]).unwrap();
+        assert_eq!(v4_range.family(), Family::V4);
+
+        let v6_range = IpRange::new(
+            [0x2001, 0x0db8, 0, 0, 0, 0, 0, 0],
+            [0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x1000],
+        )
+        .unwrap();
+        assert_eq!(v6_range.family(), Family::V6);
+
+        "10.0.0.1-10.0.0.100".parse::<IpRange>().unwrap();
+        "2001:db8::1-2001:db8::f".parse::<IpRange>().unwrap();
+
+        "10.0.0.1-2001:db8::1000".parse::<IpRange>().unwrap_err();
+        "2001:db8::1-192.168.0.2".parse::<IpRange>().unwrap_err();
+
+        "10.0.0.1-10.0.0.0".parse::<IpRange>().unwrap_err();
+        "2001:db8::1-2001:db8::0".parse::<IpRange>().unwrap_err();
+    }
 }
-- 
2.39.5


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v3 04/24] firewall: address: use new iprange type for ip entries
  2024-11-12 12:25 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v3 00/24] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (2 preceding siblings ...)
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 03/24] firewall: add ip range types Stefan Hanreich
@ 2024-11-12 12:25 ` Stefan Hanreich
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 05/24] ipset: add range variant to addresses Stefan Hanreich
                   ` (19 subsequent siblings)
  23 siblings, 0 replies; 26+ messages in thread
From: Stefan Hanreich @ 2024-11-12 12:25 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 .../src/firewall/types/address.rs             | 81 +++++++------------
 proxmox-ve-config/src/firewall/types/rule.rs  |  6 +-
 2 files changed, 31 insertions(+), 56 deletions(-)

diff --git a/proxmox-ve-config/src/firewall/types/address.rs b/proxmox-ve-config/src/firewall/types/address.rs
index f7bde51..d269054 100644
--- a/proxmox-ve-config/src/firewall/types/address.rs
+++ b/proxmox-ve-config/src/firewall/types/address.rs
@@ -439,57 +439,30 @@ impl<T: fmt::Display> fmt::Display for AddressRange<T> {
 #[cfg_attr(test, derive(Eq, PartialEq))]
 pub enum IpEntry {
     Cidr(Cidr),
-    Range(IpAddr, IpAddr),
+    Range(IpRange),
 }
 
 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!")
+        if let Ok(cidr) = s.parse() {
+            return Ok(IpEntry::Cidr(cidr));
         }
 
-        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!"),
+        if let Ok(range) = s.parse() {
+            return Ok(IpEntry::Range(range));
         }
+
+        bail!("Invalid IP entry: {s}");
     }
 }
 
 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}"),
+            Self::Cidr(ip) => ip.fmt(f),
+            Self::Range(range) => range.fmt(f),
         }
     }
 }
@@ -498,19 +471,7 @@ 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")
-            }
+            Self::Range(range) => range.family(),
         }
     }
 }
@@ -521,6 +482,12 @@ impl From<Cidr> for IpEntry {
     }
 }
 
+impl From<IpRange> for IpEntry {
+    fn from(value: IpRange) -> Self {
+        IpEntry::Range(value)
+    }
+}
+
 #[derive(Clone, Debug, DeserializeFromStr)]
 #[cfg_attr(test, derive(Eq, PartialEq))]
 pub struct IpList {
@@ -708,7 +675,9 @@ mod tests {
 
         assert_eq!(
             entry,
-            IpEntry::Range([192, 168, 0, 1].into(), [192, 168, 99, 255].into())
+            IpRange::new_v4([192, 168, 0, 1], [192, 168, 99, 255])
+                .expect("valid IP range")
+                .into()
         );
 
         entry = "fe80::1".parse().expect("valid IP entry");
@@ -733,10 +702,12 @@ mod tests {
 
         assert_eq!(
             entry,
-            IpEntry::Range(
-                [0xFD80, 0, 0, 0, 0, 0, 0, 1].into(),
-                [0xFD80, 0, 0, 0, 0, 0, 0, 0xFFFF].into(),
+            IpRange::new_v6(
+                [0xFD80, 0, 0, 0, 0, 0, 0, 1],
+                [0xFD80, 0, 0, 0, 0, 0, 0, 0xFFFF],
             )
+            .expect("valid IP range")
+            .into()
         );
 
         "192.168.100.0-192.168.99.255"
@@ -764,7 +735,9 @@ mod tests {
                 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()),
+                    IpRange::new_v4([172, 16, 0, 0], [172, 32, 255, 255])
+                        .unwrap()
+                        .into(),
                 ],
                 family: Family::V4,
             }
diff --git a/proxmox-ve-config/src/firewall/types/rule.rs b/proxmox-ve-config/src/firewall/types/rule.rs
index 20deb3a..5374bb0 100644
--- a/proxmox-ve-config/src/firewall/types/rule.rs
+++ b/proxmox-ve-config/src/firewall/types/rule.rs
@@ -242,7 +242,7 @@ impl FromStr for RuleGroup {
 #[cfg(test)]
 mod tests {
     use crate::firewall::types::{
-        address::{IpEntry, IpList},
+        address::{IpEntry, IpList, IpRange},
         alias::{AliasName, AliasScope},
         ipset::{IpsetName, IpsetScope},
         log::LogLevel,
@@ -322,7 +322,9 @@ mod tests {
                         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()),
+                                IpRange::new_v4([20, 0, 0, 0], [20, 255, 255, 255])
+                                    .unwrap()
+                                    .into(),
                                 IpEntry::Cidr(Cidr::new_v4([192, 168, 0, 0], 16).unwrap()),
                             ])
                             .unwrap()
-- 
2.39.5


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v3 05/24] ipset: add range variant to addresses
  2024-11-12 12:25 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v3 00/24] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (3 preceding siblings ...)
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 04/24] firewall: address: use new iprange type for ip entries Stefan Hanreich
@ 2024-11-12 12:25 ` Stefan Hanreich
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 06/24] iprange: add methods for converting an ip range to cidrs Stefan Hanreich
                   ` (18 subsequent siblings)
  23 siblings, 0 replies; 26+ messages in thread
From: Stefan Hanreich @ 2024-11-12 12:25 UTC (permalink / raw)
  To: pve-devel

A range can be used to store multiple IP addresses in an ipset that do
not neatly fit into a single CIDR.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/src/firewall/types/ipset.rs | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/proxmox-ve-config/src/firewall/types/ipset.rs b/proxmox-ve-config/src/firewall/types/ipset.rs
index 6fbdca8..e8131b5 100644
--- a/proxmox-ve-config/src/firewall/types/ipset.rs
+++ b/proxmox-ve-config/src/firewall/types/ipset.rs
@@ -6,7 +6,7 @@ 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::address::{Cidr, IpRange};
 use crate::firewall::types::alias::AliasName;
 use crate::guest::vm::NetworkConfig;
 
@@ -93,6 +93,7 @@ impl Display for IpsetName {
 pub enum IpsetAddress {
     Alias(AliasName),
     Cidr(Cidr),
+    Range(IpRange),
 }
 
 impl FromStr for IpsetAddress {
@@ -117,6 +118,12 @@ impl<T: Into<Cidr>> From<T> for IpsetAddress {
     }
 }
 
+impl From<IpRange> for IpsetAddress {
+    fn from(range: IpRange) -> Self {
+        IpsetAddress::Range(range)
+    }
+}
+
 #[derive(Debug)]
 #[cfg_attr(test, derive(Eq, PartialEq))]
 pub struct IpsetEntry {
-- 
2.39.5


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v3 06/24] iprange: add methods for converting an ip range to cidrs
  2024-11-12 12:25 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v3 00/24] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (4 preceding siblings ...)
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 05/24] ipset: add range variant to addresses Stefan Hanreich
@ 2024-11-12 12:25 ` Stefan Hanreich
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 07/24] ipset: address: add helper methods Stefan Hanreich
                   ` (17 subsequent siblings)
  23 siblings, 0 replies; 26+ messages in thread
From: Stefan Hanreich @ 2024-11-12 12:25 UTC (permalink / raw)
  To: pve-devel

This is mainly used in proxmox-perl-rs, so the generated ipsets can be
used in pve-firewall where only CIDRs are supported.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 .../src/firewall/types/address.rs             | 818 ++++++++++++++++++
 1 file changed, 818 insertions(+)

diff --git a/proxmox-ve-config/src/firewall/types/address.rs b/proxmox-ve-config/src/firewall/types/address.rs
index d269054..95c58a7 100644
--- a/proxmox-ve-config/src/firewall/types/address.rs
+++ b/proxmox-ve-config/src/firewall/types/address.rs
@@ -303,6 +303,17 @@ impl IpRange {
     ) -> Result<Self, IpRangeError> {
         Ok(IpRange::V6(AddressRange::new_v6(start, last)?))
     }
+
+    /// Converts an IpRange into the minimal amount of CIDRs.
+    ///
+    /// see the concrete implementations of [`AddressRange<Ipv4Addr>`] or [`AddressRange<Ipv6Addr>`]
+    /// respectively
+    pub fn to_cidrs(&self) -> Vec<Cidr> {
+        match self {
+            IpRange::V4(range) => range.to_cidrs().into_iter().map(Cidr::from).collect(),
+            IpRange::V6(range) => range.to_cidrs().into_iter().map(Cidr::from).collect(),
+        }
+    }
 }
 
 impl std::str::FromStr for IpRange {
@@ -362,6 +373,71 @@ impl AddressRange<Ipv4Addr> {
 
         Ok(Self { start, last })
     }
+
+    /// Returns the minimum amount of CIDRs that exactly represent the range
+    ///
+    /// The idea behind this algorithm is as follows:
+    ///
+    /// Start iterating with current = start of the IP range
+    ///
+    /// Find two netmasks
+    /// * The largest CIDR that the current IP can be the first of
+    /// * The largest CIDR that *only* contains IPs from current - last
+    ///
+    /// Add the smaller of the two CIDRs to our result and current to the first IP that is in
+    /// the range but not in the CIDR we just added. Proceed until we reached the last of the IP
+    /// range.
+    ///
+    pub fn to_cidrs(&self) -> Vec<Ipv4Cidr> {
+        let mut cidrs = Vec::new();
+
+        let mut current = u32::from_be_bytes(self.start.octets());
+        let last = u32::from_be_bytes(self.last.octets());
+
+        if current == last {
+            // valid Ipv4 since netmask is 32
+            cidrs.push(Ipv4Cidr::new(current, 32).unwrap());
+            return cidrs;
+        }
+
+        // special case this, since this is the only possibility of overflow
+        // when calculating delta_min_mask - makes everything a lot easier
+        if current == u32::MIN && last == u32::MAX {
+            // valid Ipv4 since it is `0.0.0.0/0`
+            cidrs.push(Ipv4Cidr::new(current, 0).unwrap());
+            return cidrs;
+        }
+
+        while current <= last {
+            // netmask of largest CIDR that current IP can be the first of
+            // cast is safe, because trailing zeroes can at most be 32
+            let current_max_mask = IPV4_LENGTH - (current.trailing_zeros() as u8);
+
+            // netmask of largest CIDR that *only* contains IPs of the remaining range
+            // is at most 32 due to unwrap_or returning 32 and ilog2 being at most 31
+            let delta_min_mask = ((last - current) + 1) // safe due to special case above
+                .checked_ilog2() // should never occur due to special case, but for good measure
+                .map(|mask| IPV4_LENGTH - mask as u8)
+                .unwrap_or(IPV4_LENGTH);
+
+            // at most 32, due to current/delta being at most 32
+            let netmask = u8::max(current_max_mask, delta_min_mask);
+
+            // netmask is at most 32, therefore safe to unwrap
+            cidrs.push(Ipv4Cidr::new(current, netmask).unwrap());
+
+            let delta = 2u32.saturating_pow((IPV4_LENGTH - netmask).into());
+
+            if let Some(result) = current.checked_add(delta) {
+                current = result
+            } else {
+                // we reached the end of IP address space
+                break;
+            }
+        }
+
+        cidrs
+    }
 }
 
 impl AddressRange<Ipv6Addr> {
@@ -377,6 +453,61 @@ impl AddressRange<Ipv6Addr> {
 
         Ok(Self { start, last })
     }
+
+    /// Returns the minimum amount of CIDRs that exactly represent the [`AddressRange`].
+    ///
+    /// This function works analogous to the IPv4 version, please refer to the respective
+    /// documentation of [`AddressRange<Ipv4Addr>`]
+    pub fn to_cidrs(&self) -> Vec<Ipv6Cidr> {
+        let mut cidrs = Vec::new();
+
+        let mut current = u128::from_be_bytes(self.start.octets());
+        let last = u128::from_be_bytes(self.last.octets());
+
+        if current == last {
+            // valid Ipv6 since netmask is 128
+            cidrs.push(Ipv6Cidr::new(current, 128).unwrap());
+            return cidrs;
+        }
+
+        // special case this, since this is the only possibility of overflow
+        // when calculating delta_min_mask - makes everything a lot easier
+        if current == u128::MIN && last == u128::MAX {
+            // valid Ipv6 since it is `::/0`
+            cidrs.push(Ipv6Cidr::new(current, 0).unwrap());
+            return cidrs;
+        }
+
+        while current <= last {
+            // netmask of largest CIDR that current IP can be the first of
+            // cast is safe, because trailing zeroes can at most be 128
+            let current_max_mask = IPV6_LENGTH - (current.trailing_zeros() as u8);
+
+            // netmask of largest CIDR that *only* contains IPs of the remaining range
+            // is at most 128 due to unwrap_or returning 128 and ilog2 being at most 31
+            let delta_min_mask = ((last - current) + 1) // safe due to special case above
+                .checked_ilog2() // should never occur due to special case, but for good measure
+                .map(|mask| IPV6_LENGTH - mask as u8)
+                .unwrap_or(IPV6_LENGTH);
+
+            // at most 128, due to current/delta being at most 128
+            let netmask = u8::max(current_max_mask, delta_min_mask);
+
+            // netmask is at most 128, therefore safe to unwrap
+            cidrs.push(Ipv6Cidr::new(current, netmask).unwrap());
+
+            let delta = 2u128.saturating_pow((IPV6_LENGTH - netmask).into());
+
+            if let Some(result) = current.checked_add(delta) {
+                current = result
+            } else {
+                // we reached the end of IP address space
+                break;
+            }
+        }
+
+        cidrs
+    }
 }
 
 impl<T> AddressRange<T> {
@@ -811,4 +942,691 @@ mod tests {
         "10.0.0.1-10.0.0.0".parse::<IpRange>().unwrap_err();
         "2001:db8::1-2001:db8::0".parse::<IpRange>().unwrap_err();
     }
+
+    #[test]
+    fn test_ipv4_to_cidrs() {
+        let range = AddressRange::new_v4([192, 168, 0, 100], [192, 168, 0, 100]).unwrap();
+
+        assert_eq!(
+            [Ipv4Cidr::new([192, 168, 0, 100], 32).unwrap()],
+            range.to_cidrs().as_slice()
+        );
+
+        let range = AddressRange::new_v4([192, 168, 0, 100], [192, 168, 0, 200]).unwrap();
+
+        assert_eq!(
+            [
+                Ipv4Cidr::new([192, 168, 0, 100], 30).unwrap(),
+                Ipv4Cidr::new([192, 168, 0, 104], 29).unwrap(),
+                Ipv4Cidr::new([192, 168, 0, 112], 28).unwrap(),
+                Ipv4Cidr::new([192, 168, 0, 128], 26).unwrap(),
+                Ipv4Cidr::new([192, 168, 0, 192], 29).unwrap(),
+                Ipv4Cidr::new([192, 168, 0, 200], 32).unwrap(),
+            ],
+            range.to_cidrs().as_slice()
+        );
+
+        let range = AddressRange::new_v4([192, 168, 0, 101], [192, 168, 0, 200]).unwrap();
+
+        assert_eq!(
+            [
+                Ipv4Cidr::new([192, 168, 0, 101], 32).unwrap(),
+                Ipv4Cidr::new([192, 168, 0, 102], 31).unwrap(),
+                Ipv4Cidr::new([192, 168, 0, 104], 29).unwrap(),
+                Ipv4Cidr::new([192, 168, 0, 112], 28).unwrap(),
+                Ipv4Cidr::new([192, 168, 0, 128], 26).unwrap(),
+                Ipv4Cidr::new([192, 168, 0, 192], 29).unwrap(),
+                Ipv4Cidr::new([192, 168, 0, 200], 32).unwrap(),
+            ],
+            range.to_cidrs().as_slice()
+        );
+
+        let range = AddressRange::new_v4([192, 168, 0, 101], [192, 168, 0, 101]).unwrap();
+
+        assert_eq!(
+            [Ipv4Cidr::new([192, 168, 0, 101], 32).unwrap()],
+            range.to_cidrs().as_slice()
+        );
+
+        let range = AddressRange::new_v4([192, 168, 0, 101], [192, 168, 0, 201]).unwrap();
+
+        assert_eq!(
+            [
+                Ipv4Cidr::new([192, 168, 0, 101], 32).unwrap(),
+                Ipv4Cidr::new([192, 168, 0, 102], 31).unwrap(),
+                Ipv4Cidr::new([192, 168, 0, 104], 29).unwrap(),
+                Ipv4Cidr::new([192, 168, 0, 112], 28).unwrap(),
+                Ipv4Cidr::new([192, 168, 0, 128], 26).unwrap(),
+                Ipv4Cidr::new([192, 168, 0, 192], 29).unwrap(),
+                Ipv4Cidr::new([192, 168, 0, 200], 31).unwrap(),
+            ],
+            range.to_cidrs().as_slice()
+        );
+
+        let range = AddressRange::new_v4([192, 168, 0, 0], [192, 168, 0, 255]).unwrap();
+
+        assert_eq!(
+            [Ipv4Cidr::new([192, 168, 0, 0], 24).unwrap(),],
+            range.to_cidrs().as_slice()
+        );
+
+        let range = AddressRange::new_v4([0, 0, 0, 0], [255, 255, 255, 255]).unwrap();
+
+        assert_eq!(
+            [Ipv4Cidr::new([0, 0, 0, 0], 0).unwrap(),],
+            range.to_cidrs().as_slice()
+        );
+
+        let range = AddressRange::new_v4([0, 0, 0, 1], [255, 255, 255, 255]).unwrap();
+
+        assert_eq!(
+            [
+                Ipv4Cidr::new([0, 0, 0, 1], 32).unwrap(),
+                Ipv4Cidr::new([0, 0, 0, 2], 31).unwrap(),
+                Ipv4Cidr::new([0, 0, 0, 4], 30).unwrap(),
+                Ipv4Cidr::new([0, 0, 0, 8], 29).unwrap(),
+                Ipv4Cidr::new([0, 0, 0, 16], 28).unwrap(),
+                Ipv4Cidr::new([0, 0, 0, 32], 27).unwrap(),
+                Ipv4Cidr::new([0, 0, 0, 64], 26).unwrap(),
+                Ipv4Cidr::new([0, 0, 0, 128], 25).unwrap(),
+                Ipv4Cidr::new([0, 0, 1, 0], 24).unwrap(),
+                Ipv4Cidr::new([0, 0, 2, 0], 23).unwrap(),
+                Ipv4Cidr::new([0, 0, 4, 0], 22).unwrap(),
+                Ipv4Cidr::new([0, 0, 8, 0], 21).unwrap(),
+                Ipv4Cidr::new([0, 0, 16, 0], 20).unwrap(),
+                Ipv4Cidr::new([0, 0, 32, 0], 19).unwrap(),
+                Ipv4Cidr::new([0, 0, 64, 0], 18).unwrap(),
+                Ipv4Cidr::new([0, 0, 128, 0], 17).unwrap(),
+                Ipv4Cidr::new([0, 1, 0, 0], 16).unwrap(),
+                Ipv4Cidr::new([0, 2, 0, 0], 15).unwrap(),
+                Ipv4Cidr::new([0, 4, 0, 0], 14).unwrap(),
+                Ipv4Cidr::new([0, 8, 0, 0], 13).unwrap(),
+                Ipv4Cidr::new([0, 16, 0, 0], 12).unwrap(),
+                Ipv4Cidr::new([0, 32, 0, 0], 11).unwrap(),
+                Ipv4Cidr::new([0, 64, 0, 0], 10).unwrap(),
+                Ipv4Cidr::new([0, 128, 0, 0], 9).unwrap(),
+                Ipv4Cidr::new([1, 0, 0, 0], 8).unwrap(),
+                Ipv4Cidr::new([2, 0, 0, 0], 7).unwrap(),
+                Ipv4Cidr::new([4, 0, 0, 0], 6).unwrap(),
+                Ipv4Cidr::new([8, 0, 0, 0], 5).unwrap(),
+                Ipv4Cidr::new([16, 0, 0, 0], 4).unwrap(),
+                Ipv4Cidr::new([32, 0, 0, 0], 3).unwrap(),
+                Ipv4Cidr::new([64, 0, 0, 0], 2).unwrap(),
+                Ipv4Cidr::new([128, 0, 0, 0], 1).unwrap(),
+            ],
+            range.to_cidrs().as_slice()
+        );
+
+        let range = AddressRange::new_v4([0, 0, 0, 0], [255, 255, 255, 254]).unwrap();
+
+        assert_eq!(
+            [
+                Ipv4Cidr::new([0, 0, 0, 0], 1).unwrap(),
+                Ipv4Cidr::new([128, 0, 0, 0], 2).unwrap(),
+                Ipv4Cidr::new([192, 0, 0, 0], 3).unwrap(),
+                Ipv4Cidr::new([224, 0, 0, 0], 4).unwrap(),
+                Ipv4Cidr::new([240, 0, 0, 0], 5).unwrap(),
+                Ipv4Cidr::new([248, 0, 0, 0], 6).unwrap(),
+                Ipv4Cidr::new([252, 0, 0, 0], 7).unwrap(),
+                Ipv4Cidr::new([254, 0, 0, 0], 8).unwrap(),
+                Ipv4Cidr::new([255, 0, 0, 0], 9).unwrap(),
+                Ipv4Cidr::new([255, 128, 0, 0], 10).unwrap(),
+                Ipv4Cidr::new([255, 192, 0, 0], 11).unwrap(),
+                Ipv4Cidr::new([255, 224, 0, 0], 12).unwrap(),
+                Ipv4Cidr::new([255, 240, 0, 0], 13).unwrap(),
+                Ipv4Cidr::new([255, 248, 0, 0], 14).unwrap(),
+                Ipv4Cidr::new([255, 252, 0, 0], 15).unwrap(),
+                Ipv4Cidr::new([255, 254, 0, 0], 16).unwrap(),
+                Ipv4Cidr::new([255, 255, 0, 0], 17).unwrap(),
+                Ipv4Cidr::new([255, 255, 128, 0], 18).unwrap(),
+                Ipv4Cidr::new([255, 255, 192, 0], 19).unwrap(),
+                Ipv4Cidr::new([255, 255, 224, 0], 20).unwrap(),
+                Ipv4Cidr::new([255, 255, 240, 0], 21).unwrap(),
+                Ipv4Cidr::new([255, 255, 248, 0], 22).unwrap(),
+                Ipv4Cidr::new([255, 255, 252, 0], 23).unwrap(),
+                Ipv4Cidr::new([255, 255, 254, 0], 24).unwrap(),
+                Ipv4Cidr::new([255, 255, 255, 0], 25).unwrap(),
+                Ipv4Cidr::new([255, 255, 255, 128], 26).unwrap(),
+                Ipv4Cidr::new([255, 255, 255, 192], 27).unwrap(),
+                Ipv4Cidr::new([255, 255, 255, 224], 28).unwrap(),
+                Ipv4Cidr::new([255, 255, 255, 240], 29).unwrap(),
+                Ipv4Cidr::new([255, 255, 255, 248], 30).unwrap(),
+                Ipv4Cidr::new([255, 255, 255, 252], 31).unwrap(),
+                Ipv4Cidr::new([255, 255, 255, 254], 32).unwrap(),
+            ],
+            range.to_cidrs().as_slice()
+        );
+
+        let range = AddressRange::new_v4([0, 0, 0, 0], [0, 0, 0, 0]).unwrap();
+
+        assert_eq!(
+            [Ipv4Cidr::new([0, 0, 0, 0], 32).unwrap(),],
+            range.to_cidrs().as_slice()
+        );
+
+        let range = AddressRange::new_v4([255, 255, 255, 255], [255, 255, 255, 255]).unwrap();
+
+        assert_eq!(
+            [Ipv4Cidr::new([255, 255, 255, 255], 32).unwrap(),],
+            range.to_cidrs().as_slice()
+        );
+    }
+
+    #[test]
+    fn test_ipv6_to_cidrs() {
+        let range = AddressRange::new_v6(
+            [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1000],
+            [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1000],
+        )
+        .unwrap();
+
+        assert_eq!(
+            [Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1000], 128).unwrap()],
+            range.to_cidrs().as_slice()
+        );
+
+        let range = AddressRange::new_v6(
+            [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1000],
+            [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x2000],
+        )
+        .unwrap();
+
+        assert_eq!(
+            [
+                Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1000], 116).unwrap(),
+                Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x2000], 128).unwrap(),
+            ],
+            range.to_cidrs().as_slice()
+        );
+
+        let range = AddressRange::new_v6(
+            [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1001],
+            [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x2000],
+        )
+        .unwrap();
+
+        assert_eq!(
+            [
+                Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1001], 128).unwrap(),
+                Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1002], 127).unwrap(),
+                Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1004], 126).unwrap(),
+                Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1008], 125).unwrap(),
+                Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1010], 124).unwrap(),
+                Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1020], 123).unwrap(),
+                Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1040], 122).unwrap(),
+                Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1080], 121).unwrap(),
+                Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1100], 120).unwrap(),
+                Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1200], 119).unwrap(),
+                Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1400], 118).unwrap(),
+                Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1800], 117).unwrap(),
+                Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x2000], 128).unwrap(),
+            ],
+            range.to_cidrs().as_slice()
+        );
+
+        let range = AddressRange::new_v6(
+            [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1001],
+            [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1001],
+        )
+        .unwrap();
+
+        assert_eq!(
+            [Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1001], 128).unwrap(),],
+            range.to_cidrs().as_slice()
+        );
+
+        let range = AddressRange::new_v6(
+            [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1001],
+            [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x2001],
+        )
+        .unwrap();
+
+        assert_eq!(
+            [
+                Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1001], 128).unwrap(),
+                Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1002], 127).unwrap(),
+                Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1004], 126).unwrap(),
+                Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1008], 125).unwrap(),
+                Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1010], 124).unwrap(),
+                Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1020], 123).unwrap(),
+                Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1040], 122).unwrap(),
+                Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1080], 121).unwrap(),
+                Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1100], 120).unwrap(),
+                Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1200], 119).unwrap(),
+                Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1400], 118).unwrap(),
+                Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1800], 117).unwrap(),
+                Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x2000], 127).unwrap(),
+            ],
+            range.to_cidrs().as_slice()
+        );
+
+        let range = AddressRange::new_v6(
+            [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0],
+            [0x2001, 0x0DB8, 0, 0, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF],
+        )
+        .unwrap();
+
+        assert_eq!(
+            [Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0], 64).unwrap()],
+            range.to_cidrs().as_slice()
+        );
+
+        let range = AddressRange::new_v6(
+            [0, 0, 0, 0, 0, 0, 0, 0],
+            [
+                0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
+            ],
+        )
+        .unwrap();
+
+        assert_eq!(
+            [Ipv6Cidr::new([0, 0, 0, 0, 0, 0, 0, 0], 0).unwrap(),],
+            range.to_cidrs().as_slice()
+        );
+
+        let range = AddressRange::new_v6(
+            [0, 0, 0, 0, 0, 0, 0, 0x0001],
+            [
+                0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
+            ],
+        )
+        .unwrap();
+
+        assert_eq!(
+            [
+                "::1/128".parse::<Ipv6Cidr>().unwrap(),
+                "::2/127".parse::<Ipv6Cidr>().unwrap(),
+                "::4/126".parse::<Ipv6Cidr>().unwrap(),
+                "::8/125".parse::<Ipv6Cidr>().unwrap(),
+                "::10/124".parse::<Ipv6Cidr>().unwrap(),
+                "::20/123".parse::<Ipv6Cidr>().unwrap(),
+                "::40/122".parse::<Ipv6Cidr>().unwrap(),
+                "::80/121".parse::<Ipv6Cidr>().unwrap(),
+                "::100/120".parse::<Ipv6Cidr>().unwrap(),
+                "::200/119".parse::<Ipv6Cidr>().unwrap(),
+                "::400/118".parse::<Ipv6Cidr>().unwrap(),
+                "::800/117".parse::<Ipv6Cidr>().unwrap(),
+                "::1000/116".parse::<Ipv6Cidr>().unwrap(),
+                "::2000/115".parse::<Ipv6Cidr>().unwrap(),
+                "::4000/114".parse::<Ipv6Cidr>().unwrap(),
+                "::8000/113".parse::<Ipv6Cidr>().unwrap(),
+                "::1:0/112".parse::<Ipv6Cidr>().unwrap(),
+                "::2:0/111".parse::<Ipv6Cidr>().unwrap(),
+                "::4:0/110".parse::<Ipv6Cidr>().unwrap(),
+                "::8:0/109".parse::<Ipv6Cidr>().unwrap(),
+                "::10:0/108".parse::<Ipv6Cidr>().unwrap(),
+                "::20:0/107".parse::<Ipv6Cidr>().unwrap(),
+                "::40:0/106".parse::<Ipv6Cidr>().unwrap(),
+                "::80:0/105".parse::<Ipv6Cidr>().unwrap(),
+                "::100:0/104".parse::<Ipv6Cidr>().unwrap(),
+                "::200:0/103".parse::<Ipv6Cidr>().unwrap(),
+                "::400:0/102".parse::<Ipv6Cidr>().unwrap(),
+                "::800:0/101".parse::<Ipv6Cidr>().unwrap(),
+                "::1000:0/100".parse::<Ipv6Cidr>().unwrap(),
+                "::2000:0/99".parse::<Ipv6Cidr>().unwrap(),
+                "::4000:0/98".parse::<Ipv6Cidr>().unwrap(),
+                "::8000:0/97".parse::<Ipv6Cidr>().unwrap(),
+                "::1:0:0/96".parse::<Ipv6Cidr>().unwrap(),
+                "::2:0:0/95".parse::<Ipv6Cidr>().unwrap(),
+                "::4:0:0/94".parse::<Ipv6Cidr>().unwrap(),
+                "::8:0:0/93".parse::<Ipv6Cidr>().unwrap(),
+                "::10:0:0/92".parse::<Ipv6Cidr>().unwrap(),
+                "::20:0:0/91".parse::<Ipv6Cidr>().unwrap(),
+                "::40:0:0/90".parse::<Ipv6Cidr>().unwrap(),
+                "::80:0:0/89".parse::<Ipv6Cidr>().unwrap(),
+                "::100:0:0/88".parse::<Ipv6Cidr>().unwrap(),
+                "::200:0:0/87".parse::<Ipv6Cidr>().unwrap(),
+                "::400:0:0/86".parse::<Ipv6Cidr>().unwrap(),
+                "::800:0:0/85".parse::<Ipv6Cidr>().unwrap(),
+                "::1000:0:0/84".parse::<Ipv6Cidr>().unwrap(),
+                "::2000:0:0/83".parse::<Ipv6Cidr>().unwrap(),
+                "::4000:0:0/82".parse::<Ipv6Cidr>().unwrap(),
+                "::8000:0:0/81".parse::<Ipv6Cidr>().unwrap(),
+                "::1:0:0:0/80".parse::<Ipv6Cidr>().unwrap(),
+                "::2:0:0:0/79".parse::<Ipv6Cidr>().unwrap(),
+                "::4:0:0:0/78".parse::<Ipv6Cidr>().unwrap(),
+                "::8:0:0:0/77".parse::<Ipv6Cidr>().unwrap(),
+                "::10:0:0:0/76".parse::<Ipv6Cidr>().unwrap(),
+                "::20:0:0:0/75".parse::<Ipv6Cidr>().unwrap(),
+                "::40:0:0:0/74".parse::<Ipv6Cidr>().unwrap(),
+                "::80:0:0:0/73".parse::<Ipv6Cidr>().unwrap(),
+                "::100:0:0:0/72".parse::<Ipv6Cidr>().unwrap(),
+                "::200:0:0:0/71".parse::<Ipv6Cidr>().unwrap(),
+                "::400:0:0:0/70".parse::<Ipv6Cidr>().unwrap(),
+                "::800:0:0:0/69".parse::<Ipv6Cidr>().unwrap(),
+                "::1000:0:0:0/68".parse::<Ipv6Cidr>().unwrap(),
+                "::2000:0:0:0/67".parse::<Ipv6Cidr>().unwrap(),
+                "::4000:0:0:0/66".parse::<Ipv6Cidr>().unwrap(),
+                "::8000:0:0:0/65".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:0:1::/64".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:0:2::/63".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:0:4::/62".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:0:8::/61".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:0:10::/60".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:0:20::/59".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:0:40::/58".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:0:80::/57".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:0:100::/56".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:0:200::/55".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:0:400::/54".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:0:800::/53".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:0:1000::/52".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:0:2000::/51".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:0:4000::/50".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:0:8000::/49".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:1::/48".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:2::/47".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:4::/46".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:8::/45".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:10::/44".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:20::/43".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:40::/42".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:80::/41".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:100::/40".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:200::/39".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:400::/38".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:800::/37".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:1000::/36".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:2000::/35".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:4000::/34".parse::<Ipv6Cidr>().unwrap(),
+                "0:0:8000::/33".parse::<Ipv6Cidr>().unwrap(),
+                "0:1::/32".parse::<Ipv6Cidr>().unwrap(),
+                "0:2::/31".parse::<Ipv6Cidr>().unwrap(),
+                "0:4::/30".parse::<Ipv6Cidr>().unwrap(),
+                "0:8::/29".parse::<Ipv6Cidr>().unwrap(),
+                "0:10::/28".parse::<Ipv6Cidr>().unwrap(),
+                "0:20::/27".parse::<Ipv6Cidr>().unwrap(),
+                "0:40::/26".parse::<Ipv6Cidr>().unwrap(),
+                "0:80::/25".parse::<Ipv6Cidr>().unwrap(),
+                "0:100::/24".parse::<Ipv6Cidr>().unwrap(),
+                "0:200::/23".parse::<Ipv6Cidr>().unwrap(),
+                "0:400::/22".parse::<Ipv6Cidr>().unwrap(),
+                "0:800::/21".parse::<Ipv6Cidr>().unwrap(),
+                "0:1000::/20".parse::<Ipv6Cidr>().unwrap(),
+                "0:2000::/19".parse::<Ipv6Cidr>().unwrap(),
+                "0:4000::/18".parse::<Ipv6Cidr>().unwrap(),
+                "0:8000::/17".parse::<Ipv6Cidr>().unwrap(),
+                "1::/16".parse::<Ipv6Cidr>().unwrap(),
+                "2::/15".parse::<Ipv6Cidr>().unwrap(),
+                "4::/14".parse::<Ipv6Cidr>().unwrap(),
+                "8::/13".parse::<Ipv6Cidr>().unwrap(),
+                "10::/12".parse::<Ipv6Cidr>().unwrap(),
+                "20::/11".parse::<Ipv6Cidr>().unwrap(),
+                "40::/10".parse::<Ipv6Cidr>().unwrap(),
+                "80::/9".parse::<Ipv6Cidr>().unwrap(),
+                "100::/8".parse::<Ipv6Cidr>().unwrap(),
+                "200::/7".parse::<Ipv6Cidr>().unwrap(),
+                "400::/6".parse::<Ipv6Cidr>().unwrap(),
+                "800::/5".parse::<Ipv6Cidr>().unwrap(),
+                "1000::/4".parse::<Ipv6Cidr>().unwrap(),
+                "2000::/3".parse::<Ipv6Cidr>().unwrap(),
+                "4000::/2".parse::<Ipv6Cidr>().unwrap(),
+                "8000::/1".parse::<Ipv6Cidr>().unwrap(),
+            ],
+            range.to_cidrs().as_slice()
+        );
+
+        let range = AddressRange::new_v6(
+            [0, 0, 0, 0, 0, 0, 0, 0],
+            [
+                0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFE,
+            ],
+        )
+        .unwrap();
+
+        assert_eq!(
+            [
+                "::/1".parse::<Ipv6Cidr>().unwrap(),
+                "8000::/2".parse::<Ipv6Cidr>().unwrap(),
+                "c000::/3".parse::<Ipv6Cidr>().unwrap(),
+                "e000::/4".parse::<Ipv6Cidr>().unwrap(),
+                "f000::/5".parse::<Ipv6Cidr>().unwrap(),
+                "f800::/6".parse::<Ipv6Cidr>().unwrap(),
+                "fc00::/7".parse::<Ipv6Cidr>().unwrap(),
+                "fe00::/8".parse::<Ipv6Cidr>().unwrap(),
+                "ff00::/9".parse::<Ipv6Cidr>().unwrap(),
+                "ff80::/10".parse::<Ipv6Cidr>().unwrap(),
+                "ffc0::/11".parse::<Ipv6Cidr>().unwrap(),
+                "ffe0::/12".parse::<Ipv6Cidr>().unwrap(),
+                "fff0::/13".parse::<Ipv6Cidr>().unwrap(),
+                "fff8::/14".parse::<Ipv6Cidr>().unwrap(),
+                "fffc::/15".parse::<Ipv6Cidr>().unwrap(),
+                "fffe::/16".parse::<Ipv6Cidr>().unwrap(),
+                "ffff::/17".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:8000::/18".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:c000::/19".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:e000::/20".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:f000::/21".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:f800::/22".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:fc00::/23".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:fe00::/24".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ff00::/25".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ff80::/26".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffc0::/27".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffe0::/28".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:fff0::/29".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:fff8::/30".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:fffc::/31".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:fffe::/32".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff::/33".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:8000::/34".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:c000::/35".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:e000::/36".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:f000::/37".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:f800::/38".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:fc00::/39".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:fe00::/40".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ff00::/41".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ff80::/42".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffc0::/43".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffe0::/44".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:fff0::/45".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:fff8::/46".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:fffc::/47".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:fffe::/48".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff::/49".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:8000::/50".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:c000::/51".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:e000::/52".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:f000::/53".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:f800::/54".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:fc00::/55".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:fe00::/56".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:ff00::/57".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:ff80::/58".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:ffc0::/59".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:ffe0::/60".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:fff0::/61".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:fff8::/62".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:fffc::/63".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:fffe::/64".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:ffff::/65".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:ffff:8000::/66".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:ffff:c000::/67".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:ffff:e000::/68".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:ffff:f000::/69".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:ffff:f800::/70".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:ffff:fc00::/71".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:ffff:fe00::/72".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:ffff:ff00::/73".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:ffff:ff80::/74".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:ffff:ffc0::/75".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:ffff:ffe0::/76".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:ffff:fff0::/77".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:ffff:fff8::/78".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:ffff:fffc::/79".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:ffff:fffe::/80".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:ffff:ffff::/81".parse::<Ipv6Cidr>().unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:8000::/82"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:c000::/83"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:e000::/84"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:f000::/85"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:f800::/86"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:fc00::/87"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:fe00::/88"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ff00::/89"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ff80::/90"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffc0::/91"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffe0::/92"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:fff0::/93"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:fff8::/94"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:fffc::/95"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:fffe::/96"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff::/97"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:8000:0/98"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:c000:0/99"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:e000:0/100"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:f000:0/101"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:f800:0/102"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:fc00:0/103"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:fe00:0/104"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:ff00:0/105"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:ff80:0/106"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:ffc0:0/107"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:ffe0:0/108"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:fff0:0/109"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:fff8:0/110"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:fffc:0/111"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:fffe:0/112"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:ffff:0/113"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:ffff:8000/114"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:ffff:c000/115"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:ffff:e000/116"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:ffff:f000/117"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:ffff:f800/118"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fc00/119"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fe00/120"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00/121"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff80/122"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffc0/123"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffe0/124"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fff0/125"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fff8/126"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffc/127"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+                "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe/128"
+                    .parse::<Ipv6Cidr>()
+                    .unwrap(),
+            ],
+            range.to_cidrs().as_slice()
+        );
+
+        let range =
+            AddressRange::new_v6([0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0]).unwrap();
+
+        assert_eq!(
+            [Ipv6Cidr::new([0, 0, 0, 0, 0, 0, 0, 0], 128).unwrap(),],
+            range.to_cidrs().as_slice()
+        );
+
+        let range = AddressRange::new_v6(
+            [
+                0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
+            ],
+            [
+                0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
+            ],
+        )
+        .unwrap();
+
+        assert_eq!(
+            [Ipv6Cidr::new(
+                [0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF],
+                128
+            )
+            .unwrap(),],
+            range.to_cidrs().as_slice()
+        );
+    }
 }
-- 
2.39.5


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v3 07/24] ipset: address: add helper methods
  2024-11-12 12:25 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v3 00/24] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (5 preceding siblings ...)
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 06/24] iprange: add methods for converting an ip range to cidrs Stefan Hanreich
@ 2024-11-12 12:25 ` Stefan Hanreich
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 08/24] firewall: guest: derive traits according to rust api guidelines Stefan Hanreich
                   ` (16 subsequent siblings)
  23 siblings, 0 replies; 26+ messages in thread
From: Stefan Hanreich @ 2024-11-12 12:25 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/src/firewall/types/address.rs | 10 ++++++++++
 proxmox-ve-config/src/firewall/types/ipset.rs   | 14 ++++++++++++++
 2 files changed, 24 insertions(+)

diff --git a/proxmox-ve-config/src/firewall/types/address.rs b/proxmox-ve-config/src/firewall/types/address.rs
index 95c58a7..938b8e1 100644
--- a/proxmox-ve-config/src/firewall/types/address.rs
+++ b/proxmox-ve-config/src/firewall/types/address.rs
@@ -11,6 +11,16 @@ pub enum Family {
     V6,
 }
 
+impl Family {
+    pub fn is_ipv4(&self) -> bool {
+        *self == Self::V4
+    }
+
+    pub fn is_ipv6(&self) -> bool {
+        *self == Self::V6
+    }
+}
+
 impl fmt::Display for Family {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         match self {
diff --git a/proxmox-ve-config/src/firewall/types/ipset.rs b/proxmox-ve-config/src/firewall/types/ipset.rs
index e8131b5..7511490 100644
--- a/proxmox-ve-config/src/firewall/types/ipset.rs
+++ b/proxmox-ve-config/src/firewall/types/ipset.rs
@@ -132,6 +132,20 @@ pub struct IpsetEntry {
     pub comment: Option<String>,
 }
 
+impl IpsetEntry {
+    pub fn new(
+        address: impl Into<IpsetAddress>,
+        nomatch: bool,
+        comment: impl Into<Option<String>>,
+    ) -> IpsetEntry {
+        IpsetEntry {
+            nomatch,
+            address: address.into(),
+            comment: comment.into(),
+        }
+    }
+}
+
 impl<T: Into<IpsetAddress>> From<T> for IpsetEntry {
     fn from(value: T) -> Self {
         Self {
-- 
2.39.5


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v3 08/24] firewall: guest: derive traits according to rust api guidelines
  2024-11-12 12:25 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v3 00/24] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (6 preceding siblings ...)
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 07/24] ipset: address: add helper methods Stefan Hanreich
@ 2024-11-12 12:25 ` Stefan Hanreich
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 09/24] common: add allowlist Stefan Hanreich
                   ` (15 subsequent siblings)
  23 siblings, 0 replies; 26+ messages in thread
From: Stefan Hanreich @ 2024-11-12 12:25 UTC (permalink / raw)
  To: pve-devel

Almost every type should implement them anyway, and many of them are
required for those types to be used in BTreeMaps, which the nftables
firewall uses for generating stable output.

Additionally, we derive Serialize and Deserialize for a few types that
occur in the sdn configuration. The following patches will use those
for (de-)serialization.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 .../src/firewall/types/address.rs             | 19 +++++++++++--------
 proxmox-ve-config/src/firewall/types/alias.rs |  4 ++--
 proxmox-ve-config/src/firewall/types/ipset.rs |  6 +++---
 proxmox-ve-config/src/guest/types.rs          |  7 ++++---
 proxmox-ve-config/src/guest/vm.rs             |  7 ++++---
 5 files changed, 24 insertions(+), 19 deletions(-)

diff --git a/proxmox-ve-config/src/firewall/types/address.rs b/proxmox-ve-config/src/firewall/types/address.rs
index 938b8e1..57efb13 100644
--- a/proxmox-ve-config/src/firewall/types/address.rs
+++ b/proxmox-ve-config/src/firewall/types/address.rs
@@ -30,8 +30,9 @@ impl fmt::Display for Family {
     }
 }
 
-#[derive(Clone, Copy, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
+#[derive(
+    Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Hash, SerializeDisplay, DeserializeFromStr,
+)]
 pub enum Cidr {
     Ipv4(Ipv4Cidr),
     Ipv6(Ipv6Cidr),
@@ -101,8 +102,7 @@ impl From<Ipv6Cidr> for Cidr {
 
 const IPV4_LENGTH: u8 = 32;
 
-#[derive(Clone, Copy, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
+#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
 pub struct Ipv4Cidr {
     addr: Ipv4Addr,
     mask: u8,
@@ -176,8 +176,7 @@ impl fmt::Display for Ipv4Cidr {
 
 const IPV6_LENGTH: u8 = 128;
 
-#[derive(Clone, Copy, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
+#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
 pub struct Ipv6Cidr {
     addr: Ipv6Addr,
     mask: u8,
@@ -271,7 +270,9 @@ impl Display for IpRangeError {
 /// Represents a range of IPv4 or IPv6 addresses.
 ///
 /// For more information see [`AddressRange`]
-#[derive(Clone, Copy, Debug, PartialEq, Eq, SerializeDisplay, DeserializeFromStr)]
+#[derive(
+    Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, SerializeDisplay, DeserializeFromStr,
+)]
 pub enum IpRange {
     V4(AddressRange<Ipv4Addr>),
     V6(AddressRange<Ipv6Addr>),
@@ -364,7 +365,9 @@ impl fmt::Display for IpRange {
 /// # Textual representation
 ///
 /// Two IP addresses separated by a hyphen, e.g.: `127.0.0.1-127.0.0.255`
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[derive(
+    Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, SerializeDisplay, DeserializeFromStr,
+)]
 pub struct AddressRange<T> {
     start: T,
     last: T,
diff --git a/proxmox-ve-config/src/firewall/types/alias.rs b/proxmox-ve-config/src/firewall/types/alias.rs
index e6aa30d..5dfaa41 100644
--- a/proxmox-ve-config/src/firewall/types/alias.rs
+++ b/proxmox-ve-config/src/firewall/types/alias.rs
@@ -2,7 +2,7 @@ use std::fmt::Display;
 use std::str::FromStr;
 
 use anyhow::{bail, format_err, Error};
-use serde_with::DeserializeFromStr;
+use serde_with::{DeserializeFromStr, SerializeDisplay};
 
 use crate::firewall::parse::{match_name, match_non_whitespace};
 use crate::firewall::types::address::Cidr;
@@ -35,7 +35,7 @@ impl Display for AliasScope {
     }
 }
 
-#[derive(Debug, Clone, DeserializeFromStr)]
+#[derive(Debug, Clone, DeserializeFromStr, SerializeDisplay)]
 #[cfg_attr(test, derive(Eq, PartialEq))]
 pub struct AliasName {
     scope: AliasScope,
diff --git a/proxmox-ve-config/src/firewall/types/ipset.rs b/proxmox-ve-config/src/firewall/types/ipset.rs
index 7511490..fe5a930 100644
--- a/proxmox-ve-config/src/firewall/types/ipset.rs
+++ b/proxmox-ve-config/src/firewall/types/ipset.rs
@@ -88,7 +88,7 @@ impl Display for IpsetName {
     }
 }
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 #[cfg_attr(test, derive(Eq, PartialEq))]
 pub enum IpsetAddress {
     Alias(AliasName),
@@ -124,7 +124,7 @@ impl From<IpRange> for IpsetAddress {
     }
 }
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 #[cfg_attr(test, derive(Eq, PartialEq))]
 pub struct IpsetEntry {
     pub nomatch: bool,
@@ -208,7 +208,7 @@ impl Ipfilter<'_> {
     }
 }
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 #[cfg_attr(test, derive(Eq, PartialEq))]
 pub struct Ipset {
     pub name: IpsetName,
diff --git a/proxmox-ve-config/src/guest/types.rs b/proxmox-ve-config/src/guest/types.rs
index 217c537..ed6a48c 100644
--- a/proxmox-ve-config/src/guest/types.rs
+++ b/proxmox-ve-config/src/guest/types.rs
@@ -2,8 +2,11 @@ use std::fmt;
 use std::str::FromStr;
 
 use anyhow::{format_err, Error};
+use serde_with::{DeserializeFromStr, SerializeDisplay};
 
-#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)]
+#[derive(
+    Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash, SerializeDisplay, DeserializeFromStr,
+)]
 pub struct Vmid(u32);
 
 impl Vmid {
@@ -34,5 +37,3 @@ impl FromStr for Vmid {
         ))
     }
 }
-
-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
index 5b5866a..ed6c66a 100644
--- a/proxmox-ve-config/src/guest/vm.rs
+++ b/proxmox-ve-config/src/guest/vm.rs
@@ -1,4 +1,3 @@
-use anyhow::{bail, Error};
 use core::fmt::Display;
 use std::io;
 use std::str::FromStr;
@@ -6,11 +5,13 @@ use std::{collections::HashMap, net::Ipv6Addr};
 
 use proxmox_schema::property_string::PropertyIterator;
 
+use anyhow::{bail, Error};
+use serde_with::DeserializeFromStr;
+
 use crate::firewall::parse::{match_digits, parse_bool};
 use crate::firewall::types::address::{Ipv4Cidr, Ipv6Cidr};
 
-#[derive(Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
+#[derive(Clone, Debug, DeserializeFromStr, PartialEq, Eq, Hash, PartialOrd, Ord)]
 pub struct MacAddress([u8; 6]);
 
 static LOCAL_PART: [u8; 8] = [0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
-- 
2.39.5


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v3 09/24] common: add allowlist
  2024-11-12 12:25 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v3 00/24] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (7 preceding siblings ...)
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 08/24] firewall: guest: derive traits according to rust api guidelines Stefan Hanreich
@ 2024-11-12 12:25 ` Stefan Hanreich
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 10/24] sdn: add name types Stefan Hanreich
                   ` (14 subsequent siblings)
  23 siblings, 0 replies; 26+ messages in thread
From: Stefan Hanreich @ 2024-11-12 12:25 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/src/common/mod.rs | 31 +++++++++++++++++++++++++++++
 proxmox-ve-config/src/lib.rs        |  1 +
 2 files changed, 32 insertions(+)
 create mode 100644 proxmox-ve-config/src/common/mod.rs

diff --git a/proxmox-ve-config/src/common/mod.rs b/proxmox-ve-config/src/common/mod.rs
new file mode 100644
index 0000000..ef09791
--- /dev/null
+++ b/proxmox-ve-config/src/common/mod.rs
@@ -0,0 +1,31 @@
+use core::hash::Hash;
+use std::cmp::Eq;
+use std::collections::HashSet;
+
+#[derive(Clone, Debug, Default)]
+pub struct Allowlist<T>(HashSet<T>);
+
+impl<T: Hash + Eq> FromIterator<T> for Allowlist<T> {
+    fn from_iter<A>(iter: A) -> Self
+    where
+        A: IntoIterator<Item = T>,
+    {
+        Allowlist(HashSet::from_iter(iter))
+    }
+}
+
+/// returns true if [`value`] is in the allowlist or if allowlist does not exist
+impl<T: Hash + Eq> Allowlist<T> {
+    pub fn is_allowed(&self, value: &T) -> bool {
+        self.0.contains(value)
+    }
+}
+
+impl<T: Hash + Eq> Allowlist<T> {
+    pub fn new<I>(iter: I) -> Self
+    where
+        I: IntoIterator<Item = T>,
+    {
+        Self::from_iter(iter)
+    }
+}
diff --git a/proxmox-ve-config/src/lib.rs b/proxmox-ve-config/src/lib.rs
index 856b14f..1b6feae 100644
--- a/proxmox-ve-config/src/lib.rs
+++ b/proxmox-ve-config/src/lib.rs
@@ -1,3 +1,4 @@
+pub mod common;
 pub mod firewall;
 pub mod guest;
 pub mod host;
-- 
2.39.5


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v3 10/24] sdn: add name types
  2024-11-12 12:25 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v3 00/24] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (8 preceding siblings ...)
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 09/24] common: add allowlist Stefan Hanreich
@ 2024-11-12 12:25 ` Stefan Hanreich
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 11/24] sdn: add ipam module Stefan Hanreich
                   ` (13 subsequent siblings)
  23 siblings, 0 replies; 26+ messages in thread
From: Stefan Hanreich @ 2024-11-12 12:25 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/src/lib.rs     |   1 +
 proxmox-ve-config/src/sdn/mod.rs | 248 +++++++++++++++++++++++++++++++
 2 files changed, 249 insertions(+)
 create mode 100644 proxmox-ve-config/src/sdn/mod.rs

diff --git a/proxmox-ve-config/src/lib.rs b/proxmox-ve-config/src/lib.rs
index 1b6feae..d17136c 100644
--- a/proxmox-ve-config/src/lib.rs
+++ b/proxmox-ve-config/src/lib.rs
@@ -2,3 +2,4 @@ pub mod common;
 pub mod firewall;
 pub mod guest;
 pub mod host;
+pub mod sdn;
diff --git a/proxmox-ve-config/src/sdn/mod.rs b/proxmox-ve-config/src/sdn/mod.rs
new file mode 100644
index 0000000..0752631
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/mod.rs
@@ -0,0 +1,248 @@
+use std::{error::Error, fmt::Display, str::FromStr};
+
+use serde_with::DeserializeFromStr;
+
+use crate::firewall::types::Cidr;
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub enum SdnNameError {
+    Empty,
+    TooLong,
+    InvalidSymbols,
+    InvalidSubnetCidr,
+    InvalidSubnetFormat,
+}
+
+impl Error for SdnNameError {}
+
+impl Display for SdnNameError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str(match self {
+            SdnNameError::TooLong => "name too long",
+            SdnNameError::InvalidSymbols => "invalid symbols in name",
+            SdnNameError::InvalidSubnetCidr => "invalid cidr in name",
+            SdnNameError::InvalidSubnetFormat => "invalid format for subnet name",
+            SdnNameError::Empty => "name is empty",
+        })
+    }
+}
+
+fn validate_sdn_name(name: &str) -> Result<(), SdnNameError> {
+    if name.is_empty() {
+        return Err(SdnNameError::Empty);
+    }
+
+    if name.len() > 8 {
+        return Err(SdnNameError::TooLong);
+    }
+
+    // safe because of empty check
+    if !name.chars().next().unwrap().is_ascii_alphabetic() {
+        return Err(SdnNameError::InvalidSymbols);
+    }
+
+    if !name.chars().all(|c| c.is_ascii_alphanumeric()) {
+        return Err(SdnNameError::InvalidSymbols);
+    }
+
+    Ok(())
+}
+
+/// represents the name of an sdn zone
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, DeserializeFromStr)]
+pub struct ZoneName(String);
+
+impl ZoneName {
+    /// construct a new zone name
+    ///
+    /// # Errors
+    ///
+    /// This function will return an error if the name is empty, too long (>8 characters), starts
+    /// with a non-alphabetic symbol or if there are non alphanumeric symbols contained in the name.
+    pub fn new(name: String) -> Result<Self, SdnNameError> {
+        validate_sdn_name(&name)?;
+        Ok(ZoneName(name))
+    }
+}
+
+impl AsRef<str> for ZoneName {
+    fn as_ref(&self) -> &str {
+        self.0.as_ref()
+    }
+}
+
+impl FromStr for ZoneName {
+    type Err = SdnNameError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Self::new(s.to_owned())
+    }
+}
+
+impl Display for ZoneName {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+/// represents the name of an sdn vnet
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, DeserializeFromStr)]
+pub struct VnetName(String);
+
+impl VnetName {
+    /// construct a new vnet name
+    ///
+    /// # Errors
+    ///
+    /// This function will return an error if the name is empty, too long (>8 characters), starts
+    /// with a non-alphabetic symbol or if there are non alphanumeric symbols contained in the name.
+    pub fn new(name: String) -> Result<Self, SdnNameError> {
+        validate_sdn_name(&name)?;
+        Ok(VnetName(name))
+    }
+
+    pub fn name(&self) -> &str {
+        &self.0
+    }
+}
+
+impl AsRef<str> for VnetName {
+    fn as_ref(&self) -> &str {
+        self.0.as_ref()
+    }
+}
+
+impl FromStr for VnetName {
+    type Err = SdnNameError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Self::new(s.to_owned())
+    }
+}
+
+impl Display for VnetName {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+/// represents the name of an sdn subnet
+///
+/// # Textual representation
+/// A subnet name has the form `{zone_id}-{cidr_ip}-{cidr_mask}`
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, DeserializeFromStr)]
+pub struct SubnetName(ZoneName, Cidr);
+
+impl SubnetName {
+    pub fn new(zone: ZoneName, cidr: Cidr) -> Self {
+        SubnetName(zone, cidr)
+    }
+
+    pub fn zone(&self) -> &ZoneName {
+        &self.0
+    }
+
+    pub fn cidr(&self) -> &Cidr {
+        &self.1
+    }
+}
+
+impl FromStr for SubnetName {
+    type Err = SdnNameError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if let Some((name, cidr_part)) = s.split_once('-') {
+            if let Some((ip, netmask)) = cidr_part.split_once('-') {
+                let zone_name = ZoneName::from_str(name)?;
+
+                let cidr: Cidr = format!("{ip}/{netmask}")
+                    .parse()
+                    .map_err(|_| SdnNameError::InvalidSubnetCidr)?;
+
+                return Ok(Self(zone_name, cidr));
+            }
+        }
+
+        Err(SdnNameError::InvalidSubnetFormat)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_zone_name() {
+        ZoneName::new("zone0".to_string()).unwrap();
+
+        assert_eq!(ZoneName::new("".to_string()), Err(SdnNameError::Empty));
+
+        assert_eq!(
+            ZoneName::new("3qwe".to_string()),
+            Err(SdnNameError::InvalidSymbols)
+        );
+
+        assert_eq!(
+            ZoneName::new("qweqweqwe".to_string()),
+            Err(SdnNameError::TooLong)
+        );
+
+        assert_eq!(
+            ZoneName::new("qß".to_string()),
+            Err(SdnNameError::InvalidSymbols)
+        );
+    }
+
+    #[test]
+    fn test_vnet_name() {
+        VnetName::new("vnet0".to_string()).unwrap();
+
+        assert_eq!(VnetName::new("".to_string()), Err(SdnNameError::Empty));
+
+        assert_eq!(
+            VnetName::new("3qwe".to_string()),
+            Err(SdnNameError::InvalidSymbols)
+        );
+
+        assert_eq!(
+            VnetName::new("qweqweqwe".to_string()),
+            Err(SdnNameError::TooLong)
+        );
+
+        assert_eq!(
+            VnetName::new("qß".to_string()),
+            Err(SdnNameError::InvalidSymbols)
+        );
+    }
+
+    #[test]
+    fn test_subnet_name() {
+        assert_eq!(
+            "qweqweqwe-10.101.0.0-16".parse::<SubnetName>(),
+            Err(SdnNameError::TooLong),
+        );
+
+        assert_eq!(
+            "zone0_10.101.0.0-16".parse::<SubnetName>(),
+            Err(SdnNameError::InvalidSubnetFormat),
+        );
+
+        assert_eq!(
+            "zone0-10.101.0.0_16".parse::<SubnetName>(),
+            Err(SdnNameError::InvalidSubnetFormat),
+        );
+
+        assert_eq!(
+            "zone0-10.101.0.0-33".parse::<SubnetName>(),
+            Err(SdnNameError::InvalidSubnetCidr),
+        );
+
+        assert_eq!(
+            "zone0-10.101.0.0-16".parse::<SubnetName>().unwrap(),
+            SubnetName::new(
+                ZoneName::new("zone0".to_string()).unwrap(),
+                Cidr::new_v4([10, 101, 0, 0], 16).unwrap()
+            )
+        )
+    }
+}
-- 
2.39.5


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v3 11/24] sdn: add ipam module
  2024-11-12 12:25 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v3 00/24] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (9 preceding siblings ...)
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 10/24] sdn: add name types Stefan Hanreich
@ 2024-11-12 12:25 ` Stefan Hanreich
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 12/24] sdn: ipam: add method for generating ipsets Stefan Hanreich
                   ` (12 subsequent siblings)
  23 siblings, 0 replies; 26+ messages in thread
From: Stefan Hanreich @ 2024-11-12 12:25 UTC (permalink / raw)
  To: pve-devel

This module includes structs for representing the JSON schema from the
PVE ipam. Those can be used to parse the current IPAM state.

We also include a general Ipam struct, and provide a method for
converting the PVE IPAM to the general struct. The idea behind this
is that we have multiple IPAM plugins in PVE and will likely add
support for importing them in the future. With the split, we can have
our dedicated structs for representing the different data formats from
the different IPAM plugins and then convert them into a common
representation that can then be used internally, decoupling the
concrete plugin from the code using the IPAM configuration.

Enforcing the invariants the way we currently do adds a bit of runtime
complexity when building the object, but we get the upside of never
being able to construct an invalid struct. For the amount of entries
the ipam usually has, this should be fine. Should it turn out to be
not performant enough we could always add a HashSet for looking up
values and speeding up the validation. For now, I wanted to avoid the
additional complexity.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 .../src/firewall/types/address.rs             |   8 +
 proxmox-ve-config/src/guest/vm.rs             |   4 +
 proxmox-ve-config/src/sdn/ipam.rs             | 330 ++++++++++++++++++
 proxmox-ve-config/src/sdn/mod.rs              |   2 +
 4 files changed, 344 insertions(+)
 create mode 100644 proxmox-ve-config/src/sdn/ipam.rs

diff --git a/proxmox-ve-config/src/firewall/types/address.rs b/proxmox-ve-config/src/firewall/types/address.rs
index 57efb13..3777dc3 100644
--- a/proxmox-ve-config/src/firewall/types/address.rs
+++ b/proxmox-ve-config/src/firewall/types/address.rs
@@ -61,6 +61,14 @@ impl Cidr {
     pub fn is_ipv6(&self) -> bool {
         matches!(self, Cidr::Ipv6(_))
     }
+
+    pub fn contains_address(&self, ip: &IpAddr) -> bool {
+        match (self, ip) {
+            (Cidr::Ipv4(cidr), IpAddr::V4(ip)) => cidr.contains_address(ip),
+            (Cidr::Ipv6(cidr), IpAddr::V6(ip)) => cidr.contains_address(ip),
+            _ => false,
+        }
+    }
 }
 
 impl fmt::Display for Cidr {
diff --git a/proxmox-ve-config/src/guest/vm.rs b/proxmox-ve-config/src/guest/vm.rs
index ed6c66a..3476b93 100644
--- a/proxmox-ve-config/src/guest/vm.rs
+++ b/proxmox-ve-config/src/guest/vm.rs
@@ -18,6 +18,10 @@ static LOCAL_PART: [u8; 8] = [0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
 static EUI64_MIDDLE_PART: [u8; 2] = [0xFF, 0xFE];
 
 impl MacAddress {
+    pub fn new(address: [u8; 6]) -> Self {
+        Self(address)
+    }
+
     /// 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];
diff --git a/proxmox-ve-config/src/sdn/ipam.rs b/proxmox-ve-config/src/sdn/ipam.rs
new file mode 100644
index 0000000..95e3ba7
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/ipam.rs
@@ -0,0 +1,330 @@
+use std::{
+    collections::{BTreeMap, HashMap},
+    error::Error,
+    fmt::Display,
+    net::IpAddr,
+};
+
+use serde::Deserialize;
+
+use crate::{
+    firewall::types::Cidr,
+    guest::{types::Vmid, vm::MacAddress},
+    sdn::{SdnNameError, SubnetName, ZoneName},
+};
+
+/// Struct for deserializing a gateway entry in PVE IPAM.
+///
+/// They are automatically generated by the PVE SDN module when creating a new subnet.
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct IpamJsonDataGateway {
+    #[serde(rename = "gateway")]
+    _gateway: u8,
+}
+
+/// Struct for deserializing a guest entry in PVE IPAM.
+///
+/// They are automatically created when adding a guest to a VNet that has a Subnet with DHCP
+/// configured.
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct IpamJsonDataVm {
+    vmid: Vmid,
+    hostname: Option<String>,
+    mac: MacAddress,
+}
+
+/// Struct for deserializing a custom entry in PVE IPAM.
+///
+/// Custom entries are created manually by the user via the Web UI / API.
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct IpamJsonDataCustom {
+    mac: MacAddress,
+}
+
+/// Enum representing the different kinds of entries that can be located in PVE IPAM.
+///
+/// For more information about the members see the documentation of the respective structs in the
+/// enum.
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
+#[serde(untagged)]
+pub enum IpamJsonData {
+    Vm(IpamJsonDataVm),
+    Gateway(IpamJsonDataGateway),
+    Custom(IpamJsonDataCustom),
+}
+
+/// Struct for deserializing IPs from the PVE IPAM.
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
+pub struct IpJson {
+    ips: BTreeMap<IpAddr, IpamJsonData>,
+}
+
+/// Struct for deserializing subnets from the PVE IPAM.
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
+pub struct SubnetJson {
+    subnets: BTreeMap<Cidr, IpJson>,
+}
+
+/// Struct for deserializing the PVE IPAM.
+///
+/// It is usually located in `/etc/pve/priv/ipam.db`
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
+pub struct IpamJson {
+    zones: BTreeMap<ZoneName, SubnetJson>,
+}
+
+/// Holds the data for the IPAM entry of a VM.
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct IpamDataVm {
+    ip: IpAddr,
+    vmid: Vmid,
+    mac: MacAddress,
+    hostname: Option<String>,
+}
+
+impl IpamDataVm {
+    pub fn new(
+        ip: impl Into<IpAddr>,
+        vmid: impl Into<Vmid>,
+        mac: MacAddress,
+        hostname: impl Into<Option<String>>,
+    ) -> Self {
+        Self {
+            ip: ip.into(),
+            vmid: vmid.into(),
+            mac,
+            hostname: hostname.into(),
+        }
+    }
+
+    pub fn from_json_data(ip: IpAddr, data: IpamJsonDataVm) -> Self {
+        Self::new(ip, data.vmid, data.mac, data.hostname)
+    }
+
+    pub fn ip(&self) -> &IpAddr {
+        &self.ip
+    }
+
+    pub fn vmid(&self) -> Vmid {
+        self.vmid
+    }
+
+    pub fn mac(&self) -> &MacAddress {
+        &self.mac
+    }
+
+    pub fn hostname(&self) -> Option<&str> {
+        self.hostname.as_deref()
+    }
+}
+
+/// Holds the data for the IPAM entry of a Gateway.
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct IpamDataGateway {
+    ip: IpAddr,
+}
+
+impl IpamDataGateway {
+    pub fn new(ip: IpAddr) -> Self {
+        Self { ip }
+    }
+
+    fn from_json_data(ip: IpAddr, _json_data: IpamJsonDataGateway) -> Self {
+        Self::new(ip)
+    }
+
+    pub fn ip(&self) -> &IpAddr {
+        &self.ip
+    }
+}
+
+/// Holds the data for a custom IPAM entry.
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct IpamDataCustom {
+    ip: IpAddr,
+    mac: MacAddress,
+}
+
+impl IpamDataCustom {
+    pub fn new(ip: IpAddr, mac: MacAddress) -> Self {
+        Self { ip, mac }
+    }
+
+    fn from_json_data(ip: IpAddr, json_data: IpamJsonDataCustom) -> Self {
+        Self::new(ip, json_data.mac)
+    }
+
+    pub fn ip(&self) -> &IpAddr {
+        &self.ip
+    }
+
+    pub fn mac(&self) -> &MacAddress {
+        &self.mac
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub enum IpamData {
+    Vm(IpamDataVm),
+    Gateway(IpamDataGateway),
+    Custom(IpamDataCustom),
+}
+
+impl IpamData {
+    pub fn from_json_data(ip: IpAddr, json_data: IpamJsonData) -> Self {
+        match json_data {
+            IpamJsonData::Vm(json_data) => IpamDataVm::from_json_data(ip, json_data).into(),
+            IpamJsonData::Gateway(json_data) => {
+                IpamDataGateway::from_json_data(ip, json_data).into()
+            }
+            IpamJsonData::Custom(json_data) => IpamDataCustom::from_json_data(ip, json_data).into(),
+        }
+    }
+
+    pub fn ip_address(&self) -> &IpAddr {
+        match &self {
+            IpamData::Vm(data) => data.ip(),
+            IpamData::Gateway(data) => data.ip(),
+            IpamData::Custom(data) => data.ip(),
+        }
+    }
+}
+
+impl From<IpamDataVm> for IpamData {
+    fn from(value: IpamDataVm) -> Self {
+        IpamData::Vm(value)
+    }
+}
+
+impl From<IpamDataGateway> for IpamData {
+    fn from(value: IpamDataGateway) -> Self {
+        IpamData::Gateway(value)
+    }
+}
+
+impl From<IpamDataCustom> for IpamData {
+    fn from(value: IpamDataCustom) -> Self {
+        IpamData::Custom(value)
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub enum IpamError {
+    NameError(SdnNameError),
+    InvalidIpAddress,
+    DuplicateIpAddress,
+    IpAddressOutOfBounds,
+}
+
+impl Error for IpamError {}
+
+impl Display for IpamError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str("")
+    }
+}
+
+/// represents an entry in the PVE IPAM database
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct IpamEntry {
+    subnet: SubnetName,
+    data: IpamData,
+}
+
+impl IpamEntry {
+    /// Creates a new PVE IPAM entry.
+    ///
+    /// # Errors
+    ///
+    /// This function will return an error if the IP address of the entry does not match the CIDR
+    /// of the subnet.
+    pub fn new(subnet: SubnetName, data: IpamData) -> Result<Self, IpamError> {
+        if !subnet.cidr().contains_address(data.ip_address()) {
+            return Err(IpamError::IpAddressOutOfBounds);
+        }
+
+        Ok(IpamEntry { subnet, data })
+    }
+
+    pub fn subnet(&self) -> &SubnetName {
+        &self.subnet
+    }
+
+    pub fn data(&self) -> &IpamData {
+        &self.data
+    }
+
+    pub fn ip_address(&self) -> &IpAddr {
+        self.data.ip_address()
+    }
+}
+
+/// Common representation of IPAM data used in SDN.
+///
+/// This should be instantiated by reading from one of the concrete IPAM implementations and then
+/// converting into this common struct.
+///
+/// # Invariants
+/// * No IP address in a Subnet is allocated twice
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
+pub struct Ipam {
+    entries: BTreeMap<SubnetName, Vec<IpamEntry>>,
+}
+
+impl Ipam {
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    pub fn from_entries(entries: impl IntoIterator<Item = IpamEntry>) -> Result<Self, IpamError> {
+        let mut ipam = Self::new();
+
+        for entry in entries {
+            ipam.add_entry(entry)?;
+        }
+
+        Ok(ipam)
+    }
+
+    /// Adds a new [`IpamEntry`] to the database.
+    ///
+    /// # Errors
+    ///
+    /// This function will return an error if the IP is already allocated by another guest.
+    pub fn add_entry(&mut self, entry: IpamEntry) -> Result<(), IpamError> {
+        if let Some(entries) = self.entries.get_mut(entry.subnet()) {
+            for ipam_entry in &*entries {
+                if ipam_entry.ip_address() == entry.ip_address() {
+                    return Err(IpamError::DuplicateIpAddress);
+                }
+            }
+
+            entries.push(entry);
+        } else {
+            self.entries
+                .insert(entry.subnet().clone(), [entry].to_vec());
+        }
+
+        Ok(())
+    }
+}
+
+impl TryFrom<IpamJson> for Ipam {
+    type Error = IpamError;
+
+    fn try_from(value: IpamJson) -> Result<Self, Self::Error> {
+        let mut ipam = Ipam::default();
+
+        for (zone_name, subnet_json) in value.zones {
+            for (cidr, ip_json) in subnet_json.subnets {
+                for (ip, json_data) in ip_json.ips {
+                    let data = IpamData::from_json_data(ip, json_data);
+                    let subnet = SubnetName::new(zone_name.clone(), cidr);
+                    ipam.add_entry(IpamEntry::new(subnet, data)?)?;
+                }
+            }
+        }
+
+        Ok(ipam)
+    }
+}
diff --git a/proxmox-ve-config/src/sdn/mod.rs b/proxmox-ve-config/src/sdn/mod.rs
index 0752631..0ea874f 100644
--- a/proxmox-ve-config/src/sdn/mod.rs
+++ b/proxmox-ve-config/src/sdn/mod.rs
@@ -1,3 +1,5 @@
+pub mod ipam;
+
 use std::{error::Error, fmt::Display, str::FromStr};
 
 use serde_with::DeserializeFromStr;
-- 
2.39.5


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v3 12/24] sdn: ipam: add method for generating ipsets
  2024-11-12 12:25 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v3 00/24] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (10 preceding siblings ...)
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 11/24] sdn: add ipam module Stefan Hanreich
@ 2024-11-12 12:25 ` Stefan Hanreich
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 13/24] sdn: add config module Stefan Hanreich
                   ` (11 subsequent siblings)
  23 siblings, 0 replies; 26+ messages in thread
From: Stefan Hanreich @ 2024-11-12 12:25 UTC (permalink / raw)
  To: pve-devel

For every guest that has at least one entry in the IPAM we generate an
ipset with the name `+sdn/guest-ipam-{vmid}`. The ipset contains all
IPs from all zones for a guest with {vmid}.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 .../src/firewall/types/address.rs             |  9 +++++
 proxmox-ve-config/src/sdn/ipam.rs             | 40 ++++++++++++++++++-
 2 files changed, 48 insertions(+), 1 deletion(-)

diff --git a/proxmox-ve-config/src/firewall/types/address.rs b/proxmox-ve-config/src/firewall/types/address.rs
index 3777dc3..9b73d3d 100644
--- a/proxmox-ve-config/src/firewall/types/address.rs
+++ b/proxmox-ve-config/src/firewall/types/address.rs
@@ -108,6 +108,15 @@ impl From<Ipv6Cidr> for Cidr {
     }
 }
 
+impl From<IpAddr> for Cidr {
+    fn from(value: IpAddr) -> Self {
+        match value {
+            IpAddr::V4(addr) => Ipv4Cidr::from(addr).into(),
+            IpAddr::V6(addr) => Ipv6Cidr::from(addr).into(),
+        }
+    }
+}
+
 const IPV4_LENGTH: u8 = 32;
 
 #[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
diff --git a/proxmox-ve-config/src/sdn/ipam.rs b/proxmox-ve-config/src/sdn/ipam.rs
index 95e3ba7..598b835 100644
--- a/proxmox-ve-config/src/sdn/ipam.rs
+++ b/proxmox-ve-config/src/sdn/ipam.rs
@@ -8,7 +8,11 @@ use std::{
 use serde::Deserialize;
 
 use crate::{
-    firewall::types::Cidr,
+    common::Allowlist,
+    firewall::types::{
+        ipset::{IpsetEntry, IpsetScope},
+        Cidr, Ipset,
+    },
     guest::{types::Vmid, vm::MacAddress},
     sdn::{SdnNameError, SubnetName, ZoneName},
 };
@@ -309,6 +313,40 @@ impl Ipam {
     }
 }
 
+impl Ipam {
+    /// Generates an [`Ipset`] for all guests with at least one entry in the IPAM.
+    ///
+    /// # Arguments
+    /// * `filter` - A [`Allowlist<Vmid>`] for which IPsets should get returned
+    ///
+    /// It contains all IPs in all VNets, that a guest has stored in IPAM.
+    /// Ipset name is of the form `guest-ipam-<vmid>`
+    pub fn ipsets(&self, filter: Option<&Allowlist<Vmid>>) -> impl Iterator<Item = Ipset> + '_ {
+        self.entries
+            .iter()
+            .flat_map(|(_, entries)| entries.iter())
+            .filter_map(|entry| {
+                if let IpamData::Vm(data) = &entry.data() {
+                    if filter.is_none_or(|list| list.is_allowed(&data.vmid)) {
+                        return Some(data);
+                    }
+                }
+
+                None
+            })
+            .fold(HashMap::<Vmid, Ipset>::new(), |mut acc, entry| {
+                acc.entry(entry.vmid)
+                    .or_insert_with(|| {
+                        Ipset::from_parts(IpsetScope::Sdn, format!("guest-ipam-{}", entry.vmid))
+                    })
+                    .push(IpsetEntry::from(entry.ip));
+
+                acc
+            })
+            .into_values()
+    }
+}
+
 impl TryFrom<IpamJson> for Ipam {
     type Error = IpamError;
 
-- 
2.39.5


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v3 13/24] sdn: add config module
  2024-11-12 12:25 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v3 00/24] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (11 preceding siblings ...)
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 12/24] sdn: ipam: add method for generating ipsets Stefan Hanreich
@ 2024-11-12 12:25 ` Stefan Hanreich
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 14/24] sdn: config: add method for generating ipsets Stefan Hanreich
                   ` (10 subsequent siblings)
  23 siblings, 0 replies; 26+ messages in thread
From: Stefan Hanreich @ 2024-11-12 12:25 UTC (permalink / raw)
  To: pve-devel

Similar to how the IPAM module works, we separate the internal
representation from the concrete schema of the configuration file.

We provide structs for parsing the running SDN configuration and a
struct that is used internally for representing an SDN configuration,
as well as a method for converting the running configuration to the
internal representation.

This is necessary because there are two possible sources for the SDN
configuration: The running configuration as well as the SectionConfig
that contains possible changes from the UI, that have not yet been
applied.

Simlarly to the IPAM, enforcing the invariants the way we currently do
adds some runtime complexity when building the object, but we get the
upside of never being able to construct an invalid struct. For the
amount of entries the sdn config usually has, this should be fine.
Should it turn out to be not performant enough we could always add a
HashSet for looking up values and speeding up the validation. For now,
I wanted to avoid the additional complexity.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/src/sdn/config.rs | 570 ++++++++++++++++++++++++++++
 proxmox-ve-config/src/sdn/mod.rs    |   1 +
 2 files changed, 571 insertions(+)
 create mode 100644 proxmox-ve-config/src/sdn/config.rs

diff --git a/proxmox-ve-config/src/sdn/config.rs b/proxmox-ve-config/src/sdn/config.rs
new file mode 100644
index 0000000..b71084b
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/config.rs
@@ -0,0 +1,570 @@
+use std::{
+    collections::{BTreeMap, HashMap},
+    error::Error,
+    fmt::Display,
+    net::IpAddr,
+    str::FromStr,
+};
+
+use proxmox_schema::{property_string::PropertyString, ApiType, ObjectSchema, StringSchema};
+
+use serde::Deserialize;
+use serde_with::{DeserializeFromStr, SerializeDisplay};
+
+use crate::{
+    common::Allowlist,
+    firewall::types::{
+        address::{IpRange, IpRangeError},
+        ipset::{IpsetEntry, IpsetName, IpsetScope},
+        Cidr, Ipset,
+    },
+    sdn::{SdnNameError, SubnetName, VnetName, ZoneName},
+};
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub enum SdnConfigError {
+    InvalidZoneType,
+    InvalidDhcpType,
+    ZoneNotFound,
+    VnetNotFound,
+    MismatchedCidrGateway,
+    MismatchedSubnetZone,
+    NameError(SdnNameError),
+    InvalidDhcpRange(IpRangeError),
+    DuplicateVnetName,
+}
+
+impl Error for SdnConfigError {
+    fn source(&self) -> Option<&(dyn Error + 'static)> {
+        match self {
+            SdnConfigError::NameError(e) => Some(e),
+            SdnConfigError::InvalidDhcpRange(e) => Some(e),
+            _ => None,
+        }
+    }
+}
+
+impl Display for SdnConfigError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            SdnConfigError::NameError(err) => write!(f, "invalid name: {err}"),
+            SdnConfigError::InvalidDhcpRange(err) => write!(f, "invalid dhcp range: {err}"),
+            SdnConfigError::ZoneNotFound => write!(f, "zone not found"),
+            SdnConfigError::VnetNotFound => write!(f, "vnet not found"),
+            SdnConfigError::MismatchedCidrGateway => {
+                write!(f, "mismatched ip address family for gateway and CIDR")
+            }
+            SdnConfigError::InvalidZoneType => write!(f, "invalid zone type"),
+            SdnConfigError::InvalidDhcpType => write!(f, "invalid dhcp type"),
+            SdnConfigError::DuplicateVnetName => write!(f, "vnet name occurs in multiple zones"),
+            SdnConfigError::MismatchedSubnetZone => {
+                write!(f, "subnet zone does not match actual zone")
+            }
+        }
+    }
+}
+
+#[derive(
+    Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, SerializeDisplay, DeserializeFromStr,
+)]
+pub enum ZoneType {
+    Simple,
+    Vlan,
+    Qinq,
+    Vxlan,
+    Evpn,
+}
+
+impl FromStr for ZoneType {
+    type Err = SdnConfigError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        match s {
+            "simple" => Ok(ZoneType::Simple),
+            "vlan" => Ok(ZoneType::Vlan),
+            "qinq" => Ok(ZoneType::Qinq),
+            "vxlan" => Ok(ZoneType::Vxlan),
+            "evpn" => Ok(ZoneType::Evpn),
+            _ => Err(SdnConfigError::InvalidZoneType),
+        }
+    }
+}
+
+impl Display for ZoneType {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str(match self {
+            ZoneType::Simple => "simple",
+            ZoneType::Vlan => "vlan",
+            ZoneType::Qinq => "qinq",
+            ZoneType::Vxlan => "vxlan",
+            ZoneType::Evpn => "evpn",
+        })
+    }
+}
+
+#[derive(
+    Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, SerializeDisplay, DeserializeFromStr,
+)]
+pub enum DhcpType {
+    Dnsmasq,
+}
+
+impl FromStr for DhcpType {
+    type Err = SdnConfigError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        match s {
+            "dnsmasq" => Ok(DhcpType::Dnsmasq),
+            _ => Err(SdnConfigError::InvalidDhcpType),
+        }
+    }
+}
+
+impl Display for DhcpType {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str(match self {
+            DhcpType::Dnsmasq => "dnsmasq",
+        })
+    }
+}
+
+/// Struct for deserializing a zone entry of the SDN running config
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct ZoneRunningConfig {
+    #[serde(rename = "type")]
+    ty: ZoneType,
+    dhcp: Option<DhcpType>,
+}
+
+/// Struct for deserializing the zones of the SDN running config
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Default)]
+pub struct ZonesRunningConfig {
+    ids: HashMap<ZoneName, ZoneRunningConfig>,
+}
+
+/// Represents the dhcp-range property string used in the SDN configuration
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct DhcpRange {
+    #[serde(rename = "start-address")]
+    start: IpAddr,
+    #[serde(rename = "end-address")]
+    end: IpAddr,
+}
+
+impl ApiType for DhcpRange {
+    const API_SCHEMA: proxmox_schema::Schema = ObjectSchema::new(
+        "DHCP range",
+        &[
+            (
+                "end-address",
+                false,
+                &StringSchema::new("end address of DHCP range").schema(),
+            ),
+            (
+                "start-address",
+                false,
+                &StringSchema::new("start address of DHCP range").schema(),
+            ),
+        ],
+    )
+    .schema();
+}
+
+impl TryFrom<DhcpRange> for IpRange {
+    type Error = IpRangeError;
+
+    fn try_from(value: DhcpRange) -> Result<Self, Self::Error> {
+        IpRange::new(value.start, value.end)
+    }
+}
+
+/// Struct for deserializing a subnet entry of the SDN running config
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct SubnetRunningConfig {
+    vnet: VnetName,
+    gateway: Option<IpAddr>,
+    snat: Option<u8>,
+    #[serde(rename = "dhcp-range")]
+    dhcp_range: Option<Vec<PropertyString<DhcpRange>>>,
+}
+
+/// Struct for deserializing the subnets of the SDN running config
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Default)]
+pub struct SubnetsRunningConfig {
+    ids: HashMap<SubnetName, SubnetRunningConfig>,
+}
+
+/// Struct for deserializing a vnet entry of the SDN running config
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct VnetRunningConfig {
+    zone: ZoneName,
+}
+
+/// struct for deserializing the vnets of the SDN running config
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Default)]
+pub struct VnetsRunningConfig {
+    ids: HashMap<VnetName, VnetRunningConfig>,
+}
+
+/// Struct for deserializing the SDN running config
+///
+/// usually taken from the content of /etc/pve/sdn/.running-config
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Default)]
+pub struct RunningConfig {
+    zones: Option<ZonesRunningConfig>,
+    subnets: Option<SubnetsRunningConfig>,
+    vnets: Option<VnetsRunningConfig>,
+}
+
+/// A struct containing the configuration for an SDN subnet
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct SubnetConfig {
+    name: SubnetName,
+    gateway: Option<IpAddr>,
+    snat: bool,
+    dhcp_range: Vec<IpRange>,
+}
+
+impl SubnetConfig {
+    pub fn new(
+        name: SubnetName,
+        gateway: impl Into<Option<IpAddr>>,
+        snat: bool,
+        dhcp_range: impl IntoIterator<Item = IpRange>,
+    ) -> Result<Self, SdnConfigError> {
+        let gateway = gateway.into();
+
+        if let Some(gateway) = gateway {
+            if !(gateway.is_ipv4() && name.cidr().is_ipv4()
+                || gateway.is_ipv6() && name.cidr().is_ipv6())
+            {
+                return Err(SdnConfigError::MismatchedCidrGateway);
+            }
+        }
+
+        Ok(Self {
+            name,
+            gateway,
+            snat,
+            dhcp_range: dhcp_range.into_iter().collect(),
+        })
+    }
+
+    pub fn try_from_running_config(
+        name: SubnetName,
+        running_config: SubnetRunningConfig,
+    ) -> Result<Self, SdnConfigError> {
+        let snat = running_config
+            .snat
+            .map(|snat| snat != 0)
+            .unwrap_or_else(|| false);
+
+        let dhcp_range: Vec<IpRange> = match running_config.dhcp_range {
+            Some(dhcp_range) => dhcp_range
+                .into_iter()
+                .map(PropertyString::into_inner)
+                .map(IpRange::try_from)
+                .collect::<Result<Vec<IpRange>, IpRangeError>>()
+                .map_err(SdnConfigError::InvalidDhcpRange)?,
+            None => Vec::new(),
+        };
+
+        Self::new(name, running_config.gateway, snat, dhcp_range)
+    }
+
+    pub fn name(&self) -> &SubnetName {
+        &self.name
+    }
+
+    pub fn gateway(&self) -> Option<&IpAddr> {
+        self.gateway.as_ref()
+    }
+
+    pub fn snat(&self) -> bool {
+        self.snat
+    }
+
+    pub fn cidr(&self) -> &Cidr {
+        self.name.cidr()
+    }
+
+    pub fn dhcp_ranges(&self) -> impl Iterator<Item = &IpRange> + '_ {
+        self.dhcp_range.iter()
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct VnetConfig {
+    name: VnetName,
+    subnets: BTreeMap<Cidr, SubnetConfig>,
+}
+
+impl VnetConfig {
+    pub fn new(name: VnetName) -> Self {
+        Self {
+            name,
+            subnets: BTreeMap::default(),
+        }
+    }
+
+    pub fn from_subnets(
+        name: VnetName,
+        subnets: impl IntoIterator<Item = SubnetConfig>,
+    ) -> Result<Self, SdnConfigError> {
+        let mut config = Self::new(name);
+        config.add_subnets(subnets)?;
+        Ok(config)
+    }
+
+    pub fn add_subnets(
+        &mut self,
+        subnets: impl IntoIterator<Item = SubnetConfig>,
+    ) -> Result<(), SdnConfigError> {
+        self.subnets
+            .extend(subnets.into_iter().map(|subnet| (*subnet.cidr(), subnet)));
+        Ok(())
+    }
+
+    pub fn add_subnet(
+        &mut self,
+        subnet: SubnetConfig,
+    ) -> Result<Option<SubnetConfig>, SdnConfigError> {
+        Ok(self.subnets.insert(*subnet.cidr(), subnet))
+    }
+
+    pub fn subnets(&self) -> impl Iterator<Item = &SubnetConfig> + '_ {
+        self.subnets.values()
+    }
+
+    pub fn subnet(&self, cidr: &Cidr) -> Option<&SubnetConfig> {
+        self.subnets.get(cidr)
+    }
+
+    pub fn name(&self) -> &VnetName {
+        &self.name
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct ZoneConfig {
+    name: ZoneName,
+    ty: ZoneType,
+    vnets: BTreeMap<VnetName, VnetConfig>,
+}
+
+impl ZoneConfig {
+    pub fn new(name: ZoneName, ty: ZoneType) -> Self {
+        Self {
+            name,
+            ty,
+            vnets: BTreeMap::default(),
+        }
+    }
+
+    pub fn from_vnets(
+        name: ZoneName,
+        ty: ZoneType,
+        vnets: impl IntoIterator<Item = VnetConfig>,
+    ) -> Result<Self, SdnConfigError> {
+        let mut config = Self::new(name, ty);
+        config.add_vnets(vnets)?;
+        Ok(config)
+    }
+
+    pub fn add_vnets(
+        &mut self,
+        vnets: impl IntoIterator<Item = VnetConfig>,
+    ) -> Result<(), SdnConfigError> {
+        self.vnets
+            .extend(vnets.into_iter().map(|vnet| (vnet.name.clone(), vnet)));
+
+        Ok(())
+    }
+
+    pub fn add_vnet(&mut self, vnet: VnetConfig) -> Result<Option<VnetConfig>, SdnConfigError> {
+        Ok(self.vnets.insert(vnet.name.clone(), vnet))
+    }
+
+    pub fn vnets(&self) -> impl Iterator<Item = &VnetConfig> + '_ {
+        self.vnets.values()
+    }
+
+    pub fn vnet(&self, name: &VnetName) -> Option<&VnetConfig> {
+        self.vnets.get(name)
+    }
+
+    pub fn vnet_mut(&mut self, name: &VnetName) -> Option<&mut VnetConfig> {
+        self.vnets.get_mut(name)
+    }
+
+    pub fn name(&self) -> &ZoneName {
+        &self.name
+    }
+
+    pub fn ty(&self) -> ZoneType {
+        self.ty
+    }
+}
+
+/// Representation of a Proxmox VE SDN configuration
+///
+/// This struct should not be instantiated directly but rather through reading the configuration
+/// from a concrete config struct (e.g [`RunningConfig`]) and then converting into this common
+/// representation.
+///
+/// # Invariants
+/// * Every Vnet name is unique, even if they are in different zones
+/// * Subnets can only be added to a zone if their name contains the same zone they are added to
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
+pub struct SdnConfig {
+    zones: BTreeMap<ZoneName, ZoneConfig>,
+}
+
+impl SdnConfig {
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    pub fn from_zones(zones: impl IntoIterator<Item = ZoneConfig>) -> Result<Self, SdnConfigError> {
+        let mut config = Self::default();
+        config.add_zones(zones)?;
+        Ok(config)
+    }
+
+    /// adds a collection of zones to the configuration, overwriting existing zones with the same
+    /// name
+    pub fn add_zones(
+        &mut self,
+        zones: impl IntoIterator<Item = ZoneConfig>,
+    ) -> Result<(), SdnConfigError> {
+        for zone in zones {
+            self.add_zone(zone)?;
+        }
+
+        Ok(())
+    }
+
+    /// adds a zone to the configuration, returning the old zone config if the zone already existed
+    pub fn add_zone(&mut self, mut zone: ZoneConfig) -> Result<Option<ZoneConfig>, SdnConfigError> {
+        let vnets = std::mem::take(&mut zone.vnets);
+
+        let zone_name = zone.name().clone();
+        let old_zone = self.zones.insert(zone_name.clone(), zone);
+
+        for vnet in vnets.into_values() {
+            self.add_vnet(&zone_name, vnet)?;
+        }
+
+        Ok(old_zone)
+    }
+
+    pub fn add_vnet(
+        &mut self,
+        zone_name: &ZoneName,
+        mut vnet: VnetConfig,
+    ) -> Result<Option<VnetConfig>, SdnConfigError> {
+        for zone in self.zones.values() {
+            if zone.name() != zone_name && zone.vnets.contains_key(vnet.name()) {
+                return Err(SdnConfigError::DuplicateVnetName);
+            }
+        }
+
+        if let Some(zone) = self.zones.get_mut(zone_name) {
+            let subnets = std::mem::take(&mut vnet.subnets);
+
+            let vnet_name = vnet.name().clone();
+            let old_vnet = zone.vnets.insert(vnet_name.clone(), vnet);
+
+            for subnet in subnets.into_values() {
+                self.add_subnet(zone_name, &vnet_name, subnet)?;
+            }
+
+            return Ok(old_vnet);
+        }
+
+        Err(SdnConfigError::ZoneNotFound)
+    }
+
+    pub fn add_subnet(
+        &mut self,
+        zone_name: &ZoneName,
+        vnet_name: &VnetName,
+        subnet: SubnetConfig,
+    ) -> Result<Option<SubnetConfig>, SdnConfigError> {
+        if zone_name != subnet.name().zone() {
+            return Err(SdnConfigError::MismatchedSubnetZone);
+        }
+
+        if let Some(zone) = self.zones.get_mut(zone_name) {
+            if let Some(vnet) = zone.vnets.get_mut(vnet_name) {
+                return Ok(vnet.subnets.insert(*subnet.name().cidr(), subnet));
+            } else {
+                return Err(SdnConfigError::VnetNotFound);
+            }
+        }
+
+        Err(SdnConfigError::ZoneNotFound)
+    }
+
+    pub fn zone(&self, name: &ZoneName) -> Option<&ZoneConfig> {
+        self.zones.get(name)
+    }
+
+    pub fn zones(&self) -> impl Iterator<Item = &ZoneConfig> + '_ {
+        self.zones.values()
+    }
+
+    pub fn vnet(&self, name: &VnetName) -> Option<(&ZoneConfig, &VnetConfig)> {
+        // we can do this because we enforce the invariant that every VNet name must be unique!
+        for zone in self.zones.values() {
+            if let Some(vnet) = zone.vnet(name) {
+                return Some((zone, vnet));
+            }
+        }
+
+        None
+    }
+
+    pub fn vnets(&self) -> impl Iterator<Item = (&ZoneConfig, &VnetConfig)> + '_ {
+        self.zones()
+            .flat_map(|zone| zone.vnets().map(move |vnet| (zone, vnet)))
+    }
+}
+
+impl TryFrom<RunningConfig> for SdnConfig {
+    type Error = SdnConfigError;
+
+    fn try_from(mut value: RunningConfig) -> Result<Self, Self::Error> {
+        let mut config = SdnConfig::default();
+
+        if let Some(running_zones) = value.zones.take() {
+            config.add_zones(
+                running_zones
+                    .ids
+                    .into_iter()
+                    .map(|(name, running_config)| ZoneConfig::new(name, running_config.ty)),
+            )?;
+        }
+
+        if let Some(running_vnets) = value.vnets.take() {
+            for (name, running_config) in running_vnets.ids {
+                config.add_vnet(&running_config.zone, VnetConfig::new(name))?;
+            }
+        }
+
+        if let Some(running_subnets) = value.subnets.take() {
+            for (name, running_config) in running_subnets.ids {
+                let zone_name = name.zone().clone();
+                let vnet_name = running_config.vnet.clone();
+
+                config.add_subnet(
+                    &zone_name,
+                    &vnet_name,
+                    SubnetConfig::try_from_running_config(name, running_config)?,
+                )?;
+            }
+        }
+
+        Ok(config)
+    }
+}
diff --git a/proxmox-ve-config/src/sdn/mod.rs b/proxmox-ve-config/src/sdn/mod.rs
index 0ea874f..c8dc724 100644
--- a/proxmox-ve-config/src/sdn/mod.rs
+++ b/proxmox-ve-config/src/sdn/mod.rs
@@ -1,3 +1,4 @@
+pub mod config;
 pub mod ipam;
 
 use std::{error::Error, fmt::Display, str::FromStr};
-- 
2.39.5


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v3 14/24] sdn: config: add method for generating ipsets
  2024-11-12 12:25 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v3 00/24] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (12 preceding siblings ...)
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 13/24] sdn: add config module Stefan Hanreich
@ 2024-11-12 12:25 ` Stefan Hanreich
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 15/24] tests: add sdn config tests Stefan Hanreich
                   ` (9 subsequent siblings)
  23 siblings, 0 replies; 26+ messages in thread
From: Stefan Hanreich @ 2024-11-12 12:25 UTC (permalink / raw)
  To: pve-devel

We generate the following ipsets for every vnet in the running sdn
configuration:

* {vnet}-all: contains all subnets of the vnet
* {vnet}-no-gateway: contains all subnets of the vnet except for all
  gateways
* {vnet}-gateway: contains all gateways in the vnet
* {vnet}-dhcp: contains all dhcp ranges configured in the vnet

All of them are in the new SDN scope, so the fully qualified name
would look something like this: `+sdn/{vnet-all}`.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/src/sdn/config.rs | 70 +++++++++++++++++++++++++++++
 1 file changed, 70 insertions(+)

diff --git a/proxmox-ve-config/src/sdn/config.rs b/proxmox-ve-config/src/sdn/config.rs
index b71084b..7ee1101 100644
--- a/proxmox-ve-config/src/sdn/config.rs
+++ b/proxmox-ve-config/src/sdn/config.rs
@@ -529,6 +529,76 @@ impl SdnConfig {
         self.zones()
             .flat_map(|zone| zone.vnets().map(move |vnet| (zone, vnet)))
     }
+
+    /// Generates multiple [`Ipset`] for all SDN VNets.
+    ///
+    /// # Arguments
+    /// * `filter` - A [`Allowlist`] of VNet names for which IPsets should get returned
+    ///
+    /// It generates the following [`Ipset`] for all VNets in the config:
+    /// * all: Contains all CIDRs of all subnets in the VNet
+    /// * gateway: Contains all gateways of all subnets in the VNet (if any gateway exists)
+    /// * no-gateway: Matches all CIDRs of all subnets, except for the gateways (if any gateway
+    ///   exists)
+    /// * dhcp: Contains all DHCP ranges of all subnets in the VNet (if any dhcp range exists)
+    pub fn ipsets<'a>(
+        &'a self,
+        filter: Option<&'a Allowlist<VnetName>>,
+    ) -> impl Iterator<Item = Ipset> + '_ {
+        self.zones
+            .values()
+            .flat_map(|zone| zone.vnets())
+            .filter(move |vnet| {
+                filter
+                    .map(|list| list.is_allowed(&vnet.name))
+                    .unwrap_or(true)
+            })
+            .flat_map(|vnet| {
+                let mut ipset_all = Ipset::new(IpsetName::new(
+                    IpsetScope::Sdn,
+                    format!("{}-all", vnet.name),
+                ));
+                ipset_all.comment = Some(format!("All subnets of VNet {}", vnet.name));
+
+                let mut ipset_gateway = Ipset::new(IpsetName::new(
+                    IpsetScope::Sdn,
+                    format!("{}-gateway", vnet.name),
+                ));
+                ipset_gateway.comment = Some(format!("All gateways of VNet {}", vnet.name));
+
+                let mut ipset_all_wo_gateway = Ipset::new(IpsetName::new(
+                    IpsetScope::Sdn,
+                    format!("{}-no-gateway", vnet.name),
+                ));
+                ipset_all_wo_gateway.comment = Some(format!(
+                    "All subnets of VNet {}, excluding gateways",
+                    vnet.name
+                ));
+
+                let mut ipset_dhcp = Ipset::new(IpsetName::new(
+                    IpsetScope::Sdn,
+                    format!("{}-dhcp", vnet.name),
+                ));
+                ipset_dhcp.comment = Some(format!("DHCP ranges of VNet {}", vnet.name));
+
+                for subnet in vnet.subnets.values() {
+                    ipset_all.push((*subnet.cidr()).into());
+
+                    ipset_all_wo_gateway.push((*subnet.cidr()).into());
+
+                    if let Some(gateway) = subnet.gateway {
+                        let gateway_nomatch = IpsetEntry::new(gateway, true, None);
+                        ipset_all_wo_gateway.push(gateway_nomatch);
+
+                        ipset_gateway.push(gateway.into());
+                    }
+
+                    ipset_dhcp.extend(subnet.dhcp_range.iter().cloned().map(IpsetEntry::from));
+                }
+
+                [ipset_all, ipset_gateway, ipset_all_wo_gateway, ipset_dhcp]
+            })
+    }
 }
 
 impl TryFrom<RunningConfig> for SdnConfig {
-- 
2.39.5


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v3 15/24] tests: add sdn config tests
  2024-11-12 12:25 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v3 00/24] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (13 preceding siblings ...)
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 14/24] sdn: config: add method for generating ipsets Stefan Hanreich
@ 2024-11-12 12:25 ` Stefan Hanreich
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 16/24] tests: add ipam tests Stefan Hanreich
                   ` (8 subsequent siblings)
  23 siblings, 0 replies; 26+ messages in thread
From: Stefan Hanreich @ 2024-11-12 12:25 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/tests/sdn/main.rs           | 144 ++++++++++++++++++
 .../tests/sdn/resources/running-config.json   |  54 +++++++
 2 files changed, 198 insertions(+)
 create mode 100644 proxmox-ve-config/tests/sdn/main.rs
 create mode 100644 proxmox-ve-config/tests/sdn/resources/running-config.json

diff --git a/proxmox-ve-config/tests/sdn/main.rs b/proxmox-ve-config/tests/sdn/main.rs
new file mode 100644
index 0000000..2ac0cb3
--- /dev/null
+++ b/proxmox-ve-config/tests/sdn/main.rs
@@ -0,0 +1,144 @@
+use std::{
+    net::{IpAddr, Ipv4Addr, Ipv6Addr},
+    str::FromStr,
+};
+
+use proxmox_ve_config::{
+    firewall::types::{address::IpRange, Cidr},
+    sdn::{
+        config::{
+            RunningConfig, SdnConfig, SdnConfigError, SubnetConfig, VnetConfig, ZoneConfig,
+            ZoneType,
+        },
+        SubnetName, VnetName, ZoneName,
+    },
+};
+
+#[test]
+fn parse_running_config() {
+    let running_config: RunningConfig =
+        serde_json::from_str(include_str!("resources/running-config.json")).unwrap();
+
+    let parsed_config = SdnConfig::try_from(running_config).unwrap();
+
+    let sdn_config = SdnConfig::from_zones([ZoneConfig::from_vnets(
+        ZoneName::from_str("zone0").unwrap(),
+        ZoneType::Simple,
+        [
+            VnetConfig::from_subnets(
+                VnetName::from_str("vnet0").unwrap(),
+                [
+                    SubnetConfig::new(
+                        SubnetName::from_str("zone0-fd80::-64").unwrap(),
+                        Some(Ipv6Addr::new(0xFD80, 0, 0, 0, 0, 0, 0, 0x1).into()),
+                        true,
+                        [IpRange::new_v6(
+                            [0xFD80, 0, 0, 0, 0, 0, 0, 0x1000],
+                            [0xFD80, 0, 0, 0, 0, 0, 0, 0xFFFF],
+                        )
+                        .unwrap()],
+                    )
+                    .unwrap(),
+                    SubnetConfig::new(
+                        SubnetName::from_str("zone0-10.101.0.0-16").unwrap(),
+                        Some(Ipv4Addr::new(10, 101, 1, 1).into()),
+                        true,
+                        [
+                            IpRange::new_v4([10, 101, 98, 100], [10, 101, 98, 200]).unwrap(),
+                            IpRange::new_v4([10, 101, 99, 100], [10, 101, 99, 200]).unwrap(),
+                        ],
+                    )
+                    .unwrap(),
+                ],
+            )
+            .unwrap(),
+            VnetConfig::from_subnets(
+                VnetName::from_str("vnet1").unwrap(),
+                [SubnetConfig::new(
+                    SubnetName::from_str("zone0-10.102.0.0-16").unwrap(),
+                    None,
+                    false,
+                    [],
+                )
+                .unwrap()],
+            )
+            .unwrap(),
+        ],
+    )
+    .unwrap()])
+    .unwrap();
+
+    assert_eq!(sdn_config, parsed_config);
+}
+
+#[test]
+fn sdn_config() {
+    let mut sdn_config = SdnConfig::new();
+
+    let zone0_name = ZoneName::new("zone0".to_string()).unwrap();
+    let zone1_name = ZoneName::new("zone1".to_string()).unwrap();
+
+    let vnet0_name = VnetName::new("vnet0".to_string()).unwrap();
+    let vnet1_name = VnetName::new("vnet1".to_string()).unwrap();
+
+    let zone0 = ZoneConfig::new(zone0_name.clone(), ZoneType::Qinq);
+    sdn_config.add_zone(zone0).unwrap();
+
+    let vnet0 = VnetConfig::new(vnet0_name.clone());
+    assert_eq!(
+        sdn_config.add_vnet(&zone1_name, vnet0.clone()),
+        Err(SdnConfigError::ZoneNotFound)
+    );
+
+    sdn_config.add_vnet(&zone0_name, vnet0.clone()).unwrap();
+
+    let subnet = SubnetConfig::new(
+        SubnetName::new(zone0_name.clone(), Cidr::new_v4([10, 0, 0, 0], 16).unwrap()),
+        IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
+        true,
+        [],
+    )
+    .unwrap();
+
+    assert_eq!(
+        sdn_config.add_subnet(&zone0_name, &vnet1_name, subnet.clone()),
+        Err(SdnConfigError::VnetNotFound),
+    );
+
+    sdn_config
+        .add_subnet(&zone0_name, &vnet0_name, subnet)
+        .unwrap();
+
+    let zone1 = ZoneConfig::from_vnets(
+        zone1_name.clone(),
+        ZoneType::Evpn,
+        [VnetConfig::from_subnets(
+            vnet1_name.clone(),
+            [SubnetConfig::new(
+                SubnetName::new(
+                    zone0_name.clone(),
+                    Cidr::new_v4([192, 168, 0, 0], 24).unwrap(),
+                ),
+                None,
+                false,
+                [],
+            )
+            .unwrap()],
+        )
+        .unwrap()],
+    )
+    .unwrap();
+
+    assert_eq!(
+        sdn_config.add_zones([zone1]),
+        Err(SdnConfigError::MismatchedSubnetZone),
+    );
+
+    let zone1 = ZoneConfig::new(zone1_name.clone(), ZoneType::Evpn);
+    sdn_config.add_zone(zone1).unwrap();
+
+    assert_eq!(
+        sdn_config.add_vnet(&zone1_name, vnet0.clone()),
+        Err(SdnConfigError::DuplicateVnetName),
+    )
+}
diff --git a/proxmox-ve-config/tests/sdn/resources/running-config.json b/proxmox-ve-config/tests/sdn/resources/running-config.json
new file mode 100644
index 0000000..b03c20f
--- /dev/null
+++ b/proxmox-ve-config/tests/sdn/resources/running-config.json
@@ -0,0 +1,54 @@
+{
+  "version": 10,
+  "subnets": {
+    "ids": {
+      "zone0-fd80::-64": {
+        "gateway": "fd80::1",
+        "type": "subnet",
+        "snat": 1,
+        "dhcp-range": [
+          "start-address=fd80::1000,end-address=fd80::ffff"
+        ],
+        "vnet": "vnet0"
+      },
+      "zone0-10.102.0.0-16": {
+        "vnet": "vnet1",
+        "type": "subnet"
+      },
+      "zone0-10.101.0.0-16": {
+        "dhcp-range": [
+          "start-address=10.101.98.100,end-address=10.101.98.200",
+          "start-address=10.101.99.100,end-address=10.101.99.200"
+        ],
+        "vnet": "vnet0",
+        "type": "subnet",
+        "gateway": "10.101.1.1",
+        "snat": 1
+      }
+    }
+  },
+  "zones": {
+    "ids": {
+      "zone0": {
+        "ipam": "pve",
+        "dhcp": "dnsmasq",
+        "type": "simple"
+      }
+    }
+  },
+  "controllers": {
+    "ids": {}
+  },
+  "vnets": {
+    "ids": {
+      "vnet0": {
+        "type": "vnet",
+        "zone": "zone0"
+      },
+      "vnet1": {
+        "type": "vnet",
+        "zone": "zone0"
+      }
+    }
+  }
+}
-- 
2.39.5


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v3 16/24] tests: add ipam tests
  2024-11-12 12:25 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v3 00/24] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (14 preceding siblings ...)
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 15/24] tests: add sdn config tests Stefan Hanreich
@ 2024-11-12 12:25 ` Stefan Hanreich
  2024-11-12 19:16   ` [pve-devel] partially-applied-series: " Thomas Lamprecht
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-firewall v3 17/24] add proxmox-ve-rs crate - move proxmox-ve-config there Stefan Hanreich
                   ` (7 subsequent siblings)
  23 siblings, 1 reply; 26+ messages in thread
From: Stefan Hanreich @ 2024-11-12 12:25 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/tests/sdn/main.rs           | 45 +++++++++++++++++++
 proxmox-ve-config/tests/sdn/resources/ipam.db | 26 +++++++++++
 2 files changed, 71 insertions(+)
 create mode 100644 proxmox-ve-config/tests/sdn/resources/ipam.db

diff --git a/proxmox-ve-config/tests/sdn/main.rs b/proxmox-ve-config/tests/sdn/main.rs
index 2ac0cb3..1815bec 100644
--- a/proxmox-ve-config/tests/sdn/main.rs
+++ b/proxmox-ve-config/tests/sdn/main.rs
@@ -5,11 +5,13 @@ use std::{
 
 use proxmox_ve_config::{
     firewall::types::{address::IpRange, Cidr},
+    guest::vm::MacAddress,
     sdn::{
         config::{
             RunningConfig, SdnConfig, SdnConfigError, SubnetConfig, VnetConfig, ZoneConfig,
             ZoneType,
         },
+        ipam::{Ipam, IpamDataVm, IpamEntry, IpamJson},
         SubnetName, VnetName, ZoneName,
     },
 };
@@ -142,3 +144,46 @@ fn sdn_config() {
         Err(SdnConfigError::DuplicateVnetName),
     )
 }
+
+#[test]
+fn parse_ipam() {
+    let ipam_json: IpamJson = serde_json::from_str(include_str!("resources/ipam.db")).unwrap();
+    let ipam = Ipam::try_from(ipam_json).unwrap();
+
+    let zone_name = ZoneName::new("zone0".to_string()).unwrap();
+
+    assert_eq!(
+        Ipam::from_entries([
+            IpamEntry::new(
+                SubnetName::new(
+                    zone_name.clone(),
+                    Cidr::new_v6([0xFD80, 0, 0, 0, 0, 0, 0, 0], 64).unwrap()
+                ),
+                IpamDataVm::new(
+                    Ipv6Addr::new(0xFD80, 0, 0, 0, 0, 0, 0, 0x1000),
+                    1000,
+                    MacAddress::new([0xBC, 0x24, 0x11, 0, 0, 0x01]),
+                    "test0".to_string()
+                )
+                .into()
+            )
+            .unwrap(),
+            IpamEntry::new(
+                SubnetName::new(
+                    zone_name.clone(),
+                    Cidr::new_v4([10, 101, 0, 0], 16).unwrap()
+                ),
+                IpamDataVm::new(
+                    Ipv4Addr::new(10, 101, 99, 101),
+                    1000,
+                    MacAddress::new([0xBC, 0x24, 0x11, 0, 0, 0x01]),
+                    "test0".to_string()
+                )
+                .into()
+            )
+            .unwrap(),
+        ])
+        .unwrap(),
+        ipam
+    )
+}
diff --git a/proxmox-ve-config/tests/sdn/resources/ipam.db b/proxmox-ve-config/tests/sdn/resources/ipam.db
new file mode 100644
index 0000000..a3e6c87
--- /dev/null
+++ b/proxmox-ve-config/tests/sdn/resources/ipam.db
@@ -0,0 +1,26 @@
+{
+  "zones": {
+    "zone0": {
+      "subnets": {
+        "fd80::/64": {
+          "ips": {
+            "fd80::1000": {
+              "vmid": "1000",
+              "mac": "BC:24:11:00:00:01",
+              "hostname": "test0"
+            }
+          }
+        },
+        "10.101.0.0/16": {
+          "ips": {
+            "10.101.99.101": {
+              "mac": "BC:24:11:00:00:01",
+              "vmid": "1000",
+              "hostname": "test0"
+            }
+          }
+        }
+      }
+    }
+  }
+}
-- 
2.39.5


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pve-devel] [PATCH proxmox-firewall v3 17/24] add proxmox-ve-rs crate - move proxmox-ve-config there
  2024-11-12 12:25 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v3 00/24] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (15 preceding siblings ...)
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 16/24] tests: add ipam tests Stefan Hanreich
@ 2024-11-12 12:25 ` Stefan Hanreich
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-firewall v3 18/24] config: tests: add support for loading sdn and ipam config Stefan Hanreich
                   ` (6 subsequent siblings)
  23 siblings, 0 replies; 26+ messages in thread
From: Stefan Hanreich @ 2024-11-12 12:25 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 Cargo.toml                                    |   4 +-
 Makefile                                      |   2 +-
 proxmox-firewall/Cargo.toml                   |   2 +-
 proxmox-nftables/Cargo.toml                   |   2 +-
 proxmox-ve-config/Cargo.toml                  |  25 -
 proxmox-ve-config/resources/ct_helper.json    |  52 -
 proxmox-ve-config/resources/macros.json       | 923 -----------------
 proxmox-ve-config/src/firewall/cluster.rs     | 374 -------
 proxmox-ve-config/src/firewall/common.rs      | 184 ----
 proxmox-ve-config/src/firewall/ct_helper.rs   | 115 ---
 proxmox-ve-config/src/firewall/fw_macros.rs   |  69 --
 proxmox-ve-config/src/firewall/guest.rs       | 237 -----
 proxmox-ve-config/src/firewall/host.rs        | 372 -------
 proxmox-ve-config/src/firewall/mod.rs         |  10 -
 proxmox-ve-config/src/firewall/parse.rs       | 494 ---------
 proxmox-ve-config/src/firewall/ports.rs       |  80 --
 .../src/firewall/types/address.rs             | 615 -----------
 proxmox-ve-config/src/firewall/types/alias.rs | 174 ----
 proxmox-ve-config/src/firewall/types/group.rs |  36 -
 proxmox-ve-config/src/firewall/types/ipset.rs | 349 -------
 proxmox-ve-config/src/firewall/types/log.rs   | 222 ----
 proxmox-ve-config/src/firewall/types/mod.rs   |  14 -
 proxmox-ve-config/src/firewall/types/port.rs  | 181 ----
 proxmox-ve-config/src/firewall/types/rule.rs  | 412 --------
 .../src/firewall/types/rule_match.rs          | 977 ------------------
 proxmox-ve-config/src/guest/mod.rs            | 115 ---
 proxmox-ve-config/src/guest/types.rs          |  38 -
 proxmox-ve-config/src/guest/vm.rs             | 510 ---------
 proxmox-ve-config/src/host/mod.rs             |   1 -
 proxmox-ve-config/src/host/utils.rs           |  70 --
 proxmox-ve-config/src/lib.rs                  |   3 -
 31 files changed, 6 insertions(+), 6656 deletions(-)
 delete mode 100644 proxmox-ve-config/Cargo.toml
 delete mode 100644 proxmox-ve-config/resources/ct_helper.json
 delete mode 100644 proxmox-ve-config/resources/macros.json
 delete mode 100644 proxmox-ve-config/src/firewall/cluster.rs
 delete mode 100644 proxmox-ve-config/src/firewall/common.rs
 delete mode 100644 proxmox-ve-config/src/firewall/ct_helper.rs
 delete mode 100644 proxmox-ve-config/src/firewall/fw_macros.rs
 delete mode 100644 proxmox-ve-config/src/firewall/guest.rs
 delete mode 100644 proxmox-ve-config/src/firewall/host.rs
 delete mode 100644 proxmox-ve-config/src/firewall/mod.rs
 delete mode 100644 proxmox-ve-config/src/firewall/parse.rs
 delete mode 100644 proxmox-ve-config/src/firewall/ports.rs
 delete mode 100644 proxmox-ve-config/src/firewall/types/address.rs
 delete mode 100644 proxmox-ve-config/src/firewall/types/alias.rs
 delete mode 100644 proxmox-ve-config/src/firewall/types/group.rs
 delete mode 100644 proxmox-ve-config/src/firewall/types/ipset.rs
 delete mode 100644 proxmox-ve-config/src/firewall/types/log.rs
 delete mode 100644 proxmox-ve-config/src/firewall/types/mod.rs
 delete mode 100644 proxmox-ve-config/src/firewall/types/port.rs
 delete mode 100644 proxmox-ve-config/src/firewall/types/rule.rs
 delete mode 100644 proxmox-ve-config/src/firewall/types/rule_match.rs
 delete mode 100644 proxmox-ve-config/src/guest/mod.rs
 delete mode 100644 proxmox-ve-config/src/guest/types.rs
 delete mode 100644 proxmox-ve-config/src/guest/vm.rs
 delete mode 100644 proxmox-ve-config/src/host/mod.rs
 delete mode 100644 proxmox-ve-config/src/host/utils.rs
 delete mode 100644 proxmox-ve-config/src/lib.rs

diff --git a/Cargo.toml b/Cargo.toml
index 1fbc2e0..3894d9f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,9 @@
 [workspace]
 members = [
-    "proxmox-ve-config",
     "proxmox-nftables",
     "proxmox-firewall",
 ]
 resolver = "2"
+
+[workspace.dependencies]
+proxmox-ve-config = { version = "0.1.0" }
diff --git a/Makefile b/Makefile
index e49e58f..a134423 100644
--- a/Makefile
+++ b/Makefile
@@ -29,7 +29,7 @@ cargo-build:
 build: $(BUILDDIR)
 $(BUILDDIR):
 	rm -rf $@ $@.tmp; mkdir $@.tmp
-	cp -a proxmox-firewall proxmox-nftables proxmox-ve-config debian Cargo.toml Makefile defines.mk $@.tmp/
+	cp -a proxmox-firewall proxmox-nftables debian Cargo.toml Makefile defines.mk $@.tmp/
 	mv $@.tmp $@
 
 .PHONY: deb
diff --git a/proxmox-firewall/Cargo.toml b/proxmox-firewall/Cargo.toml
index 6cb1b09..c2adcac 100644
--- a/proxmox-firewall/Cargo.toml
+++ b/proxmox-firewall/Cargo.toml
@@ -21,7 +21,7 @@ serde_json = "1"
 signal-hook = "0.3"
 
 proxmox-nftables = { path = "../proxmox-nftables", features = ["config-ext"] }
-proxmox-ve-config = { path = "../proxmox-ve-config" }
+proxmox-ve-config = { workspace = true }
 
 [dev-dependencies]
 insta = { version = "1.21", features = ["json"] }
diff --git a/proxmox-nftables/Cargo.toml b/proxmox-nftables/Cargo.toml
index e84509d..4ff6f41 100644
--- a/proxmox-nftables/Cargo.toml
+++ b/proxmox-nftables/Cargo.toml
@@ -22,4 +22,4 @@ serde = { version = "1", features = [ "derive" ] }
 serde_json = "1"
 serde_plain = "1"
 
-proxmox-ve-config = { path = "../proxmox-ve-config", optional = true }
+proxmox-ve-config = { workspace = true, optional = true }
diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml
deleted file mode 100644
index 0239c08..0000000
--- a/proxmox-ve-config/Cargo.toml
+++ /dev/null
@@ -1,25 +0,0 @@
-[package]
-name = "proxmox-ve-config"
-version = "0.1.0"
-edition = "2021"
-authors = [
-    "Wolfgang Bumiller <w.bumiller@proxmox.com>",
-    "Stefan Hanreich <s.hanreich@proxmox.com>",
-    "Proxmox Support Team <support@proxmox.com>",
-]
-description = "Proxmox VE config parsing"
-license = "AGPL-3"
-
-[dependencies]
-log = "0.4"
-anyhow = "1"
-nix = "0.26"
-
-serde = { version = "1", features = [ "derive" ] }
-serde_json = "1"
-serde_plain = "1"
-serde_with = "3"
-
-proxmox-schema = "3.1.2"
-proxmox-sys = "0.6"
-proxmox-sortable-macro = "0.1.3"
diff --git a/proxmox-ve-config/resources/ct_helper.json b/proxmox-ve-config/resources/ct_helper.json
deleted file mode 100644
index 5e70a3a..0000000
--- a/proxmox-ve-config/resources/ct_helper.json
+++ /dev/null
@@ -1,52 +0,0 @@
-[
-  {
-    "name": "amanda",
-    "v4": true,
-    "v6": true,
-    "udp": 10080
-  },
-  {
-    "name": "ftp",
-    "v4": true,
-    "v6": true,
-    "tcp": 21
-  } ,
-  {
-    "name": "irc",
-    "v4": true,
-    "tcp": 6667
-  },
-  {
-    "name": "netbios-ns",
-    "v4": true,
-    "udp": 137
-  },
-  {
-    "name": "pptp",
-    "v4": true,
-    "tcp": 1723
-  },
-  {
-    "name": "sane",
-    "v4": true,
-    "v6": true,
-    "tcp": 6566
-  },
-  {
-    "name": "sip",
-    "v4": true,
-    "v6": true,
-    "udp": 5060
-  },
-  {
-    "name": "snmp",
-    "v4": true,
-    "udp": 161
-  },
-  {
-    "name": "tftp",
-    "v4": true,
-    "v6": true,
-    "udp": 69
-  }
-]
diff --git a/proxmox-ve-config/resources/macros.json b/proxmox-ve-config/resources/macros.json
deleted file mode 100644
index 2fcc0fb..0000000
--- a/proxmox-ve-config/resources/macros.json
+++ /dev/null
@@ -1,923 +0,0 @@
-{
-  "Amanda": {
-    "code": [
-      {
-        "dport": "10080",
-        "proto": "udp"
-      },
-      {
-        "dport": "10080",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Amanda Backup"
-  },
-  "Auth": {
-    "code": [
-      {
-        "dport": "113",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Auth (identd) traffic"
-  },
-  "BGP": {
-    "code": [
-      {
-        "dport": "179",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Border Gateway Protocol traffic"
-  },
-  "BitTorrent": {
-    "code": [
-      {
-        "dport": "6881:6889",
-        "proto": "tcp"
-      },
-      {
-        "dport": "6881",
-        "proto": "udp"
-      }
-    ],
-    "desc": "BitTorrent traffic for BitTorrent 3.1 and earlier"
-  },
-  "BitTorrent32": {
-    "code": [
-      {
-        "dport": "6881:6999",
-        "proto": "tcp"
-      },
-      {
-        "dport": "6881",
-        "proto": "udp"
-      }
-    ],
-    "desc": "BitTorrent traffic for BitTorrent 3.2 and later"
-  },
-  "CVS": {
-    "code": [
-      {
-        "dport": "2401",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Concurrent Versions System pserver traffic"
-  },
-  "Ceph": {
-    "code": [
-      {
-        "dport": "6789",
-        "proto": "tcp"
-      },
-      {
-        "dport": "3300",
-        "proto": "tcp"
-      },
-      {
-        "dport": "6800:7300",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Ceph Storage Cluster traffic (Ceph Monitors, OSD & MDS Daemons)"
-  },
-  "Citrix": {
-    "code": [
-      {
-        "dport": "1494",
-        "proto": "tcp"
-      },
-      {
-        "dport": "1604",
-        "proto": "udp"
-      },
-      {
-        "dport": "2598",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Citrix/ICA traffic (ICA, ICA Browser, CGP)"
-  },
-  "DAAP": {
-    "code": [
-      {
-        "dport": "3689",
-        "proto": "tcp"
-      },
-      {
-        "dport": "3689",
-        "proto": "udp"
-      }
-    ],
-    "desc": "Digital Audio Access Protocol traffic (iTunes, Rythmbox daemons)"
-  },
-  "DCC": {
-    "code": [
-      {
-        "dport": "6277",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Distributed Checksum Clearinghouse spam filtering mechanism"
-  },
-  "DHCPfwd": {
-    "code": [
-      {
-        "dport": "67:68",
-        "proto": "udp",
-        "sport": "67:68"
-      }
-    ],
-    "desc": "Forwarded DHCP traffic"
-  },
-  "DHCPv6": {
-    "code": [
-      {
-        "dport": "546:547",
-        "proto": "udp",
-        "sport": "546:547"
-      }
-    ],
-    "desc": "DHCPv6 traffic"
-  },
-  "DNS": {
-    "code": [
-      {
-        "dport": "53",
-        "proto": "udp"
-      },
-      {
-        "dport": "53",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Domain Name System traffic (upd and tcp)"
-  },
-  "Distcc": {
-    "code": [
-      {
-        "dport": "3632",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Distributed Compiler service"
-  },
-  "FTP": {
-    "code": [
-      {
-        "dport": "21",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "File Transfer Protocol"
-  },
-  "Finger": {
-    "code": [
-      {
-        "dport": "79",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Finger protocol (RFC 742)"
-  },
-  "GNUnet": {
-    "code": [
-      {
-        "dport": "2086",
-        "proto": "tcp"
-      },
-      {
-        "dport": "2086",
-        "proto": "udp"
-      },
-      {
-        "dport": "1080",
-        "proto": "tcp"
-      },
-      {
-        "dport": "1080",
-        "proto": "udp"
-      }
-    ],
-    "desc": "GNUnet secure peer-to-peer networking traffic"
-  },
-  "GRE": {
-    "code": [
-      {
-        "proto": "47"
-      }
-    ],
-    "desc": "Generic Routing Encapsulation tunneling protocol"
-  },
-  "Git": {
-    "code": [
-      {
-        "dport": "9418",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Git distributed revision control traffic"
-  },
-  "HKP": {
-    "code": [
-      {
-        "dport": "11371",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "OpenPGP HTTP key server protocol traffic"
-  },
-  "HTTP": {
-    "code": [
-      {
-        "dport": "80",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Hypertext Transfer Protocol (WWW)"
-  },
-  "HTTPS": {
-    "code": [
-      {
-        "dport": "443",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Hypertext Transfer Protocol (WWW) over SSL"
-  },
-  "HTTP/3": {
-    "code": [
-      {
-        "dport": "443",
-        "proto": "udp"
-      }
-    ],
-    "desc": "Hypertext Transfer Protocol v3"
-  },
-  "ICPV2": {
-    "code": [
-      {
-        "dport": "3130",
-        "proto": "udp"
-      }
-    ],
-    "desc": "Internet Cache Protocol V2 (Squid) traffic"
-  },
-  "ICQ": {
-    "code": [
-      {
-        "dport": "5190",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "AOL Instant Messenger traffic"
-  },
-  "IMAP": {
-    "code": [
-      {
-        "dport": "143",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Internet Message Access Protocol"
-  },
-  "IMAPS": {
-    "code": [
-      {
-        "dport": "993",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Internet Message Access Protocol over SSL"
-  },
-  "IPIP": {
-    "code": [
-      {
-        "proto": "94"
-      }
-    ],
-    "desc": "IPIP capsulation traffic"
-  },
-  "IPsec": {
-    "code": [
-      {
-        "dport": "500",
-        "proto": "udp",
-        "sport": "500"
-      },
-      {
-        "proto": "50"
-      }
-    ],
-    "desc": "IPsec traffic"
-  },
-  "IPsecah": {
-    "code": [
-      {
-        "dport": "500",
-        "proto": "udp",
-        "sport": "500"
-      },
-      {
-        "proto": "51"
-      }
-    ],
-    "desc": "IPsec authentication (AH) traffic"
-  },
-  "IPsecnat": {
-    "code": [
-      {
-        "dport": "500",
-        "proto": "udp"
-      },
-      {
-        "dport": "4500",
-        "proto": "udp"
-      },
-      {
-        "proto": "50"
-      }
-    ],
-    "desc": "IPsec traffic and Nat-Traversal"
-  },
-  "IRC": {
-    "code": [
-      {
-        "dport": "6667",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Internet Relay Chat traffic"
-  },
-  "Jetdirect": {
-    "code": [
-      {
-        "dport": "9100",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "HP Jetdirect printing"
-  },
-  "L2TP": {
-    "code": [
-      {
-        "dport": "1701",
-        "proto": "udp"
-      }
-    ],
-    "desc": "Layer 2 Tunneling Protocol traffic"
-  },
-  "LDAP": {
-    "code": [
-      {
-        "dport": "389",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Lightweight Directory Access Protocol traffic"
-  },
-  "LDAPS": {
-    "code": [
-      {
-        "dport": "636",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Secure Lightweight Directory Access Protocol traffic"
-  },
-  "MDNS": {
-    "code": [
-      {
-        "dport": "5353",
-        "proto": "udp"
-      }
-    ],
-    "desc": "Multicast DNS"
-  },
-  "MSNP": {
-    "code": [
-      {
-        "dport": "1863",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Microsoft Notification Protocol"
-  },
-  "MSSQL": {
-    "code": [
-      {
-        "dport": "1433",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Microsoft SQL Server"
-  },
-  "Mail": {
-    "code": [
-      {
-        "dport": "25",
-        "proto": "tcp"
-      },
-      {
-        "dport": "465",
-        "proto": "tcp"
-      },
-      {
-        "dport": "587",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Mail traffic (SMTP, SMTPS, Submission)"
-  },
-  "Munin": {
-    "code": [
-      {
-        "dport": "4949",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Munin networked resource monitoring traffic"
-  },
-  "MySQL": {
-    "code": [
-      {
-        "dport": "3306",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "MySQL server"
-  },
-  "NNTP": {
-    "code": [
-      {
-        "dport": "119",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "NNTP traffic (Usenet)."
-  },
-  "NNTPS": {
-    "code": [
-      {
-        "dport": "563",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Encrypted NNTP traffic (Usenet)"
-  },
-  "NTP": {
-    "code": [
-      {
-        "dport": "123",
-        "proto": "udp"
-      }
-    ],
-    "desc": "Network Time Protocol (ntpd)"
-  },
-  "NeighborDiscovery": {
-    "code": [
-      {
-        "dport": "nd-router-solicit",
-        "proto": "icmpv6"
-      },
-      {
-        "dport": "nd-router-advert",
-        "proto": "icmpv6"
-      },
-      {
-        "dport": "nd-neighbor-solicit",
-        "proto": "icmpv6"
-      },
-      {
-        "dport": "nd-neighbor-advert",
-        "proto": "icmpv6"
-      }
-    ],
-    "desc": "IPv6 neighbor solicitation, neighbor and router advertisement"
-  },
-  "OSPF": {
-    "code": [
-      {
-        "proto": "89"
-      }
-    ],
-    "desc": "OSPF multicast traffic"
-  },
-  "OpenVPN": {
-    "code": [
-      {
-        "dport": "1194",
-        "proto": "udp"
-      }
-    ],
-    "desc": "OpenVPN traffic"
-  },
-  "PBS": {
-    "code": [
-      {
-        "dport": "8007",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Proxmox Backup Server"
-  },
-  "PCA": {
-    "code": [
-      {
-        "dport": "5632",
-        "proto": "udp"
-      },
-      {
-        "dport": "5631",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Symantec PCAnywere (tm)"
-  },
-  "PMG": {
-    "code": [
-      {
-        "dport": "8006",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Proxmox Mail Gateway web interface"
-  },
-  "POP3": {
-    "code": [
-      {
-        "dport": "110",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "POP3 traffic"
-  },
-  "POP3S": {
-    "code": [
-      {
-        "dport": "995",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Encrypted POP3 traffic"
-  },
-  "PPtP": {
-    "code": [
-      {
-        "proto": "47"
-      },
-      {
-        "dport": "1723",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Point-to-Point Tunneling Protocol"
-  },
-  "Ping": {
-    "code": [
-      {
-        "dport": "echo-request",
-        "proto": "icmp"
-      }
-    ],
-    "desc": "ICMP echo request"
-  },
-  "PostgreSQL": {
-    "code": [
-      {
-        "dport": "5432",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "PostgreSQL server"
-  },
-  "Printer": {
-    "code": [
-      {
-        "dport": "515",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Line Printer protocol printing"
-  },
-  "RDP": {
-    "code": [
-      {
-        "dport": "3389",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Microsoft Remote Desktop Protocol traffic"
-  },
-  "RIP": {
-    "code": [
-      {
-        "dport": "520",
-        "proto": "udp"
-      }
-    ],
-    "desc": "Routing Information Protocol (bidirectional)"
-  },
-  "RNDC": {
-    "code": [
-      {
-        "dport": "953",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "BIND remote management protocol"
-  },
-  "Razor": {
-    "code": [
-      {
-        "dport": "2703",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Razor Antispam System"
-  },
-  "Rdate": {
-    "code": [
-      {
-        "dport": "37",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Remote time retrieval (rdate)"
-  },
-  "Rsync": {
-    "code": [
-      {
-        "dport": "873",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Rsync server"
-  },
-  "SANE": {
-    "code": [
-      {
-        "dport": "6566",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "SANE network scanning"
-  },
-  "SMB": {
-    "code": [
-      {
-        "dport": "135,445",
-        "proto": "udp"
-      },
-      {
-        "dport": "137:139",
-        "proto": "udp"
-      },
-      {
-        "dport": "1024:65535",
-        "proto": "udp",
-        "sport": "137"
-      },
-      {
-        "dport": "135,139,445",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Microsoft SMB traffic"
-  },
-  "SMBswat": {
-    "code": [
-      {
-        "dport": "901",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Samba Web Administration Tool"
-  },
-  "SMTP": {
-    "code": [
-      {
-        "dport": "25",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Simple Mail Transfer Protocol"
-  },
-  "SMTPS": {
-    "code": [
-      {
-        "dport": "465",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Encrypted Simple Mail Transfer Protocol"
-  },
-  "SNMP": {
-    "code": [
-      {
-        "dport": "161:162",
-        "proto": "udp"
-      },
-      {
-        "dport": "161",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Simple Network Management Protocol"
-  },
-  "SPAMD": {
-    "code": [
-      {
-        "dport": "783",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Spam Assassin SPAMD traffic"
-  },
-  "SPICEproxy": {
-    "code": [
-      {
-        "dport": "3128",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Proxmox VE SPICE display proxy traffic"
-  },
-  "SSH": {
-    "code": [
-      {
-        "dport": "22",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Secure shell traffic"
-  },
-  "SVN": {
-    "code": [
-      {
-        "dport": "3690",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Subversion server (svnserve)"
-  },
-  "SixXS": {
-    "code": [
-      {
-        "dport": "3874",
-        "proto": "tcp"
-      },
-      {
-        "dport": "3740",
-        "proto": "udp"
-      },
-      {
-        "proto": "41"
-      },
-      {
-        "dport": "5072,8374",
-        "proto": "udp"
-      }
-    ],
-    "desc": "SixXS IPv6 Deployment and Tunnel Broker"
-  },
-  "Squid": {
-    "code": [
-      {
-        "dport": "3128",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Squid web proxy traffic"
-  },
-  "Submission": {
-    "code": [
-      {
-        "dport": "587",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Mail message submission traffic"
-  },
-  "Syslog": {
-    "code": [
-      {
-        "dport": "514",
-        "proto": "udp"
-      },
-      {
-        "dport": "514",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Syslog protocol (RFC 5424) traffic"
-  },
-  "TFTP": {
-    "code": [
-      {
-        "dport": "69",
-        "proto": "udp"
-      }
-    ],
-    "desc": "Trivial File Transfer Protocol traffic"
-  },
-  "Telnet": {
-    "code": [
-      {
-        "dport": "23",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Telnet traffic"
-  },
-  "Telnets": {
-    "code": [
-      {
-        "dport": "992",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Telnet over SSL"
-  },
-  "Time": {
-    "code": [
-      {
-        "dport": "37",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "RFC 868 Time protocol"
-  },
-  "Trcrt": {
-    "code": [
-      {
-        "dport": "33434:33524",
-        "proto": "udp"
-      },
-      {
-        "dport": "echo-request",
-        "proto": "icmp"
-      }
-    ],
-    "desc": "Traceroute (for up to 30 hops) traffic"
-  },
-  "VNC": {
-    "code": [
-      {
-        "dport": "5900:5999",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "VNC traffic for VNC display's 0 - 99"
-  },
-  "VNCL": {
-    "code": [
-      {
-        "dport": "5500",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "VNC traffic from Vncservers to Vncviewers in listen mode"
-  },
-  "Web": {
-    "code": [
-      {
-        "dport": "80",
-        "proto": "tcp"
-      },
-      {
-        "dport": "443",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "WWW traffic (HTTP and HTTPS)"
-  },
-  "Webcache": {
-    "code": [
-      {
-        "dport": "8080",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Web Cache/Proxy traffic (port 8080)"
-  },
-  "Webmin": {
-    "code": [
-      {
-        "dport": "10000",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Webmin traffic"
-  },
-  "Whois": {
-    "code": [
-      {
-        "dport": "43",
-        "proto": "tcp"
-      }
-    ],
-    "desc": "Whois (nicname, RFC 3912) traffic"
-  }
-}
diff --git a/proxmox-ve-config/src/firewall/cluster.rs b/proxmox-ve-config/src/firewall/cluster.rs
deleted file mode 100644
index 223124b..0000000
--- a/proxmox-ve-config/src/firewall/cluster.rs
+++ /dev/null
@@ -1,374 +0,0 @@
-use std::collections::BTreeMap;
-use std::io;
-
-use anyhow::Error;
-use serde::Deserialize;
-
-use crate::firewall::common::ParserConfig;
-use crate::firewall::types::ipset::{Ipset, IpsetScope};
-use crate::firewall::types::log::LogRateLimit;
-use crate::firewall::types::rule::{Direction, Verdict};
-use crate::firewall::types::{Alias, Group, Rule};
-
-use crate::firewall::parse::{serde_option_bool, serde_option_log_ratelimit};
-
-#[derive(Debug, Default)]
-pub struct Config {
-    pub(crate) config: super::common::Config<Options>,
-}
-
-/// default setting for [`Config::is_enabled()`]
-pub const CLUSTER_ENABLED_DEFAULT: bool = false;
-/// default setting for [`Config::ebtables()`]
-pub const CLUSTER_EBTABLES_DEFAULT: bool = false;
-/// default setting for [`Config::default_policy()`]
-pub const CLUSTER_POLICY_IN_DEFAULT: Verdict = Verdict::Drop;
-/// default setting for [`Config::default_policy()`]
-pub const CLUSTER_POLICY_OUT_DEFAULT: Verdict = Verdict::Accept;
-
-impl Config {
-    pub fn parse<R: io::BufRead>(input: R) -> Result<Self, Error> {
-        let parser_config = ParserConfig {
-            guest_iface_names: false,
-            ipset_scope: Some(IpsetScope::Datacenter),
-        };
-
-        Ok(Self {
-            config: super::common::Config::parse(input, &parser_config)?,
-        })
-    }
-
-    pub fn rules(&self) -> &Vec<Rule> {
-        &self.config.rules
-    }
-
-    pub fn groups(&self) -> &BTreeMap<String, Group> {
-        &self.config.groups
-    }
-
-    pub fn ipsets(&self) -> &BTreeMap<String, Ipset> {
-        &self.config.ipsets
-    }
-
-    pub fn alias(&self, name: &str) -> Option<&Alias> {
-        self.config.alias(name)
-    }
-
-    pub fn is_enabled(&self) -> bool {
-        self.config
-            .options
-            .enable
-            .unwrap_or(CLUSTER_ENABLED_DEFAULT)
-    }
-
-    /// returns the ebtables option from the cluster config or [`CLUSTER_EBTABLES_DEFAULT`] if
-    /// unset
-    ///
-    /// this setting is leftover from the old firewall, but has no effect on the nftables firewall
-    pub fn ebtables(&self) -> bool {
-        self.config
-            .options
-            .ebtables
-            .unwrap_or(CLUSTER_EBTABLES_DEFAULT)
-    }
-
-    /// returns policy_in / out or [`CLUSTER_POLICY_IN_DEFAULT`] / [`CLUSTER_POLICY_OUT_DEFAULT`] if
-    /// unset
-    pub fn default_policy(&self, dir: Direction) -> Verdict {
-        match dir {
-            Direction::In => self
-                .config
-                .options
-                .policy_in
-                .unwrap_or(CLUSTER_POLICY_IN_DEFAULT),
-            Direction::Out => self
-                .config
-                .options
-                .policy_out
-                .unwrap_or(CLUSTER_POLICY_OUT_DEFAULT),
-        }
-    }
-
-    /// returns the rate_limit for logs or [`None`] if rate limiting is disabled
-    ///
-    /// If there is no rate limit set, then [`LogRateLimit::default`] is used
-    pub fn log_ratelimit(&self) -> Option<LogRateLimit> {
-        let rate_limit = self
-            .config
-            .options
-            .log_ratelimit
-            .clone()
-            .unwrap_or_default();
-
-        match rate_limit.enabled() {
-            true => Some(rate_limit),
-            false => None,
-        }
-    }
-}
-
-#[derive(Debug, Default, Deserialize)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Options {
-    #[serde(default, with = "serde_option_bool")]
-    enable: Option<bool>,
-
-    #[serde(default, with = "serde_option_bool")]
-    ebtables: Option<bool>,
-
-    #[serde(default, with = "serde_option_log_ratelimit")]
-    log_ratelimit: Option<LogRateLimit>,
-
-    policy_in: Option<Verdict>,
-    policy_out: Option<Verdict>,
-}
-
-#[cfg(test)]
-mod tests {
-    use crate::firewall::types::{
-        address::IpList,
-        alias::{AliasName, AliasScope},
-        ipset::{IpsetAddress, IpsetEntry},
-        log::{LogLevel, LogRateLimitTimescale},
-        rule::{Kind, RuleGroup},
-        rule_match::{
-            Icmpv6, Icmpv6Code, IpAddrMatch, IpMatch, Ports, Protocol, RuleMatch, Tcp, Udp,
-        },
-        Cidr,
-    };
-
-    use super::*;
-
-    #[test]
-    fn test_parse_config() {
-        const CONFIG: &str = r#"
-[OPTIONS]
-enable: 1
-log_ratelimit: 1,rate=10/second,burst=20
-ebtables: 0
-policy_in: REJECT
-policy_out: REJECT
-
-[ALIASES]
-
-another 8.8.8.18
-analias 7.7.0.0/16 # much
-wide cccc::/64
-
-[IPSET a-set]
-
-!5.5.5.5
-1.2.3.4/30
-dc/analias # a comment
-dc/wide
-dddd::/96
-
-[RULES]
-
-GROUP tgr -i eth0 # acomm
-IN ACCEPT -p udp -dport 33 -sport 22 -log warning
-
-[group tgr] # comment for tgr
-
-|OUT ACCEPT -source fe80::1/48 -dest dddd:3:3::9/64 -p icmpv6 -log nolog -icmp-type port-unreachable
-OUT ACCEPT -p tcp -sport 33 -log nolog
-IN BGP(REJECT) -log crit -source 1.2.3.4
-"#;
-
-        let mut config = CONFIG.as_bytes();
-        let config = Config::parse(&mut config).unwrap();
-
-        assert_eq!(
-            config.config.options,
-            Options {
-                ebtables: Some(false),
-                enable: Some(true),
-                log_ratelimit: Some(LogRateLimit::new(
-                    true,
-                    10,
-                    LogRateLimitTimescale::Second,
-                    20
-                )),
-                policy_in: Some(Verdict::Reject),
-                policy_out: Some(Verdict::Reject),
-            }
-        );
-
-        assert_eq!(config.config.aliases.len(), 3);
-
-        assert_eq!(
-            config.config.aliases["another"],
-            Alias::new("another", Cidr::new_v4([8, 8, 8, 18], 32).unwrap(), None),
-        );
-
-        assert_eq!(
-            config.config.aliases["analias"],
-            Alias::new(
-                "analias",
-                Cidr::new_v4([7, 7, 0, 0], 16).unwrap(),
-                "much".to_string()
-            ),
-        );
-
-        assert_eq!(
-            config.config.aliases["wide"],
-            Alias::new(
-                "wide",
-                Cidr::new_v6(
-                    [0xCCCC, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x000],
-                    64
-                )
-                .unwrap(),
-                None
-            ),
-        );
-
-        assert_eq!(config.config.ipsets.len(), 1);
-
-        let mut ipset_elements = vec![
-            IpsetEntry {
-                nomatch: true,
-                address: Cidr::new_v4([5, 5, 5, 5], 32).unwrap().into(),
-                comment: None,
-            },
-            IpsetEntry {
-                nomatch: false,
-                address: Cidr::new_v4([1, 2, 3, 4], 30).unwrap().into(),
-                comment: None,
-            },
-            IpsetEntry {
-                nomatch: false,
-                address: IpsetAddress::Alias(AliasName::new(AliasScope::Datacenter, "analias")),
-                comment: Some("a comment".to_string()),
-            },
-            IpsetEntry {
-                nomatch: false,
-                address: IpsetAddress::Alias(AliasName::new(AliasScope::Datacenter, "wide")),
-                comment: None,
-            },
-            IpsetEntry {
-                nomatch: false,
-                address: Cidr::new_v6([0xdd, 0xdd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 96)
-                    .unwrap()
-                    .into(),
-                comment: None,
-            },
-        ];
-
-        let mut ipset = Ipset::from_parts(IpsetScope::Datacenter, "a-set");
-        ipset.append(&mut ipset_elements);
-
-        assert_eq!(config.config.ipsets["a-set"], ipset,);
-
-        assert_eq!(config.config.rules.len(), 2);
-
-        assert_eq!(
-            config.config.rules[0],
-            Rule {
-                disabled: false,
-                comment: Some("acomm".to_string()),
-                kind: Kind::Group(RuleGroup {
-                    group: "tgr".to_string(),
-                    iface: Some("eth0".to_string()),
-                }),
-            },
-        );
-
-        assert_eq!(
-            config.config.rules[1],
-            Rule {
-                disabled: false,
-                comment: None,
-                kind: Kind::Match(RuleMatch {
-                    dir: Direction::In,
-                    verdict: Verdict::Accept,
-                    proto: Some(Protocol::Udp(Udp::new(Ports::from_u16(22, 33)))),
-                    log: Some(LogLevel::Warning),
-                    ..Default::default()
-                }),
-            },
-        );
-
-        assert_eq!(config.config.groups.len(), 1);
-
-        let entry = &config.config.groups["tgr"];
-        assert_eq!(entry.comment(), Some("comment for tgr"));
-        assert_eq!(entry.rules().len(), 3);
-
-        assert_eq!(
-            entry.rules()[0],
-            Rule {
-                disabled: true,
-                comment: None,
-                kind: Kind::Match(RuleMatch {
-                    dir: Direction::Out,
-                    verdict: Verdict::Accept,
-                    ip: Some(IpMatch {
-                        src: Some(IpAddrMatch::Ip(IpList::from(
-                            Cidr::new_v6(
-                                [0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
-                                48
-                            )
-                            .unwrap()
-                        ))),
-                        dst: Some(IpAddrMatch::Ip(IpList::from(
-                            Cidr::new_v6(
-                                [0xdd, 0xdd, 0, 3, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9],
-                                64
-                            )
-                            .unwrap()
-                        ))),
-                    }),
-                    proto: Some(Protocol::Icmpv6(Icmpv6::new_code(Icmpv6Code::Named(
-                        "port-unreachable"
-                    )))),
-                    log: Some(LogLevel::Nolog),
-                    ..Default::default()
-                }),
-            },
-        );
-        assert_eq!(
-            entry.rules()[1],
-            Rule {
-                disabled: false,
-                comment: None,
-                kind: Kind::Match(RuleMatch {
-                    dir: Direction::Out,
-                    verdict: Verdict::Accept,
-                    proto: Some(Protocol::Tcp(Tcp::new(Ports::from_u16(33, None)))),
-                    log: Some(LogLevel::Nolog),
-                    ..Default::default()
-                }),
-            },
-        );
-
-        assert_eq!(
-            entry.rules()[2],
-            Rule {
-                disabled: false,
-                comment: None,
-                kind: Kind::Match(RuleMatch {
-                    dir: Direction::In,
-                    verdict: Verdict::Reject,
-                    log: Some(LogLevel::Critical),
-                    fw_macro: Some("BGP".to_string()),
-                    ip: Some(IpMatch {
-                        src: Some(IpAddrMatch::Ip(IpList::from(
-                            Cidr::new_v4([1, 2, 3, 4], 32).unwrap()
-                        ))),
-                        dst: None,
-                    }),
-                    ..Default::default()
-                }),
-            },
-        );
-
-        let empty_config = Config::parse("".as_bytes()).expect("empty config is invalid");
-
-        assert_eq!(empty_config.config.options, Options::default());
-        assert!(empty_config.config.rules.is_empty());
-        assert!(empty_config.config.aliases.is_empty());
-        assert!(empty_config.config.ipsets.is_empty());
-        assert!(empty_config.config.groups.is_empty());
-    }
-}
diff --git a/proxmox-ve-config/src/firewall/common.rs b/proxmox-ve-config/src/firewall/common.rs
deleted file mode 100644
index a08f19c..0000000
--- a/proxmox-ve-config/src/firewall/common.rs
+++ /dev/null
@@ -1,184 +0,0 @@
-use std::collections::{BTreeMap, HashMap};
-use std::io;
-
-use anyhow::{bail, format_err, Error};
-use serde::de::IntoDeserializer;
-
-use crate::firewall::parse::{parse_named_section_tail, split_key_value, SomeString};
-use crate::firewall::types::ipset::{IpsetName, IpsetScope};
-use crate::firewall::types::{Alias, Group, Ipset, Rule};
-
-#[derive(Debug, Default)]
-pub struct Config<O>
-where
-    O: Default + std::fmt::Debug + serde::de::DeserializeOwned,
-{
-    pub(crate) options: O,
-    pub(crate) rules: Vec<Rule>,
-    pub(crate) aliases: BTreeMap<String, Alias>,
-    pub(crate) ipsets: BTreeMap<String, Ipset>,
-    pub(crate) groups: BTreeMap<String, Group>,
-}
-
-enum Sec {
-    None,
-    Options,
-    Aliases,
-    Rules,
-    Ipset(String, Ipset),
-    Group(String, Group),
-}
-
-#[derive(Default)]
-pub struct ParserConfig {
-    /// Network interfaces must be of the form `netX`.
-    pub guest_iface_names: bool,
-    pub ipset_scope: Option<IpsetScope>,
-}
-
-impl<O> Config<O>
-where
-    O: Default + std::fmt::Debug + serde::de::DeserializeOwned,
-{
-    pub fn new() -> Self {
-        Self::default()
-    }
-
-    pub fn parse<R: io::BufRead>(input: R, parser_cfg: &ParserConfig) -> Result<Self, Error> {
-        let mut section = Sec::None;
-
-        let mut this = Self::new();
-        let mut options = HashMap::new();
-
-        for line in input.lines() {
-            let line = line?;
-            let line = line.trim();
-
-            if line.is_empty() || line.starts_with('#') {
-                continue;
-            }
-
-            log::trace!("parsing config line {line}");
-
-            if line.eq_ignore_ascii_case("[OPTIONS]") {
-                this.set_section(&mut section, Sec::Options)?;
-            } else if line.eq_ignore_ascii_case("[ALIASES]") {
-                this.set_section(&mut section, Sec::Aliases)?;
-            } else if line.eq_ignore_ascii_case("[RULES]") {
-                this.set_section(&mut section, Sec::Rules)?;
-            } else if let Some(line) = line.strip_prefix("[IPSET") {
-                let (name, comment) = parse_named_section_tail("ipset", line)?;
-
-                let scope = parser_cfg.ipset_scope.ok_or_else(|| {
-                    format_err!("IPSET in config, but no scope set in parser config")
-                })?;
-
-                let ipset_name = IpsetName::new(scope, name.to_string());
-                let mut ipset = Ipset::new(ipset_name);
-                ipset.comment = comment.map(str::to_owned);
-
-                this.set_section(&mut section, Sec::Ipset(name.to_string(), ipset))?;
-            } else if let Some(line) = line.strip_prefix("[group") {
-                let (name, comment) = parse_named_section_tail("group", line)?;
-                let mut group = Group::new();
-
-                group.set_comment(comment.map(str::to_owned));
-
-                this.set_section(&mut section, Sec::Group(name.to_owned(), group))?;
-            } else if line.starts_with('[') {
-                bail!("invalid section {line:?}");
-            } else {
-                match &mut section {
-                    Sec::None => bail!("config line with no section: {line:?}"),
-                    Sec::Options => Self::parse_option(line, &mut options)?,
-                    Sec::Aliases => this.parse_alias(line)?,
-                    Sec::Rules => this.parse_rule(line, parser_cfg)?,
-                    Sec::Ipset(_name, ipset) => ipset.parse_entry(line)?,
-                    Sec::Group(_name, group) => group.parse_entry(line)?,
-                }
-            }
-        }
-        this.set_section(&mut section, Sec::None)?;
-
-        this.options = O::deserialize(IntoDeserializer::<
-            '_,
-            crate::firewall::parse::SerdeStringError,
-        >::into_deserializer(options))?;
-
-        Ok(this)
-    }
-
-    fn parse_option(line: &str, options: &mut HashMap<String, SomeString>) -> Result<(), Error> {
-        let (key, value) = split_key_value(line)
-            .ok_or_else(|| format_err!("expected colon separated key and value, found {line:?}"))?;
-
-        if options.insert(key.to_string(), value.into()).is_some() {
-            bail!("duplicate option {key:?}");
-        }
-
-        Ok(())
-    }
-
-    fn parse_alias(&mut self, line: &str) -> Result<(), Error> {
-        let alias: Alias = line.parse()?;
-
-        if self
-            .aliases
-            .insert(alias.name().to_string(), alias)
-            .is_some()
-        {
-            bail!("duplicate alias: {line}");
-        }
-
-        Ok(())
-    }
-
-    fn parse_rule(&mut self, line: &str, parser_cfg: &ParserConfig) -> Result<(), Error> {
-        let rule: Rule = line.parse()?;
-
-        if parser_cfg.guest_iface_names {
-            if let Some(iface) = rule.iface() {
-                let _ = iface
-                    .strip_prefix("net")
-                    .ok_or_else(|| {
-                        format_err!("interface name must be of the form \"net<number>\"")
-                    })?
-                    .parse::<u16>()
-                    .map_err(|_| {
-                        format_err!("interface name must be of the form \"net<number>\"")
-                    })?;
-            }
-        }
-
-        self.rules.push(rule);
-        Ok(())
-    }
-
-    fn set_section(&mut self, sec: &mut Sec, to: Sec) -> Result<(), Error> {
-        let prev = std::mem::replace(sec, to);
-
-        match prev {
-            Sec::Ipset(name, ipset) => {
-                if self.ipsets.insert(name.clone(), ipset).is_some() {
-                    bail!("duplicate ipset: {name:?}");
-                }
-            }
-            Sec::Group(name, group) => {
-                if self.groups.insert(name.clone(), group).is_some() {
-                    bail!("duplicate group: {name:?}");
-                }
-            }
-            _ => (),
-        }
-
-        Ok(())
-    }
-
-    pub fn ipsets(&self) -> &BTreeMap<String, Ipset> {
-        &self.ipsets
-    }
-
-    pub fn alias(&self, name: &str) -> Option<&Alias> {
-        self.aliases.get(name)
-    }
-}
diff --git a/proxmox-ve-config/src/firewall/ct_helper.rs b/proxmox-ve-config/src/firewall/ct_helper.rs
deleted file mode 100644
index 40e4fee..0000000
--- a/proxmox-ve-config/src/firewall/ct_helper.rs
+++ /dev/null
@@ -1,115 +0,0 @@
-use anyhow::{bail, Error};
-use serde::Deserialize;
-use std::collections::HashMap;
-use std::sync::OnceLock;
-
-use crate::firewall::types::address::Family;
-use crate::firewall::types::rule_match::{Ports, Protocol, Tcp, Udp};
-
-#[derive(Clone, Debug, Deserialize)]
-pub struct CtHelperMacroJson {
-    pub v4: Option<bool>,
-    pub v6: Option<bool>,
-    pub name: String,
-    pub tcp: Option<u16>,
-    pub udp: Option<u16>,
-}
-
-impl TryFrom<CtHelperMacroJson> for CtHelperMacro {
-    type Error = Error;
-
-    fn try_from(value: CtHelperMacroJson) -> Result<Self, Self::Error> {
-        if value.tcp.is_none() && value.udp.is_none() {
-            bail!("Neither TCP nor UDP port set in CT helper!");
-        }
-
-        let family = match (value.v4, value.v6) {
-            (Some(true), Some(true)) => None,
-            (Some(true), _) => Some(Family::V4),
-            (_, Some(true)) => Some(Family::V6),
-            _ => bail!("Neither v4 nor v6 set in CT Helper Macro!"),
-        };
-
-        let mut ct_helper = CtHelperMacro {
-            family,
-            name: value.name,
-            tcp: None,
-            udp: None,
-        };
-
-        if let Some(dport) = value.tcp {
-            let ports = Ports::from_u16(None, dport);
-            ct_helper.tcp = Some(Tcp::new(ports).into());
-        }
-
-        if let Some(dport) = value.udp {
-            let ports = Ports::from_u16(None, dport);
-            ct_helper.udp = Some(Udp::new(ports).into());
-        }
-
-        Ok(ct_helper)
-    }
-}
-
-#[derive(Clone, Debug, Deserialize)]
-#[serde(try_from = "CtHelperMacroJson")]
-pub struct CtHelperMacro {
-    family: Option<Family>,
-    name: String,
-    tcp: Option<Protocol>,
-    udp: Option<Protocol>,
-}
-
-impl CtHelperMacro {
-    fn helper_name(&self, protocol: &str) -> String {
-        format!("helper-{}-{protocol}", self.name)
-    }
-
-    pub fn tcp_helper_name(&self) -> String {
-        self.helper_name("tcp")
-    }
-
-    pub fn udp_helper_name(&self) -> String {
-        self.helper_name("udp")
-    }
-
-    pub fn family(&self) -> Option<Family> {
-        self.family
-    }
-
-    pub fn name(&self) -> &str {
-        self.name.as_ref()
-    }
-
-    pub fn tcp(&self) -> Option<&Protocol> {
-        self.tcp.as_ref()
-    }
-
-    pub fn udp(&self) -> Option<&Protocol> {
-        self.udp.as_ref()
-    }
-}
-
-fn hashmap() -> &'static HashMap<String, CtHelperMacro> {
-    const MACROS: &str = include_str!("../../resources/ct_helper.json");
-    static HASHMAP: OnceLock<HashMap<String, CtHelperMacro>> = OnceLock::new();
-
-    HASHMAP.get_or_init(|| {
-        let macro_data: Vec<CtHelperMacro> = match serde_json::from_str(MACROS) {
-            Ok(data) => data,
-            Err(err) => {
-                log::error!("could not load data for ct helpers: {err}");
-                Vec::new()
-            }
-        };
-
-        macro_data
-            .into_iter()
-            .map(|elem| (elem.name.clone(), elem))
-            .collect()
-    })
-}
-
-pub fn get_cthelper(name: &str) -> Option<&'static CtHelperMacro> {
-    hashmap().get(name)
-}
diff --git a/proxmox-ve-config/src/firewall/fw_macros.rs b/proxmox-ve-config/src/firewall/fw_macros.rs
deleted file mode 100644
index 5fa8dab..0000000
--- a/proxmox-ve-config/src/firewall/fw_macros.rs
+++ /dev/null
@@ -1,69 +0,0 @@
-use std::collections::HashMap;
-
-use serde::Deserialize;
-use std::sync::OnceLock;
-
-use crate::firewall::types::rule_match::Protocol;
-
-use super::types::rule_match::RuleOptions;
-
-#[derive(Clone, Debug, Default, Deserialize)]
-struct FwMacroData {
-    #[serde(rename = "desc")]
-    pub description: &'static str,
-    pub code: Vec<RuleOptions>,
-}
-
-#[derive(Clone, Debug, Default)]
-pub struct FwMacro {
-    pub _description: &'static str,
-    pub code: Vec<Protocol>,
-}
-
-fn macros() -> &'static HashMap<String, FwMacro> {
-    const MACROS: &str = include_str!("../../resources/macros.json");
-    static HASHMAP: OnceLock<HashMap<String, FwMacro>> = OnceLock::new();
-
-    HASHMAP.get_or_init(|| {
-        let macro_data: HashMap<String, FwMacroData> = match serde_json::from_str(MACROS) {
-            Ok(m) => m,
-            Err(err) => {
-                log::error!("could not load data for macros: {err}");
-                HashMap::new()
-            }
-        };
-
-        let mut macros = HashMap::new();
-
-        'outer: for (name, data) in macro_data {
-            let mut code = Vec::new();
-
-            for c in data.code {
-                match Protocol::from_options(&c) {
-                    Ok(Some(p)) => code.push(p),
-                    Ok(None) => {
-                        continue 'outer;
-                    }
-                    Err(err) => {
-                        log::error!("could not parse data for macro {name}: {err}");
-                        continue 'outer;
-                    }
-                }
-            }
-
-            macros.insert(
-                name,
-                FwMacro {
-                    _description: data.description,
-                    code,
-                },
-            );
-        }
-
-        macros
-    })
-}
-
-pub fn get_macro(name: &str) -> Option<&'static FwMacro> {
-    macros().get(name)
-}
diff --git a/proxmox-ve-config/src/firewall/guest.rs b/proxmox-ve-config/src/firewall/guest.rs
deleted file mode 100644
index c7e282f..0000000
--- a/proxmox-ve-config/src/firewall/guest.rs
+++ /dev/null
@@ -1,237 +0,0 @@
-use std::collections::BTreeMap;
-use std::io;
-
-use crate::guest::types::Vmid;
-use crate::guest::vm::NetworkConfig;
-
-use crate::firewall::types::alias::{Alias, AliasName};
-use crate::firewall::types::ipset::IpsetScope;
-use crate::firewall::types::log::LogLevel;
-use crate::firewall::types::rule::{Direction, Rule, Verdict};
-use crate::firewall::types::Ipset;
-
-use anyhow::{bail, Error};
-use serde::Deserialize;
-
-use crate::firewall::parse::serde_option_bool;
-
-/// default return value for [`Config::is_enabled()`]
-pub const GUEST_ENABLED_DEFAULT: bool = false;
-/// default return value for [`Config::allow_ndp()`]
-pub const GUEST_ALLOW_NDP_DEFAULT: bool = true;
-/// default return value for [`Config::allow_dhcp()`]
-pub const GUEST_ALLOW_DHCP_DEFAULT: bool = true;
-/// default return value for [`Config::allow_ra()`]
-pub const GUEST_ALLOW_RA_DEFAULT: bool = false;
-/// default return value for [`Config::macfilter()`]
-pub const GUEST_MACFILTER_DEFAULT: bool = true;
-/// default return value for [`Config::ipfilter()`]
-pub const GUEST_IPFILTER_DEFAULT: bool = false;
-/// default return value for [`Config::default_policy()`]
-pub const GUEST_POLICY_IN_DEFAULT: Verdict = Verdict::Drop;
-/// default return value for [`Config::default_policy()`]
-pub const GUEST_POLICY_OUT_DEFAULT: Verdict = Verdict::Accept;
-
-#[derive(Debug, Default, Deserialize)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Options {
-    #[serde(default, with = "serde_option_bool")]
-    dhcp: Option<bool>,
-
-    #[serde(default, with = "serde_option_bool")]
-    enable: Option<bool>,
-
-    #[serde(default, with = "serde_option_bool")]
-    ipfilter: Option<bool>,
-
-    #[serde(default, with = "serde_option_bool")]
-    ndp: Option<bool>,
-
-    #[serde(default, with = "serde_option_bool")]
-    radv: Option<bool>,
-
-    log_level_in: Option<LogLevel>,
-    log_level_out: Option<LogLevel>,
-
-    #[serde(default, with = "serde_option_bool")]
-    macfilter: Option<bool>,
-
-    #[serde(rename = "policy_in")]
-    policy_in: Option<Verdict>,
-
-    #[serde(rename = "policy_out")]
-    policy_out: Option<Verdict>,
-}
-
-#[derive(Debug)]
-pub struct Config {
-    vmid: Vmid,
-
-    /// The interface prefix: "veth" for containers, "tap" for VMs.
-    iface_prefix: &'static str,
-
-    network_config: NetworkConfig,
-    config: super::common::Config<Options>,
-}
-
-impl Config {
-    pub fn parse<T: io::BufRead, U: io::BufRead>(
-        vmid: &Vmid,
-        iface_prefix: &'static str,
-        firewall_input: T,
-        network_input: U,
-    ) -> Result<Self, Error> {
-        let parser_cfg = super::common::ParserConfig {
-            guest_iface_names: true,
-            ipset_scope: Some(IpsetScope::Guest),
-        };
-
-        let config = super::common::Config::parse(firewall_input, &parser_cfg)?;
-        if !config.groups.is_empty() {
-            bail!("guest firewall config cannot declare groups");
-        }
-
-        let network_config = NetworkConfig::parse(network_input)?;
-
-        Ok(Self {
-            vmid: *vmid,
-            iface_prefix,
-            config,
-            network_config,
-        })
-    }
-
-    pub fn vmid(&self) -> Vmid {
-        self.vmid
-    }
-
-    pub fn alias(&self, name: &AliasName) -> Option<&Alias> {
-        self.config.alias(name.name())
-    }
-
-    pub fn iface_name_by_key(&self, key: &str) -> Result<String, Error> {
-        let index = NetworkConfig::index_from_net_key(key)?;
-        Ok(format!("{}{}i{index}", self.iface_prefix, self.vmid))
-    }
-
-    pub fn iface_name_by_index(&self, index: i64) -> String {
-        format!("{}{}i{index}", self.iface_prefix, self.vmid)
-    }
-
-    /// returns the value of the enabled config key or [`GUEST_ENABLED_DEFAULT`] if unset
-    pub fn is_enabled(&self) -> bool {
-        self.config.options.enable.unwrap_or(GUEST_ENABLED_DEFAULT)
-    }
-
-    pub fn rules(&self) -> &[Rule] {
-        &self.config.rules
-    }
-
-    pub fn log_level(&self, dir: Direction) -> LogLevel {
-        match dir {
-            Direction::In => self.config.options.log_level_in.unwrap_or_default(),
-            Direction::Out => self.config.options.log_level_out.unwrap_or_default(),
-        }
-    }
-
-    /// returns the value of the ndp config key or [`GUEST_ALLOW_NDP_DEFAULT`] if unset
-    pub fn allow_ndp(&self) -> bool {
-        self.config.options.ndp.unwrap_or(GUEST_ALLOW_NDP_DEFAULT)
-    }
-
-    /// returns the value of the dhcp config key or [`GUEST_ALLOW_DHCP_DEFAULT`] if unset
-    pub fn allow_dhcp(&self) -> bool {
-        self.config.options.dhcp.unwrap_or(GUEST_ALLOW_DHCP_DEFAULT)
-    }
-
-    /// returns the value of the radv config key or [`GUEST_ALLOW_RA_DEFAULT`] if unset
-    pub fn allow_ra(&self) -> bool {
-        self.config.options.radv.unwrap_or(GUEST_ALLOW_RA_DEFAULT)
-    }
-
-    /// returns the value of the macfilter config key or [`GUEST_MACFILTER_DEFAULT`] if unset
-    pub fn macfilter(&self) -> bool {
-        self.config
-            .options
-            .macfilter
-            .unwrap_or(GUEST_MACFILTER_DEFAULT)
-    }
-
-    /// returns the value of the ipfilter config key or [`GUEST_IPFILTER_DEFAULT`] if unset
-    pub fn ipfilter(&self) -> bool {
-        self.config
-            .options
-            .ipfilter
-            .unwrap_or(GUEST_IPFILTER_DEFAULT)
-    }
-
-    /// returns the value of the policy_in/out config key or
-    /// [`GUEST_POLICY_IN_DEFAULT`] / [`GUEST_POLICY_OUT_DEFAULT`] if unset
-    pub fn default_policy(&self, dir: Direction) -> Verdict {
-        match dir {
-            Direction::In => self
-                .config
-                .options
-                .policy_in
-                .unwrap_or(GUEST_POLICY_IN_DEFAULT),
-            Direction::Out => self
-                .config
-                .options
-                .policy_out
-                .unwrap_or(GUEST_POLICY_OUT_DEFAULT),
-        }
-    }
-
-    pub fn network_config(&self) -> &NetworkConfig {
-        &self.network_config
-    }
-
-    pub fn ipsets(&self) -> &BTreeMap<String, Ipset> {
-        self.config.ipsets()
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_parse_config() {
-        // most of the stuff is already tested in cluster parsing, only testing
-        // guest specific options here
-        const CONFIG: &str = r#"
-[OPTIONS]
-enable: 1
-dhcp: 1
-ipfilter: 0
-log_level_in: emerg
-log_level_out: crit
-macfilter: 0
-ndp:1
-radv:1
-policy_in: REJECT
-policy_out: REJECT
-"#;
-
-        let config = CONFIG.as_bytes();
-        let network_config: Vec<u8> = Vec::new();
-        let config =
-            Config::parse(&Vmid::new(100), "tap", config, network_config.as_slice()).unwrap();
-
-        assert_eq!(
-            config.config.options,
-            Options {
-                dhcp: Some(true),
-                enable: Some(true),
-                ipfilter: Some(false),
-                ndp: Some(true),
-                radv: Some(true),
-                log_level_in: Some(LogLevel::Emergency),
-                log_level_out: Some(LogLevel::Critical),
-                macfilter: Some(false),
-                policy_in: Some(Verdict::Reject),
-                policy_out: Some(Verdict::Reject),
-            }
-        );
-    }
-}
diff --git a/proxmox-ve-config/src/firewall/host.rs b/proxmox-ve-config/src/firewall/host.rs
deleted file mode 100644
index 3de6fad..0000000
--- a/proxmox-ve-config/src/firewall/host.rs
+++ /dev/null
@@ -1,372 +0,0 @@
-use std::io;
-use std::net::IpAddr;
-
-use anyhow::{bail, Error};
-use serde::Deserialize;
-
-use crate::host::utils::{host_ips, network_interface_cidrs};
-use proxmox_sys::nodename;
-
-use crate::firewall::parse;
-use crate::firewall::types::log::LogLevel;
-use crate::firewall::types::rule::Direction;
-use crate::firewall::types::{Alias, Cidr, Rule};
-
-/// default setting for the enabled key
-pub const HOST_ENABLED_DEFAULT: bool = true;
-/// default setting for the nftables key
-pub const HOST_NFTABLES_DEFAULT: bool = false;
-/// default return value for [`Config::allow_ndp()`]
-pub const HOST_ALLOW_NDP_DEFAULT: bool = true;
-/// default return value for [`Config::block_smurfs()`]
-pub const HOST_BLOCK_SMURFS_DEFAULT: bool = true;
-/// default return value for [`Config::block_synflood()`]
-pub const HOST_BLOCK_SYNFLOOD_DEFAULT: bool = false;
-/// default rate limit for synflood rule (packets / second)
-pub const HOST_BLOCK_SYNFLOOD_RATE_DEFAULT: i64 = 200;
-/// default rate limit for synflood rule (packets / second)
-pub const HOST_BLOCK_SYNFLOOD_BURST_DEFAULT: i64 = 1000;
-/// default return value for [`Config::block_invalid_tcp()`]
-pub const HOST_BLOCK_INVALID_TCP_DEFAULT: bool = false;
-/// default return value for [`Config::block_invalid_conntrack()`]
-pub const HOST_BLOCK_INVALID_CONNTRACK: bool = false;
-/// default setting for logging of invalid conntrack entries
-pub const HOST_LOG_INVALID_CONNTRACK: bool = false;
-
-#[derive(Debug, Default, Deserialize)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Options {
-    #[serde(default, with = "parse::serde_option_bool")]
-    enable: Option<bool>,
-
-    #[serde(default, with = "parse::serde_option_bool")]
-    nftables: Option<bool>,
-
-    log_level_in: Option<LogLevel>,
-    log_level_out: Option<LogLevel>,
-
-    #[serde(default, with = "parse::serde_option_bool")]
-    log_nf_conntrack: Option<bool>,
-    #[serde(default, with = "parse::serde_option_bool")]
-    ndp: Option<bool>,
-
-    #[serde(default, with = "parse::serde_option_bool")]
-    nf_conntrack_allow_invalid: Option<bool>,
-
-    // is Option<Vec<>> for easier deserialization
-    #[serde(default, with = "parse::serde_option_conntrack_helpers")]
-    nf_conntrack_helpers: Option<Vec<String>>,
-
-    #[serde(default, with = "parse::serde_option_number")]
-    nf_conntrack_max: Option<i64>,
-    #[serde(default, with = "parse::serde_option_number")]
-    nf_conntrack_tcp_timeout_established: Option<i64>,
-    #[serde(default, with = "parse::serde_option_number")]
-    nf_conntrack_tcp_timeout_syn_recv: Option<i64>,
-
-    #[serde(default, with = "parse::serde_option_bool")]
-    nosmurfs: Option<bool>,
-
-    #[serde(default, with = "parse::serde_option_bool")]
-    protection_synflood: Option<bool>,
-    #[serde(default, with = "parse::serde_option_number")]
-    protection_synflood_burst: Option<i64>,
-    #[serde(default, with = "parse::serde_option_number")]
-    protection_synflood_rate: Option<i64>,
-
-    smurf_log_level: Option<LogLevel>,
-    tcp_flags_log_level: Option<LogLevel>,
-
-    #[serde(default, with = "parse::serde_option_bool")]
-    tcpflags: Option<bool>,
-}
-
-#[derive(Debug, Default)]
-pub struct Config {
-    pub(crate) config: super::common::Config<Options>,
-}
-
-impl Config {
-    pub fn new() -> Self {
-        Self {
-            config: Default::default(),
-        }
-    }
-
-    pub fn parse<R: io::BufRead>(input: R) -> Result<Self, Error> {
-        let config = super::common::Config::parse(input, &Default::default())?;
-
-        if !config.groups.is_empty() {
-            bail!("host firewall config cannot declare groups");
-        }
-
-        if !config.aliases.is_empty() {
-            bail!("host firewall config cannot declare aliases");
-        }
-
-        if !config.ipsets.is_empty() {
-            bail!("host firewall config cannot declare ipsets");
-        }
-
-        Ok(Self { config })
-    }
-
-    pub fn rules(&self) -> &[Rule] {
-        &self.config.rules
-    }
-
-    pub fn management_ips() -> Result<Vec<Cidr>, Error> {
-        let mut management_cidrs = Vec::new();
-
-        for host_ip in host_ips() {
-            for network_interface_cidr in network_interface_cidrs() {
-                match (host_ip, network_interface_cidr) {
-                    (IpAddr::V4(ip), Cidr::Ipv4(cidr)) => {
-                        if cidr.contains_address(&ip) {
-                            management_cidrs.push(network_interface_cidr);
-                        }
-                    }
-                    (IpAddr::V6(ip), Cidr::Ipv6(cidr)) => {
-                        if cidr.contains_address(&ip) {
-                            management_cidrs.push(network_interface_cidr);
-                        }
-                    }
-                    _ => continue,
-                };
-            }
-        }
-
-        Ok(management_cidrs)
-    }
-
-    pub fn hostname() -> &'static str {
-        nodename()
-    }
-
-    pub fn get_alias(&self, name: &str) -> Option<&Alias> {
-        self.config.alias(name)
-    }
-
-    /// returns value of enabled key or [`HOST_ENABLED_DEFAULT`] if unset
-    pub fn is_enabled(&self) -> bool {
-        self.config.options.enable.unwrap_or(HOST_ENABLED_DEFAULT)
-    }
-
-    /// returns value of nftables key or [`HOST_NFTABLES_DEFAULT`] if unset
-    pub fn nftables(&self) -> bool {
-        self.config
-            .options
-            .nftables
-            .unwrap_or(HOST_NFTABLES_DEFAULT)
-    }
-
-    /// returns value of ndp key or [`HOST_ALLOW_NDP_DEFAULT`] if unset
-    pub fn allow_ndp(&self) -> bool {
-        self.config.options.ndp.unwrap_or(HOST_ALLOW_NDP_DEFAULT)
-    }
-
-    /// returns value of nosmurfs key or [`HOST_BLOCK_SMURFS_DEFAULT`] if unset
-    pub fn block_smurfs(&self) -> bool {
-        self.config
-            .options
-            .nosmurfs
-            .unwrap_or(HOST_BLOCK_SMURFS_DEFAULT)
-    }
-
-    /// returns the log level for the smurf protection rule
-    ///
-    /// If there is no log level set, it returns [`LogLevel::default()`]
-    pub fn block_smurfs_log_level(&self) -> LogLevel {
-        self.config.options.smurf_log_level.unwrap_or_default()
-    }
-
-    /// returns value of protection_synflood key or [`HOST_BLOCK_SYNFLOOD_DEFAULT`] if unset
-    pub fn block_synflood(&self) -> bool {
-        self.config
-            .options
-            .protection_synflood
-            .unwrap_or(HOST_BLOCK_SYNFLOOD_DEFAULT)
-    }
-
-    /// returns value of protection_synflood_rate key or [`HOST_BLOCK_SYNFLOOD_RATE_DEFAULT`] if
-    /// unset
-    pub fn synflood_rate(&self) -> i64 {
-        self.config
-            .options
-            .protection_synflood_rate
-            .unwrap_or(HOST_BLOCK_SYNFLOOD_RATE_DEFAULT)
-    }
-
-    /// returns value of protection_synflood_burst key or [`HOST_BLOCK_SYNFLOOD_BURST_DEFAULT`] if
-    /// unset
-    pub fn synflood_burst(&self) -> i64 {
-        self.config
-            .options
-            .protection_synflood_burst
-            .unwrap_or(HOST_BLOCK_SYNFLOOD_BURST_DEFAULT)
-    }
-
-    /// returns value of tcpflags key or [`HOST_BLOCK_INVALID_TCP_DEFAULT`] if unset
-    pub fn block_invalid_tcp(&self) -> bool {
-        self.config
-            .options
-            .tcpflags
-            .unwrap_or(HOST_BLOCK_INVALID_TCP_DEFAULT)
-    }
-
-    /// returns the log level for the block invalid TCP packets rule
-    ///
-    /// If there is no log level set, it returns [`LogLevel::default()`]
-    pub fn block_invalid_tcp_log_level(&self) -> LogLevel {
-        self.config.options.tcp_flags_log_level.unwrap_or_default()
-    }
-
-    /// returns value of nf_conntrack_allow_invalid key or [`HOST_BLOCK_INVALID_CONNTRACK`] if
-    /// unset
-    pub fn block_invalid_conntrack(&self) -> bool {
-        !self
-            .config
-            .options
-            .nf_conntrack_allow_invalid
-            .unwrap_or(HOST_BLOCK_INVALID_CONNTRACK)
-    }
-
-    pub fn nf_conntrack_max(&self) -> Option<i64> {
-        self.config.options.nf_conntrack_max
-    }
-
-    pub fn nf_conntrack_tcp_timeout_established(&self) -> Option<i64> {
-        self.config.options.nf_conntrack_tcp_timeout_established
-    }
-
-    pub fn nf_conntrack_tcp_timeout_syn_recv(&self) -> Option<i64> {
-        self.config.options.nf_conntrack_tcp_timeout_syn_recv
-    }
-
-    /// returns value of log_nf_conntrack key or [`HOST_LOG_INVALID_CONNTRACK`] if unset
-    pub fn log_nf_conntrack(&self) -> bool {
-        self.config
-            .options
-            .log_nf_conntrack
-            .unwrap_or(HOST_LOG_INVALID_CONNTRACK)
-    }
-
-    pub fn conntrack_helpers(&self) -> Option<&Vec<String>> {
-        self.config.options.nf_conntrack_helpers.as_ref()
-    }
-
-    /// returns the log level for the given direction
-    ///
-    /// If there is no log level set it returns [`LogLevel::default()`]
-    pub fn log_level(&self, dir: Direction) -> LogLevel {
-        match dir {
-            Direction::In => self.config.options.log_level_in.unwrap_or_default(),
-            Direction::Out => self.config.options.log_level_out.unwrap_or_default(),
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use crate::firewall::types::{
-        log::LogLevel,
-        rule::{Kind, RuleGroup, Verdict},
-        rule_match::{Ports, Protocol, RuleMatch, Udp},
-    };
-
-    use super::*;
-
-    #[test]
-    fn test_parse_config() {
-        const CONFIG: &str = r#"
-[OPTIONS]
-enable: 1
-nftables: 1
-log_level_in: debug
-log_level_out: emerg
-log_nf_conntrack: 0
-ndp: 1
-nf_conntrack_allow_invalid: yes
-nf_conntrack_helpers: ftp
-nf_conntrack_max: 44000
-nf_conntrack_tcp_timeout_established: 500000
-nf_conntrack_tcp_timeout_syn_recv: 44
-nosmurfs: no
-protection_synflood: 1
-protection_synflood_burst: 2500
-protection_synflood_rate: 300
-smurf_log_level: notice
-tcp_flags_log_level: nolog
-tcpflags: yes
-
-[RULES]
-
-GROUP tgr -i eth0 # acomm
-IN ACCEPT -p udp -dport 33 -sport 22 -log warning
-
-"#;
-
-        let mut config = CONFIG.as_bytes();
-        let config = Config::parse(&mut config).unwrap();
-
-        assert_eq!(
-            config.config.options,
-            Options {
-                enable: Some(true),
-                nftables: Some(true),
-                log_level_in: Some(LogLevel::Debug),
-                log_level_out: Some(LogLevel::Emergency),
-                log_nf_conntrack: Some(false),
-                ndp: Some(true),
-                nf_conntrack_allow_invalid: Some(true),
-                nf_conntrack_helpers: Some(vec!["ftp".to_string()]),
-                nf_conntrack_max: Some(44000),
-                nf_conntrack_tcp_timeout_established: Some(500000),
-                nf_conntrack_tcp_timeout_syn_recv: Some(44),
-                nosmurfs: Some(false),
-                protection_synflood: Some(true),
-                protection_synflood_burst: Some(2500),
-                protection_synflood_rate: Some(300),
-                smurf_log_level: Some(LogLevel::Notice),
-                tcp_flags_log_level: Some(LogLevel::Nolog),
-                tcpflags: Some(true),
-            }
-        );
-
-        assert_eq!(config.config.rules.len(), 2);
-
-        assert_eq!(
-            config.config.rules[0],
-            Rule {
-                disabled: false,
-                comment: Some("acomm".to_string()),
-                kind: Kind::Group(RuleGroup {
-                    group: "tgr".to_string(),
-                    iface: Some("eth0".to_string()),
-                }),
-            },
-        );
-
-        assert_eq!(
-            config.config.rules[1],
-            Rule {
-                disabled: false,
-                comment: None,
-                kind: Kind::Match(RuleMatch {
-                    dir: Direction::In,
-                    verdict: Verdict::Accept,
-                    proto: Some(Protocol::Udp(Udp::new(Ports::from_u16(22, 33)))),
-                    log: Some(LogLevel::Warning),
-                    ..Default::default()
-                }),
-            },
-        );
-
-        Config::parse("[ALIASES]\ntest 127.0.0.1".as_bytes())
-            .expect_err("host config cannot contain aliases");
-
-        Config::parse("[GROUP test]".as_bytes()).expect_err("host config cannot contain groups");
-
-        Config::parse("[IPSET test]".as_bytes()).expect_err("host config cannot contain ipsets");
-    }
-}
diff --git a/proxmox-ve-config/src/firewall/mod.rs b/proxmox-ve-config/src/firewall/mod.rs
deleted file mode 100644
index 2cf57e2..0000000
--- a/proxmox-ve-config/src/firewall/mod.rs
+++ /dev/null
@@ -1,10 +0,0 @@
-pub mod cluster;
-pub mod common;
-pub mod ct_helper;
-pub mod fw_macros;
-pub mod guest;
-pub mod host;
-pub mod ports;
-pub mod types;
-
-pub(crate) mod parse;
diff --git a/proxmox-ve-config/src/firewall/parse.rs b/proxmox-ve-config/src/firewall/parse.rs
deleted file mode 100644
index 7bf00c0..0000000
--- a/proxmox-ve-config/src/firewall/parse.rs
+++ /dev/null
@@ -1,494 +0,0 @@
-use std::fmt;
-
-use anyhow::{bail, format_err, Error};
-
-const NAME_SPECIAL_CHARACTERS: [u8; 2] = [b'-', b'_'];
-
-/// Parses out a "name" which can be alphanumeric and include dashes.
-///
-/// Returns `None` if the name part would be empty.
-///
-/// Returns a tuple with the name and the remainder (not trimmed).
-///
-/// # Examples
-/// ```ignore
-/// assert_eq!(match_name("some-name someremainder"), Some(("some-name", " someremainder")));
-/// assert_eq!(match_name("some-name@someremainder"), Some(("some-name", "@someremainder")));
-/// assert_eq!(match_name(""), None);
-/// assert_eq!(match_name(" someremainder"), None);
-/// ```
-pub fn match_name(line: &str) -> Option<(&str, &str)> {
-    if !line.starts_with(|c: char| c.is_ascii_alphabetic()) {
-        return None;
-    }
-
-    let end = line
-        .as_bytes()
-        .iter()
-        .position(|&b| !(b.is_ascii_alphanumeric() || NAME_SPECIAL_CHARACTERS.contains(&b)));
-
-    let (name, rest) = match end {
-        Some(end) => line.split_at(end),
-        None => (line, ""),
-    };
-
-    if name.is_empty() {
-        None
-    } else {
-        Some((name, rest))
-    }
-}
-
-/// Parses up to the next whitespace character or end of the string.
-///
-/// Returns `None` if the non-whitespace part would be empty.
-///
-/// Returns a tuple containing the parsed section and the *trimmed* remainder.
-pub fn match_non_whitespace(line: &str) -> Option<(&str, &str)> {
-    let (text, rest) = line
-        .as_bytes()
-        .iter()
-        .position(|&b| b.is_ascii_whitespace())
-        .map(|pos| {
-            let (a, b) = line.split_at(pos);
-            (a, b.trim_start())
-        })
-        .unwrap_or((line, ""));
-    if text.is_empty() {
-        None
-    } else {
-        Some((text, rest))
-    }
-}
-
-/// parses out all digits and returns the remainder
-///
-/// returns [`None`] if the digit part would be empty
-///
-/// Returns a tuple with the digits and the remainder (not trimmed).
-pub fn match_digits(line: &str) -> Option<(&str, &str)> {
-    let split_position = line.as_bytes().iter().position(|&b| !b.is_ascii_digit());
-
-    let (digits, rest) = match split_position {
-        Some(pos) => line.split_at(pos),
-        None => (line, ""),
-    };
-
-    if !digits.is_empty() {
-        return Some((digits, rest));
-    }
-
-    None
-}
-
-/// Separate a `key: value` line, trimming whitespace.
-///
-/// Returns `None` if the `key` would be empty.
-pub fn split_key_value(line: &str) -> Option<(&str, &str)> {
-    line.split_once(':')
-        .map(|(key, value)| (key.trim(), value.trim()))
-}
-
-/// Parse a boolean.
-///
-/// values that parse as [`false`]: 0, false, off, no
-/// values that parse as [`true`]: 1, true, on, yes
-///
-/// # Examples
-/// ```ignore
-/// assert_eq!(parse_bool("false"), Ok(false));
-/// assert_eq!(parse_bool("on"), Ok(true));
-/// assert!(parse_bool("proxmox").is_err());
-/// ```
-pub fn parse_bool(value: &str) -> Result<bool, Error> {
-    Ok(
-        if value == "0"
-            || value.eq_ignore_ascii_case("false")
-            || value.eq_ignore_ascii_case("off")
-            || value.eq_ignore_ascii_case("no")
-        {
-            false
-        } else if value == "1"
-            || value.eq_ignore_ascii_case("true")
-            || value.eq_ignore_ascii_case("on")
-            || value.eq_ignore_ascii_case("yes")
-        {
-            true
-        } else {
-            bail!("not a boolean: {value:?}");
-        },
-    )
-}
-
-/// Parse the *remainder* of a section line, that is `<whitespace>NAME] #optional comment`.
-/// The `kind` parameter is used for error messages and should be the section type.
-///
-/// Return the name and the optional comment.
-pub fn parse_named_section_tail<'a>(
-    kind: &'static str,
-    line: &'a str,
-) -> Result<(&'a str, Option<&'a str>), Error> {
-    if line.is_empty() || !line.as_bytes()[0].is_ascii_whitespace() {
-        bail!("incomplete {kind} section");
-    }
-
-    let line = line.trim_start();
-    let (name, line) = match_name(line)
-        .ok_or_else(|| format_err!("expected a name for the {kind} at {line:?}"))?;
-
-    let line = line
-        .strip_prefix(']')
-        .ok_or_else(|| format_err!("expected closing ']' in {kind} section header"))?
-        .trim_start();
-
-    Ok(match line.strip_prefix('#') {
-        Some(comment) => (name, Some(comment.trim())),
-        None if !line.is_empty() => bail!("trailing characters after {kind} section: {line:?}"),
-        None => (name, None),
-    })
-}
-
-// parses a number from a string OR number
-pub mod serde_option_number {
-    use std::fmt;
-
-    use serde::de::{Deserializer, Error, Visitor};
-
-    pub fn deserialize<'de, D: Deserializer<'de>>(
-        deserializer: D,
-    ) -> Result<Option<i64>, D::Error> {
-        struct V;
-
-        impl<'de> Visitor<'de> for V {
-            type Value = Option<i64>;
-
-            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
-                f.write_str("a numerical value")
-            }
-
-            fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
-                v.parse().map_err(E::custom).map(Some)
-            }
-
-            fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
-                Ok(None)
-            }
-
-            fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
-            where
-                D: Deserializer<'de>,
-            {
-                deserializer.deserialize_any(self)
-            }
-        }
-
-        deserializer.deserialize_any(V)
-    }
-}
-
-// parses a bool from a string OR bool
-pub mod serde_option_bool {
-    use std::fmt;
-
-    use serde::de::{Deserializer, Error, Visitor};
-
-    pub fn deserialize<'de, D: Deserializer<'de>>(
-        deserializer: D,
-    ) -> Result<Option<bool>, D::Error> {
-        struct V;
-
-        impl<'de> Visitor<'de> for V {
-            type Value = Option<bool>;
-
-            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
-                f.write_str("a boolean-like value")
-            }
-
-            fn visit_bool<E: Error>(self, v: bool) -> Result<Self::Value, E> {
-                Ok(Some(v))
-            }
-
-            fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
-                super::parse_bool(v).map_err(E::custom).map(Some)
-            }
-
-            fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
-                Ok(None)
-            }
-
-            fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
-            where
-                D: Deserializer<'de>,
-            {
-                deserializer.deserialize_any(self)
-            }
-        }
-
-        deserializer.deserialize_any(V)
-    }
-}
-
-// parses a comma_separated list of strings
-pub mod serde_option_conntrack_helpers {
-    use std::fmt;
-
-    use serde::de::{Deserializer, Error, Visitor};
-
-    pub fn deserialize<'de, D: Deserializer<'de>>(
-        deserializer: D,
-    ) -> Result<Option<Vec<String>>, D::Error> {
-        struct V;
-
-        impl<'de> Visitor<'de> for V {
-            type Value = Option<Vec<String>>;
-
-            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
-                f.write_str("A list of conntrack helpers")
-            }
-
-            fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
-                if v.is_empty() {
-                    return Ok(None);
-                }
-
-                Ok(Some(v.split(',').map(String::from).collect()))
-            }
-
-            fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
-                Ok(None)
-            }
-
-            fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
-            where
-                D: Deserializer<'de>,
-            {
-                deserializer.deserialize_any(self)
-            }
-        }
-
-        deserializer.deserialize_any(V)
-    }
-}
-
-// parses a log_ratelimit string: '[enable=]<1|0> [,burst=<integer>] [,rate=<rate>]'
-pub mod serde_option_log_ratelimit {
-    use std::fmt;
-
-    use serde::de::{Deserializer, Error, Visitor};
-
-    use crate::firewall::types::log::LogRateLimit;
-
-    pub fn deserialize<'de, D: Deserializer<'de>>(
-        deserializer: D,
-    ) -> Result<Option<LogRateLimit>, D::Error> {
-        struct V;
-
-        impl<'de> Visitor<'de> for V {
-            type Value = Option<LogRateLimit>;
-
-            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
-                f.write_str("a boolean-like value")
-            }
-
-            fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
-                v.parse().map_err(E::custom).map(Some)
-            }
-
-            fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
-                Ok(None)
-            }
-
-            fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
-            where
-                D: Deserializer<'de>,
-            {
-                deserializer.deserialize_any(self)
-            }
-        }
-
-        deserializer.deserialize_any(V)
-    }
-}
-
-/// `&str` deserializer which also accepts an `Option`.
-///
-/// Serde's `StringDeserializer` does not.
-#[derive(Clone, Copy, Debug)]
-pub struct SomeStrDeserializer<'a, E>(serde::de::value::StrDeserializer<'a, E>);
-
-impl<'de, 'a, E> serde::de::Deserializer<'de> for SomeStrDeserializer<'a, E>
-where
-    E: serde::de::Error,
-{
-    type Error = E;
-
-    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        self.0.deserialize_any(visitor)
-    }
-
-    fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        visitor.visit_some(self.0)
-    }
-
-    fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        self.0.deserialize_str(visitor)
-    }
-
-    fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        self.0.deserialize_string(visitor)
-    }
-
-    fn deserialize_enum<V>(
-        self,
-        _name: &str,
-        _variants: &'static [&'static str],
-        visitor: V,
-    ) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        visitor.visit_enum(self.0)
-    }
-
-    serde::forward_to_deserialize_any! {
-        bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char
-        bytes byte_buf unit unit_struct newtype_struct seq tuple
-        tuple_struct map struct identifier ignored_any
-    }
-}
-
-/// `&str` wrapper which implements `IntoDeserializer` via `SomeStrDeserializer`.
-#[derive(Clone, Debug)]
-pub struct SomeStr<'a>(pub &'a str);
-
-impl<'a> From<&'a str> for SomeStr<'a> {
-    fn from(s: &'a str) -> Self {
-        Self(s)
-    }
-}
-
-impl<'de, 'a, E> serde::de::IntoDeserializer<'de, E> for SomeStr<'a>
-where
-    E: serde::de::Error,
-{
-    type Deserializer = SomeStrDeserializer<'a, E>;
-
-    fn into_deserializer(self) -> Self::Deserializer {
-        SomeStrDeserializer(self.0.into_deserializer())
-    }
-}
-
-/// `String` deserializer which also accepts an `Option`.
-///
-/// Serde's `StringDeserializer` does not.
-#[derive(Clone, Debug)]
-pub struct SomeStringDeserializer<E>(serde::de::value::StringDeserializer<E>);
-
-impl<'de, E> serde::de::Deserializer<'de> for SomeStringDeserializer<E>
-where
-    E: serde::de::Error,
-{
-    type Error = E;
-
-    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        self.0.deserialize_any(visitor)
-    }
-
-    fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        visitor.visit_some(self.0)
-    }
-
-    fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        self.0.deserialize_str(visitor)
-    }
-
-    fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        self.0.deserialize_string(visitor)
-    }
-
-    fn deserialize_enum<V>(
-        self,
-        _name: &str,
-        _variants: &'static [&'static str],
-        visitor: V,
-    ) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        visitor.visit_enum(self.0)
-    }
-
-    serde::forward_to_deserialize_any! {
-        bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char
-        bytes byte_buf unit unit_struct newtype_struct seq tuple
-        tuple_struct map struct identifier ignored_any
-    }
-}
-
-/// `&str` wrapper which implements `IntoDeserializer` via `SomeStringDeserializer`.
-#[derive(Clone, Debug)]
-pub struct SomeString(pub String);
-
-impl From<&str> for SomeString {
-    fn from(s: &str) -> Self {
-        Self::from(s.to_string())
-    }
-}
-
-impl From<String> for SomeString {
-    fn from(s: String) -> Self {
-        Self(s)
-    }
-}
-
-impl<'de, E> serde::de::IntoDeserializer<'de, E> for SomeString
-where
-    E: serde::de::Error,
-{
-    type Deserializer = SomeStringDeserializer<E>;
-
-    fn into_deserializer(self) -> Self::Deserializer {
-        SomeStringDeserializer(self.0.into_deserializer())
-    }
-}
-
-#[derive(Debug)]
-pub struct SerdeStringError(String);
-
-impl std::error::Error for SerdeStringError {}
-
-impl fmt::Display for SerdeStringError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        f.write_str(&self.0)
-    }
-}
-
-impl serde::de::Error for SerdeStringError {
-    fn custom<T: fmt::Display>(msg: T) -> Self {
-        Self(msg.to_string())
-    }
-}
diff --git a/proxmox-ve-config/src/firewall/ports.rs b/proxmox-ve-config/src/firewall/ports.rs
deleted file mode 100644
index 9d5d1be..0000000
--- a/proxmox-ve-config/src/firewall/ports.rs
+++ /dev/null
@@ -1,80 +0,0 @@
-use anyhow::{format_err, Error};
-use std::sync::OnceLock;
-
-#[derive(Default)]
-struct NamedPorts {
-    ports: std::collections::HashMap<String, u16>,
-}
-
-impl NamedPorts {
-    fn new() -> Self {
-        use std::io::BufRead;
-
-        log::trace!("loading /etc/services");
-
-        let mut this = Self::default();
-
-        let file = match std::fs::File::open("/etc/services") {
-            Ok(file) => file,
-            Err(_) => return this,
-        };
-
-        for line in std::io::BufReader::new(file).lines() {
-            let line = match line {
-                Ok(line) => line,
-                Err(_) => break,
-            };
-
-            let line = line.trim_start();
-
-            if line.is_empty() || line.starts_with('#') {
-                continue;
-            }
-
-            let mut parts = line.split_ascii_whitespace();
-
-            let name = match parts.next() {
-                None => continue,
-                Some(name) => name.to_string(),
-            };
-
-            let proto: u16 = match parts.next() {
-                None => continue,
-                Some(proto) => match proto.split('/').next() {
-                    None => continue,
-                    Some(num) => match num.parse() {
-                        Ok(num) => num,
-                        Err(_) => continue,
-                    },
-                },
-            };
-
-            this.ports.insert(name, proto);
-            for alias in parts {
-                if alias.starts_with('#') {
-                    break;
-                }
-                this.ports.insert(alias.to_string(), proto);
-            }
-        }
-
-        this
-    }
-
-    fn find(&self, name: &str) -> Option<u16> {
-        self.ports.get(name).copied()
-    }
-}
-
-fn named_ports() -> &'static NamedPorts {
-    static NAMED_PORTS: OnceLock<NamedPorts> = OnceLock::new();
-
-    NAMED_PORTS.get_or_init(NamedPorts::new)
-}
-
-/// Parse a named port with the help of `/etc/services`.
-pub fn parse_named_port(name: &str) -> Result<u16, Error> {
-    named_ports()
-        .find(name)
-        .ok_or_else(|| format_err!("unknown port name {name:?}"))
-}
diff --git a/proxmox-ve-config/src/firewall/types/address.rs b/proxmox-ve-config/src/firewall/types/address.rs
deleted file mode 100644
index e48ac1b..0000000
--- a/proxmox-ve-config/src/firewall/types/address.rs
+++ /dev/null
@@ -1,615 +0,0 @@
-use std::fmt;
-use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
-use std::ops::Deref;
-
-use anyhow::{bail, format_err, Error};
-use serde_with::DeserializeFromStr;
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub enum Family {
-    V4,
-    V6,
-}
-
-impl fmt::Display for Family {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self {
-            Family::V4 => f.write_str("Ipv4"),
-            Family::V6 => f.write_str("Ipv6"),
-        }
-    }
-}
-
-#[derive(Clone, Copy, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum Cidr {
-    Ipv4(Ipv4Cidr),
-    Ipv6(Ipv6Cidr),
-}
-
-impl Cidr {
-    pub fn new_v4(addr: impl Into<Ipv4Addr>, mask: u8) -> Result<Self, Error> {
-        Ok(Cidr::Ipv4(Ipv4Cidr::new(addr, mask)?))
-    }
-
-    pub fn new_v6(addr: impl Into<Ipv6Addr>, mask: u8) -> Result<Self, Error> {
-        Ok(Cidr::Ipv6(Ipv6Cidr::new(addr, mask)?))
-    }
-
-    pub const fn family(&self) -> Family {
-        match self {
-            Cidr::Ipv4(_) => Family::V4,
-            Cidr::Ipv6(_) => Family::V6,
-        }
-    }
-
-    pub fn is_ipv4(&self) -> bool {
-        matches!(self, Cidr::Ipv4(_))
-    }
-
-    pub fn is_ipv6(&self) -> bool {
-        matches!(self, Cidr::Ipv6(_))
-    }
-}
-
-impl fmt::Display for Cidr {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self {
-            Self::Ipv4(ip) => f.write_str(ip.to_string().as_str()),
-            Self::Ipv6(ip) => f.write_str(ip.to_string().as_str()),
-        }
-    }
-}
-
-impl std::str::FromStr for Cidr {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Error> {
-        if let Ok(ip) = s.parse::<Ipv4Cidr>() {
-            return Ok(Cidr::Ipv4(ip));
-        }
-
-        if let Ok(ip) = s.parse::<Ipv6Cidr>() {
-            return Ok(Cidr::Ipv6(ip));
-        }
-
-        bail!("invalid ip address or CIDR: {s:?}");
-    }
-}
-
-impl From<Ipv4Cidr> for Cidr {
-    fn from(cidr: Ipv4Cidr) -> Self {
-        Cidr::Ipv4(cidr)
-    }
-}
-
-impl From<Ipv6Cidr> for Cidr {
-    fn from(cidr: Ipv6Cidr) -> Self {
-        Cidr::Ipv6(cidr)
-    }
-}
-
-const IPV4_LENGTH: u8 = 32;
-
-#[derive(Clone, Copy, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Ipv4Cidr {
-    addr: Ipv4Addr,
-    mask: u8,
-}
-
-impl Ipv4Cidr {
-    pub fn new(addr: impl Into<Ipv4Addr>, mask: u8) -> Result<Self, Error> {
-        if mask > 32 {
-            bail!("mask out of range for ipv4 cidr ({mask})");
-        }
-
-        Ok(Self {
-            addr: addr.into(),
-            mask,
-        })
-    }
-
-    pub fn contains_address(&self, other: &Ipv4Addr) -> bool {
-        let bits = u32::from_be_bytes(self.addr.octets());
-        let other_bits = u32::from_be_bytes(other.octets());
-
-        let shift_amount: u32 = IPV4_LENGTH.saturating_sub(self.mask).into();
-
-        bits.checked_shr(shift_amount).unwrap_or(0)
-            == other_bits.checked_shr(shift_amount).unwrap_or(0)
-    }
-
-    pub fn address(&self) -> &Ipv4Addr {
-        &self.addr
-    }
-
-    pub fn mask(&self) -> u8 {
-        self.mask
-    }
-}
-
-impl<T: Into<Ipv4Addr>> From<T> for Ipv4Cidr {
-    fn from(value: T) -> Self {
-        Self {
-            addr: value.into(),
-            mask: 32,
-        }
-    }
-}
-
-impl std::str::FromStr for Ipv4Cidr {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Error> {
-        Ok(match s.find('/') {
-            None => Self {
-                addr: s.parse()?,
-                mask: 32,
-            },
-            Some(pos) => {
-                let mask: u8 = s[(pos + 1)..]
-                    .parse()
-                    .map_err(|_| format_err!("invalid mask in ipv4 cidr: {s:?}"))?;
-
-                Self::new(s[..pos].parse::<Ipv4Addr>()?, mask)?
-            }
-        })
-    }
-}
-
-impl fmt::Display for Ipv4Cidr {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(f, "{}/{}", &self.addr, self.mask)
-    }
-}
-
-const IPV6_LENGTH: u8 = 128;
-
-#[derive(Clone, Copy, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Ipv6Cidr {
-    addr: Ipv6Addr,
-    mask: u8,
-}
-
-impl Ipv6Cidr {
-    pub fn new(addr: impl Into<Ipv6Addr>, mask: u8) -> Result<Self, Error> {
-        if mask > IPV6_LENGTH {
-            bail!("mask out of range for ipv6 cidr");
-        }
-
-        Ok(Self {
-            addr: addr.into(),
-            mask,
-        })
-    }
-
-    pub fn contains_address(&self, other: &Ipv6Addr) -> bool {
-        let bits = u128::from_be_bytes(self.addr.octets());
-        let other_bits = u128::from_be_bytes(other.octets());
-
-        let shift_amount: u32 = IPV6_LENGTH.saturating_sub(self.mask).into();
-
-        bits.checked_shr(shift_amount).unwrap_or(0)
-            == other_bits.checked_shr(shift_amount).unwrap_or(0)
-    }
-
-    pub fn address(&self) -> &Ipv6Addr {
-        &self.addr
-    }
-
-    pub fn mask(&self) -> u8 {
-        self.mask
-    }
-}
-
-impl std::str::FromStr for Ipv6Cidr {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Error> {
-        Ok(match s.find('/') {
-            None => Self {
-                addr: s.parse()?,
-                mask: 128,
-            },
-            Some(pos) => {
-                let mask: u8 = s[(pos + 1)..]
-                    .parse()
-                    .map_err(|_| format_err!("invalid mask in ipv6 cidr: {s:?}"))?;
-
-                Self::new(s[..pos].parse::<Ipv6Addr>()?, mask)?
-            }
-        })
-    }
-}
-
-impl fmt::Display for Ipv6Cidr {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(f, "{}/{}", &self.addr, self.mask)
-    }
-}
-
-impl<T: Into<Ipv6Addr>> From<T> for Ipv6Cidr {
-    fn from(addr: T) -> Self {
-        Self {
-            addr: addr.into(),
-            mask: 128,
-        }
-    }
-}
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum IpEntry {
-    Cidr(Cidr),
-    Range(IpAddr, IpAddr),
-}
-
-impl std::str::FromStr for IpEntry {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Error> {
-        if s.is_empty() {
-            bail!("Empty IP specification!")
-        }
-
-        let entries: Vec<&str> = s
-            .split('-')
-            .take(3) // so we can check whether there are too many
-            .collect();
-
-        match entries.as_slice() {
-            [cidr] => Ok(IpEntry::Cidr(cidr.parse()?)),
-            [beg, end] => {
-                if let Ok(beg) = beg.parse::<Ipv4Addr>() {
-                    if let Ok(end) = end.parse::<Ipv4Addr>() {
-                        if beg < end {
-                            return Ok(IpEntry::Range(beg.into(), end.into()));
-                        }
-
-                        bail!("start address is greater than end address!");
-                    }
-                }
-
-                if let Ok(beg) = beg.parse::<Ipv6Addr>() {
-                    if let Ok(end) = end.parse::<Ipv6Addr>() {
-                        if beg < end {
-                            return Ok(IpEntry::Range(beg.into(), end.into()));
-                        }
-
-                        bail!("start address is greater than end address!");
-                    }
-                }
-
-                bail!("start and end are not valid IP addresses of the same type!")
-            }
-            _ => bail!("Invalid amount of elements in IpEntry!"),
-        }
-    }
-}
-
-impl fmt::Display for IpEntry {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self {
-            Self::Cidr(ip) => write!(f, "{ip}"),
-            Self::Range(beg, end) => write!(f, "{beg}-{end}"),
-        }
-    }
-}
-
-impl IpEntry {
-    fn family(&self) -> Family {
-        match self {
-            Self::Cidr(cidr) => cidr.family(),
-            Self::Range(start, end) => {
-                if start.is_ipv4() && end.is_ipv4() {
-                    return Family::V4;
-                }
-
-                if start.is_ipv6() && end.is_ipv6() {
-                    return Family::V6;
-                }
-
-                // should never be reached due to constructors validating that
-                // start type == end type
-                unreachable!("invalid IP entry")
-            }
-        }
-    }
-}
-
-impl From<Cidr> for IpEntry {
-    fn from(value: Cidr) -> Self {
-        IpEntry::Cidr(value)
-    }
-}
-
-#[derive(Clone, Debug, DeserializeFromStr)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct IpList {
-    // guaranteed to have the same family
-    entries: Vec<IpEntry>,
-    family: Family,
-}
-
-impl Deref for IpList {
-    type Target = Vec<IpEntry>;
-
-    fn deref(&self) -> &Self::Target {
-        &self.entries
-    }
-}
-
-impl<T: Into<IpEntry>> From<T> for IpList {
-    fn from(value: T) -> Self {
-        let entry = value.into();
-
-        Self {
-            family: entry.family(),
-            entries: vec![entry],
-        }
-    }
-}
-
-impl std::str::FromStr for IpList {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Error> {
-        if s.is_empty() {
-            bail!("Empty IP specification!")
-        }
-
-        let mut entries = Vec::new();
-        let mut current_family = None;
-
-        for element in s.split(',') {
-            let entry: IpEntry = element.parse()?;
-
-            if let Some(family) = current_family {
-                if family != entry.family() {
-                    bail!("Incompatible families in IPList!")
-                }
-            } else {
-                current_family = Some(entry.family());
-            }
-
-            entries.push(entry);
-        }
-
-        if entries.is_empty() {
-            bail!("empty ip list")
-        }
-
-        Ok(IpList {
-            entries,
-            family: current_family.unwrap(), // must be set due to length check above
-        })
-    }
-}
-
-impl IpList {
-    pub fn new(entries: Vec<IpEntry>) -> Result<Self, Error> {
-        let family = entries.iter().try_fold(None, |result, entry| {
-            if let Some(family) = result {
-                if entry.family() != family {
-                    bail!("non-matching families in entries list");
-                }
-
-                Ok(Some(family))
-            } else {
-                Ok(Some(entry.family()))
-            }
-        })?;
-
-        if let Some(family) = family {
-            return Ok(Self { entries, family });
-        }
-
-        bail!("no elements in ip list entries");
-    }
-
-    pub fn family(&self) -> Family {
-        self.family
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use std::net::{Ipv4Addr, Ipv6Addr};
-
-    #[test]
-    fn test_v4_cidr() {
-        let mut cidr: Ipv4Cidr = "0.0.0.0/0".parse().expect("valid IPv4 CIDR");
-
-        assert_eq!(cidr.addr, Ipv4Addr::new(0, 0, 0, 0));
-        assert_eq!(cidr.mask, 0);
-
-        assert!(cidr.contains_address(&Ipv4Addr::new(0, 0, 0, 0)));
-        assert!(cidr.contains_address(&Ipv4Addr::new(255, 255, 255, 255)));
-
-        cidr = "192.168.100.1".parse().expect("valid IPv4 CIDR");
-
-        assert_eq!(cidr.addr, Ipv4Addr::new(192, 168, 100, 1));
-        assert_eq!(cidr.mask, 32);
-
-        assert!(cidr.contains_address(&Ipv4Addr::new(192, 168, 100, 1)));
-        assert!(!cidr.contains_address(&Ipv4Addr::new(192, 168, 100, 2)));
-        assert!(!cidr.contains_address(&Ipv4Addr::new(192, 168, 100, 0)));
-
-        cidr = "10.100.5.0/24".parse().expect("valid IPv4 CIDR");
-
-        assert_eq!(cidr.mask, 24);
-
-        assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 0)));
-        assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 1)));
-        assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 100)));
-        assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 255)));
-        assert!(!cidr.contains_address(&Ipv4Addr::new(10, 100, 4, 255)));
-        assert!(!cidr.contains_address(&Ipv4Addr::new(10, 100, 6, 0)));
-
-        "0.0.0.0/-1".parse::<Ipv4Cidr>().unwrap_err();
-        "0.0.0.0/33".parse::<Ipv4Cidr>().unwrap_err();
-        "256.256.256.256/10".parse::<Ipv4Cidr>().unwrap_err();
-
-        "fe80::1/64".parse::<Ipv4Cidr>().unwrap_err();
-        "qweasd".parse::<Ipv4Cidr>().unwrap_err();
-        "".parse::<Ipv4Cidr>().unwrap_err();
-    }
-
-    #[test]
-    fn test_v6_cidr() {
-        let mut cidr: Ipv6Cidr = "abab::1/64".parse().expect("valid IPv6 CIDR");
-
-        assert_eq!(cidr.addr, Ipv6Addr::new(0xABAB, 0, 0, 0, 0, 0, 0, 1));
-        assert_eq!(cidr.mask, 64);
-
-        assert!(cidr.contains_address(&Ipv6Addr::new(0xABAB, 0, 0, 0, 0, 0, 0, 0)));
-        assert!(cidr.contains_address(&Ipv6Addr::new(
-            0xABAB, 0, 0, 0, 0xAAAA, 0xAAAA, 0xAAAA, 0xAAAA
-        )));
-        assert!(cidr.contains_address(&Ipv6Addr::new(
-            0xABAB, 0, 0, 0, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
-        )));
-        assert!(!cidr.contains_address(&Ipv6Addr::new(0xABAB, 0, 0, 1, 0, 0, 0, 0)));
-        assert!(!cidr.contains_address(&Ipv6Addr::new(
-            0xABAA, 0, 0, 0, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
-        )));
-
-        cidr = "eeee::1".parse().expect("valid IPv6 CIDR");
-
-        assert_eq!(cidr.mask, 128);
-
-        assert!(cidr.contains_address(&Ipv6Addr::new(0xEEEE, 0, 0, 0, 0, 0, 0, 1)));
-        assert!(!cidr.contains_address(&Ipv6Addr::new(
-            0xEEED, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
-        )));
-        assert!(!cidr.contains_address(&Ipv6Addr::new(0xEEEE, 0, 0, 0, 0, 0, 0, 0)));
-
-        "eeee::1/-1".parse::<Ipv6Cidr>().unwrap_err();
-        "eeee::1/129".parse::<Ipv6Cidr>().unwrap_err();
-        "gggg::1/64".parse::<Ipv6Cidr>().unwrap_err();
-
-        "192.168.0.1".parse::<Ipv6Cidr>().unwrap_err();
-        "qweasd".parse::<Ipv6Cidr>().unwrap_err();
-        "".parse::<Ipv6Cidr>().unwrap_err();
-    }
-
-    #[test]
-    fn test_parse_ip_entry() {
-        let mut entry: IpEntry = "10.0.0.1".parse().expect("valid IP entry");
-
-        assert_eq!(entry, Cidr::new_v4([10, 0, 0, 1], 32).unwrap().into());
-
-        entry = "10.0.0.0/16".parse().expect("valid IP entry");
-
-        assert_eq!(entry, Cidr::new_v4([10, 0, 0, 0], 16).unwrap().into());
-
-        entry = "192.168.0.1-192.168.99.255"
-            .parse()
-            .expect("valid IP entry");
-
-        assert_eq!(
-            entry,
-            IpEntry::Range([192, 168, 0, 1].into(), [192, 168, 99, 255].into())
-        );
-
-        entry = "fe80::1".parse().expect("valid IP entry");
-
-        assert_eq!(
-            entry,
-            Cidr::new_v6([0xFE80, 0, 0, 0, 0, 0, 0, 1], 128)
-                .unwrap()
-                .into()
-        );
-
-        entry = "fe80::1/48".parse().expect("valid IP entry");
-
-        assert_eq!(
-            entry,
-            Cidr::new_v6([0xFE80, 0, 0, 0, 0, 0, 0, 1], 48)
-                .unwrap()
-                .into()
-        );
-
-        entry = "fd80::1-fd80::ffff".parse().expect("valid IP entry");
-
-        assert_eq!(
-            entry,
-            IpEntry::Range(
-                [0xFD80, 0, 0, 0, 0, 0, 0, 1].into(),
-                [0xFD80, 0, 0, 0, 0, 0, 0, 0xFFFF].into(),
-            )
-        );
-
-        "192.168.100.0-192.168.99.255"
-            .parse::<IpEntry>()
-            .unwrap_err();
-        "192.168.100.0-fe80::1".parse::<IpEntry>().unwrap_err();
-        "192.168.100.0-192.168.200.0/16"
-            .parse::<IpEntry>()
-            .unwrap_err();
-        "192.168.100.0-192.168.200.0-192.168.250.0"
-            .parse::<IpEntry>()
-            .unwrap_err();
-        "qweasd".parse::<IpEntry>().unwrap_err();
-    }
-
-    #[test]
-    fn test_parse_ip_list() {
-        let mut ip_list: IpList = "192.168.0.1,192.168.100.0/24,172.16.0.0-172.32.255.255"
-            .parse()
-            .expect("valid IP list");
-
-        assert_eq!(
-            ip_list,
-            IpList {
-                entries: vec![
-                    IpEntry::Cidr(Cidr::new_v4([192, 168, 0, 1], 32).unwrap()),
-                    IpEntry::Cidr(Cidr::new_v4([192, 168, 100, 0], 24).unwrap()),
-                    IpEntry::Range([172, 16, 0, 0].into(), [172, 32, 255, 255].into()),
-                ],
-                family: Family::V4,
-            }
-        );
-
-        ip_list = "fe80::1/64".parse().expect("valid IP list");
-
-        assert_eq!(
-            ip_list,
-            IpList {
-                entries: vec![IpEntry::Cidr(
-                    Cidr::new_v6([0xFE80, 0, 0, 0, 0, 0, 0, 1], 64).unwrap()
-                ),],
-                family: Family::V6,
-            }
-        );
-
-        "192.168.0.1,fe80::1".parse::<IpList>().unwrap_err();
-
-        "".parse::<IpList>().unwrap_err();
-        "proxmox".parse::<IpList>().unwrap_err();
-    }
-
-    #[test]
-    fn test_construct_ip_list() {
-        let mut ip_list = IpList::new(vec![Cidr::new_v4([10, 0, 0, 0], 8).unwrap().into()])
-            .expect("valid ip list");
-
-        assert_eq!(ip_list.family(), Family::V4);
-
-        ip_list =
-            IpList::new(vec![Cidr::new_v6([0x000; 8], 8).unwrap().into()]).expect("valid ip list");
-
-        assert_eq!(ip_list.family(), Family::V6);
-
-        IpList::new(vec![]).expect_err("empty ip list is invalid");
-
-        IpList::new(vec![
-            Cidr::new_v4([10, 0, 0, 0], 8).unwrap().into(),
-            Cidr::new_v6([0x0000; 8], 8).unwrap().into(),
-        ])
-        .expect_err("cannot mix ip families in ip list");
-    }
-}
diff --git a/proxmox-ve-config/src/firewall/types/alias.rs b/proxmox-ve-config/src/firewall/types/alias.rs
deleted file mode 100644
index e6aa30d..0000000
--- a/proxmox-ve-config/src/firewall/types/alias.rs
+++ /dev/null
@@ -1,174 +0,0 @@
-use std::fmt::Display;
-use std::str::FromStr;
-
-use anyhow::{bail, format_err, Error};
-use serde_with::DeserializeFromStr;
-
-use crate::firewall::parse::{match_name, match_non_whitespace};
-use crate::firewall::types::address::Cidr;
-
-#[derive(Debug, Clone)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum AliasScope {
-    Datacenter,
-    Guest,
-}
-
-impl FromStr for AliasScope {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        Ok(match s {
-            "dc" => AliasScope::Datacenter,
-            "guest" => AliasScope::Guest,
-            _ => bail!("invalid scope for alias: {s}"),
-        })
-    }
-}
-
-impl Display for AliasScope {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.write_str(match self {
-            AliasScope::Datacenter => "dc",
-            AliasScope::Guest => "guest",
-        })
-    }
-}
-
-#[derive(Debug, Clone, DeserializeFromStr)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct AliasName {
-    scope: AliasScope,
-    name: String,
-}
-
-impl Display for AliasName {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.write_fmt(format_args!("{}/{}", self.scope, self.name))
-    }
-}
-
-impl FromStr for AliasName {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        match s.split_once('/') {
-            Some((prefix, name)) if !name.is_empty() => Ok(Self {
-                scope: prefix.parse()?,
-                name: name.to_string(),
-            }),
-            _ => {
-                bail!("Invalid Alias name!")
-            }
-        }
-    }
-}
-
-impl AliasName {
-    pub fn new(scope: AliasScope, name: impl Into<String>) -> Self {
-        Self {
-            scope,
-            name: name.into(),
-        }
-    }
-
-    pub fn name(&self) -> &str {
-        &self.name
-    }
-
-    pub fn scope(&self) -> &AliasScope {
-        &self.scope
-    }
-}
-
-#[derive(Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Alias {
-    name: String,
-    address: Cidr,
-    comment: Option<String>,
-}
-
-impl Alias {
-    pub fn new(
-        name: impl Into<String>,
-        address: impl Into<Cidr>,
-        comment: impl Into<Option<String>>,
-    ) -> Self {
-        Self {
-            name: name.into(),
-            address: address.into(),
-            comment: comment.into(),
-        }
-    }
-
-    pub fn name(&self) -> &str {
-        &self.name
-    }
-
-    pub fn address(&self) -> &Cidr {
-        &self.address
-    }
-
-    pub fn comment(&self) -> Option<&str> {
-        self.comment.as_deref()
-    }
-}
-
-impl FromStr for Alias {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        let (name, line) =
-            match_name(s.trim_start()).ok_or_else(|| format_err!("expected an alias name"))?;
-
-        let (address, line) = match_non_whitespace(line.trim_start())
-            .ok_or_else(|| format_err!("expected a value for alias {name:?}"))?;
-
-        let address: Cidr = address.parse()?;
-
-        let line = line.trim_start();
-
-        let comment = match line.strip_prefix('#') {
-            Some(comment) => Some(comment.trim().to_string()),
-            None if !line.is_empty() => bail!("trailing characters in alias: {line:?}"),
-            None => None,
-        };
-
-        Ok(Alias {
-            name: name.to_string(),
-            address,
-            comment,
-        })
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_parse_alias() {
-        for alias in [
-            "local_network 10.0.0.0/32",
-            "test-_123-___-a---- 10.0.0.1/32",
-        ] {
-            alias.parse::<Alias>().expect("valid alias");
-        }
-
-        for alias in ["-- 10.0.0.1/32", "0asd 10.0.0.1/32", "__test 10.0.0.0/32"] {
-            alias.parse::<Alias>().expect_err("invalid alias");
-        }
-    }
-
-    #[test]
-    fn test_parse_alias_name() {
-        for name in ["dc/proxmox_123", "guest/proxmox-123"] {
-            name.parse::<AliasName>().expect("valid alias name");
-        }
-
-        for name in ["proxmox/proxmox_123", "guests/proxmox-123", "dc/", "/name"] {
-            name.parse::<AliasName>().expect_err("invalid alias name");
-        }
-    }
-}
diff --git a/proxmox-ve-config/src/firewall/types/group.rs b/proxmox-ve-config/src/firewall/types/group.rs
deleted file mode 100644
index 7455268..0000000
--- a/proxmox-ve-config/src/firewall/types/group.rs
+++ /dev/null
@@ -1,36 +0,0 @@
-use anyhow::Error;
-
-use crate::firewall::types::Rule;
-
-#[derive(Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Group {
-    rules: Vec<Rule>,
-    comment: Option<String>,
-}
-
-impl Group {
-    pub const fn new() -> Self {
-        Self {
-            rules: Vec::new(),
-            comment: None,
-        }
-    }
-
-    pub fn rules(&self) -> &Vec<Rule> {
-        &self.rules
-    }
-
-    pub fn comment(&self) -> Option<&str> {
-        self.comment.as_deref()
-    }
-
-    pub fn set_comment(&mut self, comment: Option<String>) {
-        self.comment = comment;
-    }
-
-    pub(crate) fn parse_entry(&mut self, line: &str) -> Result<(), Error> {
-        self.rules.push(line.parse()?);
-        Ok(())
-    }
-}
diff --git a/proxmox-ve-config/src/firewall/types/ipset.rs b/proxmox-ve-config/src/firewall/types/ipset.rs
deleted file mode 100644
index c1af642..0000000
--- a/proxmox-ve-config/src/firewall/types/ipset.rs
+++ /dev/null
@@ -1,349 +0,0 @@
-use core::fmt::Display;
-use std::ops::{Deref, DerefMut};
-use std::str::FromStr;
-
-use anyhow::{bail, format_err, Error};
-use serde_with::DeserializeFromStr;
-
-use crate::firewall::parse::match_non_whitespace;
-use crate::firewall::types::address::Cidr;
-use crate::firewall::types::alias::AliasName;
-use crate::guest::vm::NetworkConfig;
-
-#[derive(Debug, Clone, Copy, Eq, PartialEq)]
-pub enum IpsetScope {
-    Datacenter,
-    Guest,
-}
-
-impl FromStr for IpsetScope {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        Ok(match s {
-            "+dc" => IpsetScope::Datacenter,
-            "+guest" => IpsetScope::Guest,
-            _ => bail!("invalid scope for ipset: {s}"),
-        })
-    }
-}
-
-impl Display for IpsetScope {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        let prefix = match self {
-            Self::Datacenter => "dc",
-            Self::Guest => "guest",
-        };
-
-        f.write_str(prefix)
-    }
-}
-
-#[derive(Debug, Clone, DeserializeFromStr)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct IpsetName {
-    pub scope: IpsetScope,
-    pub name: String,
-}
-
-impl IpsetName {
-    pub fn new(scope: IpsetScope, name: impl Into<String>) -> Self {
-        Self {
-            scope,
-            name: name.into(),
-        }
-    }
-
-    pub fn name(&self) -> &str {
-        &self.name
-    }
-
-    pub fn scope(&self) -> IpsetScope {
-        self.scope
-    }
-}
-
-impl FromStr for IpsetName {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        match s.split_once('/') {
-            Some((prefix, name)) if !name.is_empty() => Ok(Self {
-                scope: prefix.parse()?,
-                name: name.to_string(),
-            }),
-            _ => {
-                bail!("Invalid IPSet name: {s}")
-            }
-        }
-    }
-}
-
-impl Display for IpsetName {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}/{}", self.scope, self.name)
-    }
-}
-
-#[derive(Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum IpsetAddress {
-    Alias(AliasName),
-    Cidr(Cidr),
-}
-
-impl FromStr for IpsetAddress {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Error> {
-        if let Ok(cidr) = s.parse() {
-            return Ok(IpsetAddress::Cidr(cidr));
-        }
-
-        if let Ok(name) = s.parse() {
-            return Ok(IpsetAddress::Alias(name));
-        }
-
-        bail!("Invalid address in IPSet: {s}")
-    }
-}
-
-impl<T: Into<Cidr>> From<T> for IpsetAddress {
-    fn from(cidr: T) -> Self {
-        IpsetAddress::Cidr(cidr.into())
-    }
-}
-
-#[derive(Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct IpsetEntry {
-    pub nomatch: bool,
-    pub address: IpsetAddress,
-    pub comment: Option<String>,
-}
-
-impl<T: Into<IpsetAddress>> From<T> for IpsetEntry {
-    fn from(value: T) -> Self {
-        Self {
-            nomatch: false,
-            address: value.into(),
-            comment: None,
-        }
-    }
-}
-
-impl FromStr for IpsetEntry {
-    type Err = Error;
-
-    fn from_str(line: &str) -> Result<Self, Error> {
-        let line = line.trim_start();
-
-        let (nomatch, line) = match line.strip_prefix('!') {
-            Some(line) => (true, line),
-            None => (false, line),
-        };
-
-        let (address, line) =
-            match_non_whitespace(line.trim_start()).ok_or_else(|| format_err!("missing value"))?;
-
-        let address: IpsetAddress = address.parse()?;
-        let line = line.trim_start();
-
-        let comment = match line.strip_prefix('#') {
-            Some(comment) => Some(comment.trim().to_string()),
-            None if !line.is_empty() => bail!("trailing characters in ipset entry: {line:?}"),
-            None => None,
-        };
-
-        Ok(Self {
-            nomatch,
-            address,
-            comment,
-        })
-    }
-}
-
-#[derive(Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Ipfilter<'a> {
-    index: i64,
-    ipset: &'a Ipset,
-}
-
-impl Ipfilter<'_> {
-    pub fn index(&self) -> i64 {
-        self.index
-    }
-
-    pub fn ipset(&self) -> &Ipset {
-        self.ipset
-    }
-
-    pub fn name_for_index(index: i64) -> String {
-        format!("ipfilter-net{index}")
-    }
-}
-
-#[derive(Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Ipset {
-    pub name: IpsetName,
-    set: Vec<IpsetEntry>,
-    pub comment: Option<String>,
-}
-
-impl Ipset {
-    pub const fn new(name: IpsetName) -> Self {
-        Self {
-            name,
-            set: Vec::new(),
-            comment: None,
-        }
-    }
-
-    pub fn name(&self) -> &IpsetName {
-        &self.name
-    }
-
-    pub fn from_parts(scope: IpsetScope, name: impl Into<String>) -> Self {
-        Self::new(IpsetName::new(scope, name))
-    }
-
-    pub(crate) fn parse_entry(&mut self, line: &str) -> Result<(), Error> {
-        self.set.push(line.parse()?);
-        Ok(())
-    }
-
-    pub fn ipfilter(&self) -> Option<Ipfilter> {
-        if self.name.scope() != IpsetScope::Guest {
-            return None;
-        }
-
-        let name = self.name.name();
-
-        if let Some(key) = name.strip_prefix("ipfilter-") {
-            let id = NetworkConfig::index_from_net_key(key);
-
-            if let Ok(id) = id {
-                return Some(Ipfilter {
-                    index: id,
-                    ipset: self,
-                });
-            }
-        }
-
-        None
-    }
-}
-
-impl Deref for Ipset {
-    type Target = Vec<IpsetEntry>;
-
-    #[inline]
-    fn deref(&self) -> &Self::Target {
-        &self.set
-    }
-}
-
-impl DerefMut for Ipset {
-    #[inline]
-    fn deref_mut(&mut self) -> &mut Vec<IpsetEntry> {
-        &mut self.set
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_parse_ipset_name() {
-        for test_case in [
-            ("+dc/proxmox-123", IpsetScope::Datacenter, "proxmox-123"),
-            ("+guest/proxmox_123", IpsetScope::Guest, "proxmox_123"),
-        ] {
-            let ipset_name = test_case.0.parse::<IpsetName>().expect("valid ipset name");
-
-            assert_eq!(
-                ipset_name,
-                IpsetName {
-                    scope: test_case.1,
-                    name: test_case.2.to_string(),
-                }
-            )
-        }
-
-        for name in ["+dc/", "+guests/proxmox_123", "guest/proxmox_123"] {
-            name.parse::<IpsetName>().expect_err("invalid ipset name");
-        }
-    }
-
-    #[test]
-    fn test_parse_ipset_address() {
-        let mut ipset_address = "10.0.0.1"
-            .parse::<IpsetAddress>()
-            .expect("valid ipset address");
-        assert!(matches!(ipset_address, IpsetAddress::Cidr(Cidr::Ipv4(..))));
-
-        ipset_address = "fe80::1/64"
-            .parse::<IpsetAddress>()
-            .expect("valid ipset address");
-        assert!(matches!(ipset_address, IpsetAddress::Cidr(Cidr::Ipv6(..))));
-
-        ipset_address = "dc/proxmox-123"
-            .parse::<IpsetAddress>()
-            .expect("valid ipset address");
-        assert!(matches!(ipset_address, IpsetAddress::Alias(..)));
-
-        ipset_address = "guest/proxmox_123"
-            .parse::<IpsetAddress>()
-            .expect("valid ipset address");
-        assert!(matches!(ipset_address, IpsetAddress::Alias(..)));
-    }
-
-    #[test]
-    fn test_ipfilter() {
-        let mut ipset = Ipset::from_parts(IpsetScope::Guest, "ipfilter-net0");
-        ipset.ipfilter().expect("is an ipfilter");
-
-        ipset = Ipset::from_parts(IpsetScope::Guest, "ipfilter-qwe");
-        assert!(ipset.ipfilter().is_none());
-
-        ipset = Ipset::from_parts(IpsetScope::Guest, "proxmox");
-        assert!(ipset.ipfilter().is_none());
-
-        ipset = Ipset::from_parts(IpsetScope::Datacenter, "ipfilter-net0");
-        assert!(ipset.ipfilter().is_none());
-    }
-
-    #[test]
-    fn test_parse_ipset_entry() {
-        let mut entry = "!10.0.0.1 # qweqweasd"
-            .parse::<IpsetEntry>()
-            .expect("valid ipset entry");
-
-        assert_eq!(
-            entry,
-            IpsetEntry {
-                nomatch: true,
-                comment: Some("qweqweasd".to_string()),
-                address: IpsetAddress::Cidr(Cidr::new_v4([10, 0, 0, 1], 32).unwrap())
-            }
-        );
-
-        entry = "fe80::1/48"
-            .parse::<IpsetEntry>()
-            .expect("valid ipset entry");
-
-        assert_eq!(
-            entry,
-            IpsetEntry {
-                nomatch: false,
-                comment: None,
-                address: IpsetAddress::Cidr(
-                    Cidr::new_v6([0xFE80, 0, 0, 0, 0, 0, 0, 1], 48).unwrap()
-                )
-            }
-        )
-    }
-}
diff --git a/proxmox-ve-config/src/firewall/types/log.rs b/proxmox-ve-config/src/firewall/types/log.rs
deleted file mode 100644
index 72344e4..0000000
--- a/proxmox-ve-config/src/firewall/types/log.rs
+++ /dev/null
@@ -1,222 +0,0 @@
-use std::fmt;
-use std::str::FromStr;
-
-use crate::firewall::parse::parse_bool;
-use anyhow::{bail, Error};
-use serde::{Deserialize, Serialize};
-
-#[derive(Copy, Clone, Debug, Deserialize, Serialize, Default)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-#[serde(rename_all = "lowercase")]
-pub enum LogRateLimitTimescale {
-    #[default]
-    Second,
-    Minute,
-    Hour,
-    Day,
-}
-
-impl FromStr for LogRateLimitTimescale {
-    type Err = Error;
-
-    fn from_str(str: &str) -> Result<Self, Error> {
-        match str {
-            "second" => Ok(LogRateLimitTimescale::Second),
-            "minute" => Ok(LogRateLimitTimescale::Minute),
-            "hour" => Ok(LogRateLimitTimescale::Hour),
-            "day" => Ok(LogRateLimitTimescale::Day),
-            _ => bail!("Invalid time scale provided"),
-        }
-    }
-}
-
-#[derive(Debug, Deserialize, Clone)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct LogRateLimit {
-    enabled: bool,
-    rate: i64, // in packets
-    per: LogRateLimitTimescale,
-    burst: i64, // in packets
-}
-
-impl LogRateLimit {
-    pub fn new(enabled: bool, rate: i64, per: LogRateLimitTimescale, burst: i64) -> Self {
-        Self {
-            enabled,
-            rate,
-            per,
-            burst,
-        }
-    }
-
-    pub fn enabled(&self) -> bool {
-        self.enabled
-    }
-
-    pub fn rate(&self) -> i64 {
-        self.rate
-    }
-
-    pub fn burst(&self) -> i64 {
-        self.burst
-    }
-
-    pub fn per(&self) -> LogRateLimitTimescale {
-        self.per
-    }
-}
-
-impl Default for LogRateLimit {
-    fn default() -> Self {
-        Self {
-            enabled: true,
-            rate: 1,
-            burst: 5,
-            per: LogRateLimitTimescale::Second,
-        }
-    }
-}
-
-impl FromStr for LogRateLimit {
-    type Err = Error;
-
-    fn from_str(str: &str) -> Result<Self, Error> {
-        let mut limit = Self::default();
-
-        for element in str.split(',') {
-            match element.split_once('=') {
-                None => {
-                    limit.enabled = parse_bool(element)?;
-                }
-                Some((key, value)) if !key.is_empty() && !value.is_empty() => match key {
-                    "enable" => limit.enabled = parse_bool(value)?,
-                    "burst" => limit.burst = i64::from_str(value)?,
-                    "rate" => match value.split_once('/') {
-                        None => {
-                            limit.rate = i64::from_str(value)?;
-                        }
-                        Some((rate, unit)) => {
-                            if unit.is_empty() {
-                                bail!("empty unit specification")
-                            }
-
-                            limit.rate = i64::from_str(rate)?;
-                            limit.per = LogRateLimitTimescale::from_str(unit)?;
-                        }
-                    },
-                    _ => bail!("Invalid value for Key found in log_ratelimit!"),
-                },
-                _ => bail!("invalid value in log_ratelimit"),
-            }
-        }
-
-        Ok(limit)
-    }
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
-pub enum LogLevel {
-    #[default]
-    Nolog,
-    Emergency,
-    Alert,
-    Critical,
-    Error,
-    Warning,
-    Notice,
-    Info,
-    Debug,
-}
-
-impl std::str::FromStr for LogLevel {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Error> {
-        Ok(match s {
-            "nolog" => LogLevel::Nolog,
-            "emerg" => LogLevel::Emergency,
-            "alert" => LogLevel::Alert,
-            "crit" => LogLevel::Critical,
-            "err" => LogLevel::Error,
-            "warn" => LogLevel::Warning,
-            "warning" => LogLevel::Warning,
-            "notice" => LogLevel::Notice,
-            "info" => LogLevel::Info,
-            "debug" => LogLevel::Debug,
-            _ => bail!("invalid log level {s:?}"),
-        })
-    }
-}
-
-impl fmt::Display for LogLevel {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        f.write_str(match self {
-            LogLevel::Nolog => "nolog",
-            LogLevel::Emergency => "emerg",
-            LogLevel::Alert => "alert",
-            LogLevel::Critical => "crit",
-            LogLevel::Error => "err",
-            LogLevel::Warning => "warn",
-            LogLevel::Notice => "notice",
-            LogLevel::Info => "info",
-            LogLevel::Debug => "debug",
-        })
-    }
-}
-
-serde_plain::derive_deserialize_from_fromstr!(LogLevel, "valid log level");
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_parse_rate_limit() {
-        let mut parsed_rate_limit = "1,burst=123,rate=44"
-            .parse::<LogRateLimit>()
-            .expect("valid rate limit");
-
-        assert_eq!(
-            parsed_rate_limit,
-            LogRateLimit {
-                enabled: true,
-                burst: 123,
-                rate: 44,
-                per: LogRateLimitTimescale::Second,
-            }
-        );
-
-        parsed_rate_limit = "1".parse::<LogRateLimit>().expect("valid rate limit");
-
-        assert_eq!(parsed_rate_limit, LogRateLimit::default());
-
-        parsed_rate_limit = "enable=0,rate=123/hour"
-            .parse::<LogRateLimit>()
-            .expect("valid rate limit");
-
-        assert_eq!(
-            parsed_rate_limit,
-            LogRateLimit {
-                enabled: false,
-                burst: 5,
-                rate: 123,
-                per: LogRateLimitTimescale::Hour,
-            }
-        );
-
-        "2".parse::<LogRateLimit>()
-            .expect_err("invalid value for enable");
-
-        "enabled=0,rate=123"
-            .parse::<LogRateLimit>()
-            .expect_err("invalid key in log ratelimit");
-
-        "enable=0,rate=123,"
-            .parse::<LogRateLimit>()
-            .expect_err("trailing comma in log rate limit specification");
-
-        "enable=0,rate=123/proxmox,"
-            .parse::<LogRateLimit>()
-            .expect_err("invalid unit for rate");
-    }
-}
diff --git a/proxmox-ve-config/src/firewall/types/mod.rs b/proxmox-ve-config/src/firewall/types/mod.rs
deleted file mode 100644
index 8fd551e..0000000
--- a/proxmox-ve-config/src/firewall/types/mod.rs
+++ /dev/null
@@ -1,14 +0,0 @@
-pub mod address;
-pub mod alias;
-pub mod group;
-pub mod ipset;
-pub mod log;
-pub mod port;
-pub mod rule;
-pub mod rule_match;
-
-pub use address::Cidr;
-pub use alias::Alias;
-pub use group::Group;
-pub use ipset::Ipset;
-pub use rule::Rule;
diff --git a/proxmox-ve-config/src/firewall/types/port.rs b/proxmox-ve-config/src/firewall/types/port.rs
deleted file mode 100644
index c1252d9..0000000
--- a/proxmox-ve-config/src/firewall/types/port.rs
+++ /dev/null
@@ -1,181 +0,0 @@
-use std::fmt;
-use std::ops::Deref;
-
-use anyhow::{bail, Error};
-use serde_with::DeserializeFromStr;
-
-use crate::firewall::ports::parse_named_port;
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum PortEntry {
-    Port(u16),
-    Range(u16, u16),
-}
-
-impl fmt::Display for PortEntry {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self {
-            Self::Port(p) => write!(f, "{p}"),
-            Self::Range(beg, end) => write!(f, "{beg}-{end}"),
-        }
-    }
-}
-
-fn parse_port(port: &str) -> Result<u16, Error> {
-    if let Ok(port) = port.parse::<u16>() {
-        return Ok(port);
-    }
-
-    if let Ok(port) = parse_named_port(port) {
-        return Ok(port);
-    }
-
-    bail!("invalid port specification: {port}")
-}
-
-impl std::str::FromStr for PortEntry {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        Ok(match s.trim().split_once(':') {
-            None => PortEntry::from(parse_port(s)?),
-            Some((first, second)) => {
-                PortEntry::try_from((parse_port(first)?, parse_port(second)?))?
-            }
-        })
-    }
-}
-
-impl From<u16> for PortEntry {
-    fn from(port: u16) -> Self {
-        PortEntry::Port(port)
-    }
-}
-
-impl TryFrom<(u16, u16)> for PortEntry {
-    type Error = Error;
-
-    fn try_from(ports: (u16, u16)) -> Result<Self, Error> {
-        if ports.0 > ports.1 {
-            bail!("start port is greater than end port!");
-        }
-
-        Ok(PortEntry::Range(ports.0, ports.1))
-    }
-}
-
-#[derive(Clone, Debug, DeserializeFromStr)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct PortList(pub(crate) Vec<PortEntry>);
-
-impl FromIterator<PortEntry> for PortList {
-    fn from_iter<T: IntoIterator<Item = PortEntry>>(iter: T) -> Self {
-        Self(iter.into_iter().collect())
-    }
-}
-
-impl<T: Into<PortEntry>> From<T> for PortList {
-    fn from(value: T) -> Self {
-        Self(vec![value.into()])
-    }
-}
-
-impl Deref for PortList {
-    type Target = Vec<PortEntry>;
-
-    fn deref(&self) -> &Self::Target {
-        &self.0
-    }
-}
-
-impl std::str::FromStr for PortList {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Error> {
-        if s.is_empty() {
-            bail!("empty port specification");
-        }
-
-        let mut entries = Vec::new();
-
-        for entry in s.trim().split(',') {
-            entries.push(entry.parse()?);
-        }
-
-        if entries.is_empty() {
-            bail!("invalid empty port list");
-        }
-
-        Ok(Self(entries))
-    }
-}
-
-impl fmt::Display for PortList {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        use fmt::Write;
-        if self.0.len() > 1 {
-            f.write_char('{')?;
-        }
-
-        let mut comma = '\0';
-        for entry in &self.0 {
-            if std::mem::replace(&mut comma, ',') != '\0' {
-                f.write_char(comma)?;
-            }
-            fmt::Display::fmt(entry, f)?;
-        }
-
-        if self.0.len() > 1 {
-            f.write_char('}')?;
-        }
-
-        Ok(())
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_parse_port_entry() {
-        let mut port_entry: PortEntry = "12345".parse().expect("valid port entry");
-        assert_eq!(port_entry, PortEntry::from(12345));
-
-        port_entry = "0:65535".parse().expect("valid port entry");
-        assert_eq!(port_entry, PortEntry::try_from((0, 65535)).unwrap());
-
-        "65536".parse::<PortEntry>().unwrap_err();
-        "100:100000".parse::<PortEntry>().unwrap_err();
-        "qweasd".parse::<PortEntry>().unwrap_err();
-        "".parse::<PortEntry>().unwrap_err();
-    }
-
-    #[test]
-    fn test_parse_port_list() {
-        let mut port_list: PortList = "12345".parse().expect("valid port list");
-        assert_eq!(port_list, PortList::from(12345));
-
-        port_list = "12345,0:65535,1337,ssh:80,https"
-            .parse()
-            .expect("valid port list");
-
-        assert_eq!(
-            port_list,
-            PortList(vec![
-                PortEntry::from(12345),
-                PortEntry::try_from((0, 65535)).unwrap(),
-                PortEntry::from(1337),
-                PortEntry::try_from((22, 80)).unwrap(),
-                PortEntry::from(443),
-            ])
-        );
-
-        "0::1337".parse::<PortList>().unwrap_err();
-        "0:1337,".parse::<PortList>().unwrap_err();
-        "70000".parse::<PortList>().unwrap_err();
-        "qweasd".parse::<PortList>().unwrap_err();
-        "".parse::<PortList>().unwrap_err();
-    }
-}
diff --git a/proxmox-ve-config/src/firewall/types/rule.rs b/proxmox-ve-config/src/firewall/types/rule.rs
deleted file mode 100644
index 20deb3a..0000000
--- a/proxmox-ve-config/src/firewall/types/rule.rs
+++ /dev/null
@@ -1,412 +0,0 @@
-use core::fmt::Display;
-use std::fmt;
-use std::str::FromStr;
-
-use anyhow::{bail, ensure, format_err, Error};
-
-use crate::firewall::parse::match_name;
-use crate::firewall::types::rule_match::RuleMatch;
-use crate::firewall::types::rule_match::RuleOptions;
-
-#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
-pub enum Direction {
-    #[default]
-    In,
-    Out,
-}
-
-impl std::str::FromStr for Direction {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Error> {
-        for (name, dir) in [("IN", Direction::In), ("OUT", Direction::Out)] {
-            if s.eq_ignore_ascii_case(name) {
-                return Ok(dir);
-            }
-        }
-
-        bail!("invalid direction: {s:?}, expect 'IN' or 'OUT'");
-    }
-}
-
-serde_plain::derive_deserialize_from_fromstr!(Direction, "valid packet direction");
-
-impl fmt::Display for Direction {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self {
-            Direction::In => f.write_str("in"),
-            Direction::Out => f.write_str("out"),
-        }
-    }
-}
-
-#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
-pub enum Verdict {
-    Accept,
-    Reject,
-    #[default]
-    Drop,
-}
-
-impl std::str::FromStr for Verdict {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Error> {
-        for (name, verdict) in [
-            ("ACCEPT", Verdict::Accept),
-            ("REJECT", Verdict::Reject),
-            ("DROP", Verdict::Drop),
-        ] {
-            if s.eq_ignore_ascii_case(name) {
-                return Ok(verdict);
-            }
-        }
-        bail!("invalid verdict {s:?}, expected one of 'ACCEPT', 'REJECT' or 'DROP'");
-    }
-}
-
-impl Display for Verdict {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let string = match self {
-            Verdict::Accept => "ACCEPT",
-            Verdict::Drop => "DROP",
-            Verdict::Reject => "REJECT",
-        };
-
-        write!(f, "{string}")
-    }
-}
-
-serde_plain::derive_deserialize_from_fromstr!(Verdict, "valid verdict");
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Rule {
-    pub(crate) disabled: bool,
-    pub(crate) kind: Kind,
-    pub(crate) comment: Option<String>,
-}
-
-impl std::ops::Deref for Rule {
-    type Target = Kind;
-
-    fn deref(&self) -> &Self::Target {
-        &self.kind
-    }
-}
-
-impl std::ops::DerefMut for Rule {
-    fn deref_mut(&mut self) -> &mut Self::Target {
-        &mut self.kind
-    }
-}
-
-impl FromStr for Rule {
-    type Err = Error;
-
-    fn from_str(input: &str) -> Result<Self, Self::Err> {
-        if input.contains(['\n', '\r']) {
-            bail!("rule must not contain any newlines!");
-        }
-
-        let (line, comment) = match input.rsplit_once('#') {
-            Some((line, comment)) if !comment.is_empty() => (line.trim(), Some(comment.trim())),
-            _ => (input.trim(), None),
-        };
-
-        let (disabled, line) = match line.strip_prefix('|') {
-            Some(line) => (true, line.trim_start()),
-            None => (false, line),
-        };
-
-        // todo: case insensitive?
-        let kind = if line.starts_with("GROUP") {
-            Kind::from(line.parse::<RuleGroup>()?)
-        } else {
-            Kind::from(line.parse::<RuleMatch>()?)
-        };
-
-        Ok(Self {
-            disabled,
-            comment: comment.map(str::to_string),
-            kind,
-        })
-    }
-}
-
-impl Rule {
-    pub fn iface(&self) -> Option<&str> {
-        match &self.kind {
-            Kind::Group(group) => group.iface(),
-            Kind::Match(rule) => rule.iface(),
-        }
-    }
-
-    pub fn disabled(&self) -> bool {
-        self.disabled
-    }
-
-    pub fn kind(&self) -> &Kind {
-        &self.kind
-    }
-
-    pub fn comment(&self) -> Option<&str> {
-        self.comment.as_deref()
-    }
-}
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum Kind {
-    Group(RuleGroup),
-    Match(RuleMatch),
-}
-
-impl Kind {
-    pub fn is_group(&self) -> bool {
-        matches!(self, Kind::Group(_))
-    }
-
-    pub fn is_match(&self) -> bool {
-        matches!(self, Kind::Match(_))
-    }
-}
-
-impl From<RuleGroup> for Kind {
-    fn from(value: RuleGroup) -> Self {
-        Kind::Group(value)
-    }
-}
-
-impl From<RuleMatch> for Kind {
-    fn from(value: RuleMatch) -> Self {
-        Kind::Match(value)
-    }
-}
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct RuleGroup {
-    pub(crate) group: String,
-    pub(crate) iface: Option<String>,
-}
-
-impl RuleGroup {
-    pub(crate) fn from_options(group: String, options: RuleOptions) -> Result<Self, Error> {
-        ensure!(
-            options.proto.is_none()
-                && options.dport.is_none()
-                && options.sport.is_none()
-                && options.dest.is_none()
-                && options.source.is_none()
-                && options.log.is_none()
-                && options.icmp_type.is_none(),
-            "only interface parameter is permitted for group rules"
-        );
-
-        Ok(Self {
-            group,
-            iface: options.iface,
-        })
-    }
-
-    pub fn group(&self) -> &str {
-        &self.group
-    }
-
-    pub fn iface(&self) -> Option<&str> {
-        self.iface.as_deref()
-    }
-}
-
-impl FromStr for RuleGroup {
-    type Err = Error;
-
-    fn from_str(input: &str) -> Result<Self, Self::Err> {
-        let (keyword, rest) = match_name(input)
-            .ok_or_else(|| format_err!("expected a leading keyword in rule group"))?;
-
-        if !keyword.eq_ignore_ascii_case("group") {
-            bail!("Expected keyword GROUP")
-        }
-
-        let (name, rest) =
-            match_name(rest.trim()).ok_or_else(|| format_err!("expected a name for rule group"))?;
-
-        let options = rest.trim_start().parse()?;
-
-        Self::from_options(name.to_string(), options)
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use crate::firewall::types::{
-        address::{IpEntry, IpList},
-        alias::{AliasName, AliasScope},
-        ipset::{IpsetName, IpsetScope},
-        log::LogLevel,
-        rule_match::{Icmp, IcmpCode, IpAddrMatch, IpMatch, Ports, Protocol, Udp},
-        Cidr,
-    };
-
-    use super::*;
-
-    #[test]
-    fn test_parse_rule() {
-        let mut rule: Rule = "|GROUP tgr -i eth0 # acomm".parse().expect("valid rule");
-
-        assert_eq!(
-            rule,
-            Rule {
-                disabled: true,
-                comment: Some("acomm".to_string()),
-                kind: Kind::Group(RuleGroup {
-                    group: "tgr".to_string(),
-                    iface: Some("eth0".to_string()),
-                }),
-            },
-        );
-
-        rule = "IN ACCEPT -p udp -dport 33 -sport 22 -log warning"
-            .parse()
-            .expect("valid rule");
-
-        assert_eq!(
-            rule,
-            Rule {
-                disabled: false,
-                comment: None,
-                kind: Kind::Match(RuleMatch {
-                    dir: Direction::In,
-                    verdict: Verdict::Accept,
-                    proto: Some(Udp::new(Ports::from_u16(22, 33)).into()),
-                    log: Some(LogLevel::Warning),
-                    ..Default::default()
-                }),
-            }
-        );
-
-        rule = "IN ACCEPT --proto udp -i eth0".parse().expect("valid rule");
-
-        assert_eq!(
-            rule,
-            Rule {
-                disabled: false,
-                comment: None,
-                kind: Kind::Match(RuleMatch {
-                    dir: Direction::In,
-                    verdict: Verdict::Accept,
-                    proto: Some(Udp::new(Ports::new(None, None)).into()),
-                    iface: Some("eth0".to_string()),
-                    ..Default::default()
-                }),
-            }
-        );
-
-        rule = " OUT DROP \
-          -source 10.0.0.0/24 -dest 20.0.0.0-20.255.255.255,192.168.0.0/16 \
-          -p icmp -log nolog -icmp-type port-unreachable "
-            .parse()
-            .expect("valid rule");
-
-        assert_eq!(
-            rule,
-            Rule {
-                disabled: false,
-                comment: None,
-                kind: Kind::Match(RuleMatch {
-                    dir: Direction::Out,
-                    verdict: Verdict::Drop,
-                    ip: IpMatch::new(
-                        IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 24).unwrap())),
-                        IpAddrMatch::Ip(
-                            IpList::new(vec![
-                                IpEntry::Range([20, 0, 0, 0].into(), [20, 255, 255, 255].into()),
-                                IpEntry::Cidr(Cidr::new_v4([192, 168, 0, 0], 16).unwrap()),
-                            ])
-                            .unwrap()
-                        ),
-                    )
-                    .ok(),
-                    proto: Some(Protocol::Icmp(Icmp::new_code(IcmpCode::Named(
-                        "port-unreachable"
-                    )))),
-                    log: Some(LogLevel::Nolog),
-                    ..Default::default()
-                }),
-            }
-        );
-
-        rule = "IN BGP(ACCEPT) --log crit --iface eth0"
-            .parse()
-            .expect("valid rule");
-
-        assert_eq!(
-            rule,
-            Rule {
-                disabled: false,
-                comment: None,
-                kind: Kind::Match(RuleMatch {
-                    dir: Direction::In,
-                    verdict: Verdict::Accept,
-                    log: Some(LogLevel::Critical),
-                    fw_macro: Some("BGP".to_string()),
-                    iface: Some("eth0".to_string()),
-                    ..Default::default()
-                }),
-            }
-        );
-
-        rule = "IN ACCEPT --source dc/test --dest +dc/test"
-            .parse()
-            .expect("valid rule");
-
-        assert_eq!(
-            rule,
-            Rule {
-                disabled: false,
-                comment: None,
-                kind: Kind::Match(RuleMatch {
-                    dir: Direction::In,
-                    verdict: Verdict::Accept,
-                    ip: Some(
-                        IpMatch::new(
-                            IpAddrMatch::Alias(AliasName::new(AliasScope::Datacenter, "test")),
-                            IpAddrMatch::Set(IpsetName::new(IpsetScope::Datacenter, "test"),),
-                        )
-                        .unwrap()
-                    ),
-                    ..Default::default()
-                }),
-            }
-        );
-
-        rule = "IN REJECT".parse().expect("valid rule");
-
-        assert_eq!(
-            rule,
-            Rule {
-                disabled: false,
-                comment: None,
-                kind: Kind::Match(RuleMatch {
-                    dir: Direction::In,
-                    verdict: Verdict::Reject,
-                    ..Default::default()
-                }),
-            }
-        );
-
-        "IN DROP ---log crit"
-            .parse::<Rule>()
-            .expect_err("too many dashes in option");
-
-        "IN DROP --log --iface eth0"
-            .parse::<Rule>()
-            .expect_err("no value for option");
-
-        "IN DROP --log crit --iface"
-            .parse::<Rule>()
-            .expect_err("no value for option");
-    }
-}
diff --git a/proxmox-ve-config/src/firewall/types/rule_match.rs b/proxmox-ve-config/src/firewall/types/rule_match.rs
deleted file mode 100644
index 94d8624..0000000
--- a/proxmox-ve-config/src/firewall/types/rule_match.rs
+++ /dev/null
@@ -1,977 +0,0 @@
-use std::collections::HashMap;
-use std::fmt;
-use std::str::FromStr;
-
-use serde::Deserialize;
-
-use anyhow::{bail, format_err, Error};
-use serde::de::IntoDeserializer;
-
-use proxmox_sortable_macro::sortable;
-
-use crate::firewall::parse::{match_name, match_non_whitespace, SomeStr};
-use crate::firewall::types::address::{Family, IpList};
-use crate::firewall::types::alias::AliasName;
-use crate::firewall::types::ipset::IpsetName;
-use crate::firewall::types::log::LogLevel;
-use crate::firewall::types::port::PortList;
-use crate::firewall::types::rule::{Direction, Verdict};
-
-#[derive(Clone, Debug, Default, Deserialize)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-#[serde(deny_unknown_fields, rename_all = "kebab-case")]
-pub(crate) struct RuleOptions {
-    #[serde(alias = "p")]
-    pub(crate) proto: Option<String>,
-
-    pub(crate) dport: Option<String>,
-    pub(crate) sport: Option<String>,
-
-    pub(crate) dest: Option<String>,
-    pub(crate) source: Option<String>,
-
-    #[serde(alias = "i")]
-    pub(crate) iface: Option<String>,
-
-    pub(crate) log: Option<LogLevel>,
-    pub(crate) icmp_type: Option<String>,
-}
-
-impl FromStr for RuleOptions {
-    type Err = Error;
-
-    fn from_str(mut line: &str) -> Result<Self, Self::Err> {
-        let mut options = HashMap::new();
-
-        loop {
-            line = line.trim_start();
-
-            if line.is_empty() {
-                break;
-            }
-
-            line = line
-                .strip_prefix('-')
-                .ok_or_else(|| format_err!("expected an option starting with '-'"))?;
-
-            // second dash is optional
-            line = line.strip_prefix('-').unwrap_or(line);
-
-            let param;
-            (param, line) = match_name(line)
-                .ok_or_else(|| format_err!("expected a parameter name after '-'"))?;
-
-            let value;
-            (value, line) = match_non_whitespace(line.trim_start())
-                .ok_or_else(|| format_err!("expected a value for {param:?}"))?;
-
-            if options.insert(param, SomeStr(value)).is_some() {
-                bail!("duplicate option in rule: {param}")
-            }
-        }
-
-        Ok(RuleOptions::deserialize(IntoDeserializer::<
-            '_,
-            crate::firewall::parse::SerdeStringError,
-        >::into_deserializer(
-            options
-        ))?)
-    }
-}
-
-#[derive(Clone, Debug, Default)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct RuleMatch {
-    pub(crate) dir: Direction,
-    pub(crate) verdict: Verdict,
-    pub(crate) fw_macro: Option<String>,
-
-    pub(crate) iface: Option<String>,
-    pub(crate) log: Option<LogLevel>,
-    pub(crate) ip: Option<IpMatch>,
-    pub(crate) proto: Option<Protocol>,
-}
-
-impl RuleMatch {
-    pub(crate) fn from_options(
-        dir: Direction,
-        verdict: Verdict,
-        fw_macro: impl Into<Option<String>>,
-        options: RuleOptions,
-    ) -> Result<Self, Error> {
-        if options.dport.is_some() && options.icmp_type.is_some() {
-            bail!("dport and icmp-type are mutually exclusive");
-        }
-
-        let ip = IpMatch::from_options(&options)?;
-        let proto = Protocol::from_options(&options)?;
-
-        // todo: check protocol & IP Version compatibility
-
-        Ok(Self {
-            dir,
-            verdict,
-            fw_macro: fw_macro.into(),
-            iface: options.iface,
-            log: options.log,
-            ip,
-            proto,
-        })
-    }
-
-    pub fn direction(&self) -> Direction {
-        self.dir
-    }
-
-    pub fn iface(&self) -> Option<&str> {
-        self.iface.as_deref()
-    }
-
-    pub fn verdict(&self) -> Verdict {
-        self.verdict
-    }
-
-    pub fn fw_macro(&self) -> Option<&str> {
-        self.fw_macro.as_deref()
-    }
-
-    pub fn log(&self) -> Option<LogLevel> {
-        self.log
-    }
-
-    pub fn ip(&self) -> Option<&IpMatch> {
-        self.ip.as_ref()
-    }
-
-    pub fn proto(&self) -> Option<&Protocol> {
-        self.proto.as_ref()
-    }
-}
-
-/// Returns `(Macro name, Verdict, RestOfTheLine)`.
-fn parse_action(line: &str) -> Result<(Option<&str>, Verdict, &str), Error> {
-    let (verdict, line) =
-        match_name(line).ok_or_else(|| format_err!("expected a verdict or macro name"))?;
-
-    Ok(if let Some(line) = line.strip_prefix('(') {
-        // <macro>(<verdict>)
-
-        let macro_name = verdict;
-        let (verdict, line) = match_name(line).ok_or_else(|| format_err!("expected a verdict"))?;
-        let line = line
-            .strip_prefix(')')
-            .ok_or_else(|| format_err!("expected closing ')' after verdict"))?;
-
-        let verdict: Verdict = verdict.parse()?;
-
-        (Some(macro_name), verdict, line.trim_start())
-    } else {
-        (None, verdict.parse()?, line.trim_start())
-    })
-}
-
-impl FromStr for RuleMatch {
-    type Err = Error;
-
-    fn from_str(line: &str) -> Result<Self, Self::Err> {
-        let (dir, rest) = match_name(line).ok_or_else(|| format_err!("expected a direction"))?;
-
-        let direction: Direction = dir.parse()?;
-
-        let (fw_macro, verdict, rest) = parse_action(rest.trim_start())?;
-
-        let options: RuleOptions = rest.trim_start().parse()?;
-
-        Self::from_options(direction, verdict, fw_macro.map(str::to_string), options)
-    }
-}
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct IpMatch {
-    pub(crate) src: Option<IpAddrMatch>,
-    pub(crate) dst: Option<IpAddrMatch>,
-}
-
-impl IpMatch {
-    pub fn new(
-        src: impl Into<Option<IpAddrMatch>>,
-        dst: impl Into<Option<IpAddrMatch>>,
-    ) -> Result<Self, Error> {
-        let source = src.into();
-        let dest = dst.into();
-
-        if source.is_none() && dest.is_none() {
-            bail!("either src or dst must be set")
-        }
-
-        if let (Some(IpAddrMatch::Ip(src)), Some(IpAddrMatch::Ip(dst))) = (&source, &dest) {
-            if src.family() != dst.family() {
-                bail!("src and dst family must be equal")
-            }
-        }
-
-        let ip_match = Self {
-            src: source,
-            dst: dest,
-        };
-
-        Ok(ip_match)
-    }
-
-    fn from_options(options: &RuleOptions) -> Result<Option<Self>, Error> {
-        let src = options
-            .source
-            .as_ref()
-            .map(|elem| elem.parse::<IpAddrMatch>())
-            .transpose()?;
-
-        let dst = options
-            .dest
-            .as_ref()
-            .map(|elem| elem.parse::<IpAddrMatch>())
-            .transpose()?;
-
-        if src.is_some() || dst.is_some() {
-            Ok(Some(IpMatch::new(src, dst)?))
-        } else {
-            Ok(None)
-        }
-    }
-
-    pub fn src(&self) -> Option<&IpAddrMatch> {
-        self.src.as_ref()
-    }
-
-    pub fn dst(&self) -> Option<&IpAddrMatch> {
-        self.dst.as_ref()
-    }
-}
-
-#[derive(Clone, Debug, Deserialize)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum IpAddrMatch {
-    Ip(IpList),
-    Set(IpsetName),
-    Alias(AliasName),
-}
-
-impl IpAddrMatch {
-    pub fn family(&self) -> Option<Family> {
-        if let IpAddrMatch::Ip(list) = self {
-            return Some(list.family());
-        }
-
-        None
-    }
-}
-
-impl FromStr for IpAddrMatch {
-    type Err = Error;
-
-    fn from_str(value: &str) -> Result<Self, Error> {
-        if value.is_empty() {
-            bail!("empty IP specification");
-        }
-
-        if let Ok(ip_list) = value.parse() {
-            return Ok(IpAddrMatch::Ip(ip_list));
-        }
-
-        if let Ok(ipset) = value.parse() {
-            return Ok(IpAddrMatch::Set(ipset));
-        }
-
-        if let Ok(name) = value.parse() {
-            return Ok(IpAddrMatch::Alias(name));
-        }
-
-        bail!("invalid IP specification: {value}")
-    }
-}
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum Protocol {
-    Dccp(Ports),
-    Sctp(Sctp),
-    Tcp(Tcp),
-    Udp(Udp),
-    UdpLite(Ports),
-    Icmp(Icmp),
-    Icmpv6(Icmpv6),
-    Named(String),
-    Numeric(u8),
-}
-
-impl Protocol {
-    pub(crate) fn from_options(options: &RuleOptions) -> Result<Option<Self>, Error> {
-        let proto = match options.proto.as_deref() {
-            Some(p) => p,
-            None => return Ok(None),
-        };
-
-        Ok(Some(match proto {
-            "dccp" | "33" => Protocol::Dccp(Ports::from_options(options)?),
-            "sctp" | "132" => Protocol::Sctp(Sctp::from_options(options)?),
-            "tcp" | "6" => Protocol::Tcp(Tcp::from_options(options)?),
-            "udp" | "17" => Protocol::Udp(Udp::from_options(options)?),
-            "udplite" | "136" => Protocol::UdpLite(Ports::from_options(options)?),
-            "icmp" | "1" => Protocol::Icmp(Icmp::from_options(options)?),
-            "ipv6-icmp" | "icmpv6" | "58" => Protocol::Icmpv6(Icmpv6::from_options(options)?),
-            other => match other.parse::<u8>() {
-                Ok(num) => Protocol::Numeric(num),
-                Err(_) => Protocol::Named(other.to_string()),
-            },
-        }))
-    }
-
-    pub fn family(&self) -> Option<Family> {
-        match self {
-            Self::Icmp(_) => Some(Family::V4),
-            Self::Icmpv6(_) => Some(Family::V6),
-            _ => None,
-        }
-    }
-}
-
-#[derive(Clone, Debug, Default)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Udp {
-    ports: Ports,
-}
-
-impl Udp {
-    fn from_options(options: &RuleOptions) -> Result<Self, Error> {
-        Ok(Self {
-            ports: Ports::from_options(options)?,
-        })
-    }
-
-    pub fn new(ports: Ports) -> Self {
-        Self { ports }
-    }
-
-    pub fn ports(&self) -> &Ports {
-        &self.ports
-    }
-}
-
-impl From<Udp> for Protocol {
-    fn from(value: Udp) -> Self {
-        Protocol::Udp(value)
-    }
-}
-
-#[derive(Clone, Debug, Default)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Ports {
-    sport: Option<PortList>,
-    dport: Option<PortList>,
-}
-
-impl Ports {
-    pub fn new(sport: impl Into<Option<PortList>>, dport: impl Into<Option<PortList>>) -> Self {
-        Self {
-            sport: sport.into(),
-            dport: dport.into(),
-        }
-    }
-
-    fn from_options(options: &RuleOptions) -> Result<Self, Error> {
-        Ok(Self {
-            sport: options.sport.as_deref().map(|s| s.parse()).transpose()?,
-            dport: options.dport.as_deref().map(|s| s.parse()).transpose()?,
-        })
-    }
-
-    pub fn from_u16(sport: impl Into<Option<u16>>, dport: impl Into<Option<u16>>) -> Self {
-        Self::new(
-            sport.into().map(PortList::from),
-            dport.into().map(PortList::from),
-        )
-    }
-
-    pub fn sport(&self) -> Option<&PortList> {
-        self.sport.as_ref()
-    }
-
-    pub fn dport(&self) -> Option<&PortList> {
-        self.dport.as_ref()
-    }
-}
-
-#[derive(Clone, Debug, Default)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Tcp {
-    ports: Ports,
-}
-
-impl Tcp {
-    pub fn new(ports: Ports) -> Self {
-        Self { ports }
-    }
-
-    fn from_options(options: &RuleOptions) -> Result<Self, Error> {
-        Ok(Self {
-            ports: Ports::from_options(options)?,
-        })
-    }
-
-    pub fn ports(&self) -> &Ports {
-        &self.ports
-    }
-}
-
-impl From<Tcp> for Protocol {
-    fn from(value: Tcp) -> Self {
-        Protocol::Tcp(value)
-    }
-}
-
-#[derive(Clone, Debug, Default)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Sctp {
-    ports: Ports,
-}
-
-impl Sctp {
-    fn from_options(options: &RuleOptions) -> Result<Self, Error> {
-        Ok(Self {
-            ports: Ports::from_options(options)?,
-        })
-    }
-
-    pub fn ports(&self) -> &Ports {
-        &self.ports
-    }
-}
-
-#[derive(Clone, Debug, Default)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Icmp {
-    ty: Option<IcmpType>,
-    code: Option<IcmpCode>,
-}
-
-impl Icmp {
-    pub fn new_ty(ty: IcmpType) -> Self {
-        Self {
-            ty: Some(ty),
-            ..Default::default()
-        }
-    }
-
-    pub fn new_code(code: IcmpCode) -> Self {
-        Self {
-            code: Some(code),
-            ..Default::default()
-        }
-    }
-
-    fn from_options(options: &RuleOptions) -> Result<Self, Error> {
-        if let Some(ty) = &options.icmp_type {
-            return ty.parse();
-        }
-
-        Ok(Self::default())
-    }
-
-    pub fn ty(&self) -> Option<&IcmpType> {
-        self.ty.as_ref()
-    }
-
-    pub fn code(&self) -> Option<&IcmpCode> {
-        self.code.as_ref()
-    }
-}
-
-impl FromStr for Icmp {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        let mut this = Self::default();
-
-        if let Ok(ty) = s.parse() {
-            this.ty = Some(ty);
-            return Ok(this);
-        }
-
-        if let Ok(code) = s.parse() {
-            this.code = Some(code);
-            return Ok(this);
-        }
-
-        bail!("supplied string is neither a valid icmp type nor code");
-    }
-}
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum IcmpType {
-    Numeric(u8),
-    Named(&'static str),
-    Any,
-}
-
-#[sortable]
-const ICMP_TYPES: [(&str, u8); 15] = sorted!([
-    ("address-mask-reply", 18),
-    ("address-mask-request", 17),
-    ("destination-unreachable", 3),
-    ("echo-reply", 0),
-    ("echo-request", 8),
-    ("info-reply", 16),
-    ("info-request", 15),
-    ("parameter-problem", 12),
-    ("redirect", 5),
-    ("router-advertisement", 9),
-    ("router-solicitation", 10),
-    ("source-quench", 4),
-    ("time-exceeded", 11),
-    ("timestamp-reply", 14),
-    ("timestamp-request", 13),
-]);
-
-impl std::str::FromStr for IcmpType {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Error> {
-        if s.eq_ignore_ascii_case("any") {
-            return Ok(Self::Any);
-        }
-
-        if let Ok(ty) = s.trim().parse::<u8>() {
-            return Ok(Self::Numeric(ty));
-        }
-
-        if let Ok(index) = ICMP_TYPES.binary_search_by(|v| v.0.cmp(s)) {
-            return Ok(Self::Named(ICMP_TYPES[index].0));
-        }
-
-        bail!("{s:?} is not a valid icmp type");
-    }
-}
-
-impl fmt::Display for IcmpType {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self {
-            IcmpType::Numeric(ty) => write!(f, "{ty}"),
-            IcmpType::Named(ty) => write!(f, "{ty}"),
-            IcmpType::Any => write!(f, "any"),
-        }
-    }
-}
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum IcmpCode {
-    Numeric(u8),
-    Named(&'static str),
-}
-
-#[sortable]
-const ICMP_CODES: [(&str, u8); 7] = sorted!([
-    ("admin-prohibited", 13),
-    ("host-prohibited", 10),
-    ("host-unreachable", 1),
-    ("net-prohibited", 9),
-    ("net-unreachable", 0),
-    ("port-unreachable", 3),
-    ("prot-unreachable", 2),
-]);
-
-impl std::str::FromStr for IcmpCode {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Error> {
-        if let Ok(code) = s.trim().parse::<u8>() {
-            return Ok(Self::Numeric(code));
-        }
-
-        if let Ok(index) = ICMP_CODES.binary_search_by(|v| v.0.cmp(s)) {
-            return Ok(Self::Named(ICMP_CODES[index].0));
-        }
-
-        bail!("{s:?} is not a valid icmp code");
-    }
-}
-
-impl fmt::Display for IcmpCode {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self {
-            IcmpCode::Numeric(code) => write!(f, "{code}"),
-            IcmpCode::Named(code) => write!(f, "{code}"),
-        }
-    }
-}
-
-#[derive(Clone, Debug, Default)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Icmpv6 {
-    pub ty: Option<Icmpv6Type>,
-    pub code: Option<Icmpv6Code>,
-}
-
-impl Icmpv6 {
-    pub fn new_ty(ty: Icmpv6Type) -> Self {
-        Self {
-            ty: Some(ty),
-            ..Default::default()
-        }
-    }
-
-    pub fn new_code(code: Icmpv6Code) -> Self {
-        Self {
-            code: Some(code),
-            ..Default::default()
-        }
-    }
-
-    fn from_options(options: &RuleOptions) -> Result<Self, Error> {
-        if let Some(ty) = &options.icmp_type {
-            return ty.parse();
-        }
-
-        Ok(Self::default())
-    }
-
-    pub fn ty(&self) -> Option<&Icmpv6Type> {
-        self.ty.as_ref()
-    }
-
-    pub fn code(&self) -> Option<&Icmpv6Code> {
-        self.code.as_ref()
-    }
-}
-
-impl FromStr for Icmpv6 {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        let mut this = Self::default();
-
-        if let Ok(ty) = s.parse() {
-            this.ty = Some(ty);
-            return Ok(this);
-        }
-
-        if let Ok(code) = s.parse() {
-            this.code = Some(code);
-            return Ok(this);
-        }
-
-        bail!("supplied string is neither a valid icmpv6 type nor code");
-    }
-}
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum Icmpv6Type {
-    Numeric(u8),
-    Named(&'static str),
-    Any,
-}
-
-#[sortable]
-const ICMPV6_TYPES: [(&str, u8); 19] = sorted!([
-    ("destination-unreachable", 1),
-    ("echo-reply", 129),
-    ("echo-request", 128),
-    ("ind-neighbor-advert", 142),
-    ("ind-neighbor-solicit", 141),
-    ("mld-listener-done", 132),
-    ("mld-listener-query", 130),
-    ("mld-listener-reduction", 132),
-    ("mld-listener-report", 131),
-    ("mld2-listener-report", 143),
-    ("nd-neighbor-advert", 136),
-    ("nd-neighbor-solicit", 135),
-    ("nd-redirect", 137),
-    ("nd-router-advert", 134),
-    ("nd-router-solicit", 133),
-    ("packet-too-big", 2),
-    ("parameter-problem", 4),
-    ("router-renumbering", 138),
-    ("time-exceeded", 3),
-]);
-
-impl std::str::FromStr for Icmpv6Type {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Error> {
-        if s.eq_ignore_ascii_case("any") {
-            return Ok(Self::Any);
-        }
-
-        if let Ok(ty) = s.trim().parse::<u8>() {
-            return Ok(Self::Numeric(ty));
-        }
-
-        if let Ok(index) = ICMPV6_TYPES.binary_search_by(|v| v.0.cmp(s)) {
-            return Ok(Self::Named(ICMPV6_TYPES[index].0));
-        }
-
-        bail!("{s:?} is not a valid icmpv6 type");
-    }
-}
-
-impl fmt::Display for Icmpv6Type {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self {
-            Icmpv6Type::Numeric(ty) => write!(f, "{ty}"),
-            Icmpv6Type::Named(ty) => write!(f, "{ty}"),
-            Icmpv6Type::Any => write!(f, "any"),
-        }
-    }
-}
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum Icmpv6Code {
-    Numeric(u8),
-    Named(&'static str),
-}
-
-#[sortable]
-const ICMPV6_CODES: [(&str, u8); 6] = sorted!([
-    ("addr-unreachable", 3),
-    ("admin-prohibited", 1),
-    ("no-route", 0),
-    ("policy-fail", 5),
-    ("port-unreachable", 4),
-    ("reject-route", 6),
-]);
-
-impl std::str::FromStr for Icmpv6Code {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Error> {
-        if let Ok(code) = s.trim().parse::<u8>() {
-            return Ok(Self::Numeric(code));
-        }
-
-        if let Ok(index) = ICMPV6_CODES.binary_search_by(|v| v.0.cmp(s)) {
-            return Ok(Self::Named(ICMPV6_CODES[index].0));
-        }
-
-        bail!("{s:?} is not a valid icmpv6 code");
-    }
-}
-
-impl fmt::Display for Icmpv6Code {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self {
-            Icmpv6Code::Numeric(code) => write!(f, "{code}"),
-            Icmpv6Code::Named(code) => write!(f, "{code}"),
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use crate::firewall::types::{alias::AliasScope::Guest, Cidr};
-
-    use super::*;
-
-    #[test]
-    fn test_parse_action() {
-        assert_eq!(parse_action("REJECT").unwrap(), (None, Verdict::Reject, ""));
-
-        assert_eq!(
-            parse_action("SSH(ACCEPT) qweasd").unwrap(),
-            (Some("SSH"), Verdict::Accept, "qweasd")
-        );
-    }
-
-    #[test]
-    fn test_parse_ip_addr_match() {
-        for input in [
-            "10.0.0.0/8",
-            "10.0.0.0/8,192.168.0.0-192.168.255.255,172.16.0.1",
-            "dc/test",
-            "+guest/proxmox",
-        ] {
-            input.parse::<IpAddrMatch>().expect("valid ip match");
-        }
-
-        for input in [
-            "10.0.0.0/",
-            "10.0.0.0/8,192.168.256.0-192.168.255.255,172.16.0.1",
-            "dcc/test",
-            "+guest/",
-            "",
-        ] {
-            input.parse::<IpAddrMatch>().expect_err("invalid ip match");
-        }
-    }
-
-    #[test]
-    fn test_parse_options() {
-        let mut options: RuleOptions =
-            "-p udp --sport 123 --dport 234 -source 127.0.0.1 --dest 127.0.0.1 -i ens1 --log crit"
-                .parse()
-                .expect("valid option string");
-
-        assert_eq!(
-            options,
-            RuleOptions {
-                proto: Some("udp".to_string()),
-                sport: Some("123".to_string()),
-                dport: Some("234".to_string()),
-                source: Some("127.0.0.1".to_string()),
-                dest: Some("127.0.0.1".to_string()),
-                iface: Some("ens1".to_string()),
-                log: Some(LogLevel::Critical),
-                icmp_type: None,
-            }
-        );
-
-        options = "".parse().expect("valid option string");
-
-        assert_eq!(options, RuleOptions::default(),);
-    }
-
-    #[test]
-    fn test_construct_ip_match() {
-        IpMatch::new(
-            IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 8).unwrap())),
-            IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 8).unwrap())),
-        )
-        .expect("valid ip match");
-
-        IpMatch::new(
-            IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 8).unwrap())),
-            IpAddrMatch::Alias(AliasName::new(Guest, "test")),
-        )
-        .expect("valid ip match");
-
-        IpMatch::new(
-            IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 8).unwrap())),
-            IpAddrMatch::Ip(IpList::from(Cidr::new_v6([0x0000; 8], 8).unwrap())),
-        )
-        .expect_err("cannot mix ip families");
-
-        IpMatch::new(None, None).expect_err("at least one ip must be set");
-    }
-
-    #[test]
-    fn test_from_options() {
-        let mut options = RuleOptions {
-            proto: Some("tcp".to_string()),
-            sport: Some("123".to_string()),
-            dport: Some("234".to_string()),
-            source: Some("192.168.0.1".to_string()),
-            dest: Some("10.0.0.1".to_string()),
-            iface: Some("eth123".to_string()),
-            log: Some(LogLevel::Error),
-            ..Default::default()
-        };
-
-        assert_eq!(
-            Protocol::from_options(&options).unwrap().unwrap(),
-            Protocol::Tcp(Tcp::new(Ports::from_u16(123, 234))),
-        );
-
-        assert_eq!(
-            IpMatch::from_options(&options).unwrap().unwrap(),
-            IpMatch::new(
-                IpAddrMatch::Ip(IpList::from(Cidr::new_v4([192, 168, 0, 1], 32).unwrap()),),
-                IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 1], 32).unwrap()),)
-            )
-            .unwrap(),
-        );
-
-        options = RuleOptions::default();
-
-        assert_eq!(Protocol::from_options(&options).unwrap(), None,);
-
-        assert_eq!(IpMatch::from_options(&options).unwrap(), None,);
-
-        options = RuleOptions {
-            proto: Some("tcp".to_string()),
-            sport: Some("qwe".to_string()),
-            source: Some("qwe".to_string()),
-            ..Default::default()
-        };
-
-        Protocol::from_options(&options).expect_err("invalid source port");
-
-        IpMatch::from_options(&options).expect_err("invalid source address");
-
-        options = RuleOptions {
-            icmp_type: Some("port-unreachable".to_string()),
-            dport: Some("123".to_string()),
-            ..Default::default()
-        };
-
-        RuleMatch::from_options(Direction::In, Verdict::Drop, None, options)
-            .expect_err("cannot mix dport and icmp-type");
-    }
-
-    #[test]
-    fn test_parse_icmp() {
-        let mut icmp: Icmp = "info-request".parse().expect("valid icmp type");
-
-        assert_eq!(
-            icmp,
-            Icmp {
-                ty: Some(IcmpType::Named("info-request")),
-                code: None
-            }
-        );
-
-        icmp = "12".parse().expect("valid icmp type");
-
-        assert_eq!(
-            icmp,
-            Icmp {
-                ty: Some(IcmpType::Numeric(12)),
-                code: None
-            }
-        );
-
-        icmp = "port-unreachable".parse().expect("valid icmp code");
-
-        assert_eq!(
-            icmp,
-            Icmp {
-                ty: None,
-                code: Some(IcmpCode::Named("port-unreachable"))
-            }
-        );
-    }
-
-    #[test]
-    fn test_parse_icmp6() {
-        let mut icmp: Icmpv6 = "echo-reply".parse().expect("valid icmpv6 type");
-
-        assert_eq!(
-            icmp,
-            Icmpv6 {
-                ty: Some(Icmpv6Type::Named("echo-reply")),
-                code: None
-            }
-        );
-
-        icmp = "12".parse().expect("valid icmpv6 type");
-
-        assert_eq!(
-            icmp,
-            Icmpv6 {
-                ty: Some(Icmpv6Type::Numeric(12)),
-                code: None
-            }
-        );
-
-        icmp = "admin-prohibited".parse().expect("valid icmpv6 code");
-
-        assert_eq!(
-            icmp,
-            Icmpv6 {
-                ty: None,
-                code: Some(Icmpv6Code::Named("admin-prohibited"))
-            }
-        );
-    }
-}
diff --git a/proxmox-ve-config/src/guest/mod.rs b/proxmox-ve-config/src/guest/mod.rs
deleted file mode 100644
index 74fd8ab..0000000
--- a/proxmox-ve-config/src/guest/mod.rs
+++ /dev/null
@@ -1,115 +0,0 @@
-use core::ops::Deref;
-use std::collections::HashMap;
-
-use anyhow::{Context, Error};
-use serde::Deserialize;
-
-use proxmox_sys::nodename;
-use types::Vmid;
-
-pub mod types;
-pub mod vm;
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize)]
-pub enum GuestType {
-    #[serde(rename = "qemu")]
-    Vm,
-    #[serde(rename = "lxc")]
-    Ct,
-}
-
-impl GuestType {
-    pub fn iface_prefix(self) -> &'static str {
-        match self {
-            GuestType::Vm => "tap",
-            GuestType::Ct => "veth",
-        }
-    }
-
-    fn config_folder(&self) -> &'static str {
-        match self {
-            GuestType::Vm => "qemu-server",
-            GuestType::Ct => "lxc",
-        }
-    }
-}
-
-#[derive(Deserialize)]
-pub struct GuestEntry {
-    node: String,
-
-    #[serde(rename = "type")]
-    ty: GuestType,
-
-    #[serde(rename = "version")]
-    _version: usize,
-}
-
-impl GuestEntry {
-    pub fn new(node: String, ty: GuestType) -> Self {
-        Self {
-            node,
-            ty,
-            _version: Default::default(),
-        }
-    }
-
-    pub fn is_local(&self) -> bool {
-        nodename() == self.node
-    }
-
-    pub fn ty(&self) -> &GuestType {
-        &self.ty
-    }
-}
-
-const VMLIST_CONFIG_PATH: &str = "/etc/pve/.vmlist";
-
-#[derive(Deserialize)]
-pub struct GuestMap {
-    #[serde(rename = "version")]
-    _version: usize,
-    #[serde(rename = "ids", default)]
-    guests: HashMap<Vmid, GuestEntry>,
-}
-
-impl From<HashMap<Vmid, GuestEntry>> for GuestMap {
-    fn from(guests: HashMap<Vmid, GuestEntry>) -> Self {
-        Self {
-            guests,
-            _version: Default::default(),
-        }
-    }
-}
-
-impl Deref for GuestMap {
-    type Target = HashMap<Vmid, GuestEntry>;
-
-    fn deref(&self) -> &Self::Target {
-        &self.guests
-    }
-}
-
-impl GuestMap {
-    pub fn new() -> Result<Self, Error> {
-        let data = std::fs::read(VMLIST_CONFIG_PATH)
-            .with_context(|| format!("failed to read guest map from {VMLIST_CONFIG_PATH}"))?;
-
-        serde_json::from_slice(&data).with_context(|| "failed to parse guest map".to_owned())
-    }
-
-    pub fn firewall_config_path(vmid: &Vmid) -> String {
-        format!("/etc/pve/firewall/{}.fw", vmid)
-    }
-
-    /// returns the local configuration path for a given Vmid.
-    ///
-    /// The caller must ensure that the given Vmid exists and is local to the node
-    pub fn config_path(vmid: &Vmid, entry: &GuestEntry) -> String {
-        format!(
-            "/etc/pve/local/{}/{}.conf",
-            entry.ty().config_folder(),
-            vmid
-        )
-    }
-}
diff --git a/proxmox-ve-config/src/guest/types.rs b/proxmox-ve-config/src/guest/types.rs
deleted file mode 100644
index 217c537..0000000
--- a/proxmox-ve-config/src/guest/types.rs
+++ /dev/null
@@ -1,38 +0,0 @@
-use std::fmt;
-use std::str::FromStr;
-
-use anyhow::{format_err, Error};
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)]
-pub struct Vmid(u32);
-
-impl Vmid {
-    pub fn new(id: u32) -> Self {
-        Vmid(id)
-    }
-}
-
-impl From<u32> for Vmid {
-    fn from(value: u32) -> Self {
-        Self::new(value)
-    }
-}
-
-impl fmt::Display for Vmid {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        fmt::Display::fmt(&self.0, f)
-    }
-}
-
-impl FromStr for Vmid {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        Ok(Self(
-            s.parse()
-                .map_err(|_| format_err!("not a valid vmid: {s:?}"))?,
-        ))
-    }
-}
-
-serde_plain::derive_deserialize_from_fromstr!(Vmid, "valid vmid");
diff --git a/proxmox-ve-config/src/guest/vm.rs b/proxmox-ve-config/src/guest/vm.rs
deleted file mode 100644
index 5b5866a..0000000
--- a/proxmox-ve-config/src/guest/vm.rs
+++ /dev/null
@@ -1,510 +0,0 @@
-use anyhow::{bail, Error};
-use core::fmt::Display;
-use std::io;
-use std::str::FromStr;
-use std::{collections::HashMap, net::Ipv6Addr};
-
-use proxmox_schema::property_string::PropertyIterator;
-
-use crate::firewall::parse::{match_digits, parse_bool};
-use crate::firewall::types::address::{Ipv4Cidr, Ipv6Cidr};
-
-#[derive(Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct MacAddress([u8; 6]);
-
-static LOCAL_PART: [u8; 8] = [0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
-static EUI64_MIDDLE_PART: [u8; 2] = [0xFF, 0xFE];
-
-impl MacAddress {
-    /// generates a link local IPv6-address according to RFC 4291 (Appendix A)
-    pub fn eui64_link_local_address(&self) -> Ipv6Addr {
-        let head = &self.0[..3];
-        let tail = &self.0[3..];
-
-        let mut eui64_address: Vec<u8> = LOCAL_PART
-            .iter()
-            .chain(head.iter())
-            .chain(EUI64_MIDDLE_PART.iter())
-            .chain(tail.iter())
-            .copied()
-            .collect();
-
-        // we need to flip the 7th bit of the first eui64 byte
-        eui64_address[8] ^= 0x02;
-
-        Ipv6Addr::from(
-            TryInto::<[u8; 16]>::try_into(eui64_address).expect("is an u8 array with 16 entries"),
-        )
-    }
-}
-
-impl FromStr for MacAddress {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        let split = s.split(':');
-
-        let parsed = split
-            .into_iter()
-            .map(|elem| u8::from_str_radix(elem, 16))
-            .collect::<Result<Vec<u8>, _>>()
-            .map_err(Error::msg)?;
-
-        if parsed.len() != 6 {
-            bail!("Invalid amount of elements in MAC address!");
-        }
-
-        let address = &parsed.as_slice()[0..6];
-        Ok(Self(address.try_into().unwrap()))
-    }
-}
-
-impl Display for MacAddress {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(
-            f,
-            "{:<02X}:{:<02X}:{:<02X}:{:<02X}:{:<02X}:{:<02X}",
-            self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5]
-        )
-    }
-}
-
-#[derive(Debug, Clone, Copy)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum NetworkDeviceModel {
-    VirtIO,
-    Veth,
-    E1000,
-    Vmxnet3,
-    RTL8139,
-}
-
-impl FromStr for NetworkDeviceModel {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        match s {
-            "virtio" => Ok(NetworkDeviceModel::VirtIO),
-            "e1000" => Ok(NetworkDeviceModel::E1000),
-            "rtl8139" => Ok(NetworkDeviceModel::RTL8139),
-            "vmxnet3" => Ok(NetworkDeviceModel::Vmxnet3),
-            "veth" => Ok(NetworkDeviceModel::Veth),
-            _ => bail!("Invalid network device model: {s}"),
-        }
-    }
-}
-
-#[derive(Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct NetworkDevice {
-    model: NetworkDeviceModel,
-    mac_address: MacAddress,
-    firewall: bool,
-    ip: Option<Ipv4Cidr>,
-    ip6: Option<Ipv6Cidr>,
-}
-
-impl NetworkDevice {
-    pub fn model(&self) -> NetworkDeviceModel {
-        self.model
-    }
-
-    pub fn mac_address(&self) -> &MacAddress {
-        &self.mac_address
-    }
-
-    pub fn ip(&self) -> Option<&Ipv4Cidr> {
-        self.ip.as_ref()
-    }
-
-    pub fn ip6(&self) -> Option<&Ipv6Cidr> {
-        self.ip6.as_ref()
-    }
-
-    pub fn has_firewall(&self) -> bool {
-        self.firewall
-    }
-}
-
-impl FromStr for NetworkDevice {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        let (mut ty, mut hwaddr, mut firewall, mut ip, mut ip6) = (None, None, true, None, None);
-
-        for entry in PropertyIterator::new(s) {
-            let (key, value) = entry.unwrap();
-
-            if let Some(key) = key {
-                match key {
-                    "type" | "model" => {
-                        ty = Some(NetworkDeviceModel::from_str(&value)?);
-                    }
-                    "hwaddr" | "macaddr" => {
-                        hwaddr = Some(MacAddress::from_str(&value)?);
-                    }
-                    "firewall" => {
-                        firewall = parse_bool(&value)?;
-                    }
-                    "ip" => {
-                        if value == "dhcp" {
-                            continue;
-                        }
-
-                        ip = Some(Ipv4Cidr::from_str(&value)?);
-                    }
-                    "ip6" => {
-                        if value == "dhcp" || value == "auto" {
-                            continue;
-                        }
-
-                        ip6 = Some(Ipv6Cidr::from_str(&value)?);
-                    }
-                    _ => {
-                        if let Ok(model) = NetworkDeviceModel::from_str(key) {
-                            ty = Some(model);
-                            hwaddr = Some(MacAddress::from_str(&value)?);
-                        }
-                    }
-                }
-            }
-        }
-
-        if let (Some(ty), Some(hwaddr)) = (ty, hwaddr) {
-            return Ok(NetworkDevice {
-                model: ty,
-                mac_address: hwaddr,
-                firewall,
-                ip,
-                ip6,
-            });
-        }
-
-        bail!("No valid network device detected in string {s}");
-    }
-}
-
-#[derive(Debug, Default)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct NetworkConfig {
-    network_devices: HashMap<i64, NetworkDevice>,
-}
-
-impl NetworkConfig {
-    pub fn new() -> Self {
-        Self::default()
-    }
-
-    pub fn index_from_net_key(key: &str) -> Result<i64, Error> {
-        if let Some(digits) = key.strip_prefix("net") {
-            if let Some((digits, rest)) = match_digits(digits) {
-                let index: i64 = digits.parse()?;
-
-                if (0..31).contains(&index) && rest.is_empty() {
-                    return Ok(index);
-                }
-            }
-        }
-
-        bail!("No index found in net key string: {key}")
-    }
-
-    pub fn network_devices(&self) -> &HashMap<i64, NetworkDevice> {
-        &self.network_devices
-    }
-
-    pub fn parse<R: io::BufRead>(input: R) -> Result<Self, Error> {
-        let mut network_devices = HashMap::new();
-
-        for line in input.lines() {
-            let line = line?;
-            let line = line.trim();
-
-            if line.is_empty() || line.starts_with('#') {
-                continue;
-            }
-
-            if line.starts_with('[') {
-                break;
-            }
-
-            if line.starts_with("net") {
-                log::trace!("parsing net config line: {line}");
-
-                if let Some((mut key, mut value)) = line.split_once(':') {
-                    if key.is_empty() || value.is_empty() {
-                        continue;
-                    }
-
-                    key = key.trim();
-                    value = value.trim();
-
-                    if let Ok(index) = Self::index_from_net_key(key) {
-                        let network_device = NetworkDevice::from_str(value)?;
-
-                        let exists = network_devices.insert(index, network_device);
-
-                        if exists.is_some() {
-                            bail!("Duplicated config key detected: {key}");
-                        }
-                    } else {
-                        bail!("Encountered invalid net key in cfg: {key}");
-                    }
-                }
-            }
-        }
-
-        Ok(Self { network_devices })
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_parse_mac_address() {
-        for input in [
-            "aa:aa:aa:11:22:33",
-            "AA:BB:FF:11:22:33",
-            "bc:24:11:AA:bb:Ef",
-        ] {
-            let mac_address = input.parse::<MacAddress>().expect("valid mac address");
-
-            assert_eq!(input.to_uppercase(), mac_address.to_string());
-        }
-
-        for input in [
-            "aa:aa:aa:11:22:33:aa",
-            "AA:BB:FF:11:22",
-            "AA:BB:GG:11:22:33",
-            "AABBGG112233",
-            "",
-        ] {
-            input
-                .parse::<MacAddress>()
-                .expect_err("invalid mac address");
-        }
-    }
-
-    #[test]
-    fn test_eui64_link_local_address() {
-        let mac_address: MacAddress = "BC:24:11:49:8D:75".parse().expect("valid MAC address");
-
-        let link_local_address =
-            Ipv6Addr::from_str("fe80::be24:11ff:fe49:8d75").expect("valid IPv6 address");
-
-        assert_eq!(link_local_address, mac_address.eui64_link_local_address());
-    }
-
-    #[test]
-    fn test_parse_network_device() {
-        let mut network_device: NetworkDevice =
-            "virtio=AA:AA:AA:17:19:81,bridge=public,firewall=1,queues=4"
-                .parse()
-                .expect("valid network configuration");
-
-        assert_eq!(
-            network_device,
-            NetworkDevice {
-                model: NetworkDeviceModel::VirtIO,
-                mac_address: MacAddress([0xAA, 0xAA, 0xAA, 0x17, 0x19, 0x81]),
-                firewall: true,
-                ip: None,
-                ip6: None,
-            }
-        );
-
-        network_device = "model=virtio,macaddr=AA:AA:AA:17:19:81,bridge=public,firewall=1,queues=4"
-            .parse()
-            .expect("valid network configuration");
-
-        assert_eq!(
-            network_device,
-            NetworkDevice {
-                model: NetworkDeviceModel::VirtIO,
-                mac_address: MacAddress([0xAA, 0xAA, 0xAA, 0x17, 0x19, 0x81]),
-                firewall: true,
-                ip: None,
-                ip6: None,
-            }
-        );
-
-        network_device =
-            "name=eth0,bridge=public,firewall=0,hwaddr=AA:AA:AA:E2:3E:24,ip=dhcp,type=veth"
-                .parse()
-                .expect("valid network configuration");
-
-        assert_eq!(
-            network_device,
-            NetworkDevice {
-                model: NetworkDeviceModel::Veth,
-                mac_address: MacAddress([0xAA, 0xAA, 0xAA, 0xE2, 0x3E, 0x24]),
-                firewall: false,
-                ip: None,
-                ip6: None,
-            }
-        );
-
-        "model=virtio"
-            .parse::<NetworkDevice>()
-            .expect_err("invalid network configuration");
-
-        "bridge=public,firewall=0"
-            .parse::<NetworkDevice>()
-            .expect_err("invalid network configuration");
-
-        "".parse::<NetworkDevice>()
-            .expect_err("invalid network configuration");
-
-        "name=eth0,bridge=public,firewall=0,hwaddr=AA:AA:AG:E2:3E:24,ip=dhcp,type=veth"
-            .parse::<NetworkDevice>()
-            .expect_err("invalid network configuration");
-    }
-
-    #[test]
-    fn test_parse_network_confg() {
-        let mut guest_config = "\
-boot: order=scsi0;net0
-cores: 4
-cpu: host
-memory: 8192
-meta: creation-qemu=8.0.2,ctime=1700141675
-name: hoan-sdn
-net0: virtio=AA:BB:CC:F2:FE:75,bridge=public
-numa: 0
-ostype: l26
-parent: uwu
-scsi0: local-lvm:vm-999-disk-0,discard=on,iothread=1,size=32G
-scsihw: virtio-scsi-single
-smbios1: uuid=addb0cc6-0393-4269-a504-1eb46604cb8a
-sockets: 1
-vmgenid: 13bcbb05-3608-4d74-bf4f-d5d20c3538e8
-
-[snapshot]
-boot: order=scsi0;ide2;net0
-cores: 4
-cpu: x86-64-v2-AES
-ide2: NFS-iso:iso/proxmox-ve_8.0-2.iso,media=cdrom,size=1166488K
-memory: 8192
-meta: creation-qemu=8.0.2,ctime=1700141675
-name: test
-net2: virtio=AA:AA:AA:F2:FE:75,bridge=public,firewall=1
-numa: 0
-ostype: l26
-parent: pre-SDN
-scsi0: local-lvm:vm-999-disk-0,discard=on,iothread=1,size=32G
-scsihw: virtio-scsi-single
-smbios1: uuid=addb0cc6-0393-4269-a504-1eb46604cb8a
-snaptime: 1700143513
-sockets: 1
-vmgenid: 706fbe99-d28b-4047-a9cd-3677c859ca8a
-
-[snapshott]
-boot: order=scsi0;ide2;net0
-cores: 4
-cpu: host
-ide2: NFS-iso:iso/proxmox-ve_8.0-2.iso,media=cdrom,size=1166488K
-memory: 8192
-meta: creation-qemu=8.0.2,ctime=1700141675
-name: hoan-sdn
-net0: virtio=AA:AA:FF:F2:FE:75,bridge=public,firewall=0
-numa: 0
-ostype: l26
-parent: SDN
-scsi0: local-lvm:vm-999-disk-0,discard=on,iothread=1,size=32G
-scsihw: virtio-scsi-single
-smbios1: uuid=addb0cc6-0393-4269-a504-1eb46604cb8a
-snaptime: 1700158473
-sockets: 1
-vmgenid: 706fbe99-d28b-4047-a9cd-3677c859ca8a"
-            .as_bytes();
-
-        let mut network_config: NetworkConfig =
-            NetworkConfig::parse(guest_config).expect("valid network configuration");
-
-        assert_eq!(network_config.network_devices().len(), 1);
-
-        assert_eq!(
-            network_config.network_devices()[&0],
-            NetworkDevice {
-                model: NetworkDeviceModel::VirtIO,
-                mac_address: MacAddress([0xAA, 0xBB, 0xCC, 0xF2, 0xFE, 0x75]),
-                firewall: true,
-                ip: None,
-                ip6: None,
-            }
-        );
-
-        guest_config = "\
-arch: amd64
-cores: 1
-features: nesting=1
-hostname: dnsct
-memory: 512
-net0: name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:11,ip=dhcp,type=veth
-net2:   name=eth0,bridge=data,firewall=0,hwaddr=BC:24:11:47:83:12,ip=123.123.123.123/24,type=veth  
-net5: name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:13,ip6=fd80::1/64,type=veth
-ostype: alpine
-rootfs: local-lvm:vm-10001-disk-0,size=1G
-swap: 512
-unprivileged: 1"
-            .as_bytes();
-
-        network_config = NetworkConfig::parse(guest_config).expect("valid network configuration");
-
-        assert_eq!(network_config.network_devices().len(), 3);
-
-        assert_eq!(
-            network_config.network_devices()[&0],
-            NetworkDevice {
-                model: NetworkDeviceModel::Veth,
-                mac_address: MacAddress([0xBC, 0x24, 0x11, 0x47, 0x83, 0x11]),
-                firewall: true,
-                ip: None,
-                ip6: None,
-            }
-        );
-
-        assert_eq!(
-            network_config.network_devices()[&2],
-            NetworkDevice {
-                model: NetworkDeviceModel::Veth,
-                mac_address: MacAddress([0xBC, 0x24, 0x11, 0x47, 0x83, 0x12]),
-                firewall: false,
-                ip: Some(Ipv4Cidr::from_str("123.123.123.123/24").expect("valid ipv4")),
-                ip6: None,
-            }
-        );
-
-        assert_eq!(
-            network_config.network_devices()[&5],
-            NetworkDevice {
-                model: NetworkDeviceModel::Veth,
-                mac_address: MacAddress([0xBC, 0x24, 0x11, 0x47, 0x83, 0x13]),
-                firewall: true,
-                ip: None,
-                ip6: Some(Ipv6Cidr::from_str("fd80::1/64").expect("valid ipv6")),
-            }
-        );
-
-        NetworkConfig::parse(
-            "netqwe: name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:11,ip=dhcp,type=veth"
-                .as_bytes(),
-        )
-        .expect_err("invalid net key");
-
-        NetworkConfig::parse(
-            "net0 name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:11,ip=dhcp,type=veth"
-                .as_bytes(),
-        )
-        .expect_err("invalid net key");
-
-        NetworkConfig::parse(
-            "net33: name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:11,ip=dhcp,type=veth"
-                .as_bytes(),
-        )
-        .expect_err("invalid net key");
-    }
-}
diff --git a/proxmox-ve-config/src/host/mod.rs b/proxmox-ve-config/src/host/mod.rs
deleted file mode 100644
index b5614dd..0000000
--- a/proxmox-ve-config/src/host/mod.rs
+++ /dev/null
@@ -1 +0,0 @@
-pub mod utils;
diff --git a/proxmox-ve-config/src/host/utils.rs b/proxmox-ve-config/src/host/utils.rs
deleted file mode 100644
index b1dc8e9..0000000
--- a/proxmox-ve-config/src/host/utils.rs
+++ /dev/null
@@ -1,70 +0,0 @@
-use std::net::{IpAddr, ToSocketAddrs};
-
-use crate::firewall::types::Cidr;
-
-use nix::sys::socket::{AddressFamily, SockaddrLike};
-use proxmox_sys::nodename;
-
-/// gets a list of IPs that the hostname of this node resolves to
-///
-/// panics if the local hostname is not resolvable
-pub fn host_ips() -> Vec<IpAddr> {
-    let hostname = nodename();
-
-    log::trace!("resolving hostname");
-
-    format!("{hostname}:0")
-        .to_socket_addrs()
-        .expect("local hostname is resolvable")
-        .map(|addr| addr.ip())
-        .collect()
-}
-
-/// gets a list of all configured CIDRs on all network interfaces of this host
-///
-/// panics if unable to query the current network configuration
-pub fn network_interface_cidrs() -> Vec<Cidr> {
-    use nix::ifaddrs::getifaddrs;
-
-    log::trace!("reading networking interface list");
-
-    let mut cidrs = Vec::new();
-
-    let interfaces = getifaddrs().expect("should be able to query network interfaces");
-
-    for interface in interfaces {
-        if let (Some(address), Some(netmask)) = (interface.address, interface.netmask) {
-            match (address.family(), netmask.family()) {
-                (Some(AddressFamily::Inet), Some(AddressFamily::Inet)) => {
-                    let address = address.as_sockaddr_in().expect("is an IPv4 address").ip();
-
-                    let netmask = netmask
-                        .as_sockaddr_in()
-                        .expect("is an IPv4 address")
-                        .ip()
-                        .count_ones()
-                        .try_into()
-                        .expect("count_ones of u32 is < u8_max");
-
-                    cidrs.push(Cidr::new_v4(address, netmask).expect("netmask is valid"));
-                }
-                (Some(AddressFamily::Inet6), Some(AddressFamily::Inet6)) => {
-                    let address = address.as_sockaddr_in6().expect("is an IPv6 address").ip();
-
-                    let netmask_address =
-                        netmask.as_sockaddr_in6().expect("is an IPv6 address").ip();
-
-                    let netmask = u128::from_be_bytes(netmask_address.octets())
-                        .count_ones()
-                        .try_into()
-                        .expect("count_ones of u128 is < u8_max");
-
-                    cidrs.push(Cidr::new_v6(address, netmask).expect("netmask is valid"));
-                }
-                _ => continue,
-            }
-        }
-    }
-
-    cidrs
-}
diff --git a/proxmox-ve-config/src/lib.rs b/proxmox-ve-config/src/lib.rs
deleted file mode 100644
index 856b14f..0000000
--- a/proxmox-ve-config/src/lib.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-pub mod firewall;
-pub mod guest;
-pub mod host;
-- 
2.39.5


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pve-devel] [PATCH proxmox-firewall v3 18/24] config: tests: add support for loading sdn and ipam config
  2024-11-12 12:25 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v3 00/24] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (16 preceding siblings ...)
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-firewall v3 17/24] add proxmox-ve-rs crate - move proxmox-ve-config there Stefan Hanreich
@ 2024-11-12 12:25 ` Stefan Hanreich
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-firewall v3 19/24] ipsets: autogenerate ipsets for vnets and ipam Stefan Hanreich
                   ` (5 subsequent siblings)
  23 siblings, 0 replies; 26+ messages in thread
From: Stefan Hanreich @ 2024-11-12 12:25 UTC (permalink / raw)
  To: pve-devel

Also add example SDN configuration files that get automatically
loaded, which can be used for future tests.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-firewall/src/config.rs                | 69 +++++++++++++++++++
 .../tests/input/.running-config.json          | 45 ++++++++++++
 proxmox-firewall/tests/input/ipam.db          | 32 +++++++++
 proxmox-firewall/tests/integration_tests.rs   | 10 +++
 proxmox-nftables/src/types.rs                 |  2 +-
 5 files changed, 157 insertions(+), 1 deletion(-)
 create mode 100644 proxmox-firewall/tests/input/.running-config.json
 create mode 100644 proxmox-firewall/tests/input/ipam.db

diff --git a/proxmox-firewall/src/config.rs b/proxmox-firewall/src/config.rs
index 5bd2512..c27aac6 100644
--- a/proxmox-firewall/src/config.rs
+++ b/proxmox-firewall/src/config.rs
@@ -16,6 +16,10 @@ use proxmox_ve_config::guest::{GuestEntry, GuestMap};
 use proxmox_nftables::command::{CommandOutput, Commands, List, ListOutput};
 use proxmox_nftables::types::ListChain;
 use proxmox_nftables::NftClient;
+use proxmox_ve_config::sdn::{
+    config::{RunningConfig, SdnConfig},
+    ipam::{Ipam, IpamJson},
+};
 
 pub trait FirewallConfigLoader {
     fn cluster(&self) -> Result<Option<Box<dyn io::BufRead>>, Error>;
@@ -27,6 +31,8 @@ pub trait FirewallConfigLoader {
         guest: &GuestEntry,
     ) -> Result<Option<Box<dyn io::BufRead>>, Error>;
     fn guest_firewall_config(&self, vmid: &Vmid) -> Result<Option<Box<dyn io::BufRead>>, Error>;
+    fn sdn_running_config(&self) -> Result<Option<Box<dyn io::BufRead>>, Error>;
+    fn ipam(&self) -> Result<Option<Box<dyn io::BufRead>>, Error>;
 }
 
 #[derive(Default)]
@@ -58,6 +64,9 @@ fn open_config_file(path: &str) -> Result<Option<File>, Error> {
 const CLUSTER_CONFIG_PATH: &str = "/etc/pve/firewall/cluster.fw";
 const HOST_CONFIG_PATH: &str = "/etc/pve/local/host.fw";
 
+const SDN_RUNNING_CONFIG_PATH: &str = "/etc/pve/sdn/.running-config";
+const SDN_IPAM_PATH: &str = "/etc/pve/priv/ipam.db";
+
 impl FirewallConfigLoader for PveFirewallConfigLoader {
     fn cluster(&self) -> Result<Option<Box<dyn io::BufRead>>, Error> {
         log::info!("loading cluster config");
@@ -119,6 +128,32 @@ impl FirewallConfigLoader for PveFirewallConfigLoader {
 
         Ok(None)
     }
+
+    fn sdn_running_config(&self) -> Result<Option<Box<dyn io::BufRead>>, Error> {
+        log::info!("loading SDN running-config");
+
+        let fd = open_config_file(SDN_RUNNING_CONFIG_PATH)?;
+
+        if let Some(file) = fd {
+            let buf_reader = Box::new(BufReader::new(file)) as Box<dyn io::BufRead>;
+            return Ok(Some(buf_reader));
+        }
+
+        Ok(None)
+    }
+
+    fn ipam(&self) -> Result<Option<Box<dyn io::BufRead>>, Error> {
+        log::info!("loading IPAM config");
+
+        let fd = open_config_file(SDN_IPAM_PATH)?;
+
+        if let Some(file) = fd {
+            let buf_reader = Box::new(BufReader::new(file)) as Box<dyn io::BufRead>;
+            return Ok(Some(buf_reader));
+        }
+
+        Ok(None)
+    }
 }
 
 pub trait NftConfigLoader {
@@ -150,6 +185,8 @@ pub struct FirewallConfig {
     host_config: HostConfig,
     guest_config: BTreeMap<Vmid, GuestConfig>,
     nft_config: BTreeMap<String, ListChain>,
+    sdn_config: Option<SdnConfig>,
+    ipam_config: Option<Ipam>,
 }
 
 impl FirewallConfig {
@@ -207,6 +244,28 @@ impl FirewallConfig {
         Ok(guests)
     }
 
+    pub fn parse_sdn(
+        firewall_loader: &dyn FirewallConfigLoader,
+    ) -> Result<Option<SdnConfig>, Error> {
+        Ok(match firewall_loader.sdn_running_config()? {
+            Some(data) => {
+                let running_config: RunningConfig = serde_json::from_reader(data)?;
+                Some(SdnConfig::try_from(running_config)?)
+            }
+            _ => None,
+        })
+    }
+
+    pub fn parse_ipam(firewall_loader: &dyn FirewallConfigLoader) -> Result<Option<Ipam>, Error> {
+        Ok(match firewall_loader.ipam()? {
+            Some(data) => {
+                let raw_ipam: IpamJson = serde_json::from_reader(data)?;
+                Some(Ipam::try_from(raw_ipam)?)
+            }
+            _ => None,
+        })
+    }
+
     pub fn parse_nft(
         nft_loader: &dyn NftConfigLoader,
     ) -> Result<BTreeMap<String, ListChain>, Error> {
@@ -233,6 +292,8 @@ impl FirewallConfig {
             cluster_config: Self::parse_cluster(firewall_loader)?,
             host_config: Self::parse_host(firewall_loader)?,
             guest_config: Self::parse_guests(firewall_loader)?,
+            sdn_config: Self::parse_sdn(firewall_loader)?,
+            ipam_config: Self::parse_ipam(firewall_loader)?,
             nft_config: Self::parse_nft(nft_loader)?,
         })
     }
@@ -253,6 +314,14 @@ impl FirewallConfig {
         &self.nft_config
     }
 
+    pub fn sdn(&self) -> Option<&SdnConfig> {
+        self.sdn_config.as_ref()
+    }
+
+    pub fn ipam(&self) -> Option<&Ipam> {
+        self.ipam_config.as_ref()
+    }
+
     pub fn is_enabled(&self) -> bool {
         self.cluster().is_enabled() && self.host().nftables()
     }
diff --git a/proxmox-firewall/tests/input/.running-config.json b/proxmox-firewall/tests/input/.running-config.json
new file mode 100644
index 0000000..a4511f0
--- /dev/null
+++ b/proxmox-firewall/tests/input/.running-config.json
@@ -0,0 +1,45 @@
+{
+  "subnets": {
+    "ids": {
+      "test-10.101.0.0-16": {
+        "gateway": "10.101.1.1",
+        "snat": 1,
+        "vnet": "public",
+        "dhcp-range": [
+          "start-address=10.101.99.100,end-address=10.101.99.200"
+        ],
+        "type": "subnet"
+      },
+      "test-fd80::-64": {
+        "snat": 1,
+        "gateway": "fd80::1",
+        "dhcp-range": [
+          "start-address=fd80::1000,end-address=fd80::ffff"
+        ],
+        "vnet": "public",
+        "type": "subnet"
+      }
+    }
+  },
+  "version": 49,
+  "vnets": {
+    "ids": {
+      "public": {
+        "zone": "test",
+        "type": "vnet"
+      }
+    }
+  },
+  "zones": {
+    "ids": {
+      "test": {
+        "dhcp": "dnsmasq",
+        "ipam": "pve",
+        "type": "simple"
+      }
+    }
+  },
+  "controllers": {
+    "ids": {}
+  }
+}
diff --git a/proxmox-firewall/tests/input/ipam.db b/proxmox-firewall/tests/input/ipam.db
new file mode 100644
index 0000000..ac2901e
--- /dev/null
+++ b/proxmox-firewall/tests/input/ipam.db
@@ -0,0 +1,32 @@
+{
+  "zones": {
+    "public": {
+      "subnets": {
+        "10.101.0.0/16": {
+          "ips": {
+            "10.101.1.1": {
+              "gateway": 1
+            },
+            "10.101.1.100": {
+              "vmid": "101",
+              "mac": "BC:24:11:11:22:33",
+              "hostname": null
+            }
+          }
+        },
+        "fd80::/64": {
+          "ips": {
+            "fd80::1": {
+              "gateway": 1
+            },
+            "fd80::1000": {
+              "mac": "BC:24:11:11:22:33",
+              "vmid": "101",
+              "hostname": "test-vm"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/proxmox-firewall/tests/integration_tests.rs b/proxmox-firewall/tests/integration_tests.rs
index e9baffe..5de1a4e 100644
--- a/proxmox-firewall/tests/integration_tests.rs
+++ b/proxmox-firewall/tests/integration_tests.rs
@@ -69,6 +69,16 @@ impl FirewallConfigLoader for MockFirewallConfigLoader {
 
         Ok(None)
     }
+
+    fn sdn_running_config(&self) -> Result<Option<Box<dyn std::io::BufRead>>, Error> {
+        Ok(Some(Box::new(
+            include_str!("input/.running-config.json").as_bytes(),
+        )))
+    }
+
+    fn ipam(&self) -> Result<Option<Box<dyn std::io::BufRead>>, Error> {
+        Ok(Some(Box::new(include_str!("input/ipam.db").as_bytes())))
+    }
 }
 
 struct MockNftConfigLoader {}
diff --git a/proxmox-nftables/src/types.rs b/proxmox-nftables/src/types.rs
index a83e958..3101436 100644
--- a/proxmox-nftables/src/types.rs
+++ b/proxmox-nftables/src/types.rs
@@ -636,7 +636,7 @@ impl SetName {
         };
 
         let name = match name.scope() {
-            IpsetScope::Datacenter => name.to_string(),
+            IpsetScope::Datacenter | IpsetScope::Sdn => name.to_string(),
             IpsetScope::Guest => {
                 if let Some(vmid) = vmid {
                     format!("guest-{vmid}/{}", name.name())
-- 
2.39.5


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pve-devel] [PATCH proxmox-firewall v3 19/24] ipsets: autogenerate ipsets for vnets and ipam
  2024-11-12 12:25 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v3 00/24] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (17 preceding siblings ...)
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-firewall v3 18/24] config: tests: add support for loading sdn and ipam config Stefan Hanreich
@ 2024-11-12 12:25 ` Stefan Hanreich
  2024-11-12 12:25 ` [pve-devel] [PATCH pve-firewall v3 20/24] add support for loading sdn firewall configuration Stefan Hanreich
                   ` (4 subsequent siblings)
  23 siblings, 0 replies; 26+ messages in thread
From: Stefan Hanreich @ 2024-11-12 12:25 UTC (permalink / raw)
  To: pve-devel

They act like virtual ipsets, similar to ipfilter-net, that can be
used for defining firewall rules for sdn objects dynamically.

The changes in proxmox-ve-config also introduced a dedicated struct
for representing ip ranges, so we update the existing code, so that it
uses that struct as well.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-firewall/src/firewall.rs              |   22 +-
 proxmox-firewall/src/object.rs                |   41 +-
 .../integration_tests__firewall.snap          | 1288 +++++++++++++++++
 proxmox-nftables/src/expression.rs            |   17 +-
 4 files changed, 1354 insertions(+), 14 deletions(-)

diff --git a/proxmox-firewall/src/firewall.rs b/proxmox-firewall/src/firewall.rs
index 941aa20..347f3af 100644
--- a/proxmox-firewall/src/firewall.rs
+++ b/proxmox-firewall/src/firewall.rs
@@ -197,6 +197,27 @@ impl Firewall {
         self.reset_firewall(&mut commands);
 
         let cluster_host_table = Self::cluster_table();
+        let guest_table = Self::guest_table();
+
+        if let Some(sdn_config) = self.config.sdn() {
+            let ipsets = sdn_config
+                .ipsets(None)
+                .map(|ipset| (ipset.name().to_string(), ipset))
+                .collect();
+
+            self.create_ipsets(&mut commands, &ipsets, &cluster_host_table, None)?;
+            self.create_ipsets(&mut commands, &ipsets, &guest_table, None)?;
+        }
+
+        if let Some(ipam_config) = self.config.ipam() {
+            let ipsets = ipam_config
+                .ipsets(None)
+                .map(|ipset| (ipset.name().to_string(), ipset))
+                .collect();
+
+            self.create_ipsets(&mut commands, &ipsets, &cluster_host_table, None)?;
+            self.create_ipsets(&mut commands, &ipsets, &guest_table, None)?;
+        }
 
         if self.config.host().is_enabled() {
             log::info!("creating cluster / host configuration");
@@ -242,7 +263,6 @@ impl Firewall {
             commands.push(Delete::table(TableName::from(Self::cluster_table())));
         }
 
-        let guest_table = Self::guest_table();
         let enabled_guests: BTreeMap<&Vmid, &GuestConfig> = self
             .config
             .guests()
diff --git a/proxmox-firewall/src/object.rs b/proxmox-firewall/src/object.rs
index 32c4ddb..cf7e773 100644
--- a/proxmox-firewall/src/object.rs
+++ b/proxmox-firewall/src/object.rs
@@ -72,20 +72,37 @@ impl ToNftObjects for Ipset {
             let mut nomatch_elements = Vec::new();
 
             for element in self.iter() {
-                let cidr = match &element.address {
-                    IpsetAddress::Cidr(cidr) => cidr,
-                    IpsetAddress::Alias(alias) => env
-                        .alias(alias)
-                        .ok_or(format_err!("could not find alias {alias} in environment"))?
-                        .address(),
+                let expression = match &element.address {
+                    IpsetAddress::Range(range) => {
+                        if family != range.family() {
+                            continue;
+                        }
+
+                        Expression::from(range)
+                    }
+                    IpsetAddress::Cidr(cidr) => {
+                        if family != cidr.family() {
+                            continue;
+                        }
+
+                        Expression::from(Prefix::from(cidr))
+                    }
+                    IpsetAddress::Alias(alias) => {
+                        let cidr = env
+                            .alias(alias)
+                            .ok_or_else(|| {
+                                format_err!("could not find alias {alias} in environment")
+                            })?
+                            .address();
+
+                        if family != cidr.family() {
+                            continue;
+                        }
+
+                        Expression::from(Prefix::from(cidr))
+                    }
                 };
 
-                if family != cidr.family() {
-                    continue;
-                }
-
-                let expression = Expression::from(Prefix::from(cidr));
-
                 if element.nomatch {
                     nomatch_elements.push(expression);
                 } else {
diff --git a/proxmox-firewall/tests/snapshots/integration_tests__firewall.snap b/proxmox-firewall/tests/snapshots/integration_tests__firewall.snap
index 40d4405..e1b599c 100644
--- a/proxmox-firewall/tests/snapshots/integration_tests__firewall.snap
+++ b/proxmox-firewall/tests/snapshots/integration_tests__firewall.snap
@@ -202,6 +202,1294 @@ expression: "firewall.full_host_fw().expect(\"firewall can be generated\")"
         }
       }
     },
+    {
+      "add": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-sdn/public-all",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-sdn/public-all"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-sdn/public-all-nomatch",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-sdn/public-all-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-sdn/public-all",
+          "elem": [
+            {
+              "prefix": {
+                "addr": "10.101.0.0",
+                "len": 16
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-sdn/public-all",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-sdn/public-all"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-sdn/public-all-nomatch",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-sdn/public-all-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-sdn/public-all",
+          "elem": [
+            {
+              "prefix": {
+                "addr": "fd80::",
+                "len": 64
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-sdn/public-dhcp",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-sdn/public-dhcp"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-sdn/public-dhcp-nomatch",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-sdn/public-dhcp-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-sdn/public-dhcp",
+          "elem": [
+            {
+              "range": [
+                "10.101.99.100",
+                "10.101.99.200"
+              ]
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-sdn/public-dhcp",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-sdn/public-dhcp"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-sdn/public-dhcp-nomatch",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-sdn/public-dhcp-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-sdn/public-dhcp",
+          "elem": [
+            {
+              "range": [
+                "fd80::1000",
+                "fd80::ffff"
+              ]
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-sdn/public-gateway",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-sdn/public-gateway"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-sdn/public-gateway-nomatch",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-sdn/public-gateway-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-sdn/public-gateway",
+          "elem": [
+            {
+              "prefix": {
+                "addr": "10.101.1.1",
+                "len": 32
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-sdn/public-gateway",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-sdn/public-gateway"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-sdn/public-gateway-nomatch",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-sdn/public-gateway-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-sdn/public-gateway",
+          "elem": [
+            {
+              "prefix": {
+                "addr": "fd80::1",
+                "len": 128
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-sdn/public-no-gateway",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-sdn/public-no-gateway"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-sdn/public-no-gateway-nomatch",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-sdn/public-no-gateway-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-sdn/public-no-gateway",
+          "elem": [
+            {
+              "prefix": {
+                "addr": "10.101.0.0",
+                "len": 16
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-sdn/public-no-gateway-nomatch",
+          "elem": [
+            {
+              "prefix": {
+                "addr": "10.101.1.1",
+                "len": 32
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-sdn/public-no-gateway",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-sdn/public-no-gateway"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-sdn/public-no-gateway-nomatch",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-sdn/public-no-gateway-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-sdn/public-no-gateway",
+          "elem": [
+            {
+              "prefix": {
+                "addr": "fd80::",
+                "len": 64
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-sdn/public-no-gateway-nomatch",
+          "elem": [
+            {
+              "prefix": {
+                "addr": "fd80::1",
+                "len": 128
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-sdn/public-all",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-sdn/public-all"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-sdn/public-all-nomatch",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-sdn/public-all-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-sdn/public-all",
+          "elem": [
+            {
+              "prefix": {
+                "addr": "10.101.0.0",
+                "len": 16
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-sdn/public-all",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-sdn/public-all"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-sdn/public-all-nomatch",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-sdn/public-all-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-sdn/public-all",
+          "elem": [
+            {
+              "prefix": {
+                "addr": "fd80::",
+                "len": 64
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-sdn/public-dhcp",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-sdn/public-dhcp"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-sdn/public-dhcp-nomatch",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-sdn/public-dhcp-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-sdn/public-dhcp",
+          "elem": [
+            {
+              "range": [
+                "10.101.99.100",
+                "10.101.99.200"
+              ]
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-sdn/public-dhcp",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-sdn/public-dhcp"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-sdn/public-dhcp-nomatch",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-sdn/public-dhcp-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-sdn/public-dhcp",
+          "elem": [
+            {
+              "range": [
+                "fd80::1000",
+                "fd80::ffff"
+              ]
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-sdn/public-gateway",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-sdn/public-gateway"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-sdn/public-gateway-nomatch",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-sdn/public-gateway-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-sdn/public-gateway",
+          "elem": [
+            {
+              "prefix": {
+                "addr": "10.101.1.1",
+                "len": 32
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-sdn/public-gateway",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-sdn/public-gateway"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-sdn/public-gateway-nomatch",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-sdn/public-gateway-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-sdn/public-gateway",
+          "elem": [
+            {
+              "prefix": {
+                "addr": "fd80::1",
+                "len": 128
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-sdn/public-no-gateway",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-sdn/public-no-gateway"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-sdn/public-no-gateway-nomatch",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-sdn/public-no-gateway-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-sdn/public-no-gateway",
+          "elem": [
+            {
+              "prefix": {
+                "addr": "10.101.0.0",
+                "len": 16
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-sdn/public-no-gateway-nomatch",
+          "elem": [
+            {
+              "prefix": {
+                "addr": "10.101.1.1",
+                "len": 32
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-sdn/public-no-gateway",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-sdn/public-no-gateway"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-sdn/public-no-gateway-nomatch",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-sdn/public-no-gateway-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-sdn/public-no-gateway",
+          "elem": [
+            {
+              "prefix": {
+                "addr": "fd80::",
+                "len": 64
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-sdn/public-no-gateway-nomatch",
+          "elem": [
+            {
+              "prefix": {
+                "addr": "fd80::1",
+                "len": 128
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-sdn/guest-ipam-101",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-sdn/guest-ipam-101"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-sdn/guest-ipam-101-nomatch",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-sdn/guest-ipam-101-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-sdn/guest-ipam-101",
+          "elem": [
+            {
+              "prefix": {
+                "addr": "10.101.1.100",
+                "len": 32
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-sdn/guest-ipam-101",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-sdn/guest-ipam-101"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-sdn/guest-ipam-101-nomatch",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-sdn/guest-ipam-101-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-sdn/guest-ipam-101",
+          "elem": [
+            {
+              "prefix": {
+                "addr": "fd80::1000",
+                "len": 128
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-sdn/guest-ipam-101",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-sdn/guest-ipam-101"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-sdn/guest-ipam-101-nomatch",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-sdn/guest-ipam-101-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-sdn/guest-ipam-101",
+          "elem": [
+            {
+              "prefix": {
+                "addr": "10.101.1.100",
+                "len": 32
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-sdn/guest-ipam-101",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-sdn/guest-ipam-101"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-sdn/guest-ipam-101-nomatch",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-sdn/guest-ipam-101-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-sdn/guest-ipam-101",
+          "elem": [
+            {
+              "prefix": {
+                "addr": "fd80::1000",
+                "len": 128
+              }
+            }
+          ]
+        }
+      }
+    },
     {
       "add": {
         "set": {
diff --git a/proxmox-nftables/src/expression.rs b/proxmox-nftables/src/expression.rs
index 18b92d4..e56a15c 100644
--- a/proxmox-nftables/src/expression.rs
+++ b/proxmox-nftables/src/expression.rs
@@ -1,4 +1,5 @@
 use crate::types::{ElemConfig, Verdict};
+use proxmox_ve_config::firewall::types::address::IpRange;
 use serde::{Deserialize, Serialize};
 use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
 
@@ -50,6 +51,10 @@ pub enum Expression {
 }
 
 impl Expression {
+    pub fn range(start: impl Into<Expression>, last: impl Into<Expression>) -> Self {
+        Expression::Range(Box::new((start.into(), last.into())))
+    }
+
     pub fn set(expressions: impl IntoIterator<Item = Expression>) -> Self {
         Expression::Set(Vec::from_iter(expressions))
     }
@@ -169,12 +174,22 @@ impl From<&IpList> for Expression {
     }
 }
 
+#[cfg(feature = "config-ext")]
+impl From<&IpRange> for Expression {
+    fn from(value: &IpRange) -> Self {
+        match value {
+            IpRange::V4(range) => Expression::range(range.start(), range.last()),
+            IpRange::V6(range) => Expression::range(range.start(), range.last()),
+        }
+    }
+}
+
 #[cfg(feature = "config-ext")]
 impl From<&IpEntry> for Expression {
     fn from(value: &IpEntry) -> Self {
         match value {
             IpEntry::Cidr(cidr) => Expression::from(Prefix::from(cidr)),
-            IpEntry::Range(beg, end) => Expression::Range(Box::new((beg.into(), end.into()))),
+            IpEntry::Range(range) => Expression::from(range),
         }
     }
 }
-- 
2.39.5


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pve-devel] [PATCH pve-firewall v3 20/24] add support for loading sdn firewall configuration
  2024-11-12 12:25 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v3 00/24] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (18 preceding siblings ...)
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-firewall v3 19/24] ipsets: autogenerate ipsets for vnets and ipam Stefan Hanreich
@ 2024-11-12 12:25 ` Stefan Hanreich
  2024-11-12 12:25 ` [pve-devel] [PATCH pve-firewall v3 21/24] api: load sdn ipsets Stefan Hanreich
                   ` (3 subsequent siblings)
  23 siblings, 0 replies; 26+ messages in thread
From: Stefan Hanreich @ 2024-11-12 12:25 UTC (permalink / raw)
  To: pve-devel

This also includes support for parsing rules referencing IPSets in the
new SDN scope and generating those IPSets in the firewall.

Loading SDN configuration is optional, since loading it requires root
privileges which we do not have in all call sites. Adding the flag
allows us to selectively load the SDN configuration only where
required and at the same time allows us to only elevate privileges in
the API where absolutely needed.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 src/PVE/Firewall.pm | 59 +++++++++++++++++++++++++++++++++++++++------
 1 file changed, 52 insertions(+), 7 deletions(-)

diff --git a/src/PVE/Firewall.pm b/src/PVE/Firewall.pm
index 09544ba..6e02873 100644
--- a/src/PVE/Firewall.pm
+++ b/src/PVE/Firewall.pm
@@ -25,6 +25,7 @@ use PVE::Tools qw($IPV4RE $IPV6RE);
 use PVE::Tools qw(run_command lock_file dir_glob_foreach);
 
 use PVE::Firewall::Helpers;
+use PVE::RS::Firewall::SDN;
 
 my $pvefw_conf_dir = "/etc/pve/firewall";
 my $clusterfw_conf_filename = "$pvefw_conf_dir/cluster.fw";
@@ -1689,9 +1690,11 @@ sub verify_rule {
 
 	if (my $value = $rule->{$name}) {
 	    if ($value =~ m/^\+/) {
-		if ($value =~ m@^\+(guest/|dc/)?(${ipset_name_pattern})$@) {
+		if ($value =~ m@^\+(guest/|dc/|sdn/)?(${ipset_name_pattern})$@) {
 		    &$add_error($name, "no such ipset '$2'")
-			if !($cluster_conf->{ipset}->{$2} || ($fw_conf && $fw_conf->{ipset}->{$2}));
+			if !($cluster_conf->{ipset}->{$2}
+			    || ($fw_conf && $fw_conf->{ipset}->{$2})
+			    || ($cluster_conf->{sdn} && $cluster_conf->{sdn}->{ipset}->{$2}));
 
 		} else {
 		    &$add_error($name, "invalid ipset name '$value'");
@@ -2108,13 +2111,15 @@ sub ipt_gen_src_or_dst_match {
 
     my $match;
     if ($adr =~ m/^\+/) {
-	if ($adr =~ m@^\+(guest/|dc/)?(${ipset_name_pattern})$@) {
+	if ($adr =~ m@^\+(guest/|dc/|sdn/)?(${ipset_name_pattern})$@) {
 	    my $scope = $1 // "";
 	    my $name = $2;
 	    my $ipset_chain;
-	    if ($scope ne 'dc/' && $fw_conf && $fw_conf->{ipset}->{$name}) {
+	    if ((!$scope || $scope eq 'guest/') && $fw_conf && $fw_conf->{ipset}->{$name}) {
 		$ipset_chain = compute_ipset_chain_name($fw_conf->{vmid}, $name, $ipversion);
-	    } elsif ($scope ne 'guest/' && $cluster_conf && $cluster_conf->{ipset}->{$name}) {
+	    } elsif ((!$scope || $scope eq 'dc/') && $cluster_conf && $cluster_conf->{ipset}->{$name}) {
+		$ipset_chain = compute_ipset_chain_name(0, $name, $ipversion);
+	    } elsif ((!$scope || $scope eq 'sdn/') && $cluster_conf->{sdn} && $cluster_conf->{sdn}->{ipset}->{$name}) {
 		$ipset_chain = compute_ipset_chain_name(0, $name, $ipversion);
 	    } else {
 		die "no such ipset '$name'\n";
@@ -3644,7 +3649,12 @@ sub lock_clusterfw_conf {
 }
 
 sub load_clusterfw_conf {
-    my ($filename) = @_;
+    my ($filename, $options) = @_;
+
+    my $sdn_conf = {};
+    if ($options->{load_sdn_config}) {
+	$sdn_conf = load_sdn_conf();
+    }
 
     $filename = $clusterfw_conf_filename if !defined($filename);
     my $empty_conf = {
@@ -3655,6 +3665,7 @@ sub load_clusterfw_conf {
 	group_comments => {},
 	ipset => {} ,
 	ipset_comments => {},
+	sdn => $sdn_conf,
     };
 
     my $cluster_conf = generic_fw_config_parser($filename, $empty_conf, $empty_conf, 'cluster');
@@ -3663,6 +3674,39 @@ sub load_clusterfw_conf {
     return $cluster_conf;
 }
 
+sub load_sdn_conf {
+    my $rpcenv = PVE::RPCEnvironment::get();
+    my $authuser = $rpcenv->get_user();
+
+    my $guests = PVE::Cluster::get_vmlist();
+    my $allowed_vms = [];
+    foreach my $vmid (sort keys %{$guests->{ids}}) {
+	next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ], 1);
+	push @$allowed_vms, $vmid;
+    }
+
+    my $vnets = PVE::Network::SDN::Vnets::config(1);
+    my $privs = [ 'SDN.Audit', 'SDN.Allocate' ];
+    my $allowed_vnets = [];
+    foreach my $vnet (sort keys %{$vnets->{ids}}) {
+	my $zone = $vnets->{ids}->{$vnet}->{zone};
+	next if !$rpcenv->check_any($authuser, "/sdn/zones/$zone/$vnet", $privs, 1);
+	push @$allowed_vnets, $vnet;
+    }
+
+    my $sdn_config = {
+	ipset => {} ,
+	ipset_comments => {},
+    };
+
+    eval {
+	$sdn_config = PVE::RS::Firewall::SDN::config($allowed_vnets, $allowed_vms);
+    };
+    warn $@ if $@;
+
+    return $sdn_config;
+}
+
 sub save_clusterfw_conf {
     my ($cluster_conf) = @_;
 
@@ -4043,6 +4087,7 @@ sub compile_ipsets {
     }
 
     generate_ipset_chains($ipset_ruleset, undef, $cluster_conf, undef, $cluster_conf->{ipset});
+    generate_ipset_chains($ipset_ruleset, undef, $cluster_conf, undef, $cluster_conf->{sdn}->{ipset});
 
     return $ipset_ruleset;
 }
@@ -4731,7 +4776,7 @@ sub init {
 sub update {
     my $code = sub {
 
-	my $cluster_conf = load_clusterfw_conf();
+	my $cluster_conf = load_clusterfw_conf(undef, { load_sdn_config => 1 });
 	my $hostfw_conf = load_hostfw_conf($cluster_conf);
 
 	if (!is_enabled_and_not_nftables($cluster_conf, $hostfw_conf)) {
-- 
2.39.5


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pve-devel] [PATCH pve-firewall v3 21/24] api: load sdn ipsets
  2024-11-12 12:25 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v3 00/24] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (19 preceding siblings ...)
  2024-11-12 12:25 ` [pve-devel] [PATCH pve-firewall v3 20/24] add support for loading sdn firewall configuration Stefan Hanreich
@ 2024-11-12 12:25 ` Stefan Hanreich
  2024-11-12 12:26 ` [pve-devel] [PATCH proxmox-perl-rs v3 22/24] add PVE::RS::Firewall::SDN module Stefan Hanreich
                   ` (2 subsequent siblings)
  23 siblings, 0 replies; 26+ messages in thread
From: Stefan Hanreich @ 2024-11-12 12:25 UTC (permalink / raw)
  To: pve-devel

Since the SDN configuration reads the IPAM config file, which resides
in /etc/pve/priv we need to add the protected flag to several
endpoints.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 src/PVE/API2/Firewall/Cluster.pm |  8 ++++++--
 src/PVE/API2/Firewall/Rules.pm   | 12 +++++++-----
 src/PVE/API2/Firewall/VM.pm      |  3 ++-
 3 files changed, 15 insertions(+), 8 deletions(-)

diff --git a/src/PVE/API2/Firewall/Cluster.pm b/src/PVE/API2/Firewall/Cluster.pm
index 48ad90d..d5e8268 100644
--- a/src/PVE/API2/Firewall/Cluster.pm
+++ b/src/PVE/API2/Firewall/Cluster.pm
@@ -214,6 +214,7 @@ __PACKAGE__->register_method({
     permissions => {
 	check => ['perm', '/', [ 'Sys.Audit' ]],
     },
+    protected => 1,
     parameters => {
 	additionalProperties => 0,
 	properties => {
@@ -253,9 +254,12 @@ __PACKAGE__->register_method({
     code => sub {
 	my ($param) = @_;
 
-	my $conf = PVE::Firewall::load_clusterfw_conf();
+	my $conf = PVE::Firewall::load_clusterfw_conf(undef, { load_sdn_config => 1 });
+
+	my $cluster_refs = PVE::Firewall::Helpers::collect_refs($conf, $param->{type}, "dc");
+	my $sdn_refs = PVE::Firewall::Helpers::collect_refs($conf->{sdn}, $param->{type}, "sdn");
 
-	return PVE::Firewall::Helpers::collect_refs($conf, $param->{type}, "dc");
+	return [@$sdn_refs, @$cluster_refs];
     }});
 
 1;
diff --git a/src/PVE/API2/Firewall/Rules.pm b/src/PVE/API2/Firewall/Rules.pm
index 9fcfb20..f5cb002 100644
--- a/src/PVE/API2/Firewall/Rules.pm
+++ b/src/PVE/API2/Firewall/Rules.pm
@@ -72,6 +72,7 @@ sub register_get_rules {
 	path => '',
 	method => 'GET',
 	description => "List rules.",
+	protected => 1,
 	permissions => PVE::Firewall::rules_audit_permissions($rule_env),
 	parameters => {
 	    additionalProperties => 0,
@@ -120,6 +121,7 @@ sub register_get_rule {
 	path => '{pos}',
 	method => 'GET',
 	description => "Get single rule data.",
+	protected => 1,
 	permissions => PVE::Firewall::rules_audit_permissions($rule_env),
 	parameters => {
 	    additionalProperties => 0,
@@ -412,7 +414,7 @@ sub lock_config {
 sub load_config {
     my ($class, $param) = @_;
 
-    my $fw_conf = PVE::Firewall::load_clusterfw_conf();
+    my $fw_conf = PVE::Firewall::load_clusterfw_conf(undef, { load_sdn_config => 1 });
     my $rules = $fw_conf->{groups}->{$param->{group}};
     die "no such security group '$param->{group}'\n" if !defined($rules);
 
@@ -488,7 +490,7 @@ sub lock_config {
 sub load_config {
     my ($class, $param) = @_;
 
-    my $fw_conf = PVE::Firewall::load_clusterfw_conf();
+    my $fw_conf = PVE::Firewall::load_clusterfw_conf(undef, { load_sdn_config => 1 });
     my $rules = $fw_conf->{rules};
 
     return (undef, $fw_conf, $rules);
@@ -528,7 +530,7 @@ sub lock_config {
 sub load_config {
     my ($class, $param) = @_;
 
-    my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
+    my $cluster_conf = PVE::Firewall::load_clusterfw_conf(undef, { load_sdn_config => 1 });
     my $fw_conf = PVE::Firewall::load_hostfw_conf($cluster_conf);
     my $rules = $fw_conf->{rules};
 
@@ -572,7 +574,7 @@ sub lock_config {
 sub load_config {
     my ($class, $param) = @_;
 
-    my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
+    my $cluster_conf = PVE::Firewall::load_clusterfw_conf(undef, { load_sdn_config => 1 });
     my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, 'vm', $param->{vmid});
     my $rules = $fw_conf->{rules};
 
@@ -616,7 +618,7 @@ sub lock_config {
 sub load_config {
     my ($class, $param) = @_;
 
-    my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
+    my $cluster_conf = PVE::Firewall::load_clusterfw_conf(undef, { load_sdn_config => 1 });
     my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, 'ct', $param->{vmid});
     my $rules = $fw_conf->{rules};
 
diff --git a/src/PVE/API2/Firewall/VM.pm b/src/PVE/API2/Firewall/VM.pm
index 4222103..4df725d 100644
--- a/src/PVE/API2/Firewall/VM.pm
+++ b/src/PVE/API2/Firewall/VM.pm
@@ -234,6 +234,7 @@ sub register_handlers {
 	path => 'refs',
 	method => 'GET',
 	description => "Lists possible IPSet/Alias reference which are allowed in source/dest properties.",
+	protected => 1,
 	permissions => {
 	    check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
 	},
@@ -278,7 +279,7 @@ sub register_handlers {
 	code => sub {
 	    my ($param) = @_;
 
-	    my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
+	    my $cluster_conf = PVE::Firewall::load_clusterfw_conf(undef, { load_sdn_config => 1 });
 	    my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, $rule_env, $param->{vmid});
 
 	    my $dc_refs = PVE::Firewall::Helpers::collect_refs($cluster_conf, $param->{type}, 'dc');
-- 
2.39.5


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pve-devel] [PATCH proxmox-perl-rs v3 22/24] add PVE::RS::Firewall::SDN module
  2024-11-12 12:25 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v3 00/24] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (20 preceding siblings ...)
  2024-11-12 12:25 ` [pve-devel] [PATCH pve-firewall v3 21/24] api: load sdn ipsets Stefan Hanreich
@ 2024-11-12 12:26 ` Stefan Hanreich
  2024-11-12 12:26 ` [pve-devel] [PATCH pve-manager v3 23/24] firewall: add sdn scope to IPRefSelector Stefan Hanreich
  2024-11-12 12:26 ` [pve-devel] [PATCH pve-docs v3 24/24] sdn: add documentation for firewall integration Stefan Hanreich
  23 siblings, 0 replies; 26+ messages in thread
From: Stefan Hanreich @ 2024-11-12 12:26 UTC (permalink / raw)
  To: pve-devel

Used for obtaining the IPSets that get autogenerated by the nftables
firewall. The returned configuration has the same format as the
pve-firewall uses internally, making it compatible with the existing
pve-firewall code.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 pve-rs/Cargo.toml          |   1 +
 pve-rs/Makefile            |   1 +
 pve-rs/src/firewall/mod.rs |   1 +
 pve-rs/src/firewall/sdn.rs | 130 +++++++++++++++++++++++++++++++++++++
 pve-rs/src/lib.rs          |   1 +
 5 files changed, 134 insertions(+)
 create mode 100644 pve-rs/src/firewall/mod.rs
 create mode 100644 pve-rs/src/firewall/sdn.rs

diff --git a/pve-rs/Cargo.toml b/pve-rs/Cargo.toml
index 9470cb7..c0af2b3 100644
--- a/pve-rs/Cargo.toml
+++ b/pve-rs/Cargo.toml
@@ -45,3 +45,4 @@ proxmox-subscription = "0.5"
 proxmox-sys = "0.6"
 proxmox-tfa = { version = "5", features = ["api"] }
 proxmox-time = "2"
+proxmox-ve-config = { version = "0.1.0" }
diff --git a/pve-rs/Makefile b/pve-rs/Makefile
index c6b4e08..d01da69 100644
--- a/pve-rs/Makefile
+++ b/pve-rs/Makefile
@@ -28,6 +28,7 @@ PERLMOD_GENPACKAGE := /usr/lib/perlmod/genpackage.pl \
 
 PERLMOD_PACKAGES := \
 	  PVE::RS::APT::Repositories \
+	  PVE::RS::Firewall::SDN \
 	  PVE::RS::OpenId \
 	  PVE::RS::ResourceScheduling::Static \
 	  PVE::RS::TFA
diff --git a/pve-rs/src/firewall/mod.rs b/pve-rs/src/firewall/mod.rs
new file mode 100644
index 0000000..8bd18a8
--- /dev/null
+++ b/pve-rs/src/firewall/mod.rs
@@ -0,0 +1 @@
+pub mod sdn;
diff --git a/pve-rs/src/firewall/sdn.rs b/pve-rs/src/firewall/sdn.rs
new file mode 100644
index 0000000..5049f74
--- /dev/null
+++ b/pve-rs/src/firewall/sdn.rs
@@ -0,0 +1,130 @@
+#[perlmod::package(name = "PVE::RS::Firewall::SDN", lib = "pve_rs")]
+mod export {
+    use std::collections::HashMap;
+    use std::{fs, io};
+
+    use anyhow::{bail, Context, Error};
+    use serde::Serialize;
+
+    use proxmox_ve_config::{
+        common::Allowlist,
+        firewall::types::ipset::{IpsetAddress, IpsetEntry},
+        firewall::types::Ipset,
+        guest::types::Vmid,
+        sdn::{
+            config::{RunningConfig, SdnConfig},
+            ipam::{Ipam, IpamJson},
+            VnetName,
+        },
+    };
+
+    #[derive(Clone, Debug, Default, Serialize)]
+    pub struct LegacyIpsetEntry {
+        nomatch: bool,
+        cidr: String,
+        comment: Option<String>,
+    }
+
+    impl LegacyIpsetEntry {
+        pub fn from_ipset_entry(entry: &IpsetEntry) -> Vec<LegacyIpsetEntry> {
+            let mut entries = Vec::new();
+
+            match &entry.address {
+                IpsetAddress::Alias(name) => {
+                    entries.push(Self {
+                        nomatch: entry.nomatch,
+                        cidr: name.to_string(),
+                        comment: entry.comment.clone(),
+                    });
+                }
+                IpsetAddress::Cidr(cidr) => {
+                    entries.push(Self {
+                        nomatch: entry.nomatch,
+                        cidr: cidr.to_string(),
+                        comment: entry.comment.clone(),
+                    });
+                }
+                IpsetAddress::Range(range) => {
+                    entries.extend(range.to_cidrs().into_iter().map(|cidr| Self {
+                        nomatch: entry.nomatch,
+                        cidr: cidr.to_string(),
+                        comment: entry.comment.clone(),
+                    }))
+                }
+            };
+
+            entries
+        }
+    }
+
+    #[derive(Clone, Debug, Default, Serialize)]
+    pub struct SdnFirewallConfig {
+        ipset: HashMap<String, Vec<LegacyIpsetEntry>>,
+        ipset_comments: HashMap<String, String>,
+    }
+
+    impl SdnFirewallConfig {
+        pub fn new() -> Self {
+            Default::default()
+        }
+
+        pub fn extend_ipsets(&mut self, ipsets: impl IntoIterator<Item = Ipset>) {
+            for ipset in ipsets {
+                let entries = ipset
+                    .iter()
+                    .flat_map(LegacyIpsetEntry::from_ipset_entry)
+                    .collect();
+
+                self.ipset.insert(ipset.name().name().to_string(), entries);
+
+                if let Some(comment) = &ipset.comment {
+                    self.ipset_comments
+                        .insert(ipset.name().name().to_string(), comment.to_string());
+                }
+            }
+        }
+    }
+
+    const SDN_RUNNING_CONFIG: &str = "/etc/pve/sdn/.running-config";
+    const SDN_IPAM: &str = "/etc/pve/priv/ipam.db";
+
+    #[export]
+    pub fn config(
+        vnet_filter: Option<Vec<VnetName>>,
+        vm_filter: Option<Vec<Vmid>>,
+    ) -> Result<SdnFirewallConfig, Error> {
+        let mut refs = SdnFirewallConfig::new();
+
+        match fs::read_to_string(SDN_RUNNING_CONFIG) {
+            Ok(data) => {
+                let running_config: RunningConfig = serde_json::from_str(&data)?;
+                let sdn_config = SdnConfig::try_from(running_config)
+                    .with_context(|| "Failed to parse SDN config".to_string())?;
+
+                let allowlist = vnet_filter.map(Allowlist::from_iter);
+                refs.extend_ipsets(sdn_config.ipsets(allowlist.as_ref()));
+            }
+            Err(e) if e.kind() == io::ErrorKind::NotFound => (),
+            Err(e) => {
+                bail!("Cannot open SDN running config: {e:#}");
+            }
+        };
+
+        match fs::read_to_string(SDN_IPAM) {
+            Ok(data) => {
+                let ipam_json: IpamJson = serde_json::from_str(&data)?;
+                let ipam: Ipam = Ipam::try_from(ipam_json)
+                    .with_context(|| "Failed to parse IPAM".to_string())?;
+
+                let allowlist = vm_filter.map(Allowlist::from_iter);
+                refs.extend_ipsets(ipam.ipsets(allowlist.as_ref()));
+            }
+            Err(e) if e.kind() == io::ErrorKind::NotFound => (),
+            Err(e) => {
+                bail!("Cannot open IPAM database: {e:#}");
+            }
+        };
+
+        Ok(refs)
+    }
+}
diff --git a/pve-rs/src/lib.rs b/pve-rs/src/lib.rs
index 5e47ac6..3de37d1 100644
--- a/pve-rs/src/lib.rs
+++ b/pve-rs/src/lib.rs
@@ -12,6 +12,7 @@ use proxmox_notify::{Config, Notification, Severity};
 pub mod common;
 
 pub mod apt;
+pub mod firewall;
 pub mod openid;
 pub mod resource_scheduling;
 pub mod tfa;
-- 
2.39.5


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pve-devel] [PATCH pve-manager v3 23/24] firewall: add sdn scope to IPRefSelector
  2024-11-12 12:25 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v3 00/24] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (21 preceding siblings ...)
  2024-11-12 12:26 ` [pve-devel] [PATCH proxmox-perl-rs v3 22/24] add PVE::RS::Firewall::SDN module Stefan Hanreich
@ 2024-11-12 12:26 ` Stefan Hanreich
  2024-11-12 12:26 ` [pve-devel] [PATCH pve-docs v3 24/24] sdn: add documentation for firewall integration Stefan Hanreich
  23 siblings, 0 replies; 26+ messages in thread
From: Stefan Hanreich @ 2024-11-12 12:26 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 www/manager6/form/IPRefSelector.js | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/www/manager6/form/IPRefSelector.js b/www/manager6/form/IPRefSelector.js
index d41cde5f5..16078e428 100644
--- a/www/manager6/form/IPRefSelector.js
+++ b/www/manager6/form/IPRefSelector.js
@@ -67,6 +67,12 @@ Ext.define('PVE.form.IPRefSelector', {
 	    });
 	}
 
+	let scopes = {
+	    'dc': gettext("Datacenter"),
+	    'guest': gettext("Guest"),
+	    'sdn': gettext("SDN"),
+	};
+
 	columns.push(
 	    {
 		header: gettext('Name'),
@@ -80,7 +86,7 @@ Ext.define('PVE.form.IPRefSelector', {
 		hideable: false,
 		width: 140,
 		renderer: function(value) {
-		    return value === 'dc' ? gettext("Datacenter") : gettext("Guest");
+		    return scopes[value] ?? "unknown scope";
 		},
 	    },
 	    {
-- 
2.39.5


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pve-devel] [PATCH pve-docs v3 24/24] sdn: add documentation for firewall integration
  2024-11-12 12:25 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v3 00/24] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (22 preceding siblings ...)
  2024-11-12 12:26 ` [pve-devel] [PATCH pve-manager v3 23/24] firewall: add sdn scope to IPRefSelector Stefan Hanreich
@ 2024-11-12 12:26 ` Stefan Hanreich
  23 siblings, 0 replies; 26+ messages in thread
From: Stefan Hanreich @ 2024-11-12 12:26 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 pvesdn.adoc | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 92 insertions(+)

diff --git a/pvesdn.adoc b/pvesdn.adoc
index 39de80f..c187365 100644
--- a/pvesdn.adoc
+++ b/pvesdn.adoc
@@ -702,6 +702,98 @@ For more information please consult the documentation of
 xref:pvesdn_ipam_plugin_pveipam[the PVE IPAM plugin]. Changing DHCP leases is
 currently not supported for the other IPAM plugins.
 
+Firewall Integration
+--------------------
+
+SDN integrates with the Proxmox VE firewall by automatically generating IPSets
+which can then be referenced in the source / destination fields of firewall
+rules. This happens automatically for VNets and IPAM entries.
+
+VNets and Subnets
+~~~~~~~~~~~~~~~~~
+
+The firewall automatically generates the following IPSets in the SDN scope for
+every VNet:
+
+`vnet-all`::
+  Contains the CIDRs of all subnets in a VNet
+`vnet-gateway`::
+  Contains the IPs of the gateways of all subnets in a VNet
+`vnet-no-gateway`::
+  Contains the CIDRs of all subnets in a VNet, but excludes the gateways
+`vnet-dhcp`::
+  Contains all DHCP ranges configured in the subnets in a VNet
+
+When making changes to your configuration, the IPSets update automatically, so
+you do not have to update your firewall rules when changing the configuration of
+your Subnets.
+
+Simple Zone Example
+^^^^^^^^^^^^^^^^^^^
+
+Assuming the configuration below for a VNet and its contained subnets:
+
+----
+# /etc/pve/sdn/vnets.cfg
+
+vnet: vnet0
+        zone simple
+
+# /etc/pve/sdn/subnets.cfg
+
+subnet: simple-192.0.2.0-24
+        vnet vnet0
+        dhcp-range start-address=192.0.2.100,end-address=192.0.2.199
+        gateway 192.0.2.1
+
+subnet: simple-2001:db8::-64
+        vnet vnet0
+        dhcp-range start-address=2001:db8::1000,end-address=2001:db8::1999
+        gateway 2001:db8::1
+----
+
+In this example we configured an IPv4 subnet in the VNet `vnet0`, with
+'192.0.2.0/24' as its IP Range, '192.0.2.1' as the gateway and the DHCP range is
+'192.0.2.100' - '192.0.2.199'.
+
+Additionally we configured an IPv6 subnet with '2001:db8::/64' as the IP range,
+'2001:db8::1' as the gateway and a DHCP range of '2001:db8::1000' -
+'2001:db8::1999'.
+
+The respective auto-generated IPsets for vnet0 would then contain the following
+elements:
+
+`vnet0-all`::
+* '192.0.2.0/24'
+* '2001:db8::/64'
+`vnet0-gateway`::
+* '192.0.2.1'
+* '2001:db8::1'
+`vnet0-no-gateway`::
+* '192.0.2.0/24'
+* '2001:db8::/64'
+* '!192.0.2.1'
+* '!2001:db8::1'
+`vnet0-dhcp`::
+* '192.0.2.100 - 192.0.2.199'
+* '2001:db8::1000 - 2001:db8::1999'
+
+IPAM
+~~~~
+
+If you are using the built-in PVE IPAM, then the firewall automatically
+generates an IPset for every guest that has entries in the IPAM. The respective
+IPset for a guest with ID 100 would be `guest-ipam-100`. It contains all IP
+addresses from all IPAM entries. So if guest 100 is member of multiple VNets,
+then the IPset would contain the IPs from *all* VNets.
+
+When entries get added / updated / deleted, then the respective IPSets will be
+updated accordingly.
+
+WARNING: When removing all entries for a guest and there are firewall rules
+still referencing the auto-generated IPSet then the firewall will fail to update
+the ruleset, since it references a non-existing IPSet.
+
 [[pvesdn_setup_examples]]
 Examples
 --------
-- 
2.39.5


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pve-devel] partially-applied-series: [PATCH proxmox-ve-rs v3 16/24] tests: add ipam tests
  2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 16/24] tests: add ipam tests Stefan Hanreich
@ 2024-11-12 19:16   ` Thomas Lamprecht
  0 siblings, 0 replies; 26+ messages in thread
From: Thomas Lamprecht @ 2024-11-12 19:16 UTC (permalink / raw)
  To: Proxmox VE development discussion, Stefan Hanreich

Am 12.11.24 um 13:25 schrieb Stefan Hanreich:
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
>  proxmox-ve-config/tests/sdn/main.rs           | 45 +++++++++++++++++++
>  proxmox-ve-config/tests/sdn/resources/ipam.db | 26 +++++++++++
>  2 files changed, 71 insertions(+)
>  create mode 100644 proxmox-ve-config/tests/sdn/resources/ipam.db
> 
>

applied the proxmox-ve-rs patrches on top of your stafff repo, thanks!

I already mirrored it publicly [0], but as long as we do not yet use it anywhere
(i.e., apply the rest of your patches) I'm fine with  force-pushing, if really
necesarry, or the like.

[0]: https://git.proxmox.com/?p=proxmox-ve-rs.git;a=summary


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 26+ messages in thread

end of thread, other threads:[~2024-11-12 19:16 UTC | newest]

Thread overview: 26+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2024-11-12 12:25 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v3 00/24] autogenerate ipsets for sdn objects Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 01/24] debian: add files for packaging Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 02/24] firewall: add sdn scope for ipsets Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 03/24] firewall: add ip range types Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 04/24] firewall: address: use new iprange type for ip entries Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 05/24] ipset: add range variant to addresses Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 06/24] iprange: add methods for converting an ip range to cidrs Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 07/24] ipset: address: add helper methods Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 08/24] firewall: guest: derive traits according to rust api guidelines Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 09/24] common: add allowlist Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 10/24] sdn: add name types Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 11/24] sdn: add ipam module Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 12/24] sdn: ipam: add method for generating ipsets Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 13/24] sdn: add config module Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 14/24] sdn: config: add method for generating ipsets Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 15/24] tests: add sdn config tests Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 16/24] tests: add ipam tests Stefan Hanreich
2024-11-12 19:16   ` [pve-devel] partially-applied-series: " Thomas Lamprecht
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-firewall v3 17/24] add proxmox-ve-rs crate - move proxmox-ve-config there Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-firewall v3 18/24] config: tests: add support for loading sdn and ipam config Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-firewall v3 19/24] ipsets: autogenerate ipsets for vnets and ipam Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH pve-firewall v3 20/24] add support for loading sdn firewall configuration Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH pve-firewall v3 21/24] api: load sdn ipsets Stefan Hanreich
2024-11-12 12:26 ` [pve-devel] [PATCH proxmox-perl-rs v3 22/24] add PVE::RS::Firewall::SDN module Stefan Hanreich
2024-11-12 12:26 ` [pve-devel] [PATCH pve-manager v3 23/24] firewall: add sdn scope to IPRefSelector Stefan Hanreich
2024-11-12 12:26 ` [pve-devel] [PATCH pve-docs v3 24/24] sdn: add documentation for firewall integration Stefan Hanreich

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal