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} v2 00/25] autogenerate ipsets for sdn objects
@ 2024-10-10 15:56 Stefan Hanreich
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 01/25] debian: add files for packaging Stefan Hanreich
                   ` (24 more replies)
  0 siblings, 25 replies; 31+ messages in thread
From: Stefan Hanreich @ 2024-10-10 15:56 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.

This patch series is based on my private repositories that split the existing
proxmox-firewall package into proxmox-firewall and proxmox-ve-rs. Those can be
found in my staff repo:

staff/s.hanreich/proxmox-ve-rs.git master
staff/s.hanreich/proxmox-firewall.git no-config

Please note that I included the debian packaging commit in this patch series,
since it is new and should get reviewed as well, I suppose. It is already
included when pulling from the proxmox-ve-rs repository.

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

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

proxmox-ve-rs:

Fabian Grünbichler (1):
  bump serde_with to 3

Stefan Hanreich (17):
  debian: add files for packaging
  bump dependencies
  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                  |   18 +-
 proxmox-ve-config/debian/changelog            |    5 +
 proxmox-ve-config/debian/control              |   43 +
 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           |  642 +++++++++
 proxmox-ve-config/src/sdn/ipam.rs             |  382 ++++++
 proxmox-ve-config/src/sdn/mod.rs              |  243 ++++
 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, 2980 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 (2):
  config: tests: add support for loading sdn and ipam config
  ipsets: autogenerate ipsets for vnets and ipam

 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/src/expression.rs            |   17 +-
 proxmox-nftables/src/types.rs                 |    2 +-
 9 files changed, 1511 insertions(+), 15 deletions(-)
 create mode 100644 proxmox-firewall/tests/input/.running-config.json
 create mode 100644 proxmox-firewall/tests/input/ipam.db


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:
  45 files changed, 4791 insertions(+), 118 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] 31+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v2 01/25] debian: add files for packaging
  2024-10-10 15:56 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v2 00/25] autogenerate ipsets for sdn objects Stefan Hanreich
@ 2024-10-10 15:56 ` Stefan Hanreich
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 02/25] bump serde_with to 3 Stefan Hanreich
                   ` (23 subsequent siblings)
  24 siblings, 0 replies; 31+ messages in thread
From: Stefan Hanreich @ 2024-10-10 15:56 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           | 16 +++---
 proxmox-ve-config/debian/changelog     |  5 ++
 proxmox-ve-config/debian/control       | 43 ++++++++++++++++
 proxmox-ve-config/debian/copyright     | 19 +++++++
 proxmox-ve-config/debian/debcargo.toml |  4 ++
 11 files changed, 255 insertions(+), 10 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..ab23d89
--- /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.70"
+
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..ab8a7a0 100644
--- a/proxmox-ve-config/Cargo.toml
+++ b/proxmox-ve-config/Cargo.toml
@@ -1,14 +1,10 @@
 [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"
@@ -20,6 +16,6 @@ serde_json = "1"
 serde_plain = "1"
 serde_with = "2.3.3"
 
-proxmox-schema = "3.1.0"
-proxmox-sys = "0.5.3"
+proxmox-schema = "3.1.1"
+proxmox-sys = "0.5.8"
 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..97f5e54
--- /dev/null
+++ b/proxmox-ve-config/debian/control
@@ -0,0 +1,43 @@
+Source: proxmox-ve-config
+Section: rust
+Priority: optional
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Build-Depends: cargo:native,
+               librust-anyhow-1+default-dev,
+               librust-log-0.4+default-dev (>= 0.4.17-~~),
+               librust-nix-0.26+default-dev (>= 0.26.1-~~),
+               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,
+Standards-Version: 4.6.2
+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-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: Proxmox's nftables-based firewall written in rust
+ This package contains a nftables-based implementation of the Proxmox VE
+ Firewall
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] 31+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v2 02/25] bump serde_with to 3
  2024-10-10 15:56 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v2 00/25] autogenerate ipsets for sdn objects Stefan Hanreich
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 01/25] debian: add files for packaging Stefan Hanreich
@ 2024-10-10 15:56 ` Stefan Hanreich
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 03/25] bump dependencies Stefan Hanreich
                   ` (22 subsequent siblings)
  24 siblings, 0 replies; 31+ messages in thread
From: Stefan Hanreich @ 2024-10-10 15:56 UTC (permalink / raw)
  To: pve-devel

From: Fabian Grünbichler <f.gruenbichler@proxmox.com>

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/Cargo.toml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml
index ab8a7a0..5f11bf9 100644
--- a/proxmox-ve-config/Cargo.toml
+++ b/proxmox-ve-config/Cargo.toml
@@ -14,7 +14,7 @@ nix = "0.26"
 serde = { version = "1", features = [ "derive" ] }
 serde_json = "1"
 serde_plain = "1"
-serde_with = "2.3.3"
+serde_with = "3"
 
 proxmox-schema = "3.1.1"
 proxmox-sys = "0.5.8"
-- 
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] 31+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v2 03/25] bump dependencies
  2024-10-10 15:56 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v2 00/25] autogenerate ipsets for sdn objects Stefan Hanreich
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 01/25] debian: add files for packaging Stefan Hanreich
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 02/25] bump serde_with to 3 Stefan Hanreich
@ 2024-10-10 15:56 ` Stefan Hanreich
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 04/25] firewall: add sdn scope for ipsets Stefan Hanreich
                   ` (21 subsequent siblings)
  24 siblings, 0 replies; 31+ messages in thread
From: Stefan Hanreich @ 2024-10-10 15:56 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/Cargo.toml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml
index 5f11bf9..79ba164 100644
--- a/proxmox-ve-config/Cargo.toml
+++ b/proxmox-ve-config/Cargo.toml
@@ -16,6 +16,6 @@ serde_json = "1"
 serde_plain = "1"
 serde_with = "3"
 
-proxmox-schema = "3.1.1"
-proxmox-sys = "0.5.8"
+proxmox-schema = "3.1.2"
+proxmox-sys = "0.6.4"
 proxmox-sortable-macro = "0.1.3"
-- 
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] 31+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v2 04/25] firewall: add sdn scope for ipsets
  2024-10-10 15:56 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v2 00/25] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (2 preceding siblings ...)
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 03/25] bump dependencies Stefan Hanreich
@ 2024-10-10 15:56 ` Stefan Hanreich
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 05/25] firewall: add ip range types Stefan Hanreich
                   ` (20 subsequent siblings)
  24 siblings, 0 replies; 31+ messages in thread
From: Stefan Hanreich @ 2024-10-10 15:56 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] 31+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v2 05/25] firewall: add ip range types
  2024-10-10 15:56 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v2 00/25] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (3 preceding siblings ...)
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 04/25] firewall: add sdn scope for ipsets Stefan Hanreich
@ 2024-10-10 15:56 ` Stefan Hanreich
  2024-11-06 13:13   ` Wolfgang Bumiller
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 06/25] firewall: address: use new iprange type for ip entries Stefan Hanreich
                   ` (19 subsequent siblings)
  24 siblings, 1 reply; 31+ messages in thread
