* [RFC proxmox 00/22] New crate for firewall api types
@ 2026-02-16 10:43 Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 01/22] firewall-api-types: add new " Dietmar Maurer
` (22 more replies)
0 siblings, 23 replies; 26+ messages in thread
From: Dietmar Maurer @ 2026-02-16 10:43 UTC (permalink / raw)
To: pve-devel
The current PVE firewall implementation is written in Perl, and Rust type
definitions can be auto-generated from its API schemas. However, many of the
more complex types are represented as opaque strings, which limits type safety.
Verifiers for complex types like ports and address matches cannot be generated
automatically, so we need to implement them manually anyway.
To address this, the crate provides hand-crafted Rust types that parse and validate these
string-encoded values into proper enums and structs, while remaining fully
compatible with the existing API wire format. The initial type definitions were
seeded from the auto-generated `pve-api-types` crate and then refined by hand.
Types from proxmox-ve-rs/proxmox-ve-config/src/firewall/ are not really designed
to be used directly, as they are not fully compatible with the API wire format. they
also depends on system crates (nix, proxmox-sys, etc.) which we want to avoid for this crate.
I tried to reuse some of those types, but in many cases it was easier to
use types generated from the perl API schemas as a starting point and then modify them
as needed.
Dependencies are minimal, so that we can use this crate for wasm targets (GUI).
This series depends on the CommaSeparatedList patch send recently.
Dietmar Maurer (22):
firewall-api-types: add new crate for firewall api types
firewall-api-types: add README.md
firewall-api-types: add firewall policy types
firewall-api-types: add logging types
firewall-api-types: add FirewallClusterOptions
firewall-api-types: add FirewallGuestOptions
firewall-api-types: add FirewallConntrackHelper enum
firewall-api-types: add FirewallNodeOptions struct
firewall-api-types: add FirewallRef type
firewall-api-types: add FirewallPortList types
firewall-api-types: add FirewallIcmpType
firewall-api-types: add FirewallIpsetReference type
firewall-api-types: add FirewallAliasReference type
firewall-api-types: add firewall address types
firewall-api-types: add FirewallRule type
firewall-api-types: use ConfigDigest from proxmox-config-digest crate
firewall-api-types: use COMMENT_SCHEMA from proxmox-schema crate
firewall-api-types: add FirewallRuleUpdater type
firewall-api-types: refactor FirewallRule and add
FirewallRuleListEntry
firewall-api-types: add DeletableFirewallRuleProperty enum
firewall-api-types: add FirewallAliasEntry API type
firewall-api-types: add FirewallIpsetListEntry and FirewallIpsetEntry
api types
Cargo.toml | 1 +
proxmox-firewall-api-types/Cargo.toml | 30 +
proxmox-firewall-api-types/README.md | 54 ++
proxmox-firewall-api-types/debian/changelog | 5 +
proxmox-firewall-api-types/debian/control | 52 ++
proxmox-firewall-api-types/debian/copyright | 18 +
.../debian/debcargo.toml | 7 +
proxmox-firewall-api-types/src/address.rs | 229 +++++++
proxmox-firewall-api-types/src/alias.rs | 181 ++++++
.../src/cluster_options.rs | 61 ++
proxmox-firewall-api-types/src/conntrack.rs | 52 ++
.../src/firewall_ref.rs | 62 ++
.../src/guest_options.rs | 97 +++
proxmox-firewall-api-types/src/icmp_type.rs | 559 ++++++++++++++++++
proxmox-firewall-api-types/src/ipset.rs | 254 ++++++++
proxmox-firewall-api-types/src/lib.rs | 46 ++
proxmox-firewall-api-types/src/log.rs | 312 ++++++++++
.../src/node_options.rs | 240 ++++++++
proxmox-firewall-api-types/src/policy.rs | 151 +++++
proxmox-firewall-api-types/src/port.rs | 177 ++++++
proxmox-firewall-api-types/src/rule.rs | 351 +++++++++++
21 files changed, 2939 insertions(+)
create mode 100644 proxmox-firewall-api-types/Cargo.toml
create mode 100644 proxmox-firewall-api-types/README.md
create mode 100644 proxmox-firewall-api-types/debian/changelog
create mode 100644 proxmox-firewall-api-types/debian/control
create mode 100644 proxmox-firewall-api-types/debian/copyright
create mode 100644 proxmox-firewall-api-types/debian/debcargo.toml
create mode 100644 proxmox-firewall-api-types/src/address.rs
create mode 100644 proxmox-firewall-api-types/src/alias.rs
create mode 100644 proxmox-firewall-api-types/src/cluster_options.rs
create mode 100644 proxmox-firewall-api-types/src/conntrack.rs
create mode 100644 proxmox-firewall-api-types/src/firewall_ref.rs
create mode 100644 proxmox-firewall-api-types/src/guest_options.rs
create mode 100644 proxmox-firewall-api-types/src/icmp_type.rs
create mode 100644 proxmox-firewall-api-types/src/ipset.rs
create mode 100644 proxmox-firewall-api-types/src/lib.rs
create mode 100644 proxmox-firewall-api-types/src/log.rs
create mode 100644 proxmox-firewall-api-types/src/node_options.rs
create mode 100644 proxmox-firewall-api-types/src/policy.rs
create mode 100644 proxmox-firewall-api-types/src/port.rs
create mode 100644 proxmox-firewall-api-types/src/rule.rs
--
2.47.3
^ permalink raw reply [flat|nested] 26+ messages in thread
* [RFC proxmox 01/22] firewall-api-types: add new crate for firewall api types
2026-02-16 10:43 [RFC proxmox 00/22] New crate for firewall api types Dietmar Maurer
@ 2026-02-16 10:43 ` Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 02/22] firewall-api-types: add README.md Dietmar Maurer
` (21 subsequent siblings)
22 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2026-02-16 10:43 UTC (permalink / raw)
To: pve-devel
This commit just adds a dummy package with no funtionality.
Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
Cargo.toml | 1 +
proxmox-firewall-api-types/Cargo.toml | 20 +++++++
proxmox-firewall-api-types/debian/changelog | 5 ++
proxmox-firewall-api-types/debian/control | 52 +++++++++++++++++++
proxmox-firewall-api-types/debian/copyright | 18 +++++++
.../debian/debcargo.toml | 7 +++
proxmox-firewall-api-types/src/lib.rs | 1 +
7 files changed, 104 insertions(+)
create mode 100644 proxmox-firewall-api-types/Cargo.toml
create mode 100644 proxmox-firewall-api-types/debian/changelog
create mode 100644 proxmox-firewall-api-types/debian/control
create mode 100644 proxmox-firewall-api-types/debian/copyright
create mode 100644 proxmox-firewall-api-types/debian/debcargo.toml
create mode 100644 proxmox-firewall-api-types/src/lib.rs
diff --git a/Cargo.toml b/Cargo.toml
index 6ce4d5ec..650868de 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -16,6 +16,7 @@ members = [
"proxmox-daemon",
"proxmox-deb-version",
"proxmox-dns-api",
+ "proxmox-firewall-api-types",
"proxmox-fixed-string",
"proxmox-docgen",
"proxmox-http",
diff --git a/proxmox-firewall-api-types/Cargo.toml b/proxmox-firewall-api-types/Cargo.toml
new file mode 100644
index 00000000..515d1efc
--- /dev/null
+++ b/proxmox-firewall-api-types/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "proxmox-firewall-api-types"
+version = "0.1.0"
+description = "Proxmox Firewall API type definitions."
+
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+exclude.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+regex.workspace = true
+proxmox-fixed-string.workspace = true
+
+serde = { workspace = true, features = [ "derive" ] }
+serde_plain = { workspace = true }
+proxmox-schema = { workspace = true, features = ["api-macro"] }
+proxmox-serde = { workspace = true, features = ["perl"] }
diff --git a/proxmox-firewall-api-types/debian/changelog b/proxmox-firewall-api-types/debian/changelog
new file mode 100644
index 00000000..0ebe9d33
--- /dev/null
+++ b/proxmox-firewall-api-types/debian/changelog
@@ -0,0 +1,5 @@
+rust-proxmox-firewall-api-types (0.1.0-1) trixie; urgency=medium
+
+ * Initial release.
+
+ -- Proxmox Support Team <support@proxmox.com> Thu, 22 Jan 2026 11:09:07 +0100
diff --git a/proxmox-firewall-api-types/debian/control b/proxmox-firewall-api-types/debian/control
new file mode 100644
index 00000000..13ea0ae1
--- /dev/null
+++ b/proxmox-firewall-api-types/debian/control
@@ -0,0 +1,52 @@
+Source: rust-proxmox-firewall-api-types
+Section: rust
+Priority: optional
+Build-Depends: debhelper-compat (= 13),
+ dh-sequence-cargo
+Build-Depends-Arch: cargo:native <!nocheck>,
+ rustc:native <!nocheck>,
+ libstd-rust-dev <!nocheck>,
+ librust-anyhow-1+default-dev <!nocheck>,
+ librust-proxmox-fixed-string-0.1+default-dev <!nocheck>,
+ librust-proxmox-schema-5+api-macro-dev (>= 5.0.1-~~) <!nocheck>,
+ librust-proxmox-schema-5+default-dev (>= 5.0.1-~~) <!nocheck>,
+ librust-proxmox-serde-1+default-dev <!nocheck>,
+ librust-proxmox-serde-1+perl-dev <!nocheck>,
+ librust-proxmox-serde-1+serde-json-dev <!nocheck>,
+ librust-regex-1+default-dev (>= 1.5-~~) <!nocheck>,
+ librust-serde-1+default-dev <!nocheck>,
+ librust-serde-1+derive-dev <!nocheck>,
+ librust-serde-plain-1+default-dev <!nocheck>
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Standards-Version: 4.7.2
+Vcs-Git: git://git.proxmox.com/git/proxmox.git
+Vcs-Browser: https://git.proxmox.com/?p=proxmox.git
+Homepage: https://git.proxmox.com/?p=proxmox.git
+X-Cargo-Crate: proxmox-firewall-api-types
+
+Package: librust-proxmox-firewall-api-types-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-anyhow-1+default-dev,
+ librust-proxmox-fixed-string-0.1+default-dev,
+ librust-proxmox-schema-5+api-macro-dev (>= 5.0.1-~~),
+ librust-proxmox-schema-5+default-dev (>= 5.0.1-~~),
+ librust-proxmox-serde-1+default-dev,
+ librust-proxmox-serde-1+perl-dev,
+ librust-proxmox-serde-1+serde-json-dev,
+ librust-regex-1+default-dev (>= 1.5-~~),
+ librust-serde-1+default-dev,
+ librust-serde-1+derive-dev,
+ librust-serde-plain-1+default-dev
+Provides:
+ librust-proxmox-firewall-api-types+default-dev (= ${binary:Version}),
+ librust-proxmox-firewall-api-types-0-dev (= ${binary:Version}),
+ librust-proxmox-firewall-api-types-0+default-dev (= ${binary:Version}),
+ librust-proxmox-firewall-api-types-0.1-dev (= ${binary:Version}),
+ librust-proxmox-firewall-api-types-0.1+default-dev (= ${binary:Version}),
+ librust-proxmox-firewall-api-types-0.1.0-dev (= ${binary:Version}),
+ librust-proxmox-firewall-api-types-0.1.0+default-dev (= ${binary:Version})
+Description: Proxmox Firewall API type definitions - Rust source code
+ Source code for Debianized Rust crate "proxmox-firewall-api-types"
diff --git a/proxmox-firewall-api-types/debian/copyright b/proxmox-firewall-api-types/debian/copyright
new file mode 100644
index 00000000..77952eba
--- /dev/null
+++ b/proxmox-firewall-api-types/debian/copyright
@@ -0,0 +1,18 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+
+Files:
+ *
+Copyright: 2019 - 2026 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-firewall-api-types/debian/debcargo.toml b/proxmox-firewall-api-types/debian/debcargo.toml
new file mode 100644
index 00000000..b7864cdb
--- /dev/null
+++ b/proxmox-firewall-api-types/debian/debcargo.toml
@@ -0,0 +1,7 @@
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support@proxmox.com>"
+
+[source]
+vcs_git = "git://git.proxmox.com/git/proxmox.git"
+vcs_browser = "https://git.proxmox.com/?p=proxmox.git"
diff --git a/proxmox-firewall-api-types/src/lib.rs b/proxmox-firewall-api-types/src/lib.rs
new file mode 100644
index 00000000..7c4a64a7
--- /dev/null
+++ b/proxmox-firewall-api-types/src/lib.rs
@@ -0,0 +1 @@
+// TODO: add code here
--
2.47.3
^ permalink raw reply [flat|nested] 26+ messages in thread
* [RFC proxmox 02/22] firewall-api-types: add README.md
2026-02-16 10:43 [RFC proxmox 00/22] New crate for firewall api types Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 01/22] firewall-api-types: add new " Dietmar Maurer
@ 2026-02-16 10:43 ` Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 03/22] firewall-api-types: add firewall policy types Dietmar Maurer
` (20 subsequent siblings)
22 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2026-02-16 10:43 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
proxmox-firewall-api-types/README.md | 54 ++++++++++++++++++++++++++++
1 file changed, 54 insertions(+)
create mode 100644 proxmox-firewall-api-types/README.md
diff --git a/proxmox-firewall-api-types/README.md b/proxmox-firewall-api-types/README.md
new file mode 100644
index 00000000..1ae22118
--- /dev/null
+++ b/proxmox-firewall-api-types/README.md
@@ -0,0 +1,54 @@
+# proxmox-firewall-api-types
+
+Strongly typed Rust definitions for the Proxmox VE Firewall API.
+
+## Motivation
+
+The current PVE firewall implementation is written in Perl, and Rust type
+definitions can be auto-generated from its API schemas. However, many of the
+more complex types are represented as opaque strings, which limits type safety.
+
+Verifiers for complex types like ports and address matches cannot be generated
+automatically, so we need to implement them manually anyway.
+
+To address this, the crate provides hand-crafted Rust types that parse and validate these
+string-encoded values into proper enums and structs, while remaining fully
+compatible with the existing API wire format. The initial type definitions were
+seeded from the auto-generated `pve-api-types` crate and then refined by hand.
+
+Dependencies are minimal, so that we can use this crate for wasm targets (GUI).
+
+## Compatibility
+
+All types must serialize and deserialize to the same JSON wire format that the
+existing Perl-based PVE API produces and consumes. There is currently no
+automated compatibility test against the live Perl API, so changes must be
+verified manually.
+
+Key techniques used to maintain wire-format compatibility:
+
+- **Perl-specific deserializers** — The Perl API encodes booleans and integers
+ as strings. Fields that come from Perl use custom deserializers such as
+ `proxmox_serde::perl::deserialize_bool` and `deserialize_u64` to accept
+ these representations.
+- **Serde renames** — `#[serde(rename = "...")]` and `#[serde(rename_all = "kebab-case")]`
+ ensure that Rust field and variant names map to the exact keys and values
+ the Perl API expects (e.g. `icmp-type`, `macro`, `type`).
+- **Forward-compatible enums** — When the `enum-fallback` feature is enabled,
+ enums gain an `UnknownEnumValue` catch-all variant (backed by
+ `proxmox-fixed-string`) so that new values added in a future API version
+ can be deserialized without errors.
+
+When adding or modifying types, compare the serde output against the
+corresponding Perl API endpoint to ensure the JSON representation matches.
+
+## Features
+
+- **`enum-fallback`** — Enables an `UnknownEnumValue` catch-all variant on
+ enums (backed by `proxmox-fixed-string`) for forward-compatible
+ deserialization of values added in newer API versions.
+
+## Usage
+
+- All public types are re-exported from the crate root.
+- Every type implements `serde::Serialize` / `Deserialize`.
--
2.47.3
^ permalink raw reply [flat|nested] 26+ messages in thread
* [RFC proxmox 03/22] firewall-api-types: add firewall policy types
2026-02-16 10:43 [RFC proxmox 00/22] New crate for firewall api types Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 01/22] firewall-api-types: add new " Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 02/22] firewall-api-types: add README.md Dietmar Maurer
@ 2026-02-16 10:43 ` Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 04/22] firewall-api-types: add logging types Dietmar Maurer
` (19 subsequent siblings)
22 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2026-02-16 10:43 UTC (permalink / raw)
To: pve-devel
Generated from perl api.
Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
proxmox-firewall-api-types/Cargo.toml | 5 +-
proxmox-firewall-api-types/src/lib.rs | 3 +-
proxmox-firewall-api-types/src/policy.rs | 151 +++++++++++++++++++++++
3 files changed, 157 insertions(+), 2 deletions(-)
create mode 100644 proxmox-firewall-api-types/src/policy.rs
diff --git a/proxmox-firewall-api-types/Cargo.toml b/proxmox-firewall-api-types/Cargo.toml
index 515d1efc..3122d813 100644
--- a/proxmox-firewall-api-types/Cargo.toml
+++ b/proxmox-firewall-api-types/Cargo.toml
@@ -9,12 +9,15 @@ license.workspace = true
repository.workspace = true
exclude.workspace = true
+[features]
+enum-fallback = ["dep:proxmox-fixed-string"]
+
[dependencies]
anyhow.workspace = true
regex.workspace = true
-proxmox-fixed-string.workspace = true
serde = { workspace = true, features = [ "derive" ] }
serde_plain = { workspace = true }
proxmox-schema = { workspace = true, features = ["api-macro"] }
proxmox-serde = { workspace = true, features = ["perl"] }
+proxmox-fixed-string = { workspace = true, optional = true }
diff --git a/proxmox-firewall-api-types/src/lib.rs b/proxmox-firewall-api-types/src/lib.rs
index 7c4a64a7..b8004c76 100644
--- a/proxmox-firewall-api-types/src/lib.rs
+++ b/proxmox-firewall-api-types/src/lib.rs
@@ -1 +1,2 @@
-// TODO: add code here
+mod policy;
+pub use policy::{FirewallFWPolicy, FirewallIOPolicy};
diff --git a/proxmox-firewall-api-types/src/policy.rs b/proxmox-firewall-api-types/src/policy.rs
new file mode 100644
index 00000000..274fbe20
--- /dev/null
+++ b/proxmox-firewall-api-types/src/policy.rs
@@ -0,0 +1,151 @@
+use serde::{Deserialize, Serialize};
+
+#[cfg(feature = "enum-fallback")]
+use proxmox_fixed_string::FixedString;
+use proxmox_schema::api;
+
+#[api]
+/// Firewall forward policy.
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
+pub enum FirewallFWPolicy {
+ #[serde(rename = "ACCEPT")]
+ #[default]
+ /// ACCEPT.
+ Accept,
+ #[serde(rename = "DROP")]
+ /// DROP.
+ Drop,
+ #[cfg(feature = "enum-fallback")]
+ #[serde(untagged)]
+ /// Unknwon
+ UnknownEnumValue(FixedString),
+}
+serde_plain::derive_display_from_serialize!(FirewallFWPolicy);
+serde_plain::derive_fromstr_from_deserialize!(FirewallFWPolicy);
+
+#[api]
+/// Firewall IO policy.
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
+pub enum FirewallIOPolicy {
+ #[serde(rename = "ACCEPT")]
+ /// ACCEPT.
+ Accept,
+ #[serde(rename = "DROP")]
+ /// DROP.
+ Drop,
+ #[serde(rename = "REJECT")]
+ /// REJECT.
+ Reject,
+ #[cfg(feature = "enum-fallback")]
+ #[serde(untagged)]
+ /// Unknwon
+ UnknownEnumValue(FixedString),
+}
+serde_plain::derive_display_from_serialize!(FirewallIOPolicy);
+serde_plain::derive_fromstr_from_deserialize!(FirewallIOPolicy);
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_firewall_fw_policy_default() {
+ assert_eq!(FirewallFWPolicy::default(), FirewallFWPolicy::Accept);
+ }
+
+ #[test]
+ #[cfg(not(feature = "enum-fallback"))]
+ fn test_firewall_fw_policy_serde_invalid() {
+ serde_plain::from_str::<FirewallFWPolicy>("REJECT")
+ .expect_err("REJECT is not valid for FWPolicy");
+ serde_plain::from_str::<FirewallFWPolicy>("accept")
+ .expect_err("lowercase should be invalid");
+ serde_plain::from_str::<FirewallFWPolicy>("").expect_err("empty string should be invalid");
+ }
+
+ #[test]
+ fn test_firewall_fw_policy_roundtrip() {
+ for policy in [FirewallFWPolicy::Accept, FirewallFWPolicy::Drop] {
+ let serialized = serde_plain::to_string(&policy).expect("serialize");
+ let parsed: FirewallFWPolicy =
+ serde_plain::from_str(&serialized).expect("roundtrip parse");
+ assert_eq!(policy, parsed);
+ }
+ }
+
+ #[test]
+ fn test_firewall_fw_policy_serde() {
+ let accept = FirewallFWPolicy::Accept;
+ let serialized = serde_plain::to_string(&accept).expect("serialize");
+ assert_eq!(serialized, "ACCEPT");
+
+ let parsed: FirewallFWPolicy = serde_plain::from_str(&serialized).expect("deserialize");
+ assert_eq!(parsed, accept);
+
+ let drop = FirewallFWPolicy::Drop;
+ let serialized = serde_plain::to_string(&drop).expect("serialize");
+ assert_eq!(serialized, "DROP");
+
+ let parsed: FirewallFWPolicy = serde_plain::from_str(&serialized).expect("deserialize");
+ assert_eq!(parsed, drop);
+ }
+
+ #[test]
+ #[cfg(feature = "enum-fallback")]
+ fn test_firewall_fw_policy_serde_enum_fallback() {
+ let unknown = FirewallFWPolicy::UnknownEnumValue(FixedString::new("TEST").unwrap());
+ let serialized = serde_plain::to_string(&unknown).expect("serialize");
+ assert_eq!(serialized, "TEST");
+
+ let parsed: FirewallFWPolicy = serde_plain::from_str(&serialized).expect("deserialize");
+ assert_eq!(parsed, unknown);
+ }
+
+ #[test]
+ #[cfg(not(feature = "enum-fallback"))]
+ fn test_firewall_io_policy_serde_invalid() {
+ serde_plain::from_str::<FirewallIOPolicy>("ALLOW").expect_err("ALLOW is not valid");
+ serde_plain::from_str::<FirewallIOPolicy>("drop").expect_err("lowercase should be invalid");
+ serde_plain::from_str::<FirewallIOPolicy>("").expect_err("empty string should be invalid");
+ }
+
+ #[test]
+ fn test_firewall_io_policy_roundtrip() {
+ for policy in [
+ FirewallIOPolicy::Accept,
+ FirewallIOPolicy::Drop,
+ FirewallIOPolicy::Reject,
+ ] {
+ let serialized = serde_plain::to_string(&policy).expect("serialize");
+ let parsed: FirewallIOPolicy =
+ serde_plain::from_str(&serialized).expect("roundtrip parse");
+ assert_eq!(policy, parsed);
+ }
+ }
+
+ #[test]
+ fn test_firewall_io_policy_serde() {
+ for (policy, expected) in [
+ (FirewallIOPolicy::Accept, "ACCEPT"),
+ (FirewallIOPolicy::Drop, "DROP"),
+ (FirewallIOPolicy::Reject, "REJECT"),
+ ] {
+ let serialized = serde_plain::to_string(&policy).expect("serialize");
+ assert_eq!(serialized, expected);
+
+ let parsed: FirewallIOPolicy = serde_plain::from_str(&serialized).expect("deserialize");
+ assert_eq!(parsed, policy);
+ }
+ }
+
+ #[test]
+ #[cfg(feature = "enum-fallback")]
+ fn test_firewall_io_policy_serde_enum_fallback() {
+ let unknown = FirewallIOPolicy::UnknownEnumValue(FixedString::new("TEST").unwrap());
+ let serialized = serde_plain::to_string(&unknown).expect("serialize");
+ assert_eq!(serialized, "TEST");
+
+ let parsed: FirewallIOPolicy = serde_plain::from_str(&serialized).expect("deserialize");
+ assert_eq!(parsed, unknown);
+ }
+}
--
2.47.3
^ permalink raw reply [flat|nested] 26+ messages in thread
* [RFC proxmox 04/22] firewall-api-types: add logging types
2026-02-16 10:43 [RFC proxmox 00/22] New crate for firewall api types Dietmar Maurer
` (2 preceding siblings ...)
2026-02-16 10:43 ` [RFC proxmox 03/22] firewall-api-types: add firewall policy types Dietmar Maurer
@ 2026-02-16 10:43 ` Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 05/22] firewall-api-types: add FirewallClusterOptions Dietmar Maurer
` (18 subsequent siblings)
22 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2026-02-16 10:43 UTC (permalink / raw)
To: pve-devel
This adds several firewall logging-related types:
- FirewallLogLevel: enum for syslog-style log levels (emerg to nolog)
- FirewallLogRateLimit: configuration for rate-limiting log messages
- FirewallPacketRate: packet rate representation (e.g., '100/second')
- FirewallPacketRateTimescale: time units for rate limiting
Includes comprehensive tests for parsing, display, and roundtrip
serialization.
Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
proxmox-firewall-api-types/src/lib.rs | 5 +
proxmox-firewall-api-types/src/log.rs | 312 ++++++++++++++++++++++++++
2 files changed, 317 insertions(+)
create mode 100644 proxmox-firewall-api-types/src/log.rs
diff --git a/proxmox-firewall-api-types/src/lib.rs b/proxmox-firewall-api-types/src/lib.rs
index b8004c76..d9ff4548 100644
--- a/proxmox-firewall-api-types/src/lib.rs
+++ b/proxmox-firewall-api-types/src/lib.rs
@@ -1,2 +1,7 @@
+mod log;
+pub use log::{
+ FirewallLogLevel, FirewallLogRateLimit, FirewallPacketRate, FirewallPacketRateTimescale,
+};
+
mod policy;
pub use policy::{FirewallFWPolicy, FirewallIOPolicy};
diff --git a/proxmox-firewall-api-types/src/log.rs b/proxmox-firewall-api-types/src/log.rs
new file mode 100644
index 00000000..fb2df49e
--- /dev/null
+++ b/proxmox-firewall-api-types/src/log.rs
@@ -0,0 +1,312 @@
+use std::fmt;
+use std::str::FromStr;
+
+use anyhow::{bail, Error};
+use serde::{Deserialize, Serialize};
+
+use proxmox_schema::api;
+
+#[cfg(feature = "enum-fallback")]
+use proxmox_fixed_string::FixedString;
+
+/// Firewall log rate limit time scales.
+#[derive(Copy, Clone, Default, PartialEq)]
+#[cfg_attr(test, derive(Debug))]
+pub enum FirewallPacketRateTimescale {
+ /// second
+ #[default]
+ Second,
+ /// minute
+ Minute,
+ /// hour
+ Hour,
+ /// day
+ Day,
+ #[cfg(feature = "enum-fallback")]
+ /// Unknown variants for forward compatibility.
+ UnknownEnumValue(FixedString),
+}
+
+impl FromStr for FirewallPacketRateTimescale {
+ type Err = Error;
+
+ fn from_str(str: &str) -> Result<Self, Error> {
+ match str {
+ "second" => Ok(FirewallPacketRateTimescale::Second),
+ "minute" => Ok(FirewallPacketRateTimescale::Minute),
+ "hour" => Ok(FirewallPacketRateTimescale::Hour),
+ "day" => Ok(FirewallPacketRateTimescale::Day),
+ "" => bail!("empty time scale specification"),
+ #[cfg(not(feature = "enum-fallback"))]
+ _ => bail!("Invalid time scale provided"),
+ #[cfg(feature = "enum-fallback")]
+ other => Ok(FirewallPacketRateTimescale::UnknownEnumValue(
+ FixedString::from_str(other)?,
+ )),
+ }
+ }
+}
+
+impl fmt::Display for FirewallPacketRateTimescale {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ FirewallPacketRateTimescale::Second => write!(f, "second"),
+ FirewallPacketRateTimescale::Minute => write!(f, "minute"),
+ FirewallPacketRateTimescale::Hour => write!(f, "hour"),
+ FirewallPacketRateTimescale::Day => write!(f, "day"),
+ #[cfg(feature = "enum-fallback")]
+ &FirewallPacketRateTimescale::UnknownEnumValue(scale) => scale.fmt(f),
+ }
+ }
+}
+
+/// Packet rate for log rate limiting.
+#[derive(Copy, Clone, PartialEq)]
+#[cfg_attr(test, derive(Debug))]
+pub struct FirewallPacketRate {
+ /// Number of packets
+ pub packets: u64,
+ /// Time scale for the rate
+ pub timescale: FirewallPacketRateTimescale,
+}
+
+serde_plain::derive_deserialize_from_fromstr!(FirewallPacketRate, "valid packet rate");
+serde_plain::derive_serialize_from_display!(FirewallPacketRate);
+
+impl fmt::Display for FirewallPacketRate {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{}/{}", self.packets, self.timescale)
+ }
+}
+
+impl FromStr for FirewallPacketRate {
+ type Err = Error;
+
+ fn from_str(str: &str) -> Result<Self, Error> {
+ match str.split_once('/') {
+ None => Ok(FirewallPacketRate {
+ packets: u64::from_str(str)?,
+ timescale: FirewallPacketRateTimescale::default(),
+ }),
+ Some((rate, unit)) => Ok(FirewallPacketRate {
+ packets: u64::from_str(rate)?,
+ timescale: FirewallPacketRateTimescale::from_str(unit)?,
+ }),
+ }
+ }
+}
+
+#[api(
+ default_key: "enable",
+ properties: {
+ burst: {
+ default: 5,
+ minimum: 0,
+ optional: true,
+ type: Integer,
+ },
+ enable: {
+ default: true,
+ },
+ rate: {
+ default: "1/second",
+ optional: true,
+ type: String,
+ },
+ },
+)]
+/// Firewall log rate limit configuration.
+#[derive(Deserialize, Serialize, Clone, PartialEq)]
+#[cfg_attr(test, derive(Debug))]
+pub struct FirewallLogRateLimit {
+ /// Initial burst of packages which will always get logged before the rate
+ /// is applied
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_u64")]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub burst: Option<u64>,
+
+ /// Enable or disable log rate limiting
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ pub enable: bool,
+
+ /// Frequency with which the burst bucket gets refilled
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub rate: Option<FirewallPacketRate>,
+}
+
+#[api]
+/// Firewall log levels.
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
+pub enum FirewallLogLevel {
+ #[serde(rename = "emerg")]
+ /// emerg.
+ Emergency,
+ #[serde(rename = "alert")]
+ /// alert.
+ Alert,
+ #[serde(rename = "crit")]
+ /// crit.
+ Critical,
+ #[serde(rename = "err")]
+ /// err.
+ Error,
+ #[serde(rename = "warning")]
+ /// warning.
+ Warning,
+ #[serde(rename = "notice")]
+ /// notice.
+ Notice,
+ #[serde(rename = "info")]
+ /// info.
+ Info,
+ #[serde(rename = "debug")]
+ /// debug.
+ Debug,
+ #[serde(rename = "nolog")]
+ #[default]
+ /// nolog.
+ Nolog,
+}
+
+serde_plain::derive_display_from_serialize!(FirewallLogLevel);
+serde_plain::derive_fromstr_from_deserialize!(FirewallLogLevel);
+
+#[cfg(test)]
+mod tests {
+ use proxmox_schema::property_string::PropertyString;
+
+ use super::*;
+
+ #[test]
+ fn test_parse_rate_limit() {
+ let mut parsed_rate_limit: PropertyString<FirewallLogRateLimit> =
+ serde_plain::from_str("1,burst=123,rate=44").expect("valid rate limit");
+
+ assert_eq!(
+ parsed_rate_limit.into_inner(),
+ FirewallLogRateLimit {
+ enable: true,
+ burst: Some(123),
+ rate: Some(FirewallPacketRate {
+ packets: 44,
+ timescale: FirewallPacketRateTimescale::Second,
+ }),
+ }
+ );
+
+ parsed_rate_limit = serde_plain::from_str("1").expect("valid rate limit");
+
+ assert_eq!(
+ parsed_rate_limit.into_inner(),
+ FirewallLogRateLimit {
+ enable: true,
+ burst: None,
+ rate: None
+ }
+ );
+
+ parsed_rate_limit =
+ serde_plain::from_str("enable=0,rate=123/hour").expect("valid rate limit");
+
+ assert_eq!(
+ parsed_rate_limit.into_inner(),
+ FirewallLogRateLimit {
+ enable: false,
+ burst: None,
+ rate: Some(FirewallPacketRate {
+ packets: 123,
+ timescale: FirewallPacketRateTimescale::Hour,
+ }),
+ }
+ );
+
+ serde_plain::from_str::<PropertyString<FirewallLogRateLimit>>("2")
+ .expect_err("invalid value for enable");
+
+ serde_plain::from_str::<PropertyString<FirewallLogRateLimit>>("enabled=0,rate=123")
+ .expect_err("invalid key in log ratelimit");
+
+ #[cfg(not(feature = "enum-fallback"))]
+ serde_plain::from_str::<PropertyString<FirewallLogRateLimit>>("enable=0,rate=123/proxmox,")
+ .expect_err("invalid unit for rate");
+ }
+
+ #[test]
+ fn test_packet_rate_parse() {
+ // Test parsing with all timescales
+ let rate: FirewallPacketRate = "100/second".parse().expect("valid rate");
+ assert_eq!(rate.packets, 100);
+ assert_eq!(rate.timescale, FirewallPacketRateTimescale::Second);
+
+ let rate: FirewallPacketRate = "50/minute".parse().expect("valid rate");
+ assert_eq!(rate.packets, 50);
+ assert_eq!(rate.timescale, FirewallPacketRateTimescale::Minute);
+
+ let rate: FirewallPacketRate = "10/hour".parse().expect("valid rate");
+ assert_eq!(rate.packets, 10);
+ assert_eq!(rate.timescale, FirewallPacketRateTimescale::Hour);
+
+ let rate: FirewallPacketRate = "1/day".parse().expect("valid rate");
+ assert_eq!(rate.packets, 1);
+ assert_eq!(rate.timescale, FirewallPacketRateTimescale::Day);
+
+ // Test default timescale when no unit specified
+ let rate: FirewallPacketRate = "42".parse().expect("valid rate without unit");
+ assert_eq!(rate.packets, 42);
+ assert_eq!(rate.timescale, FirewallPacketRateTimescale::Second);
+ }
+
+ #[test]
+ fn test_packet_rate_display() {
+ let rate = FirewallPacketRate {
+ packets: 100,
+ timescale: FirewallPacketRateTimescale::Second,
+ };
+ assert_eq!(rate.to_string(), "100/second");
+
+ let rate = FirewallPacketRate {
+ packets: 5,
+ timescale: FirewallPacketRateTimescale::Hour,
+ };
+ assert_eq!(rate.to_string(), "5/hour");
+ }
+
+ #[test]
+ fn test_packet_rate_roundtrip() {
+ let original = FirewallPacketRate {
+ packets: 123,
+ timescale: FirewallPacketRateTimescale::Minute,
+ };
+ let serialized = original.to_string();
+ let parsed: FirewallPacketRate = serialized.parse().expect("roundtrip parse");
+ assert_eq!(original, parsed);
+ }
+
+ #[test]
+ fn test_packet_rate_parse_errors() {
+ // Empty timescale
+ "100/"
+ .parse::<FirewallPacketRate>()
+ .expect_err("empty timescale");
+
+ // Invalid timescale
+ #[cfg(not(feature = "enum-fallback"))]
+ "100/invalid"
+ .parse::<FirewallPacketRate>()
+ .expect_err("invalid timescale");
+ #[cfg(feature = "enum-fallback")]
+ "100/invalid"
+ .parse::<FirewallPacketRate>()
+ .expect("valid timescale (enum fallback feature)");
+
+ // Invalid packet count
+ "abc/second"
+ .parse::<FirewallPacketRate>()
+ .expect_err("invalid packet count");
+
+ // Negative number
+ "-5/second"
+ .parse::<FirewallPacketRate>()
+ .expect_err("negative packet count");
+ }
+}
--
2.47.3
^ permalink raw reply [flat|nested] 26+ messages in thread
* [RFC proxmox 05/22] firewall-api-types: add FirewallClusterOptions
2026-02-16 10:43 [RFC proxmox 00/22] New crate for firewall api types Dietmar Maurer
` (3 preceding siblings ...)
2026-02-16 10:43 ` [RFC proxmox 04/22] firewall-api-types: add logging types Dietmar Maurer
@ 2026-02-16 10:43 ` Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 06/22] firewall-api-types: add FirewallGuestOptions Dietmar Maurer
` (17 subsequent siblings)
22 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2026-02-16 10:43 UTC (permalink / raw)
To: pve-devel
Extracted form perl api.
Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
.../src/cluster_options.rs | 61 +++++++++++++++++++
proxmox-firewall-api-types/src/lib.rs | 3 +
2 files changed, 64 insertions(+)
create mode 100644 proxmox-firewall-api-types/src/cluster_options.rs
diff --git a/proxmox-firewall-api-types/src/cluster_options.rs b/proxmox-firewall-api-types/src/cluster_options.rs
new file mode 100644
index 00000000..bd01a529
--- /dev/null
+++ b/proxmox-firewall-api-types/src/cluster_options.rs
@@ -0,0 +1,61 @@
+use proxmox_schema::{api, ApiStringFormat};
+
+use super::{FirewallFWPolicy, FirewallIOPolicy, FirewallLogRateLimit};
+
+#[api(
+ properties: {
+ ebtables: {
+ default: true,
+ optional: true,
+ },
+ enable: {
+ default: 0,
+ minimum: 0,
+ optional: true,
+ type: Integer,
+ },
+ log_ratelimit: {
+ format: &ApiStringFormat::PropertyString(&FirewallLogRateLimit::API_SCHEMA),
+ optional: true,
+ type: String,
+ },
+ policy_forward: {
+ optional: true,
+ type: FirewallFWPolicy,
+ },
+ policy_in: {
+ optional: true,
+ type: FirewallIOPolicy,
+ },
+ policy_out: {
+ optional: true,
+ type: FirewallIOPolicy,
+ },
+ },
+)]
+/// Cluster Firewall Options
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct FirewallClusterOptions {
+ /// Enable ebtables rules cluster wide.
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub ebtables: Option<bool>,
+
+ /// Enable or disable the firewall cluster wide.
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_u64")]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub enable: Option<u64>,
+
+ /// Log ratelimiting settings
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub log_ratelimit: Option<String>,
+
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub policy_forward: Option<FirewallFWPolicy>,
+
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub policy_in: Option<FirewallIOPolicy>,
+
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub policy_out: Option<FirewallIOPolicy>,
+}
diff --git a/proxmox-firewall-api-types/src/lib.rs b/proxmox-firewall-api-types/src/lib.rs
index d9ff4548..cbd4b804 100644
--- a/proxmox-firewall-api-types/src/lib.rs
+++ b/proxmox-firewall-api-types/src/lib.rs
@@ -5,3 +5,6 @@ pub use log::{
mod policy;
pub use policy::{FirewallFWPolicy, FirewallIOPolicy};
+
+mod cluster_options;
+pub use cluster_options::FirewallClusterOptions;
--
2.47.3
^ permalink raw reply [flat|nested] 26+ messages in thread
* [RFC proxmox 06/22] firewall-api-types: add FirewallGuestOptions
2026-02-16 10:43 [RFC proxmox 00/22] New crate for firewall api types Dietmar Maurer
` (4 preceding siblings ...)
2026-02-16 10:43 ` [RFC proxmox 05/22] firewall-api-types: add FirewallClusterOptions Dietmar Maurer
@ 2026-02-16 10:43 ` Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 07/22] firewall-api-types: add FirewallConntrackHelper enum Dietmar Maurer
` (16 subsequent siblings)
22 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2026-02-16 10:43 UTC (permalink / raw)
To: pve-devel
Extracted form perl api.
Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
.../src/guest_options.rs | 97 +++++++++++++++++++
proxmox-firewall-api-types/src/lib.rs | 3 +
2 files changed, 100 insertions(+)
create mode 100644 proxmox-firewall-api-types/src/guest_options.rs
diff --git a/proxmox-firewall-api-types/src/guest_options.rs b/proxmox-firewall-api-types/src/guest_options.rs
new file mode 100644
index 00000000..3c2dd774
--- /dev/null
+++ b/proxmox-firewall-api-types/src/guest_options.rs
@@ -0,0 +1,97 @@
+use proxmox_schema::api;
+
+use super::{FirewallIOPolicy, FirewallLogLevel};
+
+#[api(
+ properties: {
+ dhcp: {
+ default: false,
+ optional: true,
+ },
+ enable: {
+ default: false,
+ optional: true,
+ },
+ ipfilter: {
+ default: false,
+ optional: true,
+ },
+ log_level_in: {
+ optional: true,
+ type: FirewallLogLevel,
+ },
+ log_level_out: {
+ optional: true,
+ type: FirewallLogLevel,
+ },
+ macfilter: {
+ default: true,
+ optional: true,
+ },
+ ndp: {
+ default: true,
+ optional: true,
+ },
+ policy_in: {
+ optional: true,
+ type: FirewallIOPolicy,
+ },
+ policy_out: {
+ optional: true,
+ type: FirewallIOPolicy,
+ },
+ radv: {
+ default: false,
+ optional: true,
+ },
+ },
+)]
+/// Guest Firewall Options
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct FirewallGuestOptions {
+ /// Enable DHCP.
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub dhcp: Option<bool>,
+
+ /// Enable/disable firewall rules.
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub enable: Option<bool>,
+
+ /// Enable default IP filters. This is equivalent to adding an empty
+ /// ipfilter-net<id> ipset for every interface. Such ipsets implicitly
+ /// contain sane default restrictions such as restricting IPv6 link local
+ /// addresses to the one derived from the interface's MAC address. For
+ /// containers the configured IP addresses will be implicitly added.
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub ipfilter: Option<bool>,
+
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub log_level_in: Option<FirewallLogLevel>,
+
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub log_level_out: Option<FirewallLogLevel>,
+
+ /// Enable/disable MAC address filter.
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub macfilter: Option<bool>,
+
+ /// Enable NDP (Neighbor Discovery Protocol).
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub ndp: Option<bool>,
+
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub policy_in: Option<FirewallIOPolicy>,
+
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub policy_out: Option<FirewallIOPolicy>,
+
+ /// Allow sending Router Advertisement.
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub radv: Option<bool>,
+}
diff --git a/proxmox-firewall-api-types/src/lib.rs b/proxmox-firewall-api-types/src/lib.rs
index cbd4b804..c66262cc 100644
--- a/proxmox-firewall-api-types/src/lib.rs
+++ b/proxmox-firewall-api-types/src/lib.rs
@@ -8,3 +8,6 @@ pub use policy::{FirewallFWPolicy, FirewallIOPolicy};
mod cluster_options;
pub use cluster_options::FirewallClusterOptions;
+
+mod guest_options;
+pub use guest_options::FirewallGuestOptions;
--
2.47.3
^ permalink raw reply [flat|nested] 26+ messages in thread
* [RFC proxmox 07/22] firewall-api-types: add FirewallConntrackHelper enum
2026-02-16 10:43 [RFC proxmox 00/22] New crate for firewall api types Dietmar Maurer
` (5 preceding siblings ...)
2026-02-16 10:43 ` [RFC proxmox 06/22] firewall-api-types: add FirewallGuestOptions Dietmar Maurer
@ 2026-02-16 10:43 ` Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 08/22] firewall-api-types: add FirewallNodeOptions struct Dietmar Maurer
` (15 subsequent siblings)
22 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2026-02-16 10:43 UTC (permalink / raw)
To: pve-devel
Adds an enum for supported conntrack helpers (amanda, ftp, irc, netbios-ns,
pptp, sane, sip, snmp, tftp). Implements CommaSeparatedListSchema to allow
usage with CommaSeparatedList<FirewallConntrackHelper>.
Extracted form perl api and added "enum-fallback" feature.
Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
proxmox-firewall-api-types/src/conntrack.rs | 52 +++++++++++++++++++++
proxmox-firewall-api-types/src/lib.rs | 3 ++
2 files changed, 55 insertions(+)
create mode 100644 proxmox-firewall-api-types/src/conntrack.rs
diff --git a/proxmox-firewall-api-types/src/conntrack.rs b/proxmox-firewall-api-types/src/conntrack.rs
new file mode 100644
index 00000000..278ec2ad
--- /dev/null
+++ b/proxmox-firewall-api-types/src/conntrack.rs
@@ -0,0 +1,52 @@
+#[cfg(feature = "enum-fallback")]
+use proxmox_fixed_string::FixedString;
+use proxmox_schema::comma_separated_list::CommaSeparatedListSchema;
+use proxmox_schema::{api, ApiType, Schema};
+
+#[api]
+/// Firewall conntrack helper.
+#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
+pub enum FirewallConntrackHelper {
+ #[serde(rename = "amanda")]
+ /// amanda.
+ Amanda,
+ #[serde(rename = "ftp")]
+ /// ftp.
+ Ftp,
+ #[serde(rename = "irc")]
+ /// irc.
+ Irc,
+ #[serde(rename = "netbios-ns")]
+ /// netbios-ns.
+ NetbiosNs,
+ #[serde(rename = "pptp")]
+ /// pptp.
+ Pptp,
+ #[serde(rename = "sane")]
+ /// sane.
+ Sane,
+ #[serde(rename = "sip")]
+ /// sip.
+ Sip,
+ #[serde(rename = "snmp")]
+ /// snmp.
+ Snmp,
+ #[serde(rename = "tftp")]
+ /// tftp.
+ Tftp,
+ #[cfg(feature = "enum-fallback")]
+ /// Unknown variants for forward compatibility.
+ #[serde(untagged)]
+ UnknownEnumValue(FixedString),
+}
+serde_plain::derive_display_from_serialize!(FirewallConntrackHelper);
+serde_plain::derive_fromstr_from_deserialize!(FirewallConntrackHelper);
+
+// So that we can use CommaSeparatedList<FirewallConntrackHelper>
+impl CommaSeparatedListSchema for FirewallConntrackHelper {
+ const ARRAY_SCHEMA: Schema = proxmox_schema::ArraySchema::new(
+ "Array of firewall conntrack helper names.",
+ &FirewallConntrackHelper::API_SCHEMA,
+ )
+ .schema();
+}
diff --git a/proxmox-firewall-api-types/src/lib.rs b/proxmox-firewall-api-types/src/lib.rs
index c66262cc..c6fc9ebb 100644
--- a/proxmox-firewall-api-types/src/lib.rs
+++ b/proxmox-firewall-api-types/src/lib.rs
@@ -1,3 +1,6 @@
+mod conntrack;
+pub use conntrack::FirewallConntrackHelper;
+
mod log;
pub use log::{
FirewallLogLevel, FirewallLogRateLimit, FirewallPacketRate, FirewallPacketRateTimescale,
--
2.47.3
^ permalink raw reply [flat|nested] 26+ messages in thread
* [RFC proxmox 08/22] firewall-api-types: add FirewallNodeOptions struct
2026-02-16 10:43 [RFC proxmox 00/22] New crate for firewall api types Dietmar Maurer
` (6 preceding siblings ...)
2026-02-16 10:43 ` [RFC proxmox 07/22] firewall-api-types: add FirewallConntrackHelper enum Dietmar Maurer
@ 2026-02-16 10:43 ` Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 09/22] firewall-api-types: add FirewallRef type Dietmar Maurer
` (14 subsequent siblings)
22 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2026-02-16 10:43 UTC (permalink / raw)
To: pve-devel
Adds the FirewallNodeOptions struct for node-level firewall configuration,
including settings for host firewall enablement, conntrack parameters,
NDP, synflood protection, SMURF filter, TCP flags, and various log level
options. Uses perl-compatible deserialization for boolean/numeric fields.
Extracted form perl api, but replaced Vec<FirewallConntrackHelper> with new
<CommaSeparatedList<FirewallConntrackHelper>> helper type.
Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
proxmox-firewall-api-types/src/lib.rs | 3 +
.../src/node_options.rs | 240 ++++++++++++++++++
2 files changed, 243 insertions(+)
create mode 100644 proxmox-firewall-api-types/src/node_options.rs
diff --git a/proxmox-firewall-api-types/src/lib.rs b/proxmox-firewall-api-types/src/lib.rs
index c6fc9ebb..ef672bfe 100644
--- a/proxmox-firewall-api-types/src/lib.rs
+++ b/proxmox-firewall-api-types/src/lib.rs
@@ -14,3 +14,6 @@ pub use cluster_options::FirewallClusterOptions;
mod guest_options;
pub use guest_options::FirewallGuestOptions;
+
+mod node_options;
+pub use node_options::FirewallNodeOptions;
diff --git a/proxmox-firewall-api-types/src/node_options.rs b/proxmox-firewall-api-types/src/node_options.rs
new file mode 100644
index 00000000..b7873904
--- /dev/null
+++ b/proxmox-firewall-api-types/src/node_options.rs
@@ -0,0 +1,240 @@
+use super::{FirewallConntrackHelper, FirewallLogLevel};
+use proxmox_schema::api;
+use proxmox_schema::comma_separated_list::CommaSeparatedList;
+
+#[api(
+ properties: {
+ enable: {
+ default: true,
+ optional: true,
+ },
+ log_level_forward: {
+ optional: true,
+ type: FirewallLogLevel,
+ },
+ log_level_in: {
+ optional: true,
+ type: FirewallLogLevel,
+ },
+ log_level_out: {
+ optional: true,
+ type: FirewallLogLevel,
+ },
+ log_nf_conntrack: {
+ default: false,
+ optional: true,
+ },
+ ndp: {
+ default: true,
+ optional: true,
+ },
+ nf_conntrack_allow_invalid: {
+ default: false,
+ optional: true,
+ },
+ nf_conntrack_helpers: {
+ optional: true,
+ },
+ nf_conntrack_max: {
+ default: 262144,
+ minimum: 32768,
+ optional: true,
+ type: Integer,
+ },
+ nf_conntrack_tcp_timeout_established: {
+ default: 432000,
+ minimum: 7875,
+ optional: true,
+ type: Integer,
+ },
+ nf_conntrack_tcp_timeout_syn_recv: {
+ default: 60,
+ maximum: 60,
+ minimum: 30,
+ optional: true,
+ type: Integer,
+ },
+ nftables: {
+ default: false,
+ optional: true,
+ },
+ nosmurfs: {
+ default: false,
+ optional: true,
+ },
+ protection_synflood: {
+ default: false,
+ optional: true,
+ },
+ protection_synflood_burst: {
+ default: 1000,
+ optional: true,
+ type: Integer,
+ },
+ protection_synflood_rate: {
+ default: 200,
+ optional: true,
+ type: Integer,
+ },
+ smurf_log_level: {
+ optional: true,
+ type: FirewallLogLevel,
+ },
+ tcp_flags_log_level: {
+ optional: true,
+ type: FirewallLogLevel,
+ },
+ tcpflags: {
+ default: false,
+ optional: true,
+ },
+ },
+)]
+/// Node Firewall Options
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct FirewallNodeOptions {
+ /// Enable host firewall rules.
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub enable: Option<bool>,
+
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub log_level_forward: Option<FirewallLogLevel>,
+
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub log_level_in: Option<FirewallLogLevel>,
+
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub log_level_out: Option<FirewallLogLevel>,
+
+ /// Enable logging of conntrack information.
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub log_nf_conntrack: Option<bool>,
+
+ /// Enable NDP (Neighbor Discovery Protocol).
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub ndp: Option<bool>,
+
+ /// Allow invalid packets on connection tracking.
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub nf_conntrack_allow_invalid: Option<bool>,
+
+ /// Enable conntrack helpers for specific protocols. Supported protocols:
+ /// amanda, ftp, irc, netbios-ns, pptp, sane, sip, snmp, tftp
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub nf_conntrack_helpers: Option<CommaSeparatedList<FirewallConntrackHelper>>,
+
+ /// Maximum number of tracked connections.
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_u64")]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub nf_conntrack_max: Option<u64>,
+
+ /// Conntrack established timeout.
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_u64")]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub nf_conntrack_tcp_timeout_established: Option<u64>,
+
+ /// Conntrack syn recv timeout.
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_u8")]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub nf_conntrack_tcp_timeout_syn_recv: Option<u8>,
+
+ /// Enable nftables based firewall (tech preview)
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub nftables: Option<bool>,
+
+ /// Enable SMURFS filter.
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub nosmurfs: Option<bool>,
+
+ /// Enable synflood protection
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub protection_synflood: Option<bool>,
+
+ /// Synflood protection rate burst by ip src.
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_i64")]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub protection_synflood_burst: Option<i64>,
+
+ /// Synflood protection rate syn/sec by ip src.
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_i64")]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub protection_synflood_rate: Option<i64>,
+
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub smurf_log_level: Option<FirewallLogLevel>,
+
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub tcp_flags_log_level: Option<FirewallLogLevel>,
+
+ /// Filter illegal combinations of TCP flags.
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub tcpflags: Option<bool>,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_conntrack_list() {
+ let ok_tests = vec![
+ ("", None, ""),
+ (
+ "ftp",
+ Some(CommaSeparatedList::new(vec![FirewallConntrackHelper::Ftp])),
+ "ftp",
+ ),
+ (
+ "ftp,pptp",
+ Some(CommaSeparatedList::new(vec![
+ FirewallConntrackHelper::Ftp,
+ FirewallConntrackHelper::Pptp,
+ ])),
+ "ftp,pptp",
+ ),
+ (
+ ",,ftp;pptp;;;",
+ Some(CommaSeparatedList::new(vec![
+ FirewallConntrackHelper::Ftp,
+ FirewallConntrackHelper::Pptp,
+ ])),
+ "ftp,pptp",
+ ),
+ (
+ "ftp\0pptp\0",
+ Some(CommaSeparatedList::new(vec![
+ FirewallConntrackHelper::Ftp,
+ FirewallConntrackHelper::Pptp,
+ ])),
+ "ftp,pptp",
+ ),
+ ];
+
+ for (input, expected, expected_str) in ok_tests {
+ let helpers: Option<CommaSeparatedList<FirewallConntrackHelper>> =
+ serde_plain::from_str(input).unwrap();
+ assert_eq!(helpers, expected);
+ assert_eq!(serde_plain::to_string(&helpers).unwrap(), expected_str);
+ }
+ }
+
+ #[test]
+ #[cfg(not(feature = "enum-fallback"))]
+ fn test_conntrack_list_unknown() {
+ let err_tests = vec!["unknown", "ftp,Unknown", "ftp,pptp,un-known"];
+
+ for input in err_tests {
+ let helpers: Result<Option<CommaSeparatedList<FirewallConntrackHelper>>, _> =
+ serde_plain::from_str(input);
+ assert!(helpers.is_err());
+ }
+ }
+}
--
2.47.3
^ permalink raw reply [flat|nested] 26+ messages in thread
* [RFC proxmox 09/22] firewall-api-types: add FirewallRef type
2026-02-16 10:43 [RFC proxmox 00/22] New crate for firewall api types Dietmar Maurer
` (7 preceding siblings ...)
2026-02-16 10:43 ` [RFC proxmox 08/22] firewall-api-types: add FirewallNodeOptions struct Dietmar Maurer
@ 2026-02-16 10:43 ` Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 10/22] firewall-api-types: add FirewallPortList types Dietmar Maurer
` (13 subsequent siblings)
22 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2026-02-16 10:43 UTC (permalink / raw)
To: pve-devel
Introduce FirewallRef struct and FirewallRefType enum for representing
firewall address references (aliases and ipsets) with their metadata
(name, reference string, scope, and optional comment).
The FirewallRefType enum includes an UnknownEnumValue variant behind
the "enum-fallback" feature flag for forward compatibility with
unknown variants.
Extracted from Perl API.
Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
.../src/firewall_ref.rs | 62 +++++++++++++++++++
proxmox-firewall-api-types/src/lib.rs | 3 +
2 files changed, 65 insertions(+)
create mode 100644 proxmox-firewall-api-types/src/firewall_ref.rs
diff --git a/proxmox-firewall-api-types/src/firewall_ref.rs b/proxmox-firewall-api-types/src/firewall_ref.rs
new file mode 100644
index 00000000..483e57ce
--- /dev/null
+++ b/proxmox-firewall-api-types/src/firewall_ref.rs
@@ -0,0 +1,62 @@
+use serde::{Deserialize, Serialize};
+
+#[cfg(feature = "enum-fallback")]
+use proxmox_fixed_string::FixedString;
+use proxmox_schema::api;
+
+#[api]
+/// Firewall address reference type (ipset or alias).
+#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
+pub enum FirewallRefType {
+ #[serde(rename = "alias")]
+ /// alias.
+ Alias,
+ #[serde(rename = "ipset")]
+ /// ipset.
+ Ipset,
+ /// Unknown variants for forward compatibility.
+ #[cfg(feature = "enum-fallback")]
+ #[serde(untagged)]
+ UnknownEnumValue(FixedString),
+}
+
+#[api(
+ properties: {
+ comment: {
+ optional: true,
+ type: String,
+ description: "Descriptive comment",
+ },
+ name: {
+ type: String,
+ description: "The name of the alias or ipset.",
+ },
+ "ref": {
+ type: String,
+ description: "The reference string used in firewall rules.",
+ },
+ scope: {
+ type: String,
+ description: "The scope of the reference (e.g., SDN).",
+ },
+ type: {
+ type: FirewallRefType,
+ },
+ },
+)]
+/// Firewall address reference information.
+#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
+pub struct FirewallRef {
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub comment: Option<String>,
+
+ pub name: String,
+
+ #[serde(rename = "ref")]
+ pub r#ref: String,
+
+ pub scope: String,
+
+ #[serde(rename = "type")]
+ pub ty: FirewallRefType,
+}
diff --git a/proxmox-firewall-api-types/src/lib.rs b/proxmox-firewall-api-types/src/lib.rs
index ef672bfe..993115d8 100644
--- a/proxmox-firewall-api-types/src/lib.rs
+++ b/proxmox-firewall-api-types/src/lib.rs
@@ -17,3 +17,6 @@ pub use guest_options::FirewallGuestOptions;
mod node_options;
pub use node_options::FirewallNodeOptions;
+
+mod firewall_ref;
+pub use firewall_ref::{FirewallRef, FirewallRefType};
--
2.47.3
^ permalink raw reply [flat|nested] 26+ messages in thread
* [RFC proxmox 10/22] firewall-api-types: add FirewallPortList types
2026-02-16 10:43 [RFC proxmox 00/22] New crate for firewall api types Dietmar Maurer
` (8 preceding siblings ...)
2026-02-16 10:43 ` [RFC proxmox 09/22] firewall-api-types: add FirewallRef type Dietmar Maurer
@ 2026-02-16 10:43 ` Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 11/22] firewall-api-types: add FirewallIcmpType Dietmar Maurer
` (12 subsequent siblings)
22 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2026-02-16 10:43 UTC (permalink / raw)
To: pve-devel
Add new port specification types for firewall rules:
- FirewallPortListEntry: single numeric port, numeric port range or named service
- FirewallPortList: comma-separated list of port entries
Includes comprehensive parsing with validation and unit tests.
Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
proxmox-firewall-api-types/Cargo.toml | 1 +
proxmox-firewall-api-types/src/lib.rs | 5 +
proxmox-firewall-api-types/src/port.rs | 173 +++++++++++++++++++++++++
3 files changed, 179 insertions(+)
create mode 100644 proxmox-firewall-api-types/src/port.rs
diff --git a/proxmox-firewall-api-types/Cargo.toml b/proxmox-firewall-api-types/Cargo.toml
index 3122d813..97b477b8 100644
--- a/proxmox-firewall-api-types/Cargo.toml
+++ b/proxmox-firewall-api-types/Cargo.toml
@@ -15,6 +15,7 @@ enum-fallback = ["dep:proxmox-fixed-string"]
[dependencies]
anyhow.workspace = true
regex.workspace = true
+const_format.workspace = true
serde = { workspace = true, features = [ "derive" ] }
serde_plain = { workspace = true }
diff --git a/proxmox-firewall-api-types/src/lib.rs b/proxmox-firewall-api-types/src/lib.rs
index 993115d8..b099be0c 100644
--- a/proxmox-firewall-api-types/src/lib.rs
+++ b/proxmox-firewall-api-types/src/lib.rs
@@ -20,3 +20,8 @@ pub use node_options::FirewallNodeOptions;
mod firewall_ref;
pub use firewall_ref::{FirewallRef, FirewallRefType};
+
+mod port;
+pub use port::{
+ FirewallPortList, FirewallPortListEntry, FIREWALL_DPORT_API_SCHEMA, FIREWALL_SPORT_API_SCHEMA,
+};
diff --git a/proxmox-firewall-api-types/src/port.rs b/proxmox-firewall-api-types/src/port.rs
new file mode 100644
index 00000000..46989ba4
--- /dev/null
+++ b/proxmox-firewall-api-types/src/port.rs
@@ -0,0 +1,173 @@
+use std::fmt::Display;
+use std::str::FromStr;
+
+use anyhow::{bail, Error};
+use const_format::concatcp;
+use proxmox_schema::{ApiStringFormat, Schema, StringSchema, UpdaterType};
+
+#[derive(Clone, Debug, PartialEq)]
+/// Single entry in a TCP/UDP port list.
+///
+/// Can be a named service, a numeric port or a port range.
+pub enum FirewallPortListEntry {
+ Named(String),
+ Numeric(u16),
+ Range(u16, u16),
+}
+
+impl FromStr for FirewallPortListEntry {
+ type Err = Error;
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ Ok(match s.trim().split_once(':') {
+ None => {
+ if s.is_empty() {
+ bail!("empty port specification");
+ }
+ if s.find(|c: char| !(c.is_digit(10))).is_some() {
+ // Note: arbitrary length limit, longer than anything in /etc/services
+ if s.len() < 256 {
+ if s.contains(|c: char| !(c.is_ascii_alphanumeric() || c == '-')) {
+ bail!("invalid characters in port name");
+ }
+ FirewallPortListEntry::Named(s.to_string())
+ } else {
+ bail!("port name too long");
+ }
+ } else {
+ let port = s.parse::<u16>()?;
+ FirewallPortListEntry::Numeric(port)
+ }
+ }
+ Some((first, second)) => {
+ let first = first.parse::<u16>()?;
+ let second = second.parse::<u16>()?;
+ if first > second {
+ bail!("invalid port range: start port greater than end port")
+ }
+ FirewallPortListEntry::Range(first, second)
+ }
+ })
+ }
+}
+
+impl Display for FirewallPortListEntry {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ FirewallPortListEntry::Named(name) => write!(f, "{}", name),
+ FirewallPortListEntry::Numeric(number) => write!(f, "{}", number),
+ FirewallPortListEntry::Range(first, second) => write!(f, "{}:{}", first, second),
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+/// TCP/UDP port list.
+pub struct FirewallPortList(pub Vec<FirewallPortListEntry>);
+
+const PORT_FORMAT_DESCRIPTION: &'static str = r#"You can use service names or simple numbers (0-65535),
+as defined in '/etc/services'. Port ranges can be specified with '\d+:\d+',
+for example '80:85', and you can use comma separated list to match several ports or ranges."#;
+
+/// API schema for firewall source port list.
+pub const FIREWALL_SPORT_API_SCHEMA: Schema = StringSchema::new(concatcp!(
+ "Restrict TCP/UDP source port. ",
+ PORT_FORMAT_DESCRIPTION
+))
+.format(&ApiStringFormat::VerifyFn(verify_firewall_port_list))
+.schema();
+
+/// API schema for firewall destination port list.
+pub const FIREWALL_DPORT_API_SCHEMA: Schema = StringSchema::new(concatcp!(
+ "Restrict TCP/UDP destination port. ",
+ PORT_FORMAT_DESCRIPTION
+))
+.format(&ApiStringFormat::VerifyFn(verify_firewall_port_list))
+.schema();
+
+serde_plain::derive_deserialize_from_fromstr!(FirewallPortList, "valid port list");
+serde_plain::derive_serialize_from_display!(FirewallPortList);
+
+impl FromStr for FirewallPortList {
+ type Err = Error;
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let mut res = Vec::new();
+ for part in s.split(',') {
+ let entry = FirewallPortListEntry::from_str(part.trim())?;
+ res.push(entry);
+ }
+ Ok(FirewallPortList(res))
+ }
+}
+
+impl Display for FirewallPortList {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ for (i, entry) in self.0.iter().enumerate() {
+ if i > 0 {
+ write!(f, ",")?;
+ }
+ write!(f, "{}", entry)?;
+ }
+ Ok(())
+ }
+}
+
+fn verify_firewall_port_list(s: &str) -> Result<(), Error> {
+ FirewallPortList::from_str(s)?;
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_parse_port_entry() {
+ let mut port_entry: FirewallPortListEntry = "12345".parse().expect("valid port entry");
+ assert_eq!(port_entry, FirewallPortListEntry::Numeric(12345));
+
+ port_entry = "0:65535".parse().expect("valid port entry");
+ assert_eq!(port_entry, FirewallPortListEntry::Range(0, 65535));
+
+ "ssh:80".parse::<FirewallPortListEntry>().unwrap_err();
+ "65536".parse::<FirewallPortListEntry>().unwrap_err();
+ "100:80".parse::<FirewallPortListEntry>().unwrap_err();
+ "100:100000".parse::<FirewallPortListEntry>().unwrap_err();
+ "any-name".parse::<FirewallPortListEntry>().unwrap();
+ "TOS-network-unreachable"
+ .parse::<FirewallPortListEntry>()
+ .unwrap();
+ "no_underscores"
+ .parse::<FirewallPortListEntry>()
+ .unwrap_err();
+ "imap2".parse::<FirewallPortListEntry>().unwrap();
+ "".parse::<FirewallPortListEntry>().unwrap_err();
+ }
+
+ #[test]
+ fn test_parse_port_list() {
+ let mut port_list = FirewallPortList::from_str("12345").expect("valid port list");
+ assert_eq!(
+ port_list,
+ FirewallPortList(vec![FirewallPortListEntry::Numeric(12345)])
+ );
+
+ port_list =
+ FirewallPortList::from_str("12345,0:65535,1337,https").expect("valid port list");
+
+ assert_eq!(
+ port_list,
+ FirewallPortList(vec![
+ FirewallPortListEntry::from_str("12345").unwrap(),
+ FirewallPortListEntry::from_str("0:65535").unwrap(),
+ FirewallPortListEntry::from_str("1337").unwrap(),
+ FirewallPortListEntry::from_str("https").unwrap(),
+ ])
+ );
+
+ FirewallPortList::from_str("0::1337").unwrap_err();
+ FirewallPortList::from_str("0:1337,").unwrap_err();
+ FirewallPortList::from_str("70000").unwrap_err();
+ FirewallPortList::from_str("ssh:80").unwrap_err();
+ FirewallPortList::from_str("").unwrap_err();
+ }
+}
--
2.47.3
^ permalink raw reply [flat|nested] 26+ messages in thread
* [RFC proxmox 11/22] firewall-api-types: add FirewallIcmpType
2026-02-16 10:43 [RFC proxmox 00/22] New crate for firewall api types Dietmar Maurer
` (9 preceding siblings ...)
2026-02-16 10:43 ` [RFC proxmox 10/22] firewall-api-types: add FirewallPortList types Dietmar Maurer
@ 2026-02-16 10:43 ` Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 12/22] firewall-api-types: add FirewallIpsetReference type Dietmar Maurer
` (11 subsequent siblings)
22 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2026-02-16 10:43 UTC (permalink / raw)
To: pve-devel
This adds the `FirewallIcmpType` enum, which can represent ICMP types
either as a named variant (using `FirewallIcmpTypeName`) or as a raw
numeric value (u8).
The `FirewallIcmpTypeName` enum covers standard ICMPv4 and ICMPv6 types,
including deprecated ones and those with codes (e.g., `destination-unreachable`).
It provides `ipv4()` and `ipv6()` helper methods to check if a specific
named type is valid for the respective protocol.
Serialization and deserialization are handled via `serde` and `serde_plain`,
allowing for easy conversion to/from strings.
Includes tests for serialization, deserialization, and protocol validity checks.
---
proxmox-firewall-api-types/src/icmp_type.rs | 555 ++++++++++++++++++++
proxmox-firewall-api-types/src/lib.rs | 3 +
2 files changed, 558 insertions(+)
create mode 100644 proxmox-firewall-api-types/src/icmp_type.rs
diff --git a/proxmox-firewall-api-types/src/icmp_type.rs b/proxmox-firewall-api-types/src/icmp_type.rs
new file mode 100644
index 00000000..b45c1505
--- /dev/null
+++ b/proxmox-firewall-api-types/src/icmp_type.rs
@@ -0,0 +1,555 @@
+use std::fmt;
+
+use anyhow::{bail, Error};
+use serde::{Deserialize, Serialize};
+
+#[cfg(feature = "enum-fallback")]
+use proxmox_fixed_string::FixedString;
+
+use proxmox_schema::{ApiStringFormat, ApiType, Schema, StringSchema};
+
+#[derive(Debug, Copy, Clone, PartialEq)]
+/// ICMP type, either named or numeric.
+pub enum FirewallIcmpType {
+ /// Named ICMP type, e.g. "echo-request"
+ Named(FirewallIcmpTypeName),
+ /// Numeric ICMP type, e.g. "8"
+ Numeric(u8),
+}
+
+impl ApiType for FirewallIcmpType {
+ const API_SCHEMA: Schema = StringSchema::new(
+ r#"ICMP type, either named or numeric.
+ Only valid if proto equals 'icmp' or 'icmpv6'/'ipv6-icmp'."#,
+ )
+ .format(&ApiStringFormat::VerifyFn(verify_firewall_icmp_type))
+ .schema();
+}
+
+fn verify_firewall_icmp_type(value: &str) -> Result<(), Error> {
+ value.parse::<FirewallIcmpType>().map(|_| ())
+}
+
+impl std::str::FromStr for FirewallIcmpType {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Self, Error> {
+ let s = s.trim();
+
+ if let Ok(ty) = s.parse::<u8>() {
+ return Ok(Self::Numeric(ty));
+ }
+
+ if let Ok(named) = serde_plain::from_str::<FirewallIcmpTypeName>(s) {
+ return Ok(Self::Named(named));
+ }
+
+ bail!("{s:?} is not a valid icmp type");
+ }
+}
+
+impl fmt::Display for FirewallIcmpType {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ FirewallIcmpType::Numeric(ty) => write!(f, "{ty}"),
+ FirewallIcmpType::Named(ty) => write!(f, "{ty}"),
+ }
+ }
+}
+
+serde_plain::derive_deserialize_from_fromstr!(FirewallIcmpType, "valid icmp type name or number");
+serde_plain::derive_serialize_from_display!(FirewallIcmpType);
+
+#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq)]
+#[serde(rename_all = "kebab-case")]
+/// Named ICMP type, e.g. "echo-request".
+pub enum FirewallIcmpTypeName {
+ Any,
+
+ // IPv4 specific
+ HostUnreachable, // 3:1 (ICMP-TYPE, ICMP-CODE)
+ ProtocolUnreachable, // 3:2
+ PortUnreachable, // 3:3
+ FragmentationNeeded, // 3:4
+ SourceRouteFailed, // 3:5
+ NetworkUnknown, // 3:6
+ HostUnknown, // 3:7
+ NetworkProhibited, // 3:9
+ HostProhibited, // 3:10
+ #[serde(rename = "TOS-network-unreachable")]
+ TOSNetworkUnreachable, // 3:11
+ #[serde(rename = "TOS-host-unreachable")]
+ TOSHostUnreachable, // 3:12
+ CommunicationProhibited, // 3:13
+ HostPrecedenceViolation, // 3:14
+ PrecedenceCutoff, // 3:15
+ SourceQuench, // 4
+ NetworkRedirect, // 5:0
+ HostRedirect, // 5:1
+ #[serde(rename = "TOS-network-redirect")]
+ TOSNetworkRedirect, // 5:2
+ #[serde(rename = "TOS-host-redirect")]
+ TOSHostRedirect, // 5:3
+ TimestampRequest, // 13
+ TimestampReply, // 14
+ AddressMaskRequest, // 17
+ AddressMaskReply, // 18
+ RouterAdvertisement, // 9
+ RouterSolicitation, // 10
+ IpHeaderBad, // 12:0
+ RequiredOptionMissing, // 12:1
+
+ // IPv6 specific
+ NoRoute, // 1:0
+ BeyondScope, // 1:2
+ AddressUnreachable, // 1:3
+ FailedPolicy, // 1:5
+ RejectRoute, // 1:6
+ PacketTooBig, // 2
+ BadHeader, // 4:0
+ UnknownHeaderType, // 4:1
+ UnknownOption, // 4:2
+ NeighborSolicitation, // 135
+ NeighbourSolicitation, // 135
+ NeighborAdvertisement, // 136
+ NeighbourAdvertisement, // 136
+
+ // Common
+ DestinationUnreachable, // 3:0 (v4), 1:0 (v6) ? v6 has no-route
+ NetworkUnreachable, // 3:0
+ EchoReply, // 0 (v4), 129 (v6)
+ EchoRequest, // 8 (v4), 128 (v6)
+ TimeExceeded, // 11 (v4), 3 (v6)
+ TtlZeroDuringTransit, // 11:0 (v4), 3:0 (v6)
+ TtlZeroDuringReassembly, // 11:1 (v4), 3:1 (v6)
+ ParameterProblem, // 12 (v4), 4 (v6)
+ Redirect, // 5 (v4), 137 (v6)
+
+ #[cfg(feature = "enum-fallback")]
+ #[serde(untagged)]
+ /// Unknwon
+ UnknownEnumValue(FixedString),
+}
+
+serde_plain::derive_display_from_serialize!(FirewallIcmpTypeName);
+serde_plain::derive_fromstr_from_deserialize!(FirewallIcmpTypeName);
+
+impl FirewallIcmpTypeName {
+ pub const fn ipv4(&self) -> bool {
+ match self {
+ FirewallIcmpTypeName::Any => true,
+ FirewallIcmpTypeName::EchoReply => true,
+ FirewallIcmpTypeName::DestinationUnreachable => true,
+ FirewallIcmpTypeName::NetworkUnreachable => true,
+ FirewallIcmpTypeName::HostUnreachable => true,
+ FirewallIcmpTypeName::ProtocolUnreachable => true,
+ FirewallIcmpTypeName::PortUnreachable => true,
+ FirewallIcmpTypeName::FragmentationNeeded => true,
+ FirewallIcmpTypeName::SourceRouteFailed => true,
+ FirewallIcmpTypeName::NetworkUnknown => true,
+ FirewallIcmpTypeName::HostUnknown => true,
+ FirewallIcmpTypeName::NetworkProhibited => true,
+ FirewallIcmpTypeName::HostProhibited => true,
+ FirewallIcmpTypeName::TOSNetworkUnreachable => true,
+ FirewallIcmpTypeName::TOSHostUnreachable => true,
+ FirewallIcmpTypeName::CommunicationProhibited => true,
+ FirewallIcmpTypeName::HostPrecedenceViolation => true,
+ FirewallIcmpTypeName::PrecedenceCutoff => true,
+ FirewallIcmpTypeName::SourceQuench => true,
+ FirewallIcmpTypeName::Redirect => true,
+ FirewallIcmpTypeName::NetworkRedirect => true,
+ FirewallIcmpTypeName::HostRedirect => true,
+ FirewallIcmpTypeName::TOSNetworkRedirect => true,
+ FirewallIcmpTypeName::TOSHostRedirect => true,
+ FirewallIcmpTypeName::EchoRequest => true,
+ FirewallIcmpTypeName::RouterAdvertisement => true,
+ FirewallIcmpTypeName::RouterSolicitation => true,
+ FirewallIcmpTypeName::TimeExceeded => true,
+ FirewallIcmpTypeName::TtlZeroDuringTransit => true,
+ FirewallIcmpTypeName::TtlZeroDuringReassembly => true,
+ FirewallIcmpTypeName::ParameterProblem => true,
+ FirewallIcmpTypeName::IpHeaderBad => true,
+ FirewallIcmpTypeName::RequiredOptionMissing => true,
+ FirewallIcmpTypeName::TimestampRequest => true,
+ FirewallIcmpTypeName::TimestampReply => true,
+ FirewallIcmpTypeName::AddressMaskRequest => true,
+ FirewallIcmpTypeName::AddressMaskReply => true,
+ _ => false,
+ }
+ }
+
+ pub const fn ipv6(&self) -> bool {
+ match self {
+ FirewallIcmpTypeName::Any => true,
+ FirewallIcmpTypeName::EchoReply => true,
+ FirewallIcmpTypeName::DestinationUnreachable => true,
+ FirewallIcmpTypeName::PacketTooBig => true,
+ FirewallIcmpTypeName::TimeExceeded => true,
+ FirewallIcmpTypeName::ParameterProblem => true,
+ FirewallIcmpTypeName::EchoRequest => true,
+ FirewallIcmpTypeName::RouterSolicitation => true,
+ FirewallIcmpTypeName::RouterAdvertisement => true,
+ FirewallIcmpTypeName::NeighborSolicitation => true,
+ FirewallIcmpTypeName::NeighbourSolicitation => true,
+ FirewallIcmpTypeName::NeighborAdvertisement => true,
+ FirewallIcmpTypeName::NeighbourAdvertisement => true,
+ FirewallIcmpTypeName::Redirect => true,
+ FirewallIcmpTypeName::NoRoute => true,
+ FirewallIcmpTypeName::CommunicationProhibited => true,
+ FirewallIcmpTypeName::BeyondScope => true,
+ FirewallIcmpTypeName::AddressUnreachable => true,
+ FirewallIcmpTypeName::PortUnreachable => true,
+ FirewallIcmpTypeName::FailedPolicy => true,
+ FirewallIcmpTypeName::RejectRoute => true,
+ FirewallIcmpTypeName::TtlZeroDuringTransit => true,
+ FirewallIcmpTypeName::TtlZeroDuringReassembly => true,
+ FirewallIcmpTypeName::BadHeader => true,
+ FirewallIcmpTypeName::UnknownHeaderType => true,
+ FirewallIcmpTypeName::UnknownOption => true,
+ _ => false,
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+
+ fn test_icmp_types() {
+ // (name, variant, ipv4, ipv6)
+ let tests = [
+ ("any", FirewallIcmpTypeName::Any, true, true),
+ ("echo-reply", FirewallIcmpTypeName::EchoReply, true, true),
+ (
+ "destination-unreachable",
+ FirewallIcmpTypeName::DestinationUnreachable,
+ true,
+ true,
+ ),
+ (
+ "network-unreachable",
+ FirewallIcmpTypeName::NetworkUnreachable,
+ true,
+ false,
+ ),
+ (
+ "host-unreachable",
+ FirewallIcmpTypeName::HostUnreachable,
+ true,
+ false,
+ ),
+ (
+ "protocol-unreachable",
+ FirewallIcmpTypeName::ProtocolUnreachable,
+ true,
+ false,
+ ),
+ (
+ "port-unreachable",
+ FirewallIcmpTypeName::PortUnreachable,
+ true,
+ true,
+ ),
+ (
+ "fragmentation-needed",
+ FirewallIcmpTypeName::FragmentationNeeded,
+ true,
+ false,
+ ),
+ (
+ "source-route-failed",
+ FirewallIcmpTypeName::SourceRouteFailed,
+ true,
+ false,
+ ),
+ (
+ "network-unknown",
+ FirewallIcmpTypeName::NetworkUnknown,
+ true,
+ false,
+ ),
+ (
+ "host-unknown",
+ FirewallIcmpTypeName::HostUnknown,
+ true,
+ false,
+ ),
+ (
+ "network-prohibited",
+ FirewallIcmpTypeName::NetworkProhibited,
+ true,
+ false,
+ ),
+ (
+ "host-prohibited",
+ FirewallIcmpTypeName::HostProhibited,
+ true,
+ false,
+ ),
+ (
+ "TOS-network-unreachable",
+ FirewallIcmpTypeName::TOSNetworkUnreachable,
+ true,
+ false,
+ ),
+ (
+ "TOS-host-unreachable",
+ FirewallIcmpTypeName::TOSHostUnreachable,
+ true,
+ false,
+ ),
+ (
+ "communication-prohibited",
+ FirewallIcmpTypeName::CommunicationProhibited,
+ true,
+ true,
+ ),
+ (
+ "host-precedence-violation",
+ FirewallIcmpTypeName::HostPrecedenceViolation,
+ true,
+ false,
+ ),
+ (
+ "precedence-cutoff",
+ FirewallIcmpTypeName::PrecedenceCutoff,
+ true,
+ false,
+ ),
+ (
+ "source-quench",
+ FirewallIcmpTypeName::SourceQuench,
+ true,
+ false,
+ ),
+ ("redirect", FirewallIcmpTypeName::Redirect, true, true),
+ (
+ "network-redirect",
+ FirewallIcmpTypeName::NetworkRedirect,
+ true,
+ false,
+ ),
+ (
+ "host-redirect",
+ FirewallIcmpTypeName::HostRedirect,
+ true,
+ false,
+ ),
+ (
+ "TOS-network-redirect",
+ FirewallIcmpTypeName::TOSNetworkRedirect,
+ true,
+ false,
+ ),
+ (
+ "TOS-host-redirect",
+ FirewallIcmpTypeName::TOSHostRedirect,
+ true,
+ false,
+ ),
+ (
+ "echo-request",
+ FirewallIcmpTypeName::EchoRequest,
+ true,
+ true,
+ ),
+ (
+ "router-advertisement",
+ FirewallIcmpTypeName::RouterAdvertisement,
+ true,
+ true,
+ ),
+ (
+ "router-solicitation",
+ FirewallIcmpTypeName::RouterSolicitation,
+ true,
+ true,
+ ),
+ (
+ "time-exceeded",
+ FirewallIcmpTypeName::TimeExceeded,
+ true,
+ true,
+ ),
+ (
+ "ttl-zero-during-transit",
+ FirewallIcmpTypeName::TtlZeroDuringTransit,
+ true,
+ true,
+ ),
+ (
+ "ttl-zero-during-reassembly",
+ FirewallIcmpTypeName::TtlZeroDuringReassembly,
+ true,
+ true,
+ ),
+ (
+ "parameter-problem",
+ FirewallIcmpTypeName::ParameterProblem,
+ true,
+ true,
+ ),
+ (
+ "ip-header-bad",
+ FirewallIcmpTypeName::IpHeaderBad,
+ true,
+ false,
+ ),
+ (
+ "required-option-missing",
+ FirewallIcmpTypeName::RequiredOptionMissing,
+ true,
+ false,
+ ),
+ (
+ "timestamp-request",
+ FirewallIcmpTypeName::TimestampRequest,
+ true,
+ false,
+ ),
+ (
+ "timestamp-reply",
+ FirewallIcmpTypeName::TimestampReply,
+ true,
+ false,
+ ),
+ (
+ "address-mask-request",
+ FirewallIcmpTypeName::AddressMaskRequest,
+ true,
+ false,
+ ),
+ (
+ "address-mask-reply",
+ FirewallIcmpTypeName::AddressMaskReply,
+ true,
+ false,
+ ),
+ ("no-route", FirewallIcmpTypeName::NoRoute, false, true),
+ (
+ "beyond-scope",
+ FirewallIcmpTypeName::BeyondScope,
+ false,
+ true,
+ ),
+ (
+ "address-unreachable",
+ FirewallIcmpTypeName::AddressUnreachable,
+ false,
+ true,
+ ),
+ (
+ "failed-policy",
+ FirewallIcmpTypeName::FailedPolicy,
+ false,
+ true,
+ ),
+ (
+ "reject-route",
+ FirewallIcmpTypeName::RejectRoute,
+ false,
+ true,
+ ),
+ (
+ "packet-too-big",
+ FirewallIcmpTypeName::PacketTooBig,
+ false,
+ true,
+ ),
+ ("bad-header", FirewallIcmpTypeName::BadHeader, false, true),
+ (
+ "unknown-header-type",
+ FirewallIcmpTypeName::UnknownHeaderType,
+ false,
+ true,
+ ),
+ (
+ "unknown-option",
+ FirewallIcmpTypeName::UnknownOption,
+ false,
+ true,
+ ),
+ (
+ "neighbor-solicitation",
+ FirewallIcmpTypeName::NeighborSolicitation,
+ false,
+ true,
+ ),
+ (
+ "neighbour-solicitation",
+ FirewallIcmpTypeName::NeighbourSolicitation,
+ false,
+ true,
+ ),
+ (
+ "neighbor-advertisement",
+ FirewallIcmpTypeName::NeighborAdvertisement,
+ false,
+ true,
+ ),
+ (
+ "neighbour-advertisement",
+ FirewallIcmpTypeName::NeighbourAdvertisement,
+ false,
+ true,
+ ),
+ ];
+
+ for (input, expected, v4, v6) in tests {
+ let deserialized: FirewallIcmpTypeName =
+ serde_plain::from_str(input).expect("deserialize");
+ assert_eq!(deserialized, expected);
+ let serialized = serde_plain::to_string(&deserialized).expect("serialize");
+ assert_eq!(serialized, input);
+
+ assert_eq!(deserialized.ipv4(), v4, "ipv4 check failed for {}", input);
+ assert_eq!(deserialized.ipv6(), v6, "ipv6 check failed for {}", input);
+ }
+ }
+
+ #[test]
+ fn test_firewall_icmp_type_enum() {
+ // Numeric
+ for (input, output) in [
+ ("0", FirewallIcmpType::Numeric(0)),
+ ("10", FirewallIcmpType::Numeric(10)),
+ ("255", FirewallIcmpType::Numeric(255)),
+ ] {
+ let ty: FirewallIcmpType = input.parse().expect("valid numeric icmp type");
+ assert_eq!(ty, output);
+ assert_eq!(ty.to_string(), input);
+ }
+
+ // Named
+ for (input, output) in [
+ ("echo-request", FirewallIcmpTypeName::EchoRequest),
+ ("any", FirewallIcmpTypeName::Any),
+ ] {
+ let ty: FirewallIcmpType = input.parse().expect("valid named icmp type");
+ assert_eq!(ty, FirewallIcmpType::Named(output));
+ assert_eq!(ty.to_string(), input);
+ }
+
+ // Invalid
+ #[cfg(not(feature = "enum-fallback"))]
+ for input in ["echo-reques", "an", "256", "-1", "foo"] {
+ input
+ .parse::<FirewallIcmpType>()
+ .expect_err("invalid icmp type");
+ }
+
+ // Invalid, but with enum fallback enabled, should be parsed as unknown
+ #[cfg(feature = "enum-fallback")]
+ for input in ["echo-reques", "an", "256", "-1", "foo"] {
+ let ty = input.parse::<FirewallIcmpType>().expect("valid icmp type");
+ assert_eq!(
+ ty,
+ FirewallIcmpType::Named(FirewallIcmpTypeName::UnknownEnumValue(
+ FixedString::new(input).unwrap(),
+ )),
+ );
+ }
+ }
+}
diff --git a/proxmox-firewall-api-types/src/lib.rs b/proxmox-firewall-api-types/src/lib.rs
index b099be0c..610282bb 100644
--- a/proxmox-firewall-api-types/src/lib.rs
+++ b/proxmox-firewall-api-types/src/lib.rs
@@ -1,6 +1,9 @@
mod conntrack;
pub use conntrack::FirewallConntrackHelper;
+mod icmp_type;
+pub use icmp_type::{FirewallIcmpType, FirewallIcmpTypeName};
+
mod log;
pub use log::{
FirewallLogLevel, FirewallLogRateLimit, FirewallPacketRate, FirewallPacketRateTimescale,
--
2.47.3
^ permalink raw reply [flat|nested] 26+ messages in thread
* [RFC proxmox 12/22] firewall-api-types: add FirewallIpsetReference type
2026-02-16 10:43 [RFC proxmox 00/22] New crate for firewall api types Dietmar Maurer
` (10 preceding siblings ...)
2026-02-16 10:43 ` [RFC proxmox 11/22] firewall-api-types: add FirewallIcmpType Dietmar Maurer
@ 2026-02-16 10:43 ` Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 13/22] firewall-api-types: add FirewallAliasReference type Dietmar Maurer
` (10 subsequent siblings)
22 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2026-02-16 10:43 UTC (permalink / raw)
To: pve-devel
This adds a new type to reference ipsets with proper scope handling
(Datacenter, Guest, SDN, or None for legacy ipsets).
The implementation includes:
- FirewallIpsetScope enum for scope variants
- FirewallIpsetReference struct with validation
- Proper encapsulation with constructor and accessor methods
- FromStr implementation for parsing ipset references
Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
proxmox-firewall-api-types/src/ipset.rs | 191 ++++++++++++++++++++++++
proxmox-firewall-api-types/src/lib.rs | 3 +
2 files changed, 194 insertions(+)
create mode 100644 proxmox-firewall-api-types/src/ipset.rs
diff --git a/proxmox-firewall-api-types/src/ipset.rs b/proxmox-firewall-api-types/src/ipset.rs
new file mode 100644
index 00000000..02659394
--- /dev/null
+++ b/proxmox-firewall-api-types/src/ipset.rs
@@ -0,0 +1,191 @@
+use std::fmt;
+use std::str::FromStr;
+
+use anyhow::{bail, Error};
+
+#[cfg(feature = "enum-fallback")]
+use proxmox_fixed_string::FixedString;
+
+/// The scope of an ipset.
+#[derive(Debug, Clone, Copy, Eq, PartialEq)]
+pub enum FirewallIpsetScope {
+ /// Datacenter scope.
+ Datacenter,
+ /// Guest scope.
+ Guest,
+ /// SDN scope.
+ Sdn,
+ /// No scope (e.g. for legacy ipsets).
+ None,
+ #[cfg(feature = "enum-fallback")]
+ /// Unknown variants for forward compatibility.
+ UnknownEnumValue(FixedString),
+}
+
+/// A reference to an ipset, including its scope.
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct FirewallIpsetReference {
+ scope: FirewallIpsetScope,
+ name: String,
+}
+
+impl FirewallIpsetReference {
+ pub fn new(scope: FirewallIpsetScope, name: String) -> Result<Self, Error> {
+ verify_ipset_name(&name)?;
+ Ok(Self { scope, name })
+ }
+
+ pub fn scope(&self) -> FirewallIpsetScope {
+ self.scope
+ }
+
+ pub fn name(&self) -> &str {
+ &self.name
+ }
+}
+
+fn verify_ipset_name(name: &str) -> Result<(), Error> {
+ if name.is_empty() {
+ bail!("ipset name cannot be empty");
+ }
+
+ if !name.starts_with(|c: char| c.is_ascii_alphabetic()) {
+ bail!("ipset name must start with an ASCII letter");
+ }
+
+ if name.contains(|c: char| !c.is_ascii_alphanumeric() && c != '-' && c != '_') {
+ bail!("ipset name can only contain ASCII letters, digits, hyphens, and underscores");
+ }
+
+ Ok(())
+}
+
+impl fmt::Display for FirewallIpsetReference {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ "+".fmt(f)?;
+ match self.scope {
+ FirewallIpsetScope::Datacenter => write!(f, "dc/{}", self.name),
+ FirewallIpsetScope::Guest => write!(f, "guest/{}", self.name),
+ FirewallIpsetScope::Sdn => write!(f, "sdn/{}", self.name),
+ #[cfg(feature = "enum-fallback")]
+ FirewallIpsetScope::UnknownEnumValue(scope) => write!(f, "{}/{}", scope, self.name),
+ FirewallIpsetScope::None => self.name.fmt(f),
+ }
+ }
+}
+
+impl FromStr for FirewallIpsetReference {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let s = s.trim();
+
+ let s = match s.strip_prefix('+') {
+ Some(rest) => rest,
+ None => bail!("ipset reference must start with '+'"),
+ };
+
+ if s.is_empty() {
+ bail!("empty firewall ipset specification");
+ }
+
+ let (scope, name) = match s.split_once('/') {
+ Some(("dc", alias)) => (FirewallIpsetScope::Datacenter, alias),
+ Some(("guest", alias)) => (FirewallIpsetScope::Guest, alias),
+ Some(("sdn", alias)) => (FirewallIpsetScope::Sdn, alias),
+ #[cfg(not(feature = "enum-fallback"))]
+ Some((scope, _alias)) => bail!("invalid firewall ipset reference scope: {scope}"),
+ #[cfg(feature = "enum-fallback")]
+ Some((scope, alias)) => (
+ FirewallIpsetScope::UnknownEnumValue(FixedString::from_str(scope)?),
+ alias,
+ ),
+ None => (FirewallIpsetScope::None, s),
+ };
+
+ Self::new(scope, name.to_string())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_parse_ipset_name() {
+ for test_case in [
+ (
+ "+dc/proxmox-123",
+ FirewallIpsetScope::Datacenter,
+ "proxmox-123",
+ ),
+ (
+ "+guest/proxmox_123",
+ FirewallIpsetScope::Guest,
+ "proxmox_123",
+ ),
+ ] {
+ let ipset_name = test_case
+ .0
+ .parse::<FirewallIpsetReference>()
+ .expect("valid ipset name");
+
+ assert_eq!(
+ ipset_name,
+ FirewallIpsetReference {
+ scope: test_case.1,
+ name: test_case.2.to_string(),
+ }
+ )
+ }
+
+ for name in ["+dc/", "guest/proxmox_123"] {
+ name.parse::<FirewallIpsetReference>()
+ .expect_err("invalid ipset name");
+ }
+
+ #[cfg(feature = "enum-fallback")]
+ for name in ["+guests/proxmox_123", "+nonsense/abc"] {
+ name.parse::<FirewallIpsetReference>()
+ .expect("valid ipset name (enum fallback feature)");
+ }
+ }
+
+ #[test]
+ fn test_parse_legacy_ipset_name() {
+ for test_case in [
+ ("+proxmox_123", "proxmox_123"),
+ ("+proxmox_---123", "proxmox_---123"),
+ ] {
+ let ipset_name = test_case
+ .0
+ .parse::<FirewallIpsetReference>()
+ .expect("valid ipset name");
+
+ assert_eq!(
+ ipset_name,
+ FirewallIpsetReference {
+ scope: FirewallIpsetScope::None,
+ name: test_case.1.to_string(),
+ }
+ )
+ }
+
+ for name in ["guest/proxmox_123", "+-qwe", "+1qwe"] {
+ name.parse::<FirewallIpsetReference>()
+ .expect_err("invalid ipset name");
+ }
+ }
+
+ #[test]
+ fn test_new_ipset() {
+ let ipset =
+ FirewallIpsetReference::new(FirewallIpsetScope::Datacenter, "proxmox-123".to_string())
+ .expect("valid ipset name");
+ assert_eq!(ipset.scope(), FirewallIpsetScope::Datacenter);
+ assert_eq!(ipset.name(), "proxmox-123");
+
+ FirewallIpsetReference::new(FirewallIpsetScope::Datacenter, "+invalid".to_string())
+ .expect_err("invalid ipset name");
+ }
+}
diff --git a/proxmox-firewall-api-types/src/lib.rs b/proxmox-firewall-api-types/src/lib.rs
index 610282bb..abd78c98 100644
--- a/proxmox-firewall-api-types/src/lib.rs
+++ b/proxmox-firewall-api-types/src/lib.rs
@@ -4,6 +4,9 @@ pub use conntrack::FirewallConntrackHelper;
mod icmp_type;
pub use icmp_type::{FirewallIcmpType, FirewallIcmpTypeName};
+mod ipset;
+pub use ipset::{FirewallIpsetReference, FirewallIpsetScope};
+
mod log;
pub use log::{
FirewallLogLevel, FirewallLogRateLimit, FirewallPacketRate, FirewallPacketRateTimescale,
--
2.47.3
^ permalink raw reply [flat|nested] 26+ messages in thread
* [RFC proxmox 13/22] firewall-api-types: add FirewallAliasReference type
2026-02-16 10:43 [RFC proxmox 00/22] New crate for firewall api types Dietmar Maurer
` (11 preceding siblings ...)
2026-02-16 10:43 ` [RFC proxmox 12/22] firewall-api-types: add FirewallIpsetReference type Dietmar Maurer
@ 2026-02-16 10:43 ` Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 14/22] firewall-api-types: add firewall address types Dietmar Maurer
` (9 subsequent siblings)
22 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2026-02-16 10:43 UTC (permalink / raw)
To: pve-devel
This adds a new type to reference aliases with proper scope handling
(Datacenter, Guest, or None for legacy aliases).
The implementation includes:
- FirewallAliasScope enum for scope variants
- FirewallAliasReference struct with validation
- Proper encapsulation with constructor and accessor methods
- FromStr implementation for parsing alias references
Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
proxmox-firewall-api-types/src/alias.rs | 142 ++++++++++++++++++++++++
proxmox-firewall-api-types/src/lib.rs | 3 +
2 files changed, 145 insertions(+)
create mode 100644 proxmox-firewall-api-types/src/alias.rs
diff --git a/proxmox-firewall-api-types/src/alias.rs b/proxmox-firewall-api-types/src/alias.rs
new file mode 100644
index 00000000..7722148c
--- /dev/null
+++ b/proxmox-firewall-api-types/src/alias.rs
@@ -0,0 +1,142 @@
+use std::fmt;
+use std::str::FromStr;
+
+use anyhow::{bail, Error};
+
+#[cfg(feature = "enum-fallback")]
+use proxmox_fixed_string::FixedString;
+
+/// The scope of an alias.
+#[derive(Debug, Clone, Copy, Eq, PartialEq)]
+pub enum FirewallAliasScope {
+ /// Datacenter scope.
+ Datacenter,
+ /// Guest scope.
+ Guest,
+ /// No scope (e.g. for legacy aliases).
+ None,
+ #[cfg(feature = "enum-fallback")]
+ /// Unknown variants for forward compatibility.
+ UnknownEnumValue(FixedString),
+}
+
+/// A reference to an alias, including its scope.
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct FirewallAliasReference {
+ scope: FirewallAliasScope,
+ name: String,
+}
+
+impl FirewallAliasReference {
+ pub fn new(scope: FirewallAliasScope, name: String) -> Result<Self, Error> {
+ verify_alias_name(&name)?;
+ Ok(Self { scope, name })
+ }
+
+ pub fn scope(&self) -> FirewallAliasScope {
+ self.scope
+ }
+
+ pub fn name(&self) -> &str {
+ &self.name
+ }
+}
+
+fn verify_alias_name(name: &str) -> Result<(), Error> {
+ if name.is_empty() {
+ bail!("alias name cannot be empty");
+ }
+
+ if !name.starts_with(|c: char| c.is_ascii_alphabetic()) {
+ bail!("alias name must start with an ASCII letter");
+ }
+
+ if name.contains(|c: char| !c.is_ascii_alphanumeric() && c != '-' && c != '_') {
+ bail!("alias name can only contain ASCII letters, digits, hyphens, and underscores");
+ }
+
+ Ok(())
+}
+
+impl fmt::Display for FirewallAliasReference {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self.scope {
+ FirewallAliasScope::Datacenter => write!(f, "dc/{}", self.name),
+ FirewallAliasScope::Guest => write!(f, "guest/{}", self.name),
+ #[cfg(feature = "enum-fallback")]
+ FirewallAliasScope::UnknownEnumValue(scope) => write!(f, "{}/{}", scope, self.name),
+ FirewallAliasScope::None => self.name.fmt(f),
+ }
+ }
+}
+
+impl FromStr for FirewallAliasReference {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let s = s.trim();
+
+ if s.is_empty() {
+ bail!("empty firewall alias specification");
+ }
+
+ let (scope, name) = match s.split_once('/') {
+ Some(("dc", alias)) => (FirewallAliasScope::Datacenter, alias),
+ Some(("guest", alias)) => (FirewallAliasScope::Guest, alias),
+ #[cfg(not(feature = "enum-fallback"))]
+ Some((scope, _alias)) => bail!("invalid firewall alias scope: {scope}"),
+ #[cfg(feature = "enum-fallback")]
+ Some((scope, alias)) => (
+ FirewallAliasScope::UnknownEnumValue(FixedString::from_str(scope)?),
+ alias,
+ ),
+ None => (FirewallAliasScope::None, s),
+ };
+
+ Self::new(scope, name.to_string())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_parse_legacy_alias_name() {
+ for name in ["proxmox_123", "proxmox-123"] {
+ name.parse::<FirewallAliasReference>()
+ .expect("valid alias name");
+ }
+
+ for name in ["1proxmox_123", "-proxmox-123"] {
+ name.parse::<FirewallAliasReference>()
+ .expect_err("invalid alias name");
+ }
+ }
+
+ #[test]
+ fn test_new_alias() {
+ let alias =
+ FirewallAliasReference::new(FirewallAliasScope::Datacenter, "proxmox-123".to_string())
+ .expect("valid alias name");
+ assert_eq!(alias.scope(), FirewallAliasScope::Datacenter);
+ assert_eq!(alias.name(), "proxmox-123");
+
+ FirewallAliasReference::new(FirewallAliasScope::Datacenter, "invalid/name".to_string())
+ .expect_err("invalid alias name");
+ }
+
+ #[test]
+ fn test_parse_alias_name() {
+ for name in ["dc/proxmox_123", "guest/proxmox-123"] {
+ name.parse::<FirewallAliasReference>()
+ .expect("valid alias name");
+ }
+
+ #[cfg(not(feature = "enum-fallback"))]
+ for name in ["proxmox/proxmox_123", "guests/proxmox-123", "dc/", "/name"] {
+ name.parse::<FirewallAliasReference>()
+ .expect_err("invalid alias name");
+ }
+ }
+}
diff --git a/proxmox-firewall-api-types/src/lib.rs b/proxmox-firewall-api-types/src/lib.rs
index abd78c98..8fae5042 100644
--- a/proxmox-firewall-api-types/src/lib.rs
+++ b/proxmox-firewall-api-types/src/lib.rs
@@ -1,3 +1,6 @@
+mod alias;
+pub use alias::{FirewallAliasReference, FirewallAliasScope};
+
mod conntrack;
pub use conntrack::FirewallConntrackHelper;
--
2.47.3
^ permalink raw reply [flat|nested] 26+ messages in thread
* [RFC proxmox 14/22] firewall-api-types: add firewall address types
2026-02-16 10:43 [RFC proxmox 00/22] New crate for firewall api types Dietmar Maurer
` (12 preceding siblings ...)
2026-02-16 10:43 ` [RFC proxmox 13/22] firewall-api-types: add FirewallAliasReference type Dietmar Maurer
@ 2026-02-16 10:43 ` Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 15/22] firewall-api-types: add FirewallRule type Dietmar Maurer
` (8 subsequent siblings)
22 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2026-02-16 10:43 UTC (permalink / raw)
To: pve-devel
This adds new types for representing firewall address matches:
- FirewallAddressMatch: enum for IP lists, ipset references, or alias
references
- FirewallAddressList: validated list of address entries with consistent
address family
- FirewallAddressEntry: enum for CIDR or IP range entries
The implementation includes:
- Proper encapsulation with constructor and accessor methods
- Address family validation in FirewallAddressList::new()
- FromStr implementations for parsing address specifications
- Integration with existing FirewallIpsetReference and
FirewallAliasReference types
Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
proxmox-firewall-api-types/Cargo.toml | 1 +
proxmox-firewall-api-types/src/address.rs | 225 ++++++++++++++++++++++
proxmox-firewall-api-types/src/lib.rs | 3 +
3 files changed, 229 insertions(+)
create mode 100644 proxmox-firewall-api-types/src/address.rs
diff --git a/proxmox-firewall-api-types/Cargo.toml b/proxmox-firewall-api-types/Cargo.toml
index 97b477b8..8b77b522 100644
--- a/proxmox-firewall-api-types/Cargo.toml
+++ b/proxmox-firewall-api-types/Cargo.toml
@@ -22,3 +22,4 @@ serde_plain = { workspace = true }
proxmox-schema = { workspace = true, features = ["api-macro"] }
proxmox-serde = { workspace = true, features = ["perl"] }
proxmox-fixed-string = { workspace = true, optional = true }
+proxmox-network-types = { workspace = true, features = [ "api-types" ] }
diff --git a/proxmox-firewall-api-types/src/address.rs b/proxmox-firewall-api-types/src/address.rs
new file mode 100644
index 00000000..46166352
--- /dev/null
+++ b/proxmox-firewall-api-types/src/address.rs
@@ -0,0 +1,225 @@
+use std::fmt;
+use std::str::FromStr;
+
+use super::{FirewallAliasReference, FirewallIpsetReference};
+
+use anyhow::{bail, Error};
+use proxmox_network_types::ip_address::{Cidr, Family, IpRange};
+use proxmox_schema::{ApiStringFormat, ApiType, Schema, StringSchema};
+
+/// A match for source or destination address.
+#[derive(Clone, Debug, PartialEq)]
+pub enum FirewallAddressMatch {
+ /// IP address list match.
+ Ip(FirewallAddressList),
+ /// IP set match.
+ Ipset(FirewallIpsetReference),
+ /// Alias match.
+ Alias(FirewallAliasReference),
+}
+
+impl ApiType for FirewallAddressMatch {
+ const API_SCHEMA: Schema = StringSchema::new(
+ r#"Restrict source or destination packet address.
+ This can refer to a single IP address,
+ an IP set ('+ipsetname') or an IP alias definition. You can also specify
+ an address range like '20.34.101.207-201.3.9.99', or a list of IP
+ addresses and networks (entries are separated by comma). Please do not
+ mix IPv4 and IPv6 addresses inside such lists."#,
+ )
+ .format(&ApiStringFormat::VerifyFn(verify_firewall_address_match))
+ .max_length(512)
+ .schema();
+}
+
+fn verify_firewall_address_match(s: &str) -> Result<(), Error> {
+ FirewallAddressMatch::from_str(s).map(|_| ())
+}
+
+serde_plain::derive_deserialize_from_fromstr!(FirewallAddressMatch, "valid firewall address match");
+serde_plain::derive_serialize_from_display!(FirewallAddressMatch);
+
+/// A firewall address entry (CIDR or Range).
+#[derive(Clone, Debug, PartialEq)]
+pub enum FirewallAddressEntry {
+ /// CIDR notation.
+ Cidr(Cidr),
+ /// IP range.
+ Range(IpRange),
+}
+
+impl FirewallAddressEntry {
+ /// Get the address family of the entry.
+ pub fn family(&self) -> Family {
+ match self {
+ Self::Cidr(cidr) => cidr.family(),
+ Self::Range(range) => range.family(),
+ }
+ }
+}
+
+impl fmt::Display for FirewallAddressEntry {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ Self::Cidr(ip) => ip.fmt(f),
+ Self::Range(range) => range.fmt(f),
+ }
+ }
+}
+
+impl FromStr for FirewallAddressEntry {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Self, Error> {
+ if let Ok(cidr) = s.parse() {
+ return Ok(FirewallAddressEntry::Cidr(cidr));
+ }
+
+ if let Ok(range) = s.parse() {
+ return Ok(FirewallAddressEntry::Range(range));
+ }
+
+ bail!("Invalid IP entry: {s}");
+ }
+}
+
+/// A list of firewall address entries.
+#[derive(Clone, Debug, PartialEq)]
+pub struct FirewallAddressList {
+ // guaranteed to have the same family
+ entries: Vec<FirewallAddressEntry>,
+ family: Family,
+}
+
+impl FirewallAddressList {
+ /// Creates a new address list from a vector of entries.
+ ///
+ /// Validates that all entries have the same address family.
+ pub fn new(entries: Vec<FirewallAddressEntry>) -> Result<Self, Error> {
+ if entries.is_empty() {
+ bail!("empty address list");
+ }
+
+ let family = entries[0].family();
+
+ for entry in &entries[1..] {
+ if entry.family() != family {
+ bail!("address list entries must have the same address family");
+ }
+ }
+
+ Ok(Self { entries, family })
+ }
+
+ /// Returns the entries of the address list.
+ pub fn entries(&self) -> &[FirewallAddressEntry] {
+ &self.entries
+ }
+
+ /// Returns the address family of the address list.
+ pub fn family(&self) -> Family {
+ self.family
+ }
+}
+
+impl fmt::Display for FirewallAddressMatch {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ Self::Ip(list) => {
+ for (i, entry) in list.entries.iter().enumerate() {
+ if i > 0 {
+ write!(f, ",")?;
+ }
+ entry.fmt(f)?;
+ }
+ Ok(())
+ }
+ Self::Ipset(reference) => reference.fmt(f),
+ Self::Alias(reference) => reference.fmt(f),
+ }
+ }
+}
+
+impl FromStr for FirewallAddressMatch {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let s = s.trim();
+
+ if s.is_empty() {
+ bail!("empty firewall address specification");
+ }
+
+ if s.starts_with('+') {
+ return Ok(FirewallAddressMatch::Ipset(
+ s.parse::<FirewallIpsetReference>()?,
+ ));
+ }
+
+ if let Ok(alias_ref) = s.parse::<FirewallAliasReference>() {
+ return Ok(FirewallAddressMatch::Alias(alias_ref));
+ }
+
+ let mut entries = Vec::new();
+
+ for element in s.split(',') {
+ let entry: FirewallAddressEntry = element.parse()?;
+ entries.push(entry);
+ }
+
+ Ok(Self::Ip(FirewallAddressList::new(entries)?))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_parse_ip_addr_match() {
+ for input in [
+ "10.0.0.0/8",
+ "10.0.0.0/8,192.168.0.0-192.168.255.255,172.16.0.1",
+ "dc/test",
+ "+guest/proxmox",
+ ] {
+ input
+ .parse::<FirewallAddressMatch>()
+ .expect("valid ip match");
+ }
+
+ for input in [
+ "10.0.0.0/",
+ "10.0.0.0/8,192.168.256.0-192.168.255.255,172.16.0.1",
+ "dc/test!invalid",
+ "+guest/",
+ "",
+ ] {
+ input
+ .parse::<FirewallAddressMatch>()
+ .expect_err("invalid ip match");
+ }
+ }
+
+ #[test]
+ fn test_firewall_address_list_mixed_families() {
+ let entries = vec![
+ "10.0.0.1/32".parse().unwrap(),
+ "fe80::1/128".parse().unwrap(),
+ ];
+
+ assert!(FirewallAddressList::new(entries).is_err());
+ }
+
+ #[test]
+ fn test_firewall_address_list_valid() {
+ let entries = vec![
+ "10.0.0.1/32".parse().unwrap(),
+ "192.168.1.1/32".parse().unwrap(),
+ ];
+
+ let list = FirewallAddressList::new(entries).expect("valid list");
+ assert_eq!(list.family(), Family::V4);
+ assert_eq!(list.entries().len(), 2);
+ }
+}
diff --git a/proxmox-firewall-api-types/src/lib.rs b/proxmox-firewall-api-types/src/lib.rs
index 8fae5042..c6f00250 100644
--- a/proxmox-firewall-api-types/src/lib.rs
+++ b/proxmox-firewall-api-types/src/lib.rs
@@ -1,3 +1,6 @@
+mod address;
+pub use address::{FirewallAddressEntry, FirewallAddressList, FirewallAddressMatch};
+
mod alias;
pub use alias::{FirewallAliasReference, FirewallAliasScope};
--
2.47.3
^ permalink raw reply [flat|nested] 26+ messages in thread
* [RFC proxmox 15/22] firewall-api-types: add FirewallRule type
2026-02-16 10:43 [RFC proxmox 00/22] New crate for firewall api types Dietmar Maurer
` (13 preceding siblings ...)
2026-02-16 10:43 ` [RFC proxmox 14/22] firewall-api-types: add firewall address types Dietmar Maurer
@ 2026-02-16 10:43 ` Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 16/22] firewall-api-types: use ConfigDigest from proxmox-config-digest crate Dietmar Maurer
` (7 subsequent siblings)
22 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2026-02-16 10:43 UTC (permalink / raw)
To: pve-devel
Extracted from Perl API and migrated to Rust with type-safe implementations.
Replaced string-based types with strongly-typed Rust counterparts including:
- FirewallAction (Accept, Reject, Drop)
- FirewallLogLevel
- FirewallDirection (In, Out, Group)
- FirewallAddressMatch for source/destination addresses
- FirewallPortList for port specifications
- FirewallIcmpType for ICMP type matching
Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
proxmox-firewall-api-types/src/lib.rs | 3 +
proxmox-firewall-api-types/src/rule.rs | 192 +++++++++++++++++++++++++
2 files changed, 195 insertions(+)
create mode 100644 proxmox-firewall-api-types/src/rule.rs
diff --git a/proxmox-firewall-api-types/src/lib.rs b/proxmox-firewall-api-types/src/lib.rs
index c6f00250..25e260c3 100644
--- a/proxmox-firewall-api-types/src/lib.rs
+++ b/proxmox-firewall-api-types/src/lib.rs
@@ -37,3 +37,6 @@ mod port;
pub use port::{
FirewallPortList, FirewallPortListEntry, FIREWALL_DPORT_API_SCHEMA, FIREWALL_SPORT_API_SCHEMA,
};
+
+mod rule;
+pub use rule::{FirewallRule, FirewallRuleType};
diff --git a/proxmox-firewall-api-types/src/rule.rs b/proxmox-firewall-api-types/src/rule.rs
new file mode 100644
index 00000000..a2298609
--- /dev/null
+++ b/proxmox-firewall-api-types/src/rule.rs
@@ -0,0 +1,192 @@
+use serde::{Deserialize, Serialize};
+
+#[cfg(feature = "enum-fallback")]
+use proxmox_fixed_string::FixedString;
+use proxmox_schema::{api, const_regex, ApiStringFormat};
+
+use crate::{FirewallAddressMatch, FirewallIcmpType, FirewallPortList};
+use crate::{FIREWALL_DPORT_API_SCHEMA, FIREWALL_SPORT_API_SCHEMA};
+use super::FirewallLogLevel;
+
+const_regex! {
+ FIREWALL_RULE_IFACE_RE = r##"^[a-zA-Z][a-zA-Z0-9_]{1,20}([:\.]\d+)?$"##;
+ FIREWALL_SECURITY_GROUP_RE = r##"^[A-Za-z][A-Za-z0-9\-\_]+$"##;
+}
+
+#[api(
+ properties: {
+ action: {
+ format: &ApiStringFormat::Pattern(&FIREWALL_SECURITY_GROUP_RE),
+ max_length: 20,
+ min_length: 2,
+ type: String,
+ },
+ comment: {
+ optional: true,
+ type: String,
+ },
+ dest: {
+ optional: true,
+ },
+ digest: {
+ max_length: 64,
+ optional: true,
+ type: String,
+ },
+ dport: {
+ optional: true,
+ schema: FIREWALL_DPORT_API_SCHEMA,
+ },
+ enable: {
+ minimum: 0,
+ optional: true,
+ type: Integer,
+ },
+ "icmp-type": {
+ optional: true,
+ },
+ iface: {
+ format: &ApiStringFormat::Pattern(&FIREWALL_RULE_IFACE_RE),
+ max_length: 20,
+ min_length: 2,
+ optional: true,
+ type: String,
+ },
+ log: {
+ optional: true,
+ },
+ "macro": {
+ max_length: 128,
+ optional: true,
+ type: String,
+ },
+ pos: {
+ minimum: 0,
+ optional: true,
+ type: Integer,
+ },
+ proto: {
+ max_length: 64, // arbitrary limit, much longer than anything in /etc/protocols
+ optional: true,
+ type: String,
+ },
+ source: {
+ optional: true,
+ },
+ sport: {
+ optional: true,
+ schema: FIREWALL_SPORT_API_SCHEMA,
+ },
+ type: {
+ type: FirewallRuleType,
+ },
+ },
+)]
+/// Firewall Rule.
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct FirewallRule {
+ /// Rule action ('ACCEPT', 'DROP', 'REJECT') or security group name.
+ pub action: String,
+
+ /// Descriptive comment.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub comment: Option<String>,
+
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub dest: Option<FirewallAddressMatch>,
+
+ /// Prevent changes if current configuration file has a different digest.
+ /// This can be used to prevent concurrent modifications.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub digest: Option<String>,
+
+ /// Restrict TCP/UDP destination port.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub dport: Option<FirewallPortList>,
+
+ /// Flag to enable/disable a rule.
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_u64")]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub enable: Option<u64>,
+
+ /// Specify icmp-type. Only valid if proto equals 'icmp' or
+ /// 'icmpv6'/'ipv6-icmp'.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ #[serde(rename = "icmp-type")]
+ pub icmp_type: Option<FirewallIcmpType>,
+
+ /// Network interface name. You have to use network configuration key names
+ /// for VMs and containers ('net\d+'). Host related rules can use arbitrary
+ /// strings.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub iface: Option<String>,
+
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub log: Option<FirewallLogLevel>,
+
+ /// Use predefined standard macro.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ #[serde(rename = "macro")]
+ pub r#macro: Option<String>,
+
+ /// Update rule at position <pos>.
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_u64")]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub pos: Option<u64>,
+
+ /// IP protocol. You can use protocol names ('tcp'/'udp') or simple numbers,
+ /// as defined in '/etc/protocols'.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub proto: Option<String>,
+
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub source: Option<FirewallAddressMatch>,
+
+ /// Restrict TCP/UDP source port.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub sport: Option<FirewallPortList>,
+
+ #[serde(rename = "type")]
+ pub ty: FirewallRuleType,
+}
+
+#[api]
+/// Rule type.
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
+pub enum FirewallRuleType {
+ #[serde(rename = "in")]
+ /// in.
+ In,
+ #[serde(rename = "out")]
+ /// out.
+ Out,
+ #[serde(rename = "forward")]
+ /// forward.
+ Forward,
+ #[serde(rename = "group")]
+ /// group.
+ Group,
+ /// Unknown variants for forward compatibility.
+ #[cfg(feature = "enum-fallback")]
+ #[serde(untagged)]
+ UnknownEnumValue(FixedString),
+}
+
+serde_plain::derive_display_from_serialize!(FirewallRuleType);
+serde_plain::derive_fromstr_from_deserialize!(FirewallRuleType);
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn test_regex_compilation_firewall_rule_iface_re() {
+ use regex::Regex;
+ let _: &Regex = &FIREWALL_RULE_IFACE_RE;
+ }
+ #[test]
+ fn test_regex_compilation_firewall_security_group_re() {
+ use regex::Regex;
+ let _: &Regex = &FIREWALL_SECURITY_GROUP_RE;
+ }
+}
--
2.47.3
^ permalink raw reply [flat|nested] 26+ messages in thread
* [RFC proxmox 16/22] firewall-api-types: use ConfigDigest from proxmox-config-digest crate
2026-02-16 10:43 [RFC proxmox 00/22] New crate for firewall api types Dietmar Maurer
` (14 preceding siblings ...)
2026-02-16 10:43 ` [RFC proxmox 15/22] firewall-api-types: add FirewallRule type Dietmar Maurer
@ 2026-02-16 10:43 ` Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 17/22] firewall-api-types: use COMMENT_SCHEMA from proxmox-schema crate Dietmar Maurer
` (6 subsequent siblings)
22 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2026-02-16 10:43 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
proxmox-firewall-api-types/Cargo.toml | 1 +
proxmox-firewall-api-types/src/rule.rs | 5 ++---
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/proxmox-firewall-api-types/Cargo.toml b/proxmox-firewall-api-types/Cargo.toml
index 8b77b522..2e894606 100644
--- a/proxmox-firewall-api-types/Cargo.toml
+++ b/proxmox-firewall-api-types/Cargo.toml
@@ -23,3 +23,4 @@ proxmox-schema = { workspace = true, features = ["api-macro"] }
proxmox-serde = { workspace = true, features = ["perl"] }
proxmox-fixed-string = { workspace = true, optional = true }
proxmox-network-types = { workspace = true, features = [ "api-types" ] }
+proxmox-config-digest = { workspace = true }
\ No newline at end of file
diff --git a/proxmox-firewall-api-types/src/rule.rs b/proxmox-firewall-api-types/src/rule.rs
index a2298609..835ac96e 100644
--- a/proxmox-firewall-api-types/src/rule.rs
+++ b/proxmox-firewall-api-types/src/rule.rs
@@ -1,3 +1,4 @@
+use proxmox_config_digest::ConfigDigest;
use serde::{Deserialize, Serialize};
#[cfg(feature = "enum-fallback")]
@@ -29,9 +30,7 @@ const_regex! {
optional: true,
},
digest: {
- max_length: 64,
optional: true,
- type: String,
},
dport: {
optional: true,
@@ -98,7 +97,7 @@ pub struct FirewallRule {
/// Prevent changes if current configuration file has a different digest.
/// This can be used to prevent concurrent modifications.
#[serde(default, skip_serializing_if = "Option::is_none")]
- pub digest: Option<String>,
+ pub digest: Option<ConfigDigest>,
/// Restrict TCP/UDP destination port.
#[serde(default, skip_serializing_if = "Option::is_none")]
--
2.47.3
^ permalink raw reply [flat|nested] 26+ messages in thread
* [RFC proxmox 17/22] firewall-api-types: use COMMENT_SCHEMA from proxmox-schema crate
2026-02-16 10:43 [RFC proxmox 00/22] New crate for firewall api types Dietmar Maurer
` (15 preceding siblings ...)
2026-02-16 10:43 ` [RFC proxmox 16/22] firewall-api-types: use ConfigDigest from proxmox-config-digest crate Dietmar Maurer
@ 2026-02-16 10:43 ` Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 18/22] firewall-api-types: add FirewallRuleUpdater type Dietmar Maurer
` (5 subsequent siblings)
22 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2026-02-16 10:43 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
proxmox-firewall-api-types/src/rule.rs | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/proxmox-firewall-api-types/src/rule.rs b/proxmox-firewall-api-types/src/rule.rs
index 835ac96e..8588ca46 100644
--- a/proxmox-firewall-api-types/src/rule.rs
+++ b/proxmox-firewall-api-types/src/rule.rs
@@ -3,6 +3,8 @@ use serde::{Deserialize, Serialize};
#[cfg(feature = "enum-fallback")]
use proxmox_fixed_string::FixedString;
+
+use proxmox_schema::api_types::COMMENT_SCHEMA;
use proxmox_schema::{api, const_regex, ApiStringFormat};
use crate::{FirewallAddressMatch, FirewallIcmpType, FirewallPortList};
@@ -24,7 +26,7 @@ const_regex! {
},
comment: {
optional: true,
- type: String,
+ schema: COMMENT_SCHEMA,
},
dest: {
optional: true,
--
2.47.3
^ permalink raw reply [flat|nested] 26+ messages in thread
* [RFC proxmox 18/22] firewall-api-types: add FirewallRuleUpdater type
2026-02-16 10:43 [RFC proxmox 00/22] New crate for firewall api types Dietmar Maurer
` (16 preceding siblings ...)
2026-02-16 10:43 ` [RFC proxmox 17/22] firewall-api-types: use COMMENT_SCHEMA from proxmox-schema crate Dietmar Maurer
@ 2026-02-16 10:43 ` Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 19/22] firewall-api-types: refactor FirewallRule and add FirewallRuleListEntry Dietmar Maurer
` (4 subsequent siblings)
22 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2026-02-16 10:43 UTC (permalink / raw)
To: pve-devel
This derives the Updater trait for FirewallRule, which generates a
FirewallRuleUpdater struct for use in update APIs. The updater makes
all fields optional, allowing partial updates.
Added test_updater_type() to verify that all updater fields have the
correct types and can be properly initialized.
Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
proxmox-firewall-api-types/src/address.rs | 6 +++-
proxmox-firewall-api-types/src/icmp_type.rs | 6 +++-
proxmox-firewall-api-types/src/port.rs | 4 +++
proxmox-firewall-api-types/src/rule.rs | 39 +++++++++++++++++++--
4 files changed, 51 insertions(+), 4 deletions(-)
diff --git a/proxmox-firewall-api-types/src/address.rs b/proxmox-firewall-api-types/src/address.rs
index 46166352..196222ba 100644
--- a/proxmox-firewall-api-types/src/address.rs
+++ b/proxmox-firewall-api-types/src/address.rs
@@ -5,7 +5,7 @@ use super::{FirewallAliasReference, FirewallIpsetReference};
use anyhow::{bail, Error};
use proxmox_network_types::ip_address::{Cidr, Family, IpRange};
-use proxmox_schema::{ApiStringFormat, ApiType, Schema, StringSchema};
+use proxmox_schema::{ApiStringFormat, ApiType, Schema, StringSchema, UpdaterType};
/// A match for source or destination address.
#[derive(Clone, Debug, PartialEq)]
@@ -32,6 +32,10 @@ impl ApiType for FirewallAddressMatch {
.schema();
}
+impl UpdaterType for FirewallAddressMatch {
+ type Updater = Option<FirewallAddressMatch>;
+}
+
fn verify_firewall_address_match(s: &str) -> Result<(), Error> {
FirewallAddressMatch::from_str(s).map(|_| ())
}
diff --git a/proxmox-firewall-api-types/src/icmp_type.rs b/proxmox-firewall-api-types/src/icmp_type.rs
index b45c1505..46ddb58a 100644
--- a/proxmox-firewall-api-types/src/icmp_type.rs
+++ b/proxmox-firewall-api-types/src/icmp_type.rs
@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
#[cfg(feature = "enum-fallback")]
use proxmox_fixed_string::FixedString;
-use proxmox_schema::{ApiStringFormat, ApiType, Schema, StringSchema};
+use proxmox_schema::{ApiStringFormat, ApiType, Schema, StringSchema, UpdaterType};
#[derive(Debug, Copy, Clone, PartialEq)]
/// ICMP type, either named or numeric.
@@ -26,6 +26,10 @@ impl ApiType for FirewallIcmpType {
.schema();
}
+impl UpdaterType for FirewallIcmpType {
+ type Updater = Option<FirewallIcmpType>;
+}
+
fn verify_firewall_icmp_type(value: &str) -> Result<(), Error> {
value.parse::<FirewallIcmpType>().map(|_| ())
}
diff --git a/proxmox-firewall-api-types/src/port.rs b/proxmox-firewall-api-types/src/port.rs
index 46989ba4..572cb439 100644
--- a/proxmox-firewall-api-types/src/port.rs
+++ b/proxmox-firewall-api-types/src/port.rs
@@ -84,6 +84,10 @@ pub const FIREWALL_DPORT_API_SCHEMA: Schema = StringSchema::new(concatcp!(
.format(&ApiStringFormat::VerifyFn(verify_firewall_port_list))
.schema();
+impl UpdaterType for FirewallPortList {
+ type Updater = Option<FirewallPortList>;
+}
+
serde_plain::derive_deserialize_from_fromstr!(FirewallPortList, "valid port list");
serde_plain::derive_serialize_from_display!(FirewallPortList);
diff --git a/proxmox-firewall-api-types/src/rule.rs b/proxmox-firewall-api-types/src/rule.rs
index 8588ca46..48869b25 100644
--- a/proxmox-firewall-api-types/src/rule.rs
+++ b/proxmox-firewall-api-types/src/rule.rs
@@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
use proxmox_fixed_string::FixedString;
use proxmox_schema::api_types::COMMENT_SCHEMA;
-use proxmox_schema::{api, const_regex, ApiStringFormat};
+use proxmox_schema::{api, const_regex, ApiStringFormat, Updater};
use crate::{FirewallAddressMatch, FirewallIcmpType, FirewallPortList};
use crate::{FIREWALL_DPORT_API_SCHEMA, FIREWALL_SPORT_API_SCHEMA};
@@ -84,9 +84,10 @@ const_regex! {
},
)]
/// Firewall Rule.
-#[derive(Debug, serde::Deserialize, serde::Serialize)]
+#[derive(Debug, serde::Deserialize, serde::Serialize, Updater)]
pub struct FirewallRule {
/// Rule action ('ACCEPT', 'DROP', 'REJECT') or security group name.
+ #[updater(serde(default, skip_serializing_if = "Option::is_none"))]
pub action: String,
/// Descriptive comment.
@@ -99,6 +100,7 @@ pub struct FirewallRule {
/// Prevent changes if current configuration file has a different digest.
/// This can be used to prevent concurrent modifications.
#[serde(default, skip_serializing_if = "Option::is_none")]
+ #[updater(type = "Option<ConfigDigest>")]
pub digest: Option<ConfigDigest>,
/// Restrict TCP/UDP destination port.
@@ -148,6 +150,7 @@ pub struct FirewallRule {
pub sport: Option<FirewallPortList>,
#[serde(rename = "type")]
+ #[updater(serde(default, skip_serializing_if = "Option::is_none"))]
pub ty: FirewallRuleType,
}
@@ -179,6 +182,7 @@ serde_plain::derive_fromstr_from_deserialize!(FirewallRuleType);
#[cfg(test)]
mod test {
use super::*;
+ use crate::FirewallIcmpTypeName;
#[test]
fn test_regex_compilation_firewall_rule_iface_re() {
@@ -190,4 +194,35 @@ mod test {
use regex::Regex;
let _: &Regex = &FIREWALL_SECURITY_GROUP_RE;
}
+
+ #[test]
+ fn test_updater_type() {
+ // Test that we have all properties with correct types
+ let mut updater = FirewallRuleUpdater::default();
+
+ // Basic String fields - these are Option<String> in updater
+ updater.action = Some("ACCEPT".to_string());
+ updater.comment = Some("test comment".to_string());
+ updater.iface = Some("net0".to_string());
+ updater.r#macro = Some("test-macro".to_string());
+ updater.proto = Some("tcp".to_string());
+
+ // Numeric fields - these are Option<u64> in updater
+ updater.enable = Some(1);
+ updater.pos = Some(0);
+
+ // Enum fields - these are Option<EnumType> in updater
+ updater.ty = Some(FirewallRuleType::In);
+ updater.log = Some(FirewallLogLevel::Info);
+
+ // Fields with custom updater types #[updater(type = "Option<T>")]
+ // These are just Option<T> in the updater (not Option<Option<T>>)
+ // The #[updater(type)] attribute explicitly sets the updater field type
+ updater.dest = Some("192.168.1.1".parse().unwrap());
+ updater.source = Some("10.0.0.1".parse().unwrap());
+ updater.dport = Some("80".parse().unwrap());
+ updater.sport = Some("1024:65535".parse().unwrap());
+ updater.icmp_type = Some(FirewallIcmpType::Named(FirewallIcmpTypeName::EchoRequest));
+ updater.digest = Some(ConfigDigest::from([0u8; 32]));
+ }
}
--
2.47.3
^ permalink raw reply [flat|nested] 26+ messages in thread
* [RFC proxmox 19/22] firewall-api-types: refactor FirewallRule and add FirewallRuleListEntry
2026-02-16 10:43 [RFC proxmox 00/22] New crate for firewall api types Dietmar Maurer
` (17 preceding siblings ...)
2026-02-16 10:43 ` [RFC proxmox 18/22] firewall-api-types: add FirewallRuleUpdater type Dietmar Maurer
@ 2026-02-16 10:43 ` Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 20/22] firewall-api-types: add DeletableFirewallRuleProperty enum Dietmar Maurer
` (3 subsequent siblings)
22 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2026-02-16 10:43 UTC (permalink / raw)
To: pve-devel
Move 'pos' and 'digest' fields out of FirewallRule, since they are
not part of the rule definition itself but rather metadata associated
with a rule's position in a list.
Introduce FirewallRuleListEntry which wraps a FirewallRule together
with its position and digest, for use in list/get API responses.
Add a dedicated FIREWALL_RULE_POS_SCHEMA for the rule position field,
because we need it several times now.
Derive Clone and PartialEq on FirewallRule and FirewallRuleListEntry,
because this is a requirement to use it in the yew GUI. Drop Eq from
FirewallRuleType since PartialEq is sufficient.
Add dev-dependencies (proxmox-router, serde_json) and a
test_fake_server_client_api test module with mock create_rule,
update_rule, and get_rule API handlers to verify that the types
work correctly with the #[api] macro.
Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
proxmox-firewall-api-types/Cargo.toml | 4 +
proxmox-firewall-api-types/src/rule.rs | 130 ++++++++++++++++++++-----
2 files changed, 110 insertions(+), 24 deletions(-)
diff --git a/proxmox-firewall-api-types/Cargo.toml b/proxmox-firewall-api-types/Cargo.toml
index 2e894606..6685d75c 100644
--- a/proxmox-firewall-api-types/Cargo.toml
+++ b/proxmox-firewall-api-types/Cargo.toml
@@ -12,6 +12,10 @@ exclude.workspace = true
[features]
enum-fallback = ["dep:proxmox-fixed-string"]
+[dev-dependencies]
+proxmox-router = { workspace = true }
+serde_json = { workspace = true }
+
[dependencies]
anyhow.workspace = true
regex.workspace = true
diff --git a/proxmox-firewall-api-types/src/rule.rs b/proxmox-firewall-api-types/src/rule.rs
index 48869b25..d067c209 100644
--- a/proxmox-firewall-api-types/src/rule.rs
+++ b/proxmox-firewall-api-types/src/rule.rs
@@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
use proxmox_fixed_string::FixedString;
use proxmox_schema::api_types::COMMENT_SCHEMA;
-use proxmox_schema::{api, const_regex, ApiStringFormat, Updater};
+use proxmox_schema::{api, const_regex, ApiStringFormat, IntegerSchema, Schema, Updater};
use crate::{FirewallAddressMatch, FirewallIcmpType, FirewallPortList};
use crate::{FIREWALL_DPORT_API_SCHEMA, FIREWALL_SPORT_API_SCHEMA};
@@ -16,6 +16,10 @@ const_regex! {
FIREWALL_SECURITY_GROUP_RE = r##"^[A-Za-z][A-Za-z0-9\-\_]+$"##;
}
+const FIREWALL_RULE_POS_SCHEMA: Schema = IntegerSchema::new("Rule position <pos>.")
+ .minimum(0)
+ .schema();
+
#[api(
properties: {
action: {
@@ -31,9 +35,6 @@ const_regex! {
dest: {
optional: true,
},
- digest: {
- optional: true,
- },
dport: {
optional: true,
schema: FIREWALL_DPORT_API_SCHEMA,
@@ -61,11 +62,6 @@ const_regex! {
optional: true,
type: String,
},
- pos: {
- minimum: 0,
- optional: true,
- type: Integer,
- },
proto: {
max_length: 64, // arbitrary limit, much longer than anything in /etc/protocols
optional: true,
@@ -84,7 +80,7 @@ const_regex! {
},
)]
/// Firewall Rule.
-#[derive(Debug, serde::Deserialize, serde::Serialize, Updater)]
+#[derive(Debug, serde::Deserialize, serde::Serialize, Updater, Clone, PartialEq)]
pub struct FirewallRule {
/// Rule action ('ACCEPT', 'DROP', 'REJECT') or security group name.
#[updater(serde(default, skip_serializing_if = "Option::is_none"))]
@@ -97,12 +93,6 @@ pub struct FirewallRule {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dest: Option<FirewallAddressMatch>,
- /// Prevent changes if current configuration file has a different digest.
- /// This can be used to prevent concurrent modifications.
- #[serde(default, skip_serializing_if = "Option::is_none")]
- #[updater(type = "Option<ConfigDigest>")]
- pub digest: Option<ConfigDigest>,
-
/// Restrict TCP/UDP destination port.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dport: Option<FirewallPortList>,
@@ -132,11 +122,6 @@ pub struct FirewallRule {
#[serde(rename = "macro")]
pub r#macro: Option<String>,
- /// Update rule at position <pos>.
- #[serde(deserialize_with = "proxmox_serde::perl::deserialize_u64")]
- #[serde(default, skip_serializing_if = "Option::is_none")]
- pub pos: Option<u64>,
-
/// IP protocol. You can use protocol names ('tcp'/'udp') or simple numbers,
/// as defined in '/etc/protocols'.
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -154,9 +139,30 @@ pub struct FirewallRule {
pub ty: FirewallRuleType,
}
+#[api(
+ properties: {
+ pos: {
+ optional: true,
+ schema: FIREWALL_RULE_POS_SCHEMA,
+ },
+ rule: {
+ type: FirewallRule,
+ flatten: true,
+ }
+ },
+)]
+#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
+/// Firewall rule list entry. Includes position, digest and the rule itself.
+pub struct FirewallRuleListEntry {
+ pub pos: u64,
+ pub digest: ConfigDigest,
+ #[serde(flatten)]
+ pub rule: FirewallRule,
+}
+
#[api]
/// Rule type.
-#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
+#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
pub enum FirewallRuleType {
#[serde(rename = "in")]
/// in.
@@ -209,7 +215,6 @@ mod test {
// Numeric fields - these are Option<u64> in updater
updater.enable = Some(1);
- updater.pos = Some(0);
// Enum fields - these are Option<EnumType> in updater
updater.ty = Some(FirewallRuleType::In);
@@ -223,6 +228,83 @@ mod test {
updater.dport = Some("80".parse().unwrap());
updater.sport = Some("1024:65535".parse().unwrap());
updater.icmp_type = Some(FirewallIcmpType::Named(FirewallIcmpTypeName::EchoRequest));
- updater.digest = Some(ConfigDigest::from([0u8; 32]));
+ }
+}
+
+#[cfg(test)]
+mod test_fake_server_client_api {
+ use super::*;
+ use anyhow::Error;
+ use proxmox_schema::api;
+
+ #[api(
+ input: {
+ properties: {
+ rule: {
+ type: FirewallRule,
+ flatten: true
+ },
+ pos: {
+ optional: true,
+ schema: FIREWALL_RULE_POS_SCHEMA,
+ },
+ digest: {
+ optional: true,
+ },
+ },
+ },
+ )]
+ /// create rule
+ pub fn create_rule(
+ rule: FirewallRule,
+ pos: Option<u64>,
+ digest: Option<ConfigDigest>,
+ ) -> Result<(), Error> {
+ unimplemented!();
+ }
+
+ #[api(
+ input: {
+ properties: {
+ rule: {
+ type: FirewallRule,
+ flatten: true
+ },
+ pos: {
+ schema: FIREWALL_RULE_POS_SCHEMA,
+ },
+ digest: {
+ optional: true,
+ },
+ },
+ },
+ )]
+ /// update rule
+ pub fn update_rule(
+ pos: u64,
+ rule: FirewallRuleUpdater,
+ digest: Option<ConfigDigest>,
+ ) -> Result<(), Error> {
+ unimplemented!();
+ }
+
+ #[api(
+ input: {
+ properties: {
+ pos: {
+ schema: FIREWALL_RULE_POS_SCHEMA,
+ },
+ digest: {
+ optional: true,
+ },
+ },
+ },
+ )]
+ /// update rule
+ pub fn get_rule(
+ pos: u64,
+ digest: Option<ConfigDigest>,
+ ) -> Result<FirewallRuleListEntry, Error> {
+ unimplemented!();
}
}
--
2.47.3
^ permalink raw reply [flat|nested] 26+ messages in thread
* [RFC proxmox 20/22] firewall-api-types: add DeletableFirewallRuleProperty enum
2026-02-16 10:43 [RFC proxmox 00/22] New crate for firewall api types Dietmar Maurer
` (18 preceding siblings ...)
2026-02-16 10:43 ` [RFC proxmox 19/22] firewall-api-types: refactor FirewallRule and add FirewallRuleListEntry Dietmar Maurer
@ 2026-02-16 10:43 ` Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 21/22] firewall-api-types: add FirewallAliasEntry API type Dietmar Maurer
` (2 subsequent siblings)
22 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2026-02-16 10:43 UTC (permalink / raw)
To: pve-devel
Add a DeletableFirewallRuleProperty enum listing the optional
FirewallRule properties that can be explicitly deleted via the
update_rule API. Use kebab-case serialization to match the existing
API conventions.
Add the 'delete' parameter to the update_rule mock API handler
to verify that the new type integrates correctly with the #[api]
macro.
Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
proxmox-firewall-api-types/src/lib.rs | 4 ++-
proxmox-firewall-api-types/src/rule.rs | 43 +++++++++++++++++++++++++-
2 files changed, 45 insertions(+), 2 deletions(-)
diff --git a/proxmox-firewall-api-types/src/lib.rs b/proxmox-firewall-api-types/src/lib.rs
index 25e260c3..0eebd727 100644
--- a/proxmox-firewall-api-types/src/lib.rs
+++ b/proxmox-firewall-api-types/src/lib.rs
@@ -39,4 +39,6 @@ pub use port::{
};
mod rule;
-pub use rule::{FirewallRule, FirewallRuleType};
+pub use rule::{
+ DeletableFirewallRuleProperty, FirewallRule, FirewallRuleListEntry, FirewallRuleType,
+};
diff --git a/proxmox-firewall-api-types/src/rule.rs b/proxmox-firewall-api-types/src/rule.rs
index d067c209..f22de9e9 100644
--- a/proxmox-firewall-api-types/src/rule.rs
+++ b/proxmox-firewall-api-types/src/rule.rs
@@ -160,6 +160,37 @@ pub struct FirewallRuleListEntry {
pub rule: FirewallRule,
}
+#[api()]
+#[derive(Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+/// Deletable property name
+pub enum DeletableFirewallRuleProperty {
+ /// Delete the comment property.
+ Comment,
+ /// Delete the destination address property.
+ Dest,
+ /// Delete the destination port property.
+ DestPort,
+ /// Delete the enable property.
+ Enable,
+ /// Delete the group property.
+ Group,
+ /// Delete the ICMP type property.
+ IcmpType,
+ /// Delete the interface property.
+ Iface,
+ /// Delete the log property.
+ Log,
+ /// Delete the macro property.
+ Macro,
+ /// Delete the protocol property.
+ Proto,
+ /// Delete the source address property.
+ Source,
+ /// Delete the source port property.
+ Sport,
+}
+
#[api]
/// Rule type.
#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
@@ -232,6 +263,7 @@ mod test {
}
#[cfg(test)]
+#[allow(unused)]
mod test_fake_server_client_api {
use super::*;
use anyhow::Error;
@@ -273,9 +305,17 @@ mod test_fake_server_client_api {
pos: {
schema: FIREWALL_RULE_POS_SCHEMA,
},
- digest: {
+ delete: {
+ description: "List of properties to delete.",
+ type: Array,
optional: true,
+ items: {
+ type: DeletableFirewallRuleProperty,
+ }
},
+ digest: {
+ optional: true,
+ },
},
},
)]
@@ -283,6 +323,7 @@ mod test_fake_server_client_api {
pub fn update_rule(
pos: u64,
rule: FirewallRuleUpdater,
+ delete: Option<Vec<DeletableFirewallRuleProperty>>,
digest: Option<ConfigDigest>,
) -> Result<(), Error> {
unimplemented!();
--
2.47.3
^ permalink raw reply [flat|nested] 26+ messages in thread
* [RFC proxmox 21/22] firewall-api-types: add FirewallAliasEntry API type
2026-02-16 10:43 [RFC proxmox 00/22] New crate for firewall api types Dietmar Maurer
` (19 preceding siblings ...)
2026-02-16 10:43 ` [RFC proxmox 20/22] firewall-api-types: add DeletableFirewallRuleProperty enum Dietmar Maurer
@ 2026-02-16 10:43 ` Dietmar Maurer
2026-02-16 10:44 ` [RFC proxmox 22/22] firewall-api-types: add FirewallIpsetListEntry and FirewallIpsetEntry api types Dietmar Maurer
2026-02-17 6:17 ` [RFC proxmox 00/22] New crate for firewall " Hannes Laimer
22 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2026-02-16 10:43 UTC (permalink / raw)
To: pve-devel
Add a new FirewallAliasEntry struct that represents a firewall alias
definition (items from GET /cluster/firewall/aliases).
Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
proxmox-firewall-api-types/src/alias.rs | 39 +++++++++++++++++++++++++
proxmox-firewall-api-types/src/lib.rs | 2 +-
2 files changed, 40 insertions(+), 1 deletion(-)
diff --git a/proxmox-firewall-api-types/src/alias.rs b/proxmox-firewall-api-types/src/alias.rs
index 7722148c..9f87b43f 100644
--- a/proxmox-firewall-api-types/src/alias.rs
+++ b/proxmox-firewall-api-types/src/alias.rs
@@ -2,6 +2,11 @@ use std::fmt;
use std::str::FromStr;
use anyhow::{bail, Error};
+use serde::{Deserialize, Serialize};
+
+use proxmox_config_digest::ConfigDigest;
+use proxmox_network_types::ip_address::Cidr;
+use proxmox_schema::{api, api_types::COMMENT_SCHEMA};
#[cfg(feature = "enum-fallback")]
use proxmox_fixed_string::FixedString;
@@ -97,6 +102,40 @@ impl FromStr for FirewallAliasReference {
}
}
+#[api(
+ properties: {
+ name: {
+ type: String,
+ format: &proxmox_schema::ApiStringFormat::VerifyFn(verify_alias_name),
+ },
+ cidr: {
+ type: String,
+ },
+ comment: {
+ optional: true,
+ schema: COMMENT_SCHEMA,
+ },
+ },
+)]
+/// Firewall alias definition entry, assigns a CIDR to a name.
+///
+/// The name can be used in firewall rules to refer to the CIDR.
+#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
+pub struct FirewallAliasEntry {
+ /// The name of the alias entry.
+ pub name: String,
+
+ /// Network/IP specification in CIDR format.
+ pub cidr: Cidr,
+
+ /// Digest to detect concurrent modifications.
+ pub digest: ConfigDigest,
+
+ /// Descriptive comment.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub comment: Option<String>,
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/proxmox-firewall-api-types/src/lib.rs b/proxmox-firewall-api-types/src/lib.rs
index 0eebd727..044fc761 100644
--- a/proxmox-firewall-api-types/src/lib.rs
+++ b/proxmox-firewall-api-types/src/lib.rs
@@ -2,7 +2,7 @@ mod address;
pub use address::{FirewallAddressEntry, FirewallAddressList, FirewallAddressMatch};
mod alias;
-pub use alias::{FirewallAliasReference, FirewallAliasScope};
+pub use alias::{FirewallAliasEntry, FirewallAliasReference, FirewallAliasScope};
mod conntrack;
pub use conntrack::FirewallConntrackHelper;
--
2.47.3
^ permalink raw reply [flat|nested] 26+ messages in thread
* [RFC proxmox 22/22] firewall-api-types: add FirewallIpsetListEntry and FirewallIpsetEntry api types
2026-02-16 10:43 [RFC proxmox 00/22] New crate for firewall api types Dietmar Maurer
` (20 preceding siblings ...)
2026-02-16 10:43 ` [RFC proxmox 21/22] firewall-api-types: add FirewallAliasEntry API type Dietmar Maurer
@ 2026-02-16 10:44 ` Dietmar Maurer
2026-02-17 6:17 ` [RFC proxmox 00/22] New crate for firewall " Hannes Laimer
22 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2026-02-16 10:44 UTC (permalink / raw)
To: pve-devel
FirewallIpsetListEntry represents an ipset in a listing (GET /cluster/firewall/ipset).
FirewallIpsetEntry represents a single entry within an ipset (GET /cluster/firewall/ipset/{name}).
Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
proxmox-firewall-api-types/src/ipset.rs | 63 +++++++++++++++++++++++++
proxmox-firewall-api-types/src/lib.rs | 4 +-
2 files changed, 66 insertions(+), 1 deletion(-)
diff --git a/proxmox-firewall-api-types/src/ipset.rs b/proxmox-firewall-api-types/src/ipset.rs
index 02659394..5b870873 100644
--- a/proxmox-firewall-api-types/src/ipset.rs
+++ b/proxmox-firewall-api-types/src/ipset.rs
@@ -2,6 +2,11 @@ use std::fmt;
use std::str::FromStr;
use anyhow::{bail, Error};
+use serde::{Deserialize, Serialize};
+
+use proxmox_config_digest::ConfigDigest;
+use proxmox_network_types::ip_address::Cidr;
+use proxmox_schema::{api, api_types::COMMENT_SCHEMA};
#[cfg(feature = "enum-fallback")]
use proxmox_fixed_string::FixedString;
@@ -107,6 +112,64 @@ impl FromStr for FirewallIpsetReference {
}
}
+#[api(
+ properties: {
+ name: {
+ type: String,
+ format: &proxmox_schema::ApiStringFormat::VerifyFn(verify_ipset_name),
+ },
+ comment: {
+ optional: true,
+ schema: COMMENT_SCHEMA,
+ },
+ },
+)]
+/// Firewall ipset list entry.
+#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
+pub struct FirewallIpsetListEntry {
+ /// The name of the ipset entry.
+ pub name: String,
+
+ /// Digest to detect concurrent modifications.
+ pub digest: ConfigDigest,
+
+ /// Descriptive comment.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub comment: Option<String>,
+}
+
+#[api(
+ properties: {
+ cidr: {
+ type: String,
+ },
+ comment: {
+ optional: true,
+ schema: COMMENT_SCHEMA,
+ },
+ nomatch: {
+ optional: true,
+ },
+ },
+)]
+/// Firewall ipset content entry.
+#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
+pub struct FirewallIpsetEntry {
+ /// Network/IP specification in CIDR format.
+ pub cidr: Cidr,
+
+ /// Digest to detect concurrent modifications.
+ pub digest: ConfigDigest,
+
+ /// Descriptive comment.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub comment: Option<String>,
+
+ /// If set to true, the ipset will be used as a "nomatch" ipset.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub nomatch: Option<bool>,
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/proxmox-firewall-api-types/src/lib.rs b/proxmox-firewall-api-types/src/lib.rs
index 044fc761..9422def7 100644
--- a/proxmox-firewall-api-types/src/lib.rs
+++ b/proxmox-firewall-api-types/src/lib.rs
@@ -11,7 +11,9 @@ mod icmp_type;
pub use icmp_type::{FirewallIcmpType, FirewallIcmpTypeName};
mod ipset;
-pub use ipset::{FirewallIpsetReference, FirewallIpsetScope};
+pub use ipset::{
+ FirewallIpsetEntry, FirewallIpsetListEntry, FirewallIpsetReference, FirewallIpsetScope,
+};
mod log;
pub use log::{
--
2.47.3
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [RFC proxmox 00/22] New crate for firewall api types
2026-02-16 10:43 [RFC proxmox 00/22] New crate for firewall api types Dietmar Maurer
` (21 preceding siblings ...)
2026-02-16 10:44 ` [RFC proxmox 22/22] firewall-api-types: add FirewallIpsetListEntry and FirewallIpsetEntry api types Dietmar Maurer
@ 2026-02-17 6:17 ` Hannes Laimer
2026-02-17 6:39 ` Dietmar Maurer
22 siblings, 1 reply; 26+ messages in thread
From: Hannes Laimer @ 2026-02-17 6:17 UTC (permalink / raw)
To: Dietmar Maurer, pve-devel
On 2026-02-16 11:45, Dietmar Maurer wrote:
> The current PVE firewall implementation is written in Perl, and Rust type
> definitions can be auto-generated from its API schemas. However, many of the
> more complex types are represented as opaque strings, which limits type safety.
>
> Verifiers for complex types like ports and address matches cannot be generated
> automatically, so we need to implement them manually anyway.
>
> To address this, the crate provides hand-crafted Rust types that parse and validate these
> string-encoded values into proper enums and structs, while remaining fully
> compatible with the existing API wire format. The initial type definitions were
> seeded from the auto-generated `pve-api-types` crate and then refined by hand.
>
> Types from proxmox-ve-rs/proxmox-ve-config/src/firewall/ are not really designed
> to be used directly, as they are not fully compatible with the API wire format. they
> also depends on system crates (nix, proxmox-sys, etc.) which we want to avoid for this crate.
> I tried to reuse some of those types, but in many cases it was easier to
> use types generated from the perl API schemas as a starting point and then modify them
> as needed.
>
Generally I like the idea of having actual types for a lot of the
just-string things we do in the PVE API.
What we still need though is for the generator in `pve-api-types` to
support external types so generated functions can also reference types
that were not generated. I don't think that should be too complicated,
just something to keep in mind.
I was wondering if we actually need/want a separate crate. This
could be part of `pve-api-types`, alongside the verifiers we'd also have
something like `external types` that the generator can use if specified
for specific endpoints. The rational here would be, that for endpoint
where we want more than just verifiers we could define the types
directly. This would also make it somewhat straightforward to introduce
concrete typing in other, unrelated, places where that would make sense.
On the other hand, for re-usability without the general pve stuff having
a crate might be handy.
Either way, thanks for taking a look at this!
> Dependencies are minimal, so that we can use this crate for wasm targets (GUI).
>
>
> This series depends on the CommaSeparatedList patch send recently.
>
>
> Dietmar Maurer (22):
> firewall-api-types: add new crate for firewall api types
> firewall-api-types: add README.md
> firewall-api-types: add firewall policy types
> firewall-api-types: add logging types
> firewall-api-types: add FirewallClusterOptions
> firewall-api-types: add FirewallGuestOptions
> firewall-api-types: add FirewallConntrackHelper enum
> firewall-api-types: add FirewallNodeOptions struct
> firewall-api-types: add FirewallRef type
> firewall-api-types: add FirewallPortList types
> firewall-api-types: add FirewallIcmpType
> firewall-api-types: add FirewallIpsetReference type
> firewall-api-types: add FirewallAliasReference type
> firewall-api-types: add firewall address types
> firewall-api-types: add FirewallRule type
> firewall-api-types: use ConfigDigest from proxmox-config-digest crate
> firewall-api-types: use COMMENT_SCHEMA from proxmox-schema crate
> firewall-api-types: add FirewallRuleUpdater type
> firewall-api-types: refactor FirewallRule and add
> FirewallRuleListEntry
> firewall-api-types: add DeletableFirewallRuleProperty enum
> firewall-api-types: add FirewallAliasEntry API type
> firewall-api-types: add FirewallIpsetListEntry and FirewallIpsetEntry
> api types
>
> Cargo.toml | 1 +
> proxmox-firewall-api-types/Cargo.toml | 30 +
> proxmox-firewall-api-types/README.md | 54 ++
> proxmox-firewall-api-types/debian/changelog | 5 +
> proxmox-firewall-api-types/debian/control | 52 ++
> proxmox-firewall-api-types/debian/copyright | 18 +
> .../debian/debcargo.toml | 7 +
> proxmox-firewall-api-types/src/address.rs | 229 +++++++
> proxmox-firewall-api-types/src/alias.rs | 181 ++++++
> .../src/cluster_options.rs | 61 ++
> proxmox-firewall-api-types/src/conntrack.rs | 52 ++
> .../src/firewall_ref.rs | 62 ++
> .../src/guest_options.rs | 97 +++
> proxmox-firewall-api-types/src/icmp_type.rs | 559 ++++++++++++++++++
> proxmox-firewall-api-types/src/ipset.rs | 254 ++++++++
> proxmox-firewall-api-types/src/lib.rs | 46 ++
> proxmox-firewall-api-types/src/log.rs | 312 ++++++++++
> .../src/node_options.rs | 240 ++++++++
> proxmox-firewall-api-types/src/policy.rs | 151 +++++
> proxmox-firewall-api-types/src/port.rs | 177 ++++++
> proxmox-firewall-api-types/src/rule.rs | 351 +++++++++++
> 21 files changed, 2939 insertions(+)
> create mode 100644 proxmox-firewall-api-types/Cargo.toml
> create mode 100644 proxmox-firewall-api-types/README.md
> create mode 100644 proxmox-firewall-api-types/debian/changelog
> create mode 100644 proxmox-firewall-api-types/debian/control
> create mode 100644 proxmox-firewall-api-types/debian/copyright
> create mode 100644 proxmox-firewall-api-types/debian/debcargo.toml
> create mode 100644 proxmox-firewall-api-types/src/address.rs
> create mode 100644 proxmox-firewall-api-types/src/alias.rs
> create mode 100644 proxmox-firewall-api-types/src/cluster_options.rs
> create mode 100644 proxmox-firewall-api-types/src/conntrack.rs
> create mode 100644 proxmox-firewall-api-types/src/firewall_ref.rs
> create mode 100644 proxmox-firewall-api-types/src/guest_options.rs
> create mode 100644 proxmox-firewall-api-types/src/icmp_type.rs
> create mode 100644 proxmox-firewall-api-types/src/ipset.rs
> create mode 100644 proxmox-firewall-api-types/src/lib.rs
> create mode 100644 proxmox-firewall-api-types/src/log.rs
> create mode 100644 proxmox-firewall-api-types/src/node_options.rs
> create mode 100644 proxmox-firewall-api-types/src/policy.rs
> create mode 100644 proxmox-firewall-api-types/src/port.rs
> create mode 100644 proxmox-firewall-api-types/src/rule.rs
>
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [RFC proxmox 00/22] New crate for firewall api types
2026-02-17 6:17 ` [RFC proxmox 00/22] New crate for firewall " Hannes Laimer
@ 2026-02-17 6:39 ` Dietmar Maurer
2026-02-17 8:17 ` Hannes Laimer
0 siblings, 1 reply; 26+ messages in thread
From: Dietmar Maurer @ 2026-02-17 6:39 UTC (permalink / raw)
To: Hannes Laimer, pve-devel
> I was wondering if we actually need/want a separate crate. This
> could be part of `pve-api-types`, alongside the verifiers we'd also have
> something like `external types` that the generator can use if specified
> for specific endpoints. The rational here would be, that for endpoint
> where we want more than just verifiers we could define the types
> directly. This would also make it somewhat straightforward to introduce
> concrete typing in other, unrelated, places where that would make sense.
>
> On the other hand, for re-usability without the general pve stuff having
> a crate might be handy.
If you start implementing the firewall in Rust, you probably won't want
to import
all types from pve-api-types — that's unnecessary...
- Dietmar
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [RFC proxmox 00/22] New crate for firewall api types
2026-02-17 6:39 ` Dietmar Maurer
@ 2026-02-17 8:17 ` Hannes Laimer
0 siblings, 0 replies; 26+ messages in thread
From: Hannes Laimer @ 2026-02-17 8:17 UTC (permalink / raw)
To: Dietmar Maurer, pve-devel
On 2026-02-17 07:39, Dietmar Maurer wrote:
>> I was wondering if we actually need/want a separate crate. This
>> could be part of `pve-api-types`, alongside the verifiers we'd also have
>> something like `external types` that the generator can use if specified
>> for specific endpoints. The rational here would be, that for endpoint
>> where we want more than just verifiers we could define the types
>> directly. This would also make it somewhat straightforward to introduce
>> concrete typing in other, unrelated, places where that would make sense.
>>
>> On the other hand, for re-usability without the general pve stuff having
>> a crate might be handy.
>
> If you start implementing the firewall in Rust, you probably won't want
> to import
> all types from pve-api-types — that's unnecessary...
>
yes. We actually do something very similar with `proxmox-apt-api-types`,
but also there it would be cool if we could tell the generator to use
the existing types in `proxmox-apt-api-types` instead of generating[1]
basically the same as we have in the crate[2]
[1]
https://git.proxmox.com/?p=proxmox.git;a=blob;f=pve-api-types/src/generated/types.rs;h=d0d39b5936585adb68d58d9d5eabe68c41e6b353;hb=refs/heads/master#l39
[2]
https://git.proxmox.com/?p=proxmox.git;a=blob;f=proxmox-apt-api-types/src/lib.rs;h=d3f5b3ebf5cba36461df16ba6da7e88b69b3c148;hb=refs/heads/master#l315
> - Dietmar
>
^ permalink raw reply [flat|nested] 26+ messages in thread
end of thread, other threads:[~2026-02-17 8:16 UTC | newest]
Thread overview: 26+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-02-16 10:43 [RFC proxmox 00/22] New crate for firewall api types Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 01/22] firewall-api-types: add new " Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 02/22] firewall-api-types: add README.md Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 03/22] firewall-api-types: add firewall policy types Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 04/22] firewall-api-types: add logging types Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 05/22] firewall-api-types: add FirewallClusterOptions Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 06/22] firewall-api-types: add FirewallGuestOptions Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 07/22] firewall-api-types: add FirewallConntrackHelper enum Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 08/22] firewall-api-types: add FirewallNodeOptions struct Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 09/22] firewall-api-types: add FirewallRef type Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 10/22] firewall-api-types: add FirewallPortList types Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 11/22] firewall-api-types: add FirewallIcmpType Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 12/22] firewall-api-types: add FirewallIpsetReference type Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 13/22] firewall-api-types: add FirewallAliasReference type Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 14/22] firewall-api-types: add firewall address types Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 15/22] firewall-api-types: add FirewallRule type Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 16/22] firewall-api-types: use ConfigDigest from proxmox-config-digest crate Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 17/22] firewall-api-types: use COMMENT_SCHEMA from proxmox-schema crate Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 18/22] firewall-api-types: add FirewallRuleUpdater type Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 19/22] firewall-api-types: refactor FirewallRule and add FirewallRuleListEntry Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 20/22] firewall-api-types: add DeletableFirewallRuleProperty enum Dietmar Maurer
2026-02-16 10:43 ` [RFC proxmox 21/22] firewall-api-types: add FirewallAliasEntry API type Dietmar Maurer
2026-02-16 10:44 ` [RFC proxmox 22/22] firewall-api-types: add FirewallIpsetListEntry and FirewallIpsetEntry api types Dietmar Maurer
2026-02-17 6:17 ` [RFC proxmox 00/22] New crate for firewall " Hannes Laimer
2026-02-17 6:39 ` Dietmar Maurer
2026-02-17 8:17 ` Hannes Laimer
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox