public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation
@ 2024-04-18 16:13 Stefan Hanreich
  2024-04-18 16:13 ` [pve-devel] [PATCH proxmox-firewall v3 01/39] config: add proxmox-ve-config crate Stefan Hanreich
                   ` (39 more replies)
  0 siblings, 40 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:13 UTC (permalink / raw)
  To: pve-devel

## Introduction
This RFC provides a drop-in replacement for the current pve-firewall package
that is based on Rust and nftables.

It consists of three crates:
* proxmox-ve-config
  for parsing firewall and guest configuration files, as well as some helpers
  to access host configuration (particularly networking)
* proxmox-nftables
  contains bindings for libnftables as well as types that implement the JSON
  schema defined by libnftables-json
* proxmox-firewall
  uses the other two crates to read the firewall configuration and create the
  respective nftables configuration


## Installation
* Build & install all deb packages on your PVE instance
* Enable the nftables firewall by going to
  Web UI > <Host> > Firewall > Options > nftables
* Enable the firewall datacenter-wide if you haven't already
* Restarting running VMs/CTs is required so the changes to the fwbr creation
  go into effect

For your convenience I have provided pre-built packages on our share under
`shanreich-proxmox-firewall`.

The source code is also available on my staff repo as `proxmox-firewall`.


## Configuration
The firewall should work as a drop-in replacement for the pve-firewall, so you
should be able to configure the firewall as usual via the Web UI or
configuration files.


## Known Issues
There is currently one major issue that we still need to solve: REJECTing
packets from the guest firewalls is currently not possible for incoming traffic
(it will instead be dropped).

This is due to the fact that we are using the postrouting hook of nftables in a
table with type bridge for incoming traffic. In the bridge table in the
postrouting hook we cannot tell whether the packet has also been sent to other
ports in the bridge (e.g. when a MAC has not yet been learned and the packet
then gets flooded to all bridge ports). If we would then REJECT a packet in the
postrouting hook this can lead to a bug where the firewall rules for one guest
REJECT a packet and send a response (RST for TCP, ICMP port/host-unreachable
otherwise).

This has also been explained in the respective commit introducing the
restriction [1].

We were able to circumvent this restriction in the old firewall due to using
firewall bridges and rejecting in the firewall bridge itself. Doing this leads
to the behavior described above, which has tripped up some of our users before
[2] [3] and which is, frankly, wrong.

I currently see two possible solutions for this, both of which carry downsides.
Your input on this matter would be much appreciated, particularly if you can
think of another solution which I cannot currently see:

1. Only REJECT packets in the prerouting chain of the firewall bridge with the
destination MAC address set to the MAC address of the network device, otherwise
DROP

The downside of this is that we, once again, will have to resort to using
firewall bridges, which we wanted to eliminate. This would also be the sole
reason for still having to resort to using firewall bridges.

2. Only allow DROP in the guest firewall for incoming traffic

This would be quite awkward since, well, rejecting traffic would be quite nice
for a firewall I'd say ;)

I'm happy for all input regarding this matter.


## Useful Commands

You can check if firewall rules got created by running

```
nft list ruleset
```

You can also check that `iptables` rules are not created via
```
iptables-save
```

Further info about the services:
```
systemctl status proxmox-firewall.{service,timer}
```

You can grab the debug output from the new firewall like so:

```
RUST_LOG=trace proxmox-firewall
```

## Upcoming
There are many ideas for further features, here is a (non-exhaustive) list:
* SNAT/DNAT handling
* SDN integration + bridge/vnet-level firewalling
* counters
* flow offloading
* synproxy support
* rate-limiting for rules
* connlimit support
* brouting support

The first thing I'll be working on though is proper rustdocs for both libraries
as well as the firewall.


Changes from v2 -> v3:
* move from libnftables to nft CLI
* daemonize proxmox-firewall
* prettify chains.json
* move location of proxmox-firewall to /usr/libexec/proxmox
* update documentation to reflect the changes

Changes from v1 -> v2:
* now builds cleanly in sbuild (thanks @Fabian)
* made base ruleset more efficient
* fixed issues with some rules in the base ruleset
* refactored config loading in order to make it mockable (thanks @Lukas)
* added an integration test that spans the whole pipeline from config ->
  nftables rules
* changed many maps from HashMap to BTreeMap to make the generated nftables
  output stable (particularly important for the integration test)
* improved logging output
* implemented ARP spoofing prevention
* failing to change sysctl settings now only emits a warning instead of aborting
  the rule generation process
* fixed some bugs wrt rule generation
* added a few macros (HTTP/3, PBS)
* added documentation to pve-docs
* EUI64 link-local addresses are now added to the automatically generated ip
  filters
* incorporated suggestions from @Max and @Lukas (tyvm!)


[1] https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/net/bridge/netfilter/nft_reject_bridge.c?h=v6.8.2&id=127917c29a432c3b798e014a1714e9c1af0f87fe
[2] https://bugzilla.proxmox.com/show_bug.cgi?id=4964
[3] https://forum.proxmox.com/threads/proxmox-claiming-mac-address.52601/page-2#post-415493

proxmox-firewall:

Stefan Hanreich (34):
  config: add proxmox-ve-config crate
  config: firewall: add types for ip addresses
  config: firewall: add types for ports
  config: firewall: add types for log level and rate limit
  config: firewall: add types for aliases
  config: host: add helpers for host network configuration
  config: guest: add helpers for parsing guest network config
  config: firewall: add types for ipsets
  config: firewall: add types for rules
  config: firewall: add types for security groups
  config: firewall: add generic parser for firewall configs
  config: firewall: add cluster-specific config + option types
  config: firewall: add host specific config + option types
  config: firewall: add guest-specific config + option types
  config: firewall: add firewall macros
  config: firewall: add conntrack helper types
  nftables: add crate for libnftables bindings
  nftables: add helpers
  nftables: expression: add types
  nftables: expression: implement conversion traits for firewall config
  nftables: statement: add types
  nftables: statement: add conversion traits for config types
  nftables: commands: add types
  nftables: types: add conversion traits
  nftables: add nft client
  firewall: add firewall crate
  firewall: add base ruleset
  firewall: add config loader
  firewall: add rule generation logic
  firewall: add object generation logic
  firewall: add ruleset generation logic
  firewall: add proxmox-firewall binary and move existing code into lib
  firewall: add files for debian packaging
  firewall: add integration test


qemu-server:

Stefan Hanreich (1):
  firewall: add handling for new nft firewall

 vm-network-scripts/pve-bridge | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)


pve-container:

Stefan Hanreich (1):
  firewall: add handling for new nft firewall

 src/PVE/LXC.pm | 5 +++++
 1 file changed, 5 insertions(+)


pve-firewall:

Stefan Hanreich (1):
  add configuration option for new nftables firewall

 src/PVE/Firewall.pm | 20 ++++++++++++++++----
 1 file changed, 16 insertions(+), 4 deletions(-)


pve-manager:

Stefan Hanreich (1):
  firewall: expose configuration option for new nftables firewall

 www/manager6/grid/FirewallOptions.js | 1 +
 1 file changed, 1 insertion(+)


pve-docs:

Stefan Hanreich (1):
  firewall: add documentation for proxmox-firewall

 pve-firewall.adoc | 185 ++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 185 insertions(+)


Summary over all repositories:
  5 files changed, 214 insertions(+), 6 deletions(-)

-- 
Generated by git-murpp 0.6.0

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


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

* [pve-devel] [PATCH proxmox-firewall v3 01/39] config: add proxmox-ve-config crate
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
@ 2024-04-18 16:13 ` Stefan Hanreich
  2024-04-18 16:13 ` [pve-devel] [PATCH proxmox-firewall v3 02/39] config: firewall: add types for ip addresses Stefan Hanreich
                   ` (38 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:13 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller<w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 .cargo/config                |  5 +++++
 .gitignore                   |  6 ++++++
 Cargo.toml                   |  4 ++++
 proxmox-ve-config/Cargo.toml | 19 +++++++++++++++++++
 proxmox-ve-config/src/lib.rs |  0
 5 files changed, 34 insertions(+)
 create mode 100644 .cargo/config
 create mode 100644 .gitignore
 create mode 100644 Cargo.toml
 create mode 100644 proxmox-ve-config/Cargo.toml
 create mode 100644 proxmox-ve-config/src/lib.rs

diff --git a/.cargo/config b/.cargo/config
new file mode 100644
index 0000000..3b5b6e4
--- /dev/null
+++ b/.cargo/config
@@ -0,0 +1,5 @@
+[source]
+[source.debian-packages]
+directory = "/usr/share/cargo/registry"
+[source.crates-io]
+replace-with = "debian-packages"
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3cb8114
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+/target
+/Cargo.lock
+proxmox-firewall-*/
+*.deb
+*.buildinfo
+*.changes
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..a8d33ab
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,4 @@
+[workspace]
+members = [
+    "proxmox-ve-config",
+]
diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml
new file mode 100644
index 0000000..80b336a
--- /dev/null
+++ b/proxmox-ve-config/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "proxmox-ve-config"
+version = "0.1.0"
+edition = "2021"
+authors = [
+    "Wolfgang Bumiller <w.bumiller@proxmox.com>",
+    "Stefan Hanreich <s.hanreich@proxmox.com>",
+    "Proxmox Support Team <support@proxmox.com>",
+]
+description = "Proxmox VE config parsing"
+license = "AGPL-3"
+
+[dependencies]
+log = "0.4"
+anyhow = "1"
+
+serde = { version = "1", features = [ "derive" ] }
+serde_json = "1"
+serde_with = "2.3.3"
diff --git a/proxmox-ve-config/src/lib.rs b/proxmox-ve-config/src/lib.rs
new file mode 100644
index 0000000..e69de29
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 02/39] config: firewall: add types for ip addresses
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
  2024-04-18 16:13 ` [pve-devel] [PATCH proxmox-firewall v3 01/39] config: add proxmox-ve-config crate Stefan Hanreich
@ 2024-04-18 16:13 ` Stefan Hanreich
  2024-04-18 16:13 ` [pve-devel] [PATCH proxmox-firewall v3 03/39] config: firewall: add types for ports Stefan Hanreich
                   ` (37 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:13 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

Includes types for all kinds of IP values that can occur in the
firewall config. Additionally, FromStr implementations are available
for parsing from the config files.

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/src/firewall/mod.rs         |   1 +
 .../src/firewall/types/address.rs             | 615 ++++++++++++++++++
 proxmox-ve-config/src/firewall/types/mod.rs   |   3 +
 proxmox-ve-config/src/lib.rs                  |   1 +
 4 files changed, 620 insertions(+)
 create mode 100644 proxmox-ve-config/src/firewall/mod.rs
 create mode 100644 proxmox-ve-config/src/firewall/types/address.rs
 create mode 100644 proxmox-ve-config/src/firewall/types/mod.rs

diff --git a/proxmox-ve-config/src/firewall/mod.rs b/proxmox-ve-config/src/firewall/mod.rs
new file mode 100644
index 0000000..cd40856
--- /dev/null
+++ b/proxmox-ve-config/src/firewall/mod.rs
@@ -0,0 +1 @@
+pub mod types;
diff --git a/proxmox-ve-config/src/firewall/types/address.rs b/proxmox-ve-config/src/firewall/types/address.rs
new file mode 100644
index 0000000..e48ac1b
--- /dev/null
+++ b/proxmox-ve-config/src/firewall/types/address.rs
@@ -0,0 +1,615 @@
+use std::fmt;
+use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
+use std::ops::Deref;
+
+use anyhow::{bail, format_err, Error};
+use serde_with::DeserializeFromStr;
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum Family {
+    V4,
+    V6,
+}
+
+impl fmt::Display for Family {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Family::V4 => f.write_str("Ipv4"),
+            Family::V6 => f.write_str("Ipv6"),
+        }
+    }
+}
+
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub enum Cidr {
+    Ipv4(Ipv4Cidr),
+    Ipv6(Ipv6Cidr),
+}
+
+impl Cidr {
+    pub fn new_v4(addr: impl Into<Ipv4Addr>, mask: u8) -> Result<Self, Error> {
+        Ok(Cidr::Ipv4(Ipv4Cidr::new(addr, mask)?))
+    }
+
+    pub fn new_v6(addr: impl Into<Ipv6Addr>, mask: u8) -> Result<Self, Error> {
+        Ok(Cidr::Ipv6(Ipv6Cidr::new(addr, mask)?))
+    }
+
+    pub const fn family(&self) -> Family {
+        match self {
+            Cidr::Ipv4(_) => Family::V4,
+            Cidr::Ipv6(_) => Family::V6,
+        }
+    }
+
+    pub fn is_ipv4(&self) -> bool {
+        matches!(self, Cidr::Ipv4(_))
+    }
+
+    pub fn is_ipv6(&self) -> bool {
+        matches!(self, Cidr::Ipv6(_))
+    }
+}
+
+impl fmt::Display for Cidr {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::Ipv4(ip) => f.write_str(ip.to_string().as_str()),
+            Self::Ipv6(ip) => f.write_str(ip.to_string().as_str()),
+        }
+    }
+}
+
+impl std::str::FromStr for Cidr {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Error> {
+        if let Ok(ip) = s.parse::<Ipv4Cidr>() {
+            return Ok(Cidr::Ipv4(ip));
+        }
+
+        if let Ok(ip) = s.parse::<Ipv6Cidr>() {
+            return Ok(Cidr::Ipv6(ip));
+        }
+
+        bail!("invalid ip address or CIDR: {s:?}");
+    }
+}
+
+impl From<Ipv4Cidr> for Cidr {
+    fn from(cidr: Ipv4Cidr) -> Self {
+        Cidr::Ipv4(cidr)
+    }
+}
+
+impl From<Ipv6Cidr> for Cidr {
+    fn from(cidr: Ipv6Cidr) -> Self {
+        Cidr::Ipv6(cidr)
+    }
+}
+
+const IPV4_LENGTH: u8 = 32;
+
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct Ipv4Cidr {
+    addr: Ipv4Addr,
+    mask: u8,
+}
+
+impl Ipv4Cidr {
+    pub fn new(addr: impl Into<Ipv4Addr>, mask: u8) -> Result<Self, Error> {
+        if mask > 32 {
+            bail!("mask out of range for ipv4 cidr ({mask})");
+        }
+
+        Ok(Self {
+            addr: addr.into(),
+            mask,
+        })
+    }
+
+    pub fn contains_address(&self, other: &Ipv4Addr) -> bool {
+        let bits = u32::from_be_bytes(self.addr.octets());
+        let other_bits = u32::from_be_bytes(other.octets());
+
+        let shift_amount: u32 = IPV4_LENGTH.saturating_sub(self.mask).into();
+
+        bits.checked_shr(shift_amount).unwrap_or(0)
+            == other_bits.checked_shr(shift_amount).unwrap_or(0)
+    }
+
+    pub fn address(&self) -> &Ipv4Addr {
+        &self.addr
+    }
+
+    pub fn mask(&self) -> u8 {
+        self.mask
+    }
+}
+
+impl<T: Into<Ipv4Addr>> From<T> for Ipv4Cidr {
+    fn from(value: T) -> Self {
+        Self {
+            addr: value.into(),
+            mask: 32,
+        }
+    }
+}
+
+impl std::str::FromStr for Ipv4Cidr {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Error> {
+        Ok(match s.find('/') {
+            None => Self {
+                addr: s.parse()?,
+                mask: 32,
+            },
+            Some(pos) => {
+                let mask: u8 = s[(pos + 1)..]
+                    .parse()
+                    .map_err(|_| format_err!("invalid mask in ipv4 cidr: {s:?}"))?;
+
+                Self::new(s[..pos].parse::<Ipv4Addr>()?, mask)?
+            }
+        })
+    }
+}
+
+impl fmt::Display for Ipv4Cidr {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{}/{}", &self.addr, self.mask)
+    }
+}
+
+const IPV6_LENGTH: u8 = 128;
+
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct Ipv6Cidr {
+    addr: Ipv6Addr,
+    mask: u8,
+}
+
+impl Ipv6Cidr {
+    pub fn new(addr: impl Into<Ipv6Addr>, mask: u8) -> Result<Self, Error> {
+        if mask > IPV6_LENGTH {
+            bail!("mask out of range for ipv6 cidr");
+        }
+
+        Ok(Self {
+            addr: addr.into(),
+            mask,
+        })
+    }
+
+    pub fn contains_address(&self, other: &Ipv6Addr) -> bool {
+        let bits = u128::from_be_bytes(self.addr.octets());
+        let other_bits = u128::from_be_bytes(other.octets());
+
+        let shift_amount: u32 = IPV6_LENGTH.saturating_sub(self.mask).into();
+
+        bits.checked_shr(shift_amount).unwrap_or(0)
+            == other_bits.checked_shr(shift_amount).unwrap_or(0)
+    }
+
+    pub fn address(&self) -> &Ipv6Addr {
+        &self.addr
+    }
+
+    pub fn mask(&self) -> u8 {
+        self.mask
+    }
+}
+
+impl std::str::FromStr for Ipv6Cidr {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Error> {
+        Ok(match s.find('/') {
+            None => Self {
+                addr: s.parse()?,
+                mask: 128,
+            },
+            Some(pos) => {
+                let mask: u8 = s[(pos + 1)..]
+                    .parse()
+                    .map_err(|_| format_err!("invalid mask in ipv6 cidr: {s:?}"))?;
+
+                Self::new(s[..pos].parse::<Ipv6Addr>()?, mask)?
+            }
+        })
+    }
+}
+
+impl fmt::Display for Ipv6Cidr {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{}/{}", &self.addr, self.mask)
+    }
+}
+
+impl<T: Into<Ipv6Addr>> From<T> for Ipv6Cidr {
+    fn from(addr: T) -> Self {
+        Self {
+            addr: addr.into(),
+            mask: 128,
+        }
+    }
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub enum IpEntry {
+    Cidr(Cidr),
+    Range(IpAddr, IpAddr),
+}
+
+impl std::str::FromStr for IpEntry {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Error> {
+        if s.is_empty() {
+            bail!("Empty IP specification!")
+        }
+
+        let entries: Vec<&str> = s
+            .split('-')
+            .take(3) // so we can check whether there are too many
+            .collect();
+
+        match entries.as_slice() {
+            [cidr] => Ok(IpEntry::Cidr(cidr.parse()?)),
+            [beg, end] => {
+                if let Ok(beg) = beg.parse::<Ipv4Addr>() {
+                    if let Ok(end) = end.parse::<Ipv4Addr>() {
+                        if beg < end {
+                            return Ok(IpEntry::Range(beg.into(), end.into()));
+                        }
+
+                        bail!("start address is greater than end address!");
+                    }
+                }
+
+                if let Ok(beg) = beg.parse::<Ipv6Addr>() {
+                    if let Ok(end) = end.parse::<Ipv6Addr>() {
+                        if beg < end {
+                            return Ok(IpEntry::Range(beg.into(), end.into()));
+                        }
+
+                        bail!("start address is greater than end address!");
+                    }
+                }
+
+                bail!("start and end are not valid IP addresses of the same type!")
+            }
+            _ => bail!("Invalid amount of elements in IpEntry!"),
+        }
+    }
+}
+
+impl fmt::Display for IpEntry {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::Cidr(ip) => write!(f, "{ip}"),
+            Self::Range(beg, end) => write!(f, "{beg}-{end}"),
+        }
+    }
+}
+
+impl IpEntry {
+    fn family(&self) -> Family {
+        match self {
+            Self::Cidr(cidr) => cidr.family(),
+            Self::Range(start, end) => {
+                if start.is_ipv4() && end.is_ipv4() {
+                    return Family::V4;
+                }
+
+                if start.is_ipv6() && end.is_ipv6() {
+                    return Family::V6;
+                }
+
+                // should never be reached due to constructors validating that
+                // start type == end type
+                unreachable!("invalid IP entry")
+            }
+        }
+    }
+}
+
+impl From<Cidr> for IpEntry {
+    fn from(value: Cidr) -> Self {
+        IpEntry::Cidr(value)
+    }
+}
+
+#[derive(Clone, Debug, DeserializeFromStr)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct IpList {
+    // guaranteed to have the same family
+    entries: Vec<IpEntry>,
+    family: Family,
+}
+
+impl Deref for IpList {
+    type Target = Vec<IpEntry>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.entries
+    }
+}
+
+impl<T: Into<IpEntry>> From<T> for IpList {
+    fn from(value: T) -> Self {
+        let entry = value.into();
+
+        Self {
+            family: entry.family(),
+            entries: vec![entry],
+        }
+    }
+}
+
+impl std::str::FromStr for IpList {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Error> {
+        if s.is_empty() {
+            bail!("Empty IP specification!")
+        }
+
+        let mut entries = Vec::new();
+        let mut current_family = None;
+
+        for element in s.split(',') {
+            let entry: IpEntry = element.parse()?;
+
+            if let Some(family) = current_family {
+                if family != entry.family() {
+                    bail!("Incompatible families in IPList!")
+                }
+            } else {
+                current_family = Some(entry.family());
+            }
+
+            entries.push(entry);
+        }
+
+        if entries.is_empty() {
+            bail!("empty ip list")
+        }
+
+        Ok(IpList {
+            entries,
+            family: current_family.unwrap(), // must be set due to length check above
+        })
+    }
+}
+
+impl IpList {
+    pub fn new(entries: Vec<IpEntry>) -> Result<Self, Error> {
+        let family = entries.iter().try_fold(None, |result, entry| {
+            if let Some(family) = result {
+                if entry.family() != family {
+                    bail!("non-matching families in entries list");
+                }
+
+                Ok(Some(family))
+            } else {
+                Ok(Some(entry.family()))
+            }
+        })?;
+
+        if let Some(family) = family {
+            return Ok(Self { entries, family });
+        }
+
+        bail!("no elements in ip list entries");
+    }
+
+    pub fn family(&self) -> Family {
+        self.family
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::net::{Ipv4Addr, Ipv6Addr};
+
+    #[test]
+    fn test_v4_cidr() {
+        let mut cidr: Ipv4Cidr = "0.0.0.0/0".parse().expect("valid IPv4 CIDR");
+
+        assert_eq!(cidr.addr, Ipv4Addr::new(0, 0, 0, 0));
+        assert_eq!(cidr.mask, 0);
+
+        assert!(cidr.contains_address(&Ipv4Addr::new(0, 0, 0, 0)));
+        assert!(cidr.contains_address(&Ipv4Addr::new(255, 255, 255, 255)));
+
+        cidr = "192.168.100.1".parse().expect("valid IPv4 CIDR");
+
+        assert_eq!(cidr.addr, Ipv4Addr::new(192, 168, 100, 1));
+        assert_eq!(cidr.mask, 32);
+
+        assert!(cidr.contains_address(&Ipv4Addr::new(192, 168, 100, 1)));
+        assert!(!cidr.contains_address(&Ipv4Addr::new(192, 168, 100, 2)));
+        assert!(!cidr.contains_address(&Ipv4Addr::new(192, 168, 100, 0)));
+
+        cidr = "10.100.5.0/24".parse().expect("valid IPv4 CIDR");
+
+        assert_eq!(cidr.mask, 24);
+
+        assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 0)));
+        assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 1)));
+        assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 100)));
+        assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 255)));
+        assert!(!cidr.contains_address(&Ipv4Addr::new(10, 100, 4, 255)));
+        assert!(!cidr.contains_address(&Ipv4Addr::new(10, 100, 6, 0)));
+
+        "0.0.0.0/-1".parse::<Ipv4Cidr>().unwrap_err();
+        "0.0.0.0/33".parse::<Ipv4Cidr>().unwrap_err();
+        "256.256.256.256/10".parse::<Ipv4Cidr>().unwrap_err();
+
+        "fe80::1/64".parse::<Ipv4Cidr>().unwrap_err();
+        "qweasd".parse::<Ipv4Cidr>().unwrap_err();
+        "".parse::<Ipv4Cidr>().unwrap_err();
+    }
+
+    #[test]
+    fn test_v6_cidr() {
+        let mut cidr: Ipv6Cidr = "abab::1/64".parse().expect("valid IPv6 CIDR");
+
+        assert_eq!(cidr.addr, Ipv6Addr::new(0xABAB, 0, 0, 0, 0, 0, 0, 1));
+        assert_eq!(cidr.mask, 64);
+
+        assert!(cidr.contains_address(&Ipv6Addr::new(0xABAB, 0, 0, 0, 0, 0, 0, 0)));
+        assert!(cidr.contains_address(&Ipv6Addr::new(
+            0xABAB, 0, 0, 0, 0xAAAA, 0xAAAA, 0xAAAA, 0xAAAA
+        )));
+        assert!(cidr.contains_address(&Ipv6Addr::new(
+            0xABAB, 0, 0, 0, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
+        )));
+        assert!(!cidr.contains_address(&Ipv6Addr::new(0xABAB, 0, 0, 1, 0, 0, 0, 0)));
+        assert!(!cidr.contains_address(&Ipv6Addr::new(
+            0xABAA, 0, 0, 0, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
+        )));
+
+        cidr = "eeee::1".parse().expect("valid IPv6 CIDR");
+
+        assert_eq!(cidr.mask, 128);
+
+        assert!(cidr.contains_address(&Ipv6Addr::new(0xEEEE, 0, 0, 0, 0, 0, 0, 1)));
+        assert!(!cidr.contains_address(&Ipv6Addr::new(
+            0xEEED, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
+        )));
+        assert!(!cidr.contains_address(&Ipv6Addr::new(0xEEEE, 0, 0, 0, 0, 0, 0, 0)));
+
+        "eeee::1/-1".parse::<Ipv6Cidr>().unwrap_err();
+        "eeee::1/129".parse::<Ipv6Cidr>().unwrap_err();
+        "gggg::1/64".parse::<Ipv6Cidr>().unwrap_err();
+
+        "192.168.0.1".parse::<Ipv6Cidr>().unwrap_err();
+        "qweasd".parse::<Ipv6Cidr>().unwrap_err();
+        "".parse::<Ipv6Cidr>().unwrap_err();
+    }
+
+    #[test]
+    fn test_parse_ip_entry() {
+        let mut entry: IpEntry = "10.0.0.1".parse().expect("valid IP entry");
+
+        assert_eq!(entry, Cidr::new_v4([10, 0, 0, 1], 32).unwrap().into());
+
+        entry = "10.0.0.0/16".parse().expect("valid IP entry");
+
+        assert_eq!(entry, Cidr::new_v4([10, 0, 0, 0], 16).unwrap().into());
+
+        entry = "192.168.0.1-192.168.99.255"
+            .parse()
+            .expect("valid IP entry");
+
+        assert_eq!(
+            entry,
+            IpEntry::Range([192, 168, 0, 1].into(), [192, 168, 99, 255].into())
+        );
+
+        entry = "fe80::1".parse().expect("valid IP entry");
+
+        assert_eq!(
+            entry,
+            Cidr::new_v6([0xFE80, 0, 0, 0, 0, 0, 0, 1], 128)
+                .unwrap()
+                .into()
+        );
+
+        entry = "fe80::1/48".parse().expect("valid IP entry");
+
+        assert_eq!(
+            entry,
+            Cidr::new_v6([0xFE80, 0, 0, 0, 0, 0, 0, 1], 48)
+                .unwrap()
+                .into()
+        );
+
+        entry = "fd80::1-fd80::ffff".parse().expect("valid IP entry");
+
+        assert_eq!(
+            entry,
+            IpEntry::Range(
+                [0xFD80, 0, 0, 0, 0, 0, 0, 1].into(),
+                [0xFD80, 0, 0, 0, 0, 0, 0, 0xFFFF].into(),
+            )
+        );
+
+        "192.168.100.0-192.168.99.255"
+            .parse::<IpEntry>()
+            .unwrap_err();
+        "192.168.100.0-fe80::1".parse::<IpEntry>().unwrap_err();
+        "192.168.100.0-192.168.200.0/16"
+            .parse::<IpEntry>()
+            .unwrap_err();
+        "192.168.100.0-192.168.200.0-192.168.250.0"
+            .parse::<IpEntry>()
+            .unwrap_err();
+        "qweasd".parse::<IpEntry>().unwrap_err();
+    }
+
+    #[test]
+    fn test_parse_ip_list() {
+        let mut ip_list: IpList = "192.168.0.1,192.168.100.0/24,172.16.0.0-172.32.255.255"
+            .parse()
+            .expect("valid IP list");
+
+        assert_eq!(
+            ip_list,
+            IpList {
+                entries: vec![
+                    IpEntry::Cidr(Cidr::new_v4([192, 168, 0, 1], 32).unwrap()),
+                    IpEntry::Cidr(Cidr::new_v4([192, 168, 100, 0], 24).unwrap()),
+                    IpEntry::Range([172, 16, 0, 0].into(), [172, 32, 255, 255].into()),
+                ],
+                family: Family::V4,
+            }
+        );
+
+        ip_list = "fe80::1/64".parse().expect("valid IP list");
+
+        assert_eq!(
+            ip_list,
+            IpList {
+                entries: vec![IpEntry::Cidr(
+                    Cidr::new_v6([0xFE80, 0, 0, 0, 0, 0, 0, 1], 64).unwrap()
+                ),],
+                family: Family::V6,
+            }
+        );
+
+        "192.168.0.1,fe80::1".parse::<IpList>().unwrap_err();
+
+        "".parse::<IpList>().unwrap_err();
+        "proxmox".parse::<IpList>().unwrap_err();
+    }
+
+    #[test]
+    fn test_construct_ip_list() {
+        let mut ip_list = IpList::new(vec![Cidr::new_v4([10, 0, 0, 0], 8).unwrap().into()])
+            .expect("valid ip list");
+
+        assert_eq!(ip_list.family(), Family::V4);
+
+        ip_list =
+            IpList::new(vec![Cidr::new_v6([0x000; 8], 8).unwrap().into()]).expect("valid ip list");
+
+        assert_eq!(ip_list.family(), Family::V6);
+
+        IpList::new(vec![]).expect_err("empty ip list is invalid");
+
+        IpList::new(vec![
+            Cidr::new_v4([10, 0, 0, 0], 8).unwrap().into(),
+            Cidr::new_v6([0x0000; 8], 8).unwrap().into(),
+        ])
+        .expect_err("cannot mix ip families in ip list");
+    }
+}
diff --git a/proxmox-ve-config/src/firewall/types/mod.rs b/proxmox-ve-config/src/firewall/types/mod.rs
new file mode 100644
index 0000000..de534b4
--- /dev/null
+++ b/proxmox-ve-config/src/firewall/types/mod.rs
@@ -0,0 +1,3 @@
+pub mod address;
+
+pub use address::Cidr;
diff --git a/proxmox-ve-config/src/lib.rs b/proxmox-ve-config/src/lib.rs
index e69de29..a0734b8 100644
--- a/proxmox-ve-config/src/lib.rs
+++ b/proxmox-ve-config/src/lib.rs
@@ -0,0 +1 @@
+pub mod firewall;
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 03/39] config: firewall: add types for ports
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
  2024-04-18 16:13 ` [pve-devel] [PATCH proxmox-firewall v3 01/39] config: add proxmox-ve-config crate Stefan Hanreich
  2024-04-18 16:13 ` [pve-devel] [PATCH proxmox-firewall v3 02/39] config: firewall: add types for ip addresses Stefan Hanreich
@ 2024-04-18 16:13 ` Stefan Hanreich
  2024-04-18 16:13 ` [pve-devel] [PATCH proxmox-firewall v3 04/39] config: firewall: add types for log level and rate limit Stefan Hanreich
                   ` (36 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:13 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

Adds types for all kinds of port-related values in the firewall config
as well as FromStr implementations for parsing them from the config.

Also adds a helper for parsing the named ports from `/etc/services`.

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/src/firewall/mod.rs        |   1 +
 proxmox-ve-config/src/firewall/ports.rs      |  80 ++++++++
 proxmox-ve-config/src/firewall/types/mod.rs  |   1 +
 proxmox-ve-config/src/firewall/types/port.rs | 181 +++++++++++++++++++
 4 files changed, 263 insertions(+)
 create mode 100644 proxmox-ve-config/src/firewall/ports.rs
 create mode 100644 proxmox-ve-config/src/firewall/types/port.rs

diff --git a/proxmox-ve-config/src/firewall/mod.rs b/proxmox-ve-config/src/firewall/mod.rs
index cd40856..a9f65bf 100644
--- a/proxmox-ve-config/src/firewall/mod.rs
+++ b/proxmox-ve-config/src/firewall/mod.rs
@@ -1 +1,2 @@
+pub mod ports;
 pub mod types;
diff --git a/proxmox-ve-config/src/firewall/ports.rs b/proxmox-ve-config/src/firewall/ports.rs
new file mode 100644
index 0000000..9d5d1be
--- /dev/null
+++ b/proxmox-ve-config/src/firewall/ports.rs
@@ -0,0 +1,80 @@
+use anyhow::{format_err, Error};
+use std::sync::OnceLock;
+
+#[derive(Default)]
+struct NamedPorts {
+    ports: std::collections::HashMap<String, u16>,
+}
+
+impl NamedPorts {
+    fn new() -> Self {
+        use std::io::BufRead;
+
+        log::trace!("loading /etc/services");
+
+        let mut this = Self::default();
+
+        let file = match std::fs::File::open("/etc/services") {
+            Ok(file) => file,
+            Err(_) => return this,
+        };
+
+        for line in std::io::BufReader::new(file).lines() {
+            let line = match line {
+                Ok(line) => line,
+                Err(_) => break,
+            };
+
+            let line = line.trim_start();
+
+            if line.is_empty() || line.starts_with('#') {
+                continue;
+            }
+
+            let mut parts = line.split_ascii_whitespace();
+
+            let name = match parts.next() {
+                None => continue,
+                Some(name) => name.to_string(),
+            };
+
+            let proto: u16 = match parts.next() {
+                None => continue,
+                Some(proto) => match proto.split('/').next() {
+                    None => continue,
+                    Some(num) => match num.parse() {
+                        Ok(num) => num,
+                        Err(_) => continue,
+                    },
+                },
+            };
+
+            this.ports.insert(name, proto);
+            for alias in parts {
+                if alias.starts_with('#') {
+                    break;
+                }
+                this.ports.insert(alias.to_string(), proto);
+            }
+        }
+
+        this
+    }
+
+    fn find(&self, name: &str) -> Option<u16> {
+        self.ports.get(name).copied()
+    }
+}
+
+fn named_ports() -> &'static NamedPorts {
+    static NAMED_PORTS: OnceLock<NamedPorts> = OnceLock::new();
+
+    NAMED_PORTS.get_or_init(NamedPorts::new)
+}
+
+/// Parse a named port with the help of `/etc/services`.
+pub fn parse_named_port(name: &str) -> Result<u16, Error> {
+    named_ports()
+        .find(name)
+        .ok_or_else(|| format_err!("unknown port name {name:?}"))
+}
diff --git a/proxmox-ve-config/src/firewall/types/mod.rs b/proxmox-ve-config/src/firewall/types/mod.rs
index de534b4..b740e5d 100644
--- a/proxmox-ve-config/src/firewall/types/mod.rs
+++ b/proxmox-ve-config/src/firewall/types/mod.rs
@@ -1,3 +1,4 @@
 pub mod address;
+pub mod port;
 
 pub use address::Cidr;
diff --git a/proxmox-ve-config/src/firewall/types/port.rs b/proxmox-ve-config/src/firewall/types/port.rs
new file mode 100644
index 0000000..c1252d9
--- /dev/null
+++ b/proxmox-ve-config/src/firewall/types/port.rs
@@ -0,0 +1,181 @@
+use std::fmt;
+use std::ops::Deref;
+
+use anyhow::{bail, Error};
+use serde_with::DeserializeFromStr;
+
+use crate::firewall::ports::parse_named_port;
+
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub enum PortEntry {
+    Port(u16),
+    Range(u16, u16),
+}
+
+impl fmt::Display for PortEntry {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::Port(p) => write!(f, "{p}"),
+            Self::Range(beg, end) => write!(f, "{beg}-{end}"),
+        }
+    }
+}
+
+fn parse_port(port: &str) -> Result<u16, Error> {
+    if let Ok(port) = port.parse::<u16>() {
+        return Ok(port);
+    }
+
+    if let Ok(port) = parse_named_port(port) {
+        return Ok(port);
+    }
+
+    bail!("invalid port specification: {port}")
+}
+
+impl std::str::FromStr for PortEntry {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Ok(match s.trim().split_once(':') {
+            None => PortEntry::from(parse_port(s)?),
+            Some((first, second)) => {
+                PortEntry::try_from((parse_port(first)?, parse_port(second)?))?
+            }
+        })
+    }
+}
+
+impl From<u16> for PortEntry {
+    fn from(port: u16) -> Self {
+        PortEntry::Port(port)
+    }
+}
+
+impl TryFrom<(u16, u16)> for PortEntry {
+    type Error = Error;
+
+    fn try_from(ports: (u16, u16)) -> Result<Self, Error> {
+        if ports.0 > ports.1 {
+            bail!("start port is greater than end port!");
+        }
+
+        Ok(PortEntry::Range(ports.0, ports.1))
+    }
+}
+
+#[derive(Clone, Debug, DeserializeFromStr)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct PortList(pub(crate) Vec<PortEntry>);
+
+impl FromIterator<PortEntry> for PortList {
+    fn from_iter<T: IntoIterator<Item = PortEntry>>(iter: T) -> Self {
+        Self(iter.into_iter().collect())
+    }
+}
+
+impl<T: Into<PortEntry>> From<T> for PortList {
+    fn from(value: T) -> Self {
+        Self(vec![value.into()])
+    }
+}
+
+impl Deref for PortList {
+    type Target = Vec<PortEntry>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl std::str::FromStr for PortList {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Error> {
+        if s.is_empty() {
+            bail!("empty port specification");
+        }
+
+        let mut entries = Vec::new();
+
+        for entry in s.trim().split(',') {
+            entries.push(entry.parse()?);
+        }
+
+        if entries.is_empty() {
+            bail!("invalid empty port list");
+        }
+
+        Ok(Self(entries))
+    }
+}
+
+impl fmt::Display for PortList {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        use fmt::Write;
+        if self.0.len() > 1 {
+            f.write_char('{')?;
+        }
+
+        let mut comma = '\0';
+        for entry in &self.0 {
+            if std::mem::replace(&mut comma, ',') != '\0' {
+                f.write_char(comma)?;
+            }
+            fmt::Display::fmt(entry, f)?;
+        }
+
+        if self.0.len() > 1 {
+            f.write_char('}')?;
+        }
+
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_parse_port_entry() {
+        let mut port_entry: PortEntry = "12345".parse().expect("valid port entry");
+        assert_eq!(port_entry, PortEntry::from(12345));
+
+        port_entry = "0:65535".parse().expect("valid port entry");
+        assert_eq!(port_entry, PortEntry::try_from((0, 65535)).unwrap());
+
+        "65536".parse::<PortEntry>().unwrap_err();
+        "100:100000".parse::<PortEntry>().unwrap_err();
+        "qweasd".parse::<PortEntry>().unwrap_err();
+        "".parse::<PortEntry>().unwrap_err();
+    }
+
+    #[test]
+    fn test_parse_port_list() {
+        let mut port_list: PortList = "12345".parse().expect("valid port list");
+        assert_eq!(port_list, PortList::from(12345));
+
+        port_list = "12345,0:65535,1337,ssh:80,https"
+            .parse()
+            .expect("valid port list");
+
+        assert_eq!(
+            port_list,
+            PortList(vec![
+                PortEntry::from(12345),
+                PortEntry::try_from((0, 65535)).unwrap(),
+                PortEntry::from(1337),
+                PortEntry::try_from((22, 80)).unwrap(),
+                PortEntry::from(443),
+            ])
+        );
+
+        "0::1337".parse::<PortList>().unwrap_err();
+        "0:1337,".parse::<PortList>().unwrap_err();
+        "70000".parse::<PortList>().unwrap_err();
+        "qweasd".parse::<PortList>().unwrap_err();
+        "".parse::<PortList>().unwrap_err();
+    }
+}
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 04/39] config: firewall: add types for log level and rate limit
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (2 preceding siblings ...)
  2024-04-18 16:13 ` [pve-devel] [PATCH proxmox-firewall v3 03/39] config: firewall: add types for ports Stefan Hanreich
@ 2024-04-18 16:13 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 05/39] config: firewall: add types for aliases Stefan Hanreich
                   ` (35 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:13 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

Adds types for log and (log-)rate-limiting firewall config options as
well as FromStr implementations for parsing them from the config.

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/Cargo.toml                |   1 +
 proxmox-ve-config/src/firewall/mod.rs       |   2 +
 proxmox-ve-config/src/firewall/parse.rs     |  21 ++
 proxmox-ve-config/src/firewall/types/log.rs | 222 ++++++++++++++++++++
 proxmox-ve-config/src/firewall/types/mod.rs |   1 +
 5 files changed, 247 insertions(+)
 create mode 100644 proxmox-ve-config/src/firewall/parse.rs
 create mode 100644 proxmox-ve-config/src/firewall/types/log.rs

diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml
index 80b336a..7bb391e 100644
--- a/proxmox-ve-config/Cargo.toml
+++ b/proxmox-ve-config/Cargo.toml
@@ -16,4 +16,5 @@ anyhow = "1"
 
 serde = { version = "1", features = [ "derive" ] }
 serde_json = "1"
+serde_plain = "1"
 serde_with = "2.3.3"
diff --git a/proxmox-ve-config/src/firewall/mod.rs b/proxmox-ve-config/src/firewall/mod.rs
index a9f65bf..2e0f31e 100644
--- a/proxmox-ve-config/src/firewall/mod.rs
+++ b/proxmox-ve-config/src/firewall/mod.rs
@@ -1,2 +1,4 @@
 pub mod ports;
 pub mod types;
+
+pub(crate) mod parse;
diff --git a/proxmox-ve-config/src/firewall/parse.rs b/proxmox-ve-config/src/firewall/parse.rs
new file mode 100644
index 0000000..a75daee
--- /dev/null
+++ b/proxmox-ve-config/src/firewall/parse.rs
@@ -0,0 +1,21 @@
+use anyhow::{bail, format_err, Error};
+
+pub fn parse_bool(value: &str) -> Result<bool, Error> {
+    Ok(
+        if value == "0"
+            || value.eq_ignore_ascii_case("false")
+            || value.eq_ignore_ascii_case("off")
+            || value.eq_ignore_ascii_case("no")
+        {
+            false
+        } else if value == "1"
+            || value.eq_ignore_ascii_case("true")
+            || value.eq_ignore_ascii_case("on")
+            || value.eq_ignore_ascii_case("yes")
+        {
+            true
+        } else {
+            bail!("not a boolean: {value:?}");
+        },
+    )
+}
diff --git a/proxmox-ve-config/src/firewall/types/log.rs b/proxmox-ve-config/src/firewall/types/log.rs
new file mode 100644
index 0000000..72344e4
--- /dev/null
+++ b/proxmox-ve-config/src/firewall/types/log.rs
@@ -0,0 +1,222 @@
+use std::fmt;
+use std::str::FromStr;
+
+use crate::firewall::parse::parse_bool;
+use anyhow::{bail, Error};
+use serde::{Deserialize, Serialize};
+
+#[derive(Copy, Clone, Debug, Deserialize, Serialize, Default)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+#[serde(rename_all = "lowercase")]
+pub enum LogRateLimitTimescale {
+    #[default]
+    Second,
+    Minute,
+    Hour,
+    Day,
+}
+
+impl FromStr for LogRateLimitTimescale {
+    type Err = Error;
+
+    fn from_str(str: &str) -> Result<Self, Error> {
+        match str {
+            "second" => Ok(LogRateLimitTimescale::Second),
+            "minute" => Ok(LogRateLimitTimescale::Minute),
+            "hour" => Ok(LogRateLimitTimescale::Hour),
+            "day" => Ok(LogRateLimitTimescale::Day),
+            _ => bail!("Invalid time scale provided"),
+        }
+    }
+}
+
+#[derive(Debug, Deserialize, Clone)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct LogRateLimit {
+    enabled: bool,
+    rate: i64, // in packets
+    per: LogRateLimitTimescale,
+    burst: i64, // in packets
+}
+
+impl LogRateLimit {
+    pub fn new(enabled: bool, rate: i64, per: LogRateLimitTimescale, burst: i64) -> Self {
+        Self {
+            enabled,
+            rate,
+            per,
+            burst,
+        }
+    }
+
+    pub fn enabled(&self) -> bool {
+        self.enabled
+    }
+
+    pub fn rate(&self) -> i64 {
+        self.rate
+    }
+
+    pub fn burst(&self) -> i64 {
+        self.burst
+    }
+
+    pub fn per(&self) -> LogRateLimitTimescale {
+        self.per
+    }
+}
+
+impl Default for LogRateLimit {
+    fn default() -> Self {
+        Self {
+            enabled: true,
+            rate: 1,
+            burst: 5,
+            per: LogRateLimitTimescale::Second,
+        }
+    }
+}
+
+impl FromStr for LogRateLimit {
+    type Err = Error;
+
+    fn from_str(str: &str) -> Result<Self, Error> {
+        let mut limit = Self::default();
+
+        for element in str.split(',') {
+            match element.split_once('=') {
+                None => {
+                    limit.enabled = parse_bool(element)?;
+                }
+                Some((key, value)) if !key.is_empty() && !value.is_empty() => match key {
+                    "enable" => limit.enabled = parse_bool(value)?,
+                    "burst" => limit.burst = i64::from_str(value)?,
+                    "rate" => match value.split_once('/') {
+                        None => {
+                            limit.rate = i64::from_str(value)?;
+                        }
+                        Some((rate, unit)) => {
+                            if unit.is_empty() {
+                                bail!("empty unit specification")
+                            }
+
+                            limit.rate = i64::from_str(rate)?;
+                            limit.per = LogRateLimitTimescale::from_str(unit)?;
+                        }
+                    },
+                    _ => bail!("Invalid value for Key found in log_ratelimit!"),
+                },
+                _ => bail!("invalid value in log_ratelimit"),
+            }
+        }
+
+        Ok(limit)
+    }
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
+pub enum LogLevel {
+    #[default]
+    Nolog,
+    Emergency,
+    Alert,
+    Critical,
+    Error,
+    Warning,
+    Notice,
+    Info,
+    Debug,
+}
+
+impl std::str::FromStr for LogLevel {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Error> {
+        Ok(match s {
+            "nolog" => LogLevel::Nolog,
+            "emerg" => LogLevel::Emergency,
+            "alert" => LogLevel::Alert,
+            "crit" => LogLevel::Critical,
+            "err" => LogLevel::Error,
+            "warn" => LogLevel::Warning,
+            "warning" => LogLevel::Warning,
+            "notice" => LogLevel::Notice,
+            "info" => LogLevel::Info,
+            "debug" => LogLevel::Debug,
+            _ => bail!("invalid log level {s:?}"),
+        })
+    }
+}
+
+impl fmt::Display for LogLevel {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.write_str(match self {
+            LogLevel::Nolog => "nolog",
+            LogLevel::Emergency => "emerg",
+            LogLevel::Alert => "alert",
+            LogLevel::Critical => "crit",
+            LogLevel::Error => "err",
+            LogLevel::Warning => "warn",
+            LogLevel::Notice => "notice",
+            LogLevel::Info => "info",
+            LogLevel::Debug => "debug",
+        })
+    }
+}
+
+serde_plain::derive_deserialize_from_fromstr!(LogLevel, "valid log level");
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_parse_rate_limit() {
+        let mut parsed_rate_limit = "1,burst=123,rate=44"
+            .parse::<LogRateLimit>()
+            .expect("valid rate limit");
+
+        assert_eq!(
+            parsed_rate_limit,
+            LogRateLimit {
+                enabled: true,
+                burst: 123,
+                rate: 44,
+                per: LogRateLimitTimescale::Second,
+            }
+        );
+
+        parsed_rate_limit = "1".parse::<LogRateLimit>().expect("valid rate limit");
+
+        assert_eq!(parsed_rate_limit, LogRateLimit::default());
+
+        parsed_rate_limit = "enable=0,rate=123/hour"
+            .parse::<LogRateLimit>()
+            .expect("valid rate limit");
+
+        assert_eq!(
+            parsed_rate_limit,
+            LogRateLimit {
+                enabled: false,
+                burst: 5,
+                rate: 123,
+                per: LogRateLimitTimescale::Hour,
+            }
+        );
+
+        "2".parse::<LogRateLimit>()
+            .expect_err("invalid value for enable");
+
+        "enabled=0,rate=123"
+            .parse::<LogRateLimit>()
+            .expect_err("invalid key in log ratelimit");
+
+        "enable=0,rate=123,"
+            .parse::<LogRateLimit>()
+            .expect_err("trailing comma in log rate limit specification");
+
+        "enable=0,rate=123/proxmox,"
+            .parse::<LogRateLimit>()
+            .expect_err("invalid unit for rate");
+    }
+}
diff --git a/proxmox-ve-config/src/firewall/types/mod.rs b/proxmox-ve-config/src/firewall/types/mod.rs
index b740e5d..8bf31b8 100644
--- a/proxmox-ve-config/src/firewall/types/mod.rs
+++ b/proxmox-ve-config/src/firewall/types/mod.rs
@@ -1,4 +1,5 @@
 pub mod address;
+pub mod log;
 pub mod port;
 
 pub use address::Cidr;
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 05/39] config: firewall: add types for aliases
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (3 preceding siblings ...)
  2024-04-18 16:13 ` [pve-devel] [PATCH proxmox-firewall v3 04/39] config: firewall: add types for log level and rate limit Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 06/39] config: host: add helpers for host network configuration Stefan Hanreich
                   ` (34 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/src/firewall/parse.rs       |  52 ++++++
 proxmox-ve-config/src/firewall/types/alias.rs | 160 ++++++++++++++++++
 proxmox-ve-config/src/firewall/types/mod.rs   |   2 +
 3 files changed, 214 insertions(+)
 create mode 100644 proxmox-ve-config/src/firewall/types/alias.rs

diff --git a/proxmox-ve-config/src/firewall/parse.rs b/proxmox-ve-config/src/firewall/parse.rs
index a75daee..772e081 100644
--- a/proxmox-ve-config/src/firewall/parse.rs
+++ b/proxmox-ve-config/src/firewall/parse.rs
@@ -1,5 +1,57 @@
 use anyhow::{bail, format_err, Error};
 
+/// Parses out a "name" which can be alphanumeric and include dashes.
+///
+/// Returns `None` if the name part would be empty.
+///
+/// Returns a tuple with the name and the remainder (not trimmed).
+///
+/// # Examples
+/// ```ignore
+/// assert_eq!(match_name("some-name someremainder"), Some(("some-name", " someremainder")));
+/// assert_eq!(match_name("some-name@someremainder"), Some(("some-name", "@someremainder")));
+/// assert_eq!(match_name(""), None);
+/// assert_eq!(match_name(" someremainder"), None);
+/// ```
+pub fn match_name(line: &str) -> Option<(&str, &str)> {
+    let end = line
+        .as_bytes()
+        .iter()
+        .position(|&b| !(b.is_ascii_alphanumeric() || b == b'-'));
+
+    let (name, rest) = match end {
+        Some(end) => line.split_at(end),
+        None => (line, ""),
+    };
+
+    if name.is_empty() {
+        None
+    } else {
+        Some((name, rest))
+    }
+}
+
+/// Parses up to the next whitespace character or end of the string.
+///
+/// Returns `None` if the non-whitespace part would be empty.
+///
+/// Returns a tuple containing the parsed section and the *trimmed* remainder.
+pub fn match_non_whitespace(line: &str) -> Option<(&str, &str)> {
+    let (text, rest) = line
+        .as_bytes()
+        .iter()
+        .position(|&b| b.is_ascii_whitespace())
+        .map(|pos| {
+            let (a, b) = line.split_at(pos);
+            (a, b.trim_start())
+        })
+        .unwrap_or((line, ""));
+    if text.is_empty() {
+        None
+    } else {
+        Some((text, rest))
+    }
+}
 pub fn parse_bool(value: &str) -> Result<bool, Error> {
     Ok(
         if value == "0"
diff --git a/proxmox-ve-config/src/firewall/types/alias.rs b/proxmox-ve-config/src/firewall/types/alias.rs
new file mode 100644
index 0000000..43c6486
--- /dev/null
+++ b/proxmox-ve-config/src/firewall/types/alias.rs
@@ -0,0 +1,160 @@
+use std::fmt::Display;
+use std::str::FromStr;
+
+use anyhow::{bail, format_err, Error};
+use serde_with::DeserializeFromStr;
+
+use crate::firewall::parse::{match_name, match_non_whitespace};
+use crate::firewall::types::address::Cidr;
+
+#[derive(Debug, Clone)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub enum AliasScope {
+    Datacenter,
+    Guest,
+}
+
+impl FromStr for AliasScope {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Ok(match s {
+            "dc" => AliasScope::Datacenter,
+            "guest" => AliasScope::Guest,
+            _ => bail!("invalid scope for alias: {s}"),
+        })
+    }
+}
+
+impl Display for AliasScope {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str(match self {
+            AliasScope::Datacenter => "dc",
+            AliasScope::Guest => "guest",
+        })
+    }
+}
+
+#[derive(Debug, Clone, DeserializeFromStr)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct AliasName {
+    scope: AliasScope,
+    name: String,
+}
+
+impl Display for AliasName {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_fmt(format_args!("{}/{}", self.scope, self.name))
+    }
+}
+
+impl FromStr for AliasName {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        match s.split_once('/') {
+            Some((prefix, name)) if !name.is_empty() => Ok(Self {
+                scope: prefix.parse()?,
+                name: name.to_string(),
+            }),
+            _ => {
+                bail!("Invalid Alias name!")
+            }
+        }
+    }
+}
+
+impl AliasName {
+    pub fn new(scope: AliasScope, name: impl Into<String>) -> Self {
+        Self {
+            scope,
+            name: name.into(),
+        }
+    }
+
+    pub fn name(&self) -> &str {
+        &self.name
+    }
+
+    pub fn scope(&self) -> &AliasScope {
+        &self.scope
+    }
+}
+
+#[derive(Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct Alias {
+    name: String,
+    address: Cidr,
+    comment: Option<String>,
+}
+
+impl Alias {
+    pub fn new(
+        name: impl Into<String>,
+        address: impl Into<Cidr>,
+        comment: impl Into<Option<String>>,
+    ) -> Self {
+        Self {
+            name: name.into(),
+            address: address.into(),
+            comment: comment.into(),
+        }
+    }
+
+    pub fn name(&self) -> &str {
+        &self.name
+    }
+
+    pub fn address(&self) -> &Cidr {
+        &self.address
+    }
+
+    pub fn comment(&self) -> Option<&str> {
+        self.comment.as_deref()
+    }
+}
+
+impl FromStr for Alias {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let (name, line) =
+            match_name(s.trim_start()).ok_or_else(|| format_err!("expected an alias name"))?;
+
+        let (address, line) = match_non_whitespace(line.trim_start())
+            .ok_or_else(|| format_err!("expected a value for alias {name:?}"))?;
+
+        let address: Cidr = address.parse()?;
+
+        let line = line.trim_start();
+
+        let comment = match line.strip_prefix('#') {
+            Some(comment) => Some(comment.trim().to_string()),
+            None if !line.is_empty() => bail!("trailing characters in alias: {line:?}"),
+            None => None,
+        };
+
+        Ok(Alias {
+            name: name.to_string(),
+            address,
+            comment,
+        })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_parse_alias_name() {
+        for name in ["dc/proxmox_123", "guest/proxmox-123"] {
+            name.parse::<AliasName>().expect("valid alias name");
+        }
+
+        for name in ["proxmox/proxmox_123", "guests/proxmox-123", "dc/", "/name"] {
+            name.parse::<AliasName>().expect_err("invalid alias name");
+        }
+    }
+}
diff --git a/proxmox-ve-config/src/firewall/types/mod.rs b/proxmox-ve-config/src/firewall/types/mod.rs
index 8bf31b8..69b69f4 100644
--- a/proxmox-ve-config/src/firewall/types/mod.rs
+++ b/proxmox-ve-config/src/firewall/types/mod.rs
@@ -1,5 +1,7 @@
 pub mod address;
+pub mod alias;
 pub mod log;
 pub mod port;
 
 pub use address::Cidr;
+pub use alias::Alias;
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 06/39] config: host: add helpers for host network configuration
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (4 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 05/39] config: firewall: add types for aliases Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 07/39] config: guest: add helpers for parsing guest network config Stefan Hanreich
                   ` (33 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

Currently the helpers for obtaining the host network configuration
panic on error, which could be avoided by the use of
OnceLock::get_or_init, but this method is currently only available in
nightly versions.

Generally, if there is a problem with obtaining the network config for
the node I would deem it acceptable for now, since that would usually
mean something is amiss with the network configuration and a firewall
won't really do anything then anyway.

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/Cargo.toml        |  5 +++
 proxmox-ve-config/src/host/mod.rs   |  1 +
 proxmox-ve-config/src/host/utils.rs | 70 +++++++++++++++++++++++++++++
 proxmox-ve-config/src/lib.rs        |  1 +
 4 files changed, 77 insertions(+)
 create mode 100644 proxmox-ve-config/src/host/mod.rs
 create mode 100644 proxmox-ve-config/src/host/utils.rs

diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml
index 7bb391e..cc689c8 100644
--- a/proxmox-ve-config/Cargo.toml
+++ b/proxmox-ve-config/Cargo.toml
@@ -13,8 +13,13 @@ license = "AGPL-3"
 [dependencies]
 log = "0.4"
 anyhow = "1"
+nix = "0.26"
 
 serde = { version = "1", features = [ "derive" ] }
 serde_json = "1"
 serde_plain = "1"
 serde_with = "2.3.3"
+
+proxmox-schema = "3.1.0"
+proxmox-sys = "0.5.3"
+proxmox-sortable-macro = "0.1.3"
diff --git a/proxmox-ve-config/src/host/mod.rs b/proxmox-ve-config/src/host/mod.rs
new file mode 100644
index 0000000..b5614dd
--- /dev/null
+++ b/proxmox-ve-config/src/host/mod.rs
@@ -0,0 +1 @@
+pub mod utils;
diff --git a/proxmox-ve-config/src/host/utils.rs b/proxmox-ve-config/src/host/utils.rs
new file mode 100644
index 0000000..b1dc8e9
--- /dev/null
+++ b/proxmox-ve-config/src/host/utils.rs
@@ -0,0 +1,70 @@
+use std::net::{IpAddr, ToSocketAddrs};
+
+use crate::firewall::types::Cidr;
+
+use nix::sys::socket::{AddressFamily, SockaddrLike};
+use proxmox_sys::nodename;
+
+/// gets a list of IPs that the hostname of this node resolves to
+///
+/// panics if the local hostname is not resolvable
+pub fn host_ips() -> Vec<IpAddr> {
+    let hostname = nodename();
+
+    log::trace!("resolving hostname");
+
+    format!("{hostname}:0")
+        .to_socket_addrs()
+        .expect("local hostname is resolvable")
+        .map(|addr| addr.ip())
+        .collect()
+}
+
+/// gets a list of all configured CIDRs on all network interfaces of this host
+///
+/// panics if unable to query the current network configuration
+pub fn network_interface_cidrs() -> Vec<Cidr> {
+    use nix::ifaddrs::getifaddrs;
+
+    log::trace!("reading networking interface list");
+
+    let mut cidrs = Vec::new();
+
+    let interfaces = getifaddrs().expect("should be able to query network interfaces");
+
+    for interface in interfaces {
+        if let (Some(address), Some(netmask)) = (interface.address, interface.netmask) {
+            match (address.family(), netmask.family()) {
+                (Some(AddressFamily::Inet), Some(AddressFamily::Inet)) => {
+                    let address = address.as_sockaddr_in().expect("is an IPv4 address").ip();
+
+                    let netmask = netmask
+                        .as_sockaddr_in()
+                        .expect("is an IPv4 address")
+                        .ip()
+                        .count_ones()
+                        .try_into()
+                        .expect("count_ones of u32 is < u8_max");
+
+                    cidrs.push(Cidr::new_v4(address, netmask).expect("netmask is valid"));
+                }
+                (Some(AddressFamily::Inet6), Some(AddressFamily::Inet6)) => {
+                    let address = address.as_sockaddr_in6().expect("is an IPv6 address").ip();
+
+                    let netmask_address =
+                        netmask.as_sockaddr_in6().expect("is an IPv6 address").ip();
+
+                    let netmask = u128::from_be_bytes(netmask_address.octets())
+                        .count_ones()
+                        .try_into()
+                        .expect("count_ones of u128 is < u8_max");
+
+                    cidrs.push(Cidr::new_v6(address, netmask).expect("netmask is valid"));
+                }
+                _ => continue,
+            }
+        }
+    }
+
+    cidrs
+}
diff --git a/proxmox-ve-config/src/lib.rs b/proxmox-ve-config/src/lib.rs
index a0734b8..2bf9352 100644
--- a/proxmox-ve-config/src/lib.rs
+++ b/proxmox-ve-config/src/lib.rs
@@ -1 +1,2 @@
 pub mod firewall;
+pub mod host;
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 07/39] config: guest: add helpers for parsing guest network config
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (5 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 06/39] config: host: add helpers for host network configuration Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 08/39] config: firewall: add types for ipsets Stefan Hanreich
                   ` (32 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

Currently this is parsing the config files via the filesystem. In the
future we could also get this information from pmxcfs directly via
IPC which should be more performant, particularly for a large number
of VMs.

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/src/firewall/parse.rs |  20 +
 proxmox-ve-config/src/guest/mod.rs      | 115 ++++++
 proxmox-ve-config/src/guest/types.rs    |  38 ++
 proxmox-ve-config/src/guest/vm.rs       | 510 ++++++++++++++++++++++++
 proxmox-ve-config/src/lib.rs            |   1 +
 5 files changed, 684 insertions(+)
 create mode 100644 proxmox-ve-config/src/guest/mod.rs
 create mode 100644 proxmox-ve-config/src/guest/types.rs
 create mode 100644 proxmox-ve-config/src/guest/vm.rs

diff --git a/proxmox-ve-config/src/firewall/parse.rs b/proxmox-ve-config/src/firewall/parse.rs
index 772e081..b02f98d 100644
--- a/proxmox-ve-config/src/firewall/parse.rs
+++ b/proxmox-ve-config/src/firewall/parse.rs
@@ -52,6 +52,26 @@ pub fn match_non_whitespace(line: &str) -> Option<(&str, &str)> {
         Some((text, rest))
     }
 }
+
+/// parses out all digits and returns the remainder
+///
+/// returns [`None`] if the digit part would be empty
+///
+/// Returns a tuple with the digits and the remainder (not trimmed).
+pub fn match_digits(line: &str) -> Option<(&str, &str)> {
+    let split_position = line.as_bytes().iter().position(|&b| !b.is_ascii_digit());
+
+    let (digits, rest) = match split_position {
+        Some(pos) => line.split_at(pos),
+        None => (line, ""),
+    };
+
+    if !digits.is_empty() {
+        return Some((digits, rest));
+    }
+
+    None
+}
 pub fn parse_bool(value: &str) -> Result<bool, Error> {
     Ok(
         if value == "0"
diff --git a/proxmox-ve-config/src/guest/mod.rs b/proxmox-ve-config/src/guest/mod.rs
new file mode 100644
index 0000000..74fd8ab
--- /dev/null
+++ b/proxmox-ve-config/src/guest/mod.rs
@@ -0,0 +1,115 @@
+use core::ops::Deref;
+use std::collections::HashMap;
+
+use anyhow::{Context, Error};
+use serde::Deserialize;
+
+use proxmox_sys::nodename;
+use types::Vmid;
+
+pub mod types;
+pub mod vm;
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize)]
+pub enum GuestType {
+    #[serde(rename = "qemu")]
+    Vm,
+    #[serde(rename = "lxc")]
+    Ct,
+}
+
+impl GuestType {
+    pub fn iface_prefix(self) -> &'static str {
+        match self {
+            GuestType::Vm => "tap",
+            GuestType::Ct => "veth",
+        }
+    }
+
+    fn config_folder(&self) -> &'static str {
+        match self {
+            GuestType::Vm => "qemu-server",
+            GuestType::Ct => "lxc",
+        }
+    }
+}
+
+#[derive(Deserialize)]
+pub struct GuestEntry {
+    node: String,
+
+    #[serde(rename = "type")]
+    ty: GuestType,
+
+    #[serde(rename = "version")]
+    _version: usize,
+}
+
+impl GuestEntry {
+    pub fn new(node: String, ty: GuestType) -> Self {
+        Self {
+            node,
+            ty,
+            _version: Default::default(),
+        }
+    }
+
+    pub fn is_local(&self) -> bool {
+        nodename() == self.node
+    }
+
+    pub fn ty(&self) -> &GuestType {
+        &self.ty
+    }
+}
+
+const VMLIST_CONFIG_PATH: &str = "/etc/pve/.vmlist";
+
+#[derive(Deserialize)]
+pub struct GuestMap {
+    #[serde(rename = "version")]
+    _version: usize,
+    #[serde(rename = "ids", default)]
+    guests: HashMap<Vmid, GuestEntry>,
+}
+
+impl From<HashMap<Vmid, GuestEntry>> for GuestMap {
+    fn from(guests: HashMap<Vmid, GuestEntry>) -> Self {
+        Self {
+            guests,
+            _version: Default::default(),
+        }
+    }
+}
+
+impl Deref for GuestMap {
+    type Target = HashMap<Vmid, GuestEntry>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.guests
+    }
+}
+
+impl GuestMap {
+    pub fn new() -> Result<Self, Error> {
+        let data = std::fs::read(VMLIST_CONFIG_PATH)
+            .with_context(|| format!("failed to read guest map from {VMLIST_CONFIG_PATH}"))?;
+
+        serde_json::from_slice(&data).with_context(|| "failed to parse guest map".to_owned())
+    }
+
+    pub fn firewall_config_path(vmid: &Vmid) -> String {
+        format!("/etc/pve/firewall/{}.fw", vmid)
+    }
+
+    /// returns the local configuration path for a given Vmid.
+    ///
+    /// The caller must ensure that the given Vmid exists and is local to the node
+    pub fn config_path(vmid: &Vmid, entry: &GuestEntry) -> String {
+        format!(
+            "/etc/pve/local/{}/{}.conf",
+            entry.ty().config_folder(),
+            vmid
+        )
+    }
+}
diff --git a/proxmox-ve-config/src/guest/types.rs b/proxmox-ve-config/src/guest/types.rs
new file mode 100644
index 0000000..217c537
--- /dev/null
+++ b/proxmox-ve-config/src/guest/types.rs
@@ -0,0 +1,38 @@
+use std::fmt;
+use std::str::FromStr;
+
+use anyhow::{format_err, Error};
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)]
+pub struct Vmid(u32);
+
+impl Vmid {
+    pub fn new(id: u32) -> Self {
+        Vmid(id)
+    }
+}
+
+impl From<u32> for Vmid {
+    fn from(value: u32) -> Self {
+        Self::new(value)
+    }
+}
+
+impl fmt::Display for Vmid {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        fmt::Display::fmt(&self.0, f)
+    }
+}
+
+impl FromStr for Vmid {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Ok(Self(
+            s.parse()
+                .map_err(|_| format_err!("not a valid vmid: {s:?}"))?,
+        ))
+    }
+}
+
+serde_plain::derive_deserialize_from_fromstr!(Vmid, "valid vmid");
diff --git a/proxmox-ve-config/src/guest/vm.rs b/proxmox-ve-config/src/guest/vm.rs
new file mode 100644
index 0000000..5b5866a
--- /dev/null
+++ b/proxmox-ve-config/src/guest/vm.rs
@@ -0,0 +1,510 @@
+use anyhow::{bail, Error};
+use core::fmt::Display;
+use std::io;
+use std::str::FromStr;
+use std::{collections::HashMap, net::Ipv6Addr};
+
+use proxmox_schema::property_string::PropertyIterator;
+
+use crate::firewall::parse::{match_digits, parse_bool};
+use crate::firewall::types::address::{Ipv4Cidr, Ipv6Cidr};
+
+#[derive(Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct MacAddress([u8; 6]);
+
+static LOCAL_PART: [u8; 8] = [0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
+static EUI64_MIDDLE_PART: [u8; 2] = [0xFF, 0xFE];
+
+impl MacAddress {
+    /// generates a link local IPv6-address according to RFC 4291 (Appendix A)
+    pub fn eui64_link_local_address(&self) -> Ipv6Addr {
+        let head = &self.0[..3];
+        let tail = &self.0[3..];
+
+        let mut eui64_address: Vec<u8> = LOCAL_PART
+            .iter()
+            .chain(head.iter())
+            .chain(EUI64_MIDDLE_PART.iter())
+            .chain(tail.iter())
+            .copied()
+            .collect();
+
+        // we need to flip the 7th bit of the first eui64 byte
+        eui64_address[8] ^= 0x02;
+
+        Ipv6Addr::from(
+            TryInto::<[u8; 16]>::try_into(eui64_address).expect("is an u8 array with 16 entries"),
+        )
+    }
+}
+
+impl FromStr for MacAddress {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let split = s.split(':');
+
+        let parsed = split
+            .into_iter()
+            .map(|elem| u8::from_str_radix(elem, 16))
+            .collect::<Result<Vec<u8>, _>>()
+            .map_err(Error::msg)?;
+
+        if parsed.len() != 6 {
+            bail!("Invalid amount of elements in MAC address!");
+        }
+
+        let address = &parsed.as_slice()[0..6];
+        Ok(Self(address.try_into().unwrap()))
+    }
+}
+
+impl Display for MacAddress {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "{:<02X}:{:<02X}:{:<02X}:{:<02X}:{:<02X}:{:<02X}",
+            self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5]
+        )
+    }
+}
+
+#[derive(Debug, Clone, Copy)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub enum NetworkDeviceModel {
+    VirtIO,
+    Veth,
+    E1000,
+    Vmxnet3,
+    RTL8139,
+}
+
+impl FromStr for NetworkDeviceModel {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        match s {
+            "virtio" => Ok(NetworkDeviceModel::VirtIO),
+            "e1000" => Ok(NetworkDeviceModel::E1000),
+            "rtl8139" => Ok(NetworkDeviceModel::RTL8139),
+            "vmxnet3" => Ok(NetworkDeviceModel::Vmxnet3),
+            "veth" => Ok(NetworkDeviceModel::Veth),
+            _ => bail!("Invalid network device model: {s}"),
+        }
+    }
+}
+
+#[derive(Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct NetworkDevice {
+    model: NetworkDeviceModel,
+    mac_address: MacAddress,
+    firewall: bool,
+    ip: Option<Ipv4Cidr>,
+    ip6: Option<Ipv6Cidr>,
+}
+
+impl NetworkDevice {
+    pub fn model(&self) -> NetworkDeviceModel {
+        self.model
+    }
+
+    pub fn mac_address(&self) -> &MacAddress {
+        &self.mac_address
+    }
+
+    pub fn ip(&self) -> Option<&Ipv4Cidr> {
+        self.ip.as_ref()
+    }
+
+    pub fn ip6(&self) -> Option<&Ipv6Cidr> {
+        self.ip6.as_ref()
+    }
+
+    pub fn has_firewall(&self) -> bool {
+        self.firewall
+    }
+}
+
+impl FromStr for NetworkDevice {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let (mut ty, mut hwaddr, mut firewall, mut ip, mut ip6) = (None, None, true, None, None);
+
+        for entry in PropertyIterator::new(s) {
+            let (key, value) = entry.unwrap();
+
+            if let Some(key) = key {
+                match key {
+                    "type" | "model" => {
+                        ty = Some(NetworkDeviceModel::from_str(&value)?);
+                    }
+                    "hwaddr" | "macaddr" => {
+                        hwaddr = Some(MacAddress::from_str(&value)?);
+                    }
+                    "firewall" => {
+                        firewall = parse_bool(&value)?;
+                    }
+                    "ip" => {
+                        if value == "dhcp" {
+                            continue;
+                        }
+
+                        ip = Some(Ipv4Cidr::from_str(&value)?);
+                    }
+                    "ip6" => {
+                        if value == "dhcp" || value == "auto" {
+                            continue;
+                        }
+
+                        ip6 = Some(Ipv6Cidr::from_str(&value)?);
+                    }
+                    _ => {
+                        if let Ok(model) = NetworkDeviceModel::from_str(key) {
+                            ty = Some(model);
+                            hwaddr = Some(MacAddress::from_str(&value)?);
+                        }
+                    }
+                }
+            }
+        }
+
+        if let (Some(ty), Some(hwaddr)) = (ty, hwaddr) {
+            return Ok(NetworkDevice {
+                model: ty,
+                mac_address: hwaddr,
+                firewall,
+                ip,
+                ip6,
+            });
+        }
+
+        bail!("No valid network device detected in string {s}");
+    }
+}
+
+#[derive(Debug, Default)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct NetworkConfig {
+    network_devices: HashMap<i64, NetworkDevice>,
+}
+
+impl NetworkConfig {
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    pub fn index_from_net_key(key: &str) -> Result<i64, Error> {
+        if let Some(digits) = key.strip_prefix("net") {
+            if let Some((digits, rest)) = match_digits(digits) {
+                let index: i64 = digits.parse()?;
+
+                if (0..31).contains(&index) && rest.is_empty() {
+                    return Ok(index);
+                }
+            }
+        }
+
+        bail!("No index found in net key string: {key}")
+    }
+
+    pub fn network_devices(&self) -> &HashMap<i64, NetworkDevice> {
+        &self.network_devices
+    }
+
+    pub fn parse<R: io::BufRead>(input: R) -> Result<Self, Error> {
+        let mut network_devices = HashMap::new();
+
+        for line in input.lines() {
+            let line = line?;
+            let line = line.trim();
+
+            if line.is_empty() || line.starts_with('#') {
+                continue;
+            }
+
+            if line.starts_with('[') {
+                break;
+            }
+
+            if line.starts_with("net") {
+                log::trace!("parsing net config line: {line}");
+
+                if let Some((mut key, mut value)) = line.split_once(':') {
+                    if key.is_empty() || value.is_empty() {
+                        continue;
+                    }
+
+                    key = key.trim();
+                    value = value.trim();
+
+                    if let Ok(index) = Self::index_from_net_key(key) {
+                        let network_device = NetworkDevice::from_str(value)?;
+
+                        let exists = network_devices.insert(index, network_device);
+
+                        if exists.is_some() {
+                            bail!("Duplicated config key detected: {key}");
+                        }
+                    } else {
+                        bail!("Encountered invalid net key in cfg: {key}");
+                    }
+                }
+            }
+        }
+
+        Ok(Self { network_devices })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_parse_mac_address() {
+        for input in [
+            "aa:aa:aa:11:22:33",
+            "AA:BB:FF:11:22:33",
+            "bc:24:11:AA:bb:Ef",
+        ] {
+            let mac_address = input.parse::<MacAddress>().expect("valid mac address");
+
+            assert_eq!(input.to_uppercase(), mac_address.to_string());
+        }
+
+        for input in [
+            "aa:aa:aa:11:22:33:aa",
+            "AA:BB:FF:11:22",
+            "AA:BB:GG:11:22:33",
+            "AABBGG112233",
+            "",
+        ] {
+            input
+                .parse::<MacAddress>()
+                .expect_err("invalid mac address");
+        }
+    }
+
+    #[test]
+    fn test_eui64_link_local_address() {
+        let mac_address: MacAddress = "BC:24:11:49:8D:75".parse().expect("valid MAC address");
+
+        let link_local_address =
+            Ipv6Addr::from_str("fe80::be24:11ff:fe49:8d75").expect("valid IPv6 address");
+
+        assert_eq!(link_local_address, mac_address.eui64_link_local_address());
+    }
+
+    #[test]
+    fn test_parse_network_device() {
+        let mut network_device: NetworkDevice =
+            "virtio=AA:AA:AA:17:19:81,bridge=public,firewall=1,queues=4"
+                .parse()
+                .expect("valid network configuration");
+
+        assert_eq!(
+            network_device,
+            NetworkDevice {
+                model: NetworkDeviceModel::VirtIO,
+                mac_address: MacAddress([0xAA, 0xAA, 0xAA, 0x17, 0x19, 0x81]),
+                firewall: true,
+                ip: None,
+                ip6: None,
+            }
+        );
+
+        network_device = "model=virtio,macaddr=AA:AA:AA:17:19:81,bridge=public,firewall=1,queues=4"
+            .parse()
+            .expect("valid network configuration");
+
+        assert_eq!(
+            network_device,
+            NetworkDevice {
+                model: NetworkDeviceModel::VirtIO,
+                mac_address: MacAddress([0xAA, 0xAA, 0xAA, 0x17, 0x19, 0x81]),
+                firewall: true,
+                ip: None,
+                ip6: None,
+            }
+        );
+
+        network_device =
+            "name=eth0,bridge=public,firewall=0,hwaddr=AA:AA:AA:E2:3E:24,ip=dhcp,type=veth"
+                .parse()
+                .expect("valid network configuration");
+
+        assert_eq!(
+            network_device,
+            NetworkDevice {
+                model: NetworkDeviceModel::Veth,
+                mac_address: MacAddress([0xAA, 0xAA, 0xAA, 0xE2, 0x3E, 0x24]),
+                firewall: false,
+                ip: None,
+                ip6: None,
+            }
+        );
+
+        "model=virtio"
+            .parse::<NetworkDevice>()
+            .expect_err("invalid network configuration");
+
+        "bridge=public,firewall=0"
+            .parse::<NetworkDevice>()
+            .expect_err("invalid network configuration");
+
+        "".parse::<NetworkDevice>()
+            .expect_err("invalid network configuration");
+
+        "name=eth0,bridge=public,firewall=0,hwaddr=AA:AA:AG:E2:3E:24,ip=dhcp,type=veth"
+            .parse::<NetworkDevice>()
+            .expect_err("invalid network configuration");
+    }
+
+    #[test]
+    fn test_parse_network_confg() {
+        let mut guest_config = "\
+boot: order=scsi0;net0
+cores: 4
+cpu: host
+memory: 8192
+meta: creation-qemu=8.0.2,ctime=1700141675
+name: hoan-sdn
+net0: virtio=AA:BB:CC:F2:FE:75,bridge=public
+numa: 0
+ostype: l26
+parent: uwu
+scsi0: local-lvm:vm-999-disk-0,discard=on,iothread=1,size=32G
+scsihw: virtio-scsi-single
+smbios1: uuid=addb0cc6-0393-4269-a504-1eb46604cb8a
+sockets: 1
+vmgenid: 13bcbb05-3608-4d74-bf4f-d5d20c3538e8
+
+[snapshot]
+boot: order=scsi0;ide2;net0
+cores: 4
+cpu: x86-64-v2-AES
+ide2: NFS-iso:iso/proxmox-ve_8.0-2.iso,media=cdrom,size=1166488K
+memory: 8192
+meta: creation-qemu=8.0.2,ctime=1700141675
+name: test
+net2: virtio=AA:AA:AA:F2:FE:75,bridge=public,firewall=1
+numa: 0
+ostype: l26
+parent: pre-SDN
+scsi0: local-lvm:vm-999-disk-0,discard=on,iothread=1,size=32G
+scsihw: virtio-scsi-single
+smbios1: uuid=addb0cc6-0393-4269-a504-1eb46604cb8a
+snaptime: 1700143513
+sockets: 1
+vmgenid: 706fbe99-d28b-4047-a9cd-3677c859ca8a
+
+[snapshott]
+boot: order=scsi0;ide2;net0
+cores: 4
+cpu: host
+ide2: NFS-iso:iso/proxmox-ve_8.0-2.iso,media=cdrom,size=1166488K
+memory: 8192
+meta: creation-qemu=8.0.2,ctime=1700141675
+name: hoan-sdn
+net0: virtio=AA:AA:FF:F2:FE:75,bridge=public,firewall=0
+numa: 0
+ostype: l26
+parent: SDN
+scsi0: local-lvm:vm-999-disk-0,discard=on,iothread=1,size=32G
+scsihw: virtio-scsi-single
+smbios1: uuid=addb0cc6-0393-4269-a504-1eb46604cb8a
+snaptime: 1700158473
+sockets: 1
+vmgenid: 706fbe99-d28b-4047-a9cd-3677c859ca8a"
+            .as_bytes();
+
+        let mut network_config: NetworkConfig =
+            NetworkConfig::parse(guest_config).expect("valid network configuration");
+
+        assert_eq!(network_config.network_devices().len(), 1);
+
+        assert_eq!(
+            network_config.network_devices()[&0],
+            NetworkDevice {
+                model: NetworkDeviceModel::VirtIO,
+                mac_address: MacAddress([0xAA, 0xBB, 0xCC, 0xF2, 0xFE, 0x75]),
+                firewall: true,
+                ip: None,
+                ip6: None,
+            }
+        );
+
+        guest_config = "\
+arch: amd64
+cores: 1
+features: nesting=1
+hostname: dnsct
+memory: 512
+net0: name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:11,ip=dhcp,type=veth
+net2:   name=eth0,bridge=data,firewall=0,hwaddr=BC:24:11:47:83:12,ip=123.123.123.123/24,type=veth  
+net5: name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:13,ip6=fd80::1/64,type=veth
+ostype: alpine
+rootfs: local-lvm:vm-10001-disk-0,size=1G
+swap: 512
+unprivileged: 1"
+            .as_bytes();
+
+        network_config = NetworkConfig::parse(guest_config).expect("valid network configuration");
+
+        assert_eq!(network_config.network_devices().len(), 3);
+
+        assert_eq!(
+            network_config.network_devices()[&0],
+            NetworkDevice {
+                model: NetworkDeviceModel::Veth,
+                mac_address: MacAddress([0xBC, 0x24, 0x11, 0x47, 0x83, 0x11]),
+                firewall: true,
+                ip: None,
+                ip6: None,
+            }
+        );
+
+        assert_eq!(
+            network_config.network_devices()[&2],
+            NetworkDevice {
+                model: NetworkDeviceModel::Veth,
+                mac_address: MacAddress([0xBC, 0x24, 0x11, 0x47, 0x83, 0x12]),
+                firewall: false,
+                ip: Some(Ipv4Cidr::from_str("123.123.123.123/24").expect("valid ipv4")),
+                ip6: None,
+            }
+        );
+
+        assert_eq!(
+            network_config.network_devices()[&5],
+            NetworkDevice {
+                model: NetworkDeviceModel::Veth,
+                mac_address: MacAddress([0xBC, 0x24, 0x11, 0x47, 0x83, 0x13]),
+                firewall: true,
+                ip: None,
+                ip6: Some(Ipv6Cidr::from_str("fd80::1/64").expect("valid ipv6")),
+            }
+        );
+
+        NetworkConfig::parse(
+            "netqwe: name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:11,ip=dhcp,type=veth"
+                .as_bytes(),
+        )
+        .expect_err("invalid net key");
+
+        NetworkConfig::parse(
+            "net0 name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:11,ip=dhcp,type=veth"
+                .as_bytes(),
+        )
+        .expect_err("invalid net key");
+
+        NetworkConfig::parse(
+            "net33: name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:11,ip=dhcp,type=veth"
+                .as_bytes(),
+        )
+        .expect_err("invalid net key");
+    }
+}
diff --git a/proxmox-ve-config/src/lib.rs b/proxmox-ve-config/src/lib.rs
index 2bf9352..856b14f 100644
--- a/proxmox-ve-config/src/lib.rs
+++ b/proxmox-ve-config/src/lib.rs
@@ -1,2 +1,3 @@
 pub mod firewall;
+pub mod guest;
 pub mod host;
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 08/39] config: firewall: add types for ipsets
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (6 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 07/39] config: guest: add helpers for parsing guest network config Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 09/39] config: firewall: add types for rules Stefan Hanreich
                   ` (31 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/src/firewall/types/ipset.rs | 349 ++++++++++++++++++
 proxmox-ve-config/src/firewall/types/mod.rs   |   2 +
 2 files changed, 351 insertions(+)
 create mode 100644 proxmox-ve-config/src/firewall/types/ipset.rs

diff --git a/proxmox-ve-config/src/firewall/types/ipset.rs b/proxmox-ve-config/src/firewall/types/ipset.rs
new file mode 100644
index 0000000..c1af642
--- /dev/null
+++ b/proxmox-ve-config/src/firewall/types/ipset.rs
@@ -0,0 +1,349 @@
+use core::fmt::Display;
+use std::ops::{Deref, DerefMut};
+use std::str::FromStr;
+
+use anyhow::{bail, format_err, Error};
+use serde_with::DeserializeFromStr;
+
+use crate::firewall::parse::match_non_whitespace;
+use crate::firewall::types::address::Cidr;
+use crate::firewall::types::alias::AliasName;
+use crate::guest::vm::NetworkConfig;
+
+#[derive(Debug, Clone, Copy, Eq, PartialEq)]
+pub enum IpsetScope {
+    Datacenter,
+    Guest,
+}
+
+impl FromStr for IpsetScope {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Ok(match s {
+            "+dc" => IpsetScope::Datacenter,
+            "+guest" => IpsetScope::Guest,
+            _ => bail!("invalid scope for ipset: {s}"),
+        })
+    }
+}
+
+impl Display for IpsetScope {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let prefix = match self {
+            Self::Datacenter => "dc",
+            Self::Guest => "guest",
+        };
+
+        f.write_str(prefix)
+    }
+}
+
+#[derive(Debug, Clone, DeserializeFromStr)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct IpsetName {
+    pub scope: IpsetScope,
+    pub name: String,
+}
+
+impl IpsetName {
+    pub fn new(scope: IpsetScope, name: impl Into<String>) -> Self {
+        Self {
+            scope,
+            name: name.into(),
+        }
+    }
+
+    pub fn name(&self) -> &str {
+        &self.name
+    }
+
+    pub fn scope(&self) -> IpsetScope {
+        self.scope
+    }
+}
+
+impl FromStr for IpsetName {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        match s.split_once('/') {
+            Some((prefix, name)) if !name.is_empty() => Ok(Self {
+                scope: prefix.parse()?,
+                name: name.to_string(),
+            }),
+            _ => {
+                bail!("Invalid IPSet name: {s}")
+            }
+        }
+    }
+}
+
+impl Display for IpsetName {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}/{}", self.scope, self.name)
+    }
+}
+
+#[derive(Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub enum IpsetAddress {
+    Alias(AliasName),
+    Cidr(Cidr),
+}
+
+impl FromStr for IpsetAddress {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Error> {
+        if let Ok(cidr) = s.parse() {
+            return Ok(IpsetAddress::Cidr(cidr));
+        }
+
+        if let Ok(name) = s.parse() {
+            return Ok(IpsetAddress::Alias(name));
+        }
+
+        bail!("Invalid address in IPSet: {s}")
+    }
+}
+
+impl<T: Into<Cidr>> From<T> for IpsetAddress {
+    fn from(cidr: T) -> Self {
+        IpsetAddress::Cidr(cidr.into())
+    }
+}
+
+#[derive(Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct IpsetEntry {
+    pub nomatch: bool,
+    pub address: IpsetAddress,
+    pub comment: Option<String>,
+}
+
+impl<T: Into<IpsetAddress>> From<T> for IpsetEntry {
+    fn from(value: T) -> Self {
+        Self {
+            nomatch: false,
+            address: value.into(),
+            comment: None,
+        }
+    }
+}
+
+impl FromStr for IpsetEntry {
+    type Err = Error;
+
+    fn from_str(line: &str) -> Result<Self, Error> {
+        let line = line.trim_start();
+
+        let (nomatch, line) = match line.strip_prefix('!') {
+            Some(line) => (true, line),
+            None => (false, line),
+        };
+
+        let (address, line) =
+            match_non_whitespace(line.trim_start()).ok_or_else(|| format_err!("missing value"))?;
+
+        let address: IpsetAddress = address.parse()?;
+        let line = line.trim_start();
+
+        let comment = match line.strip_prefix('#') {
+            Some(comment) => Some(comment.trim().to_string()),
+            None if !line.is_empty() => bail!("trailing characters in ipset entry: {line:?}"),
+            None => None,
+        };
+
+        Ok(Self {
+            nomatch,
+            address,
+            comment,
+        })
+    }
+}
+
+#[derive(Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct Ipfilter<'a> {
+    index: i64,
+    ipset: &'a Ipset,
+}
+
+impl Ipfilter<'_> {
+    pub fn index(&self) -> i64 {
+        self.index
+    }
+
+    pub fn ipset(&self) -> &Ipset {
+        self.ipset
+    }
+
+    pub fn name_for_index(index: i64) -> String {
+        format!("ipfilter-net{index}")
+    }
+}
+
+#[derive(Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct Ipset {
+    pub name: IpsetName,
+    set: Vec<IpsetEntry>,
+    pub comment: Option<String>,
+}
+
+impl Ipset {
+    pub const fn new(name: IpsetName) -> Self {
+        Self {
+            name,
+            set: Vec::new(),
+            comment: None,
+        }
+    }
+
+    pub fn name(&self) -> &IpsetName {
+        &self.name
+    }
+
+    pub fn from_parts(scope: IpsetScope, name: impl Into<String>) -> Self {
+        Self::new(IpsetName::new(scope, name))
+    }
+
+    pub(crate) fn parse_entry(&mut self, line: &str) -> Result<(), Error> {
+        self.set.push(line.parse()?);
+        Ok(())
+    }
+
+    pub fn ipfilter(&self) -> Option<Ipfilter> {
+        if self.name.scope() != IpsetScope::Guest {
+            return None;
+        }
+
+        let name = self.name.name();
+
+        if let Some(key) = name.strip_prefix("ipfilter-") {
+            let id = NetworkConfig::index_from_net_key(key);
+
+            if let Ok(id) = id {
+                return Some(Ipfilter {
+                    index: id,
+                    ipset: self,
+                });
+            }
+        }
+
+        None
+    }
+}
+
+impl Deref for Ipset {
+    type Target = Vec<IpsetEntry>;
+
+    #[inline]
+    fn deref(&self) -> &Self::Target {
+        &self.set
+    }
+}
+
+impl DerefMut for Ipset {
+    #[inline]
+    fn deref_mut(&mut self) -> &mut Vec<IpsetEntry> {
+        &mut self.set
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_parse_ipset_name() {
+        for test_case in [
+            ("+dc/proxmox-123", IpsetScope::Datacenter, "proxmox-123"),
+            ("+guest/proxmox_123", IpsetScope::Guest, "proxmox_123"),
+        ] {
+            let ipset_name = test_case.0.parse::<IpsetName>().expect("valid ipset name");
+
+            assert_eq!(
+                ipset_name,
+                IpsetName {
+                    scope: test_case.1,
+                    name: test_case.2.to_string(),
+                }
+            )
+        }
+
+        for name in ["+dc/", "+guests/proxmox_123", "guest/proxmox_123"] {
+            name.parse::<IpsetName>().expect_err("invalid ipset name");
+        }
+    }
+
+    #[test]
+    fn test_parse_ipset_address() {
+        let mut ipset_address = "10.0.0.1"
+            .parse::<IpsetAddress>()
+            .expect("valid ipset address");
+        assert!(matches!(ipset_address, IpsetAddress::Cidr(Cidr::Ipv4(..))));
+
+        ipset_address = "fe80::1/64"
+            .parse::<IpsetAddress>()
+            .expect("valid ipset address");
+        assert!(matches!(ipset_address, IpsetAddress::Cidr(Cidr::Ipv6(..))));
+
+        ipset_address = "dc/proxmox-123"
+            .parse::<IpsetAddress>()
+            .expect("valid ipset address");
+        assert!(matches!(ipset_address, IpsetAddress::Alias(..)));
+
+        ipset_address = "guest/proxmox_123"
+            .parse::<IpsetAddress>()
+            .expect("valid ipset address");
+        assert!(matches!(ipset_address, IpsetAddress::Alias(..)));
+    }
+
+    #[test]
+    fn test_ipfilter() {
+        let mut ipset = Ipset::from_parts(IpsetScope::Guest, "ipfilter-net0");
+        ipset.ipfilter().expect("is an ipfilter");
+
+        ipset = Ipset::from_parts(IpsetScope::Guest, "ipfilter-qwe");
+        assert!(ipset.ipfilter().is_none());
+
+        ipset = Ipset::from_parts(IpsetScope::Guest, "proxmox");
+        assert!(ipset.ipfilter().is_none());
+
+        ipset = Ipset::from_parts(IpsetScope::Datacenter, "ipfilter-net0");
+        assert!(ipset.ipfilter().is_none());
+    }
+
+    #[test]
+    fn test_parse_ipset_entry() {
+        let mut entry = "!10.0.0.1 # qweqweasd"
+            .parse::<IpsetEntry>()
+            .expect("valid ipset entry");
+
+        assert_eq!(
+            entry,
+            IpsetEntry {
+                nomatch: true,
+                comment: Some("qweqweasd".to_string()),
+                address: IpsetAddress::Cidr(Cidr::new_v4([10, 0, 0, 1], 32).unwrap())
+            }
+        );
+
+        entry = "fe80::1/48"
+            .parse::<IpsetEntry>()
+            .expect("valid ipset entry");
+
+        assert_eq!(
+            entry,
+            IpsetEntry {
+                nomatch: false,
+                comment: None,
+                address: IpsetAddress::Cidr(
+                    Cidr::new_v6([0xFE80, 0, 0, 0, 0, 0, 0, 1], 48).unwrap()
+                )
+            }
+        )
+    }
+}
diff --git a/proxmox-ve-config/src/firewall/types/mod.rs b/proxmox-ve-config/src/firewall/types/mod.rs
index 69b69f4..5833787 100644
--- a/proxmox-ve-config/src/firewall/types/mod.rs
+++ b/proxmox-ve-config/src/firewall/types/mod.rs
@@ -1,7 +1,9 @@
 pub mod address;
 pub mod alias;
+pub mod ipset;
 pub mod log;
 pub mod port;
 
 pub use address::Cidr;
 pub use alias::Alias;
+pub use ipset::Ipset;
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 09/39] config: firewall: add types for rules
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (7 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 08/39] config: firewall: add types for ipsets Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 10/39] config: firewall: add types for security groups Stefan Hanreich
                   ` (30 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

Additionally we implement FromStr for all rule types and parts, which
can be used for parsing firewall config rules. Initial rule parsing
works by parsing the different options into a HashMap and only then
de-serializing a struct from the parsed options.

This intermediate step makes rule parsing a lot easier, since we can
reuse the deserialization logic from serde. Also, we can split the
parsing/deserialization logic from the validation logic.

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/src/firewall/parse.rs       | 185 ++++
 proxmox-ve-config/src/firewall/types/mod.rs   |   3 +
 proxmox-ve-config/src/firewall/types/rule.rs  | 412 ++++++++
 .../src/firewall/types/rule_match.rs          | 965 ++++++++++++++++++
 4 files changed, 1565 insertions(+)
 create mode 100644 proxmox-ve-config/src/firewall/types/rule.rs
 create mode 100644 proxmox-ve-config/src/firewall/types/rule_match.rs

diff --git a/proxmox-ve-config/src/firewall/parse.rs b/proxmox-ve-config/src/firewall/parse.rs
index b02f98d..e2ce463 100644
--- a/proxmox-ve-config/src/firewall/parse.rs
+++ b/proxmox-ve-config/src/firewall/parse.rs
@@ -1,3 +1,5 @@
+use std::fmt;
+
 use anyhow::{bail, format_err, Error};
 
 /// Parses out a "name" which can be alphanumeric and include dashes.
@@ -91,3 +93,186 @@ pub fn parse_bool(value: &str) -> Result<bool, Error> {
         },
     )
 }
+
+/// `&str` deserializer which also accepts an `Option`.
+///
+/// Serde's `StringDeserializer` does not.
+#[derive(Clone, Copy, Debug)]
+pub struct SomeStrDeserializer<'a, E>(serde::de::value::StrDeserializer<'a, E>);
+
+impl<'de, 'a, E> serde::de::Deserializer<'de> for SomeStrDeserializer<'a, E>
+where
+    E: serde::de::Error,
+{
+    type Error = E;
+
+    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: serde::de::Visitor<'de>,
+    {
+        self.0.deserialize_any(visitor)
+    }
+
+    fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: serde::de::Visitor<'de>,
+    {
+        visitor.visit_some(self.0)
+    }
+
+    fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: serde::de::Visitor<'de>,
+    {
+        self.0.deserialize_str(visitor)
+    }
+
+    fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: serde::de::Visitor<'de>,
+    {
+        self.0.deserialize_string(visitor)
+    }
+
+    fn deserialize_enum<V>(
+        self,
+        _name: &str,
+        _variants: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value, Self::Error>
+    where
+        V: serde::de::Visitor<'de>,
+    {
+        visitor.visit_enum(self.0)
+    }
+
+    serde::forward_to_deserialize_any! {
+        bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char
+        bytes byte_buf unit unit_struct newtype_struct seq tuple
+        tuple_struct map struct identifier ignored_any
+    }
+}
+
+/// `&str` wrapper which implements `IntoDeserializer` via `SomeStrDeserializer`.
+#[derive(Clone, Debug)]
+pub struct SomeStr<'a>(pub &'a str);
+
+impl<'a> From<&'a str> for SomeStr<'a> {
+    fn from(s: &'a str) -> Self {
+        Self(s)
+    }
+}
+
+impl<'de, 'a, E> serde::de::IntoDeserializer<'de, E> for SomeStr<'a>
+where
+    E: serde::de::Error,
+{
+    type Deserializer = SomeStrDeserializer<'a, E>;
+
+    fn into_deserializer(self) -> Self::Deserializer {
+        SomeStrDeserializer(self.0.into_deserializer())
+    }
+}
+
+/// `String` deserializer which also accepts an `Option`.
+///
+/// Serde's `StringDeserializer` does not.
+#[derive(Clone, Debug)]
+pub struct SomeStringDeserializer<E>(serde::de::value::StringDeserializer<E>);
+
+impl<'de, E> serde::de::Deserializer<'de> for SomeStringDeserializer<E>
+where
+    E: serde::de::Error,
+{
+    type Error = E;
+
+    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: serde::de::Visitor<'de>,
+    {
+        self.0.deserialize_any(visitor)
+    }
+
+    fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: serde::de::Visitor<'de>,
+    {
+        visitor.visit_some(self.0)
+    }
+
+    fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: serde::de::Visitor<'de>,
+    {
+        self.0.deserialize_str(visitor)
+    }
+
+    fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: serde::de::Visitor<'de>,
+    {
+        self.0.deserialize_string(visitor)
+    }
+
+    fn deserialize_enum<V>(
+        self,
+        _name: &str,
+        _variants: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value, Self::Error>
+    where
+        V: serde::de::Visitor<'de>,
+    {
+        visitor.visit_enum(self.0)
+    }
+
+    serde::forward_to_deserialize_any! {
+        bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char
+        bytes byte_buf unit unit_struct newtype_struct seq tuple
+        tuple_struct map struct identifier ignored_any
+    }
+}
+
+/// `&str` wrapper which implements `IntoDeserializer` via `SomeStringDeserializer`.
+#[derive(Clone, Debug)]
+pub struct SomeString(pub String);
+
+impl From<&str> for SomeString {
+    fn from(s: &str) -> Self {
+        Self::from(s.to_string())
+    }
+}
+
+impl From<String> for SomeString {
+    fn from(s: String) -> Self {
+        Self(s)
+    }
+}
+
+impl<'de, E> serde::de::IntoDeserializer<'de, E> for SomeString
+where
+    E: serde::de::Error,
+{
+    type Deserializer = SomeStringDeserializer<E>;
+
+    fn into_deserializer(self) -> Self::Deserializer {
+        SomeStringDeserializer(self.0.into_deserializer())
+    }
+}
+
+#[derive(Debug)]
+pub struct SerdeStringError(String);
+
+impl std::error::Error for SerdeStringError {}
+
+impl fmt::Display for SerdeStringError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.write_str(&self.0)
+    }
+}
+
+impl serde::de::Error for SerdeStringError {
+    fn custom<T: fmt::Display>(msg: T) -> Self {
+        Self(msg.to_string())
+    }
+}
diff --git a/proxmox-ve-config/src/firewall/types/mod.rs b/proxmox-ve-config/src/firewall/types/mod.rs
index 5833787..b4a6b12 100644
--- a/proxmox-ve-config/src/firewall/types/mod.rs
+++ b/proxmox-ve-config/src/firewall/types/mod.rs
@@ -3,7 +3,10 @@ pub mod alias;
 pub mod ipset;
 pub mod log;
 pub mod port;
+pub mod rule;
+pub mod rule_match;
 
 pub use address::Cidr;
 pub use alias::Alias;
 pub use ipset::Ipset;
+pub use rule::Rule;
diff --git a/proxmox-ve-config/src/firewall/types/rule.rs b/proxmox-ve-config/src/firewall/types/rule.rs
new file mode 100644
index 0000000..20deb3a
--- /dev/null
+++ b/proxmox-ve-config/src/firewall/types/rule.rs
@@ -0,0 +1,412 @@
+use core::fmt::Display;
+use std::fmt;
+use std::str::FromStr;
+
+use anyhow::{bail, ensure, format_err, Error};
+
+use crate::firewall::parse::match_name;
+use crate::firewall::types::rule_match::RuleMatch;
+use crate::firewall::types::rule_match::RuleOptions;
+
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
+pub enum Direction {
+    #[default]
+    In,
+    Out,
+}
+
+impl std::str::FromStr for Direction {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Error> {
+        for (name, dir) in [("IN", Direction::In), ("OUT", Direction::Out)] {
+            if s.eq_ignore_ascii_case(name) {
+                return Ok(dir);
+            }
+        }
+
+        bail!("invalid direction: {s:?}, expect 'IN' or 'OUT'");
+    }
+}
+
+serde_plain::derive_deserialize_from_fromstr!(Direction, "valid packet direction");
+
+impl fmt::Display for Direction {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Direction::In => f.write_str("in"),
+            Direction::Out => f.write_str("out"),
+        }
+    }
+}
+
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
+pub enum Verdict {
+    Accept,
+    Reject,
+    #[default]
+    Drop,
+}
+
+impl std::str::FromStr for Verdict {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Error> {
+        for (name, verdict) in [
+            ("ACCEPT", Verdict::Accept),
+            ("REJECT", Verdict::Reject),
+            ("DROP", Verdict::Drop),
+        ] {
+            if s.eq_ignore_ascii_case(name) {
+                return Ok(verdict);
+            }
+        }
+        bail!("invalid verdict {s:?}, expected one of 'ACCEPT', 'REJECT' or 'DROP'");
+    }
+}
+
+impl Display for Verdict {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let string = match self {
+            Verdict::Accept => "ACCEPT",
+            Verdict::Drop => "DROP",
+            Verdict::Reject => "REJECT",
+        };
+
+        write!(f, "{string}")
+    }
+}
+
+serde_plain::derive_deserialize_from_fromstr!(Verdict, "valid verdict");
+
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct Rule {
+    pub(crate) disabled: bool,
+    pub(crate) kind: Kind,
+    pub(crate) comment: Option<String>,
+}
+
+impl std::ops::Deref for Rule {
+    type Target = Kind;
+
+    fn deref(&self) -> &Self::Target {
+        &self.kind
+    }
+}
+
+impl std::ops::DerefMut for Rule {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.kind
+    }
+}
+
+impl FromStr for Rule {
+    type Err = Error;
+
+    fn from_str(input: &str) -> Result<Self, Self::Err> {
+        if input.contains(['\n', '\r']) {
+            bail!("rule must not contain any newlines!");
+        }
+
+        let (line, comment) = match input.rsplit_once('#') {
+            Some((line, comment)) if !comment.is_empty() => (line.trim(), Some(comment.trim())),
+            _ => (input.trim(), None),
+        };
+
+        let (disabled, line) = match line.strip_prefix('|') {
+            Some(line) => (true, line.trim_start()),
+            None => (false, line),
+        };
+
+        // todo: case insensitive?
+        let kind = if line.starts_with("GROUP") {
+            Kind::from(line.parse::<RuleGroup>()?)
+        } else {
+            Kind::from(line.parse::<RuleMatch>()?)
+        };
+
+        Ok(Self {
+            disabled,
+            comment: comment.map(str::to_string),
+            kind,
+        })
+    }
+}
+
+impl Rule {
+    pub fn iface(&self) -> Option<&str> {
+        match &self.kind {
+            Kind::Group(group) => group.iface(),
+            Kind::Match(rule) => rule.iface(),
+        }
+    }
+
+    pub fn disabled(&self) -> bool {
+        self.disabled
+    }
+
+    pub fn kind(&self) -> &Kind {
+        &self.kind
+    }
+
+    pub fn comment(&self) -> Option<&str> {
+        self.comment.as_deref()
+    }
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub enum Kind {
+    Group(RuleGroup),
+    Match(RuleMatch),
+}
+
+impl Kind {
+    pub fn is_group(&self) -> bool {
+        matches!(self, Kind::Group(_))
+    }
+
+    pub fn is_match(&self) -> bool {
+        matches!(self, Kind::Match(_))
+    }
+}
+
+impl From<RuleGroup> for Kind {
+    fn from(value: RuleGroup) -> Self {
+        Kind::Group(value)
+    }
+}
+
+impl From<RuleMatch> for Kind {
+    fn from(value: RuleMatch) -> Self {
+        Kind::Match(value)
+    }
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct RuleGroup {
+    pub(crate) group: String,
+    pub(crate) iface: Option<String>,
+}
+
+impl RuleGroup {
+    pub(crate) fn from_options(group: String, options: RuleOptions) -> Result<Self, Error> {
+        ensure!(
+            options.proto.is_none()
+                && options.dport.is_none()
+                && options.sport.is_none()
+                && options.dest.is_none()
+                && options.source.is_none()
+                && options.log.is_none()
+                && options.icmp_type.is_none(),
+            "only interface parameter is permitted for group rules"
+        );
+
+        Ok(Self {
+            group,
+            iface: options.iface,
+        })
+    }
+
+    pub fn group(&self) -> &str {
+        &self.group
+    }
+
+    pub fn iface(&self) -> Option<&str> {
+        self.iface.as_deref()
+    }
+}
+
+impl FromStr for RuleGroup {
+    type Err = Error;
+
+    fn from_str(input: &str) -> Result<Self, Self::Err> {
+        let (keyword, rest) = match_name(input)
+            .ok_or_else(|| format_err!("expected a leading keyword in rule group"))?;
+
+        if !keyword.eq_ignore_ascii_case("group") {
+            bail!("Expected keyword GROUP")
+        }
+
+        let (name, rest) =
+            match_name(rest.trim()).ok_or_else(|| format_err!("expected a name for rule group"))?;
+
+        let options = rest.trim_start().parse()?;
+
+        Self::from_options(name.to_string(), options)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::firewall::types::{
+        address::{IpEntry, IpList},
+        alias::{AliasName, AliasScope},
+        ipset::{IpsetName, IpsetScope},
+        log::LogLevel,
+        rule_match::{Icmp, IcmpCode, IpAddrMatch, IpMatch, Ports, Protocol, Udp},
+        Cidr,
+    };
+
+    use super::*;
+
+    #[test]
+    fn test_parse_rule() {
+        let mut rule: Rule = "|GROUP tgr -i eth0 # acomm".parse().expect("valid rule");
+
+        assert_eq!(
+            rule,
+            Rule {
+                disabled: true,
+                comment: Some("acomm".to_string()),
+                kind: Kind::Group(RuleGroup {
+                    group: "tgr".to_string(),
+                    iface: Some("eth0".to_string()),
+                }),
+            },
+        );
+
+        rule = "IN ACCEPT -p udp -dport 33 -sport 22 -log warning"
+            .parse()
+            .expect("valid rule");
+
+        assert_eq!(
+            rule,
+            Rule {
+                disabled: false,
+                comment: None,
+                kind: Kind::Match(RuleMatch {
+                    dir: Direction::In,
+                    verdict: Verdict::Accept,
+                    proto: Some(Udp::new(Ports::from_u16(22, 33)).into()),
+                    log: Some(LogLevel::Warning),
+                    ..Default::default()
+                }),
+            }
+        );
+
+        rule = "IN ACCEPT --proto udp -i eth0".parse().expect("valid rule");
+
+        assert_eq!(
+            rule,
+            Rule {
+                disabled: false,
+                comment: None,
+                kind: Kind::Match(RuleMatch {
+                    dir: Direction::In,
+                    verdict: Verdict::Accept,
+                    proto: Some(Udp::new(Ports::new(None, None)).into()),
+                    iface: Some("eth0".to_string()),
+                    ..Default::default()
+                }),
+            }
+        );
+
+        rule = " OUT DROP \
+          -source 10.0.0.0/24 -dest 20.0.0.0-20.255.255.255,192.168.0.0/16 \
+          -p icmp -log nolog -icmp-type port-unreachable "
+            .parse()
+            .expect("valid rule");
+
+        assert_eq!(
+            rule,
+            Rule {
+                disabled: false,
+                comment: None,
+                kind: Kind::Match(RuleMatch {
+                    dir: Direction::Out,
+                    verdict: Verdict::Drop,
+                    ip: IpMatch::new(
+                        IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 24).unwrap())),
+                        IpAddrMatch::Ip(
+                            IpList::new(vec![
+                                IpEntry::Range([20, 0, 0, 0].into(), [20, 255, 255, 255].into()),
+                                IpEntry::Cidr(Cidr::new_v4([192, 168, 0, 0], 16).unwrap()),
+                            ])
+                            .unwrap()
+                        ),
+                    )
+                    .ok(),
+                    proto: Some(Protocol::Icmp(Icmp::new_code(IcmpCode::Named(
+                        "port-unreachable"
+                    )))),
+                    log: Some(LogLevel::Nolog),
+                    ..Default::default()
+                }),
+            }
+        );
+
+        rule = "IN BGP(ACCEPT) --log crit --iface eth0"
+            .parse()
+            .expect("valid rule");
+
+        assert_eq!(
+            rule,
+            Rule {
+                disabled: false,
+                comment: None,
+                kind: Kind::Match(RuleMatch {
+                    dir: Direction::In,
+                    verdict: Verdict::Accept,
+                    log: Some(LogLevel::Critical),
+                    fw_macro: Some("BGP".to_string()),
+                    iface: Some("eth0".to_string()),
+                    ..Default::default()
+                }),
+            }
+        );
+
+        rule = "IN ACCEPT --source dc/test --dest +dc/test"
+            .parse()
+            .expect("valid rule");
+
+        assert_eq!(
+            rule,
+            Rule {
+                disabled: false,
+                comment: None,
+                kind: Kind::Match(RuleMatch {
+                    dir: Direction::In,
+                    verdict: Verdict::Accept,
+                    ip: Some(
+                        IpMatch::new(
+                            IpAddrMatch::Alias(AliasName::new(AliasScope::Datacenter, "test")),
+                            IpAddrMatch::Set(IpsetName::new(IpsetScope::Datacenter, "test"),),
+                        )
+                        .unwrap()
+                    ),
+                    ..Default::default()
+                }),
+            }
+        );
+
+        rule = "IN REJECT".parse().expect("valid rule");
+
+        assert_eq!(
+            rule,
+            Rule {
+                disabled: false,
+                comment: None,
+                kind: Kind::Match(RuleMatch {
+                    dir: Direction::In,
+                    verdict: Verdict::Reject,
+                    ..Default::default()
+                }),
+            }
+        );
+
+        "IN DROP ---log crit"
+            .parse::<Rule>()
+            .expect_err("too many dashes in option");
+
+        "IN DROP --log --iface eth0"
+            .parse::<Rule>()
+            .expect_err("no value for option");
+
+        "IN DROP --log crit --iface"
+            .parse::<Rule>()
+            .expect_err("no value for option");
+    }
+}
diff --git a/proxmox-ve-config/src/firewall/types/rule_match.rs b/proxmox-ve-config/src/firewall/types/rule_match.rs
new file mode 100644
index 0000000..948b426
--- /dev/null
+++ b/proxmox-ve-config/src/firewall/types/rule_match.rs
@@ -0,0 +1,965 @@
+use std::collections::HashMap;
+use std::fmt;
+use std::str::FromStr;
+
+use serde::Deserialize;
+
+use anyhow::{bail, format_err, Error};
+use serde::de::IntoDeserializer;
+
+use proxmox_sortable_macro::sortable;
+
+use crate::firewall::parse::{match_name, match_non_whitespace, SomeStr};
+use crate::firewall::types::address::{Family, IpList};
+use crate::firewall::types::alias::AliasName;
+use crate::firewall::types::ipset::IpsetName;
+use crate::firewall::types::log::LogLevel;
+use crate::firewall::types::port::PortList;
+use crate::firewall::types::rule::{Direction, Verdict};
+
+#[derive(Clone, Debug, Default, Deserialize)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+#[serde(deny_unknown_fields, rename_all = "kebab-case")]
+pub(crate) struct RuleOptions {
+    #[serde(alias = "p")]
+    pub(crate) proto: Option<String>,
+
+    pub(crate) dport: Option<String>,
+    pub(crate) sport: Option<String>,
+
+    pub(crate) dest: Option<String>,
+    pub(crate) source: Option<String>,
+
+    #[serde(alias = "i")]
+    pub(crate) iface: Option<String>,
+
+    pub(crate) log: Option<LogLevel>,
+    pub(crate) icmp_type: Option<String>,
+}
+
+impl FromStr for RuleOptions {
+    type Err = Error;
+
+    fn from_str(mut line: &str) -> Result<Self, Self::Err> {
+        let mut options = HashMap::new();
+
+        loop {
+            line = line.trim_start();
+
+            if line.is_empty() {
+                break;
+            }
+
+            line = line
+                .strip_prefix('-')
+                .ok_or_else(|| format_err!("expected an option starting with '-'"))?;
+
+            // second dash is optional
+            line = line.strip_prefix('-').unwrap_or(line);
+
+            let param;
+            (param, line) = match_name(line)
+                .ok_or_else(|| format_err!("expected a parameter name after '-'"))?;
+
+            let value;
+            (value, line) = match_non_whitespace(line.trim_start())
+                .ok_or_else(|| format_err!("expected a value for {param:?}"))?;
+
+            if options.insert(param, SomeStr(value)).is_some() {
+                bail!("duplicate option in rule: {param}")
+            }
+        }
+
+        Ok(RuleOptions::deserialize(IntoDeserializer::<
+            '_,
+            crate::firewall::parse::SerdeStringError,
+        >::into_deserializer(
+            options
+        ))?)
+    }
+}
+
+#[derive(Clone, Debug, Default)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct RuleMatch {
+    pub(crate) dir: Direction,
+    pub(crate) verdict: Verdict,
+    pub(crate) fw_macro: Option<String>,
+
+    pub(crate) iface: Option<String>,
+    pub(crate) log: Option<LogLevel>,
+    pub(crate) ip: Option<IpMatch>,
+    pub(crate) proto: Option<Protocol>,
+}
+
+impl RuleMatch {
+    pub(crate) fn from_options(
+        dir: Direction,
+        verdict: Verdict,
+        fw_macro: impl Into<Option<String>>,
+        options: RuleOptions,
+    ) -> Result<Self, Error> {
+        if options.dport.is_some() && options.icmp_type.is_some() {
+            bail!("dport and icmp-type are mutually exclusive");
+        }
+
+        let ip = IpMatch::from_options(&options)?;
+        let proto = Protocol::from_options(&options)?;
+
+        // todo: check protocol & IP Version compatibility
+
+        Ok(Self {
+            dir,
+            verdict,
+            fw_macro: fw_macro.into(),
+            iface: options.iface,
+            log: options.log,
+            ip,
+            proto,
+        })
+    }
+
+    pub fn direction(&self) -> Direction {
+        self.dir
+    }
+
+    pub fn iface(&self) -> Option<&str> {
+        self.iface.as_deref()
+    }
+
+    pub fn verdict(&self) -> Verdict {
+        self.verdict
+    }
+
+    pub fn fw_macro(&self) -> Option<&str> {
+        self.fw_macro.as_deref()
+    }
+
+    pub fn log(&self) -> Option<LogLevel> {
+        self.log
+    }
+
+    pub fn ip(&self) -> Option<&IpMatch> {
+        self.ip.as_ref()
+    }
+
+    pub fn proto(&self) -> Option<&Protocol> {
+        self.proto.as_ref()
+    }
+}
+
+/// Returns `(Macro name, Verdict, RestOfTheLine)`.
+fn parse_action(line: &str) -> Result<(Option<&str>, Verdict, &str), Error> {
+    let (verdict, line) =
+        match_name(line).ok_or_else(|| format_err!("expected a verdict or macro name"))?;
+
+    Ok(if let Some(line) = line.strip_prefix('(') {
+        // <macro>(<verdict>)
+
+        let macro_name = verdict;
+        let (verdict, line) = match_name(line).ok_or_else(|| format_err!("expected a verdict"))?;
+        let line = line
+            .strip_prefix(')')
+            .ok_or_else(|| format_err!("expected closing ')' after verdict"))?;
+
+        let verdict: Verdict = verdict.parse()?;
+
+        (Some(macro_name), verdict, line.trim_start())
+    } else {
+        (None, verdict.parse()?, line.trim_start())
+    })
+}
+
+impl FromStr for RuleMatch {
+    type Err = Error;
+
+    fn from_str(line: &str) -> Result<Self, Self::Err> {
+        let (dir, rest) = match_name(line).ok_or_else(|| format_err!("expected a direction"))?;
+
+        let direction: Direction = dir.parse()?;
+
+        let (fw_macro, verdict, rest) = parse_action(rest.trim_start())?;
+
+        let options: RuleOptions = rest.trim_start().parse()?;
+
+        Self::from_options(direction, verdict, fw_macro.map(str::to_string), options)
+    }
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct IpMatch {
+    pub(crate) src: Option<IpAddrMatch>,
+    pub(crate) dst: Option<IpAddrMatch>,
+}
+
+impl IpMatch {
+    pub fn new(
+        src: impl Into<Option<IpAddrMatch>>,
+        dst: impl Into<Option<IpAddrMatch>>,
+    ) -> Result<Self, Error> {
+        let source = src.into();
+        let dest = dst.into();
+
+        if source.is_none() && dest.is_none() {
+            bail!("either src or dst must be set")
+        }
+
+        if let (Some(IpAddrMatch::Ip(src)), Some(IpAddrMatch::Ip(dst))) = (&source, &dest) {
+            if src.family() != dst.family() {
+                bail!("src and dst family must be equal")
+            }
+        }
+
+        let ip_match = Self {
+            src: source,
+            dst: dest,
+        };
+
+        Ok(ip_match)
+    }
+
+    fn from_options(options: &RuleOptions) -> Result<Option<Self>, Error> {
+        let src = options
+            .source
+            .as_ref()
+            .map(|elem| elem.parse::<IpAddrMatch>())
+            .transpose()?;
+
+        let dst = options
+            .dest
+            .as_ref()
+            .map(|elem| elem.parse::<IpAddrMatch>())
+            .transpose()?;
+
+        if src.is_some() || dst.is_some() {
+            Ok(Some(IpMatch::new(src, dst)?))
+        } else {
+            Ok(None)
+        }
+    }
+
+    pub fn src(&self) -> Option<&IpAddrMatch> {
+        self.src.as_ref()
+    }
+
+    pub fn dst(&self) -> Option<&IpAddrMatch> {
+        self.dst.as_ref()
+    }
+}
+
+#[derive(Clone, Debug, Deserialize)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub enum IpAddrMatch {
+    Ip(IpList),
+    Set(IpsetName),
+    Alias(AliasName),
+}
+
+impl IpAddrMatch {
+    pub fn family(&self) -> Option<Family> {
+        if let IpAddrMatch::Ip(list) = self {
+            return Some(list.family());
+        }
+
+        None
+    }
+}
+
+impl FromStr for IpAddrMatch {
+    type Err = Error;
+
+    fn from_str(value: &str) -> Result<Self, Error> {
+        if value.is_empty() {
+            bail!("empty IP specification");
+        }
+
+        if let Ok(ip_list) = value.parse() {
+            return Ok(IpAddrMatch::Ip(ip_list));
+        }
+
+        if let Ok(ipset) = value.parse() {
+            return Ok(IpAddrMatch::Set(ipset));
+        }
+
+        if let Ok(name) = value.parse() {
+            return Ok(IpAddrMatch::Alias(name));
+        }
+
+        bail!("invalid IP specification: {value}")
+    }
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub enum Protocol {
+    Dccp(Ports),
+    Sctp(Sctp),
+    Tcp(Tcp),
+    Udp(Udp),
+    UdpLite(Ports),
+    Icmp(Icmp),
+    Icmpv6(Icmpv6),
+    Named(String),
+    Numeric(u8),
+}
+
+impl Protocol {
+    pub(crate) fn from_options(options: &RuleOptions) -> Result<Option<Self>, Error> {
+        let proto = match options.proto.as_deref() {
+            Some(p) => p,
+            None => return Ok(None),
+        };
+
+        Ok(Some(match proto {
+            "dccp" | "33" => Protocol::Dccp(Ports::from_options(options)?),
+            "sctp" | "132" => Protocol::Sctp(Sctp::from_options(options)?),
+            "tcp" | "6" => Protocol::Tcp(Tcp::from_options(options)?),
+            "udp" | "17" => Protocol::Udp(Udp::from_options(options)?),
+            "udplite" | "136" => Protocol::UdpLite(Ports::from_options(options)?),
+            "icmp" | "1" => Protocol::Icmp(Icmp::from_options(options)?),
+            "ipv6-icmp" | "icmpv6" | "58" => Protocol::Icmpv6(Icmpv6::from_options(options)?),
+            other => match other.parse::<u8>() {
+                Ok(num) => Protocol::Numeric(num),
+                Err(_) => Protocol::Named(other.to_string()),
+            },
+        }))
+    }
+
+    pub fn family(&self) -> Option<Family> {
+        match self {
+            Self::Icmp(_) => Some(Family::V4),
+            Self::Icmpv6(_) => Some(Family::V6),
+            _ => None,
+        }
+    }
+}
+
+#[derive(Clone, Debug, Default)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct Udp {
+    ports: Ports,
+}
+
+impl Udp {
+    fn from_options(options: &RuleOptions) -> Result<Self, Error> {
+        Ok(Self {
+            ports: Ports::from_options(options)?,
+        })
+    }
+
+    pub fn new(ports: Ports) -> Self {
+        Self { ports }
+    }
+
+    pub fn ports(&self) -> &Ports {
+        &self.ports
+    }
+}
+
+impl From<Udp> for Protocol {
+    fn from(value: Udp) -> Self {
+        Protocol::Udp(value)
+    }
+}
+
+#[derive(Clone, Debug, Default)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct Ports {
+    sport: Option<PortList>,
+    dport: Option<PortList>,
+}
+
+impl Ports {
+    pub fn new(sport: impl Into<Option<PortList>>, dport: impl Into<Option<PortList>>) -> Self {
+        Self {
+            sport: sport.into(),
+            dport: dport.into(),
+        }
+    }
+
+    fn from_options(options: &RuleOptions) -> Result<Self, Error> {
+        Ok(Self {
+            sport: options.sport.as_deref().map(|s| s.parse()).transpose()?,
+            dport: options.dport.as_deref().map(|s| s.parse()).transpose()?,
+        })
+    }
+
+    pub fn from_u16(sport: impl Into<Option<u16>>, dport: impl Into<Option<u16>>) -> Self {
+        Self::new(
+            sport.into().map(PortList::from),
+            dport.into().map(PortList::from),
+        )
+    }
+
+    pub fn sport(&self) -> Option<&PortList> {
+        self.sport.as_ref()
+    }
+
+    pub fn dport(&self) -> Option<&PortList> {
+        self.dport.as_ref()
+    }
+}
+
+#[derive(Clone, Debug, Default)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct Tcp {
+    ports: Ports,
+}
+
+impl Tcp {
+    pub fn new(ports: Ports) -> Self {
+        Self { ports }
+    }
+
+    fn from_options(options: &RuleOptions) -> Result<Self, Error> {
+        Ok(Self {
+            ports: Ports::from_options(options)?,
+        })
+    }
+
+    pub fn ports(&self) -> &Ports {
+        &self.ports
+    }
+}
+
+impl From<Tcp> for Protocol {
+    fn from(value: Tcp) -> Self {
+        Protocol::Tcp(value)
+    }
+}
+
+#[derive(Clone, Debug, Default)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct Sctp {
+    ports: Ports,
+}
+
+impl Sctp {
+    fn from_options(options: &RuleOptions) -> Result<Self, Error> {
+        Ok(Self {
+            ports: Ports::from_options(options)?,
+        })
+    }
+
+    pub fn ports(&self) -> &Ports {
+        &self.ports
+    }
+}
+
+#[derive(Clone, Debug, Default)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct Icmp {
+    ty: Option<IcmpType>,
+    code: Option<IcmpCode>,
+}
+
+impl Icmp {
+    pub fn new_ty(ty: IcmpType) -> Self {
+        Self {
+            ty: Some(ty),
+            ..Default::default()
+        }
+    }
+
+    pub fn new_code(code: IcmpCode) -> Self {
+        Self {
+            code: Some(code),
+            ..Default::default()
+        }
+    }
+
+    fn from_options(options: &RuleOptions) -> Result<Self, Error> {
+        if let Some(ty) = &options.icmp_type {
+            return ty.parse();
+        }
+
+        Ok(Self::default())
+    }
+
+    pub fn ty(&self) -> Option<&IcmpType> {
+        self.ty.as_ref()
+    }
+
+    pub fn code(&self) -> Option<&IcmpCode> {
+        self.code.as_ref()
+    }
+}
+
+impl FromStr for Icmp {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let mut this = Self::default();
+
+        if let Ok(ty) = s.parse() {
+            this.ty = Some(ty);
+            return Ok(this);
+        }
+
+        if let Ok(code) = s.parse() {
+            this.code = Some(code);
+            return Ok(this);
+        }
+
+        bail!("supplied string is neither a valid icmp type nor code");
+    }
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub enum IcmpType {
+    Numeric(u8),
+    Named(&'static str),
+}
+
+#[sortable]
+const ICMP_TYPES: [(&str, u8); 15] = sorted!([
+    ("address-mask-reply", 18),
+    ("address-mask-request", 17),
+    ("destination-unreachable", 3),
+    ("echo-reply", 0),
+    ("echo-request", 8),
+    ("info-reply", 16),
+    ("info-request", 15),
+    ("parameter-problem", 12),
+    ("redirect", 5),
+    ("router-advertisement", 9),
+    ("router-solicitation", 10),
+    ("source-quench", 4),
+    ("time-exceeded", 11),
+    ("timestamp-reply", 14),
+    ("timestamp-request", 13),
+]);
+
+impl std::str::FromStr for IcmpType {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Error> {
+        if let Ok(ty) = s.trim().parse::<u8>() {
+            return Ok(Self::Numeric(ty));
+        }
+
+        if let Ok(index) = ICMP_TYPES.binary_search_by(|v| v.0.cmp(s)) {
+            return Ok(Self::Named(ICMP_TYPES[index].0));
+        }
+
+        bail!("{s:?} is not a valid icmp type");
+    }
+}
+
+impl fmt::Display for IcmpType {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            IcmpType::Numeric(ty) => write!(f, "{ty}"),
+            IcmpType::Named(ty) => write!(f, "{ty}"),
+        }
+    }
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub enum IcmpCode {
+    Numeric(u8),
+    Named(&'static str),
+}
+
+#[sortable]
+const ICMP_CODES: [(&str, u8); 7] = sorted!([
+    ("admin-prohibited", 13),
+    ("host-prohibited", 10),
+    ("host-unreachable", 1),
+    ("net-prohibited", 9),
+    ("net-unreachable", 0),
+    ("port-unreachable", 3),
+    ("prot-unreachable", 2),
+]);
+
+impl std::str::FromStr for IcmpCode {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Error> {
+        if let Ok(code) = s.trim().parse::<u8>() {
+            return Ok(Self::Numeric(code));
+        }
+
+        if let Ok(index) = ICMP_CODES.binary_search_by(|v| v.0.cmp(s)) {
+            return Ok(Self::Named(ICMP_CODES[index].0));
+        }
+
+        bail!("{s:?} is not a valid icmp code");
+    }
+}
+
+impl fmt::Display for IcmpCode {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            IcmpCode::Numeric(code) => write!(f, "{code}"),
+            IcmpCode::Named(code) => write!(f, "{code}"),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Default)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct Icmpv6 {
+    pub ty: Option<Icmpv6Type>,
+    pub code: Option<Icmpv6Code>,
+}
+
+impl Icmpv6 {
+    pub fn new_ty(ty: Icmpv6Type) -> Self {
+        Self {
+            ty: Some(ty),
+            ..Default::default()
+        }
+    }
+
+    pub fn new_code(code: Icmpv6Code) -> Self {
+        Self {
+            code: Some(code),
+            ..Default::default()
+        }
+    }
+
+    fn from_options(options: &RuleOptions) -> Result<Self, Error> {
+        if let Some(ty) = &options.icmp_type {
+            return ty.parse();
+        }
+
+        Ok(Self::default())
+    }
+
+    pub fn ty(&self) -> Option<&Icmpv6Type> {
+        self.ty.as_ref()
+    }
+
+    pub fn code(&self) -> Option<&Icmpv6Code> {
+        self.code.as_ref()
+    }
+}
+
+impl FromStr for Icmpv6 {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let mut this = Self::default();
+
+        if let Ok(ty) = s.parse() {
+            this.ty = Some(ty);
+            return Ok(this);
+        }
+
+        if let Ok(code) = s.parse() {
+            this.code = Some(code);
+            return Ok(this);
+        }
+
+        bail!("supplied string is neither a valid icmpv6 type nor code");
+    }
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub enum Icmpv6Type {
+    Numeric(u8),
+    Named(&'static str),
+}
+
+#[sortable]
+const ICMPV6_TYPES: [(&str, u8); 19] = sorted!([
+    ("destination-unreachable", 1),
+    ("echo-reply", 129),
+    ("echo-request", 128),
+    ("ind-neighbor-advert", 142),
+    ("ind-neighbor-solicit", 141),
+    ("mld-listener-done", 132),
+    ("mld-listener-query", 130),
+    ("mld-listener-reduction", 132),
+    ("mld-listener-report", 131),
+    ("mld2-listener-report", 143),
+    ("nd-neighbor-advert", 136),
+    ("nd-neighbor-solicit", 135),
+    ("nd-redirect", 137),
+    ("nd-router-advert", 134),
+    ("nd-router-solicit", 133),
+    ("packet-too-big", 2),
+    ("parameter-problem", 4),
+    ("router-renumbering", 138),
+    ("time-exceeded", 3),
+]);
+
+impl std::str::FromStr for Icmpv6Type {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Error> {
+        if let Ok(ty) = s.trim().parse::<u8>() {
+            return Ok(Self::Numeric(ty));
+        }
+
+        if let Ok(index) = ICMPV6_TYPES.binary_search_by(|v| v.0.cmp(s)) {
+            return Ok(Self::Named(ICMPV6_TYPES[index].0));
+        }
+
+        bail!("{s:?} is not a valid icmpv6 type");
+    }
+}
+
+impl fmt::Display for Icmpv6Type {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Icmpv6Type::Numeric(ty) => write!(f, "{ty}"),
+            Icmpv6Type::Named(ty) => write!(f, "{ty}"),
+        }
+    }
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub enum Icmpv6Code {
+    Numeric(u8),
+    Named(&'static str),
+}
+
+#[sortable]
+const ICMPV6_CODES: [(&str, u8); 6] = sorted!([
+    ("addr-unreachable", 3),
+    ("admin-prohibited", 1),
+    ("no-route", 0),
+    ("policy-fail", 5),
+    ("port-unreachable", 4),
+    ("reject-route", 6),
+]);
+
+impl std::str::FromStr for Icmpv6Code {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Error> {
+        if let Ok(code) = s.trim().parse::<u8>() {
+            return Ok(Self::Numeric(code));
+        }
+
+        if let Ok(index) = ICMPV6_CODES.binary_search_by(|v| v.0.cmp(s)) {
+            return Ok(Self::Named(ICMPV6_CODES[index].0));
+        }
+
+        bail!("{s:?} is not a valid icmpv6 code");
+    }
+}
+
+impl fmt::Display for Icmpv6Code {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Icmpv6Code::Numeric(code) => write!(f, "{code}"),
+            Icmpv6Code::Named(code) => write!(f, "{code}"),
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::firewall::types::{alias::AliasScope::Guest, Cidr};
+
+    use super::*;
+
+    #[test]
+    fn test_parse_action() {
+        assert_eq!(parse_action("REJECT").unwrap(), (None, Verdict::Reject, ""));
+
+        assert_eq!(
+            parse_action("SSH(ACCEPT) qweasd").unwrap(),
+            (Some("SSH"), Verdict::Accept, "qweasd")
+        );
+    }
+
+    #[test]
+    fn test_parse_ip_addr_match() {
+        for input in [
+            "10.0.0.0/8",
+            "10.0.0.0/8,192.168.0.0-192.168.255.255,172.16.0.1",
+            "dc/test",
+            "+guest/proxmox",
+        ] {
+            input.parse::<IpAddrMatch>().expect("valid ip match");
+        }
+
+        for input in [
+            "10.0.0.0/",
+            "10.0.0.0/8,192.168.256.0-192.168.255.255,172.16.0.1",
+            "dcc/test",
+            "+guest/",
+            "",
+        ] {
+            input.parse::<IpAddrMatch>().expect_err("invalid ip match");
+        }
+    }
+
+    #[test]
+    fn test_parse_options() {
+        let mut options: RuleOptions =
+            "-p udp --sport 123 --dport 234 -source 127.0.0.1 --dest 127.0.0.1 -i ens1 --log crit"
+                .parse()
+                .expect("valid option string");
+
+        assert_eq!(
+            options,
+            RuleOptions {
+                proto: Some("udp".to_string()),
+                sport: Some("123".to_string()),
+                dport: Some("234".to_string()),
+                source: Some("127.0.0.1".to_string()),
+                dest: Some("127.0.0.1".to_string()),
+                iface: Some("ens1".to_string()),
+                log: Some(LogLevel::Critical),
+                icmp_type: None,
+            }
+        );
+
+        options = "".parse().expect("valid option string");
+
+        assert_eq!(options, RuleOptions::default(),);
+    }
+
+    #[test]
+    fn test_construct_ip_match() {
+        IpMatch::new(
+            IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 8).unwrap())),
+            IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 8).unwrap())),
+        )
+        .expect("valid ip match");
+
+        IpMatch::new(
+            IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 8).unwrap())),
+            IpAddrMatch::Alias(AliasName::new(Guest, "test")),
+        )
+        .expect("valid ip match");
+
+        IpMatch::new(
+            IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 8).unwrap())),
+            IpAddrMatch::Ip(IpList::from(Cidr::new_v6([0x0000; 8], 8).unwrap())),
+        )
+        .expect_err("cannot mix ip families");
+
+        IpMatch::new(None, None).expect_err("at least one ip must be set");
+    }
+
+    #[test]
+    fn test_from_options() {
+        let mut options = RuleOptions {
+            proto: Some("tcp".to_string()),
+            sport: Some("123".to_string()),
+            dport: Some("234".to_string()),
+            source: Some("192.168.0.1".to_string()),
+            dest: Some("10.0.0.1".to_string()),
+            iface: Some("eth123".to_string()),
+            log: Some(LogLevel::Error),
+            ..Default::default()
+        };
+
+        assert_eq!(
+            Protocol::from_options(&options).unwrap().unwrap(),
+            Protocol::Tcp(Tcp::new(Ports::from_u16(123, 234))),
+        );
+
+        assert_eq!(
+            IpMatch::from_options(&options).unwrap().unwrap(),
+            IpMatch::new(
+                IpAddrMatch::Ip(IpList::from(Cidr::new_v4([192, 168, 0, 1], 32).unwrap()),),
+                IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 1], 32).unwrap()),)
+            )
+            .unwrap(),
+        );
+
+        options = RuleOptions::default();
+
+        assert_eq!(Protocol::from_options(&options).unwrap(), None,);
+
+        assert_eq!(IpMatch::from_options(&options).unwrap(), None,);
+
+        options = RuleOptions {
+            proto: Some("tcp".to_string()),
+            sport: Some("qwe".to_string()),
+            source: Some("qwe".to_string()),
+            ..Default::default()
+        };
+
+        Protocol::from_options(&options).expect_err("invalid source port");
+
+        IpMatch::from_options(&options).expect_err("invalid source address");
+
+        options = RuleOptions {
+            icmp_type: Some("port-unreachable".to_string()),
+            dport: Some("123".to_string()),
+            ..Default::default()
+        };
+
+        RuleMatch::from_options(Direction::In, Verdict::Drop, None, options)
+            .expect_err("cannot mix dport and icmp-type");
+    }
+
+    #[test]
+    fn test_parse_icmp() {
+        let mut icmp: Icmp = "info-request".parse().expect("valid icmp type");
+
+        assert_eq!(
+            icmp,
+            Icmp {
+                ty: Some(IcmpType::Named("info-request")),
+                code: None
+            }
+        );
+
+        icmp = "12".parse().expect("valid icmp type");
+
+        assert_eq!(
+            icmp,
+            Icmp {
+                ty: Some(IcmpType::Numeric(12)),
+                code: None
+            }
+        );
+
+        icmp = "port-unreachable".parse().expect("valid icmp code");
+
+        assert_eq!(
+            icmp,
+            Icmp {
+                ty: None,
+                code: Some(IcmpCode::Named("port-unreachable"))
+            }
+        );
+    }
+
+    #[test]
+    fn test_parse_icmp6() {
+        let mut icmp: Icmpv6 = "echo-reply".parse().expect("valid icmpv6 type");
+
+        assert_eq!(
+            icmp,
+            Icmpv6 {
+                ty: Some(Icmpv6Type::Named("echo-reply")),
+                code: None
+            }
+        );
+
+        icmp = "12".parse().expect("valid icmpv6 type");
+
+        assert_eq!(
+            icmp,
+            Icmpv6 {
+                ty: Some(Icmpv6Type::Numeric(12)),
+                code: None
+            }
+        );
+
+        icmp = "admin-prohibited".parse().expect("valid icmpv6 code");
+
+        assert_eq!(
+            icmp,
+            Icmpv6 {
+                ty: None,
+                code: Some(Icmpv6Code::Named("admin-prohibited"))
+            }
+        );
+    }
+}
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 10/39] config: firewall: add types for security groups
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (8 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 09/39] config: firewall: add types for rules Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 11/39] config: firewall: add generic parser for firewall configs Stefan Hanreich
                   ` (29 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/src/firewall/types/group.rs | 36 +++++++++++++++++++
 proxmox-ve-config/src/firewall/types/mod.rs   |  2 ++
 2 files changed, 38 insertions(+)
 create mode 100644 proxmox-ve-config/src/firewall/types/group.rs

diff --git a/proxmox-ve-config/src/firewall/types/group.rs b/proxmox-ve-config/src/firewall/types/group.rs
new file mode 100644
index 0000000..7455268
--- /dev/null
+++ b/proxmox-ve-config/src/firewall/types/group.rs
@@ -0,0 +1,36 @@
+use anyhow::Error;
+
+use crate::firewall::types::Rule;
+
+#[derive(Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct Group {
+    rules: Vec<Rule>,
+    comment: Option<String>,
+}
+
+impl Group {
+    pub const fn new() -> Self {
+        Self {
+            rules: Vec::new(),
+            comment: None,
+        }
+    }
+
+    pub fn rules(&self) -> &Vec<Rule> {
+        &self.rules
+    }
+
+    pub fn comment(&self) -> Option<&str> {
+        self.comment.as_deref()
+    }
+
+    pub fn set_comment(&mut self, comment: Option<String>) {
+        self.comment = comment;
+    }
+
+    pub(crate) fn parse_entry(&mut self, line: &str) -> Result<(), Error> {
+        self.rules.push(line.parse()?);
+        Ok(())
+    }
+}
diff --git a/proxmox-ve-config/src/firewall/types/mod.rs b/proxmox-ve-config/src/firewall/types/mod.rs
index b4a6b12..8fd551e 100644
--- a/proxmox-ve-config/src/firewall/types/mod.rs
+++ b/proxmox-ve-config/src/firewall/types/mod.rs
@@ -1,5 +1,6 @@
 pub mod address;
 pub mod alias;
+pub mod group;
 pub mod ipset;
 pub mod log;
 pub mod port;
@@ -8,5 +9,6 @@ pub mod rule_match;
 
 pub use address::Cidr;
 pub use alias::Alias;
+pub use group::Group;
 pub use ipset::Ipset;
 pub use rule::Rule;
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 11/39] config: firewall: add generic parser for firewall configs
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (9 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 10/39] config: firewall: add types for security groups Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 12/39] config: firewall: add cluster-specific config + option types Stefan Hanreich
                   ` (28 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

Since the basic format of cluster, host and guest firewall
configurations is the same, we create a generic parser that can handle
the common config format. The main difference is in the available
options, which can be passed via a generic parameter.

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/src/firewall/common.rs | 184 ++++++++++++++++++++
 proxmox-ve-config/src/firewall/mod.rs    |   1 +
 proxmox-ve-config/src/firewall/parse.rs  | 210 +++++++++++++++++++++++
 3 files changed, 395 insertions(+)
 create mode 100644 proxmox-ve-config/src/firewall/common.rs

diff --git a/proxmox-ve-config/src/firewall/common.rs b/proxmox-ve-config/src/firewall/common.rs
new file mode 100644
index 0000000..a08f19c
--- /dev/null
+++ b/proxmox-ve-config/src/firewall/common.rs
@@ -0,0 +1,184 @@
+use std::collections::{BTreeMap, HashMap};
+use std::io;
+
+use anyhow::{bail, format_err, Error};
+use serde::de::IntoDeserializer;
+
+use crate::firewall::parse::{parse_named_section_tail, split_key_value, SomeString};
+use crate::firewall::types::ipset::{IpsetName, IpsetScope};
+use crate::firewall::types::{Alias, Group, Ipset, Rule};
+
+#[derive(Debug, Default)]
+pub struct Config<O>
+where
+    O: Default + std::fmt::Debug + serde::de::DeserializeOwned,
+{
+    pub(crate) options: O,
+    pub(crate) rules: Vec<Rule>,
+    pub(crate) aliases: BTreeMap<String, Alias>,
+    pub(crate) ipsets: BTreeMap<String, Ipset>,
+    pub(crate) groups: BTreeMap<String, Group>,
+}
+
+enum Sec {
+    None,
+    Options,
+    Aliases,
+    Rules,
+    Ipset(String, Ipset),
+    Group(String, Group),
+}
+
+#[derive(Default)]
+pub struct ParserConfig {
+    /// Network interfaces must be of the form `netX`.
+    pub guest_iface_names: bool,
+    pub ipset_scope: Option<IpsetScope>,
+}
+
+impl<O> Config<O>
+where
+    O: Default + std::fmt::Debug + serde::de::DeserializeOwned,
+{
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    pub fn parse<R: io::BufRead>(input: R, parser_cfg: &ParserConfig) -> Result<Self, Error> {
+        let mut section = Sec::None;
+
+        let mut this = Self::new();
+        let mut options = HashMap::new();
+
+        for line in input.lines() {
+            let line = line?;
+            let line = line.trim();
+
+            if line.is_empty() || line.starts_with('#') {
+                continue;
+            }
+
+            log::trace!("parsing config line {line}");
+
+            if line.eq_ignore_ascii_case("[OPTIONS]") {
+                this.set_section(&mut section, Sec::Options)?;
+            } else if line.eq_ignore_ascii_case("[ALIASES]") {
+                this.set_section(&mut section, Sec::Aliases)?;
+            } else if line.eq_ignore_ascii_case("[RULES]") {
+                this.set_section(&mut section, Sec::Rules)?;
+            } else if let Some(line) = line.strip_prefix("[IPSET") {
+                let (name, comment) = parse_named_section_tail("ipset", line)?;
+
+                let scope = parser_cfg.ipset_scope.ok_or_else(|| {
+                    format_err!("IPSET in config, but no scope set in parser config")
+                })?;
+
+                let ipset_name = IpsetName::new(scope, name.to_string());
+                let mut ipset = Ipset::new(ipset_name);
+                ipset.comment = comment.map(str::to_owned);
+
+                this.set_section(&mut section, Sec::Ipset(name.to_string(), ipset))?;
+            } else if let Some(line) = line.strip_prefix("[group") {
+                let (name, comment) = parse_named_section_tail("group", line)?;
+                let mut group = Group::new();
+
+                group.set_comment(comment.map(str::to_owned));
+
+                this.set_section(&mut section, Sec::Group(name.to_owned(), group))?;
+            } else if line.starts_with('[') {
+                bail!("invalid section {line:?}");
+            } else {
+                match &mut section {
+                    Sec::None => bail!("config line with no section: {line:?}"),
+                    Sec::Options => Self::parse_option(line, &mut options)?,
+                    Sec::Aliases => this.parse_alias(line)?,
+                    Sec::Rules => this.parse_rule(line, parser_cfg)?,
+                    Sec::Ipset(_name, ipset) => ipset.parse_entry(line)?,
+                    Sec::Group(_name, group) => group.parse_entry(line)?,
+                }
+            }
+        }
+        this.set_section(&mut section, Sec::None)?;
+
+        this.options = O::deserialize(IntoDeserializer::<
+            '_,
+            crate::firewall::parse::SerdeStringError,
+        >::into_deserializer(options))?;
+
+        Ok(this)
+    }
+
+    fn parse_option(line: &str, options: &mut HashMap<String, SomeString>) -> Result<(), Error> {
+        let (key, value) = split_key_value(line)
+            .ok_or_else(|| format_err!("expected colon separated key and value, found {line:?}"))?;
+
+        if options.insert(key.to_string(), value.into()).is_some() {
+            bail!("duplicate option {key:?}");
+        }
+
+        Ok(())
+    }
+
+    fn parse_alias(&mut self, line: &str) -> Result<(), Error> {
+        let alias: Alias = line.parse()?;
+
+        if self
+            .aliases
+            .insert(alias.name().to_string(), alias)
+            .is_some()
+        {
+            bail!("duplicate alias: {line}");
+        }
+
+        Ok(())
+    }
+
+    fn parse_rule(&mut self, line: &str, parser_cfg: &ParserConfig) -> Result<(), Error> {
+        let rule: Rule = line.parse()?;
+
+        if parser_cfg.guest_iface_names {
+            if let Some(iface) = rule.iface() {
+                let _ = iface
+                    .strip_prefix("net")
+                    .ok_or_else(|| {
+                        format_err!("interface name must be of the form \"net<number>\"")
+                    })?
+                    .parse::<u16>()
+                    .map_err(|_| {
+                        format_err!("interface name must be of the form \"net<number>\"")
+                    })?;
+            }
+        }
+
+        self.rules.push(rule);
+        Ok(())
+    }
+
+    fn set_section(&mut self, sec: &mut Sec, to: Sec) -> Result<(), Error> {
+        let prev = std::mem::replace(sec, to);
+
+        match prev {
+            Sec::Ipset(name, ipset) => {
+                if self.ipsets.insert(name.clone(), ipset).is_some() {
+                    bail!("duplicate ipset: {name:?}");
+                }
+            }
+            Sec::Group(name, group) => {
+                if self.groups.insert(name.clone(), group).is_some() {
+                    bail!("duplicate group: {name:?}");
+                }
+            }
+            _ => (),
+        }
+
+        Ok(())
+    }
+
+    pub fn ipsets(&self) -> &BTreeMap<String, Ipset> {
+        &self.ipsets
+    }
+
+    pub fn alias(&self, name: &str) -> Option<&Alias> {
+        self.aliases.get(name)
+    }
+}
diff --git a/proxmox-ve-config/src/firewall/mod.rs b/proxmox-ve-config/src/firewall/mod.rs
index 2e0f31e..591ee52 100644
--- a/proxmox-ve-config/src/firewall/mod.rs
+++ b/proxmox-ve-config/src/firewall/mod.rs
@@ -1,3 +1,4 @@
+pub mod common;
 pub mod ports;
 pub mod types;
 
diff --git a/proxmox-ve-config/src/firewall/parse.rs b/proxmox-ve-config/src/firewall/parse.rs
index e2ce463..93cf014 100644
--- a/proxmox-ve-config/src/firewall/parse.rs
+++ b/proxmox-ve-config/src/firewall/parse.rs
@@ -74,6 +74,26 @@ pub fn match_digits(line: &str) -> Option<(&str, &str)> {
 
     None
 }
+
+/// Separate a `key: value` line, trimming whitespace.
+///
+/// Returns `None` if the `key` would be empty.
+pub fn split_key_value(line: &str) -> Option<(&str, &str)> {
+    line.split_once(':')
+        .map(|(key, value)| (key.trim(), value.trim()))
+}
+
+/// Parse a boolean.
+///
+/// values that parse as [`false`]: 0, false, off, no
+/// values that parse as [`true`]: 1, true, on, yes
+///
+/// # Examples
+/// ```ignore
+/// assert_eq!(parse_bool("false"), Ok(false));
+/// assert_eq!(parse_bool("on"), Ok(true));
+/// assert!(parse_bool("proxmox").is_err());
+/// ```
 pub fn parse_bool(value: &str) -> Result<bool, Error> {
     Ok(
         if value == "0"
@@ -94,6 +114,196 @@ pub fn parse_bool(value: &str) -> Result<bool, Error> {
     )
 }
 
+/// Parse the *remainder* of a section line, that is `<whitespace>NAME] #optional comment`.
+/// The `kind` parameter is used for error messages and should be the section type.
+///
+/// Return the name and the optional comment.
+pub fn parse_named_section_tail<'a>(
+    kind: &'static str,
+    line: &'a str,
+) -> Result<(&'a str, Option<&'a str>), Error> {
+    if line.is_empty() || !line.as_bytes()[0].is_ascii_whitespace() {
+        bail!("incomplete {kind} section");
+    }
+
+    let line = line.trim_start();
+    let (name, line) = match_name(line)
+        .ok_or_else(|| format_err!("expected a name for the {kind} at {line:?}"))?;
+
+    let line = line
+        .strip_prefix(']')
+        .ok_or_else(|| format_err!("expected closing ']' in {kind} section header"))?
+        .trim_start();
+
+    Ok(match line.strip_prefix('#') {
+        Some(comment) => (name, Some(comment.trim())),
+        None if !line.is_empty() => bail!("trailing characters after {kind} section: {line:?}"),
+        None => (name, None),
+    })
+}
+
+// parses a number from a string OR number
+pub mod serde_option_number {
+    use std::fmt;
+
+    use serde::de::{Deserializer, Error, Visitor};
+
+    pub fn deserialize<'de, D: Deserializer<'de>>(
+        deserializer: D,
+    ) -> Result<Option<i64>, D::Error> {
+        struct V;
+
+        impl<'de> Visitor<'de> for V {
+            type Value = Option<i64>;
+
+            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
+                f.write_str("a numerical value")
+            }
+
+            fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
+                v.parse().map_err(E::custom).map(Some)
+            }
+
+            fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
+                Ok(None)
+            }
+
+            fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
+            where
+                D: Deserializer<'de>,
+            {
+                deserializer.deserialize_any(self)
+            }
+        }
+
+        deserializer.deserialize_any(V)
+    }
+}
+
+// parses a bool from a string OR bool
+pub mod serde_option_bool {
+    use std::fmt;
+
+    use serde::de::{Deserializer, Error, Visitor};
+
+    pub fn deserialize<'de, D: Deserializer<'de>>(
+        deserializer: D,
+    ) -> Result<Option<bool>, D::Error> {
+        struct V;
+
+        impl<'de> Visitor<'de> for V {
+            type Value = Option<bool>;
+
+            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
+                f.write_str("a boolean-like value")
+            }
+
+            fn visit_bool<E: Error>(self, v: bool) -> Result<Self::Value, E> {
+                Ok(Some(v))
+            }
+
+            fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
+                super::parse_bool(v).map_err(E::custom).map(Some)
+            }
+
+            fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
+                Ok(None)
+            }
+
+            fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
+            where
+                D: Deserializer<'de>,
+            {
+                deserializer.deserialize_any(self)
+            }
+        }
+
+        deserializer.deserialize_any(V)
+    }
+}
+
+// parses a comma_separated list of strings
+pub mod serde_option_conntrack_helpers {
+    use std::fmt;
+
+    use serde::de::{Deserializer, Error, Visitor};
+
+    pub fn deserialize<'de, D: Deserializer<'de>>(
+        deserializer: D,
+    ) -> Result<Option<Vec<String>>, D::Error> {
+        struct V;
+
+        impl<'de> Visitor<'de> for V {
+            type Value = Option<Vec<String>>;
+
+            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
+                f.write_str("A list of conntrack helpers")
+            }
+
+            fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
+                if v.is_empty() {
+                    return Ok(None);
+                }
+
+                Ok(Some(v.split(',').map(String::from).collect()))
+            }
+
+            fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
+                Ok(None)
+            }
+
+            fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
+            where
+                D: Deserializer<'de>,
+            {
+                deserializer.deserialize_any(self)
+            }
+        }
+
+        deserializer.deserialize_any(V)
+    }
+}
+
+// parses a log_ratelimit string: '[enable=]<1|0> [,burst=<integer>] [,rate=<rate>]'
+pub mod serde_option_log_ratelimit {
+    use std::fmt;
+
+    use serde::de::{Deserializer, Error, Visitor};
+
+    use crate::firewall::types::log::LogRateLimit;
+
+    pub fn deserialize<'de, D: Deserializer<'de>>(
+        deserializer: D,
+    ) -> Result<Option<LogRateLimit>, D::Error> {
+        struct V;
+
+        impl<'de> Visitor<'de> for V {
+            type Value = Option<LogRateLimit>;
+
+            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
+                f.write_str("a boolean-like value")
+            }
+
+            fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
+                v.parse().map_err(E::custom).map(Some)
+            }
+
+            fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
+                Ok(None)
+            }
+
+            fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
+            where
+                D: Deserializer<'de>,
+            {
+                deserializer.deserialize_any(self)
+            }
+        }
+
+        deserializer.deserialize_any(V)
+    }
+}
+
 /// `&str` deserializer which also accepts an `Option`.
 ///
 /// Serde's `StringDeserializer` does not.
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 12/39] config: firewall: add cluster-specific config + option types
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (10 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 11/39] config: firewall: add generic parser for firewall configs Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 13/39] config: firewall: add host specific " Stefan Hanreich
                   ` (27 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/src/firewall/cluster.rs | 374 ++++++++++++++++++++++
 proxmox-ve-config/src/firewall/mod.rs     |   1 +
 2 files changed, 375 insertions(+)
 create mode 100644 proxmox-ve-config/src/firewall/cluster.rs

diff --git a/proxmox-ve-config/src/firewall/cluster.rs b/proxmox-ve-config/src/firewall/cluster.rs
new file mode 100644
index 0000000..223124b
--- /dev/null
+++ b/proxmox-ve-config/src/firewall/cluster.rs
@@ -0,0 +1,374 @@
+use std::collections::BTreeMap;
+use std::io;
+
+use anyhow::Error;
+use serde::Deserialize;
+
+use crate::firewall::common::ParserConfig;
+use crate::firewall::types::ipset::{Ipset, IpsetScope};
+use crate::firewall::types::log::LogRateLimit;
+use crate::firewall::types::rule::{Direction, Verdict};
+use crate::firewall::types::{Alias, Group, Rule};
+
+use crate::firewall::parse::{serde_option_bool, serde_option_log_ratelimit};
+
+#[derive(Debug, Default)]
+pub struct Config {
+    pub(crate) config: super::common::Config<Options>,
+}
+
+/// default setting for [`Config::is_enabled()`]
+pub const CLUSTER_ENABLED_DEFAULT: bool = false;
+/// default setting for [`Config::ebtables()`]
+pub const CLUSTER_EBTABLES_DEFAULT: bool = false;
+/// default setting for [`Config::default_policy()`]
+pub const CLUSTER_POLICY_IN_DEFAULT: Verdict = Verdict::Drop;
+/// default setting for [`Config::default_policy()`]
+pub const CLUSTER_POLICY_OUT_DEFAULT: Verdict = Verdict::Accept;
+
+impl Config {
+    pub fn parse<R: io::BufRead>(input: R) -> Result<Self, Error> {
+        let parser_config = ParserConfig {
+            guest_iface_names: false,
+            ipset_scope: Some(IpsetScope::Datacenter),
+        };
+
+        Ok(Self {
+            config: super::common::Config::parse(input, &parser_config)?,
+        })
+    }
+
+    pub fn rules(&self) -> &Vec<Rule> {
+        &self.config.rules
+    }
+
+    pub fn groups(&self) -> &BTreeMap<String, Group> {
+        &self.config.groups
+    }
+
+    pub fn ipsets(&self) -> &BTreeMap<String, Ipset> {
+        &self.config.ipsets
+    }
+
+    pub fn alias(&self, name: &str) -> Option<&Alias> {
+        self.config.alias(name)
+    }
+
+    pub fn is_enabled(&self) -> bool {
+        self.config
+            .options
+            .enable
+            .unwrap_or(CLUSTER_ENABLED_DEFAULT)
+    }
+
+    /// returns the ebtables option from the cluster config or [`CLUSTER_EBTABLES_DEFAULT`] if
+    /// unset
+    ///
+    /// this setting is leftover from the old firewall, but has no effect on the nftables firewall
+    pub fn ebtables(&self) -> bool {
+        self.config
+            .options
+            .ebtables
+            .unwrap_or(CLUSTER_EBTABLES_DEFAULT)
+    }
+
+    /// returns policy_in / out or [`CLUSTER_POLICY_IN_DEFAULT`] / [`CLUSTER_POLICY_OUT_DEFAULT`] if
+    /// unset
+    pub fn default_policy(&self, dir: Direction) -> Verdict {
+        match dir {
+            Direction::In => self
+                .config
+                .options
+                .policy_in
+                .unwrap_or(CLUSTER_POLICY_IN_DEFAULT),
+            Direction::Out => self
+                .config
+                .options
+                .policy_out
+                .unwrap_or(CLUSTER_POLICY_OUT_DEFAULT),
+        }
+    }
+
+    /// returns the rate_limit for logs or [`None`] if rate limiting is disabled
+    ///
+    /// If there is no rate limit set, then [`LogRateLimit::default`] is used
+    pub fn log_ratelimit(&self) -> Option<LogRateLimit> {
+        let rate_limit = self
+            .config
+            .options
+            .log_ratelimit
+            .clone()
+            .unwrap_or_default();
+
+        match rate_limit.enabled() {
+            true => Some(rate_limit),
+            false => None,
+        }
+    }
+}
+
+#[derive(Debug, Default, Deserialize)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct Options {
+    #[serde(default, with = "serde_option_bool")]
+    enable: Option<bool>,
+
+    #[serde(default, with = "serde_option_bool")]
+    ebtables: Option<bool>,
+
+    #[serde(default, with = "serde_option_log_ratelimit")]
+    log_ratelimit: Option<LogRateLimit>,
+
+    policy_in: Option<Verdict>,
+    policy_out: Option<Verdict>,
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::firewall::types::{
+        address::IpList,
+        alias::{AliasName, AliasScope},
+        ipset::{IpsetAddress, IpsetEntry},
+        log::{LogLevel, LogRateLimitTimescale},
+        rule::{Kind, RuleGroup},
+        rule_match::{
+            Icmpv6, Icmpv6Code, IpAddrMatch, IpMatch, Ports, Protocol, RuleMatch, Tcp, Udp,
+        },
+        Cidr,
+    };
+
+    use super::*;
+
+    #[test]
+    fn test_parse_config() {
+        const CONFIG: &str = r#"
+[OPTIONS]
+enable: 1
+log_ratelimit: 1,rate=10/second,burst=20
+ebtables: 0
+policy_in: REJECT
+policy_out: REJECT
+
+[ALIASES]
+
+another 8.8.8.18
+analias 7.7.0.0/16 # much
+wide cccc::/64
+
+[IPSET a-set]
+
+!5.5.5.5
+1.2.3.4/30
+dc/analias # a comment
+dc/wide
+dddd::/96
+
+[RULES]
+
+GROUP tgr -i eth0 # acomm
+IN ACCEPT -p udp -dport 33 -sport 22 -log warning
+
+[group tgr] # comment for tgr
+
+|OUT ACCEPT -source fe80::1/48 -dest dddd:3:3::9/64 -p icmpv6 -log nolog -icmp-type port-unreachable
+OUT ACCEPT -p tcp -sport 33 -log nolog
+IN BGP(REJECT) -log crit -source 1.2.3.4
+"#;
+
+        let mut config = CONFIG.as_bytes();
+        let config = Config::parse(&mut config).unwrap();
+
+        assert_eq!(
+            config.config.options,
+            Options {
+                ebtables: Some(false),
+                enable: Some(true),
+                log_ratelimit: Some(LogRateLimit::new(
+                    true,
+                    10,
+                    LogRateLimitTimescale::Second,
+                    20
+                )),
+                policy_in: Some(Verdict::Reject),
+                policy_out: Some(Verdict::Reject),
+            }
+        );
+
+        assert_eq!(config.config.aliases.len(), 3);
+
+        assert_eq!(
+            config.config.aliases["another"],
+            Alias::new("another", Cidr::new_v4([8, 8, 8, 18], 32).unwrap(), None),
+        );
+
+        assert_eq!(
+            config.config.aliases["analias"],
+            Alias::new(
+                "analias",
+                Cidr::new_v4([7, 7, 0, 0], 16).unwrap(),
+                "much".to_string()
+            ),
+        );
+
+        assert_eq!(
+            config.config.aliases["wide"],
+            Alias::new(
+                "wide",
+                Cidr::new_v6(
+                    [0xCCCC, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x000],
+                    64
+                )
+                .unwrap(),
+                None
+            ),
+        );
+
+        assert_eq!(config.config.ipsets.len(), 1);
+
+        let mut ipset_elements = vec![
+            IpsetEntry {
+                nomatch: true,
+                address: Cidr::new_v4([5, 5, 5, 5], 32).unwrap().into(),
+                comment: None,
+            },
+            IpsetEntry {
+                nomatch: false,
+                address: Cidr::new_v4([1, 2, 3, 4], 30).unwrap().into(),
+                comment: None,
+            },
+            IpsetEntry {
+                nomatch: false,
+                address: IpsetAddress::Alias(AliasName::new(AliasScope::Datacenter, "analias")),
+                comment: Some("a comment".to_string()),
+            },
+            IpsetEntry {
+                nomatch: false,
+                address: IpsetAddress::Alias(AliasName::new(AliasScope::Datacenter, "wide")),
+                comment: None,
+            },
+            IpsetEntry {
+                nomatch: false,
+                address: Cidr::new_v6([0xdd, 0xdd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 96)
+                    .unwrap()
+                    .into(),
+                comment: None,
+            },
+        ];
+
+        let mut ipset = Ipset::from_parts(IpsetScope::Datacenter, "a-set");
+        ipset.append(&mut ipset_elements);
+
+        assert_eq!(config.config.ipsets["a-set"], ipset,);
+
+        assert_eq!(config.config.rules.len(), 2);
+
+        assert_eq!(
+            config.config.rules[0],
+            Rule {
+                disabled: false,
+                comment: Some("acomm".to_string()),
+                kind: Kind::Group(RuleGroup {
+                    group: "tgr".to_string(),
+                    iface: Some("eth0".to_string()),
+                }),
+            },
+        );
+
+        assert_eq!(
+            config.config.rules[1],
+            Rule {
+                disabled: false,
+                comment: None,
+                kind: Kind::Match(RuleMatch {
+                    dir: Direction::In,
+                    verdict: Verdict::Accept,
+                    proto: Some(Protocol::Udp(Udp::new(Ports::from_u16(22, 33)))),
+                    log: Some(LogLevel::Warning),
+                    ..Default::default()
+                }),
+            },
+        );
+
+        assert_eq!(config.config.groups.len(), 1);
+
+        let entry = &config.config.groups["tgr"];
+        assert_eq!(entry.comment(), Some("comment for tgr"));
+        assert_eq!(entry.rules().len(), 3);
+
+        assert_eq!(
+            entry.rules()[0],
+            Rule {
+                disabled: true,
+                comment: None,
+                kind: Kind::Match(RuleMatch {
+                    dir: Direction::Out,
+                    verdict: Verdict::Accept,
+                    ip: Some(IpMatch {
+                        src: Some(IpAddrMatch::Ip(IpList::from(
+                            Cidr::new_v6(
+                                [0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
+                                48
+                            )
+                            .unwrap()
+                        ))),
+                        dst: Some(IpAddrMatch::Ip(IpList::from(
+                            Cidr::new_v6(
+                                [0xdd, 0xdd, 0, 3, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9],
+                                64
+                            )
+                            .unwrap()
+                        ))),
+                    }),
+                    proto: Some(Protocol::Icmpv6(Icmpv6::new_code(Icmpv6Code::Named(
+                        "port-unreachable"
+                    )))),
+                    log: Some(LogLevel::Nolog),
+                    ..Default::default()
+                }),
+            },
+        );
+        assert_eq!(
+            entry.rules()[1],
+            Rule {
+                disabled: false,
+                comment: None,
+                kind: Kind::Match(RuleMatch {
+                    dir: Direction::Out,
+                    verdict: Verdict::Accept,
+                    proto: Some(Protocol::Tcp(Tcp::new(Ports::from_u16(33, None)))),
+                    log: Some(LogLevel::Nolog),
+                    ..Default::default()
+                }),
+            },
+        );
+
+        assert_eq!(
+            entry.rules()[2],
+            Rule {
+                disabled: false,
+                comment: None,
+                kind: Kind::Match(RuleMatch {
+                    dir: Direction::In,
+                    verdict: Verdict::Reject,
+                    log: Some(LogLevel::Critical),
+                    fw_macro: Some("BGP".to_string()),
+                    ip: Some(IpMatch {
+                        src: Some(IpAddrMatch::Ip(IpList::from(
+                            Cidr::new_v4([1, 2, 3, 4], 32).unwrap()
+                        ))),
+                        dst: None,
+                    }),
+                    ..Default::default()
+                }),
+            },
+        );
+
+        let empty_config = Config::parse("".as_bytes()).expect("empty config is invalid");
+
+        assert_eq!(empty_config.config.options, Options::default());
+        assert!(empty_config.config.rules.is_empty());
+        assert!(empty_config.config.aliases.is_empty());
+        assert!(empty_config.config.ipsets.is_empty());
+        assert!(empty_config.config.groups.is_empty());
+    }
+}
diff --git a/proxmox-ve-config/src/firewall/mod.rs b/proxmox-ve-config/src/firewall/mod.rs
index 591ee52..82689c3 100644
--- a/proxmox-ve-config/src/firewall/mod.rs
+++ b/proxmox-ve-config/src/firewall/mod.rs
@@ -1,3 +1,4 @@
+pub mod cluster;
 pub mod common;
 pub mod ports;
 pub mod types;
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 13/39] config: firewall: add host specific config + option types
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (11 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 12/39] config: firewall: add cluster-specific config + option types Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 14/39] config: firewall: add guest-specific " Stefan Hanreich
                   ` (26 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/src/firewall/host.rs | 372 +++++++++++++++++++++++++
 proxmox-ve-config/src/firewall/mod.rs  |   1 +
 2 files changed, 373 insertions(+)
 create mode 100644 proxmox-ve-config/src/firewall/host.rs

diff --git a/proxmox-ve-config/src/firewall/host.rs b/proxmox-ve-config/src/firewall/host.rs
new file mode 100644
index 0000000..3de6fad
--- /dev/null
+++ b/proxmox-ve-config/src/firewall/host.rs
@@ -0,0 +1,372 @@
+use std::io;
+use std::net::IpAddr;
+
+use anyhow::{bail, Error};
+use serde::Deserialize;
+
+use crate::host::utils::{host_ips, network_interface_cidrs};
+use proxmox_sys::nodename;
+
+use crate::firewall::parse;
+use crate::firewall::types::log::LogLevel;
+use crate::firewall::types::rule::Direction;
+use crate::firewall::types::{Alias, Cidr, Rule};
+
+/// default setting for the enabled key
+pub const HOST_ENABLED_DEFAULT: bool = true;
+/// default setting for the nftables key
+pub const HOST_NFTABLES_DEFAULT: bool = false;
+/// default return value for [`Config::allow_ndp()`]
+pub const HOST_ALLOW_NDP_DEFAULT: bool = true;
+/// default return value for [`Config::block_smurfs()`]
+pub const HOST_BLOCK_SMURFS_DEFAULT: bool = true;
+/// default return value for [`Config::block_synflood()`]
+pub const HOST_BLOCK_SYNFLOOD_DEFAULT: bool = false;
+/// default rate limit for synflood rule (packets / second)
+pub const HOST_BLOCK_SYNFLOOD_RATE_DEFAULT: i64 = 200;
+/// default rate limit for synflood rule (packets / second)
+pub const HOST_BLOCK_SYNFLOOD_BURST_DEFAULT: i64 = 1000;
+/// default return value for [`Config::block_invalid_tcp()`]
+pub const HOST_BLOCK_INVALID_TCP_DEFAULT: bool = false;
+/// default return value for [`Config::block_invalid_conntrack()`]
+pub const HOST_BLOCK_INVALID_CONNTRACK: bool = false;
+/// default setting for logging of invalid conntrack entries
+pub const HOST_LOG_INVALID_CONNTRACK: bool = false;
+
+#[derive(Debug, Default, Deserialize)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct Options {
+    #[serde(default, with = "parse::serde_option_bool")]
+    enable: Option<bool>,
+
+    #[serde(default, with = "parse::serde_option_bool")]
+    nftables: Option<bool>,
+
+    log_level_in: Option<LogLevel>,
+    log_level_out: Option<LogLevel>,
+
+    #[serde(default, with = "parse::serde_option_bool")]
+    log_nf_conntrack: Option<bool>,
+    #[serde(default, with = "parse::serde_option_bool")]
+    ndp: Option<bool>,
+
+    #[serde(default, with = "parse::serde_option_bool")]
+    nf_conntrack_allow_invalid: Option<bool>,
+
+    // is Option<Vec<>> for easier deserialization
+    #[serde(default, with = "parse::serde_option_conntrack_helpers")]
+    nf_conntrack_helpers: Option<Vec<String>>,
+
+    #[serde(default, with = "parse::serde_option_number")]
+    nf_conntrack_max: Option<i64>,
+    #[serde(default, with = "parse::serde_option_number")]
+    nf_conntrack_tcp_timeout_established: Option<i64>,
+    #[serde(default, with = "parse::serde_option_number")]
+    nf_conntrack_tcp_timeout_syn_recv: Option<i64>,
+
+    #[serde(default, with = "parse::serde_option_bool")]
+    nosmurfs: Option<bool>,
+
+    #[serde(default, with = "parse::serde_option_bool")]
+    protection_synflood: Option<bool>,
+    #[serde(default, with = "parse::serde_option_number")]
+    protection_synflood_burst: Option<i64>,
+    #[serde(default, with = "parse::serde_option_number")]
+    protection_synflood_rate: Option<i64>,
+
+    smurf_log_level: Option<LogLevel>,
+    tcp_flags_log_level: Option<LogLevel>,
+
+    #[serde(default, with = "parse::serde_option_bool")]
+    tcpflags: Option<bool>,
+}
+
+#[derive(Debug, Default)]
+pub struct Config {
+    pub(crate) config: super::common::Config<Options>,
+}
+
+impl Config {
+    pub fn new() -> Self {
+        Self {
+            config: Default::default(),
+        }
+    }
+
+    pub fn parse<R: io::BufRead>(input: R) -> Result<Self, Error> {
+        let config = super::common::Config::parse(input, &Default::default())?;
+
+        if !config.groups.is_empty() {
+            bail!("host firewall config cannot declare groups");
+        }
+
+        if !config.aliases.is_empty() {
+            bail!("host firewall config cannot declare aliases");
+        }
+
+        if !config.ipsets.is_empty() {
+            bail!("host firewall config cannot declare ipsets");
+        }
+
+        Ok(Self { config })
+    }
+
+    pub fn rules(&self) -> &[Rule] {
+        &self.config.rules
+    }
+
+    pub fn management_ips() -> Result<Vec<Cidr>, Error> {
+        let mut management_cidrs = Vec::new();
+
+        for host_ip in host_ips() {
+            for network_interface_cidr in network_interface_cidrs() {
+                match (host_ip, network_interface_cidr) {
+                    (IpAddr::V4(ip), Cidr::Ipv4(cidr)) => {
+                        if cidr.contains_address(&ip) {
+                            management_cidrs.push(network_interface_cidr);
+                        }
+                    }
+                    (IpAddr::V6(ip), Cidr::Ipv6(cidr)) => {
+                        if cidr.contains_address(&ip) {
+                            management_cidrs.push(network_interface_cidr);
+                        }
+                    }
+                    _ => continue,
+                };
+            }
+        }
+
+        Ok(management_cidrs)
+    }
+
+    pub fn hostname() -> &'static str {
+        nodename()
+    }
+
+    pub fn get_alias(&self, name: &str) -> Option<&Alias> {
+        self.config.alias(name)
+    }
+
+    /// returns value of enabled key or [`HOST_ENABLED_DEFAULT`] if unset
+    pub fn is_enabled(&self) -> bool {
+        self.config.options.enable.unwrap_or(HOST_ENABLED_DEFAULT)
+    }
+
+    /// returns value of nftables key or [`HOST_NFTABLES_DEFAULT`] if unset
+    pub fn nftables(&self) -> bool {
+        self.config
+            .options
+            .nftables
+            .unwrap_or(HOST_NFTABLES_DEFAULT)
+    }
+
+    /// returns value of ndp key or [`HOST_ALLOW_NDP_DEFAULT`] if unset
+    pub fn allow_ndp(&self) -> bool {
+        self.config.options.ndp.unwrap_or(HOST_ALLOW_NDP_DEFAULT)
+    }
+
+    /// returns value of nosmurfs key or [`HOST_BLOCK_SMURFS_DEFAULT`] if unset
+    pub fn block_smurfs(&self) -> bool {
+        self.config
+            .options
+            .nosmurfs
+            .unwrap_or(HOST_BLOCK_SMURFS_DEFAULT)
+    }
+
+    /// returns the log level for the smurf protection rule
+    ///
+    /// If there is no log level set, it returns [`LogLevel::default()`]
+    pub fn block_smurfs_log_level(&self) -> LogLevel {
+        self.config.options.smurf_log_level.unwrap_or_default()
+    }
+
+    /// returns value of protection_synflood key or [`HOST_BLOCK_SYNFLOOD_DEFAULT`] if unset
+    pub fn block_synflood(&self) -> bool {
+        self.config
+            .options
+            .protection_synflood
+            .unwrap_or(HOST_BLOCK_SYNFLOOD_DEFAULT)
+    }
+
+    /// returns value of protection_synflood_rate key or [`HOST_BLOCK_SYNFLOOD_RATE_DEFAULT`] if
+    /// unset
+    pub fn synflood_rate(&self) -> i64 {
+        self.config
+            .options
+            .protection_synflood_rate
+            .unwrap_or(HOST_BLOCK_SYNFLOOD_RATE_DEFAULT)
+    }
+
+    /// returns value of protection_synflood_burst key or [`HOST_BLOCK_SYNFLOOD_BURST_DEFAULT`] if
+    /// unset
+    pub fn synflood_burst(&self) -> i64 {
+        self.config
+            .options
+            .protection_synflood_burst
+            .unwrap_or(HOST_BLOCK_SYNFLOOD_BURST_DEFAULT)
+    }
+
+    /// returns value of tcpflags key or [`HOST_BLOCK_INVALID_TCP_DEFAULT`] if unset
+    pub fn block_invalid_tcp(&self) -> bool {
+        self.config
+            .options
+            .tcpflags
+            .unwrap_or(HOST_BLOCK_INVALID_TCP_DEFAULT)
+    }
+
+    /// returns the log level for the block invalid TCP packets rule
+    ///
+    /// If there is no log level set, it returns [`LogLevel::default()`]
+    pub fn block_invalid_tcp_log_level(&self) -> LogLevel {
+        self.config.options.tcp_flags_log_level.unwrap_or_default()
+    }
+
+    /// returns value of nf_conntrack_allow_invalid key or [`HOST_BLOCK_INVALID_CONNTRACK`] if
+    /// unset
+    pub fn block_invalid_conntrack(&self) -> bool {
+        !self
+            .config
+            .options
+            .nf_conntrack_allow_invalid
+            .unwrap_or(HOST_BLOCK_INVALID_CONNTRACK)
+    }
+
+    pub fn nf_conntrack_max(&self) -> Option<i64> {
+        self.config.options.nf_conntrack_max
+    }
+
+    pub fn nf_conntrack_tcp_timeout_established(&self) -> Option<i64> {
+        self.config.options.nf_conntrack_tcp_timeout_established
+    }
+
+    pub fn nf_conntrack_tcp_timeout_syn_recv(&self) -> Option<i64> {
+        self.config.options.nf_conntrack_tcp_timeout_syn_recv
+    }
+
+    /// returns value of log_nf_conntrack key or [`HOST_LOG_INVALID_CONNTRACK`] if unset
+    pub fn log_nf_conntrack(&self) -> bool {
+        self.config
+            .options
+            .log_nf_conntrack
+            .unwrap_or(HOST_LOG_INVALID_CONNTRACK)
+    }
+
+    pub fn conntrack_helpers(&self) -> Option<&Vec<String>> {
+        self.config.options.nf_conntrack_helpers.as_ref()
+    }
+
+    /// returns the log level for the given direction
+    ///
+    /// If there is no log level set it returns [`LogLevel::default()`]
+    pub fn log_level(&self, dir: Direction) -> LogLevel {
+        match dir {
+            Direction::In => self.config.options.log_level_in.unwrap_or_default(),
+            Direction::Out => self.config.options.log_level_out.unwrap_or_default(),
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::firewall::types::{
+        log::LogLevel,
+        rule::{Kind, RuleGroup, Verdict},
+        rule_match::{Ports, Protocol, RuleMatch, Udp},
+    };
+
+    use super::*;
+
+    #[test]
+    fn test_parse_config() {
+        const CONFIG: &str = r#"
+[OPTIONS]
+enable: 1
+nftables: 1
+log_level_in: debug
+log_level_out: emerg
+log_nf_conntrack: 0
+ndp: 1
+nf_conntrack_allow_invalid: yes
+nf_conntrack_helpers: ftp
+nf_conntrack_max: 44000
+nf_conntrack_tcp_timeout_established: 500000
+nf_conntrack_tcp_timeout_syn_recv: 44
+nosmurfs: no
+protection_synflood: 1
+protection_synflood_burst: 2500
+protection_synflood_rate: 300
+smurf_log_level: notice
+tcp_flags_log_level: nolog
+tcpflags: yes
+
+[RULES]
+
+GROUP tgr -i eth0 # acomm
+IN ACCEPT -p udp -dport 33 -sport 22 -log warning
+
+"#;
+
+        let mut config = CONFIG.as_bytes();
+        let config = Config::parse(&mut config).unwrap();
+
+        assert_eq!(
+            config.config.options,
+            Options {
+                enable: Some(true),
+                nftables: Some(true),
+                log_level_in: Some(LogLevel::Debug),
+                log_level_out: Some(LogLevel::Emergency),
+                log_nf_conntrack: Some(false),
+                ndp: Some(true),
+                nf_conntrack_allow_invalid: Some(true),
+                nf_conntrack_helpers: Some(vec!["ftp".to_string()]),
+                nf_conntrack_max: Some(44000),
+                nf_conntrack_tcp_timeout_established: Some(500000),
+                nf_conntrack_tcp_timeout_syn_recv: Some(44),
+                nosmurfs: Some(false),
+                protection_synflood: Some(true),
+                protection_synflood_burst: Some(2500),
+                protection_synflood_rate: Some(300),
+                smurf_log_level: Some(LogLevel::Notice),
+                tcp_flags_log_level: Some(LogLevel::Nolog),
+                tcpflags: Some(true),
+            }
+        );
+
+        assert_eq!(config.config.rules.len(), 2);
+
+        assert_eq!(
+            config.config.rules[0],
+            Rule {
+                disabled: false,
+                comment: Some("acomm".to_string()),
+                kind: Kind::Group(RuleGroup {
+                    group: "tgr".to_string(),
+                    iface: Some("eth0".to_string()),
+                }),
+            },
+        );
+
+        assert_eq!(
+            config.config.rules[1],
+            Rule {
+                disabled: false,
+                comment: None,
+                kind: Kind::Match(RuleMatch {
+                    dir: Direction::In,
+                    verdict: Verdict::Accept,
+                    proto: Some(Protocol::Udp(Udp::new(Ports::from_u16(22, 33)))),
+                    log: Some(LogLevel::Warning),
+                    ..Default::default()
+                }),
+            },
+        );
+
+        Config::parse("[ALIASES]\ntest 127.0.0.1".as_bytes())
+            .expect_err("host config cannot contain aliases");
+
+        Config::parse("[GROUP test]".as_bytes()).expect_err("host config cannot contain groups");
+
+        Config::parse("[IPSET test]".as_bytes()).expect_err("host config cannot contain ipsets");
+    }
+}
diff --git a/proxmox-ve-config/src/firewall/mod.rs b/proxmox-ve-config/src/firewall/mod.rs
index 82689c3..85fe6c4 100644
--- a/proxmox-ve-config/src/firewall/mod.rs
+++ b/proxmox-ve-config/src/firewall/mod.rs
@@ -1,5 +1,6 @@
 pub mod cluster;
 pub mod common;
+pub mod host;
 pub mod ports;
 pub mod types;
 
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 14/39] config: firewall: add guest-specific config + option types
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (12 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 13/39] config: firewall: add host specific " Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 15/39] config: firewall: add firewall macros Stefan Hanreich
                   ` (25 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/src/firewall/guest.rs | 237 ++++++++++++++++++++++++
 proxmox-ve-config/src/firewall/mod.rs   |   1 +
 2 files changed, 238 insertions(+)
 create mode 100644 proxmox-ve-config/src/firewall/guest.rs

diff --git a/proxmox-ve-config/src/firewall/guest.rs b/proxmox-ve-config/src/firewall/guest.rs
new file mode 100644
index 0000000..c7e282f
--- /dev/null
+++ b/proxmox-ve-config/src/firewall/guest.rs
@@ -0,0 +1,237 @@
+use std::collections::BTreeMap;
+use std::io;
+
+use crate::guest::types::Vmid;
+use crate::guest::vm::NetworkConfig;
+
+use crate::firewall::types::alias::{Alias, AliasName};
+use crate::firewall::types::ipset::IpsetScope;
+use crate::firewall::types::log::LogLevel;
+use crate::firewall::types::rule::{Direction, Rule, Verdict};
+use crate::firewall::types::Ipset;
+
+use anyhow::{bail, Error};
+use serde::Deserialize;
+
+use crate::firewall::parse::serde_option_bool;
+
+/// default return value for [`Config::is_enabled()`]
+pub const GUEST_ENABLED_DEFAULT: bool = false;
+/// default return value for [`Config::allow_ndp()`]
+pub const GUEST_ALLOW_NDP_DEFAULT: bool = true;
+/// default return value for [`Config::allow_dhcp()`]
+pub const GUEST_ALLOW_DHCP_DEFAULT: bool = true;
+/// default return value for [`Config::allow_ra()`]
+pub const GUEST_ALLOW_RA_DEFAULT: bool = false;
+/// default return value for [`Config::macfilter()`]
+pub const GUEST_MACFILTER_DEFAULT: bool = true;
+/// default return value for [`Config::ipfilter()`]
+pub const GUEST_IPFILTER_DEFAULT: bool = false;
+/// default return value for [`Config::default_policy()`]
+pub const GUEST_POLICY_IN_DEFAULT: Verdict = Verdict::Drop;
+/// default return value for [`Config::default_policy()`]
+pub const GUEST_POLICY_OUT_DEFAULT: Verdict = Verdict::Accept;
+
+#[derive(Debug, Default, Deserialize)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct Options {
+    #[serde(default, with = "serde_option_bool")]
+    dhcp: Option<bool>,
+
+    #[serde(default, with = "serde_option_bool")]
+    enable: Option<bool>,
+
+    #[serde(default, with = "serde_option_bool")]
+    ipfilter: Option<bool>,
+
+    #[serde(default, with = "serde_option_bool")]
+    ndp: Option<bool>,
+
+    #[serde(default, with = "serde_option_bool")]
+    radv: Option<bool>,
+
+    log_level_in: Option<LogLevel>,
+    log_level_out: Option<LogLevel>,
+
+    #[serde(default, with = "serde_option_bool")]
+    macfilter: Option<bool>,
+
+    #[serde(rename = "policy_in")]
+    policy_in: Option<Verdict>,
+
+    #[serde(rename = "policy_out")]
+    policy_out: Option<Verdict>,
+}
+
+#[derive(Debug)]
+pub struct Config {
+    vmid: Vmid,
+
+    /// The interface prefix: "veth" for containers, "tap" for VMs.
+    iface_prefix: &'static str,
+
+    network_config: NetworkConfig,
+    config: super::common::Config<Options>,
+}
+
+impl Config {
+    pub fn parse<T: io::BufRead, U: io::BufRead>(
+        vmid: &Vmid,
+        iface_prefix: &'static str,
+        firewall_input: T,
+        network_input: U,
+    ) -> Result<Self, Error> {
+        let parser_cfg = super::common::ParserConfig {
+            guest_iface_names: true,
+            ipset_scope: Some(IpsetScope::Guest),
+        };
+
+        let config = super::common::Config::parse(firewall_input, &parser_cfg)?;
+        if !config.groups.is_empty() {
+            bail!("guest firewall config cannot declare groups");
+        }
+
+        let network_config = NetworkConfig::parse(network_input)?;
+
+        Ok(Self {
+            vmid: *vmid,
+            iface_prefix,
+            config,
+            network_config,
+        })
+    }
+
+    pub fn vmid(&self) -> Vmid {
+        self.vmid
+    }
+
+    pub fn alias(&self, name: &AliasName) -> Option<&Alias> {
+        self.config.alias(name.name())
+    }
+
+    pub fn iface_name_by_key(&self, key: &str) -> Result<String, Error> {
+        let index = NetworkConfig::index_from_net_key(key)?;
+        Ok(format!("{}{}i{index}", self.iface_prefix, self.vmid))
+    }
+
+    pub fn iface_name_by_index(&self, index: i64) -> String {
+        format!("{}{}i{index}", self.iface_prefix, self.vmid)
+    }
+
+    /// returns the value of the enabled config key or [`GUEST_ENABLED_DEFAULT`] if unset
+    pub fn is_enabled(&self) -> bool {
+        self.config.options.enable.unwrap_or(GUEST_ENABLED_DEFAULT)
+    }
+
+    pub fn rules(&self) -> &[Rule] {
+        &self.config.rules
+    }
+
+    pub fn log_level(&self, dir: Direction) -> LogLevel {
+        match dir {
+            Direction::In => self.config.options.log_level_in.unwrap_or_default(),
+            Direction::Out => self.config.options.log_level_out.unwrap_or_default(),
+        }
+    }
+
+    /// returns the value of the ndp config key or [`GUEST_ALLOW_NDP_DEFAULT`] if unset
+    pub fn allow_ndp(&self) -> bool {
+        self.config.options.ndp.unwrap_or(GUEST_ALLOW_NDP_DEFAULT)
+    }
+
+    /// returns the value of the dhcp config key or [`GUEST_ALLOW_DHCP_DEFAULT`] if unset
+    pub fn allow_dhcp(&self) -> bool {
+        self.config.options.dhcp.unwrap_or(GUEST_ALLOW_DHCP_DEFAULT)
+    }
+
+    /// returns the value of the radv config key or [`GUEST_ALLOW_RA_DEFAULT`] if unset
+    pub fn allow_ra(&self) -> bool {
+        self.config.options.radv.unwrap_or(GUEST_ALLOW_RA_DEFAULT)
+    }
+
+    /// returns the value of the macfilter config key or [`GUEST_MACFILTER_DEFAULT`] if unset
+    pub fn macfilter(&self) -> bool {
+        self.config
+            .options
+            .macfilter
+            .unwrap_or(GUEST_MACFILTER_DEFAULT)
+    }
+
+    /// returns the value of the ipfilter config key or [`GUEST_IPFILTER_DEFAULT`] if unset
+    pub fn ipfilter(&self) -> bool {
+        self.config
+            .options
+            .ipfilter
+            .unwrap_or(GUEST_IPFILTER_DEFAULT)
+    }
+
+    /// returns the value of the policy_in/out config key or
+    /// [`GUEST_POLICY_IN_DEFAULT`] / [`GUEST_POLICY_OUT_DEFAULT`] if unset
+    pub fn default_policy(&self, dir: Direction) -> Verdict {
+        match dir {
+            Direction::In => self
+                .config
+                .options
+                .policy_in
+                .unwrap_or(GUEST_POLICY_IN_DEFAULT),
+            Direction::Out => self
+                .config
+                .options
+                .policy_out
+                .unwrap_or(GUEST_POLICY_OUT_DEFAULT),
+        }
+    }
+
+    pub fn network_config(&self) -> &NetworkConfig {
+        &self.network_config
+    }
+
+    pub fn ipsets(&self) -> &BTreeMap<String, Ipset> {
+        self.config.ipsets()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_parse_config() {
+        // most of the stuff is already tested in cluster parsing, only testing
+        // guest specific options here
+        const CONFIG: &str = r#"
+[OPTIONS]
+enable: 1
+dhcp: 1
+ipfilter: 0
+log_level_in: emerg
+log_level_out: crit
+macfilter: 0
+ndp:1
+radv:1
+policy_in: REJECT
+policy_out: REJECT
+"#;
+
+        let config = CONFIG.as_bytes();
+        let network_config: Vec<u8> = Vec::new();
+        let config =
+            Config::parse(&Vmid::new(100), "tap", config, network_config.as_slice()).unwrap();
+
+        assert_eq!(
+            config.config.options,
+            Options {
+                dhcp: Some(true),
+                enable: Some(true),
+                ipfilter: Some(false),
+                ndp: Some(true),
+                radv: Some(true),
+                log_level_in: Some(LogLevel::Emergency),
+                log_level_out: Some(LogLevel::Critical),
+                macfilter: Some(false),
+                policy_in: Some(Verdict::Reject),
+                policy_out: Some(Verdict::Reject),
+            }
+        );
+    }
+}
diff --git a/proxmox-ve-config/src/firewall/mod.rs b/proxmox-ve-config/src/firewall/mod.rs
index 85fe6c4..afc3dcc 100644
--- a/proxmox-ve-config/src/firewall/mod.rs
+++ b/proxmox-ve-config/src/firewall/mod.rs
@@ -1,5 +1,6 @@
 pub mod cluster;
 pub mod common;
+pub mod guest;
 pub mod host;
 pub mod ports;
 pub mod types;
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 15/39] config: firewall: add firewall macros
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (13 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 14/39] config: firewall: add guest-specific " Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 16/39] config: firewall: add conntrack helper types Stefan Hanreich
                   ` (24 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/resources/macros.json     | 914 ++++++++++++++++++++
 proxmox-ve-config/src/firewall/fw_macros.rs |  69 ++
 proxmox-ve-config/src/firewall/mod.rs       |   1 +
 3 files changed, 984 insertions(+)
 create mode 100644 proxmox-ve-config/resources/macros.json
 create mode 100644 proxmox-ve-config/src/firewall/fw_macros.rs

diff --git a/proxmox-ve-config/resources/macros.json b/proxmox-ve-config/resources/macros.json
new file mode 100644
index 0000000..67e1d89
--- /dev/null
+++ b/proxmox-ve-config/resources/macros.json
@@ -0,0 +1,914 @@
+{
+  "Amanda": {
+    "code": [
+      {
+        "dport": "10080",
+        "proto": "udp"
+      },
+      {
+        "dport": "10080",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Amanda Backup"
+  },
+  "Auth": {
+    "code": [
+      {
+        "dport": "113",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Auth (identd) traffic"
+  },
+  "BGP": {
+    "code": [
+      {
+        "dport": "179",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Border Gateway Protocol traffic"
+  },
+  "BitTorrent": {
+    "code": [
+      {
+        "dport": "6881:6889",
+        "proto": "tcp"
+      },
+      {
+        "dport": "6881",
+        "proto": "udp"
+      }
+    ],
+    "desc": "BitTorrent traffic for BitTorrent 3.1 and earlier"
+  },
+  "BitTorrent32": {
+    "code": [
+      {
+        "dport": "6881:6999",
+        "proto": "tcp"
+      },
+      {
+        "dport": "6881",
+        "proto": "udp"
+      }
+    ],
+    "desc": "BitTorrent traffic for BitTorrent 3.2 and later"
+  },
+  "CVS": {
+    "code": [
+      {
+        "dport": "2401",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Concurrent Versions System pserver traffic"
+  },
+  "Ceph": {
+    "code": [
+      {
+        "dport": "6789",
+        "proto": "tcp"
+      },
+      {
+        "dport": "3300",
+        "proto": "tcp"
+      },
+      {
+        "dport": "6800:7300",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Ceph Storage Cluster traffic (Ceph Monitors, OSD & MDS Daemons)"
+  },
+  "Citrix": {
+    "code": [
+      {
+        "dport": "1494",
+        "proto": "tcp"
+      },
+      {
+        "dport": "1604",
+        "proto": "udp"
+      },
+      {
+        "dport": "2598",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Citrix/ICA traffic (ICA, ICA Browser, CGP)"
+  },
+  "DAAP": {
+    "code": [
+      {
+        "dport": "3689",
+        "proto": "tcp"
+      },
+      {
+        "dport": "3689",
+        "proto": "udp"
+      }
+    ],
+    "desc": "Digital Audio Access Protocol traffic (iTunes, Rythmbox daemons)"
+  },
+  "DCC": {
+    "code": [
+      {
+        "dport": "6277",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Distributed Checksum Clearinghouse spam filtering mechanism"
+  },
+  "DHCPfwd": {
+    "code": [
+      {
+        "dport": "67:68",
+        "proto": "udp",
+        "sport": "67:68"
+      }
+    ],
+    "desc": "Forwarded DHCP traffic"
+  },
+  "DHCPv6": {
+    "code": [
+      {
+        "dport": "546:547",
+        "proto": "udp",
+        "sport": "546:547"
+      }
+    ],
+    "desc": "DHCPv6 traffic"
+  },
+  "DNS": {
+    "code": [
+      {
+        "dport": "53",
+        "proto": "udp"
+      },
+      {
+        "dport": "53",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Domain Name System traffic (upd and tcp)"
+  },
+  "Distcc": {
+    "code": [
+      {
+        "dport": "3632",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Distributed Compiler service"
+  },
+  "FTP": {
+    "code": [
+      {
+        "dport": "21",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "File Transfer Protocol"
+  },
+  "Finger": {
+    "code": [
+      {
+        "dport": "79",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Finger protocol (RFC 742)"
+  },
+  "GNUnet": {
+    "code": [
+      {
+        "dport": "2086",
+        "proto": "tcp"
+      },
+      {
+        "dport": "2086",
+        "proto": "udp"
+      },
+      {
+        "dport": "1080",
+        "proto": "tcp"
+      },
+      {
+        "dport": "1080",
+        "proto": "udp"
+      }
+    ],
+    "desc": "GNUnet secure peer-to-peer networking traffic"
+  },
+  "GRE": {
+    "code": [
+      {
+        "proto": "47"
+      }
+    ],
+    "desc": "Generic Routing Encapsulation tunneling protocol"
+  },
+  "Git": {
+    "code": [
+      {
+        "dport": "9418",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Git distributed revision control traffic"
+  },
+  "HKP": {
+    "code": [
+      {
+        "dport": "11371",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "OpenPGP HTTP key server protocol traffic"
+  },
+  "HTTP": {
+    "code": [
+      {
+        "dport": "80",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Hypertext Transfer Protocol (WWW)"
+  },
+  "HTTPS": {
+    "code": [
+      {
+        "dport": "443",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Hypertext Transfer Protocol (WWW) over SSL"
+  },
+  "HTTP/3": {
+    "code": [
+      {
+        "dport": "443",
+        "proto": "udp"
+      }
+    ],
+    "desc": "Hypertext Transfer Protocol v3"
+  },
+  "ICPV2": {
+    "code": [
+      {
+        "dport": "3130",
+        "proto": "udp"
+      }
+    ],
+    "desc": "Internet Cache Protocol V2 (Squid) traffic"
+  },
+  "ICQ": {
+    "code": [
+      {
+        "dport": "5190",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "AOL Instant Messenger traffic"
+  },
+  "IMAP": {
+    "code": [
+      {
+        "dport": "143",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Internet Message Access Protocol"
+  },
+  "IMAPS": {
+    "code": [
+      {
+        "dport": "993",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Internet Message Access Protocol over SSL"
+  },
+  "IPIP": {
+    "code": [
+      {
+        "proto": "94"
+      }
+    ],
+    "desc": "IPIP capsulation traffic"
+  },
+  "IPsec": {
+    "code": [
+      {
+        "dport": "500",
+        "proto": "udp",
+        "sport": "500"
+      },
+      {
+        "proto": "50"
+      }
+    ],
+    "desc": "IPsec traffic"
+  },
+  "IPsecah": {
+    "code": [
+      {
+        "dport": "500",
+        "proto": "udp",
+        "sport": "500"
+      },
+      {
+        "proto": "51"
+      }
+    ],
+    "desc": "IPsec authentication (AH) traffic"
+  },
+  "IPsecnat": {
+    "code": [
+      {
+        "dport": "500",
+        "proto": "udp"
+      },
+      {
+        "dport": "4500",
+        "proto": "udp"
+      },
+      {
+        "proto": "50"
+      }
+    ],
+    "desc": "IPsec traffic and Nat-Traversal"
+  },
+  "IRC": {
+    "code": [
+      {
+        "dport": "6667",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Internet Relay Chat traffic"
+  },
+  "Jetdirect": {
+    "code": [
+      {
+        "dport": "9100",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "HP Jetdirect printing"
+  },
+  "L2TP": {
+    "code": [
+      {
+        "dport": "1701",
+        "proto": "udp"
+      }
+    ],
+    "desc": "Layer 2 Tunneling Protocol traffic"
+  },
+  "LDAP": {
+    "code": [
+      {
+        "dport": "389",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Lightweight Directory Access Protocol traffic"
+  },
+  "LDAPS": {
+    "code": [
+      {
+        "dport": "636",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Secure Lightweight Directory Access Protocol traffic"
+  },
+  "MDNS": {
+    "code": [
+      {
+        "dport": "5353",
+        "proto": "udp"
+      }
+    ],
+    "desc": "Multicast DNS"
+  },
+  "MSNP": {
+    "code": [
+      {
+        "dport": "1863",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Microsoft Notification Protocol"
+  },
+  "MSSQL": {
+    "code": [
+      {
+        "dport": "1433",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Microsoft SQL Server"
+  },
+  "Mail": {
+    "code": [
+      {
+        "dport": "25",
+        "proto": "tcp"
+      },
+      {
+        "dport": "465",
+        "proto": "tcp"
+      },
+      {
+        "dport": "587",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Mail traffic (SMTP, SMTPS, Submission)"
+  },
+  "Munin": {
+    "code": [
+      {
+        "dport": "4949",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Munin networked resource monitoring traffic"
+  },
+  "MySQL": {
+    "code": [
+      {
+        "dport": "3306",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "MySQL server"
+  },
+  "NNTP": {
+    "code": [
+      {
+        "dport": "119",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "NNTP traffic (Usenet)."
+  },
+  "NNTPS": {
+    "code": [
+      {
+        "dport": "563",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Encrypted NNTP traffic (Usenet)"
+  },
+  "NTP": {
+    "code": [
+      {
+        "dport": "123",
+        "proto": "udp"
+      }
+    ],
+    "desc": "Network Time Protocol (ntpd)"
+  },
+  "NeighborDiscovery": {
+    "code": [
+      {
+        "dport": "nd-router-solicit",
+        "proto": "icmpv6"
+      },
+      {
+        "dport": "nd-router-advert",
+        "proto": "icmpv6"
+      },
+      {
+        "dport": "nd-neighbor-solicit",
+        "proto": "icmpv6"
+      },
+      {
+        "dport": "nd-neighbor-advert",
+        "proto": "icmpv6"
+      }
+    ],
+    "desc": "IPv6 neighbor solicitation, neighbor and router advertisement"
+  },
+  "OSPF": {
+    "code": [
+      {
+        "proto": "89"
+      }
+    ],
+    "desc": "OSPF multicast traffic"
+  },
+  "OpenVPN": {
+    "code": [
+      {
+        "dport": "1194",
+        "proto": "udp"
+      }
+    ],
+    "desc": "OpenVPN traffic"
+  },
+  "PBS": {
+    "code": [
+      {
+        "dport": "8007",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Proxmox Backup Server"
+  },
+  "PCA": {
+    "code": [
+      {
+        "dport": "5632",
+        "proto": "udp"
+      },
+      {
+        "dport": "5631",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Symantec PCAnywere (tm)"
+  },
+  "PMG": {
+    "code": [
+      {
+        "dport": "8006",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Proxmox Mail Gateway web interface"
+  },
+  "POP3": {
+    "code": [
+      {
+        "dport": "110",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "POP3 traffic"
+  },
+  "POP3S": {
+    "code": [
+      {
+        "dport": "995",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Encrypted POP3 traffic"
+  },
+  "PPtP": {
+    "code": [
+      {
+        "proto": "47"
+      },
+      {
+        "dport": "1723",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Point-to-Point Tunneling Protocol"
+  },
+  "Ping": {
+    "code": [
+      {
+        "dport": "echo-request",
+        "proto": "icmp"
+      }
+    ],
+    "desc": "ICMP echo request"
+  },
+  "PostgreSQL": {
+    "code": [
+      {
+        "dport": "5432",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "PostgreSQL server"
+  },
+  "Printer": {
+    "code": [
+      {
+        "dport": "515",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Line Printer protocol printing"
+  },
+  "RDP": {
+    "code": [
+      {
+        "dport": "3389",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Microsoft Remote Desktop Protocol traffic"
+  },
+  "RIP": {
+    "code": [
+      {
+        "dport": "520",
+        "proto": "udp"
+      }
+    ],
+    "desc": "Routing Information Protocol (bidirectional)"
+  },
+  "RNDC": {
+    "code": [
+      {
+        "dport": "953",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "BIND remote management protocol"
+  },
+  "Razor": {
+    "code": [
+      {
+        "dport": "2703",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Razor Antispam System"
+  },
+  "Rdate": {
+    "code": [
+      {
+        "dport": "37",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Remote time retrieval (rdate)"
+  },
+  "Rsync": {
+    "code": [
+      {
+        "dport": "873",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Rsync server"
+  },
+  "SANE": {
+    "code": [
+      {
+        "dport": "6566",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "SANE network scanning"
+  },
+  "SMB": {
+    "code": [
+      {
+        "dport": "135,445",
+        "proto": "udp"
+      },
+      {
+        "dport": "137:139",
+        "proto": "udp"
+      },
+      {
+        "dport": "1024:65535",
+        "proto": "udp",
+        "sport": "137"
+      },
+      {
+        "dport": "135,139,445",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Microsoft SMB traffic"
+  },
+  "SMBswat": {
+    "code": [
+      {
+        "dport": "901",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Samba Web Administration Tool"
+  },
+  "SMTP": {
+    "code": [
+      {
+        "dport": "25",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Simple Mail Transfer Protocol"
+  },
+  "SMTPS": {
+    "code": [
+      {
+        "dport": "465",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Encrypted Simple Mail Transfer Protocol"
+  },
+  "SNMP": {
+    "code": [
+      {
+        "dport": "161:162",
+        "proto": "udp"
+      },
+      {
+        "dport": "161",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Simple Network Management Protocol"
+  },
+  "SPAMD": {
+    "code": [
+      {
+        "dport": "783",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Spam Assassin SPAMD traffic"
+  },
+  "SSH": {
+    "code": [
+      {
+        "dport": "22",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Secure shell traffic"
+  },
+  "SVN": {
+    "code": [
+      {
+        "dport": "3690",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Subversion server (svnserve)"
+  },
+  "SixXS": {
+    "code": [
+      {
+        "dport": "3874",
+        "proto": "tcp"
+      },
+      {
+        "dport": "3740",
+        "proto": "udp"
+      },
+      {
+        "proto": "41"
+      },
+      {
+        "dport": "5072,8374",
+        "proto": "udp"
+      }
+    ],
+    "desc": "SixXS IPv6 Deployment and Tunnel Broker"
+  },
+  "Squid": {
+    "code": [
+      {
+        "dport": "3128",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Squid web proxy traffic"
+  },
+  "Submission": {
+    "code": [
+      {
+        "dport": "587",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Mail message submission traffic"
+  },
+  "Syslog": {
+    "code": [
+      {
+        "dport": "514",
+        "proto": "udp"
+      },
+      {
+        "dport": "514",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Syslog protocol (RFC 5424) traffic"
+  },
+  "TFTP": {
+    "code": [
+      {
+        "dport": "69",
+        "proto": "udp"
+      }
+    ],
+    "desc": "Trivial File Transfer Protocol traffic"
+  },
+  "Telnet": {
+    "code": [
+      {
+        "dport": "23",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Telnet traffic"
+  },
+  "Telnets": {
+    "code": [
+      {
+        "dport": "992",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Telnet over SSL"
+  },
+  "Time": {
+    "code": [
+      {
+        "dport": "37",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "RFC 868 Time protocol"
+  },
+  "Trcrt": {
+    "code": [
+      {
+        "dport": "33434:33524",
+        "proto": "udp"
+      },
+      {
+        "dport": "echo-request",
+        "proto": "icmp"
+      }
+    ],
+    "desc": "Traceroute (for up to 30 hops) traffic"
+  },
+  "VNC": {
+    "code": [
+      {
+        "dport": "5900:5999",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "VNC traffic for VNC display's 0 - 99"
+  },
+  "VNCL": {
+    "code": [
+      {
+        "dport": "5500",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "VNC traffic from Vncservers to Vncviewers in listen mode"
+  },
+  "Web": {
+    "code": [
+      {
+        "dport": "80",
+        "proto": "tcp"
+      },
+      {
+        "dport": "443",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "WWW traffic (HTTP and HTTPS)"
+  },
+  "Webcache": {
+    "code": [
+      {
+        "dport": "8080",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Web Cache/Proxy traffic (port 8080)"
+  },
+  "Webmin": {
+    "code": [
+      {
+        "dport": "10000",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Webmin traffic"
+  },
+  "Whois": {
+    "code": [
+      {
+        "dport": "43",
+        "proto": "tcp"
+      }
+    ],
+    "desc": "Whois (nicname, RFC 3912) traffic"
+  }
+}
diff --git a/proxmox-ve-config/src/firewall/fw_macros.rs b/proxmox-ve-config/src/firewall/fw_macros.rs
new file mode 100644
index 0000000..5fa8dab
--- /dev/null
+++ b/proxmox-ve-config/src/firewall/fw_macros.rs
@@ -0,0 +1,69 @@
+use std::collections::HashMap;
+
+use serde::Deserialize;
+use std::sync::OnceLock;
+
+use crate::firewall::types::rule_match::Protocol;
+
+use super::types::rule_match::RuleOptions;
+
+#[derive(Clone, Debug, Default, Deserialize)]
+struct FwMacroData {
+    #[serde(rename = "desc")]
+    pub description: &'static str,
+    pub code: Vec<RuleOptions>,
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct FwMacro {
+    pub _description: &'static str,
+    pub code: Vec<Protocol>,
+}
+
+fn macros() -> &'static HashMap<String, FwMacro> {
+    const MACROS: &str = include_str!("../../resources/macros.json");
+    static HASHMAP: OnceLock<HashMap<String, FwMacro>> = OnceLock::new();
+
+    HASHMAP.get_or_init(|| {
+        let macro_data: HashMap<String, FwMacroData> = match serde_json::from_str(MACROS) {
+            Ok(m) => m,
+            Err(err) => {
+                log::error!("could not load data for macros: {err}");
+                HashMap::new()
+            }
+        };
+
+        let mut macros = HashMap::new();
+
+        'outer: for (name, data) in macro_data {
+            let mut code = Vec::new();
+
+            for c in data.code {
+                match Protocol::from_options(&c) {
+                    Ok(Some(p)) => code.push(p),
+                    Ok(None) => {
+                        continue 'outer;
+                    }
+                    Err(err) => {
+                        log::error!("could not parse data for macro {name}: {err}");
+                        continue 'outer;
+                    }
+                }
+            }
+
+            macros.insert(
+                name,
+                FwMacro {
+                    _description: data.description,
+                    code,
+                },
+            );
+        }
+
+        macros
+    })
+}
+
+pub fn get_macro(name: &str) -> Option<&'static FwMacro> {
+    macros().get(name)
+}
diff --git a/proxmox-ve-config/src/firewall/mod.rs b/proxmox-ve-config/src/firewall/mod.rs
index afc3dcc..0f438ca 100644
--- a/proxmox-ve-config/src/firewall/mod.rs
+++ b/proxmox-ve-config/src/firewall/mod.rs
@@ -1,5 +1,6 @@
 pub mod cluster;
 pub mod common;
+pub mod fw_macros;
 pub mod guest;
 pub mod host;
 pub mod ports;
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 16/39] config: firewall: add conntrack helper types
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (14 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 15/39] config: firewall: add firewall macros Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 17/39] nftables: add crate for libnftables bindings Stefan Hanreich
                   ` (23 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/resources/ct_helper.json  |  52 +++++++++
 proxmox-ve-config/src/firewall/ct_helper.rs | 115 ++++++++++++++++++++
 proxmox-ve-config/src/firewall/mod.rs       |   1 +
 3 files changed, 168 insertions(+)
 create mode 100644 proxmox-ve-config/resources/ct_helper.json
 create mode 100644 proxmox-ve-config/src/firewall/ct_helper.rs

diff --git a/proxmox-ve-config/resources/ct_helper.json b/proxmox-ve-config/resources/ct_helper.json
new file mode 100644
index 0000000..5e70a3a
--- /dev/null
+++ b/proxmox-ve-config/resources/ct_helper.json
@@ -0,0 +1,52 @@
+[
+  {
+    "name": "amanda",
+    "v4": true,
+    "v6": true,
+    "udp": 10080
+  },
+  {
+    "name": "ftp",
+    "v4": true,
+    "v6": true,
+    "tcp": 21
+  } ,
+  {
+    "name": "irc",
+    "v4": true,
+    "tcp": 6667
+  },
+  {
+    "name": "netbios-ns",
+    "v4": true,
+    "udp": 137
+  },
+  {
+    "name": "pptp",
+    "v4": true,
+    "tcp": 1723
+  },
+  {
+    "name": "sane",
+    "v4": true,
+    "v6": true,
+    "tcp": 6566
+  },
+  {
+    "name": "sip",
+    "v4": true,
+    "v6": true,
+    "udp": 5060
+  },
+  {
+    "name": "snmp",
+    "v4": true,
+    "udp": 161
+  },
+  {
+    "name": "tftp",
+    "v4": true,
+    "v6": true,
+    "udp": 69
+  }
+]
diff --git a/proxmox-ve-config/src/firewall/ct_helper.rs b/proxmox-ve-config/src/firewall/ct_helper.rs
new file mode 100644
index 0000000..40e4fee
--- /dev/null
+++ b/proxmox-ve-config/src/firewall/ct_helper.rs
@@ -0,0 +1,115 @@
+use anyhow::{bail, Error};
+use serde::Deserialize;
+use std::collections::HashMap;
+use std::sync::OnceLock;
+
+use crate::firewall::types::address::Family;
+use crate::firewall::types::rule_match::{Ports, Protocol, Tcp, Udp};
+
+#[derive(Clone, Debug, Deserialize)]
+pub struct CtHelperMacroJson {
+    pub v4: Option<bool>,
+    pub v6: Option<bool>,
+    pub name: String,
+    pub tcp: Option<u16>,
+    pub udp: Option<u16>,
+}
+
+impl TryFrom<CtHelperMacroJson> for CtHelperMacro {
+    type Error = Error;
+
+    fn try_from(value: CtHelperMacroJson) -> Result<Self, Self::Error> {
+        if value.tcp.is_none() && value.udp.is_none() {
+            bail!("Neither TCP nor UDP port set in CT helper!");
+        }
+
+        let family = match (value.v4, value.v6) {
+            (Some(true), Some(true)) => None,
+            (Some(true), _) => Some(Family::V4),
+            (_, Some(true)) => Some(Family::V6),
+            _ => bail!("Neither v4 nor v6 set in CT Helper Macro!"),
+        };
+
+        let mut ct_helper = CtHelperMacro {
+            family,
+            name: value.name,
+            tcp: None,
+            udp: None,
+        };
+
+        if let Some(dport) = value.tcp {
+            let ports = Ports::from_u16(None, dport);
+            ct_helper.tcp = Some(Tcp::new(ports).into());
+        }
+
+        if let Some(dport) = value.udp {
+            let ports = Ports::from_u16(None, dport);
+            ct_helper.udp = Some(Udp::new(ports).into());
+        }
+
+        Ok(ct_helper)
+    }
+}
+
+#[derive(Clone, Debug, Deserialize)]
+#[serde(try_from = "CtHelperMacroJson")]
+pub struct CtHelperMacro {
+    family: Option<Family>,
+    name: String,
+    tcp: Option<Protocol>,
+    udp: Option<Protocol>,
+}
+
+impl CtHelperMacro {
+    fn helper_name(&self, protocol: &str) -> String {
+        format!("helper-{}-{protocol}", self.name)
+    }
+
+    pub fn tcp_helper_name(&self) -> String {
+        self.helper_name("tcp")
+    }
+
+    pub fn udp_helper_name(&self) -> String {
+        self.helper_name("udp")
+    }
+
+    pub fn family(&self) -> Option<Family> {
+        self.family
+    }
+
+    pub fn name(&self) -> &str {
+        self.name.as_ref()
+    }
+
+    pub fn tcp(&self) -> Option<&Protocol> {
+        self.tcp.as_ref()
+    }
+
+    pub fn udp(&self) -> Option<&Protocol> {
+        self.udp.as_ref()
+    }
+}
+
+fn hashmap() -> &'static HashMap<String, CtHelperMacro> {
+    const MACROS: &str = include_str!("../../resources/ct_helper.json");
+    static HASHMAP: OnceLock<HashMap<String, CtHelperMacro>> = OnceLock::new();
+
+    HASHMAP.get_or_init(|| {
+        let macro_data: Vec<CtHelperMacro> = match serde_json::from_str(MACROS) {
+            Ok(data) => data,
+            Err(err) => {
+                log::error!("could not load data for ct helpers: {err}");
+                Vec::new()
+            }
+        };
+
+        macro_data
+            .into_iter()
+            .map(|elem| (elem.name.clone(), elem))
+            .collect()
+    })
+}
+
+pub fn get_cthelper(name: &str) -> Option<&'static CtHelperMacro> {
+    hashmap().get(name)
+}
diff --git a/proxmox-ve-config/src/firewall/mod.rs b/proxmox-ve-config/src/firewall/mod.rs
index 0f438ca..2cf57e2 100644
--- a/proxmox-ve-config/src/firewall/mod.rs
+++ b/proxmox-ve-config/src/firewall/mod.rs
@@ -1,5 +1,6 @@
 pub mod cluster;
 pub mod common;
+pub mod ct_helper;
 pub mod fw_macros;
 pub mod guest;
 pub mod host;
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 17/39] nftables: add crate for libnftables bindings
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (15 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 16/39] config: firewall: add conntrack helper types Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 18/39] nftables: add helpers Stefan Hanreich
                   ` (22 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 Cargo.toml                  |  1 +
 proxmox-nftables/Cargo.toml | 16 ++++++++++++++++
 proxmox-nftables/src/lib.rs |  0
 3 files changed, 17 insertions(+)
 create mode 100644 proxmox-nftables/Cargo.toml
 create mode 100644 proxmox-nftables/src/lib.rs

diff --git a/Cargo.toml b/Cargo.toml
index a8d33ab..877f103 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,4 +1,5 @@
 [workspace]
 members = [
     "proxmox-ve-config",
+    "proxmox-nftables",
 ]
diff --git a/proxmox-nftables/Cargo.toml b/proxmox-nftables/Cargo.toml
new file mode 100644
index 0000000..764e231
--- /dev/null
+++ b/proxmox-nftables/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "proxmox-nftables"
+version = "0.1.0"
+edition = "2021"
+authors = [
+    "Wolfgang Bumiller <w.bumiller@proxmox.com>",
+    "Stefan Hanreich <s.hanreich@proxmox.com>",
+    "Proxmox Support Team <support@proxmox.com>",
+]
+description = "Proxmox VE nftables"
+license = "AGPL-3"
+
+[dependencies]
+log = "0.4"
+
+proxmox-ve-config = { path = "../proxmox-ve-config", optional = true }
diff --git a/proxmox-nftables/src/lib.rs b/proxmox-nftables/src/lib.rs
new file mode 100644
index 0000000..e69de29
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 18/39] nftables: add helpers
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (16 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 17/39] nftables: add crate for libnftables bindings Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 19/39] nftables: expression: add types Stefan Hanreich
                   ` (21 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

Several objects, statements and expressions in nftables-json require
null values, for instance:

    { "flush": { "ruleset": null }}

For this purpose we define our own Null type, which we can then easily
use for defining types that accept Null as value.

Several keys accept as value either a singular element (string or
object) if there is only one object, but an array if there are
multiple objects. For instance when adding a single element to a set:

           { "element": {
	       ...
	       "elem": "element1"
           }}

but when adding multiple elements:

           { "element": {
	       ...
	       "elem": ["element1", "element2"]
           }}

NfVec<T> is a wrapper for Vec<T> that serializes into T iff Vec
contains one element, otherwise it serializes like a Vec would
normally do.

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-nftables/Cargo.toml    |   4 +
 proxmox-nftables/src/helper.rs | 190 +++++++++++++++++++++++++++++++++
 proxmox-nftables/src/lib.rs    |   1 +
 3 files changed, 195 insertions(+)
 create mode 100644 proxmox-nftables/src/helper.rs

diff --git a/proxmox-nftables/Cargo.toml b/proxmox-nftables/Cargo.toml
index 764e231..ebece9d 100644
--- a/proxmox-nftables/Cargo.toml
+++ b/proxmox-nftables/Cargo.toml
@@ -13,4 +13,8 @@ license = "AGPL-3"
 [dependencies]
 log = "0.4"
 
+serde = { version = "1", features = [ "derive" ] }
+serde_json = "1"
+serde_plain = "1"
+
 proxmox-ve-config = { path = "../proxmox-ve-config", optional = true }
diff --git a/proxmox-nftables/src/helper.rs b/proxmox-nftables/src/helper.rs
new file mode 100644
index 0000000..77ce347
--- /dev/null
+++ b/proxmox-nftables/src/helper.rs
@@ -0,0 +1,190 @@
+use std::fmt;
+use std::marker::PhantomData;
+
+use serde::{Deserialize, Serialize};
+
+#[derive(Clone, Copy, Debug)]
+pub struct Null;
+
+impl<'de> Deserialize<'de> for Null {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        use serde::de::Error;
+
+        match Option::<()>::deserialize(deserializer)? {
+            None => Ok(Self),
+            Some(_) => Err(D::Error::custom("expected null")),
+        }
+    }
+}
+
+impl Serialize for Null {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        serializer.serialize_none()
+    }
+}
+
+impl fmt::Display for Null {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.write_str("null")
+    }
+}
+
+#[derive(Clone, Debug)]
+pub struct NfVec<T>(pub(crate) Vec<T>);
+
+impl<T> Default for NfVec<T> {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl<T> NfVec<T> {
+    pub const fn new() -> Self {
+        Self(Vec::new())
+    }
+
+    pub fn one(value: T) -> Self {
+        Self(vec![value])
+    }
+}
+
+impl<T> From<Vec<T>> for NfVec<T> {
+    fn from(v: Vec<T>) -> Self {
+        Self(v)
+    }
+}
+
+impl<T> From<NfVec<T>> for Vec<T> {
+    fn from(v: NfVec<T>) -> Self {
+        v.0
+    }
+}
+
+impl<T> FromIterator<T> for NfVec<T> {
+    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
+        Self(iter.into_iter().collect())
+    }
+}
+
+impl<T> std::ops::Deref for NfVec<T> {
+    type Target = Vec<T>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl<T> std::ops::DerefMut for NfVec<T> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
+}
+
+impl<T: Serialize> Serialize for NfVec<T> {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        if self.len() == 1 {
+            self[0].serialize(serializer)
+        } else {
+            self.0.serialize(serializer)
+        }
+    }
+}
+
+macro_rules! visit_value {
+    ($( ($visit:ident, $($ty:tt)+), )+) => {
+        $(
+            fn $visit<E>(self, value: $($ty)+) -> Result<Self::Value, E>
+            where
+                E: Error,
+            {
+                T::deserialize(value.into_deserializer()).map(NfVec::one)
+            }
+        )+
+    };
+}
+
+impl<'de, T: Deserialize<'de>> Deserialize<'de> for NfVec<T> {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        use serde::de::{Error, IntoDeserializer};
+
+        struct V<T>(PhantomData<T>);
+
+        impl<'de, T: Deserialize<'de>> serde::de::Visitor<'de> for V<T> {
+            type Value = NfVec<T>;
+
+            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
+                f.write_str("an array or single element")
+            }
+
+            fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
+            where
+                A: serde::de::SeqAccess<'de>,
+            {
+                Vec::<T>::deserialize(serde::de::value::SeqAccessDeserializer::new(seq)).map(NfVec)
+            }
+
+            fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
+            where
+                A: serde::de::MapAccess<'de>,
+            {
+                T::deserialize(serde::de::value::MapAccessDeserializer::new(map)).map(NfVec::one)
+            }
+
+            fn visit_none<E>(self) -> Result<Self::Value, E>
+            where
+                E: Error,
+            {
+                Ok(NfVec::new())
+            }
+
+            fn visit_unit<E>(self) -> Result<Self::Value, E>
+            where
+                E: Error,
+            {
+                Ok(NfVec::new())
+            }
+
+            fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
+            where
+                D: serde::Deserializer<'de>,
+            {
+                deserializer.deserialize_any(self)
+            }
+
+            visit_value! {
+                (visit_bool, bool),
+                (visit_borrowed_bytes, &'de [u8]),
+                (visit_borrowed_str, &'de str),
+                (visit_byte_buf, Vec<u8>),
+                (visit_bytes, &[u8]),
+                (visit_char, char),
+                (visit_f32, f32),
+                (visit_f64, f64),
+                (visit_i8, i8),
+                (visit_i16, i16),
+                (visit_i32, i32),
+                (visit_i64, i64),
+                (visit_u8, u8),
+                (visit_u16, u16),
+                (visit_u32, u32),
+                (visit_u64, u64),
+                (visit_str, &str),
+                (visit_string, String),
+            }
+        }
+
+        deserializer.deserialize_any(V::<T>(PhantomData))
+    }
+}
diff --git a/proxmox-nftables/src/lib.rs b/proxmox-nftables/src/lib.rs
index e69de29..485bb81 100644
--- a/proxmox-nftables/src/lib.rs
+++ b/proxmox-nftables/src/lib.rs
@@ -0,0 +1 @@
+pub mod helper;
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 19/39] nftables: expression: add types
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (17 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 18/39] nftables: add helpers Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 20/39] nftables: expression: implement conversion traits for firewall config Stefan Hanreich
                   ` (20 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

Adds an enum containing most of the expressions defined in the
nftables-json schema [1].

[1] https://manpages.debian.org/bookworm/libnftables1/libnftables-json.5.en.html#EXPRESSIONS

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-nftables/Cargo.toml        |   2 +-
 proxmox-nftables/src/expression.rs | 268 +++++++++++++++++++++++++++++
 proxmox-nftables/src/lib.rs        |   4 +
 proxmox-nftables/src/types.rs      |  53 ++++++
 4 files changed, 326 insertions(+), 1 deletion(-)
 create mode 100644 proxmox-nftables/src/expression.rs
 create mode 100644 proxmox-nftables/src/types.rs

diff --git a/proxmox-nftables/Cargo.toml b/proxmox-nftables/Cargo.toml
index ebece9d..909869b 100644
--- a/proxmox-nftables/Cargo.toml
+++ b/proxmox-nftables/Cargo.toml
@@ -17,4 +17,4 @@ serde = { version = "1", features = [ "derive" ] }
 serde_json = "1"
 serde_plain = "1"
 
-proxmox-ve-config = { path = "../proxmox-ve-config", optional = true }
+proxmox-ve-config = { path = "../proxmox-ve-config" }
diff --git a/proxmox-nftables/src/expression.rs b/proxmox-nftables/src/expression.rs
new file mode 100644
index 0000000..5478291
--- /dev/null
+++ b/proxmox-nftables/src/expression.rs
@@ -0,0 +1,268 @@
+use crate::types::{ElemConfig, Verdict};
+use serde::{Deserialize, Serialize};
+use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
+
+use crate::helper::NfVec;
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum Expression {
+    Concat(Vec<Expression>),
+    Set(Vec<Expression>),
+    Range(Box<(Expression, Expression)>),
+    Map(Box<Map>),
+    Prefix(Prefix),
+    Payload(Payload),
+    Meta(Meta),
+    Ct(Ct),
+    Elem(Box<Element>),
+
+    #[serde(rename = "|")]
+    Or(Box<(Expression, Expression)>),
+    #[serde(rename = "&")]
+    And(Box<(Expression, Expression)>),
+    #[serde(rename = "^")]
+    Xor(Box<(Expression, Expression)>),
+    #[serde(rename = "<<")]
+    ShiftLeft(Box<(Expression, Expression)>),
+    #[serde(rename = ">>")]
+    ShiftRight(Box<(Expression, Expression)>),
+
+    #[serde(untagged)]
+    List(Vec<Expression>),
+
+    #[serde(untagged)]
+    Verdict(Verdict),
+
+    #[serde(untagged)]
+    Bool(bool),
+    #[serde(untagged)]
+    Number(i64),
+    #[serde(untagged)]
+    String(String),
+}
+
+impl Expression {
+    pub fn set(expressions: impl IntoIterator<Item = Expression>) -> Self {
+        Expression::Set(Vec::from_iter(expressions))
+    }
+
+    pub fn concat(expressions: impl IntoIterator<Item = Expression>) -> Self {
+        Expression::Concat(Vec::from_iter(expressions))
+    }
+}
+
+impl From<bool> for Expression {
+    #[inline]
+    fn from(v: bool) -> Self {
+        Expression::Bool(v)
+    }
+}
+
+impl From<i64> for Expression {
+    #[inline]
+    fn from(v: i64) -> Self {
+        Expression::Number(v)
+    }
+}
+
+impl From<u16> for Expression {
+    #[inline]
+    fn from(v: u16) -> Self {
+        Expression::Number(v.into())
+    }
+}
+
+impl From<u8> for Expression {
+    #[inline]
+    fn from(v: u8) -> Self {
+        Expression::Number(v.into())
+    }
+}
+
+impl From<&str> for Expression {
+    #[inline]
+    fn from(v: &str) -> Self {
+        Expression::String(v.to_string())
+    }
+}
+
+impl From<String> for Expression {
+    #[inline]
+    fn from(v: String) -> Self {
+        Expression::String(v)
+    }
+}
+
+impl From<Meta> for Expression {
+    #[inline]
+    fn from(meta: Meta) -> Self {
+        Expression::Meta(meta)
+    }
+}
+
+impl From<Ct> for Expression {
+    #[inline]
+    fn from(ct: Ct) -> Self {
+        Expression::Ct(ct)
+    }
+}
+
+impl From<Payload> for Expression {
+    #[inline]
+    fn from(payload: Payload) -> Self {
+        Expression::Payload(payload)
+    }
+}
+
+impl From<Prefix> for Expression {
+    #[inline]
+    fn from(prefix: Prefix) -> Self {
+        Expression::Prefix(prefix)
+    }
+}
+
+impl From<Verdict> for Expression {
+    #[inline]
+    fn from(value: Verdict) -> Self {
+        Expression::Verdict(value)
+    }
+}
+
+impl From<&IpAddr> for Expression {
+    fn from(value: &IpAddr) -> Self {
+        Expression::String(value.to_string())
+    }
+}
+
+impl From<&Ipv6Addr> for Expression {
+    fn from(address: &Ipv6Addr) -> Self {
+        Expression::String(address.to_string())
+    }
+}
+
+impl From<&Ipv4Addr> for Expression {
+    fn from(address: &Ipv4Addr) -> Self {
+        Expression::String(address.to_string())
+    }
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum IpFamily {
+    Ip,
+    Ip6,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct Meta {
+    key: String,
+}
+
+impl Meta {
+    pub fn new(key: impl Into<String>) -> Self {
+        Self { key: key.into() }
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct Map {
+    key: Expression,
+    data: Expression,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct Ct {
+    key: String,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    family: Option<IpFamily>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    dir: Option<CtDirection>,
+}
+
+impl Ct {
+    pub fn new(key: impl Into<String>, family: impl Into<Option<IpFamily>>) -> Self {
+        Self {
+            key: key.into(),
+            family: family.into(),
+            dir: None,
+        }
+    }
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum CtDirection {
+    Original,
+    Reply,
+}
+serde_plain::derive_display_from_serialize!(CtDirection);
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(untagged)]
+pub enum Payload {
+    Raw(PayloadRaw),
+    Field(PayloadField),
+}
+
+impl Payload {
+    pub fn field(protocol: impl Into<String>, field: impl Into<String>) -> Self {
+        Self::Field(PayloadField {
+            protocol: protocol.into(),
+            field: field.into(),
+        })
+    }
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
+pub enum PayloadBase {
+    #[serde(rename = "ll")]
+    Link,
+    #[serde(rename = "nh")]
+    Network,
+    #[serde(rename = "th")]
+    Transport,
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
+pub struct PayloadRaw {
+    base: PayloadBase,
+    offset: i64,
+    len: i64,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct PayloadField {
+    protocol: String,
+    field: String,
+}
+
+impl PayloadField {
+    pub fn protocol_for_ip_family(family: IpFamily) -> String {
+        match family {
+            IpFamily::Ip => "ip".to_string(),
+            IpFamily::Ip6 => "ip6".to_string(),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct Prefix {
+    addr: Box<Expression>,
+    len: u8,
+}
+
+impl Prefix {
+    pub fn new(addr: impl Into<Expression>, len: u8) -> Self {
+        Self {
+            addr: Box::new(addr.into()),
+            len,
+        }
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct Element {
+    #[serde(flatten)]
+    config: ElemConfig,
+    val: Expression,
+}
diff --git a/proxmox-nftables/src/lib.rs b/proxmox-nftables/src/lib.rs
index 485bb81..712858b 100644
--- a/proxmox-nftables/src/lib.rs
+++ b/proxmox-nftables/src/lib.rs
@@ -1 +1,5 @@
+pub mod expression;
 pub mod helper;
+pub mod types;
+
+pub use expression::Expression;
diff --git a/proxmox-nftables/src/types.rs b/proxmox-nftables/src/types.rs
new file mode 100644
index 0000000..942c866
--- /dev/null
+++ b/proxmox-nftables/src/types.rs
@@ -0,0 +1,53 @@
+use std::fmt::Display;
+
+use serde::{Deserialize, Serialize};
+
+use crate::helper::Null;
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "snake_case")]
+pub enum Verdict {
+    Accept(Null),
+    Drop(Null),
+    Continue(Null),
+    Return(Null),
+    Goto { target: String },
+    Jump { target: String },
+}
+
+impl Display for Verdict {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let output = match self {
+            Verdict::Accept(_) => "ACCEPT",
+            Verdict::Drop(_) => "DROP",
+            Verdict::Continue(_) => "CONTINUE",
+            Verdict::Return(_) => "RETURN",
+            Verdict::Jump { .. } => "JUMP",
+            Verdict::Goto { .. } => "GOTO",
+        };
+
+        f.write_str(output)
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct ElemConfig {
+    timeout: Option<i64>,
+    expires: Option<i64>,
+    comment: Option<String>,
+}
+
+impl ElemConfig {
+    pub fn new(
+        timeout: impl Into<Option<i64>>,
+        expires: impl Into<Option<i64>>,
+        comment: impl Into<Option<String>>,
+    ) -> Self {
+        Self {
+            timeout: timeout.into(),
+            expires: expires.into(),
+            comment: comment.into(),
+        }
+    }
+}
+
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 20/39] nftables: expression: implement conversion traits for firewall config
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (18 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 19/39] nftables: expression: add types Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 21/39] nftables: statement: add types Stefan Hanreich
                   ` (19 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

Some types from the firewall configuration map directly onto nftables
expressions. For those we implement conversion traits so we can
conveniently convert between the configuration types and the
respective nftables types.

Those are guarded behind a feature so the nftables crate can be used
standalone without having to pull in the proxmox-ve-config crate.

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-nftables/Cargo.toml        |   5 +-
 proxmox-nftables/src/expression.rs | 124 +++++++++++++++++++++++++++--
 2 files changed, 122 insertions(+), 7 deletions(-)

diff --git a/proxmox-nftables/Cargo.toml b/proxmox-nftables/Cargo.toml
index 909869b..7e607e8 100644
--- a/proxmox-nftables/Cargo.toml
+++ b/proxmox-nftables/Cargo.toml
@@ -10,6 +10,9 @@ authors = [
 description = "Proxmox VE nftables"
 license = "AGPL-3"
 
+[features]
+config-ext = ["dep:proxmox-ve-config"]
+
 [dependencies]
 log = "0.4"
 
@@ -17,4 +20,4 @@ serde = { version = "1", features = [ "derive" ] }
 serde_json = "1"
 serde_plain = "1"
 
-proxmox-ve-config = { path = "../proxmox-ve-config" }
+proxmox-ve-config = { path = "../proxmox-ve-config", optional = true }
diff --git a/proxmox-nftables/src/expression.rs b/proxmox-nftables/src/expression.rs
index 5478291..3b8ade0 100644
--- a/proxmox-nftables/src/expression.rs
+++ b/proxmox-nftables/src/expression.rs
@@ -2,7 +2,14 @@ use crate::types::{ElemConfig, Verdict};
 use serde::{Deserialize, Serialize};
 use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
 
-use crate::helper::NfVec;
+#[cfg(feature = "config-ext")]
+use proxmox_ve_config::firewall::types::address::{Family, IpEntry, IpList};
+#[cfg(feature = "config-ext")]
+use proxmox_ve_config::firewall::types::port::{PortEntry, PortList};
+#[cfg(feature = "config-ext")]
+use proxmox_ve_config::firewall::types::rule_match::{IcmpCode, IcmpType, Icmpv6Code, Icmpv6Type};
+#[cfg(feature = "config-ext")]
+use proxmox_ve_config::firewall::types::Cidr;
 
 #[derive(Clone, Debug, Deserialize, Serialize)]
 #[serde(rename_all = "lowercase")]
@@ -147,11 +154,88 @@ impl From<&Ipv4Addr> for Expression {
     }
 }
 
-#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
-#[serde(rename_all = "lowercase")]
-pub enum IpFamily {
-    Ip,
-    Ip6,
+#[cfg(feature = "config-ext")]
+impl From<&IpList> for Expression {
+    fn from(value: &IpList) -> Self {
+        if value.len() == 1 {
+            return Expression::from(value.first().unwrap());
+        }
+
+        Expression::set(value.iter().map(Expression::from))
+    }
+}
+
+#[cfg(feature = "config-ext")]
+impl From<&IpEntry> for Expression {
+    fn from(value: &IpEntry) -> Self {
+        match value {
+            IpEntry::Cidr(cidr) => Expression::from(Prefix::from(cidr)),
+            IpEntry::Range(beg, end) => Expression::Range(Box::new((beg.into(), end.into()))),
+        }
+    }
+}
+
+#[cfg(feature = "config-ext")]
+impl From<&IcmpType> for Expression {
+    fn from(value: &IcmpType) -> Self {
+        match value {
+            IcmpType::Numeric(id) => Expression::from(*id),
+            IcmpType::Named(name) => Expression::from(*name),
+        }
+    }
+}
+
+#[cfg(feature = "config-ext")]
+impl From<&IcmpCode> for Expression {
+    fn from(value: &IcmpCode) -> Self {
+        match value {
+            IcmpCode::Numeric(id) => Expression::from(*id),
+            IcmpCode::Named(name) => Expression::from(*name),
+        }
+    }
+}
+
+#[cfg(feature = "config-ext")]
+impl From<&Icmpv6Type> for Expression {
+    fn from(value: &Icmpv6Type) -> Self {
+        match value {
+            Icmpv6Type::Numeric(id) => Expression::from(*id),
+            Icmpv6Type::Named(name) => Expression::from(*name),
+        }
+    }
+}
+
+#[cfg(feature = "config-ext")]
+impl From<&Icmpv6Code> for Expression {
+    fn from(value: &Icmpv6Code) -> Self {
+        match value {
+            Icmpv6Code::Numeric(id) => Expression::from(*id),
+            Icmpv6Code::Named(name) => Expression::from(*name),
+        }
+    }
+}
+
+#[cfg(feature = "config-ext")]
+impl From<&PortEntry> for Expression {
+    fn from(value: &PortEntry) -> Self {
+        match value {
+            PortEntry::Port(port) => Expression::from(*port),
+            PortEntry::Range(beg, end) => {
+                Expression::Range(Box::new(((*beg).into(), (*end).into())))
+            }
+        }
+    }
+}
+
+#[cfg(feature = "config-ext")]
+impl From<&PortList> for Expression {
+    fn from(value: &PortList) -> Self {
+        if value.len() == 1 {
+            return Expression::from(value.first().unwrap());
+        }
+
+        Expression::set(value.iter().map(Expression::from))
+    }
 }
 
 #[derive(Clone, Debug, Deserialize, Serialize)]
@@ -197,6 +281,24 @@ pub enum CtDirection {
     Reply,
 }
 serde_plain::derive_display_from_serialize!(CtDirection);
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum IpFamily {
+    Ip,
+    Ip6,
+}
+
+#[cfg(feature = "config-ext")]
+impl From<Family> for IpFamily {
+    fn from(value: Family) -> Self {
+        match value {
+            Family::V4 => IpFamily::Ip,
+            Family::V6 => IpFamily::Ip6,
+        }
+    }
+}
+
 #[derive(Clone, Debug, Deserialize, Serialize)]
 #[serde(untagged)]
 pub enum Payload {
@@ -260,6 +362,16 @@ impl Prefix {
     }
 }
 
+#[cfg(feature = "config-ext")]
+impl From<&Cidr> for Prefix {
+    fn from(value: &Cidr) -> Self {
+        match value {
+            Cidr::Ipv4(cidr) => Self::new(cidr.address(), cidr.mask()),
+            Cidr::Ipv6(cidr) => Self::new(cidr.address(), cidr.mask()),
+        }
+    }
+}
+
 #[derive(Clone, Debug, Deserialize, Serialize)]
 pub struct Element {
     #[serde(flatten)]
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 21/39] nftables: statement: add types
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (19 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 20/39] nftables: expression: implement conversion traits for firewall config Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 22/39] nftables: statement: add conversion traits for config types Stefan Hanreich
                   ` (18 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

Adds an enum containing most of the statements defined in the
nftables-json schema [1].

[1] https://manpages.debian.org/bookworm/libnftables1/libnftables-json.5.en.html#STATEMENTS

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-nftables/Cargo.toml       |   2 +
 proxmox-nftables/src/lib.rs       |   2 +
 proxmox-nftables/src/statement.rs | 321 ++++++++++++++++++++++++++++++
 proxmox-nftables/src/types.rs     |  18 +-
 4 files changed, 342 insertions(+), 1 deletion(-)
 create mode 100644 proxmox-nftables/src/statement.rs

diff --git a/proxmox-nftables/Cargo.toml b/proxmox-nftables/Cargo.toml
index 7e607e8..e84509d 100644
--- a/proxmox-nftables/Cargo.toml
+++ b/proxmox-nftables/Cargo.toml
@@ -15,6 +15,8 @@ config-ext = ["dep:proxmox-ve-config"]
 
 [dependencies]
 log = "0.4"
+anyhow = "1"
+thiserror = "1"
 
 serde = { version = "1", features = [ "derive" ] }
 serde_json = "1"
diff --git a/proxmox-nftables/src/lib.rs b/proxmox-nftables/src/lib.rs
index 712858b..40f6bab 100644
--- a/proxmox-nftables/src/lib.rs
+++ b/proxmox-nftables/src/lib.rs
@@ -1,5 +1,7 @@
 pub mod expression;
 pub mod helper;
+pub mod statement;
 pub mod types;
 
 pub use expression::Expression;
+pub use statement::Statement;
diff --git a/proxmox-nftables/src/statement.rs b/proxmox-nftables/src/statement.rs
new file mode 100644
index 0000000..e6371f6
--- /dev/null
+++ b/proxmox-nftables/src/statement.rs
@@ -0,0 +1,321 @@
+use anyhow::{bail, Error};
+use serde::{Deserialize, Serialize};
+
+use crate::expression::Meta;
+use crate::helper::{NfVec, Null};
+use crate::types::{RateTimescale, RateUnit, Verdict};
+use crate::Expression;
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum Statement {
+    Match(Match),
+    Mangle(Mangle),
+    Limit(Limit),
+    Notrack(Null),
+    Reject(Reject),
+    Set(Set),
+    Log(Log),
+    #[serde(rename = "ct helper")]
+    CtHelper(String),
+    Vmap(Vmap),
+    Comment(String),
+
+    #[serde(untagged)]
+    Verdict(Verdict),
+}
+
+impl Statement {
+    pub const fn make_accept() -> Self {
+        Statement::Verdict(Verdict::Accept(Null))
+    }
+
+    pub const fn make_drop() -> Self {
+        Statement::Verdict(Verdict::Drop(Null))
+    }
+
+    pub const fn make_return() -> Self {
+        Statement::Verdict(Verdict::Return(Null))
+    }
+
+    pub const fn make_continue() -> Self {
+        Statement::Verdict(Verdict::Continue(Null))
+    }
+
+    pub fn jump(target: impl Into<String>) -> Self {
+        Statement::Verdict(Verdict::Jump {
+            target: target.into(),
+        })
+    }
+
+    pub fn goto(target: impl Into<String>) -> Self {
+        Statement::Verdict(Verdict::Goto {
+            target: target.into(),
+        })
+    }
+}
+
+impl From<Match> for Statement {
+    #[inline]
+    fn from(m: Match) -> Statement {
+        Statement::Match(m)
+    }
+}
+
+impl From<Mangle> for Statement {
+    #[inline]
+    fn from(m: Mangle) -> Statement {
+        Statement::Mangle(m)
+    }
+}
+
+impl From<Reject> for Statement {
+    #[inline]
+    fn from(m: Reject) -> Statement {
+        Statement::Reject(m)
+    }
+}
+
+impl From<Set> for Statement {
+    #[inline]
+    fn from(m: Set) -> Statement {
+        Statement::Set(m)
+    }
+}
+
+impl From<Vmap> for Statement {
+    #[inline]
+    fn from(m: Vmap) -> Statement {
+        Statement::Vmap(m)
+    }
+}
+
+impl From<Log> for Statement {
+    #[inline]
+    fn from(log: Log) -> Statement {
+        Statement::Log(log)
+    }
+}
+
+impl<T: Into<Limit>> From<T> for Statement {
+    #[inline]
+    fn from(limit: T) -> Statement {
+        Statement::Limit(limit.into())
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum RejectType {
+    #[serde(rename = "tcp reset")]
+    TcpRst,
+    IcmpX,
+    Icmp,
+    IcmpV6,
+}
+
+#[derive(Clone, Debug, Default, Deserialize, Serialize)]
+pub struct Reject {
+    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
+    ty: Option<RejectType>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    expr: Option<Expression>,
+}
+
+#[derive(Clone, Debug, Default, Deserialize, Serialize)]
+#[serde(rename_all = "kebab-case")]
+pub struct Log {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    prefix: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    group: Option<i64>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    snaplen: Option<i64>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    queue_threshold: Option<i64>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    level: Option<LogLevel>,
+
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    flags: NfVec<LogFlag>,
+}
+
+impl Log {
+    pub fn new_nflog(prefix: String, group: i64) -> Self {
+        Self {
+            prefix: Some(prefix),
+            group: Some(group),
+            ..Default::default()
+        }
+    }
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum LogLevel {
+    Emerg,
+    Alert,
+    Crit,
+    Err,
+    Warn,
+    Notice,
+    Info,
+    Debug,
+    Audit,
+}
+
+impl LogLevel {
+    pub fn nflog_level(&self) -> u8 {
+        match self {
+            LogLevel::Emerg => 0,
+            LogLevel::Alert => 1,
+            LogLevel::Crit => 2,
+            LogLevel::Err => 3,
+            LogLevel::Warn => 4,
+            LogLevel::Notice => 5,
+            LogLevel::Info => 6,
+            LogLevel::Debug => 7,
+            LogLevel::Audit => 7,
+        }
+    }
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum LogFlag {
+    #[serde(rename = "tcp sequence")]
+    TcpSequence,
+    #[serde(rename = "tcp options")]
+    TcpOptions,
+    #[serde(rename = "ip options")]
+    IpOptions,
+
+    Skuid,
+    Ether,
+    All,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(untagged)]
+pub enum Limit {
+    Named(String),
+    Anonymous(AnonymousLimit),
+}
+
+impl<T: Into<AnonymousLimit>> From<T> for Limit {
+    fn from(value: T) -> Self {
+        Limit::Anonymous(value.into())
+    }
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, Serialize, Default)]
+pub struct AnonymousLimit {
+    pub rate: i64,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub rate_unit: Option<RateUnit>,
+
+    pub per: RateTimescale,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub burst: Option<i64>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub burst_unit: Option<RateUnit>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub inv: Option<bool>,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct Vmap {
+    key: Expression,
+    data: Expression,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct Match {
+    op: Operator,
+    left: Expression,
+    right: Expression,
+}
+
+impl Match {
+    pub fn new(op: Operator, left: impl Into<Expression>, right: impl Into<Expression>) -> Self {
+        Self {
+            op,
+            left: left.into(),
+            right: right.into(),
+        }
+    }
+
+    pub fn new_eq(left: impl Into<Expression>, right: impl Into<Expression>) -> Self {
+        Self::new(Operator::Eq, left, right)
+    }
+
+    pub fn new_ne(left: impl Into<Expression>, right: impl Into<Expression>) -> Self {
+        Self::new(Operator::Ne, left, right)
+    }
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
+pub enum Operator {
+    #[serde(rename = "&")]
+    And,
+    #[serde(rename = "|")]
+    Or,
+    #[serde(rename = "^")]
+    Xor,
+    #[serde(rename = "<<")]
+    ShiftLeft,
+    #[serde(rename = ">>")]
+    ShiftRight,
+    #[serde(rename = "==")]
+    Eq,
+    #[serde(rename = "!=")]
+    Ne,
+    #[serde(rename = "<")]
+    Lt,
+    #[serde(rename = ">")]
+    Gt,
+    #[serde(rename = "<=")]
+    Le,
+    #[serde(rename = ">=")]
+    Ge,
+    #[serde(rename = "in")]
+    In,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct Mangle {
+    pub key: Expression,
+    pub value: Expression,
+}
+
+impl Mangle {
+    pub fn set_mark(value: impl Into<Expression>) -> Self {
+        Self {
+            key: Meta::new("mark").into(),
+            value: value.into(),
+        }
+    }
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum SetOperation {
+    Add,
+    Update,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct Set {
+    pub op: SetOperation,
+    pub elem: Expression,
+    pub set: String,
+    pub stmt: Option<NfVec<Statement>>,
+}
diff --git a/proxmox-nftables/src/types.rs b/proxmox-nftables/src/types.rs
index 942c866..a8ec599 100644
--- a/proxmox-nftables/src/types.rs
+++ b/proxmox-nftables/src/types.rs
@@ -30,6 +30,23 @@ impl Display for Verdict {
     }
 }
 
+#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
+pub enum RateUnit {
+    Packets,
+    Bytes,
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, Serialize, Default)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+#[serde(rename_all = "lowercase")]
+pub enum RateTimescale {
+    #[default]
+    Second,
+    Minute,
+    Hour,
+    Day,
+}
+
 #[derive(Clone, Debug, Deserialize, Serialize)]
 pub struct ElemConfig {
     timeout: Option<i64>,
@@ -50,4 +67,3 @@ impl ElemConfig {
         }
     }
 }
-
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 22/39] nftables: statement: add conversion traits for config types
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (20 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 21/39] nftables: statement: add types Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 23/39] nftables: commands: add types Stefan Hanreich
                   ` (17 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

Some types from the firewall configuration map directly onto nftables
statements. For those we implement conversion traits so we can
conveniently convert between the configuration types and the
respective nftables types.

As with the expressions, those are guarded behind a feature so the
nftables crate can be used standalone without having to pull in the
proxmox-ve-config crate.

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-nftables/src/statement.rs | 71 ++++++++++++++++++++++++++++++-
 1 file changed, 70 insertions(+), 1 deletion(-)

diff --git a/proxmox-nftables/src/statement.rs b/proxmox-nftables/src/statement.rs
index e6371f6..e89f678 100644
--- a/proxmox-nftables/src/statement.rs
+++ b/proxmox-nftables/src/statement.rs
@@ -1,6 +1,15 @@
 use anyhow::{bail, Error};
 use serde::{Deserialize, Serialize};
 
+#[cfg(feature = "config-ext")]
+use proxmox_ve_config::firewall::types::log::LogLevel as ConfigLogLevel;
+#[cfg(feature = "config-ext")]
+use proxmox_ve_config::firewall::types::log::LogRateLimit;
+#[cfg(feature = "config-ext")]
+use proxmox_ve_config::firewall::types::rule::Verdict as ConfigVerdict;
+#[cfg(feature = "config-ext")]
+use proxmox_ve_config::guest::types::Vmid;
+
 use crate::expression::Meta;
 use crate::helper::{NfVec, Null};
 use crate::types::{RateTimescale, RateUnit, Verdict};
@@ -104,7 +113,18 @@ impl<T: Into<Limit>> From<T> for Statement {
     }
 }
 
-#[derive(Clone, Debug, Deserialize, Serialize)]
+#[cfg(feature = "config-ext")]
+impl From<ConfigVerdict> for Statement {
+    fn from(value: ConfigVerdict) -> Self {
+        match value {
+            ConfigVerdict::Accept => Statement::make_accept(),
+            ConfigVerdict::Reject => Statement::make_drop(),
+            ConfigVerdict::Drop => Statement::make_drop(),
+        }
+    }
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
 #[serde(rename_all = "lowercase")]
 pub enum RejectType {
     #[serde(rename = "tcp reset")]
@@ -145,6 +165,22 @@ pub struct Log {
 }
 
 impl Log {
+    #[cfg(feature = "config-ext")]
+    pub fn generate_prefix(
+        vmid: impl Into<Option<Vmid>>,
+        log_level: LogLevel,
+        chain_name: &str,
+        verdict: ConfigVerdict,
+    ) -> String {
+        format!(
+            ":{}:{}:{}: {}: ",
+            vmid.into().unwrap_or(Vmid::new(0)),
+            log_level.nflog_level(),
+            chain_name,
+            verdict,
+        )
+    }
+
     pub fn new_nflog(prefix: String, group: i64) -> Self {
         Self {
             prefix: Some(prefix),
@@ -168,6 +204,25 @@ pub enum LogLevel {
     Audit,
 }
 
+#[cfg(feature = "config-ext")]
+impl TryFrom<ConfigLogLevel> for LogLevel {
+    type Error = Error;
+
+    fn try_from(value: ConfigLogLevel) -> Result<Self, Self::Error> {
+        match value {
+            ConfigLogLevel::Emergency => Ok(LogLevel::Emerg),
+            ConfigLogLevel::Alert => Ok(LogLevel::Alert),
+            ConfigLogLevel::Critical => Ok(LogLevel::Crit),
+            ConfigLogLevel::Error => Ok(LogLevel::Err),
+            ConfigLogLevel::Warning => Ok(LogLevel::Warn),
+            ConfigLogLevel::Notice => Ok(LogLevel::Notice),
+            ConfigLogLevel::Info => Ok(LogLevel::Info),
+            ConfigLogLevel::Debug => Ok(LogLevel::Debug),
+            _ => bail!("cannot convert config log level to nftables"),
+        }
+    }
+}
+
 impl LogLevel {
     pub fn nflog_level(&self) -> u8 {
         match self {
@@ -231,6 +286,20 @@ pub struct AnonymousLimit {
     pub inv: Option<bool>,
 }
 
+#[cfg(feature = "config-ext")]
+impl From<LogRateLimit> for AnonymousLimit {
+    fn from(config: LogRateLimit) -> Self {
+        AnonymousLimit {
+            rate: config.rate(),
+            per: config.per().into(),
+            rate_unit: None,
+            burst: Some(config.burst()),
+            burst_unit: None,
+            inv: None,
+        }
+    }
+}
+
 #[derive(Clone, Debug, Deserialize, Serialize)]
 pub struct Vmap {
     key: Expression,
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 23/39] nftables: commands: add types
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (21 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 22/39] nftables: statement: add conversion traits for config types Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 24/39] nftables: types: add conversion traits Stefan Hanreich
                   ` (16 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel

Add rust types for most of the nftables commands as defined by
libnftables-json [1].

Different commands require different keys to be set for the same type
of object. E.g. deleting an object usually only requires a name +
name of the container (table/chain/rule). Creating an object usually
requires a few more keys, depending on the type of object created.

In order to be able to model the different objects for the different
commands, I've created specific models for a command where necessary.
Parts that are common across multiple commands (e.g. names) have been
moved to their own structs, so they can be reused.

[1] https://manpages.debian.org/bookworm/libnftables1/libnftables-json.5.en.html#COMMAND_OBJECTS

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-nftables/src/command.rs | 233 ++++++++++
 proxmox-nftables/src/lib.rs     |   2 +
 proxmox-nftables/src/types.rs   | 770 +++++++++++++++++++++++++++++++-
 3 files changed, 1004 insertions(+), 1 deletion(-)
 create mode 100644 proxmox-nftables/src/command.rs

diff --git a/proxmox-nftables/src/command.rs b/proxmox-nftables/src/command.rs
new file mode 100644
index 0000000..193fe46
--- /dev/null
+++ b/proxmox-nftables/src/command.rs
@@ -0,0 +1,233 @@
+use std::ops::{Deref, DerefMut};
+
+use crate::helper::Null;
+use crate::types::*;
+use serde::{Deserialize, Serialize};
+
+#[derive(Clone, Debug, Default, Deserialize, Serialize)]
+pub struct Commands {
+    nftables: Vec<Command>,
+}
+
+impl Commands {
+    pub fn new(commands: Vec<Command>) -> Self {
+        Self { nftables: commands }
+    }
+}
+
+impl Deref for Commands {
+    type Target = Vec<Command>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.nftables
+    }
+}
+
+impl DerefMut for Commands {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.nftables
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum Command {
+    Add(Add),
+    Create(Add),
+    Delete(Delete),
+    Flush(Flush),
+    List(List),
+    // Insert(super::Rule),
+    // Rename(RenameChain),
+    // Replace(super::Rule),
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum List {
+    Chains(Null),
+    Sets(Null),
+}
+
+impl List {
+    #[inline]
+    pub fn chains() -> Command {
+        Command::List(List::Chains(Null))
+    }
+
+    #[inline]
+    pub fn sets() -> Command {
+        Command::List(List::Sets(Null))
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum Add {
+    Table(AddTable),
+    Chain(AddChain),
+    Rule(AddRule),
+    Set(AddSet),
+    Map(AddMap),
+    Limit(AddLimit),
+    Element(AddElement),
+    #[serde(rename = "ct helper")]
+    CtHelper(AddCtHelper),
+}
+
+impl Add {
+    #[inline]
+    pub fn table(table: impl Into<AddTable>) -> Command {
+        Command::Add(Add::Table(table.into()))
+    }
+
+    #[inline]
+    pub fn chain(chain: impl Into<AddChain>) -> Command {
+        Command::Add(Add::Chain(chain.into()))
+    }
+
+    #[inline]
+    pub fn rule(rule: impl Into<AddRule>) -> Command {
+        Command::Add(Add::Rule(rule.into()))
+    }
+
+    #[inline]
+    pub fn set(set: impl Into<AddSet>) -> Command {
+        Command::Add(Add::Set(set.into()))
+    }
+
+    #[inline]
+    pub fn map(map: impl Into<AddMap>) -> Command {
+        Command::Add(Add::Map(map.into()))
+    }
+
+    #[inline]
+    pub fn limit(limit: impl Into<AddLimit>) -> Command {
+        Command::Add(Add::Limit(limit.into()))
+    }
+
+    #[inline]
+    pub fn element(element: impl Into<AddElement>) -> Command {
+        Command::Add(Add::Element(element.into()))
+    }
+
+    #[inline]
+    pub fn ct_helper(ct_helper: impl Into<AddCtHelper>) -> Command {
+        Command::Add(Add::CtHelper(ct_helper.into()))
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum Flush {
+    Table(TableName),
+    Chain(ChainName),
+    Set(SetName),
+    Map(SetName),
+    Ruleset(Null),
+}
+
+impl Flush {
+    #[inline]
+    pub fn table(table: impl Into<TableName>) -> Command {
+        Command::Flush(Flush::Table(table.into()))
+    }
+
+    #[inline]
+    pub fn chain(chain: impl Into<ChainName>) -> Command {
+        Command::Flush(Flush::Chain(chain.into()))
+    }
+
+    #[inline]
+    pub fn set(set: impl Into<SetName>) -> Command {
+        Command::Flush(Flush::Set(set.into()))
+    }
+
+    #[inline]
+    pub fn map(map: impl Into<SetName>) -> Command {
+        Command::Flush(Flush::Map(map.into()))
+    }
+
+    #[inline]
+    pub fn ruleset() -> Command {
+        Command::Flush(Flush::Ruleset(Null))
+    }
+}
+
+impl From<TableName> for Flush {
+    #[inline]
+    fn from(value: TableName) -> Self {
+        Flush::Table(value)
+    }
+}
+
+impl From<ChainName> for Flush {
+    #[inline]
+    fn from(value: ChainName) -> Self {
+        Flush::Chain(value)
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum Delete {
+    Table(TableName),
+    Chain(ChainName),
+    Set(SetName),
+}
+
+impl Delete {
+    #[inline]
+    pub fn table(table: impl Into<TableName>) -> Command {
+        Command::Delete(Delete::Table(table.into()))
+    }
+
+    #[inline]
+    pub fn chain(chain: impl Into<ChainName>) -> Command {
+        Command::Delete(Delete::Chain(chain.into()))
+    }
+
+    #[inline]
+    pub fn set(set: impl Into<SetName>) -> Command {
+        Command::Delete(Delete::Set(set.into()))
+    }
+}
+
+impl From<TableName> for Delete {
+    #[inline]
+    fn from(value: TableName) -> Self {
+        Delete::Table(value)
+    }
+}
+
+impl From<ChainName> for Delete {
+    #[inline]
+    fn from(value: ChainName) -> Self {
+        Delete::Chain(value)
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum ListOutput {
+    Metainfo(serde_json::Value),
+    // Table(super::AddTable),
+    Chain(ListChain),
+    // Rule(super::Rule),
+    Set(ListSet),
+    // Map(super::Map),
+    // Element(super::SetElement),
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct CommandOutput {
+    pub nftables: Vec<ListOutput>,
+}
+
+impl Deref for CommandOutput {
+    type Target = Vec<ListOutput>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.nftables
+    }
+}
diff --git a/proxmox-nftables/src/lib.rs b/proxmox-nftables/src/lib.rs
index 40f6bab..60ddb3f 100644
--- a/proxmox-nftables/src/lib.rs
+++ b/proxmox-nftables/src/lib.rs
@@ -1,7 +1,9 @@
+pub mod command;
 pub mod expression;
 pub mod helper;
 pub mod statement;
 pub mod types;
 
+pub use command::Command;
 pub use expression::Expression;
 pub use statement::Statement;
diff --git a/proxmox-nftables/src/types.rs b/proxmox-nftables/src/types.rs
index a8ec599..90d3466 100644
--- a/proxmox-nftables/src/types.rs
+++ b/proxmox-nftables/src/types.rs
@@ -1,8 +1,92 @@
 use std::fmt::Display;
+use std::ops::{Deref, DerefMut};
+
+use crate::expression::IpFamily;
+use crate::helper::{NfVec, Null};
+use crate::{Expression, Statement};
 
 use serde::{Deserialize, Serialize};
 
-use crate::helper::Null;
+#[cfg(feature = "config-ext")]
+use proxmox_ve_config::guest::types::Vmid;
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
+pub struct Handle(i32);
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum TableFamily {
+    Ip,
+    Ip6,
+    Inet,
+    Arp,
+    Bridge,
+    Netdev,
+}
+serde_plain::derive_display_from_serialize!(TableFamily);
+
+impl TableFamily {
+    pub fn ip_families(&self) -> Vec<IpFamily> {
+        match self {
+            TableFamily::Ip => vec![IpFamily::Ip],
+            TableFamily::Ip6 => vec![IpFamily::Ip6],
+            _ => vec![IpFamily::Ip, IpFamily::Ip6],
+        }
+    }
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
+#[serde(rename_all = "snake_case")]
+pub enum ElementType {
+    Ifname,
+    Ipv4Addr,
+    Ipv6Addr,
+}
+serde_plain::derive_display_from_serialize!(ElementType);
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum ChainType {
+    Filter,
+    Nat,
+    Route,
+}
+serde_plain::derive_display_from_serialize!(ChainType);
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum SetPolicy {
+    Performance,
+    Memory,
+}
+serde_plain::derive_display_from_serialize!(SetPolicy);
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum SetFlag {
+    Constant,
+    Interval,
+    Timeout,
+}
+serde_plain::derive_display_from_serialize!(SetFlag);
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
+#[serde(rename_all = "snake_case")]
+pub enum OutputType {
+    Verdict,
+    Type(ElementType),
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum Hook {
+    Prerouting,
+    Input,
+    Forward,
+    Output,
+    Postrouting,
+}
+serde_plain::derive_display_from_serialize!(Hook);
 
 #[derive(Clone, Debug, Deserialize, Serialize)]
 #[serde(rename_all = "snake_case")]
@@ -30,6 +114,32 @@ impl Display for Verdict {
     }
 }
 
+#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "snake_case")]
+pub enum ChainPolicy {
+    Accept,
+    Drop,
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum PriorityKeyword {
+    Raw,
+    Mangle,
+    DstNat,
+    Filter,
+    Security,
+    SrcNat,
+    Out,
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
+#[serde(untagged)]
+pub enum Priority {
+    Keyword(PriorityKeyword),
+    Number(i64),
+}
+
 #[derive(Clone, Copy, Debug, Deserialize, Serialize)]
 pub enum RateUnit {
     Packets,
@@ -47,6 +157,529 @@ pub enum RateTimescale {
     Day,
 }
 
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct TableName {
+    family: TableFamily,
+    name: String,
+}
+
+impl TableName {
+    pub fn new(family: TableFamily, name: impl Into<String>) -> Self {
+        Self {
+            family,
+            name: name.into(),
+        }
+    }
+
+    pub fn family(&self) -> &TableFamily {
+        &self.family
+    }
+
+    pub fn name(&self) -> &str {
+        &self.name
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct TablePart {
+    family: TableFamily,
+    table: String,
+}
+
+impl TablePart {
+    pub fn new(family: TableFamily, name: impl Into<String>) -> Self {
+        Self {
+            family,
+            table: name.into(),
+        }
+    }
+
+    pub fn family(&self) -> &TableFamily {
+        &self.family
+    }
+
+    pub fn table(&self) -> &str {
+        &self.table
+    }
+}
+
+impl From<TablePart> for TableName {
+    fn from(t: TablePart) -> Self {
+        Self {
+            family: t.family,
+            name: t.table,
+        }
+    }
+}
+
+impl From<TableName> for TablePart {
+    fn from(t: TableName) -> Self {
+        Self {
+            family: t.family,
+            table: t.name,
+        }
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct ChainName {
+    #[serde(flatten)]
+    table: TablePart,
+    name: String,
+}
+
+impl From<AddChain> for ChainName {
+    fn from(value: AddChain) -> Self {
+        Self {
+            table: value.table,
+            name: value.name,
+        }
+    }
+}
+
+impl From<ListChain> for ChainName {
+    fn from(value: ListChain) -> Self {
+        Self {
+            table: value.table,
+            name: value.name,
+        }
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct ChainPart {
+    #[serde(flatten)]
+    table: TablePart,
+    chain: String,
+}
+
+impl ChainPart {
+    pub fn new(table: TablePart, chain: impl Into<String>) -> Self {
+        Self {
+            table,
+            chain: chain.into(),
+        }
+    }
+
+    pub fn table(&self) -> &TablePart {
+        &self.table
+    }
+
+    pub fn name(&self) -> &str {
+        &self.chain
+    }
+}
+
+impl From<ChainName> for ChainPart {
+    fn from(c: ChainName) -> Self {
+        Self {
+            table: c.table,
+            chain: c.name,
+        }
+    }
+}
+
+impl From<ChainPart> for ChainName {
+    fn from(c: ChainPart) -> Self {
+        Self {
+            table: c.table,
+            name: c.chain,
+        }
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct AddTable {
+    family: TableFamily,
+    name: String,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    handle: Option<Handle>,
+}
+
+impl AddTable {
+    pub fn new(family: TableFamily, name: impl Into<String>) -> Self {
+        Self {
+            family,
+            name: name.into(),
+            handle: None,
+        }
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct BaseChainConfig {
+    #[serde(rename = "type")]
+    ty: ChainType,
+    hook: Hook,
+    prio: Expression,
+    policy: ChainPolicy,
+
+    /// netdev family only
+    #[serde(skip_serializing_if = "Option::is_none")]
+    dev: Option<String>,
+}
+
+impl BaseChainConfig {
+    pub fn new(
+        ty: ChainType,
+        hook: Hook,
+        prio: impl Into<Expression>,
+        policy: ChainPolicy,
+    ) -> Self {
+        Self {
+            ty,
+            hook,
+            prio: prio.into(),
+            policy,
+            dev: None,
+        }
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct AddChain {
+    #[serde(flatten)]
+    table: TablePart,
+    name: String,
+
+    #[serde(flatten, skip_serializing_if = "Option::is_none")]
+    config: Option<BaseChainConfig>,
+}
+
+impl AddChain {
+    pub fn new(table: TablePart, name: impl Into<String>) -> Self {
+        Self {
+            table,
+            name: name.into(),
+            config: None,
+        }
+    }
+
+    pub fn new_base_chain(
+        table: TablePart,
+        name: impl Into<String>,
+        config: BaseChainConfig,
+    ) -> Self {
+        Self {
+            table,
+            name: name.into(),
+            config: Some(config),
+        }
+    }
+}
+
+impl From<ChainPart> for AddChain {
+    #[inline]
+    fn from(part: ChainPart) -> Self {
+        Self::new(part.table, part.chain)
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct AddRule {
+    #[serde(flatten)]
+    chain: ChainPart,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    handle: Option<Handle>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    index: Option<u64>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    comment: Option<String>,
+
+    expr: Vec<Statement>,
+}
+
+impl Deref for AddRule {
+    type Target = Vec<Statement>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.expr
+    }
+}
+
+impl DerefMut for AddRule {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.expr
+    }
+}
+
+impl AddRule {
+    pub fn from_statement(chain: ChainPart, expression: impl Into<Statement>) -> Self {
+        Self {
+            chain,
+            expr: vec![expression.into()],
+            handle: None,
+            index: None,
+            comment: None,
+        }
+    }
+
+    pub fn from_statements<I: IntoIterator<Item = Statement>>(
+        chain: ChainPart,
+        expression: I,
+    ) -> Self {
+        Self {
+            chain,
+            expr: expression.into_iter().collect(),
+            handle: None,
+            index: None,
+            comment: None,
+        }
+    }
+
+    pub fn new(chain: ChainPart) -> Self {
+        Self {
+            chain,
+            expr: Vec::new(),
+            handle: None,
+            index: None,
+            comment: None,
+        }
+    }
+
+    pub fn with_comment(mut self, comment: impl Into<String>) -> Self {
+        self.comment = Some(comment.into());
+        self
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "kebab-case")]
+pub struct SetConfig {
+    #[serde(flatten)]
+    name: SetName,
+
+    #[serde(rename = "type", default, skip_serializing_if = "Vec::is_empty")]
+    ty: NfVec<ElementType>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    policy: Option<SetPolicy>,
+
+    #[serde(skip_serializing_if = "Vec::is_empty", default)]
+    flags: Vec<SetFlag>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    timeout: Option<i64>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    gc_interval: Option<i64>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    size: Option<i64>,
+}
+
+impl SetConfig {
+    pub fn new(name: impl Into<SetName>, ty: impl IntoIterator<Item = ElementType>) -> Self {
+        Self {
+            name: name.into(),
+            ty: NfVec::from_iter(ty),
+            flags: Vec::new(),
+            policy: None,
+            timeout: None,
+            gc_interval: None,
+            size: None,
+        }
+    }
+
+    pub fn name(&self) -> &SetName {
+        &self.name
+    }
+
+    pub fn with_flag(mut self, flag: SetFlag) -> Self {
+        self.flags.push(flag);
+        self
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "kebab-case")]
+pub struct AddMap {
+    #[serde(flatten)]
+    config: SetConfig,
+
+    map: OutputType,
+
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    elem: NfVec<MapElem>,
+}
+
+impl AddMap {
+    pub fn new(config: SetConfig, output_type: OutputType) -> Self {
+        Self {
+            config,
+            map: output_type,
+            elem: NfVec::new(),
+        }
+    }
+}
+
+impl Deref for AddMap {
+    type Target = Vec<MapElem>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.elem
+    }
+}
+
+impl DerefMut for AddMap {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.elem
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct AddSet {
+    #[serde(flatten)]
+    config: SetConfig,
+
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    elem: NfVec<SetElem>,
+}
+
+impl From<SetConfig> for AddSet {
+    fn from(value: SetConfig) -> Self {
+        Self {
+            config: value,
+            elem: NfVec::new(),
+        }
+    }
+}
+
+impl Deref for AddSet {
+    type Target = Vec<SetElem>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.elem
+    }
+}
+
+impl DerefMut for AddSet {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.elem
+    }
+}
+
+impl AddSet {
+    pub fn new(config: impl Into<SetConfig>, elements: impl IntoIterator<Item = SetElem>) -> Self {
+        Self {
+            config: config.into(),
+            elem: NfVec::from_iter(elements),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct SetName {
+    #[serde(flatten)]
+    table: TablePart,
+    name: String,
+}
+
+impl SetName {
+    pub fn new(table: TablePart, name: impl Into<String>) -> Self {
+        Self {
+            table,
+            name: name.into(),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct SetElem(Expression);
+
+impl From<Expression> for SetElem {
+    #[inline]
+    fn from(value: Expression) -> Self {
+        Self(value)
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(untagged)]
+pub enum MapValue {
+    Expression(Expression),
+    Verdict(Verdict),
+    // Concat
+}
+
+impl From<Verdict> for MapValue {
+    #[inline]
+    fn from(value: Verdict) -> Self {
+        Self::Verdict(value)
+    }
+}
+
+impl From<Expression> for MapValue {
+    #[inline]
+    fn from(value: Expression) -> Self {
+        Self::Expression(value)
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct MapElem((Expression, MapValue));
+
+impl MapElem {
+    pub fn new(key: Expression, value: impl Into<MapValue>) -> Self {
+        Self((key, value.into()))
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct AddSetElement {
+    #[serde(flatten)]
+    set: SetName,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    elem: Vec<SetElement>,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct AddMapElement {
+    #[serde(flatten)]
+    map: SetName,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    elem: Vec<MapElement>,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(untagged)]
+pub enum AddElement {
+    Set(AddSetElement),
+    Map(AddMapElement),
+}
+
+impl AddElement {
+    pub fn map_from_expressions(
+        map: SetName,
+        elem: impl IntoIterator<Item = (Expression, MapValue)>,
+    ) -> Self {
+        Self::Map(AddMapElement {
+            map,
+            elem: Vec::from_iter(
+                elem.into_iter()
+                    .map(|(key, value)| MapElem::new(key, value).into()),
+            ),
+        })
+    }
+
+    pub fn set_from_expressions(set: SetName, elem: impl IntoIterator<Item = Expression>) -> Self {
+        Self::Set(AddSetElement {
+            set,
+            elem: Vec::from_iter(elem.into_iter().map(SetElement::from)),
+        })
+    }
+}
+
+impl From<AddSetElement> for AddElement {
+    fn from(value: AddSetElement) -> Self {
+        AddElement::Set(value)
+    }
+}
+
 #[derive(Clone, Debug, Deserialize, Serialize)]
 pub struct ElemConfig {
     timeout: Option<i64>,
@@ -67,3 +700,138 @@ impl ElemConfig {
         }
     }
 }
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct SetElemObject {
+    #[serde(flatten)]
+    config: ElemConfig,
+    elem: SetElem,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct MapElemObject {
+    #[serde(flatten)]
+    config: ElemConfig,
+    elem: MapElem,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub enum MapElement {
+    #[serde(rename = "elem")]
+    Object(MapElemObject),
+    #[serde(untagged)]
+    Value(MapElem),
+}
+
+impl From<MapElem> for MapElement {
+    fn from(value: MapElem) -> Self {
+        Self::Value(value)
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub enum SetElement {
+    #[serde(rename = "elem")]
+    Object(SetElemObject),
+    #[serde(untagged)]
+    Value(SetElem),
+}
+
+impl From<Expression> for SetElement {
+    #[inline]
+    fn from(value: Expression) -> Self {
+        Self::Value(SetElem::from(value))
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub struct AddLimit {
+    #[serde(flatten)]
+    table: TablePart,
+
+    name: String,
+
+    rate: i64,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    unit: Option<RateUnit>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    per: Option<RateTimescale>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    burst: Option<i64>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    inv: Option<bool>,
+}
+
+impl AddLimit {
+    pub fn new(table: TablePart, name: String, rate: i64) -> Self {
+        Self {
+            table,
+            name,
+            rate,
+            unit: None,
+            per: None,
+            burst: None,
+            inv: None,
+        }
+    }
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum L3Protocol {
+    Ip,
+    Ip6,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum CtHelperProtocol {
+    TCP,
+    UDP,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename = "ct helper")]
+pub struct AddCtHelper {
+    #[serde(flatten)]
+    pub table: TablePart,
+    pub name: String,
+    #[serde(rename = "type")]
+    pub ty: String,
+    pub protocol: CtHelperProtocol,
+    pub l3proto: Option<L3Protocol>,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct ListChain {
+    #[serde(flatten)]
+    table: TablePart,
+    name: String,
+    handle: i64,
+
+    #[serde(flatten)]
+    config: Option<BaseChainConfig>,
+}
+
+impl ListChain {
+    pub fn name(&self) -> &str {
+        &self.name
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct ListSet {
+    #[serde(flatten)]
+    name: SetName,
+}
+
+impl ListSet {
+    pub fn name(&self) -> &SetName {
+        &self.name
+    }
+}
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 24/39] nftables: types: add conversion traits
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (22 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 23/39] nftables: commands: add types Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 25/39] nftables: add nft client Stefan Hanreich
                   ` (15 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

Some parts of the firewall config map directly to nftables objects, so
we introduce conversion traits for convenient conversion into the
respective nftables objects / types.

They are guarded behind a feature, so the nftables crate can be used
standalone without depending on the proxmox-ve-config crate.

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-nftables/src/types.rs | 80 ++++++++++++++++++++++++++++++++++-
 1 file changed, 79 insertions(+), 1 deletion(-)

diff --git a/proxmox-nftables/src/types.rs b/proxmox-nftables/src/types.rs
index 90d3466..a83e958 100644
--- a/proxmox-nftables/src/types.rs
+++ b/proxmox-nftables/src/types.rs
@@ -7,6 +7,12 @@ use crate::{Expression, Statement};
 
 use serde::{Deserialize, Serialize};
 
+#[cfg(feature = "config-ext")]
+use proxmox_ve_config::firewall::types::address::Family;
+
+#[cfg(feature = "config-ext")]
+use proxmox_ve_config::firewall::types::ipset::IpsetName;
+
 #[cfg(feature = "config-ext")]
 use proxmox_ve_config::guest::types::Vmid;
 
@@ -33,6 +39,15 @@ impl TableFamily {
             _ => vec![IpFamily::Ip, IpFamily::Ip6],
         }
     }
+
+    #[cfg(feature = "config-ext")]
+    pub fn families(&self) -> Vec<Family> {
+        match self {
+            TableFamily::Ip => vec![Family::V4],
+            TableFamily::Ip6 => vec![Family::V6],
+            _ => vec![Family::V4, Family::V6],
+        }
+    }
 }
 
 #[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
@@ -157,6 +172,21 @@ pub enum RateTimescale {
     Day,
 }
 
+#[cfg(feature = "config-ext")]
+use proxmox_ve_config::firewall::types::log::LogRateLimitTimescale;
+
+#[cfg(feature = "config-ext")]
+impl From<LogRateLimitTimescale> for RateTimescale {
+    fn from(value: LogRateLimitTimescale) -> Self {
+        match value {
+            LogRateLimitTimescale::Second => RateTimescale::Second,
+            LogRateLimitTimescale::Minute => RateTimescale::Minute,
+            LogRateLimitTimescale::Hour => RateTimescale::Hour,
+            LogRateLimitTimescale::Day => RateTimescale::Day,
+        }
+    }
+}
+
 #[derive(Clone, Debug, Deserialize, Serialize)]
 pub struct TableName {
     family: TableFamily,
@@ -586,6 +616,44 @@ impl SetName {
             name: name.into(),
         }
     }
+
+    pub fn name(&self) -> &str {
+        self.name.as_ref()
+    }
+
+    #[cfg(feature = "config-ext")]
+    pub fn ipset_name(
+        family: Family,
+        name: &IpsetName,
+        vmid: Option<Vmid>,
+        nomatch: bool,
+    ) -> String {
+        use proxmox_ve_config::firewall::types::ipset::IpsetScope;
+
+        let prefix = match family {
+            Family::V4 => "v4",
+            Family::V6 => "v6",
+        };
+
+        let name = match name.scope() {
+            IpsetScope::Datacenter => name.to_string(),
+            IpsetScope::Guest => {
+                if let Some(vmid) = vmid {
+                    format!("guest-{vmid}/{}", name.name())
+                } else {
+                    log::warn!("Creating IPSet for guest without vmid parameter!");
+                    name.to_string()
+                }
+            }
+        };
+
+        let suffix = match nomatch {
+            true => "-nomatch",
+            false => "",
+        };
+
+        format!("{prefix}-{name}{suffix}")
+    }
 }
 
 #[derive(Clone, Debug, Deserialize, Serialize)]
@@ -788,7 +856,17 @@ pub enum L3Protocol {
     Ip6,
 }
 
-#[derive(Clone, Debug, Deserialize, Serialize)]
+#[cfg(feature = "config-ext")]
+impl From<Family> for L3Protocol {
+    fn from(value: Family) -> Self {
+        match value {
+            Family::V4 => L3Protocol::Ip,
+            Family::V6 => L3Protocol::Ip6,
+        }
+    }
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
 #[serde(rename_all = "lowercase")]
 pub enum CtHelperProtocol {
     TCP,
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 25/39] nftables: add nft client
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (23 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 24/39] nftables: types: add conversion traits Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 26/39] firewall: add firewall crate Stefan Hanreich
                   ` (14 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

Add a thin wrapper around nft, which can be used to run commands
defined by the rust types.

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-nftables/src/client.rs | 85 ++++++++++++++++++++++++++++++++++
 proxmox-nftables/src/lib.rs    |  2 +
 2 files changed, 87 insertions(+)
 create mode 100644 proxmox-nftables/src/client.rs

diff --git a/proxmox-nftables/src/client.rs b/proxmox-nftables/src/client.rs
new file mode 100644
index 0000000..69e464b
--- /dev/null
+++ b/proxmox-nftables/src/client.rs
@@ -0,0 +1,85 @@
+use std::io::prelude::*;
+use std::process::{Command, Stdio};
+
+use thiserror::Error;
+
+use crate::command::{CommandOutput, Commands};
+
+#[derive(Error, Debug)]
+pub enum NftError {
+    #[error("cannot communicate with child process")]
+    Io(#[from] std::io::Error),
+    #[error("cannot execute nftables commands")]
+    Command(String),
+}
+
+pub struct NftClient;
+
+impl NftClient {
+    fn execute_nft_commands(json: bool, input: &[u8]) -> Result<String, NftError> {
+        let mut command = Command::new("nft");
+
+        if json {
+            command.arg("-j");
+        }
+
+        let mut child = command
+            .arg("-f")
+            .arg("-")
+            .stdin(Stdio::piped())
+            .stdout(Stdio::piped())
+            .stderr(Stdio::piped())
+            .spawn()
+            .map_err(NftError::from)?;
+
+        if let Err(error) = child.stdin.take().expect("can get stdin").write_all(input) {
+            return Err(NftError::from(error));
+        };
+
+        let mut error_output = String::new();
+
+        match child
+            .stderr
+            .take()
+            .expect("can get stderr")
+            .read_to_string(&mut error_output)
+        {
+            Ok(_) if !error_output.is_empty() => {
+                return Err(NftError::Command(error_output));
+            }
+            Err(error) => {
+                return Err(NftError::from(error));
+            }
+            _ => (),
+        };
+
+        let mut output = String::new();
+
+        if let Err(error) = child
+            .stdout
+            .take()
+            .expect("can get stdout")
+            .read_to_string(&mut output)
+        {
+            return Err(NftError::from(error));
+        };
+
+        Ok(output)
+    }
+
+    pub fn run_json_commands(commands: &Commands) -> Result<Option<CommandOutput>, NftError> {
+        let json = serde_json::to_vec(commands).expect("can serialize commands struct");
+        let output = Self::execute_nft_commands(true, &json)?;
+
+        if !output.is_empty() {
+            let parsed_output: Option<CommandOutput> = serde_json::from_str(&output).ok();
+            return Ok(parsed_output);
+        }
+
+        Ok(None)
+    }
+
+    pub fn run_commands(commands: &str) -> Result<String, NftError> {
+        Self::execute_nft_commands(false, commands.as_bytes())
+    }
+}
diff --git a/proxmox-nftables/src/lib.rs b/proxmox-nftables/src/lib.rs
index 60ddb3f..2003e1b 100644
--- a/proxmox-nftables/src/lib.rs
+++ b/proxmox-nftables/src/lib.rs
@@ -1,9 +1,11 @@
+pub mod client;
 pub mod command;
 pub mod expression;
 pub mod helper;
 pub mod statement;
 pub mod types;
 
+pub use client::NftClient;
 pub use command::Command;
 pub use expression::Expression;
 pub use statement::Statement;
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 26/39] firewall: add firewall crate
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (24 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 25/39] nftables: add nft client Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 27/39] firewall: add base ruleset Stefan Hanreich
                   ` (13 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 Cargo.toml                   |  1 +
 proxmox-firewall/Cargo.toml  | 17 +++++++++++++++++
 proxmox-firewall/src/main.rs |  5 +++++
 3 files changed, 23 insertions(+)
 create mode 100644 proxmox-firewall/Cargo.toml
 create mode 100644 proxmox-firewall/src/main.rs

diff --git a/Cargo.toml b/Cargo.toml
index 877f103..f353fbf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,4 +2,5 @@
 members = [
     "proxmox-ve-config",
     "proxmox-nftables",
+    "proxmox-firewall",
 ]
diff --git a/proxmox-firewall/Cargo.toml b/proxmox-firewall/Cargo.toml
new file mode 100644
index 0000000..b59d973
--- /dev/null
+++ b/proxmox-firewall/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "proxmox-firewall"
+version = "0.1.0"
+edition = "2021"
+authors = [
+    "Wolfgang Bumiller <w.bumiller@proxmox.com>",
+    "Stefan Hanreich <s.hanreich@proxmox.com>",
+    "Proxmox Support Team <support@proxmox.com>",
+]
+description = "Proxmox VE nftables firewall implementation"
+license = "AGPL-3"
+
+[dependencies]
+anyhow = "1"
+
+proxmox-nftables = { path = "../proxmox-nftables", features = ["config-ext"] }
+proxmox-ve-config = { path = "../proxmox-ve-config" }
diff --git a/proxmox-firewall/src/main.rs b/proxmox-firewall/src/main.rs
new file mode 100644
index 0000000..248ac39
--- /dev/null
+++ b/proxmox-firewall/src/main.rs
@@ -0,0 +1,5 @@
+use anyhow::Error;
+
+fn main() -> Result<(), Error> {
+    Ok(())
+}
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 27/39] firewall: add base ruleset
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (25 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 26/39] firewall: add firewall crate Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 28/39] firewall: add config loader Stefan Hanreich
                   ` (12 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

This is the skeleton for the firewall that contains all the base
chains required for the firewall.

The file applies atomically, which means that it flushes all objects
and recreates them - except for the cluster/host/guest chain. This
means that it can be run at any point in time, since it only updates
the chains that are not managed by the firewall itself.

This also means that when we change the rules in the chains (e.g.
during an update) we can always just re-run the nft-file and the
firewall should use the new chains while still retaining the
configuration generated by the firewall daemon.

This also means that when re-creating the firewall rules, the
cluster/host/guest chains need to be flushed manually before creating
new rules.

Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 .../resources/proxmox-firewall.nft            | 305 ++++++++++++++++++
 1 file changed, 305 insertions(+)
 create mode 100644 proxmox-firewall/resources/proxmox-firewall.nft

diff --git a/proxmox-firewall/resources/proxmox-firewall.nft b/proxmox-firewall/resources/proxmox-firewall.nft
new file mode 100644
index 0000000..67dd8c8
--- /dev/null
+++ b/proxmox-firewall/resources/proxmox-firewall.nft
@@ -0,0 +1,305 @@
+#!/usr/sbin/nft -f
+
+define ipv6_mask = ffff:ffff:ffff:ffff::
+
+add table inet proxmox-firewall
+add table bridge proxmox-firewall-guests
+
+add chain inet proxmox-firewall do-reject
+add chain inet proxmox-firewall accept-management
+add chain inet proxmox-firewall block-synflood
+add chain inet proxmox-firewall log-drop-invalid-tcp
+add chain inet proxmox-firewall block-invalid-tcp
+add chain inet proxmox-firewall allow-ndp-in
+add chain inet proxmox-firewall block-ndp-in
+add chain inet proxmox-firewall allow-ndp-out
+add chain inet proxmox-firewall block-ndp-out
+add chain inet proxmox-firewall block-conntrack-invalid
+add chain inet proxmox-firewall block-smurfs
+add chain inet proxmox-firewall log-drop-smurfs
+add chain inet proxmox-firewall default-in
+add chain inet proxmox-firewall default-out
+add chain inet proxmox-firewall input {type filter hook input priority filter; policy drop;}
+add chain inet proxmox-firewall output {type filter hook output priority filter; policy accept;}
+
+add chain bridge proxmox-firewall-guests allow-dhcp-in
+add chain bridge proxmox-firewall-guests allow-dhcp-out
+add chain bridge proxmox-firewall-guests block-dhcp-in
+add chain bridge proxmox-firewall-guests block-dhcp-out
+add chain bridge proxmox-firewall-guests allow-ndp-in
+add chain bridge proxmox-firewall-guests block-ndp-in
+add chain bridge proxmox-firewall-guests allow-ndp-out
+add chain bridge proxmox-firewall-guests block-ndp-out
+add chain bridge proxmox-firewall-guests allow-ra-out
+add chain bridge proxmox-firewall-guests block-ra-out
+add chain bridge proxmox-firewall-guests after-vm-in
+add chain bridge proxmox-firewall-guests do-reject
+add chain bridge proxmox-firewall-guests vm-out {type filter hook prerouting priority 0; policy accept;}
+add chain bridge proxmox-firewall-guests vm-in {type filter hook postrouting priority 0; policy accept;}
+
+flush chain inet proxmox-firewall do-reject
+flush chain inet proxmox-firewall accept-management
+flush chain inet proxmox-firewall block-synflood
+flush chain inet proxmox-firewall log-drop-invalid-tcp
+flush chain inet proxmox-firewall block-invalid-tcp
+flush chain inet proxmox-firewall allow-ndp-in
+flush chain inet proxmox-firewall block-ndp-in
+flush chain inet proxmox-firewall allow-ndp-out
+flush chain inet proxmox-firewall block-ndp-out
+flush chain inet proxmox-firewall block-conntrack-invalid
+flush chain inet proxmox-firewall block-smurfs
+flush chain inet proxmox-firewall log-drop-smurfs
+flush chain inet proxmox-firewall default-in
+flush chain inet proxmox-firewall default-out
+flush chain inet proxmox-firewall input
+flush chain inet proxmox-firewall output
+
+flush chain bridge proxmox-firewall-guests allow-dhcp-in
+flush chain bridge proxmox-firewall-guests allow-dhcp-out
+flush chain bridge proxmox-firewall-guests block-dhcp-in
+flush chain bridge proxmox-firewall-guests block-dhcp-out
+flush chain bridge proxmox-firewall-guests allow-ndp-in
+flush chain bridge proxmox-firewall-guests block-ndp-in
+flush chain bridge proxmox-firewall-guests allow-ndp-out
+flush chain bridge proxmox-firewall-guests block-ndp-out
+flush chain bridge proxmox-firewall-guests allow-ra-out
+flush chain bridge proxmox-firewall-guests block-ra-out
+flush chain bridge proxmox-firewall-guests after-vm-in
+flush chain bridge proxmox-firewall-guests do-reject
+flush chain bridge proxmox-firewall-guests vm-out
+flush chain bridge proxmox-firewall-guests vm-in
+
+table inet proxmox-firewall {
+    chain do-reject {
+        meta pkttype broadcast drop
+        ip saddr 224.0.0.0/4 drop
+
+        meta l4proto tcp reject with tcp reset
+        meta l4proto icmp reject with icmp type port-unreachable
+        reject with icmp type host-prohibited
+    }
+
+    set v4-dc/management {
+        type ipv4_addr; flags interval; auto-merge
+    }
+
+    set v4-dc/management-nomatch {
+        type ipv4_addr; flags interval; auto-merge
+    }
+
+    set v6-dc/management {
+        type ipv6_addr; flags interval; auto-merge
+    }
+
+    set v6-dc/management-nomatch {
+        type ipv6_addr; flags interval; auto-merge
+    }
+
+    chain accept-management {
+        ip saddr @v4-dc/management ip saddr != @v4-dc/management-nomatch accept
+        ip6 saddr @v6-dc/management ip6 saddr != @v6-dc/management-nomatch accept
+    }
+
+    set v4-synflood-limit {
+        type ipv4_addr
+        timeout 60s
+        flags dynamic
+    }
+
+    set v6-synflood-limit {
+        type ipv6_addr
+        timeout 60s
+        flags dynamic
+    }
+
+    chain ratelimit-synflood {
+
+    }
+
+    # todo: move to prerouting
+    chain block-synflood {
+        tcp flags & (fin|syn|rst|ack) != syn return
+        jump ratelimit-synflood
+        drop
+    }
+
+    chain log-invalid-tcp {}
+
+    chain log-drop-invalid-tcp {
+        # looks weird but that way we can just flush the other chain
+        # when regenerating from the config
+        jump log-invalid-tcp
+        drop
+    }
+
+    chain block-invalid-tcp {
+        tcp flags & (fin|syn|rst|psh|ack|urg) == fin|psh|urg goto log-drop-invalid-tcp
+        tcp flags & (fin|syn|rst|psh|ack|urg) == 0x0 goto log-drop-invalid-tcp
+        tcp flags & (syn|rst) == syn|rst goto log-drop-invalid-tcp
+        tcp flags & (fin|syn) == fin|syn goto log-drop-invalid-tcp
+        tcp sport 0 tcp flags & (fin|syn|rst|ack) == syn goto log-drop-invalid-tcp
+    }
+
+    chain allow-ndp-in {
+        icmpv6 type { nd-router-solicit, nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert, nd-redirect } accept
+    }
+
+    chain block-ndp-in {
+        icmpv6 type { nd-router-solicit, nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert, nd-redirect } drop
+    }
+
+    chain allow-ndp-out {
+        icmpv6 type { nd-router-solicit, nd-neighbor-solicit, nd-neighbor-advert } accept
+    }
+
+    chain block-ndp-out {
+        icmpv6 type { nd-router-solicit, nd-neighbor-solicit, nd-neighbor-advert } drop
+    }
+
+    chain block-conntrack-invalid {
+        ct state invalid drop
+    }
+
+    chain block-smurfs {
+        ip saddr 0.0.0.0/32 return
+        meta pkttype broadcast goto log-drop-smurfs
+        ip saddr 224.0.0.0/4 goto log-drop-smurfs
+    }
+
+    chain log-smurfs {}
+
+    chain log-drop-smurfs {
+        # looks weird but that way we can just flush the other chain
+        # when regenerating from the config
+        jump log-smurfs
+        drop
+    }
+
+    chain default-in {
+        iifname "lo" accept
+
+        ct state related,established accept
+
+        meta l4proto igmp accept
+
+        tcp dport { 8006, 5900-5999, 3128, 22 } jump accept-management
+        udp dport 5405-5412 accept
+
+        meta l4proto icmp icmp type { destination-unreachable, time-exceeded } accept
+
+        # Drop Microsoft SMB noise
+        udp dport { 135, 137-139, 445 } goto do-reject
+        udp sport 137 udp dport 1024-65535 goto do-reject
+        tcp dport { 135, 139, 445 } goto do-reject
+        udp dport 1900 drop
+
+        # Drop new/NotSyn traffic so that it doesn't get logged
+        # tcp flags & (fin | syn | rst | ack) == syn drop
+
+        # Drop DNS replies
+        udp sport 53 drop
+    }
+
+    chain default-out {
+        oifname "lo" accept
+
+        ct state vmap { invalid : drop, established : accept, related : accept }
+    }
+
+    chain option-in {}
+    chain option-out {}
+
+    chain input {
+        type filter hook input priority filter; policy accept;
+        jump default-in
+        jump ct-in
+        jump option-in
+        jump host-in
+        jump cluster-in
+    }
+
+    chain output {
+        type filter hook output priority filter; policy accept;
+        jump default-out
+        jump option-out
+        jump host-out
+        jump cluster-out
+    }
+
+    chain cluster-in {}
+    chain cluster-out {}
+
+    chain host-in {}
+    chain host-out {}
+
+    chain ct-in {}
+}
+
+table bridge proxmox-firewall-guests {
+    map vm-map-in {
+        typeof oifname : verdict
+    }
+
+    map vm-map-out {
+        typeof iifname : verdict
+    }
+
+    chain allow-dhcp-in {
+        udp sport . udp dport { 67 . 68, 547 . 546 } accept
+    }
+
+    chain block-dhcp-in {
+        udp sport . udp dport { 67 . 68, 547 . 546 } drop
+    }
+
+    chain allow-dhcp-out {
+        udp sport . udp dport { 68 . 67, 546 . 547 } accept
+    }
+
+    chain block-dhcp-out {
+        udp sport . udp dport { 68 . 67, 546 . 547 } drop
+    }
+
+    chain allow-ndp-in {
+        icmpv6 type { nd-router-solicit, nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert, nd-redirect } accept
+    }
+
+    chain block-ndp-in {
+        icmpv6 type { nd-router-solicit, nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert, nd-redirect } drop
+    }
+
+    chain allow-ndp-out {
+        icmpv6 type { nd-router-solicit, nd-neighbor-solicit, nd-neighbor-advert } accept
+    }
+
+    chain block-ndp-out {
+        icmpv6 type { nd-router-solicit, nd-neighbor-solicit, nd-neighbor-advert } drop
+    }
+
+    chain allow-ra-out {
+        icmpv6 type { nd-router-advert, nd-redirect } accept
+    }
+
+    chain block-ra-out {
+        icmpv6 type { nd-router-advert, nd-redirect } drop
+    }
+
+    chain do-reject {
+        drop
+    }
+
+    chain after-vm-in {
+        ct state established,related accept
+        ether type != arp ct state invalid drop
+    }
+
+    chain vm-out {
+        type filter hook prerouting priority 0; policy accept;
+        iifname vmap @vm-map-out
+    }
+
+    chain vm-in {
+        type filter hook postrouting priority 0; policy accept;
+        oifname vmap @vm-map-in
+    }
+}
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 28/39] firewall: add config loader
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (26 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 27/39] firewall: add base ruleset Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 29/39] firewall: add rule generation logic Stefan Hanreich
                   ` (11 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

We load the firewall configuration from the default paths, as well as
only the guest configurations that are local to the node itself. In
the future we could change this to use pmxcfs directly instead.

We also load information from nftables directly about dynamically
created chains (mostly chains for the guest firewall).

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-firewall/Cargo.toml    |   2 +
 proxmox-firewall/src/config.rs | 283 +++++++++++++++++++++++++++++++++
 proxmox-firewall/src/main.rs   |   3 +
 3 files changed, 288 insertions(+)
 create mode 100644 proxmox-firewall/src/config.rs

diff --git a/proxmox-firewall/Cargo.toml b/proxmox-firewall/Cargo.toml
index b59d973..431e71a 100644
--- a/proxmox-firewall/Cargo.toml
+++ b/proxmox-firewall/Cargo.toml
@@ -11,6 +11,8 @@ description = "Proxmox VE nftables firewall implementation"
 license = "AGPL-3"
 
 [dependencies]
+log = "0.4"
+env_logger = "0.10"
 anyhow = "1"
 
 proxmox-nftables = { path = "../proxmox-nftables", features = ["config-ext"] }
diff --git a/proxmox-firewall/src/config.rs b/proxmox-firewall/src/config.rs
new file mode 100644
index 0000000..2cf3e39
--- /dev/null
+++ b/proxmox-firewall/src/config.rs
@@ -0,0 +1,283 @@
+use std::collections::BTreeMap;
+use std::default::Default;
+use std::fs::File;
+use std::io::{self, BufReader};
+use std::sync::OnceLock;
+
+use anyhow::Error;
+
+use proxmox_ve_config::firewall::cluster::Config as ClusterConfig;
+use proxmox_ve_config::firewall::guest::Config as GuestConfig;
+use proxmox_ve_config::firewall::host::Config as HostConfig;
+use proxmox_ve_config::firewall::types::alias::{Alias, AliasName, AliasScope};
+
+use proxmox_ve_config::guest::types::Vmid;
+use proxmox_ve_config::guest::{GuestEntry, GuestMap};
+
+use proxmox_nftables::command::{CommandOutput, Commands, List, ListOutput};
+use proxmox_nftables::types::ListChain;
+use proxmox_nftables::NftClient;
+
+pub trait FirewallConfigLoader {
+    fn cluster(&self) -> Option<Box<dyn io::BufRead>>;
+    fn host(&self) -> Option<Box<dyn io::BufRead>>;
+    fn guest_list(&self) -> GuestMap;
+    fn guest_config(&self, vmid: &Vmid, guest: &GuestEntry) -> Option<Box<dyn io::BufRead>>;
+    fn guest_firewall_config(&self, vmid: &Vmid) -> Option<Box<dyn io::BufRead>>;
+}
+
+#[derive(Default)]
+struct PveFirewallConfigLoader {}
+
+impl PveFirewallConfigLoader {
+    pub fn new() -> Self {
+        Default::default()
+    }
+}
+
+/// opens a configuration file
+///
+/// It returns a file handle to the file or [`None`] if it doesn't exist.
+fn open_config_file(path: &str) -> Result<Option<File>, Error> {
+    match File::open(path) {
+        Ok(data) => Ok(Some(data)),
+        Err(err) if err.kind() == io::ErrorKind::NotFound => {
+            log::info!("config file does not exist: {path}");
+            Ok(None)
+        }
+        Err(err) => {
+            let context = format!("unable to open configuration file at {path}");
+            Err(anyhow::Error::new(err).context(context))
+        }
+    }
+}
+
+const CLUSTER_CONFIG_PATH: &str = "/etc/pve/firewall/cluster.fw";
+const HOST_CONFIG_PATH: &str = "/etc/pve/local/host.fw";
+
+impl FirewallConfigLoader for PveFirewallConfigLoader {
+    fn cluster(&self) -> Option<Box<dyn io::BufRead>> {
+        log::info!("loading cluster config");
+
+        let fd =
+            open_config_file(CLUSTER_CONFIG_PATH).expect("able to read cluster firewall config");
+
+        if let Some(file) = fd {
+            let buf_reader = Box::new(BufReader::new(file)) as Box<dyn io::BufRead>;
+            return Some(buf_reader);
+        }
+
+        None
+    }
+
+    fn host(&self) -> Option<Box<dyn io::BufRead>> {
+        log::info!("loading host config");
+
+        let fd = open_config_file(HOST_CONFIG_PATH).expect("able to read host firewall config");
+
+        if let Some(file) = fd {
+            let buf_reader = Box::new(BufReader::new(file)) as Box<dyn io::BufRead>;
+            return Some(buf_reader);
+        }
+
+        None
+    }
+
+    fn guest_list(&self) -> GuestMap {
+        log::info!("loading vmlist");
+        GuestMap::new().expect("able to read vmlist")
+    }
+
+    fn guest_config(&self, vmid: &Vmid, entry: &GuestEntry) -> Option<Box<dyn io::BufRead>> {
+        log::info!("loading guest #{vmid} config");
+
+        let fd = open_config_file(&GuestMap::config_path(vmid, entry))
+            .expect("able to read guest config");
+
+        if let Some(file) = fd {
+            let buf_reader = Box::new(BufReader::new(file)) as Box<dyn io::BufRead>;
+            return Some(buf_reader);
+        }
+
+        None
+    }
+
+    fn guest_firewall_config(&self, vmid: &Vmid) -> Option<Box<dyn io::BufRead>> {
+        log::info!("loading guest #{vmid} firewall config");
+
+        let fd = open_config_file(&GuestMap::firewall_config_path(vmid))
+            .expect("able to read guest firewall config");
+
+        if let Some(file) = fd {
+            let buf_reader = Box::new(BufReader::new(file)) as Box<dyn io::BufRead>;
+            return Some(buf_reader);
+        }
+
+        None
+    }
+}
+
+pub trait NftConfigLoader {
+    fn chains(&self) -> CommandOutput;
+}
+
+#[derive(Debug, Default)]
+pub struct PveNftConfigLoader {}
+
+impl PveNftConfigLoader {
+    pub fn new() -> Self {
+        Default::default()
+    }
+}
+
+impl NftConfigLoader for PveNftConfigLoader {
+    fn chains(&self) -> CommandOutput {
+        log::info!("querying nftables config for chains");
+
+        let commands = Commands::new(vec![List::chains()]);
+
+        NftClient::run_json_commands(&commands)
+            .expect("can query chains in nftables")
+            .expect("nft returned output")
+    }
+}
+
+pub struct FirewallConfig {
+    firewall_loader: Box<dyn FirewallConfigLoader>,
+    nft_loader: Box<dyn NftConfigLoader>,
+    cluster_config: OnceLock<ClusterConfig>,
+    host_config: OnceLock<HostConfig>,
+    guest_config: OnceLock<BTreeMap<Vmid, GuestConfig>>,
+    nft_config: OnceLock<BTreeMap<String, ListChain>>,
+}
+
+impl Default for FirewallConfig {
+    fn default() -> Self {
+        Self {
+            firewall_loader: Box::new(PveFirewallConfigLoader::new()),
+            nft_loader: Box::new(PveNftConfigLoader::new()),
+            cluster_config: OnceLock::new(),
+            host_config: OnceLock::new(),
+            guest_config: OnceLock::new(),
+            nft_config: OnceLock::new(),
+        }
+    }
+}
+
+impl FirewallConfig {
+    pub fn new(
+        firewall_loader: Box<dyn FirewallConfigLoader>,
+        nft_loader: Box<dyn NftConfigLoader>,
+    ) -> Self {
+        Self {
+            firewall_loader,
+            nft_loader,
+            cluster_config: OnceLock::new(),
+            host_config: OnceLock::new(),
+            guest_config: OnceLock::new(),
+            nft_config: OnceLock::new(),
+        }
+    }
+
+    pub fn cluster(&self) -> &ClusterConfig {
+        self.cluster_config.get_or_init(|| {
+            let raw_config = self.firewall_loader.cluster();
+
+            match raw_config {
+                Some(data) => ClusterConfig::parse(data).expect("cluster firewall config is valid"),
+                None => {
+                    log::info!("no cluster config found, falling back to default");
+                    ClusterConfig::default()
+                }
+            }
+        })
+    }
+
+    pub fn host(&self) -> &HostConfig {
+        self.host_config.get_or_init(|| {
+            let raw_config = self.firewall_loader.host();
+
+            match raw_config {
+                Some(data) => HostConfig::parse(data).expect("host firewall config is valid"),
+                None => {
+                    log::info!("no host config found, falling back to default");
+                    HostConfig::default()
+                }
+            }
+        })
+    }
+
+    pub fn guests(&self) -> &BTreeMap<Vmid, GuestConfig> {
+        self.guest_config.get_or_init(|| {
+            let mut guests = BTreeMap::new();
+
+            for (vmid, entry) in self.firewall_loader.guest_list().iter() {
+                if !entry.is_local() {
+                    log::debug!("guest #{vmid} is not local, skipping");
+                    continue;
+                }
+
+                let raw_firewall_config = self.firewall_loader.guest_firewall_config(vmid);
+
+                if let Some(raw_firewall_config) = raw_firewall_config {
+                    log::debug!("found firewall config for #{vmid}, loading guest config");
+
+                    let raw_config = self
+                        .firewall_loader
+                        .guest_config(vmid, entry)
+                        .expect("guest config exists if firewall config exists");
+
+                    let config = GuestConfig::parse(
+                        vmid,
+                        entry.ty().iface_prefix(),
+                        raw_firewall_config,
+                        raw_config,
+                    )
+                    .expect("guest config is valid");
+
+                    guests.insert(*vmid, config);
+                }
+            }
+
+            guests
+        })
+    }
+
+    pub fn nft_chains(&self) -> &BTreeMap<String, ListChain> {
+        self.nft_config.get_or_init(|| {
+            let output = self.nft_loader.chains();
+            let mut chains = BTreeMap::new();
+
+            for element in &output.nftables {
+                if let ListOutput::Chain(chain) = element {
+                    chains.insert(chain.name().to_owned(), chain.clone());
+                }
+            }
+
+            chains
+        })
+    }
+
+    pub fn is_enabled(&self) -> bool {
+        self.cluster().is_enabled() && self.host().nftables()
+    }
+
+    pub fn alias(&self, name: &AliasName, vmid: Option<Vmid>) -> Option<&Alias> {
+        log::trace!("getting alias {name:?}");
+
+        match name.scope() {
+            AliasScope::Datacenter => self.cluster().alias(name.name()),
+            AliasScope::Guest => {
+                if let Some(vmid) = vmid {
+                    if let Some(entry) = self.guests().get(&vmid) {
+                        return entry.alias(name);
+                    }
+
+                    log::warn!("trying to get alias {name} for non-existing guest: #{vmid}");
+                }
+
+                None
+            }
+        }
+    }
+}
diff --git a/proxmox-firewall/src/main.rs b/proxmox-firewall/src/main.rs
index 248ac39..656ac15 100644
--- a/proxmox-firewall/src/main.rs
+++ b/proxmox-firewall/src/main.rs
@@ -1,5 +1,8 @@
 use anyhow::Error;
 
+mod config;
+
 fn main() -> Result<(), Error> {
+    env_logger::init();
     Ok(())
 }
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 29/39] firewall: add rule generation logic
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (27 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 28/39] firewall: add config loader Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 30/39] firewall: add object " Stefan Hanreich
                   ` (10 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

ToNftRules is basically a conversion trait for firewall config structs
to convert them into the respective nftables statements.

We are passing a list of rules to the method, which then modifies the
list of rules such that all relevant rules in the list have statements
appended that apply the configured constraints from the firewall
config.

This is particularly relevant for the rule generation logic for
ipsets. Due to how sets work in nftables we need to generate two rules
for every ipset: a rule for the v4 ipset and a rule for the v6 ipset.
This is because sets can only contain either v4 or v6 addresses. By
passing a list of all generated rules we can duplicate all rules and
then add a statement for the v4 or v6 set respectively.

This also enables us to start with multiple rules, which is required
for using log statements in conjunction with limit statements.

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-firewall/src/main.rs       |   1 +
 proxmox-firewall/src/rule.rs       | 761 +++++++++++++++++++++++++++++
 proxmox-nftables/src/expression.rs |   4 +
 3 files changed, 766 insertions(+)
 create mode 100644 proxmox-firewall/src/rule.rs

diff --git a/proxmox-firewall/src/main.rs b/proxmox-firewall/src/main.rs
index 656ac15..ae832e3 100644
--- a/proxmox-firewall/src/main.rs
+++ b/proxmox-firewall/src/main.rs
@@ -1,6 +1,7 @@
 use anyhow::Error;
 
 mod config;
+mod rule;
 
 fn main() -> Result<(), Error> {
     env_logger::init();
diff --git a/proxmox-firewall/src/rule.rs b/proxmox-firewall/src/rule.rs
new file mode 100644
index 0000000..c8099d0
--- /dev/null
+++ b/proxmox-firewall/src/rule.rs
@@ -0,0 +1,761 @@
+use std::ops::{Deref, DerefMut};
+
+use anyhow::{format_err, Error};
+use proxmox_nftables::{
+    expression::{Ct, IpFamily, Meta, Payload, Prefix},
+    statement::{Log, LogLevel, Match, Operator},
+    types::{AddRule, ChainPart, SetName},
+    Expression, Statement,
+};
+use proxmox_ve_config::{
+    firewall::{
+        ct_helper::CtHelperMacro,
+        fw_macros::{get_macro, FwMacro},
+        types::{
+            address::Family,
+            alias::AliasName,
+            ipset::{Ipfilter, IpsetName},
+            log::LogRateLimit,
+            rule::{Direction, Kind, RuleGroup},
+            rule_match::{
+                Icmp, Icmpv6, IpAddrMatch, IpMatch, Ports, Protocol, RuleMatch, Sctp, Tcp, Udp,
+            },
+            Alias, Rule,
+        },
+    },
+    guest::types::Vmid,
+};
+
+use crate::config::FirewallConfig;
+
+#[derive(Debug, Clone)]
+pub(crate) struct NftRule {
+    family: Option<Family>,
+    statements: Vec<Statement>,
+    terminal_statements: Vec<Statement>,
+}
+
+impl NftRule {
+    pub fn from_terminal_statements(terminal_statements: Vec<Statement>) -> Self {
+        Self {
+            family: None,
+            statements: Vec::new(),
+            terminal_statements,
+        }
+    }
+
+    pub fn new(terminal_statement: Statement) -> Self {
+        Self {
+            family: None,
+            statements: Vec::new(),
+            terminal_statements: vec![terminal_statement],
+        }
+    }
+
+    pub fn from_config_rule(rule: &Rule, env: &NftRuleEnv) -> Result<Vec<NftRule>, Error> {
+        let mut rules = Vec::new();
+
+        if rule.disabled() {
+            return Ok(rules);
+        }
+
+        rule.to_nft_rules(&mut rules, env)?;
+
+        Ok(rules)
+    }
+
+    pub fn from_ct_helper(
+        ct_helper: &CtHelperMacro,
+        env: &NftRuleEnv,
+    ) -> Result<Vec<NftRule>, Error> {
+        let mut rules = Vec::new();
+        ct_helper.to_nft_rules(&mut rules, env)?;
+        Ok(rules)
+    }
+
+    pub fn from_ipfilter(ipfilter: &Ipfilter, env: &NftRuleEnv) -> Result<Vec<NftRule>, Error> {
+        let mut rules = Vec::new();
+        ipfilter.to_nft_rules(&mut rules, env)?;
+        Ok(rules)
+    }
+}
+
+impl Deref for NftRule {
+    type Target = Vec<Statement>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.statements
+    }
+}
+
+impl DerefMut for NftRule {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.statements
+    }
+}
+
+impl NftRule {
+    pub fn into_add_rule(self, chain: ChainPart) -> AddRule {
+        let statements = self.statements.into_iter().chain(self.terminal_statements);
+
+        AddRule::from_statements(chain, statements)
+    }
+
+    pub fn family(&self) -> Option<Family> {
+        self.family
+    }
+
+    pub fn set_family(&mut self, family: Family) {
+        self.family = Some(family);
+    }
+}
+
+pub(crate) struct NftRuleEnv<'a> {
+    pub(crate) chain: ChainPart,
+    pub(crate) direction: Direction,
+    pub(crate) firewall_config: &'a FirewallConfig,
+    pub(crate) vmid: Option<Vmid>,
+}
+
+impl NftRuleEnv<'_> {
+    fn alias(&self, name: &AliasName) -> Option<&Alias> {
+        self.firewall_config.alias(name, self.vmid)
+    }
+
+    fn iface_name(&self, rule_iface: &str) -> String {
+        match &self.vmid {
+            Some(vmid) => {
+                if let Some(config) = self.firewall_config.guests().get(vmid) {
+                    if let Ok(name) = config.iface_name_by_key(rule_iface) {
+                        return name;
+                    }
+                }
+
+                log::warn!("Unable to resolve interface name {rule_iface} for VM #{vmid}");
+
+                rule_iface.to_string()
+            }
+            None => rule_iface.to_string(),
+        }
+    }
+
+    fn default_log_limit(&self) -> Option<LogRateLimit> {
+        self.firewall_config.cluster().log_ratelimit()
+    }
+
+    fn contains_family(&self, family: Family) -> bool {
+        self.chain.table().family().families().contains(&family)
+    }
+}
+
+pub(crate) trait ToNftRules {
+    fn to_nft_rules(&self, rules: &mut Vec<NftRule>, env: &NftRuleEnv) -> Result<(), Error>;
+}
+
+impl ToNftRules for Rule {
+    fn to_nft_rules(&self, rules: &mut Vec<NftRule>, env: &NftRuleEnv) -> Result<(), Error> {
+        log::trace!("generating nft rules for config rule {self:?}");
+
+        match self.kind() {
+            Kind::Match(rule) => rule.to_nft_rules(rules, env)?,
+            Kind::Group(group) => group.to_nft_rules(rules, env)?,
+        };
+
+        Ok(())
+    }
+}
+
+fn handle_iface(rules: &mut [NftRule], env: &NftRuleEnv, name: &str) -> Result<(), Error> {
+    let iface_key = match (env.vmid, env.direction) {
+        (Some(_), Direction::In) => "oifname",
+        (Some(_), Direction::Out) => "iifname",
+        (None, Direction::In) => "iifname",
+        (None, Direction::Out) => "oifname",
+    };
+
+    let iface_name = env.iface_name(name);
+
+    log::trace!("adding interface: {iface_name}");
+
+    for rule in rules.iter_mut() {
+        rule.push(
+            Match::new_eq(
+                Expression::from(Meta::new(iface_key.to_string())),
+                Expression::from(iface_name.clone()),
+            )
+            .into(),
+        )
+    }
+
+    Ok(())
+}
+
+impl ToNftRules for RuleGroup {
+    fn to_nft_rules(&self, rules: &mut Vec<NftRule>, env: &NftRuleEnv) -> Result<(), Error> {
+        let chain_name = format!("group-{}-{}", self.group(), env.direction);
+
+        rules.push(NftRule::new(Statement::jump(chain_name)));
+
+        if let Some(name) = &self.iface() {
+            handle_iface(rules, env, name)?;
+        }
+
+        Ok(())
+    }
+}
+
+impl ToNftRules for RuleMatch {
+    fn to_nft_rules(&self, rules: &mut Vec<NftRule>, env: &NftRuleEnv) -> Result<(), Error> {
+        if env.direction != self.direction() {
+            return Ok(());
+        }
+
+        if let Some(log) = self.log() {
+            if let Ok(log_level) = LogLevel::try_from(log) {
+                let mut terminal_statements = Vec::new();
+
+                if let Some(limit) = env.default_log_limit() {
+                    terminal_statements.push(Statement::from(limit));
+                }
+
+                terminal_statements.push(
+                    Log::new_nflog(
+                        Log::generate_prefix(env.vmid, log_level, env.chain.name(), self.verdict()),
+                        0,
+                    )
+                    .into(),
+                );
+
+                rules.push(NftRule::from_terminal_statements(terminal_statements));
+            }
+        }
+
+        rules.push(NftRule::new(Statement::from(self.verdict())));
+
+        if let Some(name) = &self.iface() {
+            handle_iface(rules, env, name)?;
+        }
+
+        if let Some(protocol) = self.proto() {
+            protocol.to_nft_rules(rules, env)?;
+        }
+
+        if let Some(name) = self.fw_macro() {
+            let fw_macro =
+                get_macro(name).ok_or_else(|| format_err!("cannot find macro {name}"))?;
+
+            fw_macro.to_nft_rules(rules, env)?;
+        }
+
+        if let Some(ip) = self.ip() {
+            ip.to_nft_rules(rules, env)?;
+        }
+
+        Ok(())
+    }
+}
+
+fn handle_set(
+    rules: &mut Vec<NftRule>,
+    name: &IpsetName,
+    field_name: &str,
+    env: &NftRuleEnv,
+    contains: bool,
+) -> Result<(), Error> {
+    let mut new_rules = rules
+        .drain(..)
+        .flat_map(|rule| {
+            let mut new_rules = Vec::new();
+
+            if matches!(rule.family(), Some(Family::V4) | None) && env.contains_family(Family::V4) {
+                let field = Payload::field("ip", field_name);
+
+                let mut rule = rule.clone();
+                rule.set_family(Family::V4);
+
+                rule.append(&mut vec![
+                    Match::new(
+                        if contains { Operator::Eq } else { Operator::Ne },
+                        field.clone(),
+                        Expression::set_name(&SetName::ipset_name(
+                            Family::V4,
+                            name,
+                            env.vmid,
+                            false,
+                        )),
+                    )
+                    .into(),
+                    Match::new(
+                        if contains { Operator::Ne } else { Operator::Eq },
+                        field,
+                        Expression::set_name(&SetName::ipset_name(
+                            Family::V4,
+                            name,
+                            env.vmid,
+                            true,
+                        )),
+                    )
+                    .into(),
+                ]);
+
+                new_rules.push(rule);
+            }
+
+            if matches!(rule.family(), Some(Family::V6) | None) && env.contains_family(Family::V6) {
+                let field = Payload::field("ip6", field_name);
+
+                let mut rule = rule;
+                rule.set_family(Family::V6);
+
+                rule.append(&mut vec![
+                    Match::new(
+                        if contains { Operator::Eq } else { Operator::Ne },
+                        field.clone(),
+                        Expression::set_name(&SetName::ipset_name(
+                            Family::V6,
+                            name,
+                            env.vmid,
+                            false,
+                        )),
+                    )
+                    .into(),
+                    Match::new(
+                        if contains { Operator::Ne } else { Operator::Eq },
+                        field,
+                        Expression::set_name(&SetName::ipset_name(
+                            Family::V6,
+                            name,
+                            env.vmid,
+                            true,
+                        )),
+                    )
+                    .into(),
+                ]);
+
+                new_rules.push(rule);
+            }
+
+            new_rules
+        })
+        .collect::<Vec<NftRule>>();
+
+    rules.append(&mut new_rules);
+
+    Ok(())
+}
+
+fn handle_match(
+    rules: &mut Vec<NftRule>,
+    ip: &IpAddrMatch,
+    field_name: &str,
+    env: &NftRuleEnv,
+) -> Result<(), Error> {
+    match ip {
+        IpAddrMatch::Ip(list) => {
+            if !env.contains_family(list.family()) {
+                return Ok(());
+            }
+
+            let field = match list.family() {
+                Family::V4 => Payload::field("ip", field_name),
+                Family::V6 => Payload::field("ip6", field_name),
+            };
+
+            for rule in rules {
+                match rule.family() {
+                    None => {
+                        rule.push(Match::new_eq(field.clone(), Expression::from(list)).into());
+
+                        rule.set_family(list.family());
+                    }
+                    Some(rule_family) if rule_family == list.family() => {
+                        rule.push(Match::new_eq(field.clone(), Expression::from(list)).into());
+                    }
+                    _ => (),
+                };
+            }
+
+            Ok(())
+        }
+        IpAddrMatch::Alias(alias_name) => {
+            let alias = env
+                .alias(alias_name)
+                .ok_or_else(|| format_err!("could not find alias {alias_name}"))?;
+
+            if !env.contains_family(alias.address().family()) {
+                return Ok(());
+            }
+
+            let field = match alias.address().family() {
+                Family::V4 => Payload::field("ip", field_name),
+                Family::V6 => Payload::field("ip6", field_name),
+            };
+
+            for rule in rules {
+                match rule.family() {
+                    None => {
+                        rule.push(
+                            Match::new_eq(
+                                field.clone(),
+                                Expression::from(Prefix::from(alias.address())),
+                            )
+                            .into(),
+                        );
+
+                        rule.set_family(alias.address().family());
+                    }
+                    Some(rule_family) if rule_family == alias.address().family() => {
+                        rule.push(
+                            Match::new_eq(
+                                field.clone(),
+                                Expression::from(Prefix::from(alias.address())),
+                            )
+                            .into(),
+                        );
+                    }
+                    _ => (),
+                }
+            }
+
+            Ok(())
+        }
+        IpAddrMatch::Set(name) => handle_set(rules, name, field_name, env, true),
+    }
+}
+
+impl ToNftRules for IpMatch {
+    fn to_nft_rules(&self, rules: &mut Vec<NftRule>, env: &NftRuleEnv) -> Result<(), Error> {
+        log::trace!("adding ip match: {self:?}");
+
+        if let Some(src) = self.src() {
+            log::trace!("adding src: {src:?}");
+            handle_match(rules, src, "saddr", env)?;
+        }
+
+        if let Some(dst) = self.dst() {
+            log::trace!("adding dst: {dst:?}");
+            handle_match(rules, dst, "daddr", env)?;
+        }
+
+        Ok(())
+    }
+}
+
+fn handle_protocol(rules: &mut [NftRule], _env: &NftRuleEnv, name: &str) -> Result<(), Error> {
+    for rule in rules.iter_mut() {
+        rule.push(Match::new_eq(Meta::new("l4proto"), Expression::from(name)).into());
+    }
+
+    Ok(())
+}
+
+impl ToNftRules for Protocol {
+    fn to_nft_rules(&self, rules: &mut Vec<NftRule>, env: &NftRuleEnv) -> Result<(), Error> {
+        log::trace!("adding protocol: {self:?}");
+
+        match self {
+            Protocol::Tcp(tcp) => tcp.to_nft_rules(rules, env),
+            Protocol::Udp(udp) => udp.to_nft_rules(rules, env),
+            Protocol::Dccp(ports) => {
+                handle_protocol(rules, env, "dccp")?;
+                ports.to_nft_rules(rules, env)
+            }
+            Protocol::UdpLite(ports) => {
+                handle_protocol(rules, env, "udplite")?;
+                ports.to_nft_rules(rules, env)
+            }
+            Protocol::Sctp(sctp) => sctp.to_nft_rules(rules, env),
+            Protocol::Icmp(icmp) => icmp.to_nft_rules(rules, env),
+            Protocol::Icmpv6(icmpv6) => icmpv6.to_nft_rules(rules, env),
+            Protocol::Named(name) => handle_protocol(rules, env, name),
+            Protocol::Numeric(id) => {
+                for rule in rules.iter_mut() {
+                    rule.push(Match::new_eq(Meta::new("l4proto"), Expression::from(*id)).into());
+                }
+
+                Ok(())
+            }
+        }
+    }
+}
+
+impl ToNftRules for Tcp {
+    fn to_nft_rules(&self, rules: &mut Vec<NftRule>, env: &NftRuleEnv) -> Result<(), Error> {
+        handle_protocol(rules, env, "tcp")?;
+        self.ports().to_nft_rules(rules, env)
+    }
+}
+
+impl ToNftRules for Udp {
+    fn to_nft_rules(&self, rules: &mut Vec<NftRule>, env: &NftRuleEnv) -> Result<(), Error> {
+        handle_protocol(rules, env, "udp")?;
+        self.ports().to_nft_rules(rules, env)
+    }
+}
+
+impl ToNftRules for Sctp {
+    fn to_nft_rules(&self, rules: &mut Vec<NftRule>, env: &NftRuleEnv) -> Result<(), Error> {
+        handle_protocol(rules, env, "sctp")?;
+        self.ports().to_nft_rules(rules, env)
+    }
+}
+
+impl ToNftRules for Icmp {
+    fn to_nft_rules(&self, rules: &mut Vec<NftRule>, _env: &NftRuleEnv) -> Result<(), Error> {
+        for rule in rules.iter_mut() {
+            if matches!(rule.family(), Some(Family::V4) | None) {
+                if let Some(icmp_code) = self.code() {
+                    rule.push(
+                        Match::new_eq(Payload::field("icmp", "code"), Expression::from(icmp_code))
+                            .into(),
+                    );
+                } else if let Some(icmp_type) = self.ty() {
+                    rule.push(
+                        Match::new_eq(Payload::field("icmp", "type"), Expression::from(icmp_type))
+                            .into(),
+                    );
+                } else {
+                    rule.push(Match::new_eq(Meta::new("l4proto"), Expression::from("icmp")).into());
+                }
+
+                rule.set_family(Family::V4);
+            }
+        }
+
+        Ok(())
+    }
+}
+
+impl ToNftRules for Icmpv6 {
+    fn to_nft_rules(&self, rules: &mut Vec<NftRule>, _env: &NftRuleEnv) -> Result<(), Error> {
+        log::trace!("applying icmpv6: {self:?}");
+
+        for rule in rules.iter_mut() {
+            if matches!(rule.family(), Some(Family::V6) | None) {
+                if let Some(icmp_code) = self.code() {
+                    rule.push(
+                        Match::new_eq(
+                            Payload::field("icmpv6", "code"),
+                            Expression::from(icmp_code),
+                        )
+                        .into(),
+                    );
+                } else if let Some(icmp_type) = self.ty() {
+                    rule.push(
+                        Match::new_eq(
+                            Payload::field("icmpv6", "type"),
+                            Expression::from(icmp_type),
+                        )
+                        .into(),
+                    );
+                } else {
+                    rule.push(
+                        Match::new_eq(Meta::new("l4proto"), Expression::from("icmpv6")).into(),
+                    );
+                }
+
+                rule.set_family(Family::V6);
+            }
+        }
+
+        Ok(())
+    }
+}
+
+impl ToNftRules for Ports {
+    fn to_nft_rules(&self, rules: &mut Vec<NftRule>, _env: &NftRuleEnv) -> Result<(), Error> {
+        log::trace!("applying ports: {self:?}");
+
+        for rule in rules {
+            if let Some(sport) = self.sport() {
+                log::trace!("applying sport: {sport:?}");
+
+                rule.push(
+                    Match::new_eq(
+                        Expression::from(Payload::field("th", "sport")),
+                        Expression::from(sport),
+                    )
+                    .into(),
+                )
+            }
+
+            if let Some(dport) = self.dport() {
+                log::trace!("applying dport: {dport:?}");
+
+                rule.push(
+                    Match::new_eq(
+                        Expression::from(Payload::field("th", "dport")),
+                        Expression::from(dport),
+                    )
+                    .into(),
+                )
+            }
+        }
+
+        Ok(())
+    }
+}
+
+impl ToNftRules for Ipfilter<'_> {
+    fn to_nft_rules(&self, rules: &mut Vec<NftRule>, env: &NftRuleEnv) -> Result<(), Error> {
+        let vmid = env
+            .vmid
+            .ok_or_else(|| format_err!("can only create ipfilter for guests"))?;
+
+        let guest_config = env
+            .firewall_config
+            .guests()
+            .get(&vmid)
+            .ok_or_else(|| format_err!("no guest config found!"))?;
+
+        if !guest_config.ipfilter() {
+            return Ok(());
+        }
+
+        match env.direction {
+            Direction::In => {
+                if env.contains_family(Family::V4) {
+                    let mut rule = NftRule::new(Statement::make_drop());
+                    rule.set_family(Family::V4);
+
+                    rule.append(&mut vec![
+                        Match::new_eq(
+                            Expression::from(Meta::new("oifname")),
+                            guest_config.iface_name_by_index(self.index()),
+                        )
+                        .into(),
+                        Match::new_ne(
+                            Payload::field("arp", "daddr ip"),
+                            Expression::set_name(&SetName::ipset_name(
+                                Family::V4,
+                                self.ipset().name(),
+                                env.vmid,
+                                false,
+                            )),
+                        )
+                        .into(),
+                    ]);
+
+                    rules.push(rule);
+                }
+            }
+            Direction::Out => {
+                let mut base_rule = NftRule::new(Statement::make_drop());
+
+                base_rule.push(
+                    Match::new_eq(
+                        Expression::from(Meta::new("iifname")),
+                        guest_config.iface_name_by_index(self.index()),
+                    )
+                    .into(),
+                );
+
+                let mut ipfilter_rules = vec![base_rule.clone()];
+                handle_set(
+                    &mut ipfilter_rules,
+                    self.ipset().name(),
+                    "saddr",
+                    env,
+                    false,
+                )?;
+                rules.append(&mut ipfilter_rules);
+
+                if env.contains_family(Family::V4) {
+                    base_rule.set_family(Family::V4);
+
+                    base_rule.append(&mut vec![Match::new_ne(
+                        Payload::field("arp", "saddr ip"),
+                        Expression::set_name(&SetName::ipset_name(
+                            Family::V4,
+                            self.ipset().name(),
+                            env.vmid,
+                            false,
+                        )),
+                    )
+                    .into()]);
+
+                    rules.push(base_rule);
+                }
+            }
+        }
+
+        Ok(())
+    }
+}
+
+impl ToNftRules for CtHelperMacro {
+    fn to_nft_rules(&self, rules: &mut Vec<NftRule>, env: &NftRuleEnv) -> Result<(), Error> {
+        if let Some(family) = self.family() {
+            if !env.contains_family(family) {
+                return Ok(());
+            }
+        }
+
+        if self.tcp().is_none() && self.udp().is_none() {
+            return Ok(());
+        }
+
+        log::trace!("applying ct helper: {self:?}");
+
+        let ip_family = self.family().map(IpFamily::from);
+
+        if let Some(protocol) = self.tcp() {
+            let base_rule = NftRule::from_terminal_statements(vec![
+                Match::new_eq(
+                    Ct::new("state", None),
+                    Expression::List(vec!["new".into(), "established".into()]),
+                )
+                .into(),
+                Statement::make_accept(),
+            ]);
+
+            let helper_rule = NftRule::new(Statement::CtHelper(self.tcp_helper_name()));
+
+            let mut ct_rules = vec![base_rule, helper_rule];
+            protocol.to_nft_rules(&mut ct_rules, env)?;
+            rules.append(&mut ct_rules);
+        }
+
+        if let Some(protocol) = self.udp() {
+            let base_rule = NftRule::from_terminal_statements(vec![
+                Match::new_eq(
+                    Ct::new("state", None),
+                    Expression::List(vec!["new".into(), "established".into()]),
+                )
+                .into(),
+                Statement::make_accept(),
+            ]);
+
+            let helper_rule = NftRule::new(Statement::CtHelper(self.udp_helper_name()));
+
+            let mut ct_rules = vec![base_rule, helper_rule];
+            protocol.to_nft_rules(&mut ct_rules, env)?;
+            rules.append(&mut ct_rules);
+        }
+
+        let mut ct_helper_rule = NftRule::new(Statement::make_accept());
+
+        ct_helper_rule.push(Match::new_eq(Ct::new("helper", ip_family), self.name()).into());
+
+        rules.push(ct_helper_rule);
+
+        Ok(())
+    }
+}
+
+impl ToNftRules for FwMacro {
+    fn to_nft_rules(&self, rules: &mut Vec<NftRule>, env: &NftRuleEnv) -> Result<(), Error> {
+        log::trace!("applying macro: {self:?}");
+
+        let initial_rules: Vec<NftRule> = rules.drain(..).collect();
+
+        for protocol in &self.code {
+            let mut new_rules = initial_rules.to_vec();
+            protocol.to_nft_rules(&mut new_rules, env)?;
+
+            rules.append(&mut new_rules);
+        }
+
+        Ok(())
+    }
+}
diff --git a/proxmox-nftables/src/expression.rs b/proxmox-nftables/src/expression.rs
index 3b8ade0..20559e8 100644
--- a/proxmox-nftables/src/expression.rs
+++ b/proxmox-nftables/src/expression.rs
@@ -57,6 +57,10 @@ impl Expression {
     pub fn concat(expressions: impl IntoIterator<Item = Expression>) -> Self {
         Expression::Concat(Vec::from_iter(expressions))
     }
+
+    pub fn set_name(name: &str) -> Self {
+        Expression::String(format!("@{name}"))
+    }
 }
 
 impl From<bool> for Expression {
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 30/39] firewall: add object generation logic
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (28 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 29/39] firewall: add rule generation logic Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 31/39] firewall: add ruleset " Stefan Hanreich
                   ` (9 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

ToNftObjects is basically a conversion trait that converts firewall
config structs into nftables objects. It returns a list of commands
that create the respective nftables objects.

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-firewall/src/main.rs   |   1 +
 proxmox-firewall/src/object.rs | 140 +++++++++++++++++++++++++++++++++
 2 files changed, 141 insertions(+)
 create mode 100644 proxmox-firewall/src/object.rs

diff --git a/proxmox-firewall/src/main.rs b/proxmox-firewall/src/main.rs
index ae832e3..a4979a7 100644
--- a/proxmox-firewall/src/main.rs
+++ b/proxmox-firewall/src/main.rs
@@ -1,6 +1,7 @@
 use anyhow::Error;
 
 mod config;
+mod object;
 mod rule;
 
 fn main() -> Result<(), Error> {
diff --git a/proxmox-firewall/src/object.rs b/proxmox-firewall/src/object.rs
new file mode 100644
index 0000000..32c4ddb
--- /dev/null
+++ b/proxmox-firewall/src/object.rs
@@ -0,0 +1,140 @@
+use anyhow::{format_err, Error};
+use proxmox_nftables::{
+    command::{Add, Flush},
+    expression::Prefix,
+    types::{
+        AddCtHelper, AddElement, CtHelperProtocol, ElementType, L3Protocol, SetConfig, SetFlag,
+        SetName, TablePart,
+    },
+    Command, Expression,
+};
+use proxmox_ve_config::{
+    firewall::{
+        ct_helper::CtHelperMacro,
+        types::{address::Family, alias::AliasName, ipset::IpsetAddress, Alias, Ipset},
+    },
+    guest::types::Vmid,
+};
+
+use crate::config::FirewallConfig;
+
+pub(crate) struct NftObjectEnv<'a, 'b> {
+    pub(crate) table: &'a TablePart,
+    pub(crate) firewall_config: &'b FirewallConfig,
+    pub(crate) vmid: Option<Vmid>,
+}
+
+impl NftObjectEnv<'_, '_> {
+    pub(crate) fn alias(&self, name: &AliasName) -> Option<&Alias> {
+        self.firewall_config.alias(name, self.vmid)
+    }
+}
+
+pub(crate) trait ToNftObjects {
+    fn to_nft_objects(&self, env: &NftObjectEnv) -> Result<Vec<Command>, Error>;
+}
+
+impl ToNftObjects for CtHelperMacro {
+    fn to_nft_objects(&self, env: &NftObjectEnv) -> Result<Vec<Command>, Error> {
+        let mut commands = Vec::new();
+
+        if let Some(_protocol) = self.tcp() {
+            commands.push(Add::ct_helper(AddCtHelper {
+                table: env.table.clone(),
+                name: self.tcp_helper_name(),
+                ty: self.name().to_string(),
+                protocol: CtHelperProtocol::TCP,
+                l3proto: self.family().map(L3Protocol::from),
+            }));
+        }
+
+        if let Some(_protocol) = self.udp() {
+            commands.push(Add::ct_helper(AddCtHelper {
+                table: env.table.clone(),
+                name: self.udp_helper_name(),
+                ty: self.name().to_string(),
+                protocol: CtHelperProtocol::UDP,
+                l3proto: self.family().map(L3Protocol::from),
+            }));
+        }
+
+        Ok(commands)
+    }
+}
+
+impl ToNftObjects for Ipset {
+    fn to_nft_objects(&self, env: &NftObjectEnv) -> Result<Vec<Command>, Error> {
+        let mut commands = Vec::new();
+        log::trace!("generating objects for ipset: {self:?}");
+
+        for family in env.table.family().families() {
+            let mut elements = Vec::new();
+            let mut nomatch_elements = Vec::new();
+
+            for element in self.iter() {
+                let cidr = match &element.address {
+                    IpsetAddress::Cidr(cidr) => cidr,
+                    IpsetAddress::Alias(alias) => env
+                        .alias(alias)
+                        .ok_or(format_err!("could not find alias {alias} in environment"))?
+                        .address(),
+                };
+
+                if family != cidr.family() {
+                    continue;
+                }
+
+                let expression = Expression::from(Prefix::from(cidr));
+
+                if element.nomatch {
+                    nomatch_elements.push(expression);
+                } else {
+                    elements.push(expression);
+                }
+            }
+
+            let element_type = match family {
+                Family::V4 => ElementType::Ipv4Addr,
+                Family::V6 => ElementType::Ipv6Addr,
+            };
+
+            let set_name = SetName::new(
+                env.table.clone(),
+                SetName::ipset_name(family, self.name(), env.vmid, false),
+            );
+
+            let set_config =
+                SetConfig::new(set_name.clone(), vec![element_type]).with_flag(SetFlag::Interval);
+
+            let nomatch_name = SetName::new(
+                env.table.clone(),
+                SetName::ipset_name(family, self.name(), env.vmid, true),
+            );
+
+            let nomatch_config = SetConfig::new(nomatch_name.clone(), vec![element_type])
+                .with_flag(SetFlag::Interval);
+
+            commands.append(&mut vec![
+                Add::set(set_config),
+                Flush::set(set_name.clone()),
+                Add::set(nomatch_config),
+                Flush::set(nomatch_name.clone()),
+            ]);
+
+            if !elements.is_empty() {
+                commands.push(Add::element(AddElement::set_from_expressions(
+                    set_name, elements,
+                )));
+            }
+
+            if !nomatch_elements.is_empty() {
+                commands.push(Add::element(AddElement::set_from_expressions(
+                    nomatch_name,
+                    nomatch_elements,
+                )));
+            }
+        }
+
+        Ok(commands)
+    }
+}
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 31/39] firewall: add ruleset generation logic
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (29 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 30/39] firewall: add object " Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 32/39] firewall: add proxmox-firewall binary and move existing code into lib Stefan Hanreich
                   ` (8 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

We create the rules from the firewall config by utilizing the
ToNftRules and ToNftObjects traits to convert the firewall config
structs to nftables objects/chains/rules.

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-firewall/Cargo.toml      |   5 +
 proxmox-firewall/src/firewall.rs | 899 +++++++++++++++++++++++++++++++
 proxmox-firewall/src/main.rs     |   1 +
 3 files changed, 905 insertions(+)
 create mode 100644 proxmox-firewall/src/firewall.rs

diff --git a/proxmox-firewall/Cargo.toml b/proxmox-firewall/Cargo.toml
index 431e71a..bec7552 100644
--- a/proxmox-firewall/Cargo.toml
+++ b/proxmox-firewall/Cargo.toml
@@ -15,5 +15,10 @@ log = "0.4"
 env_logger = "0.10"
 anyhow = "1"
 
+serde = { version = "1", features = [ "derive" ] }
+serde_json = "1"
+
+signal-hook = "0.3"
+
 proxmox-nftables = { path = "../proxmox-nftables", features = ["config-ext"] }
 proxmox-ve-config = { path = "../proxmox-ve-config" }
diff --git a/proxmox-firewall/src/firewall.rs b/proxmox-firewall/src/firewall.rs
new file mode 100644
index 0000000..2195a07
--- /dev/null
+++ b/proxmox-firewall/src/firewall.rs
@@ -0,0 +1,899 @@
+use std::collections::BTreeMap;
+use std::fs;
+
+use anyhow::Error;
+
+use proxmox_nftables::command::{Add, Commands, Delete, Flush};
+use proxmox_nftables::expression::{Meta, Payload};
+use proxmox_nftables::helper::NfVec;
+use proxmox_nftables::statement::{AnonymousLimit, Log, LogLevel, Match, Set, SetOperation};
+use proxmox_nftables::types::{
+    AddElement, AddRule, ChainPart, MapValue, RateTimescale, SetName, TableFamily, TableName,
+    TablePart, Verdict,
+};
+use proxmox_nftables::{Expression, Statement};
+
+use proxmox_ve_config::firewall::ct_helper::get_cthelper;
+use proxmox_ve_config::firewall::guest::Config as GuestConfig;
+use proxmox_ve_config::firewall::host::Config as HostConfig;
+
+use proxmox_ve_config::firewall::types::address::Ipv6Cidr;
+use proxmox_ve_config::firewall::types::ipset::{
+    Ipfilter, Ipset, IpsetEntry, IpsetName, IpsetScope,
+};
+use proxmox_ve_config::firewall::types::log::{LogLevel as ConfigLogLevel, LogRateLimit};
+use proxmox_ve_config::firewall::types::rule::{Direction, Verdict as ConfigVerdict};
+use proxmox_ve_config::firewall::types::Group;
+use proxmox_ve_config::guest::types::Vmid;
+
+use crate::config::FirewallConfig;
+use crate::object::{NftObjectEnv, ToNftObjects};
+use crate::rule::{NftRule, NftRuleEnv};
+
+static CLUSTER_TABLE_NAME: &str = "proxmox-firewall";
+static HOST_TABLE_NAME: &str = "proxmox-firewall";
+static GUEST_TABLE_NAME: &str = "proxmox-firewall-guests";
+
+static NF_CONNTRACK_MAX_FILE: &str = "/proc/sys/net/netfilter/nf_conntrack_max";
+static NF_CONNTRACK_TCP_TIMEOUT_ESTABLISHED: &str =
+    "/proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established";
+static NF_CONNTRACK_TCP_TIMEOUT_SYN_RECV: &str =
+    "/proc/sys/net/netfilter/nf_conntrack_tcp_timeout_syn_recv";
+static LOG_CONNTRACK_FILE: &str = "/var/lib/pve-firewall/log_nf_conntrack";
+
+#[derive(Default)]
+pub struct Firewall {
+    config: FirewallConfig,
+}
+
+impl From<FirewallConfig> for Firewall {
+    fn from(config: FirewallConfig) -> Self {
+        Self { config }
+    }
+}
+
+impl Firewall {
+    pub fn new() -> Self {
+        Self {
+            ..Default::default()
+        }
+    }
+
+    pub fn is_enabled(&self) -> bool {
+        self.config.is_enabled()
+    }
+
+    fn cluster_table() -> TablePart {
+        TablePart::new(TableFamily::Inet, CLUSTER_TABLE_NAME)
+    }
+
+    fn host_table() -> TablePart {
+        TablePart::new(TableFamily::Inet, HOST_TABLE_NAME)
+    }
+
+    fn guest_table() -> TablePart {
+        TablePart::new(TableFamily::Bridge, GUEST_TABLE_NAME)
+    }
+
+    fn guest_vmap(dir: Direction) -> SetName {
+        SetName::new(Self::guest_table(), format!("vm-map-{dir}"))
+    }
+
+    fn cluster_chain(dir: Direction) -> ChainPart {
+        ChainPart::new(Self::cluster_table(), format!("cluster-{dir}"))
+    }
+
+    fn host_chain(dir: Direction) -> ChainPart {
+        ChainPart::new(Self::host_table(), format!("host-{dir}"))
+    }
+
+    fn guest_chain(dir: Direction, vmid: Vmid) -> ChainPart {
+        ChainPart::new(Self::guest_table(), format!("guest-{vmid}-{dir}"))
+    }
+
+    fn group_chain(table: TablePart, name: &str, dir: Direction) -> ChainPart {
+        ChainPart::new(table, format!("group-{name}-{dir}"))
+    }
+
+    fn host_conntrack_chain() -> ChainPart {
+        ChainPart::new(Self::host_table(), "ct-in".to_string())
+    }
+
+    fn host_option_chain(dir: Direction) -> ChainPart {
+        ChainPart::new(Self::host_table(), format!("option-{dir}"))
+    }
+
+    fn synflood_limit_chain() -> ChainPart {
+        ChainPart::new(Self::host_table(), "ratelimit-synflood")
+    }
+
+    fn log_invalid_tcp_chain() -> ChainPart {
+        ChainPart::new(Self::host_table(), "log-invalid-tcp")
+    }
+
+    fn log_smurfs_chain() -> ChainPart {
+        ChainPart::new(Self::host_table(), "log-smurfs")
+    }
+
+    fn default_log_limit(&self) -> Option<LogRateLimit> {
+        self.config.cluster().log_ratelimit()
+    }
+
+    fn reset_firewall(&self, commands: &mut Commands) {
+        commands.append(&mut vec![
+            Flush::chain(Self::cluster_chain(Direction::In)),
+            Flush::chain(Self::cluster_chain(Direction::Out)),
+            Add::chain(Self::host_chain(Direction::In)),
+            Flush::chain(Self::host_chain(Direction::In)),
+            Flush::chain(Self::host_option_chain(Direction::In)),
+            Add::chain(Self::host_chain(Direction::Out)),
+            Flush::chain(Self::host_chain(Direction::Out)),
+            Flush::chain(Self::host_option_chain(Direction::Out)),
+            Flush::map(Self::guest_vmap(Direction::In)),
+            Flush::map(Self::guest_vmap(Direction::Out)),
+            Flush::chain(Self::host_conntrack_chain()),
+            Flush::chain(Self::synflood_limit_chain()),
+            Flush::chain(Self::log_invalid_tcp_chain()),
+            Flush::chain(Self::log_smurfs_chain()),
+        ]);
+
+        /*
+        for prefix in ["v4-guest-", "v6-guest-", "v4-dc/", "v6-dc/"] {
+            for (name, set) in &self.config.nft().sets {
+                if name.starts_with(prefix) {
+                    commands.push(Delete::set(set.name().clone()))
+                }
+            }
+        }
+        */
+
+        // we need to remove guest chains before group chains
+        for prefix in ["guest-", "group-"] {
+            for (name, chain) in self.config.nft_chains() {
+                if name.starts_with(prefix) {
+                    commands.push(Delete::chain(chain.clone()))
+                }
+            }
+        }
+    }
+
+    pub fn remove_commands() -> Commands {
+        Commands::new(vec![
+            Delete::table(Self::cluster_table()),
+            Delete::table(Self::guest_table()),
+        ])
+    }
+
+    fn create_management_ipset(&self, commands: &mut Commands) -> Result<(), Error> {
+        if self.config.cluster().ipsets().get("management").is_none() {
+            log::trace!("auto-generating management ipset");
+
+            let management_ips = HostConfig::management_ips()?;
+
+            let mut ipset = Ipset::from_parts(IpsetScope::Datacenter, "management");
+            ipset.reserve(management_ips.len());
+
+            let entries = management_ips.into_iter().map(IpsetEntry::from);
+
+            ipset.extend(entries);
+
+            let env = NftObjectEnv {
+                table: &Self::cluster_table(),
+                firewall_config: &self.config,
+                vmid: None,
+            };
+
+            commands.append(&mut ipset.to_nft_objects(&env)?);
+        }
+
+        Ok(())
+    }
+
+    pub fn full_host_fw(&self) -> Result<Commands, Error> {
+        let mut commands = Commands::default();
+
+        if !self.config.is_enabled() {
+            log::info!("firewall is disabled - doing nothing!");
+            return Ok(commands);
+        }
+
+        self.reset_firewall(&mut commands);
+
+        let cluster_host_table = Self::cluster_table();
+
+        if self.config.host().is_enabled() {
+            log::info!("creating cluster / host configuration");
+
+            self.create_management_ipset(&mut commands)?;
+
+            self.create_ipsets(
+                &mut commands,
+                self.config.cluster().ipsets(),
+                &cluster_host_table,
+                None,
+            )?;
+
+            for (name, group) in self.config.cluster().groups() {
+                self.create_group_chain(
+                    &mut commands,
+                    &cluster_host_table,
+                    group,
+                    name,
+                    Direction::In,
+                )?;
+                self.create_group_chain(
+                    &mut commands,
+                    &cluster_host_table,
+                    group,
+                    name,
+                    Direction::Out,
+                )?;
+            }
+
+            self.create_cluster_rules(&mut commands, Direction::In)?;
+            self.create_cluster_rules(&mut commands, Direction::Out)?;
+
+            log::debug!("Generating host firewall config");
+
+            self.setup_ct_helper(&mut commands)?;
+
+            self.handle_host_options(&mut commands)?;
+
+            self.create_host_rules(&mut commands, Direction::In)?;
+            self.create_host_rules(&mut commands, Direction::Out)?;
+        } else {
+            commands.push(Delete::table(TableName::from(Self::cluster_table())));
+        }
+
+        let guest_table = Self::guest_table();
+        let enabled_guests: BTreeMap<&Vmid, &GuestConfig> = self
+            .config
+            .guests()
+            .iter()
+            .filter(|(_, config)| config.is_enabled())
+            .collect();
+
+        if !enabled_guests.is_empty() {
+            log::info!("creating guest configuration");
+
+            self.create_ipsets(
+                &mut commands,
+                self.config.cluster().ipsets(),
+                &guest_table,
+                None,
+            )?;
+
+            for (name, group) in self.config.cluster().groups() {
+                self.create_group_chain(&mut commands, &guest_table, group, name, Direction::In)?;
+                self.create_group_chain(&mut commands, &guest_table, group, name, Direction::Out)?;
+            }
+        } else {
+            commands.push(Delete::table(TableName::from(Self::guest_table())));
+        }
+
+        for (vmid, config) in enabled_guests {
+            log::debug!("Generating firewall config for VM #{vmid}");
+
+            self.create_guest_chain(&mut commands, *vmid, Direction::In)?;
+            self.create_guest_chain(&mut commands, *vmid, Direction::Out)?;
+
+            self.create_ipsets(&mut commands, config.ipsets(), &guest_table, config)?;
+
+            self.handle_guest_options(&mut commands, *vmid, config)?;
+
+            self.create_guest_rules(&mut commands, *vmid, config, Direction::In)?;
+            self.create_guest_rules(&mut commands, *vmid, config, Direction::Out)?;
+        }
+
+        Ok(commands)
+    }
+
+    fn handle_host_options(&self, commands: &mut Commands) -> Result<(), Error> {
+        log::info!("setting host options");
+
+        let chain_in = Self::host_option_chain(Direction::In);
+        let chain_out = Self::host_option_chain(Direction::Out);
+
+        let ndp_chains = if self.config.host().allow_ndp() {
+            ("allow-ndp-in", "allow-ndp-out")
+        } else {
+            ("block-ndp-in", "block-ndp-out")
+        };
+
+        commands.append(&mut vec![
+            Add::rule(AddRule::from_statement(
+                chain_in.clone(),
+                Statement::jump(ndp_chains.0),
+            )),
+            Add::rule(AddRule::from_statement(
+                chain_out,
+                Statement::jump(ndp_chains.1),
+            )),
+        ]);
+
+        if self.config.host().block_synflood() {
+            log::debug!("set block_synflood");
+
+            let rate_limit = Statement::from(AnonymousLimit {
+                rate: self.config.host().synflood_rate(),
+                per: RateTimescale::Second,
+                burst: Some(self.config.host().synflood_burst()),
+                inv: Some(true),
+                ..Default::default()
+            });
+
+            let synflood_limit_chain = Self::synflood_limit_chain();
+
+            let v4_rule = AddRule::from_statements(
+                synflood_limit_chain.clone(),
+                [
+                    Statement::Set(Set {
+                        op: SetOperation::Update,
+                        elem: Expression::from(Payload::field("ip", "saddr")),
+                        stmt: Some(NfVec::one(rate_limit.clone())),
+                        set: "@v4-synflood-limit".to_string(),
+                    }),
+                    Statement::make_drop(),
+                ],
+            );
+
+            let v6_rule = AddRule::from_statements(
+                synflood_limit_chain,
+                [
+                    Statement::Set(Set {
+                        op: SetOperation::Update,
+                        elem: Expression::from(Payload::field("ip6", "saddr")),
+                        stmt: Some(NfVec::one(rate_limit)),
+                        set: "@v6-synflood-limit".to_string(),
+                    }),
+                    Statement::make_drop(),
+                ],
+            );
+
+            commands.append(&mut vec![
+                Add::rule(AddRule::from_statement(
+                    chain_in.clone(),
+                    Statement::jump("block-synflood"),
+                )),
+                Add::rule(v4_rule),
+                Add::rule(v6_rule),
+            ])
+        }
+
+        if self.config.host().block_invalid_tcp() {
+            log::debug!("set block_invalid_tcp");
+
+            commands.push(Add::rule(AddRule::from_statement(
+                chain_in.clone(),
+                Statement::jump("block-invalid-tcp"),
+            )));
+
+            self.create_log_rule(
+                commands,
+                self.config.host().block_invalid_tcp_log_level(),
+                Self::log_invalid_tcp_chain(),
+                ConfigVerdict::Drop,
+                None,
+            )?;
+        }
+
+        if self.config.host().block_smurfs() {
+            log::debug!("set block_smurfs");
+
+            commands.push(Add::rule(AddRule::from_statement(
+                chain_in.clone(),
+                Statement::jump("block-smurfs"),
+            )));
+
+            self.create_log_rule(
+                commands,
+                self.config.host().block_smurfs_log_level(),
+                Self::log_smurfs_chain(),
+                ConfigVerdict::Drop,
+                None,
+            )?;
+        }
+
+        if self.config.host().block_invalid_conntrack() {
+            log::debug!("set block_invalid_conntrack");
+
+            commands.push(Add::rule(AddRule::from_statement(
+                chain_in,
+                Statement::jump("block-conntrack-invalid"),
+            )));
+        }
+
+        if let Some(value) = self.config.host().nf_conntrack_max() {
+            log::debug!("set nf_conntrack_max");
+            fs::write(NF_CONNTRACK_MAX_FILE, value.to_string())
+                .unwrap_or_else(|_| log::warn!("cannot set nf_conntrack_max"));
+        }
+
+        if let Some(value) = self.config.host().nf_conntrack_tcp_timeout_established() {
+            log::debug!("set nf_conntrack_tcp_timeout_established");
+            fs::write(NF_CONNTRACK_TCP_TIMEOUT_ESTABLISHED, value.to_string())
+                .unwrap_or_else(|_| log::warn!("cannot set nf_conntrack_tcp_timeout_established"));
+        }
+
+        if let Some(value) = self.config.host().nf_conntrack_tcp_timeout_syn_recv() {
+            log::debug!("set nf_conntrack_tcp_timeout_syn_recv");
+            fs::write(NF_CONNTRACK_TCP_TIMEOUT_SYN_RECV, value.to_string())
+                .unwrap_or_else(|_| log::warn!("cannot set nf_conntrack_tcp_timeout_syn_recv"));
+        }
+
+        let value = (self.config.host().log_nf_conntrack() as u8).to_string();
+        fs::write(LOG_CONNTRACK_FILE, value)
+            .unwrap_or_else(|_| log::warn!("cannot set conntrack_log_file"));
+
+        /*
+        CliCommand::new("systemctl")
+            .args(["try-reload-or-restart", "pvefw-logger.service"])
+            .output()
+            .map_err(anyhow::Error::msg)?;
+        */
+
+        Ok(())
+    }
+
+    fn handle_guest_options(
+        &self,
+        commands: &mut Commands,
+        vmid: Vmid,
+        config: &GuestConfig,
+    ) -> Result<(), Error> {
+        let chain_in = Self::guest_chain(Direction::In, vmid);
+        let chain_out = Self::guest_chain(Direction::Out, vmid);
+
+        if config.macfilter() {
+            log::debug!("setting macfilter for guest #{vmid}");
+            let mac_address_set: Vec<Expression> = config
+                .network_config()
+                .network_devices()
+                .iter()
+                .map(|(index, device)| {
+                    Expression::concat([
+                        Expression::from(config.iface_name_by_index(*index)),
+                        Expression::from(device.mac_address().to_string()),
+                    ])
+                })
+                .collect();
+
+            if !mac_address_set.is_empty() {
+                let macfilter_rule = AddRule::from_statements(
+                    chain_out.clone(),
+                    [
+                        Match::new_ne(
+                            Expression::concat([
+                                Expression::from(Meta::new("iifname")),
+                                Expression::from(Payload::field("ether", "saddr")),
+                            ]),
+                            Expression::set(mac_address_set.clone()),
+                        )
+                        .into(),
+                        Statement::make_drop(),
+                    ],
+                );
+
+                let macfilter_arp_rule = AddRule::from_statements(
+                    chain_out.clone(),
+                    [
+                        Match::new_ne(
+                            Expression::concat([
+                                Expression::from(Meta::new("iifname")),
+                                Expression::from(Payload::field("arp", "saddr ether")),
+                            ]),
+                            Expression::set(mac_address_set),
+                        )
+                        .into(),
+                        Statement::make_drop(),
+                    ],
+                );
+
+                commands.push(Add::rule(macfilter_rule));
+                commands.push(Add::rule(macfilter_arp_rule));
+            }
+        }
+
+        let dhcp_chains = if config.allow_dhcp() {
+            ("allow-dhcp-in", "allow-dhcp-out")
+        } else {
+            ("block-dhcp-in", "block-dhcp-out")
+        };
+
+        commands.append(&mut vec![
+            Add::rule(AddRule::from_statement(
+                chain_in.clone(),
+                Statement::jump(dhcp_chains.0),
+            )),
+            Add::rule(AddRule::from_statement(
+                chain_out.clone(),
+                Statement::jump(dhcp_chains.1),
+            )),
+        ]);
+
+        let ndp_chains = if config.allow_ndp() {
+            ("allow-ndp-in", "allow-ndp-out")
+        } else {
+            ("block-ndp-in", "block-ndp-out")
+        };
+
+        commands.append(&mut vec![
+            Add::rule(AddRule::from_statement(
+                chain_in.clone(),
+                Statement::jump(ndp_chains.0),
+            )),
+            Add::rule(AddRule::from_statement(
+                chain_out.clone(),
+                Statement::jump(ndp_chains.1),
+            )),
+        ]);
+
+        let ra_chain_out = if config.allow_ra() {
+            "allow-ra-out"
+        } else {
+            "block-ra-out"
+        };
+
+        commands.push(Add::rule(AddRule::from_statement(
+            chain_out,
+            Statement::jump(ra_chain_out),
+        )));
+
+        // we allow incoming ARP by default, except if blocked by any option above
+        let arp_rule = vec![
+            Match::new_eq(Payload::field("ether", "type"), Expression::from("arp")).into(),
+            Statement::make_accept(),
+        ];
+
+        commands.push(Add::rule(AddRule::from_statements(chain_in, arp_rule)));
+
+        Ok(())
+    }
+
+    fn setup_ct_helper(&self, commands: &mut Commands) -> Result<(), Error> {
+        let chain_in = Self::host_conntrack_chain();
+
+        if let Some(helpers) = self.config.host().conntrack_helpers() {
+            let object_env = NftObjectEnv {
+                table: chain_in.table(),
+                firewall_config: &self.config,
+                vmid: None,
+            };
+
+            let rule_env = NftRuleEnv {
+                chain: chain_in.clone(),
+                direction: Direction::In,
+                firewall_config: &self.config,
+                vmid: None,
+            };
+
+            for helper in helpers {
+                log::debug!("adding conntrack helper: {helper:?}");
+
+                let helper_macro = get_cthelper(&helper.to_string());
+
+                if let Some(helper_macro) = helper_macro {
+                    commands.append(&mut helper_macro.to_nft_objects(&object_env)?);
+
+                    // todo: use vmap
+                    for rule in NftRule::from_ct_helper(helper_macro, &rule_env)? {
+                        commands.push(Add::rule(rule.into_add_rule(chain_in.clone())));
+                    }
+                } else {
+                    log::warn!("provided invalid helper macro name: {:?}", helper);
+                }
+            }
+        }
+
+        Ok(())
+    }
+
+    fn create_ipfilter_rules(
+        &self,
+        commands: &mut Commands,
+        vmid: Vmid,
+        ipfilter: &Ipfilter,
+    ) -> Result<(), Error> {
+        for direction in [Direction::In, Direction::Out] {
+            let chain = Self::guest_chain(direction, vmid);
+
+            let rule_env = NftRuleEnv {
+                chain: chain.clone(),
+                direction,
+                firewall_config: &self.config,
+                vmid: Some(vmid),
+            };
+
+            for rule in NftRule::from_ipfilter(ipfilter, &rule_env)? {
+                commands.push(Add::rule(rule.into_add_rule(chain.clone())));
+            }
+        }
+
+        Ok(())
+    }
+
+    fn create_ipsets<'a>(
+        &self,
+        commands: &mut Commands,
+        ipsets: &BTreeMap<String, Ipset>,
+        table: &TablePart,
+        guest_config: impl Into<Option<&'a GuestConfig>>,
+    ) -> Result<(), Error> {
+        let config = guest_config.into();
+        let vmid = config.map(|cfg| cfg.vmid());
+
+        let env = NftObjectEnv {
+            table,
+            vmid,
+            firewall_config: &self.config,
+        };
+
+        for (name, ipset) in ipsets {
+            if ipset.ipfilter().is_some() {
+                continue;
+            }
+
+            log::info!("creating ipset {name} in table {}", table.table());
+            commands.append(&mut ipset.to_nft_objects(&env)?);
+        }
+
+        if let (Some(cfg), Some(vmid)) = (config, vmid) {
+            let network_devices = cfg.network_config().network_devices();
+
+            for (index, network_device) in network_devices {
+                let ipfilter_name = Ipfilter::name_for_index(*index);
+
+                if let Some(ipset) = ipsets.get(&ipfilter_name) {
+                    log::debug!("creating ipfilter for guest #{vmid} net{index}");
+
+                    commands.append(&mut ipset.to_nft_objects(&env)?);
+                    // safe due to constructing the name above
+                    let ipfilter = ipset.ipfilter().expect("is an ip filter");
+                    self.create_ipfilter_rules(commands, vmid, &ipfilter)?;
+                } else if cfg.ipfilter() {
+                    log::debug!("generating default ipfilter for guest #{vmid} net{index}");
+                    let ipset_name = IpsetName::new(IpsetScope::Guest, ipfilter_name);
+                    let mut ipset = Ipset::new(ipset_name);
+
+                    let cidr =
+                        Ipv6Cidr::from(network_device.mac_address().eui64_link_local_address());
+
+                    ipset.push(cidr.into());
+
+                    if let Some(ip_address) = network_device.ip() {
+                        ipset.push(IpsetEntry::from(*ip_address));
+                    }
+
+                    if let Some(ip6_address) = network_device.ip6() {
+                        ipset.push(IpsetEntry::from(*ip6_address));
+                    }
+
+                    commands.append(&mut ipset.to_nft_objects(&env)?);
+                    // safe due to constructing the name above
+                    let ipfilter = ipset.ipfilter().expect("is an ip filter");
+                    self.create_ipfilter_rules(commands, vmid, &ipfilter)?;
+                };
+            }
+        }
+
+        Ok(())
+    }
+
+    fn create_cluster_rules(
+        &self,
+        commands: &mut Commands,
+        direction: Direction,
+    ) -> Result<(), Error> {
+        log::info!("creating cluster chain {direction}");
+
+        let chain = Self::cluster_chain(direction);
+
+        let env = NftRuleEnv {
+            chain: chain.clone(),
+            direction,
+            firewall_config: &self.config,
+            vmid: None,
+        };
+
+        let rules = self.config.cluster().rules();
+
+        commands.reserve(rules.len());
+
+        for config_rule in rules {
+            for rule in NftRule::from_config_rule(config_rule, &env)? {
+                commands.push(Add::rule(rule.into_add_rule(chain.clone())));
+            }
+        }
+
+        let default_policy = self.config.cluster().default_policy(direction);
+
+        self.create_log_rule(
+            commands,
+            self.config.host().log_level(direction),
+            chain.clone(),
+            default_policy,
+            None,
+        )?;
+
+        commands.push(Add::rule(AddRule::from_statement(chain, default_policy)));
+
+        Ok(())
+    }
+
+    fn create_host_rules(
+        &self,
+        commands: &mut Commands,
+        direction: Direction,
+    ) -> Result<(), Error> {
+        log::info!("creating host chain {direction}");
+
+        let chain = Self::host_chain(direction);
+
+        let env = NftRuleEnv {
+            chain: chain.clone(),
+            direction,
+            firewall_config: &self.config,
+            vmid: None,
+        };
+
+        let rules = self.config.host().rules();
+        commands.reserve(rules.len());
+
+        for config_rule in rules {
+            for rule in NftRule::from_config_rule(config_rule, &env)? {
+                commands.push(Add::rule(rule.into_add_rule(chain.clone())));
+            }
+        }
+
+        Ok(())
+    }
+
+    fn create_guest_chain(
+        &self,
+        commands: &mut Commands,
+        vmid: Vmid,
+        direction: Direction,
+    ) -> Result<(), Error> {
+        log::info!("creating guest chain #{vmid} {direction}");
+
+        let chain = Self::guest_chain(direction, vmid);
+
+        commands.append(&mut vec![Add::chain(chain.clone()), Flush::chain(chain)]);
+
+        Ok(())
+    }
+
+    fn create_guest_rules(
+        &self,
+        commands: &mut Commands,
+        vmid: Vmid,
+        config: &GuestConfig,
+        direction: Direction,
+    ) -> Result<(), Error> {
+        log::info!("creating guest rules #{vmid} {direction}");
+
+        let chain = Self::guest_chain(direction, vmid);
+
+        let env = NftRuleEnv {
+            chain: chain.clone(),
+            direction,
+            firewall_config: &self.config,
+            vmid: Some(vmid),
+        };
+
+        commands.reserve(config.rules().len());
+
+        for config_rule in config.rules() {
+            for rule in NftRule::from_config_rule(config_rule, &env)? {
+                commands.push(Add::rule(rule.into_add_rule(chain.clone())))
+            }
+        }
+
+        let network_devices = config.network_config().network_devices();
+
+        if !network_devices.is_empty() {
+            let map_elements = network_devices
+                .iter()
+                .filter(|(_, device)| device.has_firewall())
+                .map(|(index, _)| {
+                    (
+                        Expression::from(config.iface_name_by_index(*index)),
+                        MapValue::from(Verdict::Goto {
+                            target: chain.name().to_string(),
+                        }),
+                    )
+                });
+
+            commands.push(Add::element(AddElement::map_from_expressions(
+                Self::guest_vmap(direction),
+                map_elements,
+            )));
+        }
+
+        if direction == Direction::In {
+            commands.push(Add::rule(AddRule::from_statement(
+                chain.clone(),
+                Statement::jump("after-vm-in"),
+            )));
+        }
+
+        self.create_log_rule(
+            commands,
+            config.log_level(direction),
+            chain.clone(),
+            config.default_policy(direction),
+            vmid,
+        )?;
+
+        commands.push(Add::rule(AddRule::from_statement(
+            chain,
+            config.default_policy(direction),
+        )));
+
+        Ok(())
+    }
+
+    fn create_group_chain(
+        &self,
+        commands: &mut Commands,
+        table: &TablePart,
+        group: &Group,
+        name: &str,
+        direction: Direction,
+    ) -> Result<(), Error> {
+        log::info!(
+            "creating group chain {name} in table {} {direction}",
+            table.table()
+        );
+
+        let chain = Self::group_chain(table.clone(), name, direction);
+
+        let env = NftRuleEnv {
+            chain: chain.clone(),
+            direction,
+            firewall_config: &self.config,
+            vmid: None,
+        };
+
+        commands.append(&mut vec![
+            Add::chain(chain.clone()),
+            Flush::chain(chain.clone()),
+        ]);
+
+        for rule in group.rules() {
+            for firewall_rule in NftRule::from_config_rule(rule, &env)? {
+                commands.push(Add::rule(firewall_rule.into_add_rule(chain.clone())))
+            }
+        }
+
+        Ok(())
+    }
+
+    fn create_log_rule(
+        &self,
+        commands: &mut Commands,
+        log_level: ConfigLogLevel,
+        chain: ChainPart,
+        verdict: ConfigVerdict,
+        vmid: impl Into<Option<Vmid>>,
+    ) -> Result<(), Error> {
+        if let Ok(log_level) = LogLevel::try_from(log_level) {
+            let mut log_rule = AddRule::new(chain.clone());
+
+            if let Some(limit) = self.default_log_limit() {
+                log_rule.push(Statement::from(limit));
+            }
+
+            let log_statement = Log::new_nflog(
+                Log::generate_prefix(vmid, log_level, chain.name(), verdict),
+                0,
+            );
+
+            log_rule.push(Statement::from(log_statement));
+
+            commands.push(Add::rule(log_rule));
+        }
+
+        Ok(())
+    }
+}
diff --git a/proxmox-firewall/src/main.rs b/proxmox-firewall/src/main.rs
index a4979a7..53c1289 100644
--- a/proxmox-firewall/src/main.rs
+++ b/proxmox-firewall/src/main.rs
@@ -1,6 +1,7 @@
 use anyhow::Error;
 
 mod config;
+mod firewall;
 mod object;
 mod rule;
 
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 32/39] firewall: add proxmox-firewall binary and move existing code into lib
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (30 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 31/39] firewall: add ruleset " Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 33/39] firewall: add files for debian packaging Stefan Hanreich
                   ` (7 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel; +Cc: Wolfgang Bumiller

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Co-authored-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-firewall/src/bin/proxmox-firewall.rs | 96 ++++++++++++++++++++
 proxmox-firewall/src/lib.rs                  |  4 +
 proxmox-firewall/src/main.rs                 | 11 ---
 3 files changed, 100 insertions(+), 11 deletions(-)
 create mode 100644 proxmox-firewall/src/bin/proxmox-firewall.rs
 create mode 100644 proxmox-firewall/src/lib.rs
 delete mode 100644 proxmox-firewall/src/main.rs

diff --git a/proxmox-firewall/src/bin/proxmox-firewall.rs b/proxmox-firewall/src/bin/proxmox-firewall.rs
new file mode 100644
index 0000000..2f4875f
--- /dev/null
+++ b/proxmox-firewall/src/bin/proxmox-firewall.rs
@@ -0,0 +1,96 @@
+use std::io::Write;
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::Arc;
+use std::time::{Duration, Instant};
+
+use anyhow::{Context, Error};
+
+use proxmox_firewall::firewall::Firewall;
+use proxmox_nftables::{client::NftError, NftClient};
+
+const RULE_BASE: &str = include_str!("../../resources/proxmox-firewall.nft");
+
+fn remove_firewall() -> Result<(), std::io::Error> {
+    log::info!("removing existing firewall rules");
+    let commands = Firewall::remove_commands();
+
+    // can ignore other errors, since it fails when tables do not exist
+    if let Err(NftError::Io(err)) = NftClient::run_json_commands(&commands) {
+        return Err(err);
+    }
+
+    Ok(())
+}
+
+fn handle_firewall() -> Result<(), Error> {
+    let firewall = Firewall::new();
+
+    if !firewall.is_enabled() {
+        return remove_firewall().with_context(|| "could not remove firewall tables".to_string());
+    }
+
+    log::info!("creating the firewall skeleton");
+    NftClient::run_commands(RULE_BASE)?;
+
+    let commands = firewall.full_host_fw()?;
+
+    log::info!("Running proxmox-firewall commands");
+    for (idx, c) in commands.iter().enumerate() {
+        log::debug!("cmd #{idx} {}", serde_json::to_string(&c)?);
+    }
+
+    let response = NftClient::run_json_commands(&commands)?;
+
+    if let Some(output) = response {
+        log::debug!("got response from nftables: {output:?}");
+    }
+
+    Ok(())
+}
+
+fn init_logger() {
+    match std::env::var("RUST_LOG_STYLE") {
+        Ok(s) if s == "SYSTEMD" => env_logger::builder()
+            .format(|buf, record| {
+                writeln!(
+                    buf,
+                    "<{}>{}: {}",
+                    match record.level() {
+                        log::Level::Error => 3,
+                        log::Level::Warn => 4,
+                        log::Level::Info => 6,
+                        log::Level::Debug => 7,
+                        log::Level::Trace => 7,
+                    },
+                    record.target(),
+                    record.args()
+                )
+            })
+            .init(),
+        _ => env_logger::init(),
+    };
+}
+
+fn main() -> Result<(), std::io::Error> {
+    init_logger();
+
+    let term = Arc::new(AtomicBool::new(false));
+
+    signal_hook::flag::register(signal_hook::consts::SIGTERM, Arc::clone(&term))?;
+    signal_hook::flag::register(signal_hook::consts::SIGINT, Arc::clone(&term))?;
+
+    while !term.load(Ordering::Relaxed) {
+        let start = Instant::now();
+
+        if let Err(error) = handle_firewall() {
+            log::error!("error creating firewall rules: {error}");
+        }
+
+        let duration = start.elapsed();
+        log::info!("firewall update time: {}ms", duration.as_millis());
+
+        std::thread::sleep(Duration::from_secs(5));
+    }
+
+    remove_firewall()
+}
diff --git a/proxmox-firewall/src/lib.rs b/proxmox-firewall/src/lib.rs
new file mode 100644
index 0000000..c4b037a
--- /dev/null
+++ b/proxmox-firewall/src/lib.rs
@@ -0,0 +1,4 @@
+pub mod config;
+pub mod firewall;
+pub mod object;
+pub mod rule;
diff --git a/proxmox-firewall/src/main.rs b/proxmox-firewall/src/main.rs
deleted file mode 100644
index 53c1289..0000000
--- a/proxmox-firewall/src/main.rs
+++ /dev/null
@@ -1,11 +0,0 @@
-use anyhow::Error;
-
-mod config;
-mod firewall;
-mod object;
-mod rule;
-
-fn main() -> Result<(), Error> {
-    env_logger::init();
-    Ok(())
-}
-- 
2.39.2


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


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

* [pve-devel] [PATCH proxmox-firewall v3 33/39] firewall: add files for debian packaging
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (31 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 32/39] firewall: add proxmox-firewall binary and move existing code into lib Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 34/39] firewall: add integration test Stefan Hanreich
                   ` (6 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel

Suggested-By: Fabian Grünbichler <f.gruenbichler@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 .gitignore                      |  3 ++
 Makefile                        | 70 +++++++++++++++++++++++++++++++++
 debian/changelog                |  5 +++
 debian/control                  | 39 ++++++++++++++++++
 debian/copyright                | 16 ++++++++
 debian/proxmox-firewall.install |  1 +
 debian/proxmox-firewall.service | 14 +++++++
 debian/rules                    | 31 +++++++++++++++
 debian/source/format            |  1 +
 defines.mk                      | 13 ++++++
 10 files changed, 193 insertions(+)
 create mode 100644 Makefile
 create mode 100644 debian/changelog
 create mode 100644 debian/control
 create mode 100644 debian/copyright
 create mode 100644 debian/proxmox-firewall.install
 create mode 100644 debian/proxmox-firewall.service
 create mode 100755 debian/rules
 create mode 100644 debian/source/format
 create mode 100644 defines.mk

diff --git a/.gitignore b/.gitignore
index 3cb8114..90749ee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,8 @@
 /Cargo.lock
 proxmox-firewall-*/
 *.deb
+*.dsc
+*.tar*
+*.build
 *.buildinfo
 *.changes
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..c235b93
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,70 @@
+include /usr/share/dpkg/pkg-info.mk
+include /usr/share/dpkg/architecture.mk
+include defines.mk
+
+PACKAGE=proxmox-firewall
+BUILDDIR ?= $(PACKAGE)-$(DEB_VERSION_UPSTREAM)
+CARGO ?= cargo
+
+DEB=$(PACKAGE)_$(DEB_VERSION_UPSTREAM_REVISION)_$(DEB_HOST_ARCH).deb
+DBG_DEB=$(PACKAGE)-dbgsym_$(DEB_VERSION_UPSTREAM_REVISION)_$(DEB_HOST_ARCH).deb
+DSC=rust-$(PACKAGE)_$(DEB_VERSION_UPSTREAM_REVISION).dsc
+
+DEBS = $(DEB) $(DBG_DEB)
+
+ifeq ($(BUILD_MODE), release)
+CARGO_BUILD_ARGS += --release
+COMPILEDIR := target/release
+else
+COMPILEDIR := target/debug
+endif
+
+
+all: cargo-build
+
+.PHONY: cargo-build
+cargo-build:
+	$(CARGO) build $(CARGO_BUILD_ARGS)
+
+.PHONY: build
+build: $(BUILDDIR)
+$(BUILDDIR):
+	rm -rf $@ $@.tmp; mkdir $@.tmp
+	cp -a proxmox-firewall proxmox-nftables proxmox-ve-config debian Cargo.toml Makefile defines.mk $@.tmp/
+	mv $@.tmp $@
+
+.PHONY: deb
+deb: $(DEB)
+$(HELPER_DEB) $(DBG_DEB) $(HELPER_DBG_DEB) $(DOC_DEB): $(DEB)
+$(DEB): $(BUILDDIR)
+	cd $(BUILDDIR); dpkg-buildpackage -b -us -uc --no-pre-clean
+	lintian $(DEB) $(DOC_DEB) $(HELPER_DEB)
+
+.PHONY: test
+test:
+	$(CARGO) test
+
+.PHONY: dsc
+dsc:
+	rm -rf $(BUILDDIR) $(DSC)
+	$(MAKE) $(DSC)
+	lintian $(DSC)
+$(DSC): $(BUILDDIR)
+	cd $(BUILDDIR); dpkg-buildpackage -S -us -uc -d -nc
+
+sbuild: $(DSC)
+	sbuild $<
+
+.PHONY: dinstall
+dinstall: $(DEB)
+	dpkg -i $(DEB) $(DBG_DEB) $(DOC_DEB)
+
+.PHONY: distclean
+distclean: clean
+
+.PHONY: clean
+clean:
+	$(CARGO) clean
+	rm -f *.deb *.build *.buildinfo *.changes *.dsc rust-$(PACKAGE)*.tar*
+	rm -rf $(PACKAGE)-[0-9]*/
+	find . -name '*~' -exec rm {} ';'
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..3ca5833
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,5 @@
+rust-proxmox-firewall (0.1) UNRELEASED; urgency=medium
+
+  * Initial release.
+
+ -- Stefan Hanreich <s.hanreich@proxmox.com>  Thu, 07 Mar 2024 10:15:10 +0100
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..97f9e89
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,39 @@
+Source: rust-proxmox-firewall
+Section: admin
+Priority: optional
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Build-Depends: cargo:native,
+               debhelper-compat (= 13),
+               librust-anyhow-1+default-dev,
+               librust-env-logger-0.10+default-dev,
+               librust-log-0.4+default-dev (>= 0.4.17-~~),
+               librust-nix-0.26+default-dev (>= 0.26.1-~~),
+               librust-proxmox-sys-dev,
+               librust-proxmox-sortable-macro-dev,
+               librust-serde-1+default-dev,
+               librust-serde-1+derive-dev,
+               librust-serde-json-1+default-dev,
+               librust-serde-plain-1+default-dev,
+               librust-serde-plain-1+default-dev,
+               librust-serde-with+default-dev,
+               librust-signal-hook-dev,
+               librust-thiserror-dev,
+               librust-libc-0.2+default-dev,
+               librust-proxmox-schema-3+default-dev,
+               libstd-rust-dev,
+               netbase,
+               python3,
+               rustc:native,
+Standards-Version: 4.6.2
+Homepage: https://www.proxmox.com
+
+Package: proxmox-firewall
+Architecture: any
+Conflicts: ulogd,
+Depends: ${misc:Depends}, ${shlibs:Depends},
+         pve-firewall,
+         nftables,
+         netbase,
+Description: Proxmox nftables firewall
+ This package contains a nftables-based implementation of the Proxmox VE
+ Firewall
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..fe09a1b
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,16 @@
+Copyright (C) 2018-2024 Proxmox Server Solutions GmbH
+
+This software is written by Proxmox Server Solutions GmbH <support@proxmox.com>
+
+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 <http://www.gnu.org/licenses/>.
diff --git a/debian/proxmox-firewall.install b/debian/proxmox-firewall.install
new file mode 100644
index 0000000..8eb376c
--- /dev/null
+++ b/debian/proxmox-firewall.install
@@ -0,0 +1 @@
+target/x86_64-unknown-linux-gnu/release/proxmox-firewall usr/libexec/proxmox
diff --git a/debian/proxmox-firewall.service b/debian/proxmox-firewall.service
new file mode 100644
index 0000000..ad2324b
--- /dev/null
+++ b/debian/proxmox-firewall.service
@@ -0,0 +1,14 @@
+[Unit]
+Description=Proxmox nftables firewall
+Wants=pve-cluster.service pvefw-logger.service
+After=pvefw-logger.service pve-cluster.service network.target systemd-modules-load.service
+
+[Service]
+ExecStart=/usr/libexec/proxmox/proxmox-firewall
+PIDFile=/run/proxmox-firewall.pid
+Environment="RUST_LOG_STYLE=SYSTEMD"
+Environment="RUST_LOG=warn"
+
+[Install]
+WantedBy=multi-user.target
+
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..1c1af5a
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,31 @@
+#!/usr/bin/make -f
+
+# Uncomment this to turn on verbose mode.
+export DH_VERBOSE=1
+
+include /usr/share/dpkg/pkg-info.mk
+include /usr/share/rustc/architecture.mk
+
+export BUILD_MODE=release
+
+export CFLAGS CXXFLAGS CPPFLAGS LDFLAGS
+export DEB_HOST_RUST_TYPE DEB_HOST_GNU_TYPE
+
+export CARGO=/usr/share/cargo/bin/cargo
+export CARGO_HOME = $(CURDIR)/debian/cargo_home
+
+export DEB_CARGO_CRATE=proxmox-firewall$(DEB_VERSION_UPSTREAM)
+export DEB_CARGO_PACKAGE=proxmox-firewall
+
+%:
+	dh $@
+
+override_dh_auto_configure:
+	@perl -ne 'if (/^version\s*=\s*"(\d+(?:\.\d+)+)"/) { my $$v_cargo = $$1; my $$v_deb = "$(DEB_VERSION_UPSTREAM)"; \
+	    die "ERROR: d/changelog <-> Cargo.toml version mismatch: $$v_cargo != $$v_deb\n" if $$v_cargo ne $$v_deb; exit(0); }' Cargo.toml
+	$(CARGO) prepare-debian $(CURDIR)/debian/cargo_registry --link-from-system
+	dh_auto_configure
+
+override_dh_installsystemd:
+	dh_installsystemd proxmox-firewall.service
+
diff --git a/debian/source/format b/debian/source/format
new file mode 100644
index 0000000..89ae9db
--- /dev/null
+++ b/debian/source/format
@@ -0,0 +1 @@
+3.0 (native)
diff --git a/defines.mk b/defines.mk
new file mode 100644
index 0000000..e01164d
--- /dev/null
+++ b/defines.mk
@@ -0,0 +1,13 @@
+PREFIX = /usr
+BINDIR = $(PREFIX)/bin
+SBINDIR = $(PREFIX)/sbin
+LIBDIR = $(PREFIX)/lib
+LIBEXECDIR = $(LIBDIR)
+DATAROOTDIR = $(PREFIX)/share
+MAN1DIR = $(PREFIX)/share/man/man1
+MAN5DIR = $(PREFIX)/share/man/man5
+SYSCONFDIR = /etc
+
+# For local overrides
+-include local.mak
+
-- 
2.39.2


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

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

* [pve-devel] [PATCH proxmox-firewall v3 34/39] firewall: add integration test
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (32 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 33/39] firewall: add files for debian packaging Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH qemu-server v3 35/39] firewall: add handling for new nft firewall Stefan Hanreich
                   ` (5 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 .gitignore                                    |    1 +
 debian/control                                |    1 +
 proxmox-firewall/Cargo.toml                   |    4 +
 proxmox-firewall/tests/input/100.conf         |   10 +
 proxmox-firewall/tests/input/100.fw           |   22 +
 proxmox-firewall/tests/input/101.conf         |   11 +
 proxmox-firewall/tests/input/101.fw           |   19 +
 proxmox-firewall/tests/input/chains.json      |  427 ++
 proxmox-firewall/tests/input/cluster.fw       |   26 +
 proxmox-firewall/tests/input/host.fw          |   23 +
 proxmox-firewall/tests/integration_tests.rs   |   90 +
 .../integration_tests__firewall.snap          | 3530 +++++++++++++++++
 12 files changed, 4164 insertions(+)
 create mode 100644 proxmox-firewall/tests/input/100.conf
 create mode 100644 proxmox-firewall/tests/input/100.fw
 create mode 100644 proxmox-firewall/tests/input/101.conf
 create mode 100644 proxmox-firewall/tests/input/101.fw
 create mode 100644 proxmox-firewall/tests/input/chains.json
 create mode 100644 proxmox-firewall/tests/input/cluster.fw
 create mode 100644 proxmox-firewall/tests/input/host.fw
 create mode 100644 proxmox-firewall/tests/integration_tests.rs
 create mode 100644 proxmox-firewall/tests/snapshots/integration_tests__firewall.snap

diff --git a/.gitignore b/.gitignore
index 90749ee..c5474ef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ proxmox-firewall-*/
 *.build
 *.buildinfo
 *.changes
+*.snap.new
diff --git a/debian/control b/debian/control
index 97f9e89..845b84d 100644
--- a/debian/control
+++ b/debian/control
@@ -20,6 +20,7 @@ Build-Depends: cargo:native,
                librust-thiserror-dev,
                librust-libc-0.2+default-dev,
                librust-proxmox-schema-3+default-dev,
+               librust-insta-dev,
                libstd-rust-dev,
                netbase,
                python3,
diff --git a/proxmox-firewall/Cargo.toml b/proxmox-firewall/Cargo.toml
index bec7552..163ab17 100644
--- a/proxmox-firewall/Cargo.toml
+++ b/proxmox-firewall/Cargo.toml
@@ -22,3 +22,7 @@ signal-hook = "0.3"
 
 proxmox-nftables = { path = "../proxmox-nftables", features = ["config-ext"] }
 proxmox-ve-config = { path = "../proxmox-ve-config" }
+
+[dev-dependencies]
+insta = { version = "1.21", features = ["json"] }
+proxmox-sys = "0.5.3"
diff --git a/proxmox-firewall/tests/input/100.conf b/proxmox-firewall/tests/input/100.conf
new file mode 100644
index 0000000..495f899
--- /dev/null
+++ b/proxmox-firewall/tests/input/100.conf
@@ -0,0 +1,10 @@
+arch: amd64
+cores: 1
+features: nesting=1
+hostname: host1
+memory: 512
+net1: name=eth0,bridge=simple1,firewall=1,hwaddr=BC:24:11:4D:B0:FF,ip=dhcp,ip6=fd80::1234/64,type=veth
+ostype: debian
+rootfs: local-lvm:vm-90001-disk-0,size=2G
+swap: 512
+unprivileged: 1
diff --git a/proxmox-firewall/tests/input/100.fw b/proxmox-firewall/tests/input/100.fw
new file mode 100644
index 0000000..6cf9fff
--- /dev/null
+++ b/proxmox-firewall/tests/input/100.fw
@@ -0,0 +1,22 @@
+[OPTIONS]
+
+enable: 1
+ndp: 1
+ipfilter: 1
+dhcp: 1
+log_level_in: crit
+log_level_out: alert
+policy_in: DROP
+policy_out: REJECT
+macfilter: 0
+
+[IPSET ipfilter-net1]
+
+dc/network1
+
+[RULES]
+
+GROUP network1 -i net1
+IN ACCEPT -source 192.168.0.1/24,127.0.0.1-127.255.255.0,172.16.0.1 -dport 123,222:333 -sport http -p tcp
+IN DROP --icmp-type echo-request --proto icmp --log info
+
diff --git a/proxmox-firewall/tests/input/101.conf b/proxmox-firewall/tests/input/101.conf
new file mode 100644
index 0000000..394e2e4
--- /dev/null
+++ b/proxmox-firewall/tests/input/101.conf
@@ -0,0 +1,11 @@
+boot: order=ide2
+cores: 2
+cpu: x86-64-v2-AES
+memory: 2048
+meta: creation-qemu=8.1.5,ctime=1712322773
+numa: 0
+ostype: l26
+scsihw: virtio-scsi-single
+smbios1: uuid=78ec7794-78f7-4c03-bf08-18b0000721a6
+sockets: 1
+vmgenid: ec7d4834-cd0a-4376-9c1d-af8a82da8d54
diff --git a/proxmox-firewall/tests/input/101.fw b/proxmox-firewall/tests/input/101.fw
new file mode 100644
index 0000000..c77cb5a
--- /dev/null
+++ b/proxmox-firewall/tests/input/101.fw
@@ -0,0 +1,19 @@
+[OPTIONS]
+
+ndp: 0
+enable: 1
+dhcp: 1
+radv: 0
+policy_out: ACCEPT
+
+[ALIASES]
+
+analias 123.123.123.123
+
+[IPSET testing]
+
+
+[RULES]
+
+IN ACCEPT -source guest/analias -dest dc/network2 -log nolog
+
diff --git a/proxmox-firewall/tests/input/chains.json b/proxmox-firewall/tests/input/chains.json
new file mode 100644
index 0000000..aabfc6e
--- /dev/null
+++ b/proxmox-firewall/tests/input/chains.json
@@ -0,0 +1,427 @@
+{
+  "nftables": [
+    {
+      "metainfo": {
+        "version": "1.0.6",
+        "release_name": "Lester Gooch #5",
+        "json_schema_version": 1
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "proxmox-firewall",
+        "name": "do-reject",
+        "handle": 1
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "proxmox-firewall",
+        "name": "accept-management",
+        "handle": 2
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "proxmox-firewall",
+        "name": "block-synflood",
+        "handle": 3
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "proxmox-firewall",
+        "name": "log-drop-invalid-tcp",
+        "handle": 4
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "proxmox-firewall",
+        "name": "block-invalid-tcp",
+        "handle": 5
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "proxmox-firewall",
+        "name": "allow-ndp-in",
+        "handle": 6
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "proxmox-firewall",
+        "name": "block-ndp-in",
+        "handle": 7
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "proxmox-firewall",
+        "name": "allow-ndp-out",
+        "handle": 8
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "proxmox-firewall",
+        "name": "block-ndp-out",
+        "handle": 9
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "proxmox-firewall",
+        "name": "block-conntrack-invalid",
+        "handle": 10
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "proxmox-firewall",
+        "name": "block-smurfs",
+        "handle": 11
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "proxmox-firewall",
+        "name": "log-drop-smurfs",
+        "handle": 12
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "proxmox-firewall",
+        "name": "default-in",
+        "handle": 13
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "proxmox-firewall",
+        "name": "default-out",
+        "handle": 14
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "proxmox-firewall",
+        "name": "input",
+        "handle": 15,
+        "type": "filter",
+        "hook": "input",
+        "prio": 0,
+        "policy": "accept"
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "proxmox-firewall",
+        "name": "output",
+        "handle": 16,
+        "type": "filter",
+        "hook": "output",
+        "prio": 0,
+        "policy": "accept"
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "proxmox-firewall",
+        "name": "ratelimit-synflood",
+        "handle": 17
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "proxmox-firewall",
+        "name": "log-invalid-tcp",
+        "handle": 18
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "proxmox-firewall",
+        "name": "log-smurfs",
+        "handle": 19
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "proxmox-firewall",
+        "name": "option-in",
+        "handle": 20
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "proxmox-firewall",
+        "name": "option-out",
+        "handle": 21
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "proxmox-firewall",
+        "name": "cluster-in",
+        "handle": 22
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "proxmox-firewall",
+        "name": "cluster-out",
+        "handle": 23
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "proxmox-firewall",
+        "name": "host-in",
+        "handle": 24
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "proxmox-firewall",
+        "name": "host-out",
+        "handle": 25
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "proxmox-firewall",
+        "name": "ct-in",
+        "handle": 26
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "proxmox-firewall",
+        "name": "group-network1-in",
+        "handle": 95
+      }
+    },
+    {
+      "chain": {
+        "family": "inet",
+        "table": "proxmox-firewall",
+        "name": "group-network1-out",
+        "handle": 97
+      }
+    },
+    {
+      "chain": {
+        "family": "bridge",
+        "table": "proxmox-firewall-guests",
+        "name": "allow-dhcp-in",
+        "handle": 1
+      }
+    },
+    {
+      "chain": {
+        "family": "bridge",
+        "table": "proxmox-firewall-guests",
+        "name": "allow-dhcp-out",
+        "handle": 2
+      }
+    },
+    {
+      "chain": {
+        "family": "bridge",
+        "table": "proxmox-firewall-guests",
+        "name": "block-dhcp-in",
+        "handle": 3
+      }
+    },
+    {
+      "chain": {
+        "family": "bridge",
+        "table": "proxmox-firewall-guests",
+        "name": "block-dhcp-out",
+        "handle": 4
+      }
+    },
+    {
+      "chain": {
+        "family": "bridge",
+        "table": "proxmox-firewall-guests",
+        "name": "allow-ndp-in",
+        "handle": 5
+      }
+    },
+    {
+      "chain": {
+        "family": "bridge",
+        "table": "proxmox-firewall-guests",
+        "name": "block-ndp-in",
+        "handle": 6
+      }
+    },
+    {
+      "chain": {
+        "family": "bridge",
+        "table": "proxmox-firewall-guests",
+        "name": "allow-ndp-out",
+        "handle": 7
+      }
+    },
+    {
+      "chain": {
+        "family": "bridge",
+        "table": "proxmox-firewall-guests",
+        "name": "block-ndp-out",
+        "handle": 8
+      }
+    },
+    {
+      "chain": {
+        "family": "bridge",
+        "table": "proxmox-firewall-guests",
+        "name": "allow-ra-out",
+        "handle": 9
+      }
+    },
+    {
+      "chain": {
+        "family": "bridge",
+        "table": "proxmox-firewall-guests",
+        "name": "block-ra-out",
+        "handle": 10
+      }
+    },
+    {
+      "chain": {
+        "family": "bridge",
+        "table": "proxmox-firewall-guests",
+        "name": "after-vm-in",
+        "handle": 11
+      }
+    },
+    {
+      "chain": {
+        "family": "bridge",
+        "table": "proxmox-firewall-guests",
+        "name": "do-reject",
+        "handle": 12
+      }
+    },
+    {
+      "chain": {
+        "family": "bridge",
+        "table": "proxmox-firewall-guests",
+        "name": "vm-out",
+        "handle": 13,
+        "type": "filter",
+        "hook": "prerouting",
+        "prio": 0,
+        "policy": "accept"
+      }
+    },
+    {
+      "chain": {
+        "family": "bridge",
+        "table": "proxmox-firewall-guests",
+        "name": "vm-in",
+        "handle": 14,
+        "type": "filter",
+        "hook": "postrouting",
+        "prio": 0,
+        "policy": "accept"
+      }
+    },
+    {
+      "chain": {
+        "family": "bridge",
+        "table": "proxmox-firewall-guests",
+        "name": "group-network1-in",
+        "handle": 6138
+      }
+    },
+    {
+      "chain": {
+        "family": "bridge",
+        "table": "proxmox-firewall-guests",
+        "name": "group-network1-out",
+        "handle": 6140
+      }
+    },
+    {
+      "chain": {
+        "family": "bridge",
+        "table": "proxmox-firewall-guests",
+        "name": "guest-90002-in",
+        "handle": 6141
+      }
+    },
+    {
+      "chain": {
+        "family": "bridge",
+        "table": "proxmox-firewall-guests",
+        "name": "guest-90002-out",
+        "handle": 6142
+      }
+    },
+    {
+      "chain": {
+        "family": "bridge",
+        "table": "proxmox-firewall-guests",
+        "name": "guest-90001-in",
+        "handle": 6158
+      }
+    },
+    {
+      "chain": {
+        "family": "bridge",
+        "table": "proxmox-firewall-guests",
+        "name": "guest-90001-out",
+        "handle": 6159
+      }
+    },
+    {
+      "chain": {
+        "family": "bridge",
+        "table": "proxmox-firewall-guests",
+        "name": "guest-800001-in",
+        "handle": 6179
+      }
+    },
+    {
+      "chain": {
+        "family": "bridge",
+        "table": "proxmox-firewall-guests",
+        "name": "guest-800001-out",
+        "handle": 6180
+      }
+    }
+  ]
+}
diff --git a/proxmox-firewall/tests/input/cluster.fw b/proxmox-firewall/tests/input/cluster.fw
new file mode 100644
index 0000000..23168ae
--- /dev/null
+++ b/proxmox-firewall/tests/input/cluster.fw
@@ -0,0 +1,26 @@
+[OPTIONS]
+
+log_ratelimit: burst=12,enable=1,rate=2/second
+enable: 1
+
+[ALIASES]
+
+network1 172.16.100.0/24
+network2 172.16.200.0/24
+
+[IPSET network1]
+
+dc/network1
+
+[IPSET management]
+
+127.0.0.1/8
+
+[RULES]
+
+IN ACCEPT -log nolog
+
+[group network1]
+
+IN ACCEPT -source dc/network1 -dest dc/network1 -log nolog
+
diff --git a/proxmox-firewall/tests/input/host.fw b/proxmox-firewall/tests/input/host.fw
new file mode 100644
index 0000000..8fa57e6
--- /dev/null
+++ b/proxmox-firewall/tests/input/host.fw
@@ -0,0 +1,23 @@
+[OPTIONS]
+
+log_level_out: notice
+tcpflags: 1
+nftables: 1
+tcp_flags_log_level: err
+log_level_in: info
+enable: 1
+smurf_log_level: alert
+ndp: 1
+protection_synflood: 1
+protection_synflood_burst: 1337
+protection_synflood_rate: 400
+nosmurfs: 1
+nf_conntrack_helpers: amanda,ftp,irc,netbios-ns,pptp,sane,sip,snmp,tftp
+
+
+[RULES]
+
+IN DNS(ACCEPT) -source dc/network1 -log nolog
+IN DHCPv6(ACCEPT) -log nolog
+IN DHCPfwd(ACCEPT) -log nolog
+
diff --git a/proxmox-firewall/tests/integration_tests.rs b/proxmox-firewall/tests/integration_tests.rs
new file mode 100644
index 0000000..860c78d
--- /dev/null
+++ b/proxmox-firewall/tests/integration_tests.rs
@@ -0,0 +1,90 @@
+use std::collections::HashMap;
+
+use proxmox_firewall::config::{FirewallConfig, FirewallConfigLoader, NftConfigLoader};
+use proxmox_firewall::firewall::Firewall;
+use proxmox_nftables::command::CommandOutput;
+use proxmox_sys::nodename;
+use proxmox_ve_config::guest::types::Vmid;
+use proxmox_ve_config::guest::{GuestEntry, GuestMap, GuestType};
+
+struct MockFirewallConfigLoader {}
+
+impl MockFirewallConfigLoader {
+    pub fn new() -> Self {
+        Self {}
+    }
+}
+
+impl FirewallConfigLoader for MockFirewallConfigLoader {
+    fn cluster(&self) -> Option<Box<dyn std::io::BufRead>> {
+        Some(Box::new(include_str!("input/cluster.fw").as_bytes()))
+    }
+
+    fn host(&self) -> Option<Box<dyn std::io::BufRead>> {
+        Some(Box::new(include_str!("input/host.fw").as_bytes()))
+    }
+
+    fn guest_list(&self) -> GuestMap {
+        let hostname = nodename().to_string();
+
+        let mut map = HashMap::new();
+
+        let entry = GuestEntry::new(hostname.clone(), GuestType::Vm);
+        map.insert(101.into(), entry);
+
+        let entry = GuestEntry::new(hostname, GuestType::Ct);
+        map.insert(100.into(), entry);
+
+        GuestMap::from(map)
+    }
+
+    fn guest_config(&self, vmid: &Vmid, _guest: &GuestEntry) -> Option<Box<dyn std::io::BufRead>> {
+        if *vmid == Vmid::new(101) {
+            return Some(Box::new(include_str!("input/101.conf").as_bytes()));
+        }
+
+        if *vmid == Vmid::new(100) {
+            return Some(Box::new(include_str!("input/100.conf").as_bytes()));
+        }
+
+        None
+    }
+
+    fn guest_firewall_config(&self, vmid: &Vmid) -> Option<Box<dyn std::io::BufRead>> {
+        if *vmid == Vmid::new(101) {
+            return Some(Box::new(include_str!("input/101.fw").as_bytes()));
+        }
+
+        if *vmid == Vmid::new(100) {
+            return Some(Box::new(include_str!("input/100.fw").as_bytes()));
+        }
+
+        None
+    }
+}
+
+struct MockNftConfigLoader {}
+
+impl MockNftConfigLoader {
+    pub fn new() -> Self {
+        Self {}
+    }
+}
+
+impl NftConfigLoader for MockNftConfigLoader {
+    fn chains(&self) -> CommandOutput {
+        serde_json::from_str(include_str!("input/chains.json")).expect("valid chains.json")
+    }
+}
+
+#[test]
+fn test_firewall() {
+    let firewall_config = FirewallConfig::new(
+        Box::new(MockFirewallConfigLoader::new()),
+        Box::new(MockNftConfigLoader::new()),
+    );
+
+    let firewall = Firewall::from(firewall_config);
+
+    insta::assert_json_snapshot!(firewall.full_host_fw().expect("firewall can be generated"));
+}
diff --git a/proxmox-firewall/tests/snapshots/integration_tests__firewall.snap b/proxmox-firewall/tests/snapshots/integration_tests__firewall.snap
new file mode 100644
index 0000000..7611a64
--- /dev/null
+++ b/proxmox-firewall/tests/snapshots/integration_tests__firewall.snap
@@ -0,0 +1,3530 @@
+---
+source: proxmox-firewall/tests/integration_tests.rs
+expression: "firewall.full_host_fw().expect(\"firewall can be generated\")"
+---
+{
+  "nftables": [
+    {
+      "flush": {
+        "chain": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "cluster-in"
+        }
+      }
+    },
+    {
+      "flush": {
+        "chain": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "cluster-out"
+        }
+      }
+    },
+    {
+      "add": {
+        "chain": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "host-in"
+        }
+      }
+    },
+    {
+      "flush": {
+        "chain": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "host-in"
+        }
+      }
+    },
+    {
+      "flush": {
+        "chain": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "option-in"
+        }
+      }
+    },
+    {
+      "add": {
+        "chain": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "host-out"
+        }
+      }
+    },
+    {
+      "flush": {
+        "chain": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "host-out"
+        }
+      }
+    },
+    {
+      "flush": {
+        "chain": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "option-out"
+        }
+      }
+    },
+    {
+      "flush": {
+        "map": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "vm-map-in"
+        }
+      }
+    },
+    {
+      "flush": {
+        "map": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "vm-map-out"
+        }
+      }
+    },
+    {
+      "flush": {
+        "chain": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "ct-in"
+        }
+      }
+    },
+    {
+      "flush": {
+        "chain": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "ratelimit-synflood"
+        }
+      }
+    },
+    {
+      "flush": {
+        "chain": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "log-invalid-tcp"
+        }
+      }
+    },
+    {
+      "flush": {
+        "chain": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "log-smurfs"
+        }
+      }
+    },
+    {
+      "delete": {
+        "chain": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "guest-800001-in"
+        }
+      }
+    },
+    {
+      "delete": {
+        "chain": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "guest-800001-out"
+        }
+      }
+    },
+    {
+      "delete": {
+        "chain": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "guest-90001-in"
+        }
+      }
+    },
+    {
+      "delete": {
+        "chain": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "guest-90001-out"
+        }
+      }
+    },
+    {
+      "delete": {
+        "chain": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "guest-90002-in"
+        }
+      }
+    },
+    {
+      "delete": {
+        "chain": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "guest-90002-out"
+        }
+      }
+    },
+    {
+      "delete": {
+        "chain": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "group-network1-in"
+        }
+      }
+    },
+    {
+      "delete": {
+        "chain": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "group-network1-out"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-dc/management",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-dc/management"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-dc/management-nomatch",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-dc/management-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-dc/management",
+          "elem": [
+            {
+              "prefix": {
+                "addr": "127.0.0.1",
+                "len": 8
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-dc/management",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-dc/management"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-dc/management-nomatch",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-dc/management-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-dc/network1",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-dc/network1"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-dc/network1-nomatch",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-dc/network1-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v4-dc/network1",
+          "elem": [
+            {
+              "prefix": {
+                "addr": "172.16.100.0",
+                "len": 24
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-dc/network1",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-dc/network1"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-dc/network1-nomatch",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "v6-dc/network1-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "chain": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "group-network1-in"
+        }
+      }
+    },
+    {
+      "flush": {
+        "chain": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "group-network1-in"
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "group-network1-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "ip",
+                    "field": "saddr"
+                  }
+                },
+                "right": {
+                  "prefix": {
+                    "addr": "172.16.100.0",
+                    "len": 24
+                  }
+                }
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "ip",
+                    "field": "daddr"
+                  }
+                },
+                "right": {
+                  "prefix": {
+                    "addr": "172.16.100.0",
+                    "len": 24
+                  }
+                }
+              }
+            },
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "chain": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "group-network1-out"
+        }
+      }
+    },
+    {
+      "flush": {
+        "chain": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "group-network1-out"
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "cluster-in",
+          "expr": [
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "cluster-in",
+          "expr": [
+            {
+              "limit": {
+                "rate": 2,
+                "per": "second",
+                "burst": 12
+              }
+            },
+            {
+              "log": {
+                "prefix": ":0:6:cluster-in: DROP: ",
+                "group": 0
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "cluster-in",
+          "expr": [
+            {
+              "drop": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "cluster-out",
+          "expr": [
+            {
+              "limit": {
+                "rate": 2,
+                "per": "second",
+                "burst": 12
+              }
+            },
+            {
+              "log": {
+                "prefix": ":0:5:cluster-out: ACCEPT: ",
+                "group": 0
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "cluster-out",
+          "expr": [
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "ct helper": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "helper-amanda-udp",
+          "type": "amanda",
+          "protocol": "udp",
+          "l3proto": null
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ct-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "l4proto"
+                  }
+                },
+                "right": "udp"
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "th",
+                    "field": "dport"
+                  }
+                },
+                "right": 10080
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "ct": {
+                    "key": "state"
+                  }
+                },
+                "right": [
+                  "new",
+                  "established"
+                ]
+              }
+            },
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ct-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "l4proto"
+                  }
+                },
+                "right": "udp"
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "th",
+                    "field": "dport"
+                  }
+                },
+                "right": 10080
+              }
+            },
+            {
+              "ct helper": "helper-amanda-udp"
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ct-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "ct": {
+                    "key": "helper"
+                  }
+                },
+                "right": "amanda"
+              }
+            },
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "ct helper": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "helper-ftp-tcp",
+          "type": "ftp",
+          "protocol": "tcp",
+          "l3proto": null
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ct-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "l4proto"
+                  }
+                },
+                "right": "tcp"
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "th",
+                    "field": "dport"
+                  }
+                },
+                "right": 21
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "ct": {
+                    "key": "state"
+                  }
+                },
+                "right": [
+                  "new",
+                  "established"
+                ]
+              }
+            },
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ct-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "l4proto"
+                  }
+                },
+                "right": "tcp"
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "th",
+                    "field": "dport"
+                  }
+                },
+                "right": 21
+              }
+            },
+            {
+              "ct helper": "helper-ftp-tcp"
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ct-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "ct": {
+                    "key": "helper"
+                  }
+                },
+                "right": "ftp"
+              }
+            },
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "ct helper": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "helper-irc-tcp",
+          "type": "irc",
+          "protocol": "tcp",
+          "l3proto": "ip"
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ct-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "l4proto"
+                  }
+                },
+                "right": "tcp"
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "th",
+                    "field": "dport"
+                  }
+                },
+                "right": 6667
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "ct": {
+                    "key": "state"
+                  }
+                },
+                "right": [
+                  "new",
+                  "established"
+                ]
+              }
+            },
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ct-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "l4proto"
+                  }
+                },
+                "right": "tcp"
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "th",
+                    "field": "dport"
+                  }
+                },
+                "right": 6667
+              }
+            },
+            {
+              "ct helper": "helper-irc-tcp"
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ct-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "ct": {
+                    "key": "helper",
+                    "family": "ip"
+                  }
+                },
+                "right": "irc"
+              }
+            },
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "ct helper": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "helper-netbios-ns-udp",
+          "type": "netbios-ns",
+          "protocol": "udp",
+          "l3proto": "ip"
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ct-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "l4proto"
+                  }
+                },
+                "right": "udp"
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "th",
+                    "field": "dport"
+                  }
+                },
+                "right": 137
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "ct": {
+                    "key": "state"
+                  }
+                },
+                "right": [
+                  "new",
+                  "established"
+                ]
+              }
+            },
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ct-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "l4proto"
+                  }
+                },
+                "right": "udp"
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "th",
+                    "field": "dport"
+                  }
+                },
+                "right": 137
+              }
+            },
+            {
+              "ct helper": "helper-netbios-ns-udp"
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ct-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "ct": {
+                    "key": "helper",
+                    "family": "ip"
+                  }
+                },
+                "right": "netbios-ns"
+              }
+            },
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "ct helper": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "helper-pptp-tcp",
+          "type": "pptp",
+          "protocol": "tcp",
+          "l3proto": "ip"
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ct-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "l4proto"
+                  }
+                },
+                "right": "tcp"
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "th",
+                    "field": "dport"
+                  }
+                },
+                "right": 1723
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "ct": {
+                    "key": "state"
+                  }
+                },
+                "right": [
+                  "new",
+                  "established"
+                ]
+              }
+            },
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ct-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "l4proto"
+                  }
+                },
+                "right": "tcp"
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "th",
+                    "field": "dport"
+                  }
+                },
+                "right": 1723
+              }
+            },
+            {
+              "ct helper": "helper-pptp-tcp"
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ct-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "ct": {
+                    "key": "helper",
+                    "family": "ip"
+                  }
+                },
+                "right": "pptp"
+              }
+            },
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "ct helper": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "helper-sane-tcp",
+          "type": "sane",
+          "protocol": "tcp",
+          "l3proto": null
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ct-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "l4proto"
+                  }
+                },
+                "right": "tcp"
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "th",
+                    "field": "dport"
+                  }
+                },
+                "right": 6566
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "ct": {
+                    "key": "state"
+                  }
+                },
+                "right": [
+                  "new",
+                  "established"
+                ]
+              }
+            },
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ct-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "l4proto"
+                  }
+                },
+                "right": "tcp"
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "th",
+                    "field": "dport"
+                  }
+                },
+                "right": 6566
+              }
+            },
+            {
+              "ct helper": "helper-sane-tcp"
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ct-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "ct": {
+                    "key": "helper"
+                  }
+                },
+                "right": "sane"
+              }
+            },
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "ct helper": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "helper-sip-udp",
+          "type": "sip",
+          "protocol": "udp",
+          "l3proto": null
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ct-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "l4proto"
+                  }
+                },
+                "right": "udp"
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "th",
+                    "field": "dport"
+                  }
+                },
+                "right": 5060
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "ct": {
+                    "key": "state"
+                  }
+                },
+                "right": [
+                  "new",
+                  "established"
+                ]
+              }
+            },
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ct-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "l4proto"
+                  }
+                },
+                "right": "udp"
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "th",
+                    "field": "dport"
+                  }
+                },
+                "right": 5060
+              }
+            },
+            {
+              "ct helper": "helper-sip-udp"
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ct-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "ct": {
+                    "key": "helper"
+                  }
+                },
+                "right": "sip"
+              }
+            },
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "ct helper": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "helper-snmp-udp",
+          "type": "snmp",
+          "protocol": "udp",
+          "l3proto": "ip"
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ct-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "l4proto"
+                  }
+                },
+                "right": "udp"
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "th",
+                    "field": "dport"
+                  }
+                },
+                "right": 161
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "ct": {
+                    "key": "state"
+                  }
+                },
+                "right": [
+                  "new",
+                  "established"
+                ]
+              }
+            },
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ct-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "l4proto"
+                  }
+                },
+                "right": "udp"
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "th",
+                    "field": "dport"
+                  }
+                },
+                "right": 161
+              }
+            },
+            {
+              "ct helper": "helper-snmp-udp"
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ct-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "ct": {
+                    "key": "helper",
+                    "family": "ip"
+                  }
+                },
+                "right": "snmp"
+              }
+            },
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "ct helper": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "name": "helper-tftp-udp",
+          "type": "tftp",
+          "protocol": "udp",
+          "l3proto": null
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ct-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "l4proto"
+                  }
+                },
+                "right": "udp"
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "th",
+                    "field": "dport"
+                  }
+                },
+                "right": 69
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "ct": {
+                    "key": "state"
+                  }
+                },
+                "right": [
+                  "new",
+                  "established"
+                ]
+              }
+            },
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ct-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "l4proto"
+                  }
+                },
+                "right": "udp"
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "th",
+                    "field": "dport"
+                  }
+                },
+                "right": 69
+              }
+            },
+            {
+              "ct helper": "helper-tftp-udp"
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ct-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "ct": {
+                    "key": "helper"
+                  }
+                },
+                "right": "tftp"
+              }
+            },
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "option-in",
+          "expr": [
+            {
+              "jump": {
+                "target": "allow-ndp-in"
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "option-out",
+          "expr": [
+            {
+              "jump": {
+                "target": "allow-ndp-out"
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "option-in",
+          "expr": [
+            {
+              "jump": {
+                "target": "block-synflood"
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ratelimit-synflood",
+          "expr": [
+            {
+              "set": {
+                "op": "update",
+                "elem": {
+                  "payload": {
+                    "protocol": "ip",
+                    "field": "saddr"
+                  }
+                },
+                "set": "@v4-synflood-limit",
+                "stmt": {
+                  "limit": {
+                    "rate": 400,
+                    "per": "second",
+                    "burst": 1337,
+                    "inv": true
+                  }
+                }
+              }
+            },
+            {
+              "drop": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "ratelimit-synflood",
+          "expr": [
+            {
+              "set": {
+                "op": "update",
+                "elem": {
+                  "payload": {
+                    "protocol": "ip6",
+                    "field": "saddr"
+                  }
+                },
+                "set": "@v6-synflood-limit",
+                "stmt": {
+                  "limit": {
+                    "rate": 400,
+                    "per": "second",
+                    "burst": 1337,
+                    "inv": true
+                  }
+                }
+              }
+            },
+            {
+              "drop": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "option-in",
+          "expr": [
+            {
+              "jump": {
+                "target": "block-invalid-tcp"
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "log-invalid-tcp",
+          "expr": [
+            {
+              "limit": {
+                "rate": 2,
+                "per": "second",
+                "burst": 12
+              }
+            },
+            {
+              "log": {
+                "prefix": ":0:3:log-invalid-tcp: DROP: ",
+                "group": 0
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "option-in",
+          "expr": [
+            {
+              "jump": {
+                "target": "block-smurfs"
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "log-smurfs",
+          "expr": [
+            {
+              "limit": {
+                "rate": 2,
+                "per": "second",
+                "burst": 12
+              }
+            },
+            {
+              "log": {
+                "prefix": ":0:1:log-smurfs: DROP: ",
+                "group": 0
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "option-in",
+          "expr": [
+            {
+              "jump": {
+                "target": "block-conntrack-invalid"
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "host-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "l4proto"
+                  }
+                },
+                "right": "udp"
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "th",
+                    "field": "dport"
+                  }
+                },
+                "right": 53
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "ip",
+                    "field": "saddr"
+                  }
+                },
+                "right": {
+                  "prefix": {
+                    "addr": "172.16.100.0",
+                    "len": 24
+                  }
+                }
+              }
+            },
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "host-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "l4proto"
+                  }
+                },
+                "right": "tcp"
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "th",
+                    "field": "dport"
+                  }
+                },
+                "right": 53
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "ip",
+                    "field": "saddr"
+                  }
+                },
+                "right": {
+                  "prefix": {
+                    "addr": "172.16.100.0",
+                    "len": 24
+                  }
+                }
+              }
+            },
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "host-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "l4proto"
+                  }
+                },
+                "right": "udp"
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "th",
+                    "field": "sport"
+                  }
+                },
+                "right": {
+                  "range": [
+                    546,
+                    547
+                  ]
+                }
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "th",
+                    "field": "dport"
+                  }
+                },
+                "right": {
+                  "range": [
+                    546,
+                    547
+                  ]
+                }
+              }
+            },
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "inet",
+          "table": "proxmox-firewall",
+          "chain": "host-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "l4proto"
+                  }
+                },
+                "right": "udp"
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "th",
+                    "field": "sport"
+                  }
+                },
+                "right": {
+                  "range": [
+                    67,
+                    68
+                  ]
+                }
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "th",
+                    "field": "dport"
+                  }
+                },
+                "right": {
+                  "range": [
+                    67,
+                    68
+                  ]
+                }
+              }
+            },
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-dc/management",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-dc/management"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-dc/management-nomatch",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-dc/management-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-dc/management",
+          "elem": [
+            {
+              "prefix": {
+                "addr": "127.0.0.1",
+                "len": 8
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-dc/management",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-dc/management"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-dc/management-nomatch",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-dc/management-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-dc/network1",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-dc/network1"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-dc/network1-nomatch",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-dc/network1-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-dc/network1",
+          "elem": [
+            {
+              "prefix": {
+                "addr": "172.16.100.0",
+                "len": 24
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-dc/network1",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-dc/network1"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-dc/network1-nomatch",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-dc/network1-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "chain": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "group-network1-in"
+        }
+      }
+    },
+    {
+      "flush": {
+        "chain": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "group-network1-in"
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "group-network1-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "ip",
+                    "field": "saddr"
+                  }
+                },
+                "right": {
+                  "prefix": {
+                    "addr": "172.16.100.0",
+                    "len": 24
+                  }
+                }
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "ip",
+                    "field": "daddr"
+                  }
+                },
+                "right": {
+                  "prefix": {
+                    "addr": "172.16.100.0",
+                    "len": 24
+                  }
+                }
+              }
+            },
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "chain": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "group-network1-out"
+        }
+      }
+    },
+    {
+      "flush": {
+        "chain": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "group-network1-out"
+        }
+      }
+    },
+    {
+      "add": {
+        "chain": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "guest-100-in"
+        }
+      }
+    },
+    {
+      "flush": {
+        "chain": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "guest-100-in"
+        }
+      }
+    },
+    {
+      "add": {
+        "chain": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "guest-100-out"
+        }
+      }
+    },
+    {
+      "flush": {
+        "chain": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "guest-100-out"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-guest-100/ipfilter-net1",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-guest-100/ipfilter-net1"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-guest-100/ipfilter-net1-nomatch",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-guest-100/ipfilter-net1-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-guest-100/ipfilter-net1",
+          "elem": [
+            {
+              "prefix": {
+                "addr": "172.16.100.0",
+                "len": 24
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-guest-100/ipfilter-net1",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-guest-100/ipfilter-net1"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-guest-100/ipfilter-net1-nomatch",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-guest-100/ipfilter-net1-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-100-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "oifname"
+                  }
+                },
+                "right": "veth100i1"
+              }
+            },
+            {
+              "match": {
+                "op": "!=",
+                "left": {
+                  "payload": {
+                    "protocol": "arp",
+                    "field": "daddr ip"
+                  }
+                },
+                "right": "@v4-guest-100/ipfilter-net1"
+              }
+            },
+            {
+              "drop": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-100-out",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "iifname"
+                  }
+                },
+                "right": "veth100i1"
+              }
+            },
+            {
+              "match": {
+                "op": "!=",
+                "left": {
+                  "payload": {
+                    "protocol": "ip",
+                    "field": "saddr"
+                  }
+                },
+                "right": "@v4-guest-100/ipfilter-net1"
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "ip",
+                    "field": "saddr"
+                  }
+                },
+                "right": "@v4-guest-100/ipfilter-net1-nomatch"
+              }
+            },
+            {
+              "drop": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-100-out",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "iifname"
+                  }
+                },
+                "right": "veth100i1"
+              }
+            },
+            {
+              "match": {
+                "op": "!=",
+                "left": {
+                  "payload": {
+                    "protocol": "ip6",
+                    "field": "saddr"
+                  }
+                },
+                "right": "@v6-guest-100/ipfilter-net1"
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "ip6",
+                    "field": "saddr"
+                  }
+                },
+                "right": "@v6-guest-100/ipfilter-net1-nomatch"
+              }
+            },
+            {
+              "drop": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-100-out",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "iifname"
+                  }
+                },
+                "right": "veth100i1"
+              }
+            },
+            {
+              "match": {
+                "op": "!=",
+                "left": {
+                  "payload": {
+                    "protocol": "arp",
+                    "field": "saddr ip"
+                  }
+                },
+                "right": "@v4-guest-100/ipfilter-net1"
+              }
+            },
+            {
+              "drop": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-100-in",
+          "expr": [
+            {
+              "jump": {
+                "target": "allow-dhcp-in"
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-100-out",
+          "expr": [
+            {
+              "jump": {
+                "target": "allow-dhcp-out"
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-100-in",
+          "expr": [
+            {
+              "jump": {
+                "target": "allow-ndp-in"
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-100-out",
+          "expr": [
+            {
+              "jump": {
+                "target": "allow-ndp-out"
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-100-out",
+          "expr": [
+            {
+              "jump": {
+                "target": "block-ra-out"
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-100-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "ether",
+                    "field": "type"
+                  }
+                },
+                "right": "arp"
+              }
+            },
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-100-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "oifname"
+                  }
+                },
+                "right": "veth100i1"
+              }
+            },
+            {
+              "jump": {
+                "target": "group-network1-in"
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-100-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "l4proto"
+                  }
+                },
+                "right": "tcp"
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "th",
+                    "field": "sport"
+                  }
+                },
+                "right": 80
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "th",
+                    "field": "dport"
+                  }
+                },
+                "right": {
+                  "set": [
+                    123,
+                    {
+                      "range": [
+                        222,
+                        333
+                      ]
+                    }
+                  ]
+                }
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "ip",
+                    "field": "saddr"
+                  }
+                },
+                "right": {
+                  "set": [
+                    {
+                      "prefix": {
+                        "addr": "192.168.0.1",
+                        "len": 24
+                      }
+                    },
+                    {
+                      "range": [
+                        "127.0.0.1",
+                        "127.255.255.0"
+                      ]
+                    },
+                    {
+                      "prefix": {
+                        "addr": "172.16.0.1",
+                        "len": 32
+                      }
+                    }
+                  ]
+                }
+              }
+            },
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-100-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "icmp",
+                    "field": "type"
+                  }
+                },
+                "right": "echo-request"
+              }
+            },
+            {
+              "limit": {
+                "rate": 2,
+                "per": "second",
+                "burst": 12
+              }
+            },
+            {
+              "log": {
+                "prefix": ":100:6:guest-100-in: DROP: ",
+                "group": 0
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-100-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "icmp",
+                    "field": "type"
+                  }
+                },
+                "right": "echo-request"
+              }
+            },
+            {
+              "drop": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "vm-map-in",
+          "elem": [
+            [
+              "veth100i1",
+              {
+                "goto": {
+                  "target": "guest-100-in"
+                }
+              }
+            ]
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-100-in",
+          "expr": [
+            {
+              "jump": {
+                "target": "after-vm-in"
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-100-in",
+          "expr": [
+            {
+              "limit": {
+                "rate": 2,
+                "per": "second",
+                "burst": 12
+              }
+            },
+            {
+              "log": {
+                "prefix": ":100:2:guest-100-in: DROP: ",
+                "group": 0
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-100-in",
+          "expr": [
+            {
+              "drop": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-100-out",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "meta": {
+                    "key": "iifname"
+                  }
+                },
+                "right": "veth100i1"
+              }
+            },
+            {
+              "jump": {
+                "target": "group-network1-out"
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "element": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "vm-map-out",
+          "elem": [
+            [
+              "veth100i1",
+              {
+                "goto": {
+                  "target": "guest-100-out"
+                }
+              }
+            ]
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-100-out",
+          "expr": [
+            {
+              "limit": {
+                "rate": 2,
+                "per": "second",
+                "burst": 12
+              }
+            },
+            {
+              "log": {
+                "prefix": ":100:1:guest-100-out: REJECT: ",
+                "group": 0
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-100-out",
+          "expr": [
+            {
+              "drop": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "chain": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "guest-101-in"
+        }
+      }
+    },
+    {
+      "flush": {
+        "chain": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "guest-101-in"
+        }
+      }
+    },
+    {
+      "add": {
+        "chain": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "guest-101-out"
+        }
+      }
+    },
+    {
+      "flush": {
+        "chain": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "guest-101-out"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-guest-101/testing",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-guest-101/testing"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-guest-101/testing-nomatch",
+          "type": "ipv4_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v4-guest-101/testing-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-guest-101/testing",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-guest-101/testing"
+        }
+      }
+    },
+    {
+      "add": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-guest-101/testing-nomatch",
+          "type": "ipv6_addr",
+          "flags": [
+            "interval"
+          ]
+        }
+      }
+    },
+    {
+      "flush": {
+        "set": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "name": "v6-guest-101/testing-nomatch"
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-101-in",
+          "expr": [
+            {
+              "jump": {
+                "target": "allow-dhcp-in"
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-101-out",
+          "expr": [
+            {
+              "jump": {
+                "target": "allow-dhcp-out"
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-101-in",
+          "expr": [
+            {
+              "jump": {
+                "target": "block-ndp-in"
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-101-out",
+          "expr": [
+            {
+              "jump": {
+                "target": "block-ndp-out"
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-101-out",
+          "expr": [
+            {
+              "jump": {
+                "target": "block-ra-out"
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-101-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "ether",
+                    "field": "type"
+                  }
+                },
+                "right": "arp"
+              }
+            },
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-101-in",
+          "expr": [
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "ip",
+                    "field": "saddr"
+                  }
+                },
+                "right": {
+                  "prefix": {
+                    "addr": "123.123.123.123",
+                    "len": 32
+                  }
+                }
+              }
+            },
+            {
+              "match": {
+                "op": "==",
+                "left": {
+                  "payload": {
+                    "protocol": "ip",
+                    "field": "daddr"
+                  }
+                },
+                "right": {
+                  "prefix": {
+                    "addr": "172.16.200.0",
+                    "len": 24
+                  }
+                }
+              }
+            },
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-101-in",
+          "expr": [
+            {
+              "jump": {
+                "target": "after-vm-in"
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-101-in",
+          "expr": [
+            {
+              "drop": null
+            }
+          ]
+        }
+      }
+    },
+    {
+      "add": {
+        "rule": {
+          "family": "bridge",
+          "table": "proxmox-firewall-guests",
+          "chain": "guest-101-out",
+          "expr": [
+            {
+              "accept": null
+            }
+          ]
+        }
+      }
+    }
+  ]
+}
-- 
2.39.2


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


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

* [pve-devel] [PATCH qemu-server v3 35/39] firewall: add handling for new nft firewall
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (33 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 34/39] firewall: add integration test Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 21:08   ` Thomas Lamprecht
  2024-04-18 16:14 ` [pve-devel] [PATCH pve-container v3 36/39] " Stefan Hanreich
                   ` (4 subsequent siblings)
  39 siblings, 1 reply; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel

When the nftables firewall is enabled, we do not need to create
firewall bridges.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 vm-network-scripts/pve-bridge | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/vm-network-scripts/pve-bridge b/vm-network-scripts/pve-bridge
index 85997a0..ac2eb3b 100755
--- a/vm-network-scripts/pve-bridge
+++ b/vm-network-scripts/pve-bridge
@@ -6,6 +6,7 @@ use warnings;
 use PVE::QemuServer;
 use PVE::Tools qw(run_command);
 use PVE::Network;
+use PVE::Firewall;
 
 my $have_sdn;
 eval {
@@ -44,13 +45,17 @@ die "unable to get network config '$netid'\n"
 my $net = PVE::QemuServer::parse_net($netconf);
 die "unable to parse network config '$netid'\n" if !$net;
 
+my $cluster_fw_conf = PVE::Firewall::load_clusterfw_conf();
+my $host_fw_conf = PVE::Firewall::load_hostfw_conf($cluster_fw_conf);
+my $firewall = $net->{firewall} && !($host_fw_conf->{options}->{nftables} // 0);
+
 if ($have_sdn) {
     PVE::Network::SDN::Vnets::add_dhcp_mapping($net->{bridge}, $net->{macaddr}, $vmid, $conf->{name});
     PVE::Network::SDN::Zones::tap_create($iface, $net->{bridge});
-    PVE::Network::SDN::Zones::tap_plug($iface, $net->{bridge}, $net->{tag}, $net->{firewall}, $net->{trunks}, $net->{rate});
+    PVE::Network::SDN::Zones::tap_plug($iface, $net->{bridge}, $net->{tag}, $firewall, $net->{trunks}, $net->{rate});
 } else {
     PVE::Network::tap_create($iface, $net->{bridge});
-    PVE::Network::tap_plug($iface, $net->{bridge}, $net->{tag}, $net->{firewall}, $net->{trunks}, $net->{rate});
+    PVE::Network::tap_plug($iface, $net->{bridge}, $net->{tag}, $firewall, $net->{trunks}, $net->{rate});
 }
 
 exit 0;
-- 
2.39.2


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


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

* [pve-devel] [PATCH pve-container v3 36/39] firewall: add handling for new nft firewall
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (34 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH qemu-server v3 35/39] firewall: add handling for new nft firewall Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH pve-firewall v3 37/39] add configuration option for new nftables firewall Stefan Hanreich
                   ` (3 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel

When the nftables firewall is enabled, we do not need to create
firewall bridges.

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

diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm
index e688ea6..85800ea 100644
--- a/src/PVE/LXC.pm
+++ b/src/PVE/LXC.pm
@@ -18,6 +18,7 @@ use PVE::AccessControl;
 use PVE::CGroup;
 use PVE::CpuSet;
 use PVE::Exception qw(raise_perm_exc);
+use PVE::Firewall;
 use PVE::GuestHelpers qw(check_vnet_access safe_string_ne safe_num_ne safe_boolean_ne);
 use PVE::INotify;
 use PVE::JSONSchema qw(get_standard_option);
@@ -949,6 +950,10 @@ sub net_tap_plug : prototype($$) {
     my ($bridge, $tag, $firewall, $trunks, $rate, $hwaddr) =
 	$net->@{'bridge', 'tag', 'firewall', 'trunks', 'rate', 'hwaddr'};
 
+    my $cluster_fw_conf = PVE::Firewall::load_clusterfw_conf();
+    my $host_fw_conf = PVE::Firewall::load_hostfw_conf($cluster_fw_conf);
+    $firewall = $net->{firewall} && !($host_fw_conf->{options}->{nftables} // 0);
+
     if ($have_sdn) {
 	PVE::Network::SDN::Zones::tap_plug($iface, $bridge, $tag, $firewall, $trunks, $rate);
 	PVE::Network::SDN::Zones::add_bridge_fdb($iface, $hwaddr, $bridge);
-- 
2.39.2


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


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

* [pve-devel] [PATCH pve-firewall v3 37/39] add configuration option for new nftables firewall
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (35 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH pve-container v3 36/39] " Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH pve-manager v3 38/39] firewall: expose " Stefan Hanreich
                   ` (2 subsequent siblings)
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel

Introduces new nftables configuration option that en/disables the new
nftables firewall.

pve-firewall reads this option and only generates iptables rules when
nftables is set to `0`. Conversely proxmox-firewall only generates
nftables rules when the option is set to `1`.

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

diff --git a/src/PVE/Firewall.pm b/src/PVE/Firewall.pm
index 81a8798..b39843d 100644
--- a/src/PVE/Firewall.pm
+++ b/src/PVE/Firewall.pm
@@ -1408,6 +1408,12 @@ our $host_option_properties = {
 	default => 0,
 	optional => 1
     },
+    nftables => {
+	description => "Enable nftables based firewall",
+	type => 'boolean',
+	default => 0,
+	optional => 1,
+    },
 };
 
 our $vm_option_properties = {
@@ -2929,7 +2935,7 @@ sub parse_hostfw_option {
 
     my $loglevels = "emerg|alert|crit|err|warning|notice|info|debug|nolog";
 
-    if ($line =~ m/^(enable|nosmurfs|tcpflags|ndp|log_nf_conntrack|nf_conntrack_allow_invalid|protection_synflood):\s*(0|1)\s*$/i) {
+    if ($line =~ m/^(enable|nosmurfs|tcpflags|ndp|log_nf_conntrack|nf_conntrack_allow_invalid|protection_synflood|nftables):\s*(0|1)\s*$/i) {
 	$opt = lc($1);
 	$value = int($2);
     } elsif ($line =~ m/^(log_level_in|log_level_out|tcp_flags_log_level|smurf_log_level):\s*(($loglevels)\s*)?$/i) {
@@ -4676,7 +4682,11 @@ sub remove_pvefw_chains_ebtables {
 sub init {
     my $cluster_conf = load_clusterfw_conf();
     my $cluster_options = $cluster_conf->{options};
-    my $enable = $cluster_options->{enable};
+
+    my $host_conf = load_hostfw_conf($cluster_conf);
+    my $host_options = $host_conf->{options};
+
+    my $enable = $cluster_options->{enable} && !$host_options->{nftables};
 
     return if !$enable;
 
@@ -4689,12 +4699,14 @@ sub update {
 	my $cluster_conf = load_clusterfw_conf();
 	my $cluster_options = $cluster_conf->{options};
 
-	if (!$cluster_options->{enable}) {
+	my $hostfw_conf = load_hostfw_conf($cluster_conf);
+	my $host_options = $hostfw_conf->{options};
+
+	if (!$cluster_options->{enable} || $host_options->{nftables}) {
 	    PVE::Firewall::remove_pvefw_chains();
 	    return;
 	}
 
-	my $hostfw_conf = load_hostfw_conf($cluster_conf);
 
 	my ($ruleset, $ipset_ruleset, $rulesetv6, $ebtables_ruleset) = compile($cluster_conf, $hostfw_conf);
 
-- 
2.39.2


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


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

* [pve-devel] [PATCH pve-manager v3 38/39] firewall: expose configuration option for new nftables firewall
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (36 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH pve-firewall v3 37/39] add configuration option for new nftables firewall Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 16:14 ` [pve-devel] [PATCH pve-docs v3 39/39] firewall: add documentation for proxmox-firewall Stefan Hanreich
  2024-04-18 20:05 ` [pve-devel] partially-applied-series: [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Thomas Lamprecht
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 www/manager6/grid/FirewallOptions.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/www/manager6/grid/FirewallOptions.js b/www/manager6/grid/FirewallOptions.js
index 0ac9979c4..6aacb47be 100644
--- a/www/manager6/grid/FirewallOptions.js
+++ b/www/manager6/grid/FirewallOptions.js
@@ -83,6 +83,7 @@ Ext.define('PVE.FirewallOptions', {
 	    add_log_row('log_level_out');
 	    add_log_row('tcp_flags_log_level', 120);
 	    add_log_row('smurf_log_level');
+	    add_boolean_row('nftables', gettext('nftables (tech preview)'), 0);
 	} else if (me.fwtype === 'vm') {
 	    me.rows.enable = {
 		required: true,
-- 
2.39.2


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


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

* [pve-devel] [PATCH pve-docs v3 39/39] firewall: add documentation for proxmox-firewall
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (37 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH pve-manager v3 38/39] firewall: expose " Stefan Hanreich
@ 2024-04-18 16:14 ` Stefan Hanreich
  2024-04-18 20:05 ` [pve-devel] partially-applied-series: [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Thomas Lamprecht
  39 siblings, 0 replies; 42+ messages in thread
From: Stefan Hanreich @ 2024-04-18 16:14 UTC (permalink / raw)
  To: pve-devel

Add a section that explains how to use the new nftables-based
proxmox-firewall.

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

diff --git a/pve-firewall.adoc b/pve-firewall.adoc
index a5e40f9..ec713ea 100644
--- a/pve-firewall.adoc
+++ b/pve-firewall.adoc
@@ -379,6 +379,7 @@ discovery protocol to work.
 ----
 
 
+[[pve_firewall_services_commands]]
 Services and Commands
 ---------------------
 
@@ -637,6 +638,190 @@ Ports used by {pve}
 * corosync cluster traffic: 5405-5412 UDP
 * live migration (VM memory and local-disk data): 60000-60050 (TCP)
 
+
+nftables
+--------
+
+As an alternative to `pve-firewall` we offer `proxmox-firewall`, which is an
+implementation of the Proxmox VE firewall based on the newer
+https://wiki.nftables.org/wiki-nftables/index.php/What_is_nftables%3F[nftables]
+rather than iptables.
+
+WARNING: `proxmox-firewall` is currently in tech preview. There might be bugs or
+incompatibilies with the original firewall. It is currently not suited for
+production use.
+
+This implementation uses the same configuration files and configuration format,
+so you can use your old configuration when switching. It provides the exact same
+functionality with a few exceptions:
+
+* REJECT is currently not possible for guest traffic (traffic will instead be
+  dropped).
+* Using the `NDP`, `Router Advertisement` or `DHCP` options will *always* create
+  firewall rules, irregardless of your default policy.
+* firewall rules for guests are evaluated even for connections that have
+  conntrack table entries.
+
+
+Installation and Usage
+~~~~~~~~~~~~~~~~~~~~~~
+
+Install the `proxmox-firewall` package:
+
+----
+apt install proxmox-firewall
+----
+
+Enable the nftables backend via the Web UI on your hosts (Host > Firewall >
+Options > nftables), or by enabling it in the configuration file for your hosts
+(`/etc/pve/nodes/<node_name>/host.fw`):
+
+----
+[OPTIONS]
+
+nftables: 1
+----
+
+WARNING: If you enable nftables without installing the `proxmox-firewall`
+package, then *no* firewall rules will be generated and your host and guests are
+left unprotected.
+
+NOTE: After enabling `proxmox-firewall`, all running VMs and containers need to
+be restarted for the new firewall to work.
+
+After setting the `nftables` configuration key, the new `proxmox-firewall`
+service will take over. You can check if the new service is working by 
+checking the systemctl status of `proxmox-firewall`:
+
+----
+systemctl status proxmox-firewall
+----
+
+You can also examine the generated ruleset. You can find more information about
+this in the section xref:pve_firewall_nft_helpful_commands[Helpful Commands].
+You should also check whether `pve-firewall` is no longer generating iptables
+rules, you can find the respective commands in the
+xref:pve_firewall_services_commands[Services and Commands] section.
+
+Switching back to the old firewall can be done by simply setting the
+configuration value back to 0 / No.
+
+Usage
+~~~~~
+
+`proxmox-firewall` will create two tables that are managed by the
+`proxmox-firewall` service: `proxmox-firewall` and `proxmox-firewall-guests`. If
+you want to create custom rules that live outside the Proxmox VE firewall
+configuration you can create your own tables to manage your custom firewall
+rules. `proxmox-firewall` will only touch the tables it generates, so you can
+easily extend and modify the behavior of the `proxmox-firewall` by adding your
+own tables.
+
+Instead of using the `pve-firewall` command, the nftables-based firewall uses
+`proxmox-firewall`. It is a systemd service, so you can start and stop it via
+`systemctl`:
+
+----
+systemctl start proxmox-firewall
+systemctl stop proxmox-firewall
+----
+
+Stopping the firewall service will remove all generated rules.
+
+To query the status of the firewall, you can query the status of the systemctl
+service:
+
+----
+systemctl status proxmox-firewall
+----
+
+
+[[pve_firewall_nft_helpful_commands]]
+Helpful Commands
+~~~~~~~~~~~~~~~~
+You can check the generated ruleset via the following command:
+
+----
+nft list ruleset
+----
+
+If you want to debug `proxmox-firewall` you can simply run the daemon in
+foreground with the `RUST_LOG` environment variable set to `trace`. This should
+provide you with detailed debugging output:
+
+----
+RUST_LOG=trace /usr/libexec/proxmox/proxmox-firewall
+----
+
+You can also edit the systemctl service if you want to have detailed output for
+your firewall daemon:
+
+----
+systemctl edit proxmox-firewall
+----
+
+Then you need to add the override for the `RUST_LOG` environment variable:
+
+----
+[Service]
+Environment="RUST_LOG=trace"
+----
+
+This will generate a large amount of logs very quickly, so only use this for
+debugging purposes. Other, less verbose, log levels are `info` and `debug`.
+
+Running in foreground writes the log output to STDERR, so you can redirect it
+with the following command (e.g. for submitting logs to the community forum):
+
+----
+RUST_LOG=trace /usr/libexec/proxmox/proxmox-firewall 2> firewall_log_$(hostname).txt
+----
+
+It can be helpful to trace packet flow through the different chains in order to
+debug firewall rules. This can be achieved by setting `nftrace` to 1 for packets
+that you want to track. It is advisable that you do not set this flag for *all*
+packets, in the example below we only examine ICMP packets.
+
+----
+#!/usr/sbin/nft -f
+table bridge tracebridge
+delete table bridge tracebridge
+
+table bridge tracebridge {
+    chain trace {
+        meta l4proto icmp meta nftrace set 1
+    }
+
+    chain prerouting {
+        type filter hook prerouting priority -350; policy accept;
+        jump trace
+    }
+
+    chain postrouting {
+        type filter hook postrouting priority -350; policy accept;
+        jump trace
+    }
+}
+----
+
+Saving this file, making it executable, and then running it once will create the
+respective tracing chains. You can then inspect the tracing output via the
+Proxmox VE Web UI (Firewall > Log) or via `nft monitor trace`.
+
+The above example traces traffic on all bridges, which is usually where guest
+traffic flows through. If you want to examine host traffic, create those chains
+in the `inet` table instead of the `bridge` table.
+
+NOTE: Be aware that this can generate a *lot* of log spam and slow down the
+performance of your networking stack significantly.
+
+You can remove the tracing rules via running the following command:
+
+----
+nft delete table bridge tracebridge
+----
+
+
 ifdef::manvolnum[]
 
 Macro Definitions
-- 
2.39.2


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


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

* [pve-devel] partially-applied-series: [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation
  2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
                   ` (38 preceding siblings ...)
  2024-04-18 16:14 ` [pve-devel] [PATCH pve-docs v3 39/39] firewall: add documentation for proxmox-firewall Stefan Hanreich
@ 2024-04-18 20:05 ` Thomas Lamprecht
  39 siblings, 0 replies; 42+ messages in thread
From: Thomas Lamprecht @ 2024-04-18 20:05 UTC (permalink / raw)
  To: Proxmox VE development discussion, Stefan Hanreich

Am 18/04/2024 um 18:13 schrieb Stefan Hanreich:
> proxmox-firewall:
> 
> Stefan Hanreich (34):
>   config: add proxmox-ve-config crate
>   config: firewall: add types for ip addresses
>   config: firewall: add types for ports
>   config: firewall: add types for log level and rate limit
>   config: firewall: add types for aliases
>   config: host: add helpers for host network configuration
>   config: guest: add helpers for parsing guest network config
>   config: firewall: add types for ipsets
>   config: firewall: add types for rules
>   config: firewall: add types for security groups
>   config: firewall: add generic parser for firewall configs
>   config: firewall: add cluster-specific config + option types
>   config: firewall: add host specific config + option types
>   config: firewall: add guest-specific config + option types
>   config: firewall: add firewall macros
>   config: firewall: add conntrack helper types
>   nftables: add crate for libnftables bindings
>   nftables: add helpers
>   nftables: expression: add types
>   nftables: expression: implement conversion traits for firewall config
>   nftables: statement: add types
>   nftables: statement: add conversion traits for config types
>   nftables: commands: add types
>   nftables: types: add conversion traits
>   nftables: add nft client
>   firewall: add firewall crate
>   firewall: add base ruleset
>   firewall: add config loader
>   firewall: add rule generation logic
>   firewall: add object generation logic
>   firewall: add ruleset generation logic
>   firewall: add proxmox-firewall binary and move existing code into lib
>   firewall: add files for debian packaging
>   firewall: add integration test
> 

applied above proxmox-firewall patches, thanks!

I squashed in some fixes into the packaging change and rebased the whole thing
to fix the git trailers order (that one should grow only downward, so the R-b
one go below your S-o-b as they came in later)

Also created public repos and uploaded a build to our internal repo.


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


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

* Re: [pve-devel] [PATCH qemu-server v3 35/39] firewall: add handling for new nft firewall
  2024-04-18 16:14 ` [pve-devel] [PATCH qemu-server v3 35/39] firewall: add handling for new nft firewall Stefan Hanreich
@ 2024-04-18 21:08   ` Thomas Lamprecht
  0 siblings, 0 replies; 42+ messages in thread
From: Thomas Lamprecht @ 2024-04-18 21:08 UTC (permalink / raw)
  To: Proxmox VE development discussion, Stefan Hanreich

Am 18/04/2024 um 18:14 schrieb Stefan Hanreich:
> When the nftables firewall is enabled, we do not need to create
> firewall bridges.
> 
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
>  vm-network-scripts/pve-bridge | 9 +++++++--
>  1 file changed, 7 insertions(+), 2 deletions(-)
> 
> diff --git a/vm-network-scripts/pve-bridge b/vm-network-scripts/pve-bridge
> index 85997a0..ac2eb3b 100755
> --- a/vm-network-scripts/pve-bridge
> +++ b/vm-network-scripts/pve-bridge
> @@ -6,6 +6,7 @@ use warnings;
>  use PVE::QemuServer;
>  use PVE::Tools qw(run_command);
>  use PVE::Network;
> +use PVE::Firewall;
>  
>  my $have_sdn;
>  eval {
> @@ -44,13 +45,17 @@ die "unable to get network config '$netid'\n"
>  my $net = PVE::QemuServer::parse_net($netconf);
>  die "unable to parse network config '$netid'\n" if !$net;
>  
> +my $cluster_fw_conf = PVE::Firewall::load_clusterfw_conf();
> +my $host_fw_conf = PVE::Firewall::load_hostfw_conf($cluster_fw_conf);
> +my $firewall = $net->{firewall} && !($host_fw_conf->{options}->{nftables} // 0);

we could add a helper for this in PVE::Firewall to make this and the container
one a bit shorter, while it's not much we'd have to bump firewall anyway, so
not a high cost to do.

> +
>  if ($have_sdn) {
>      PVE::Network::SDN::Vnets::add_dhcp_mapping($net->{bridge}, $net->{macaddr}, $vmid, $conf->{name});
>      PVE::Network::SDN::Zones::tap_create($iface, $net->{bridge});
> -    PVE::Network::SDN::Zones::tap_plug($iface, $net->{bridge}, $net->{tag}, $net->{firewall}, $net->{trunks}, $net->{rate});
> +    PVE::Network::SDN::Zones::tap_plug($iface, $net->{bridge}, $net->{tag}, $firewall, $net->{trunks}, $net->{rate});
>  } else {
>      PVE::Network::tap_create($iface, $net->{bridge});
> -    PVE::Network::tap_plug($iface, $net->{bridge}, $net->{tag}, $net->{firewall}, $net->{trunks}, $net->{rate});
> +    PVE::Network::tap_plug($iface, $net->{bridge}, $net->{tag}, $firewall, $net->{trunks}, $net->{rate});
>  }
>  
>  exit 0;



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


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

end of thread, other threads:[~2024-04-19  7:48 UTC | newest]

Thread overview: 42+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2024-04-18 16:13 [pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Stefan Hanreich
2024-04-18 16:13 ` [pve-devel] [PATCH proxmox-firewall v3 01/39] config: add proxmox-ve-config crate Stefan Hanreich
2024-04-18 16:13 ` [pve-devel] [PATCH proxmox-firewall v3 02/39] config: firewall: add types for ip addresses Stefan Hanreich
2024-04-18 16:13 ` [pve-devel] [PATCH proxmox-firewall v3 03/39] config: firewall: add types for ports Stefan Hanreich
2024-04-18 16:13 ` [pve-devel] [PATCH proxmox-firewall v3 04/39] config: firewall: add types for log level and rate limit Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 05/39] config: firewall: add types for aliases Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 06/39] config: host: add helpers for host network configuration Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 07/39] config: guest: add helpers for parsing guest network config Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 08/39] config: firewall: add types for ipsets Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 09/39] config: firewall: add types for rules Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 10/39] config: firewall: add types for security groups Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 11/39] config: firewall: add generic parser for firewall configs Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 12/39] config: firewall: add cluster-specific config + option types Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 13/39] config: firewall: add host specific " Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 14/39] config: firewall: add guest-specific " Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 15/39] config: firewall: add firewall macros Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 16/39] config: firewall: add conntrack helper types Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 17/39] nftables: add crate for libnftables bindings Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 18/39] nftables: add helpers Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 19/39] nftables: expression: add types Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 20/39] nftables: expression: implement conversion traits for firewall config Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 21/39] nftables: statement: add types Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 22/39] nftables: statement: add conversion traits for config types Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 23/39] nftables: commands: add types Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 24/39] nftables: types: add conversion traits Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 25/39] nftables: add nft client Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 26/39] firewall: add firewall crate Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 27/39] firewall: add base ruleset Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 28/39] firewall: add config loader Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 29/39] firewall: add rule generation logic Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 30/39] firewall: add object " Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 31/39] firewall: add ruleset " Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 32/39] firewall: add proxmox-firewall binary and move existing code into lib Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 33/39] firewall: add files for debian packaging Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH proxmox-firewall v3 34/39] firewall: add integration test Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH qemu-server v3 35/39] firewall: add handling for new nft firewall Stefan Hanreich
2024-04-18 21:08   ` Thomas Lamprecht
2024-04-18 16:14 ` [pve-devel] [PATCH pve-container v3 36/39] " Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH pve-firewall v3 37/39] add configuration option for new nftables firewall Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH pve-manager v3 38/39] firewall: expose " Stefan Hanreich
2024-04-18 16:14 ` [pve-devel] [PATCH pve-docs v3 39/39] firewall: add documentation for proxmox-firewall Stefan Hanreich
2024-04-18 20:05 ` [pve-devel] partially-applied-series: [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation Thomas Lamprecht

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