From: Stefan Hanreich @ 2024-10-10 15:56 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..42ec1a1 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,
+    StartGreaterThanEnd,
+    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::StartGreaterThanEnd => "start is greater than end",
+            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 end IP address are not from the same family.
+    pub fn new(start: impl Into<IpAddr>, end: impl Into<IpAddr>) -> Result<Self, IpRangeError> {
+        match (start.into(), end.into()) {
+            (IpAddr::V4(start), IpAddr::V4(end)) => Self::new_v4(start, end),
+            (IpAddr::V6(start), IpAddr::V6(end)) => Self::new_v6(start, end),
+            _ => Err(IpRangeError::MismatchedFamilies),
+        }
+    }
+
+    /// construct a new Ipv4 Range
+    pub fn new_v4(
+        start: impl Into<Ipv4Addr>,
+        end: impl Into<Ipv4Addr>,
+    ) -> Result<Self, IpRangeError> {
+        Ok(IpRange::V4(AddressRange::new_v4(start, end)?))
+    }
+
+    pub fn new_v6(
+        start: impl Into<Ipv6Addr>,
+        end: impl Into<Ipv6Addr>,
+    ) -> Result<Self, IpRangeError> {
+        Ok(IpRange::V6(AddressRange::new_v6(start, end)?))
+    }
+}
+
+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 end
+///
+/// This type is for encapsulation purposes for the [`IpRange`] enum and should be instantiated via
+/// that enum.
+///
+/// # Invariants
+///
+/// * start and end have the same IP address family
+/// * start is lesser than or equal to end
+///
+/// # 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,
+    end: T,
+}
+
+impl AddressRange<Ipv4Addr> {
+    pub(crate) fn new_v4(
+        start: impl Into<Ipv4Addr>,
+        end: impl Into<Ipv4Addr>,
+    ) -> Result<AddressRange<Ipv4Addr>, IpRangeError> {
+        let (start, end) = (start.into(), end.into());
+
+        if start > end {
+            return Err(IpRangeError::StartGreaterThanEnd);
+        }
+
+        Ok(Self { start, end })
+    }
+}
+
+impl AddressRange<Ipv6Addr> {
+    pub(crate) fn new_v6(
+        start: impl Into<Ipv6Addr>,
+        end: impl Into<Ipv6Addr>,
+    ) -> Result<AddressRange<Ipv6Addr>, IpRangeError> {
+        let (start, end) = (start.into(), end.into());
+
+        if start > end {
+            return Err(IpRangeError::StartGreaterThanEnd);
+        }
+
+        Ok(Self { start, end })
+    }
+}
+
+impl<T> AddressRange<T> {
+    pub fn start(&self) -> &T {
+        &self.start
+    }
+
+    pub fn end(&self) -> &T {
+        &self.end
+    }
+}
+
+impl std::str::FromStr for AddressRange<Ipv4Addr> {
+    type Err = IpRangeError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if let Some((start, end)) = s.split_once('-') {
+            let start_address = start
+                .parse::<Ipv4Addr>()
+                .map_err(|_| IpRangeError::InvalidFormat)?;
+
+            let end_address = end
+                .parse::<Ipv4Addr>()
+                .map_err(|_| IpRangeError::InvalidFormat)?;
+
+            return Self::new_v4(start_address, end_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, end)) = s.split_once('-') {
+            let start_address = start
+                .parse::<Ipv6Addr>()
+                .map_err(|_| IpRangeError::InvalidFormat)?;
+
+            let end_address = end
+                .parse::<Ipv6Addr>()
+                .map_err(|_| IpRangeError::InvalidFormat)?;
+
+            return Self::new_v6(start_address, end_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.end)
+    }
+}
+
 #[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] 31+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v2 06/25] firewall: address: use new iprange type for ip entries
  2024-10-10 15:56 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v2 00/25] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (4 preceding siblings ...)
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 05/25] firewall: add ip range types Stefan Hanreich
@ 2024-10-10 15:56 ` Stefan Hanreich
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 07/25] ipset: add range variant to addresses Stefan Hanreich
                   ` (18 subsequent siblings)
  24 siblings, 0 replies; 31+ messages in thread
From: Stefan Hanreich @ 2024-10-10 15:56 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 42ec1a1..019884c 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] 31+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v2 07/25] ipset: add range variant to addresses
  2024-10-10 15:56 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v2 00/25] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (5 preceding siblings ...)
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 06/25] firewall: address: use new iprange type for ip entries Stefan Hanreich
@ 2024-10-10 15:56 ` Stefan Hanreich
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 08/25] iprange: add methods for converting an ip range to cidrs Stefan Hanreich
                   ` (17 subsequent siblings)
  24 siblings, 0 replies; 31+ messages in thread
From: Stefan Hanreich @ 2024-10-10 15:56 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] 31+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v2 08/25] iprange: add methods for converting an ip range to cidrs
  2024-10-10 15:56 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v2 00/25] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (6 preceding siblings ...)
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 07/25] ipset: add range variant to addresses Stefan Hanreich
@ 2024-10-10 15:56 ` Stefan Hanreich
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 09/25] ipset: address: add helper methods Stefan Hanreich
                   ` (16 subsequent siblings)
  24 siblings, 0 replies; 31+ messages in thread
From: Stefan Hanreich @ 2024-10-10 15:56 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 019884c..4baafa7 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, end)?))
     }
+
+    /// 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, end })
     }
+
+    /// 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 - end
+    ///
+    /// 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 end 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 end = u32::from_be_bytes(self.end.octets());
+
+        if current == end {
+            // 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 && end == u32::MAX {
+            // valid Ipv4 since it is `0.0.0.0/0`
+            cidrs.push(Ipv4Cidr::new(current, 0).unwrap());
+            return cidrs;
+        }
+
+        while current <= end {
+            // 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 = ((end - 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, end })
     }
+
+    /// Returns the minimum amount of CIDRs that exactly represent the range
+    ///
+    /// 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 end = u128::from_be_bytes(self.end.octets());
+
+        if current == end {
+            // 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 && end == u128::MAX {
+            // valid Ipv6 since it is `::/0`
+            cidrs.push(Ipv6Cidr::new(current, 0).unwrap());
+            return cidrs;
+        }
+
+        while current <= end {
+            // 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 = ((end - 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] 31+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v2 09/25] ipset: address: add helper methods
  2024-10-10 15:56 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v2 00/25] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (7 preceding siblings ...)
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 08/25] iprange: add methods for converting an ip range to cidrs Stefan Hanreich
@ 2024-10-10 15:56 ` Stefan Hanreich
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 10/25] firewall: guest: derive traits according to rust api guidelines Stefan Hanreich
                   ` (15 subsequent siblings)
  24 siblings, 0 replies; 31+ messages in thread
From: Stefan Hanreich @ 2024-10-10 15:56 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 4baafa7..9f4ad02 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] 31+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v2 10/25] firewall: guest: derive traits according to rust api guidelines
  2024-10-10 15:56 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v2 00/25] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (8 preceding siblings ...)
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 09/25] ipset: address: add helper methods Stefan Hanreich
@ 2024-10-10 15:56 ` Stefan Hanreich
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 11/25] common: add allowlist Stefan Hanreich
                   ` (14 subsequent siblings)
  24 siblings, 0 replies; 31+ messages in thread
From: Stefan Hanreich @ 2024-10-10 15:56 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 9f4ad02..6978a8f 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,
     end: 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] 31+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v2 11/25] common: add allowlist
  2024-10-10 15:56 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v2 00/25] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (9 preceding siblings ...)
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 10/25] firewall: guest: derive traits according to rust api guidelines Stefan Hanreich
@ 2024-10-10 15:56 ` Stefan Hanreich
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 12/25] sdn: add name types Stefan Hanreich
                   ` (13 subsequent siblings)
  24 siblings, 0 replies; 31+ messages in thread
From: Stefan Hanreich @ 2024-10-10 15:56 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] 31+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v2 12/25] sdn: add name types
  2024-10-10 15:56 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v2 00/25] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (10 preceding siblings ...)
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 11/25] common: add allowlist Stefan Hanreich
@ 2024-10-10 15:56 ` Stefan Hanreich
  2024-11-06 14:18   ` Wolfgang Bumiller
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 13/25] sdn: add ipam module Stefan Hanreich
                   ` (12 subsequent siblings)
  24 siblings, 1 reply; 31+ messages in thread
From: Stefan Hanreich @ 2024-10-10 15:56 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 | 240 +++++++++++++++++++++++++++++++
 2 files changed, 241 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..4e7c525
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/mod.rs
@@ -0,0 +1,240 @@
+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))
+    }
+
+    pub fn name(&self) -> &str {
+        &self.0
+    }
+}
+
+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 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] 31+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v2 13/25] sdn: add ipam module
  2024-10-10 15:56 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v2 00/25] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (11 preceding siblings ...)
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 12/25] sdn: add name types Stefan Hanreich
@ 2024-10-10 15:56 ` Stefan Hanreich
  2024-11-06 14:52   ` Wolfgang Bumiller
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 14/25] sdn: ipam: add method for generating ipsets Stefan Hanreich
                   ` (11 subsequent siblings)
  24 siblings, 1 reply; 31+ messages in thread
From: Stefan Hanreich @ 2024-10-10 15:56 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 6978a8f..a7bb6ad 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..682bbe7
--- /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 4e7c525..67af24e 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] 31+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v2 14/25] sdn: ipam: add method for generating ipsets
  2024-10-10 15:56 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v2 00/25] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (12 preceding siblings ...)
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 13/25] sdn: add ipam module Stefan Hanreich
@ 2024-10-10 15:56 ` Stefan Hanreich
  2024-11-06 15:12   ` Wolfgang Bumiller
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 15/25] sdn: add config module Stefan Hanreich
                   ` (10 subsequent siblings)
  24 siblings, 1 reply; 31+ messages in thread
From: Stefan Hanreich @ 2024-10-10 15:56 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             | 54 ++++++++++++++++++-
 2 files changed, 62 insertions(+), 1 deletion(-)

diff --git a/proxmox-ve-config/src/firewall/types/address.rs b/proxmox-ve-config/src/firewall/types/address.rs
index a7bb6ad..e5a3709 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 682bbe7..075c0f3 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,54 @@ 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<'a>(
+        &self,
+        filter: impl Into<Option<&'a Allowlist<Vmid>>>,
+    ) -> impl Iterator<Item = Ipset> + '_ {
+        let filter = filter.into();
+
+        self.entries
+            .iter()
+            .flat_map(|(_, entries)| entries.iter())
+            .filter_map(|entry| {
+                if let IpamData::Vm(data) = &entry.data() {
+                    if filter
+                        .map(|list| list.is_allowed(&data.vmid))
+                        .unwrap_or(true)
+                    {
+                        return Some(data);
+                    }
+                }
+
+                None
+            })
+            .fold(HashMap::<Vmid, Ipset>::new(), |mut acc, entry| {
+                match acc.get_mut(&entry.vmid) {
+                    Some(ipset) => {
+                        ipset.push(IpsetEntry::from(entry.ip));
+                    }
+                    None => {
+                        let ipset_name = format!("guest-ipam-{}", entry.vmid);
+                        let mut ipset = Ipset::from_parts(IpsetScope::Sdn, ipset_name);
+                        ipset.push(IpsetEntry::from(entry.ip));
+                        acc.insert(entry.vmid, ipset);
+                    }
+                };
+
+                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] 31+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v2 15/25] sdn: add config module
  2024-10-10 15:56 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v2 00/25] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (13 preceding siblings ...)
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 14/25] sdn: ipam: add method for generating ipsets Stefan Hanreich
@ 2024-10-10 15:56 ` Stefan Hanreich
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 16/25] sdn: config: add method for generating ipsets Stefan Hanreich
                   ` (9 subsequent siblings)
  24 siblings, 0 replies; 31+ messages in thread
From: Stefan Hanreich @ 2024-10-10 15:56 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 67af24e..f02c170 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] 31+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v2 16/25] sdn: config: add method for generating ipsets
  2024-10-10 15:56 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v2 00/25] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (14 preceding siblings ...)
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 15/25] sdn: add config module Stefan Hanreich
@ 2024-10-10 15:56 ` Stefan Hanreich
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 17/25] tests: add sdn config tests Stefan Hanreich
                   ` (8 subsequent siblings)
  24 siblings, 0 replies; 31+ messages in thread
From: Stefan Hanreich @ 2024-10-10 15:56 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 | 72 +++++++++++++++++++++++++++++
 1 file changed, 72 insertions(+)

diff --git a/proxmox-ve-config/src/sdn/config.rs b/proxmox-ve-config/src/sdn/config.rs
index b71084b..f6fc8c2 100644
--- a/proxmox-ve-config/src/sdn/config.rs
+++ b/proxmox-ve-config/src/sdn/config.rs
@@ -529,6 +529,78 @@ 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: impl Into<Option<&'a Allowlist<VnetName>>>,
+    ) -> impl Iterator<Item = Ipset> + '_ {
+        let filter = filter.into();
+
+        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] 31+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v2 17/25] tests: add sdn config tests
  2024-10-10 15:56 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v2 00/25] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (15 preceding siblings ...)
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 16/25] sdn: config: add method for generating ipsets Stefan Hanreich
@ 2024-10-10 15:56 ` Stefan Hanreich
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 18/25] tests: add ipam tests Stefan Hanreich
                   ` (7 subsequent siblings)
  24 siblings, 0 replies; 31+ messages in thread
From: Stefan Hanreich @ 2024-10-10 15:56 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] 31+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs v2 18/25] tests: add ipam tests
  2024-10-10 15:56 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v2 00/25] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (16 preceding siblings ...)
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 17/25] tests: add sdn config tests Stefan Hanreich
@ 2024-10-10 15:56 ` Stefan Hanreich
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-firewall v2 19/25] config: tests: add support for loading sdn and ipam config Stefan Hanreich
                   ` (6 subsequent siblings)
  24 siblings, 0 replies; 31+ messages in thread
From: Stefan Hanreich @ 2024-10-10 15:56 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] 31+ messages in thread

* [pve-devel] [PATCH proxmox-firewall v2 19/25] config: tests: add support for loading sdn and ipam config
  2024-10-10 15:56 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v2 00/25] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (17 preceding siblings ...)
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 18/25] tests: add ipam tests Stefan Hanreich
@ 2024-10-10 15:56 ` Stefan Hanreich
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-firewall v2 20/25] ipsets: autogenerate ipsets for vnets and ipam Stefan Hanreich
                   ` (5 subsequent siblings)
  24 siblings, 0 replies; 31+ messages in thread
From: Stefan Hanreich @ 2024-10-10 15:56 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] 31+ messages in thread

* [pve-devel] [PATCH proxmox-firewall v2 20/25] ipsets: autogenerate ipsets for vnets and ipam
  2024-10-10 15:56 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v2 00/25] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (18 preceding siblings ...)
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-firewall v2 19/25] config: tests: add support for loading sdn and ipam config Stefan Hanreich
@ 2024-10-10 15:56 ` Stefan Hanreich
  2024-10-10 15:56 ` [pve-devel] [PATCH pve-firewall v2 21/25] add support for loading sdn firewall configuration Stefan Hanreich
                   ` (4 subsequent siblings)
  24 siblings, 0 replies; 31+ messages in thread
From: Stefan Hanreich @ 2024-10-10 15:56 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..71a90eb 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>, end: impl Into<Expression>) -> Self {
+        Expression::Range(Box::new((start.into(), end.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.end()),
+            IpRange::V6(range) => Expression::range(range.start(), range.end()),
+        }
+    }
+}
+
 #[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] 31+ messages in thread

* [pve-devel] [PATCH pve-firewall v2 21/25] add support for loading sdn firewall configuration
  2024-10-10 15:56 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v2 00/25] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (19 preceding siblings ...)
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-firewall v2 20/25] ipsets: autogenerate ipsets for vnets and ipam Stefan Hanreich
@ 2024-10-10 15:56 ` Stefan Hanreich
  2024-11-07 10:44   ` Wolfgang Bumiller
  2024-10-10 15:56 ` [pve-devel] [PATCH pve-firewall v2 22/25] api: load sdn ipsets Stefan Hanreich
                   ` (3 subsequent siblings)
  24 siblings, 1 reply; 31+ messages in thread
From: Stefan Hanreich @ 2024-10-10 15:56 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..9943f2e 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 'guest/') && $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] 31+ messages in thread

* [pve-devel] [PATCH pve-firewall v2 22/25] api: load sdn ipsets
  2024-10-10 15:56 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v2 00/25] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (20 preceding siblings ...)
  2024-10-10 15:56 ` [pve-devel] [PATCH pve-firewall v2 21/25] add support for loading sdn firewall configuration Stefan Hanreich
@ 2024-10-10 15:56 ` Stefan Hanreich
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-perl-rs v2 23/25] add PVE::RS::Firewall::SDN module Stefan Hanreich
                   ` (2 subsequent siblings)
  24 siblings, 0 replies; 31+ messages in thread
From: Stefan Hanreich @ 2024-10-10 15:56 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] 31+ messages in thread

* [pve-devel] [PATCH proxmox-perl-rs v2 23/25] add PVE::RS::Firewall::SDN module
  2024-10-10 15:56 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v2 00/25] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (21 preceding siblings ...)
  2024-10-10 15:56 ` [pve-devel] [PATCH pve-firewall v2 22/25] api: load sdn ipsets Stefan Hanreich
@ 2024-10-10 15:56 ` Stefan Hanreich
  2024-10-10 15:56 ` [pve-devel] [PATCH pve-manager v2 24/25] firewall: add sdn scope to IPRefSelector Stefan Hanreich
  2024-10-10 15:56 ` [pve-devel] [PATCH pve-docs v2 25/25] sdn: add documentation for firewall integration Stefan Hanreich
  24 siblings, 0 replies; 31+ messages in thread
From: Stefan Hanreich @ 2024-10-10 15:56 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 72d548d..cab9a83 100644
--- a/pve-rs/Cargo.toml
+++ b/pve-rs/Cargo.toml
@@ -45,3 +45,4 @@ proxmox-subscription = "0.4"
 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] 31+ messages in thread

* [pve-devel] [PATCH pve-manager v2 24/25] firewall: add sdn scope to IPRefSelector
  2024-10-10 15:56 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v2 00/25] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (22 preceding siblings ...)
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-perl-rs v2 23/25] add PVE::RS::Firewall::SDN module Stefan Hanreich
@ 2024-10-10 15:56 ` Stefan Hanreich
  2024-10-10 15:56 ` [pve-devel] [PATCH pve-docs v2 25/25] sdn: add documentation for firewall integration Stefan Hanreich
  24 siblings, 0 replies; 31+ messages in thread
From: Stefan Hanreich @ 2024-10-10 15:56 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] 31+ messages in thread

* [pve-devel] [PATCH pve-docs v2 25/25] sdn: add documentation for firewall integration
  2024-10-10 15:56 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v2 00/25] autogenerate ipsets for sdn objects Stefan Hanreich
                   ` (23 preceding siblings ...)
  2024-10-10 15:56 ` [pve-devel] [PATCH pve-manager v2 24/25] firewall: add sdn scope to IPRefSelector Stefan Hanreich
@ 2024-10-10 15:56 ` Stefan Hanreich
  24 siblings, 0 replies; 31+ messages in thread
From: Stefan Hanreich @ 2024-10-10 15:56 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] 31+ messages in thread

* Re: [pve-devel] [PATCH proxmox-ve-rs v2 05/25] firewall: add ip range types
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 05/25] firewall: add ip range types Stefan Hanreich
@ 2024-11-06 13:13   ` Wolfgang Bumiller
  0 siblings, 0 replies; 31+ messages in thread
From: Wolfgang Bumiller @ 2024-11-06 13:13 UTC (permalink / raw)
  To: Stefan Hanreich; +Cc: pve-devel

On Thu, Oct 10, 2024 at 05:56:17PM GMT, Stefan Hanreich wrote:
> 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..42ec1a1 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,
> +    StartGreaterThanEnd,
> +    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::StartGreaterThanEnd => "start is greater than end",
> +            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 end IP address are not from the same family.
> +    pub fn new(start: impl Into<IpAddr>, end: impl Into<IpAddr>) -> Result<Self, IpRangeError> {
> +        match (start.into(), end.into()) {
> +            (IpAddr::V4(start), IpAddr::V4(end)) => Self::new_v4(start, end),
> +            (IpAddr::V6(start), IpAddr::V6(end)) => Self::new_v6(start, end),
> +            _ => Err(IpRangeError::MismatchedFamilies),
> +        }
> +    }
> +
> +    /// construct a new Ipv4 Range
> +    pub fn new_v4(
> +        start: impl Into<Ipv4Addr>,
> +        end: impl Into<Ipv4Addr>,
> +    ) -> Result<Self, IpRangeError> {
> +        Ok(IpRange::V4(AddressRange::new_v4(start, end)?))
> +    }
> +
> +    pub fn new_v6(
> +        start: impl Into<Ipv6Addr>,
> +        end: impl Into<Ipv6Addr>,
> +    ) -> Result<Self, IpRangeError> {
> +        Ok(IpRange::V6(AddressRange::new_v6(start, end)?))
> +    }
> +}
> +
> +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 end
> +///
> +/// This type is for encapsulation purposes for the [`IpRange`] enum and should be instantiated via
> +/// that enum.
> +///
> +/// # Invariants
> +///
> +/// * start and end have the same IP address family
> +/// * start is lesser than or equal to end

lesser -> less

Also:

This range *includes* the `end`. In rust `std` we have `std::ops::Range`
while *this* works like `std::ops::RangeInclusive`.

This might be fine, given it's a vastly different context, however...

> +///
> +/// # 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,
> +    end: T,

...I think we should name this `last`, so it's less confusing and...

> +}
> +
> +impl AddressRange<Ipv4Addr> {
> +    pub(crate) fn new_v4(
> +        start: impl Into<Ipv4Addr>,
> +        end: impl Into<Ipv4Addr>,
> +    ) -> Result<AddressRange<Ipv4Addr>, IpRangeError> {
> +        let (start, end) = (start.into(), end.into());
> +
> +        if start > end {
> +            return Err(IpRangeError::StartGreaterThanEnd);
> +        }
> +
> +        Ok(Self { start, end })
> +    }
> +}
> +
> +impl AddressRange<Ipv6Addr> {
> +    pub(crate) fn new_v6(
> +        start: impl Into<Ipv6Addr>,
> +        end: impl Into<Ipv6Addr>,
> +    ) -> Result<AddressRange<Ipv6Addr>, IpRangeError> {
> +        let (start, end) = (start.into(), end.into());
> +
> +        if start > end {
> +            return Err(IpRangeError::StartGreaterThanEnd);
> +        }
> +
> +        Ok(Self { start, end })
> +    }
> +}
> +
> +impl<T> AddressRange<T> {
> +    pub fn start(&self) -> &T {
> +        &self.start
> +    }
> +
> +    pub fn end(&self) -> &T {

... similarly these getters should be named `last`. Mainly because with
the ranges being inclusive, this represents the "*last* usable address",
while "end" is also used in `std::ops::Range` to mean "fist *unusable*
number".

> +        &self.end
> +    }
> +}
> +
> +impl std::str::FromStr for AddressRange<Ipv4Addr> {
> +    type Err = IpRangeError;
> +
> +    fn from_str(s: &str) -> Result<Self, Self::Err> {
> +        if let Some((start, end)) = s.split_once('-') {
> +            let start_address = start
> +                .parse::<Ipv4Addr>()
> +                .map_err(|_| IpRangeError::InvalidFormat)?;
> +
> +            let end_address = end
> +                .parse::<Ipv4Addr>()
> +                .map_err(|_| IpRangeError::InvalidFormat)?;
> +
> +            return Self::new_v4(start_address, end_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, end)) = s.split_once('-') {
> +            let start_address = start
> +                .parse::<Ipv6Addr>()
> +                .map_err(|_| IpRangeError::InvalidFormat)?;
> +
> +            let end_address = end
> +                .parse::<Ipv6Addr>()
> +                .map_err(|_| IpRangeError::InvalidFormat)?;
> +
> +            return Self::new_v6(start_address, end_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.end)
> +    }
> +}
> +
>  #[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] 31+ messages in thread

* Re: [pve-devel] [PATCH proxmox-ve-rs v2 12/25] sdn: add name types
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 12/25] sdn: add name types Stefan Hanreich
@ 2024-11-06 14:18   ` Wolfgang Bumiller
  0 siblings, 0 replies; 31+ messages in thread
From: Wolfgang Bumiller @ 2024-11-06 14:18 UTC (permalink / raw)
  To: Stefan Hanreich; +Cc: pve-devel

On Thu, Oct 10, 2024 at 05:56:24PM GMT, Stefan Hanreich wrote:
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
>  proxmox-ve-config/src/lib.rs     |   1 +
>  proxmox-ve-config/src/sdn/mod.rs | 240 +++++++++++++++++++++++++++++++
>  2 files changed, 241 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..4e7c525
> --- /dev/null
> +++ b/proxmox-ve-config/src/sdn/mod.rs
> @@ -0,0 +1,240 @@
> +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))
> +    }
> +
> +    pub fn name(&self) -> &str {

^ I wonder if this should be `as_str()`
And would it make sense to `impl AsRef<str>`?


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


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

* Re: [pve-devel] [PATCH proxmox-ve-rs v2 13/25] sdn: add ipam module
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 13/25] sdn: add ipam module Stefan Hanreich
@ 2024-11-06 14:52   ` Wolfgang Bumiller
  0 siblings, 0 replies; 31+ messages in thread
From: Wolfgang Bumiller @ 2024-11-06 14:52 UTC (permalink / raw)
  To: Stefan Hanreich; +Cc: pve-devel

On Thu, Oct 10, 2024 at 05:56:25PM GMT, Stefan Hanreich wrote:
> 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 6978a8f..a7bb6ad 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..682bbe7
> --- /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

Max already mentioned it: docs are full sentences, so capitalize & end
with a `.` please.


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


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

* Re: [pve-devel] [PATCH proxmox-ve-rs v2 14/25] sdn: ipam: add method for generating ipsets
  2024-10-10 15:56 ` [pve-devel] [PATCH proxmox-ve-rs v2 14/25] sdn: ipam: add method for generating ipsets Stefan Hanreich
@ 2024-11-06 15:12   ` Wolfgang Bumiller
  0 siblings, 0 replies; 31+ messages in thread
From: Wolfgang Bumiller @ 2024-11-06 15:12 UTC (permalink / raw)
  To: Stefan Hanreich; +Cc: pve-devel

On Thu, Oct 10, 2024 at 05:56:26PM GMT, Stefan Hanreich wrote:
> 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             | 54 ++++++++++++++++++-
>  2 files changed, 62 insertions(+), 1 deletion(-)
> 
> diff --git a/proxmox-ve-config/src/firewall/types/address.rs b/proxmox-ve-config/src/firewall/types/address.rs
> index a7bb6ad..e5a3709 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 682bbe7..075c0f3 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,54 @@ 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<'a>(
> +        &self,
> +        filter: impl Into<Option<&'a Allowlist<Vmid>>>,

^ Why the `impl Into`? All our current uses should work for just the
Option directly (and then we can also drop the named lifetime).

> +    ) -> impl Iterator<Item = Ipset> + '_ {
> +        let filter = filter.into();
> +
> +        self.entries
> +            .iter()
> +            .flat_map(|(_, entries)| entries.iter())
> +            .filter_map(|entry| {
> +                if let IpamData::Vm(data) = &entry.data() {
> +                    if filter
> +                        .map(|list| list.is_allowed(&data.vmid))
> +                        .unwrap_or(true)

Let's bump MSRV to 1.82 and use

    if filter.is_none_or(|list| list.is_allowed(&data.vmid)) {

? :)

> +                    {
> +                        return Some(data);
> +                    }
> +                }
> +
> +                None
> +            })
> +            .fold(HashMap::<Vmid, Ipset>::new(), |mut acc, entry| {
> +                match acc.get_mut(&entry.vmid) {
> +                    Some(ipset) => {
> +                        ipset.push(IpsetEntry::from(entry.ip));
> +                    }
> +                    None => {
> +                        let ipset_name = format!("guest-ipam-{}", entry.vmid);
> +                        let mut ipset = Ipset::from_parts(IpsetScope::Sdn, ipset_name);
> +                        ipset.push(IpsetEntry::from(entry.ip));
> +                        acc.insert(entry.vmid, ipset);
> +                    }
> +                };

Mhhhh. The `ipset.upsh()` is identical in both cases, and vmid is a
simple Copy type, so we could use the entry api for this:

    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] 31+ messages in thread

* Re: [pve-devel] [PATCH pve-firewall v2 21/25] add support for loading sdn firewall configuration
  2024-10-10 15:56 ` [pve-devel] [PATCH pve-firewall v2 21/25] add support for loading sdn firewall configuration Stefan Hanreich
@ 2024-11-07 10:44   ` Wolfgang Bumiller
  0 siblings, 0 replies; 31+ messages in thread
From: Wolfgang Bumiller @ 2024-11-07 10:44 UTC (permalink / raw)
  To: Stefan Hanreich; +Cc: pve-devel

On Thu, Oct 10, 2024 at 05:56:33PM GMT, Stefan Hanreich wrote:
> 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..9943f2e 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;

The hunk below looks fishy.

> -	    if ($scope ne 'dc/' && $fw_conf && $fw_conf->{ipset}->{$name}) {

Previously $scope was dc or guest (or nothing).
This was the "guest" case, and guests took precedence, hence `ne 'dc/'`

> +	    if ((!$scope || $scope eq 'guest/') && $fw_conf && $fw_conf->{ipset}->{$name}) {

^ So this seems fine:
    "(not set or set to guest) and a guest entry exists"

>  		$ipset_chain = compute_ipset_chain_name($fw_conf->{vmid}, $name, $ipversion);
> -	    } elsif ($scope ne 'guest/' && $cluster_conf && $cluster_conf->{ipset}->{$name}) {

^ Then we had "not guest", so:
    "not set or set to datacenter and a cluster entry exists"

> +	    } elsif ((!$scope || $scope eq 'guest/') && $cluster_conf && $cluster_conf->{ipset}->{$name}) {

^ But here we have a 2nd instance of `eq 'guest/'`, so:
    "not set or set to guest and a cluster entry exists"

Should probably be `(!$scope && $scope eq 'dc/')`, no?

> +		$ipset_chain = compute_ipset_chain_name(0, $name, $ipversion);
> +	    } elsif ((!$scope || $scope eq 'sdn/') && $cluster_conf->{sdn} && $cluster_conf->{sdn}->{ipset}->{$name}) {

^ Finally the new case for "not set or scope is sdn and sdn entry
exists" which means that now we can also "fall through" to an sdn entry
if no scope is set (which makes sense I guess).

>  		$ipset_chain = compute_ipset_chain_name(0, $name, $ipversion);
>  	    } else {
>  		die "no such ipset '$name'\n";


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


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

end of thread, other threads:[~2024-11-07 10:44 UTC | newest]

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