[pve-devel] [PATCH proxmox-firewall 1/1] rules: allow vital ICMP(v6) types

2024-06-10 Thread Stefan Hanreich
There are certain ICMP messages that should always pass through a
firewall irregardless of any other rules. This is particularly
important for ICMPv6. While we already handled NDP, there are certain
control messages that should always be able to pass through any
firewall, according to RFC 4890.

For ICMP we additionally allow 'Source Quench' as well.

Signed-off-by: Stefan Hanreich 
---
While Source Quench is deprecated, there might be niche use cases
using it and allowing it shouldn't really hurt so I've thrown it into
the mix as well.

 .../resources/proxmox-firewall.nft| 22 +--
 1 file changed, 20 insertions(+), 2 deletions(-)

diff --git a/proxmox-firewall/resources/proxmox-firewall.nft 
b/proxmox-firewall/resources/proxmox-firewall.nft
index 537ba88..ea2cd7d 100644
--- a/proxmox-firewall/resources/proxmox-firewall.nft
+++ b/proxmox-firewall/resources/proxmox-firewall.nft
@@ -16,6 +16,7 @@ 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 allow-icmp
 add chain inet proxmox-firewall log-drop-smurfs
 add chain inet proxmox-firewall default-in
 add chain inet proxmox-firewall default-out
@@ -32,6 +33,7 @@ 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 allow-icmp
 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;}
@@ -47,6 +49,7 @@ 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 allow-icmp
 flush chain inet proxmox-firewall log-drop-smurfs
 flush chain inet proxmox-firewall default-in
 flush chain inet proxmox-firewall default-out
@@ -63,6 +66,7 @@ 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 allow-icmp
 flush chain bridge proxmox-firewall-guests do-reject
 flush chain bridge proxmox-firewall-guests vm-out
 flush chain bridge proxmox-firewall-guests vm-in
@@ -175,9 +179,16 @@ table inet proxmox-firewall {
 drop
 }
 
+chain allow-icmp {
+icmp type { destination-unreachable, source-quench, time-exceeded } 
accept
+# based on RFC 4890 - NDP is handled separately
+icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, 
parameter-problem } accept
+}
+
 chain default-in {
 iifname "lo" accept
 
+jump allow-icmp
 ct state related,established accept
 
 meta l4proto igmp accept
@@ -185,8 +196,6 @@ table inet proxmox-firewall {
 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
@@ -203,6 +212,7 @@ table inet proxmox-firewall {
 chain default-out {
 oifname "lo" accept
 
+jump allow-icmp
 ct state vmap { invalid : drop, established : accept, related : accept 
}
 }
 
@@ -284,6 +294,12 @@ table bridge proxmox-firewall-guests {
 icmpv6 type { nd-router-advert, nd-redirect } drop
 }
 
+chain allow-icmp {
+icmp type { destination-unreachable, source-quench, time-exceeded } 
accept
+# based on RFC 4890 - NDP is handled separately
+icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, 
parameter-problem } accept
+}
+
 chain do-reject {
 meta pkttype broadcast drop
 ip saddr 224.0.0.0/4 drop
@@ -297,12 +313,14 @@ table bridge proxmox-firewall-guests {
 
 chain vm-out {
 type filter hook prerouting priority 0; policy accept;
+jump allow-icmp
 ether type != arp ct state vmap { established : accept, related : 
accept, invalid : drop }
 iifname vmap @vm-map-out
 }
 
 chain vm-in {
 type filter hook postrouting priority 0; policy accept;
+jump allow-icmp
 ether type != arp ct state vmap { established : accept, related : 
accept, invalid : drop }
 ether type arp accept
 oifname vmap @

[pve-devel] [PATCH proxmox-firewall 1/1] service: flush firewall rules on force disable

2024-05-29 Thread Stefan Hanreich
When disabling the nftables firewall again, there is a race condition
where the nftables ruleset never gets flushed and persists after
disabling. In practice this almost never happens due to pve-firewall
running every 10 seconds, and proxmox-firewall running every 5
seconds, so the proxmox-firewall main loop almost always runs at least
once before the force disable file gets created and flushes the
ruleset.

Reported-by: Hannes Laimer 
Signed-off-by: Stefan Hanreich 
---
 proxmox-firewall/src/bin/proxmox-firewall.rs | 4 
 1 file changed, 4 insertions(+)

diff --git a/proxmox-firewall/src/bin/proxmox-firewall.rs 
b/proxmox-firewall/src/bin/proxmox-firewall.rs
index f7e816e..5133cbf 100644
--- a/proxmox-firewall/src/bin/proxmox-firewall.rs
+++ b/proxmox-firewall/src/bin/proxmox-firewall.rs
@@ -91,6 +91,10 @@ fn main() -> Result<(), std::io::Error> {
 
 while !term.load(Ordering::Relaxed) {
 if force_disable_flag.exists() {
+if let Err(error) = remove_firewall() {
+log::error!("unable to disable firewall: {error:#}");
+}
+
 std::thread::sleep(Duration::from_secs(5));
 continue;
 }
-- 
2.39.2


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



[pve-devel] [PATCH proxmox-firewall 1/2] firewall: improve handling of ARP traffic for guests

2024-05-15 Thread Stefan Hanreich
In order to be able to send outgoing ARP packets when the default
policy is set to drop or reject, we need to explicitly allow ARP
traffic in the outgoing chain of guests. We need to do this in the
guest chain itself in order to be able to filter spoofed packets via
the MAC filter.

Contrary to the out direction we can simply accept all incoming ARP
traffic, since we do not do any MAC filtering for incoming traffic.
Since we create fdb entries for every NIC, guests should only see ARP
traffic for their MAC addresses anyway.

Signed-off-by: Stefan Hanreich 
Originally-by: Laurent Guerby 
---
 proxmox-firewall/resources/proxmox-firewall.nft   | 1 +
 proxmox-firewall/src/firewall.rs  | 8 
 .../tests/snapshots/integration_tests__firewall.snap  | 4 ++--
 3 files changed, 7 insertions(+), 6 deletions(-)

diff --git a/proxmox-firewall/resources/proxmox-firewall.nft 
b/proxmox-firewall/resources/proxmox-firewall.nft
index f36bf3b..90b5d5a 100644
--- a/proxmox-firewall/resources/proxmox-firewall.nft
+++ b/proxmox-firewall/resources/proxmox-firewall.nft
@@ -305,6 +305,7 @@ table bridge proxmox-firewall-guests {
 
 chain vm-in {
 type filter hook postrouting priority 0; policy accept;
+ether type arp accept
 oifname vmap @vm-map-in
 }
 }
diff --git a/proxmox-firewall/src/firewall.rs b/proxmox-firewall/src/firewall.rs
index 41b1df2..0da3ab7 100644
--- a/proxmox-firewall/src/firewall.rs
+++ b/proxmox-firewall/src/firewall.rs
@@ -516,7 +516,7 @@ impl Firewall {
 
 commands.append( vec![
 Add::rule(AddRule::from_statement(
-chain_in.clone(),
+chain_in,
 Statement::jump(ndp_chains.0),
 )),
 Add::rule(AddRule::from_statement(
@@ -532,17 +532,17 @@ impl Firewall {
 };
 
 commands.push(Add::rule(AddRule::from_statement(
-chain_out,
+chain_out.clone(),
 Statement::jump(ra_chain_out),
 )));
 
-// we allow incoming ARP by default, except if blocked by any option 
above
+// we allow outgoing ARP, except if blocked by the MAC filter 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)));
+commands.push(Add::rule(AddRule::from_statements(chain_out, 
arp_rule)));
 
 Ok(())
 }
diff --git a/proxmox-firewall/tests/snapshots/integration_tests__firewall.snap 
b/proxmox-firewall/tests/snapshots/integration_tests__firewall.snap
index 092ccef..2ca151f 100644
--- a/proxmox-firewall/tests/snapshots/integration_tests__firewall.snap
+++ b/proxmox-firewall/tests/snapshots/integration_tests__firewall.snap
@@ -2923,7 +2923,7 @@ expression: "firewall.full_host_fw().expect(\"firewall 
can be generated\")"
 "rule": {
   "family": "bridge",
   "table": "proxmox-firewall-guests",
-  "chain": "guest-100-in",
+  "chain": "guest-100-out",
   "expr": [
 {
   "match": {
@@ -3569,7 +3569,7 @@ expression: "firewall.full_host_fw().expect(\"firewall 
can be generated\")"
 "rule": {
   "family": "bridge",
   "table": "proxmox-firewall-guests",
-  "chain": "guest-101-in",
+  "chain": "guest-101-out",
   "expr": [
 {
   "match": {
-- 
2.39.2


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



[pve-devel] [PATCH proxmox-firewall 2/2] firewall: improve conntrack handling

2024-05-15 Thread Stefan Hanreich
The output chain did not have any conntrack rules, which lead to
issues when the default output policy is not accept. Also, move the
conntrack rules to the beginning of all chains.

Signed-off-by: Stefan Hanreich 
Originally-by: Laurent Guerby 
---
Based this on the earlier patch in order to avoid conflicts when
applying both patches.

 .../resources/proxmox-firewall.nft|  9 ++
 proxmox-firewall/src/firewall.rs  |  7 
 .../integration_tests__firewall.snap  | 32 ---
 3 files changed, 2 insertions(+), 46 deletions(-)

diff --git a/proxmox-firewall/resources/proxmox-firewall.nft 
b/proxmox-firewall/resources/proxmox-firewall.nft
index 90b5d5a..411e143 100644
--- a/proxmox-firewall/resources/proxmox-firewall.nft
+++ b/proxmox-firewall/resources/proxmox-firewall.nft
@@ -32,7 +32,6 @@ 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;}
@@ -64,7 +63,6 @@ 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
@@ -293,18 +291,15 @@ table bridge proxmox-firewall-guests {
 reject with icmp type host-prohibited
 }
 
-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;
+ether type != arp ct state vmap { established : accept, related : 
accept, invalid : drop }
 iifname vmap @vm-map-out
 }
 
 chain vm-in {
 type filter hook postrouting priority 0; policy accept;
+ether type != arp ct state vmap { established : accept, related : 
accept, invalid : drop }
 ether type arp accept
 oifname vmap @vm-map-in
 }
diff --git a/proxmox-firewall/src/firewall.rs b/proxmox-firewall/src/firewall.rs
index 0da3ab7..4c85ea2 100644
--- a/proxmox-firewall/src/firewall.rs
+++ b/proxmox-firewall/src/firewall.rs
@@ -810,13 +810,6 @@ impl Firewall {
 )));
 }
 
-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),
diff --git a/proxmox-firewall/tests/snapshots/integration_tests__firewall.snap 
b/proxmox-firewall/tests/snapshots/integration_tests__firewall.snap
index 2ca151f..669bad9 100644
--- a/proxmox-firewall/tests/snapshots/integration_tests__firewall.snap
+++ b/proxmox-firewall/tests/snapshots/integration_tests__firewall.snap
@@ -3181,22 +3181,6 @@ expression: "firewall.full_host_fw().expect(\"firewall 
can be generated\")"
 }
   }
 },
-{
-  "add": {
-"rule": {
-  "family": "bridge",
-  "table": "proxmox-firewall-guests",
-  "chain": "guest-100-in",
-  "expr": [
-{
-  "jump": {
-"target": "after-vm-in"
-  }
-}
-  ]
-}
-  }
-},
 {
   "add": {
 "rule": {
@@ -3638,22 +3622,6 @@ expression: "firewall.full_host_fw().expect(\"firewall 
can be generated\")"
 }
   }
 },
-{
-  "add": {
-"rule": {
-  "family": "bridge",
-  "table": "proxmox-firewall-guests",
-  "chain": "guest-101-in",
-  "expr": [
-{
-  "jump": {
-"target": "after-vm-in"
-  }
-}
-  ]
-}
-  }
-},
 {
   "add": {
 "rule": {
-- 
2.39.2


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



Re: [pve-devel] [PATCH proxmox-firewall 1/1] firewall: properly reject ipv6 traffic

2024-05-13 Thread Stefan Hanreich
v2 available:

https://lists.proxmox.com/pipermail/pve-devel/2024-May/063839.html

On 5/13/24 13:35, Stefan Hanreich wrote:
> ICMPv6 has different message types for rejecting traffic. With ICMP we
> used host-prohibited as rejection type, which doesn't exist in ICMPv6.
> Add an additional rule for IPv6, so it uses admin-prohibited.
> 
> Signed-off-by: Stefan Hanreich 
> ---
>  proxmox-firewall/resources/proxmox-firewall.nft | 6 --
>  1 file changed, 4 insertions(+), 2 deletions(-)
> 
> diff --git a/proxmox-firewall/resources/proxmox-firewall.nft 
> b/proxmox-firewall/resources/proxmox-firewall.nft
> index f36bf3b..0a220bf 100644
> --- a/proxmox-firewall/resources/proxmox-firewall.nft
> +++ b/proxmox-firewall/resources/proxmox-firewall.nft
> @@ -75,8 +75,9 @@ table inet proxmox-firewall {
>  ip saddr 224.0.0.0/4 drop
>  
>  meta l4proto tcp reject with tcp reset
> -meta l4proto icmp reject with icmp type port-unreachable
> +meta l4proto icmp reject with icmpx type port-unreachable
>  reject with icmp type host-prohibited
> +reject with icmpv6 type admin-prohibited
>  }
>  
>  set v4-dc/management {
> @@ -289,8 +290,9 @@ table bridge proxmox-firewall-guests {
>  ip saddr 224.0.0.0/4 drop
>  
>  meta l4proto tcp reject with tcp reset
> -meta l4proto icmp reject with icmp type port-unreachable
> +meta l4proto icmp reject with icmpx type port-unreachable
>  reject with icmp type host-prohibited
> +reject with icmpv6 type admin-prohibited
>  }
>  
>  chain after-vm-in {


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



[pve-devel] [PATCH proxmox-firewall v2 1/1] firewall: properly reject ipv6 traffic

2024-05-13 Thread Stefan Hanreich
ICMPv6 has different message types for rejecting traffic. With ICMP we
used host-prohibited as rejection type, which doesn't exist in ICMPv6.
Add an additional rule for IPv6, so it uses admin-prohibited.

Additionally, add a terminal drop statement in order to prevent any
traffic that does not get matched from bypassing the reject chain.

Signed-off-by: Stefan Hanreich 
---
Changes from v1 -> v2:
* add a terminal drop statement to prevent any unmatched traffic from
  bypassing the reject chain
* properly match ICMPv6 traffic via l4proto

 proxmox-firewall/resources/proxmox-firewall.nft | 8 ++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/proxmox-firewall/resources/proxmox-firewall.nft 
b/proxmox-firewall/resources/proxmox-firewall.nft
index f36bf3b..f60f8b5 100644
--- a/proxmox-firewall/resources/proxmox-firewall.nft
+++ b/proxmox-firewall/resources/proxmox-firewall.nft
@@ -75,8 +75,10 @@ table inet proxmox-firewall {
 ip saddr 224.0.0.0/4 drop
 
 meta l4proto tcp reject with tcp reset
-meta l4proto icmp reject with icmp type port-unreachable
+meta l4proto { icmp, ipv6-icmp } reject with icmpx type 
port-unreachable
 reject with icmp type host-prohibited
+reject with icmpv6 type admin-prohibited
+drop
 }
 
 set v4-dc/management {
@@ -289,8 +291,10 @@ table bridge proxmox-firewall-guests {
 ip saddr 224.0.0.0/4 drop
 
 meta l4proto tcp reject with tcp reset
-meta l4proto icmp reject with icmp type port-unreachable
+meta l4proto { icmp, ipv6-icmp } reject with icmpx type 
port-unreachable
 reject with icmp type host-prohibited
+reject with icmpv6 type admin-prohibited
+drop
 }
 
 chain after-vm-in {
-- 
2.39.2


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



[pve-devel] [PATCH proxmox-firewall 1/1] firewall: properly reject ipv6 traffic

2024-05-13 Thread Stefan Hanreich
ICMPv6 has different message types for rejecting traffic. With ICMP we
used host-prohibited as rejection type, which doesn't exist in ICMPv6.
Add an additional rule for IPv6, so it uses admin-prohibited.

Signed-off-by: Stefan Hanreich 
---
 proxmox-firewall/resources/proxmox-firewall.nft | 6 --
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/proxmox-firewall/resources/proxmox-firewall.nft 
b/proxmox-firewall/resources/proxmox-firewall.nft
index f36bf3b..0a220bf 100644
--- a/proxmox-firewall/resources/proxmox-firewall.nft
+++ b/proxmox-firewall/resources/proxmox-firewall.nft
@@ -75,8 +75,9 @@ table inet proxmox-firewall {
 ip saddr 224.0.0.0/4 drop
 
 meta l4proto tcp reject with tcp reset
-meta l4proto icmp reject with icmp type port-unreachable
+meta l4proto icmp reject with icmpx type port-unreachable
 reject with icmp type host-prohibited
+reject with icmpv6 type admin-prohibited
 }
 
 set v4-dc/management {
@@ -289,8 +290,9 @@ table bridge proxmox-firewall-guests {
 ip saddr 224.0.0.0/4 drop
 
 meta l4proto tcp reject with tcp reset
-meta l4proto icmp reject with icmp type port-unreachable
+meta l4proto icmp reject with icmpx type port-unreachable
 reject with icmp type host-prohibited
+reject with icmpv6 type admin-prohibited
 }
 
 chain after-vm-in {
-- 
2.39.2


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



[pve-devel] [PATCH proxmox-firewall] firewall: improve error handling of firewall

2024-04-25 Thread Stefan Hanreich
Error handling of the firewall binary should now be much more robust
on configuration errors. Instead of panicking in some cases it should
now log an error.

Signed-off-by: Stefan Hanreich 
---
 proxmox-firewall/src/bin/proxmox-firewall.rs |   7 +-
 proxmox-firewall/src/config.rs   | 239 +--
 proxmox-firewall/src/firewall.rs |   7 +-
 proxmox-firewall/tests/integration_tests.rs  |  51 ++--
 4 files changed, 155 insertions(+), 149 deletions(-)

diff --git a/proxmox-firewall/src/bin/proxmox-firewall.rs 
b/proxmox-firewall/src/bin/proxmox-firewall.rs
index 4e07993..b61007d 100644
--- a/proxmox-firewall/src/bin/proxmox-firewall.rs
+++ b/proxmox-firewall/src/bin/proxmox-firewall.rs
@@ -5,6 +5,7 @@ use std::time::{Duration, Instant};
 
 use anyhow::{Context, Error};
 
+use proxmox_firewall::config::{FirewallConfig, PveFirewallConfigLoader, 
PveNftConfigLoader};
 use proxmox_firewall::firewall::Firewall;
 use proxmox_nftables::{client::NftError, NftClient};
 
@@ -24,7 +25,9 @@ fn remove_firewall() -> Result<(), std::io::Error> {
 }
 
 fn handle_firewall() -> Result<(), Error> {
-let firewall = Firewall::new();
+let config = FirewallConfig::new(::new(), 
::new())?;
+
+let firewall = Firewall::new(config);
 
 if !firewall.is_enabled() {
 return remove_firewall().with_context(|| "could not remove firewall 
tables".to_string());
@@ -84,7 +87,7 @@ fn main() -> Result<(), std::io::Error> {
 let start = Instant::now();
 
 if let Err(error) = handle_firewall() {
-log::error!("error creating firewall rules: {error}");
+log::error!("error updating firewall rules: {error}");
 }
 
 let duration = start.elapsed();
diff --git a/proxmox-firewall/src/config.rs b/proxmox-firewall/src/config.rs
index 2cf3e39..5bd2512 100644
--- a/proxmox-firewall/src/config.rs
+++ b/proxmox-firewall/src/config.rs
@@ -2,9 +2,8 @@ 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 anyhow::{format_err, Context, Error};
 
 use proxmox_ve_config::firewall::cluster::Config as ClusterConfig;
 use proxmox_ve_config::firewall::guest::Config as GuestConfig;
@@ -19,15 +18,19 @@ use proxmox_nftables::types::ListChain;
 use proxmox_nftables::NftClient;
 
 pub trait FirewallConfigLoader {
-fn cluster() -> Option>;
-fn host() -> Option>;
-fn guest_list() -> GuestMap;
-fn guest_config(, vmid: , guest: ) -> Option>;
-fn guest_firewall_config(, vmid: ) -> Option>;
+fn cluster() -> Result>, Error>;
+fn host() -> Result>, Error>;
+fn guest_list() -> Result;
+fn guest_config(
+,
+vmid: ,
+guest: ,
+) -> Result>, Error>;
+fn guest_firewall_config(, vmid: ) -> Result>, Error>;
 }
 
 #[derive(Default)]
-struct PveFirewallConfigLoader {}
+pub struct PveFirewallConfigLoader {}
 
 impl PveFirewallConfigLoader {
 pub fn new() -> Self {
@@ -56,69 +59,70 @@ const CLUSTER_CONFIG_PATH:  = 
"/etc/pve/firewall/cluster.fw";
 const HOST_CONFIG_PATH:  = "/etc/pve/local/host.fw";
 
 impl FirewallConfigLoader for PveFirewallConfigLoader {
-fn cluster() -> Option> {
+fn cluster() -> Result>, Error> {
 log::info!("loading cluster config");
 
-let fd =
-open_config_file(CLUSTER_CONFIG_PATH).expect("able to read cluster 
firewall config");
+let fd = open_config_file(CLUSTER_CONFIG_PATH)?;
 
 if let Some(file) = fd {
 let buf_reader = Box::new(BufReader::new(file)) as Box;
-return Some(buf_reader);
+return Ok(Some(buf_reader));
 }
 
-None
+Ok(None)
 }
 
-fn host() -> Option> {
+fn host() -> Result>, Error> {
 log::info!("loading host config");
 
-let fd = open_config_file(HOST_CONFIG_PATH).expect("able to read host 
firewall config");
+let fd = open_config_file(HOST_CONFIG_PATH)?;
 
 if let Some(file) = fd {
 let buf_reader = Box::new(BufReader::new(file)) as Box;
-return Some(buf_reader);
+return Ok(Some(buf_reader));
 }
 
-None
+Ok(None)
 }
 
-fn guest_list() -> GuestMap {
+fn guest_list() -> Result {
 log::info!("loading vmlist");
-GuestMap::new().expect("able to read vmlist")
+GuestMap::new()
 }
 
-fn guest_config(, vmid: , entry: ) -> Option> {
+fn guest_config(
+,
+vmid: ,
+entry: ,
+) -> Result>, Error> {
 log::info!("loading guest #{vmid} config");
 
-let fd = open_config_file(::config_path(vmid, entry))
-  

[pve-devel] [PATCH proxmox-firewall] config: nftables: add support for icmp-type any

2024-04-25 Thread Stefan Hanreich
We support any as wildcard for matching all icmp types. Implement
parsing logic for parsing the any value and support converting the any
value into an nftables expression.

Signed-off-by: Stefan Hanreich 
---
 proxmox-nftables/src/expression.rs |  2 ++
 proxmox-ve-config/src/firewall/types/rule_match.rs | 12 
 2 files changed, 14 insertions(+)

diff --git a/proxmox-nftables/src/expression.rs 
b/proxmox-nftables/src/expression.rs
index 20559e8..18b92d4 100644
--- a/proxmox-nftables/src/expression.rs
+++ b/proxmox-nftables/src/expression.rs
@@ -185,6 +185,7 @@ impl From<> for Expression {
 match value {
 IcmpType::Numeric(id) => Expression::from(*id),
 IcmpType::Named(name) => Expression::from(*name),
+IcmpType::Any => Expression::Range(Box::new((u8::MIN.into(), 
u8::MAX.into(,
 }
 }
 }
@@ -205,6 +206,7 @@ impl From<> for Expression {
 match value {
 Icmpv6Type::Numeric(id) => Expression::from(*id),
 Icmpv6Type::Named(name) => Expression::from(*name),
+Icmpv6Type::Any => Expression::Range(Box::new((u8::MIN.into(), 
u8::MAX.into(,
 }
 }
 }
diff --git a/proxmox-ve-config/src/firewall/types/rule_match.rs 
b/proxmox-ve-config/src/firewall/types/rule_match.rs
index 948b426..94d8624 100644
--- a/proxmox-ve-config/src/firewall/types/rule_match.rs
+++ b/proxmox-ve-config/src/firewall/types/rule_match.rs
@@ -511,6 +511,7 @@ impl FromStr for Icmp {
 pub enum IcmpType {
 Numeric(u8),
 Named(&'static str),
+Any,
 }
 
 #[sortable]
@@ -536,6 +537,10 @@ impl std::str::FromStr for IcmpType {
 type Err = Error;
 
 fn from_str(s: ) -> Result {
+if s.eq_ignore_ascii_case("any") {
+return Ok(Self::Any);
+}
+
 if let Ok(ty) = s.trim().parse::() {
 return Ok(Self::Numeric(ty));
 }
@@ -553,6 +558,7 @@ impl fmt::Display for IcmpType {
 match self {
 IcmpType::Numeric(ty) => write!(f, "{ty}"),
 IcmpType::Named(ty) => write!(f, "{ty}"),
+IcmpType::Any => write!(f, "any"),
 }
 }
 }
@@ -664,6 +670,7 @@ impl FromStr for Icmpv6 {
 pub enum Icmpv6Type {
 Numeric(u8),
 Named(&'static str),
+Any,
 }
 
 #[sortable]
@@ -693,6 +700,10 @@ impl std::str::FromStr for Icmpv6Type {
 type Err = Error;
 
 fn from_str(s: ) -> Result {
+if s.eq_ignore_ascii_case("any") {
+return Ok(Self::Any);
+}
+
 if let Ok(ty) = s.trim().parse::() {
 return Ok(Self::Numeric(ty));
 }
@@ -710,6 +721,7 @@ impl fmt::Display for Icmpv6Type {
 match self {
 Icmpv6Type::Numeric(ty) => write!(f, "{ty}"),
 Icmpv6Type::Named(ty) => write!(f, "{ty}"),
+Icmpv6Type::Any => write!(f, "any"),
 }
 }
 }
-- 
2.39.2


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



[pve-devel] [PATCH proxmox-firewall] config: macros: add SPICEproxy macro

2024-04-25 Thread Stefan Hanreich
Signed-off-by: Stefan Hanreich 
---
 proxmox-ve-config/resources/macros.json | 9 +
 1 file changed, 9 insertions(+)

diff --git a/proxmox-ve-config/resources/macros.json 
b/proxmox-ve-config/resources/macros.json
index 67e1d89..2fcc0fb 100644
--- a/proxmox-ve-config/resources/macros.json
+++ b/proxmox-ve-config/resources/macros.json
@@ -735,6 +735,15 @@
 ],
 "desc": "Spam Assassin SPAMD traffic"
   },
+  "SPICEproxy": {
+"code": [
+  {
+"dport": "3128",
+"proto": "tcp"
+  }
+],
+"desc": "Proxmox VE SPICE display proxy traffic"
+  },
   "SSH": {
 "code": [
   {
-- 
2.39.2


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



[pve-devel] [PATCH proxmox-firewall] fix #5410: config: fix naming scheme for names in firewall config

2024-04-24 Thread Stefan Hanreich
This should bring the allowed names on par with the pve-firewall
naming scheme [1].

[1] 
https://git.proxmox.com/?p=pve-firewall.git;a=blob;f=src/PVE/Firewall.pm;h=0abfeccffc94cec940760e69a894e392dc33f151;hb=29b48c381d14bf425232dc65c9c0d18f95c8f222#l51

Signed-off-by: Stefan Hanreich 
---
 proxmox-ve-config/src/firewall/parse.rs   |  8 +++-
 proxmox-ve-config/src/firewall/types/alias.rs | 14 ++
 2 files changed, 21 insertions(+), 1 deletion(-)

diff --git a/proxmox-ve-config/src/firewall/parse.rs 
b/proxmox-ve-config/src/firewall/parse.rs
index 93cf014..7bf00c0 100644
--- a/proxmox-ve-config/src/firewall/parse.rs
+++ b/proxmox-ve-config/src/firewall/parse.rs
@@ -2,6 +2,8 @@ use std::fmt;
 
 use anyhow::{bail, format_err, Error};
 
+const NAME_SPECIAL_CHARACTERS: [u8; 2] = [b'-', b'_'];
+
 /// Parses out a "name" which can be alphanumeric and include dashes.
 ///
 /// Returns `None` if the name part would be empty.
@@ -16,10 +18,14 @@ use anyhow::{bail, format_err, Error};
 /// assert_eq!(match_name(" someremainder"), None);
 /// ```
 pub fn match_name(line: ) -> Option<(, )> {
+if !line.starts_with(|c: char| c.is_ascii_alphabetic()) {
+return None;
+}
+
 let end = line
 .as_bytes()
 .iter()
-.position(|| !(b.is_ascii_alphanumeric() || b == b'-'));
+.position(|| !(b.is_ascii_alphanumeric() || 
NAME_SPECIAL_CHARACTERS.contains()));
 
 let (name, rest) = match end {
 Some(end) => line.split_at(end),
diff --git a/proxmox-ve-config/src/firewall/types/alias.rs 
b/proxmox-ve-config/src/firewall/types/alias.rs
index 43c6486..e6aa30d 100644
--- a/proxmox-ve-config/src/firewall/types/alias.rs
+++ b/proxmox-ve-config/src/firewall/types/alias.rs
@@ -147,6 +147,20 @@ impl FromStr for Alias {
 mod tests {
 use super::*;
 
+#[test]
+fn test_parse_alias() {
+for alias in [
+"local_network 10.0.0.0/32",
+"test-_123-___-a 10.0.0.1/32",
+] {
+alias.parse::().expect("valid alias");
+}
+
+for alias in ["-- 10.0.0.1/32", "0asd 10.0.0.1/32", "__test 
10.0.0.0/32"] {
+alias.parse::().expect_err("invalid alias");
+}
+}
+
 #[test]
 fn test_parse_alias_name() {
 for name in ["dc/proxmox_123", "guest/proxmox-123"] {
-- 
2.39.2


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



Re: [pve-devel] [PATCH proxmox-firewall] firewall: properly handle REJECT rules

2024-04-23 Thread Stefan Hanreich
On 4/23/24 18:02, Stefan Hanreich wrote:
> Currently we generated DROP statements for all rules involving REJECT.
> We only need to generate DROP when in the postrouting chain of tables
> with type bridge, since REJECT is disallowed there. Otherwise we jump
> into the do-reject chain which properly handles rejects for different
> protocol types.
> 
> Signed-off-by: Stefan Hanreich 

Forgot trailer:

Reported-By: Stefan Sterz 


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



[pve-devel] [PATCH proxmox-firewall] firewall: properly handle REJECT rules

2024-04-23 Thread Stefan Hanreich
Currently we generated DROP statements for all rules involving REJECT.
We only need to generate DROP when in the postrouting chain of tables
with type bridge, since REJECT is disallowed there. Otherwise we jump
into the do-reject chain which properly handles rejects for different
protocol types.

Signed-off-by: Stefan Hanreich 
---
Seems like the proper handling for this got lost somewhere during my
big refactoring :/

 .../resources/proxmox-firewall.nft|   7 +-
 proxmox-firewall/src/firewall.rs  |   9 +-
 proxmox-firewall/src/rule.rs  |  22 ++-
 proxmox-firewall/tests/input/100.fw   |   2 +
 proxmox-firewall/tests/input/host.fw  |   2 +
 .../integration_tests__firewall.snap  | 158 +-
 proxmox-nftables/src/statement.rs |   6 +-
 7 files changed, 197 insertions(+), 9 deletions(-)

diff --git a/proxmox-firewall/resources/proxmox-firewall.nft 
b/proxmox-firewall/resources/proxmox-firewall.nft
index 67dd8c8..f36bf3b 100644
--- a/proxmox-firewall/resources/proxmox-firewall.nft
+++ b/proxmox-firewall/resources/proxmox-firewall.nft
@@ -285,7 +285,12 @@ table bridge proxmox-firewall-guests {
 }
 
 chain do-reject {
-drop
+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
 }
 
 chain after-vm-in {
diff --git a/proxmox-firewall/src/firewall.rs b/proxmox-firewall/src/firewall.rs
index b137f58..509e295 100644
--- a/proxmox-firewall/src/firewall.rs
+++ b/proxmox-firewall/src/firewall.rs
@@ -28,7 +28,7 @@ use proxmox_ve_config::guest::types::Vmid;
 
 use crate::config::FirewallConfig;
 use crate::object::{NftObjectEnv, ToNftObjects};
-use crate::rule::{NftRule, NftRuleEnv};
+use crate::rule::{generate_verdict, NftRule, NftRuleEnv};
 
 static CLUSTER_TABLE_NAME:  = "proxmox-firewall";
 static HOST_TABLE_NAME:  = "proxmox-firewall";
@@ -715,7 +715,10 @@ impl Firewall {
 None,
 )?;
 
-commands.push(Add::rule(AddRule::from_statement(chain, 
default_policy)));
+commands.push(Add::rule(AddRule::from_statement(
+chain,
+generate_verdict(default_policy, ),
+)));
 
 Ok(())
 }
@@ -827,7 +830,7 @@ impl Firewall {
 
 commands.push(Add::rule(AddRule::from_statement(
 chain,
-config.default_policy(direction),
+generate_verdict(config.default_policy(direction), ),
 )));
 
 Ok(())
diff --git a/proxmox-firewall/src/rule.rs b/proxmox-firewall/src/rule.rs
index c8099d0..02f964e 100644
--- a/proxmox-firewall/src/rule.rs
+++ b/proxmox-firewall/src/rule.rs
@@ -4,7 +4,7 @@ use anyhow::{format_err, Error};
 use proxmox_nftables::{
 expression::{Ct, IpFamily, Meta, Payload, Prefix},
 statement::{Log, LogLevel, Match, Operator},
-types::{AddRule, ChainPart, SetName},
+types::{AddRule, ChainPart, SetName, TableFamily, TablePart},
 Expression, Statement,
 };
 use proxmox_ve_config::{
@@ -16,7 +16,7 @@ use proxmox_ve_config::{
 alias::AliasName,
 ipset::{Ipfilter, IpsetName},
 log::LogRateLimit,
-rule::{Direction, Kind, RuleGroup},
+rule::{Direction, Kind, RuleGroup, Verdict as ConfigVerdict},
 rule_match::{
 Icmp, Icmpv6, IpAddrMatch, IpMatch, Ports, Protocol, 
RuleMatch, Sctp, Tcp, Udp,
 },
@@ -146,6 +146,14 @@ impl NftRuleEnv<'_> {
 fn contains_family(, family: Family) -> bool {
 self.chain.table().family().families().contains()
 }
+
+fn table() ->  {
+self.chain.table()
+}
+
+fn direction() -> Direction {
+self.direction
+}
 }
 
 pub(crate) trait ToNftRules {
@@ -204,6 +212,14 @@ impl ToNftRules for RuleGroup {
 }
 }
 
+pub(crate) fn generate_verdict(verdict: ConfigVerdict, env: ) -> 
Statement {
+match (env.table().family(), env.direction(), verdict) {
+(TableFamily::Bridge, Direction::In, ConfigVerdict::Reject) => 
Statement::make_drop(),
+(_, _, ConfigVerdict::Reject) => Statement::jump("do-reject"),
+_ => Statement::from(verdict),
+}
+}
+
 impl ToNftRules for RuleMatch {
 fn to_nft_rules(, rules:  Vec, env: ) -> 
Result<(), Error> {
 if env.direction != self.direction() {
@@ -230,7 +246,7 @@ impl ToNftRules for RuleMatch {
 }
 }
 
-rules.push(NftRule::new(Statement::from(self.verdict(;
+rules.push(NftRule::new(generate_verdict(self.verdict(), env)));
 
 if let Some(name) = () {
 handle_iface(rules, env, name)?;
diff --git a/proxmox-firewall/tests/input/100.fw 
b/proxmox-firewall/tests/input/100.fw
index 6cf9fff..1aa9b00 100644
--- a/proxmox-firewall/tests/input/100

[pve-devel] [PATCH proxmox-firewall] firewall: properly cleanup tables when firewall is inactive

2024-04-23 Thread Stefan Hanreich
When executing multiple nft commands they are transactional, either
all get applied or none. When only the host or guest firewall is
active, only one table exists and this causes the delete commands to
fail. To fix this we need to send the delete commands separately.

It might make sense to support running multiple separate batches in
the NftClient in the future in order to avoid having to call nft
twice.

Signed-off-by: Stefan Hanreich 
---
 proxmox-firewall/src/bin/proxmox-firewall.rs |  9 +
 proxmox-firewall/src/firewall.rs | 10 +-
 2 files changed, 10 insertions(+), 9 deletions(-)

diff --git a/proxmox-firewall/src/bin/proxmox-firewall.rs 
b/proxmox-firewall/src/bin/proxmox-firewall.rs
index 2f4875f..4e07993 100644
--- a/proxmox-firewall/src/bin/proxmox-firewall.rs
+++ b/proxmox-firewall/src/bin/proxmox-firewall.rs
@@ -12,11 +12,12 @@ const RULE_BASE:  = 
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() {
-return Err(err);
+for command in 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() 
{
+return Err(err);
+}
 }
 
 Ok(())
diff --git a/proxmox-firewall/src/firewall.rs b/proxmox-firewall/src/firewall.rs
index 2195a07..b137f58 100644
--- a/proxmox-firewall/src/firewall.rs
+++ b/proxmox-firewall/src/firewall.rs
@@ -157,11 +157,11 @@ impl Firewall {
 }
 }
 
-pub fn remove_commands() -> Commands {
-Commands::new(vec![
-Delete::table(Self::cluster_table()),
-Delete::table(Self::guest_table()),
-])
+pub fn remove_commands() -> Vec {
+vec![
+Commands::new(vec![Delete::table(Self::cluster_table())]),
+Commands::new(vec![Delete::table(Self::guest_table())]),
+]
 }
 
 fn create_management_ipset(, commands:  Commands) -> Result<(), 
Error> {
-- 
2.39.2


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



Re: [pve-devel] [PATCH pve-network 0/3] Advertise MTU via DHCP / RA

2024-04-22 Thread Stefan Hanreich


On 4/22/24 14:06, Thomas Lamprecht wrote:
> seems OK from a high-level glance, would need a rebase now though

sent a rebased v2:
https://lists.proxmox.com/pipermail/pve-devel/2024-April/063588.html


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



[pve-devel] [PATCH pve-network v2 3/3] dhcp: dnsmasq: send mtu option via dhcp

2024-04-22 Thread Stefan Hanreich
Signed-off-by: Stefan Hanreich 
---
 src/PVE/Network/SDN/Dhcp.pm | 2 +-
 src/PVE/Network/SDN/Dhcp/Dnsmasq.pm | 7 ++-
 src/PVE/Network/SDN/Dhcp/Plugin.pm  | 2 +-
 3 files changed, 8 insertions(+), 3 deletions(-)

diff --git a/src/PVE/Network/SDN/Dhcp.pm b/src/PVE/Network/SDN/Dhcp.pm
index 7876c08..d48de34 100644
--- a/src/PVE/Network/SDN/Dhcp.pm
+++ b/src/PVE/Network/SDN/Dhcp.pm
@@ -84,7 +84,7 @@ sub regenerate_config {
 
die "Could not find DHCP plugin: $dhcp_plugin_name" if !$dhcp_plugin;
 
-   eval { $dhcp_plugin->before_configure($zoneid) };
+   eval { $dhcp_plugin->before_configure($zoneid, $zone) };
die "Could not run before_configure for DHCP server $zoneid $@\n" if $@;
 
for my $vnetid (sort keys %{$vnet_cfg->{ids}}) {
diff --git a/src/PVE/Network/SDN/Dhcp/Dnsmasq.pm 
b/src/PVE/Network/SDN/Dhcp/Dnsmasq.pm
index c14f5d7..ae52d31 100644
--- a/src/PVE/Network/SDN/Dhcp/Dnsmasq.pm
+++ b/src/PVE/Network/SDN/Dhcp/Dnsmasq.pm
@@ -177,7 +177,7 @@ sub systemctl_service {
 }
 
 sub before_configure {
-my ($class, $dhcpid) = @_;
+my ($class, $dhcpid, $zone_cfg) = @_;
 
 my $dbus_config = <https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel



[pve-devel] [PATCH pve-network v2 2/3] zones: add method for getting MTU

2024-04-22 Thread Stefan Hanreich
Signed-off-by: Stefan Hanreich 
---
 src/PVE/Network/SDN/Zones.pm  | 8 
 src/PVE/Network/SDN/Zones/Plugin.pm   | 7 +++
 src/PVE/Network/SDN/Zones/SimplePlugin.pm | 8 +++-
 3 files changed, 22 insertions(+), 1 deletion(-)

diff --git a/src/PVE/Network/SDN/Zones.pm b/src/PVE/Network/SDN/Zones.pm
index 5bd3536..c1c7745 100644
--- a/src/PVE/Network/SDN/Zones.pm
+++ b/src/PVE/Network/SDN/Zones.pm
@@ -27,6 +27,7 @@ PVE::Network::SDN::Zones::SimplePlugin->register();
 PVE::Network::SDN::Zones::Plugin->init();
 
 my $local_network_sdn_file = "/etc/network/interfaces.d/sdn";
+my $default_mtu = 1500;
 
 sub sdn_zones_config {
 my ($cfg, $id, $noerr) = @_;
@@ -369,5 +370,12 @@ sub del_bridge_fdb {
 $plugin->del_bridge_fdb($plugin_config, $iface, $macaddr);
 }
 
+sub get_mtu {
+my ($zone_config) = @_;
+
+my $plugin = 
PVE::Network::SDN::Zones::Plugin->lookup($zone_config->{type});
+return $plugin->get_mtu($zone_config) // $default_mtu;
+}
+
 1;
 
diff --git a/src/PVE/Network/SDN/Zones/Plugin.pm 
b/src/PVE/Network/SDN/Zones/Plugin.pm
index 247d0b2..26cc0da 100644
--- a/src/PVE/Network/SDN/Zones/Plugin.pm
+++ b/src/PVE/Network/SDN/Zones/Plugin.pm
@@ -361,4 +361,11 @@ sub datacenter_config {
 return PVE::Cluster::cfs_read_file('datacenter.cfg');
 }
 
+
+sub get_mtu {
+my ($class, $plugin_config) = @_;
+
+die "please implement inside plugin";
+}
+
 1;
diff --git a/src/PVE/Network/SDN/Zones/SimplePlugin.pm 
b/src/PVE/Network/SDN/Zones/SimplePlugin.pm
index 65e9ad4..1416d39 100644
--- a/src/PVE/Network/SDN/Zones/SimplePlugin.pm
+++ b/src/PVE/Network/SDN/Zones/SimplePlugin.pm
@@ -56,7 +56,7 @@ sub generate_sdn_config {
 
 my $mac = $vnet->{mac};
 my $alias = $vnet->{alias};
-my $mtu = $plugin_config->{mtu} if $plugin_config->{mtu};
+my $mtu = $class->get_mtu($plugin_config);
 
 # vnet bridge
 my @iface_config = ();
@@ -144,6 +144,12 @@ sub vnet_update_hook {
 }
 }
 
+sub get_mtu {
+my ($class, $plugin_config) = @_;
+
+return $plugin_config->{mtu};
+}
+
 1;
 
 
-- 
2.39.2


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



[pve-devel] [PATCH pve-network v2 1/3] dhcp: fix function signatures in abstract class

2024-04-22 Thread Stefan Hanreich
Signed-off-by: Stefan Hanreich 
---
 src/PVE/Network/SDN/Dhcp/Plugin.pm | 12 ++--
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/PVE/Network/SDN/Dhcp/Plugin.pm 
b/src/PVE/Network/SDN/Dhcp/Plugin.pm
index b99f598..6e985cd 100644
--- a/src/PVE/Network/SDN/Dhcp/Plugin.pm
+++ b/src/PVE/Network/SDN/Dhcp/Plugin.pm
@@ -28,12 +28,12 @@ sub add_ip_mapping {
 }
 
 sub configure_range {
-my ($class, $dhcpid, $vnetid, $subnet_config, $range_config) = @_;
+my ($class, $config, $dhcpid, $vnetid, $subnet_config, $range_config) = @_;
 die 'implement in sub class';
 }
 
 sub configure_subnet {
-my ($class, $dhcpid, $vnetid, $subnet_config) = @_;
+my ($class, $config, $dhcpid, $vnetid, $subnet_config) = @_;
 die 'implement in sub class';
 }
 
@@ -43,22 +43,22 @@ sub configure_vnet {
 }
 
 sub before_configure {
-my ($class, $dhcp_config) = @_;
+my ($class, $dhcpid) = @_;
 die 'implement in sub class';
 }
 
 sub after_configure {
-my ($class, $dhcp_config) = @_;
+my ($class, $dhcpid, $noerr) = @_;
 die 'implement in sub class';
 }
 
 sub before_regenerate {
-my ($class) = @_;
+my ($class, $noerr) = @_;
 die 'implement in sub class';
 }
 
 sub after_regenerate {
-my ($class, $dhcp_config) = @_;
+my ($class) = @_;
 die 'implement in sub class';
 }
 
-- 
2.39.2


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



[pve-devel] [PATCH network v2 0/3] Advertise MTU via DHCP / RA

2024-04-22 Thread Stefan Hanreich
Changes from v1 -> v2:
* rebased branch, everything else unchanged

pve-network:

Stefan Hanreich (3):
  dhcp: fix function signatures in abstract class
  zones: add method for getting MTU
  dhcp: dnsmasq: send mtu option via dhcp

 src/PVE/Network/SDN/Dhcp.pm   |  2 +-
 src/PVE/Network/SDN/Dhcp/Dnsmasq.pm   |  7 ++-
 src/PVE/Network/SDN/Dhcp/Plugin.pm| 12 ++--
 src/PVE/Network/SDN/Zones.pm  |  8 
 src/PVE/Network/SDN/Zones/Plugin.pm   |  7 +++
 src/PVE/Network/SDN/Zones/SimplePlugin.pm |  8 +++-
 6 files changed, 35 insertions(+), 9 deletions(-)


Summary over all repositories:
  6 files changed, 35 insertions(+), 9 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



[pve-devel] [PATCH proxmox-firewall 2/2] firewall: improve systemd unit file

2024-04-19 Thread Stefan Hanreich
Explicitly mark the service as simple and remove the PIDFile
attribute, which doesn't do anything with simple services.

Signed-off-by: Stefan Hanreich 
---
 debian/proxmox-firewall.service | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/debian/proxmox-firewall.service b/debian/proxmox-firewall.service
index ad2324b..c2dc903 100644
--- a/debian/proxmox-firewall.service
+++ b/debian/proxmox-firewall.service
@@ -5,7 +5,7 @@ After=pvefw-logger.service pve-cluster.service network.target 
systemd-modules-lo
 
 [Service]
 ExecStart=/usr/libexec/proxmox/proxmox-firewall
-PIDFile=/run/proxmox-firewall.pid
+Type=simple
 Environment="RUST_LOG_STYLE=SYSTEMD"
 Environment="RUST_LOG=warn"
 
-- 
2.39.2


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



[pve-devel] [PATCH proxmox-firewall 1/2] firewall: wait for nft process

2024-04-19 Thread Stefan Hanreich
NftClient never waits for the child process to terminate leading to
defunct leftover processes.

Signed-off-by: Stefan Hanreich 
---
 proxmox-nftables/src/client.rs | 38 --
 1 file changed, 9 insertions(+), 29 deletions(-)

diff --git a/proxmox-nftables/src/client.rs b/proxmox-nftables/src/client.rs
index 69e464b..eaa3dd2 100644
--- a/proxmox-nftables/src/client.rs
+++ b/proxmox-nftables/src/client.rs
@@ -36,35 +36,15 @@ impl NftClient {
 return Err(NftError::from(error));
 };
 
-let mut error_output = String::new();
-
-match child
-.stderr
-.take()
-.expect("can get stderr")
-.read_to_string( 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( output)
-{
-return Err(NftError::from(error));
-};
-
-Ok(output)
+let output = child.wait_with_output().map_err(NftError::from)?;
+
+if output.status.success() {
+Ok(String::from_utf8(output.stdout).expect("output is valid 
utf-8"))
+} else {
+Err(NftError::Command(
+String::from_utf8(output.stderr).expect("output is valid 
utf-8"),
+))
+}
 }
 
 pub fn run_json_commands(commands: ) -> 
Result, NftError> {
-- 
2.39.2


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



[pve-devel] [PATCH pve-docs v4 5/5] firewall: add documentation for proxmox-firewall

2024-04-19 Thread Stefan Hanreich
Add a section that explains how to use the new nftables-based
proxmox-firewall.

Signed-off-by: Stefan Hanreich 
---
 pve-firewall.adoc | 181 ++
 1 file changed, 181 insertions(+)

diff --git a/pve-firewall.adoc b/pve-firewall.adoc
index a5e40f9..9fb4e46 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,186 @@ Ports used by {pve}
 * corosync cluster traffic: 5405-5412 UDP
 * live migration (VM memory and local-disk data): 6-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//host.fw`):
+
+
+[OPTIONS]
+
+nftables: 1
+
+
+NOTE: After enabling/disabling `proxmox-firewall`, all running VMs and
+containers need to be restarted for the old/new firewall to work properly.
+
+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_$(hostnam

[pve-devel] [PATCH pve-manager v4 4/5] firewall: expose configuration option for new nftables firewall

2024-04-19 Thread Stefan Hanreich
Signed-off-by: Stefan Hanreich 
---
 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



[pve-devel] [PATCH pve-firewall v4 3/5] add configuration option for new nftables firewall

2024-04-19 Thread Stefan Hanreich
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` or if the proxmox-firewall package is not
installed at all. Conversely, proxmox-firewall only generates rules
when the option is set to `1`.

Signed-off-by: Stefan Hanreich 
---
This looks a bit awkward, but I wanted to avoid having to re-parse the
configuration when calling from pve-firewall but also avoid having to
load the config manually when calling from qemu-server / pve-container

 src/PVE/Firewall.pm | 41 -
 1 file changed, 32 insertions(+), 9 deletions(-)

diff --git a/src/PVE/Firewall.pm b/src/PVE/Firewall.pm
index 81a8798..21eb5fc 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)
 {
@@ -4673,12 +4679,30 @@ sub remove_pvefw_chains_ebtables {
 ebtables_restore_cmdlist(get_ebtables_cmdlist({}));
 }
 
-sub init {
-my $cluster_conf = load_clusterfw_conf();
-my $cluster_options = $cluster_conf->{options};
-my $enable = $cluster_options->{enable};
+sub is_nftables {
+my ($cluster_conf, $host_conf) = @_;
+
+if (!-x "/usr/libexec/proxmox/proxmox-firewall") {
+   return 0;
+}
+
+$cluster_conf = load_clusterfw_conf() if !defined($cluster_conf);
+$host_conf = load_hostfw_conf($cluster_conf) if !defined($host_conf);
 
-return if !$enable;
+return $host_conf->{options}->{nftables};
+}
+
+sub is_enabled {
+my ($cluster_conf, $host_conf) = @_;
+
+$cluster_conf = load_clusterfw_conf() if !defined($cluster_conf);
+$host_conf = load_hostfw_conf($cluster_conf) if !defined($host_conf);
+
+return $cluster_conf->{options}->{enable} && !is_nftables($cluster_conf, 
$host_conf);
+}
+
+sub init {
+return if !is_enabled();
 
 # load required modules here
 }
@@ -4687,14 +4711,13 @@ sub update {
 my $code = sub {
 
my $cluster_conf = load_clusterfw_conf();
-   my $cluster_options = $cluster_conf->{options};
+   my $hostfw_conf = load_hostfw_conf($cluster_conf);
 
-   if (!$cluster_options->{enable}) {
+   if (!is_enabled($cluster_conf, $hostfw_conf)) {
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



[pve-devel] [PATCH container/docs/firewall/manager/qemu-server v4 0/5] proxmox firewall nftables

2024-04-19 Thread Stefan Hanreich
This patch series contains the remaining patches that are necessary for
proxmox-firewall to work. It adds documentation as well as changes how
firewall-bridges are created when proxmox-firewall is activated. It also patches
pve-firewall to not generate rules when proxmox-firewall is active.

Dependencies:
* qemu-server, pve-container & pve-manager depend on a bump of pve-firewall

Changes from v3 -> v4:
* additionally check for the existence of proxmox-firewall bin
* extracted checks into helper functions
* update docs to reflect the changes in behavior

(omitted description & changes only relevant for the firewall itself)

qemu-server:

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

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


pve-container:

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

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


pve-firewall:

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

 src/PVE/Firewall.pm | 41 -
 1 file changed, 32 insertions(+), 9 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 | 181 ++
 1 file changed, 181 insertions(+)


Summary over all repositories:
  5 files changed, 224 insertions(+), 13 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



[pve-devel] [PATCH qemu-server v4 1/5] firewall: add handling for new nft firewall

2024-04-19 Thread Stefan Hanreich
When the nftables firewall is enabled, we do not need to create
firewall bridges.

Signed-off-by: Stefan Hanreich 
---
 vm-network-scripts/pve-bridge | 7 +--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/vm-network-scripts/pve-bridge b/vm-network-scripts/pve-bridge
index 85997a0..fe5a702 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,15 @@ 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 $firewall = $net->{firewall} && !PVE::Firewall::is_nftables();
+
 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



[pve-devel] [PATCH pve-container v4 2/5] firewall: add handling for new nft firewall

2024-04-19 Thread Stefan Hanreich
When the nftables firewall is enabled, we do not need to create
firewall bridges.

Signed-off-by: Stefan Hanreich 
---
 src/PVE/LXC.pm | 7 +--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm
index 400cf4f..44f5ccf 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);
@@ -946,8 +947,10 @@ sub net_tap_plug : prototype($$) {
return;
 }
 
-my ($bridge, $tag, $firewall, $trunks, $rate, $hwaddr) =
-   $net->@{'bridge', 'tag', 'firewall', 'trunks', 'rate', 'hwaddr'};
+my ($bridge, $tag, $trunks, $rate, $hwaddr) =
+   $net->@{'bridge', 'tag', 'trunks', 'rate', 'hwaddr'};
+
+my $firewall = $net->{firewall} && !PVE::Firewall::is_nftables();
 
 if ($have_sdn) {
PVE::Network::SDN::Zones::tap_plug($iface, $bridge, $tag, $firewall, 
$trunks, $rate);
-- 
2.39.2


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



[pve-devel] [PATCH proxmox-firewall v3 34/39] firewall: add integration test

2024-04-19 Thread Stefan Hanreich
Signed-off-by: Stefan Hanreich 
---
 .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 000..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 000..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 000..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-18b721a6
+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 000..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 000..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

[pve-devel] [PATCH pve-firewall v3 37/39] add configuration option for new nftables firewall

2024-04-19 Thread Stefan Hanreich
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 
---
 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



[pve-devel] [PATCH proxmox-firewall v3 19/39] nftables: expression: add types

2024-04-19 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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),
+Set(Vec),
+Range(Box<(Expression, Expression)>),
+Map(Box),
+Prefix(Prefix),
+Payload(Payload),
+Meta(Meta),
+Ct(Ct),
+Elem(Box),
+
+#[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),
+
+#[serde(untagged)]
+Verdict(Verdict),
+
+#[serde(untagged)]
+Bool(bool),
+#[serde(untagged)]
+Number(i64),
+#[serde(untagged)]
+String(String),
+}
+
+impl Expression {
+pub fn set(expressions: impl IntoIterator) -> Self {
+Expression::Set(Vec::from_iter(expressions))
+}
+
+pub fn concat(expressions: impl IntoIterator) -> Self {
+Expression::Concat(Vec::from_iter(expressions))
+}
+}
+
+impl From for Expression {
+#[inline]
+fn from(v: bool) -> Self {
+Expression::Bool(v)
+}
+}
+
+impl From for Expression {
+#[inline]
+fn from(v: i64) -> Self {
+Expression::Number(v)
+}
+}
+
+impl From for Expression {
+#[inline]
+fn from(v: u16) -> Self {
+Expression::Number(v.into())
+}
+}
+
+impl From for Expression {
+#[inline]
+fn from(v: u8) -> Self {
+Expression::Number(v.into())
+}
+}
+
+impl From<> for Expression {
+#[inline]
+fn from(v: ) -> Self {
+Expression::String(v.to_string())
+}
+}
+
+impl From for Expression {
+#[inline]
+fn from(v: String) -> Self {
+Expression::String(v)
+}
+}
+
+impl From for Expression {
+#[inline]
+fn from(meta: Meta) -> Self {
+Expression::Meta(meta)
+}
+}
+
+impl From for Expression {
+#[inline]
+fn from(ct: Ct) -> Self {
+Expression::Ct(ct)
+}
+}
+
+impl From for Expression {
+#[inline]
+fn from(payload: Payload) -> Self {
+Expression::Payload(payload)
+}
+}
+
+impl From for Expression {
+#[inline]
+fn from(prefix: Prefix) -> Self {
+Expression::Prefix(prefix)
+}
+}
+
+impl From for Expression {
+#[inline]
+fn from(value: Verdict) -> Self {
+Expression::Verdict(value)
+}
+}
+
+impl From<> for Expression {
+fn from(value: ) -> Self {
+Expression::String(value.to_string())
+}
+}
+
+impl From<> for Expression {
+fn from(address: ) -> Self {
+Expression::String(address.to_string())
+}
+}
+
+impl From<> for Expression {
+fn from(address: ) -> 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) -> Self {
+Self { key: key.into() }
+}
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct Map {
+key: Expres

[pve-devel] [PATCH proxmox-firewall v3 31/39] firewall: add ruleset generation logic

2024-04-19 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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:  = "proxmox-firewall";
+static HOST_TABLE_NAME:  = "proxmox-firewall";
+static GUEST_TABLE_NAME:  = "proxmox-firewall-guests";
+
+static NF_CONNTRACK_MAX_FILE:  = 
"/proc/sys/net/netfilter/nf_conntrack_max";
+static NF_CONNTRACK_TCP_TIMEOUT_ESTABLISHED:  =
+"/proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established";
+static NF_CONNTRACK_TCP_TIMEOUT_SYN_RECV:  =
+"/proc/sys/net/netfilter/nf_conntrack_tcp_timeout_syn_recv";
+static LOG_CONNTRACK_FILE:  = "/var/lib/pve-firewall/log_nf_conntrack";
+
+#[derive(Default)]
+pub struct Firewall {
+config: FirewallConfig,
+}
+
+impl From for Firewall {
+fn from(config: FirewallConfig) -> Self {
+Self { config }
+}
+}
+
+impl Firewall {
+pub fn new() -> Self {
+Self {
+..Default::default()
+}
+}
+
+pub fn is_enabled() -> 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: , 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() -> ChainP

[pve-devel] [PATCH proxmox-firewall v3 22/39] nftables: statement: add conversion traits for config types

2024-04-19 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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> From for Statement {
 }
 }
 
-#[derive(Clone, Debug, Deserialize, Serialize)]
+#[cfg(feature = "config-ext")]
+impl From 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>,
+log_level: LogLevel,
+chain_name: ,
+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 for LogLevel {
+type Error = Error;
+
+fn try_from(value: ConfigLogLevel) -> Result {
+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() -> u8 {
 match self {
@@ -231,6 +286,20 @@ pub struct AnonymousLimit {
 pub inv: Option,
 }
 
+#[cfg(feature = "config-ext")]
+impl From 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



[pve-devel] [PATCH proxmox-firewall v3 25/39] nftables: add nft client

2024-04-19 Thread Stefan Hanreich
Add a thin wrapper around nft, which can be used to run commands
defined by the rust types.

Reviewed-by: Lukas Wagner 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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 {
+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( 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( output)
+{
+return Err(NftError::from(error));
+};
+
+Ok(output)
+}
+
+pub fn run_json_commands(commands: ) -> 
Result, NftError> {
+let json = serde_json::to_vec(commands).expect("can serialize commands 
struct");
+let output = Self::execute_nft_commands(true, )?;
+
+if !output.is_empty() {
+let parsed_output: Option = 
serde_json::from_str().ok();
+return Ok(parsed_output);
+}
+
+Ok(None)
+}
+
+pub fn run_commands(commands: ) -> Result {
+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



[pve-devel] [PATCH proxmox-firewall v3 28/39] firewall: add config loader

2024-04-19 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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() -> Option>;
+fn host() -> Option>;
+fn guest_list() -> GuestMap;
+fn guest_config(, vmid: , guest: ) -> Option>;
+fn guest_firewall_config(, vmid: ) -> Option>;
+}
+
+#[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: ) -> Result, 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:  = "/etc/pve/firewall/cluster.fw";
+const HOST_CONFIG_PATH:  = "/etc/pve/local/host.fw";
+
+impl FirewallConfigLoader for PveFirewallConfigLoader {
+fn cluster() -> Option> {
+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;
+return Some(buf_reader);
+}
+
+None
+}
+
+fn host() -> Option> {
+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;
+return Some(buf_reader);
+}
+
+None
+}
+
+fn guest_list() -> GuestMap {
+log::info!("loading vmlist");
+GuestMap::new().expect("able to read vmlist")
+}
+
+fn guest_config(, vmid: , entry: ) -> Option> {
+log::info!("loading guest #{vmid} config");
+
+let fd = open_config_file(::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;
+return Some(buf_reader);
+}
+
+None
+}
+
+fn guest_firewall_config(, vmid: ) -> Option> {
+log::info!("loading guest #{vmid} firewall config");
+
+let fd = open_config_file(::firewall_config_path(vmid))
+.expect("able to read guest firewall config");
+
+if let Some(file) = fd {
+let buf_reader = Box:

[pve-devel] [PATCH proxmox-firewall v3 16/39] config: firewall: add conntrack helper types

2024-04-19 Thread Stefan Hanreich
Reviewed-by: Lukas Wagner 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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 000..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,
+pub v6: Option,
+pub name: String,
+pub tcp: Option,
+pub udp: Option,
+}
+
+impl TryFrom for CtHelperMacro {
+type Error = Error;
+
+fn try_from(value: CtHelperMacroJson) -> Result {
+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,
+name: String,
+tcp: Option,
+udp: Option,
+}
+
+impl CtHelperMacro {
+fn helper_name(, protocol: ) -> String {
+format!("helper-{}-{protocol}", self.name)
+}
+
+pub fn tcp_helper_name() -> String {
+self.helper_name("tcp")
+}
+
+pub fn udp_helper_name() -> String {
+self.helper_name("udp")
+}
+
+pub fn family() -> Option {
+self.family
+}
+
+pub fn name() ->  {
+self.name.as_ref()
+}
+
+pub fn tcp() -> Option<> {
+self.tcp.as_ref()
+}
+
+pub fn udp() -> Option<> {
+self.udp.as_ref()
+}
+}
+
+fn hashmap() -> &'static HashMap {
+const MACROS:  = include_str!("../../resources/ct_helper.json");
+static HASHMAP: OnceLock> = OnceLock::new();
+
+HASHMAP.get_or_init(|| {
+let macro_data: Vec = 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 

[pve-devel] [PATCH proxmox-firewall v3 09/39] config: firewall: add types for rules

2024-04-18 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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: ) -> Result {
 },
 )
 }
+
+/// `` 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(self, visitor: V) -> Result
+where
+V: serde::de::Visitor<'de>,
+{
+self.0.deserialize_any(visitor)
+}
+
+fn deserialize_option(self, visitor: V) -> Result
+where
+V: serde::de::Visitor<'de>,
+{
+visitor.visit_some(self.0)
+}
+
+fn deserialize_str(self, visitor: V) -> Result
+where
+V: serde::de::Visitor<'de>,
+{
+self.0.deserialize_str(visitor)
+}
+
+fn deserialize_string(self, visitor: V) -> Result
+where
+V: serde::de::Visitor<'de>,
+{
+self.0.deserialize_string(visitor)
+}
+
+fn deserialize_enum(
+self,
+_name: ,
+_variants: &'static [&'static str],
+visitor: V,
+) -> Result
+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
+}
+}
+
+/// `` 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(serde::de::value::StringDeserializer);
+
+impl<'de, E> serde::de::Deserializer<'de> for SomeStringDeserializer
+where
+E: serde::de::Error,
+{
+type Error = E;
+
+fn deserialize_any(self, visitor: V) -> Result
+where
+V: serde::de::Visitor<'de>,
+{
+self.0.deserialize_any(visitor)
+}
+
+fn deserialize_option(self, visitor: V) -> Result
+where
+V: serde::de::Visitor<'de>,
+{
+visitor.visit_some(self.0)
+}
+
+fn deserialize_str(self, visitor: V) -> Result
+where
+V: serde::de::Visitor<'de>,
+{
+self.0.deserialize_str(visitor)
+}
+
+fn deserialize_string(self, visitor: V) -> Result
+where
+V: serde::de::Visitor<'de>,
+{
+self.0.deserialize_string(visitor)
+}
+
+fn deserialize_enum(
+self,
+_name: ,
+_variants: &'static [&'static str],
+visitor: V,
+) -> Result
+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

[pve-devel] [PATCH proxmox-firewall v3 33/39] firewall: add files for debian packaging

2024-04-18 Thread Stefan Hanreich
Suggested-By: Fabian Grünbichler 
Signed-off-by: Stefan Hanreich 
---
 .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 000..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 000..3ca5833
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,5 @@
+rust-proxmox-firewall (0.1) UNRELEASED; urgency=medium
+
+  * Initial release.
+
+ -- Stefan Hanreich   Thu, 07 Mar 2024 10:15:10 +0100
diff --git a/debian/control b/debian/control
new file mode 100644
index 000..97f9e89
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,39 @@
+Source: rust-proxmox-firewall
+Section: admin
+Priority: optional
+Maintainer: Proxmox Support Team 
+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 000..fe09a1b
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,16 @@
+Copyright (C) 

[pve-devel] [PATCH proxmox-firewall v3 30/39] firewall: add object generation logic

2024-04-18 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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,
+}
+
+impl NftObjectEnv<'_, '_> {
+pub(crate) fn alias(, name: ) -> Option<> {
+self.firewall_config.alias(name, self.vmid)
+}
+}
+
+pub(crate) trait ToNftObjects {
+fn to_nft_objects(, env: ) -> Result, 
Error>;
+}
+
+impl ToNftObjects for CtHelperMacro {
+fn to_nft_objects(, env: ) -> Result, 
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(, env: ) -> Result, 
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  {
+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( vec![
+Add::set(set_config),
+Flush::set(set_name.clone()),
+

[pve-devel] [PATCH pve-container v3 36/39] firewall: add handling for new nft firewall

2024-04-18 Thread Stefan Hanreich
When the nftables firewall is enabled, we do not need to create
firewall bridges.

Signed-off-by: Stefan Hanreich 
---
 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



[pve-devel] [PATCH qemu-server v3 35/39] firewall: add handling for new nft firewall

2024-04-18 Thread Stefan Hanreich
When the nftables firewall is enabled, we do not need to create
firewall bridges.

Signed-off-by: Stefan Hanreich 
---
 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



[pve-devel] [PATCH proxmox-firewall v3 21/39] nftables: statement: add types

2024-04-18 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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) -> Self {
+Statement::Verdict(Verdict::Jump {
+target: target.into(),
+})
+}
+
+pub fn goto(target: impl Into) -> Self {
+Statement::Verdict(Verdict::Goto {
+target: target.into(),
+})
+}
+}
+
+impl From for Statement {
+#[inline]
+fn from(m: Match) -> Statement {
+Statement::Match(m)
+}
+}
+
+impl From for Statement {
+#[inline]
+fn from(m: Mangle) -> Statement {
+Statement::Mangle(m)
+}
+}
+
+impl From for Statement {
+#[inline]
+fn from(m: Reject) -> Statement {
+Statement::Reject(m)
+}
+}
+
+impl From for Statement {
+#[inline]
+fn from(m: Set) -> Statement {
+Statement::Set(m)
+}
+}
+
+impl From for Statement {
+#[inline]
+fn from(m: Vmap) -> Statement {
+Statement::Vmap(m)
+}
+}
+
+impl From for Statement {
+#[inline]
+fn from(log: Log) -> Statement {
+Statement::Log(log)
+}
+}
+
+impl> From 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,
+#[serde(skip_serializing_if = "Option::is_none")]
+expr: Option,
+}
+
+#[derive(Clone, Debug, Default, Deserialize, Serialize)]
+#[serde(rename_all = "kebab-case")]
+pub struct Log {
+#[serde(skip_serializing_if = "Option::is_none")]
+prefix: Option,
+
+#[serde(skip_serializing_if = "Option::is_none")]
+group: Option,
+
+#[serde(skip_serializing_if = "Option::is_none")]
+snaplen: Option,
+
+#[serde(skip_serializing_if = "Option::is_none")]
+queue_threshold: Option,
+
+#[serde(skip_serializing_if = "Option::is_none")]
+level: Option,
+
+#[serde(default, skip_serializing_if = "Vec::is_empty")]
+flags: NfVec,
+}
+
+impl Log {
+

[pve-devel] [PATCH proxmox-firewall v3 18/39] nftables: add helpers

2024-04-18 Thread Stefan Hanreich
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 is a wrapper for Vec that serializes into T iff Vec
contains one element, otherwise it serializes like a Vec would
normally do.

Reviewed-by: Lukas Wagner 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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(deserializer: D) -> Result
+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(, serializer: S) -> Result
+where
+S: serde::Serializer,
+{
+serializer.serialize_none()
+}
+}
+
+impl fmt::Display for Null {
+fn fmt(, f:  fmt::Formatter) -> fmt::Result {
+f.write_str("null")
+}
+}
+
+#[derive(Clone, Debug)]
+pub struct NfVec(pub(crate) Vec);
+
+impl Default for NfVec {
+fn default() -> Self {
+Self::new()
+}
+}
+
+impl NfVec {
+pub const fn new() -> Self {
+Self(Vec::new())
+}
+
+pub fn one(value: T) -> Self {
+Self(vec![value])
+}
+}
+
+impl From> for NfVec {
+fn from(v: Vec) -> Self {
+Self(v)
+}
+}
+
+impl From> for Vec {
+fn from(v: NfVec) -> Self {
+v.0
+}
+}
+
+impl FromIterator for NfVec {
+fn from_iter>(iter: I) -> Self {
+Self(iter.into_iter().collect())
+}
+}
+
+impl std::ops::Deref for NfVec {
+type Target = Vec;
+
+fn deref() -> ::Target {
+
+}
+}
+
+impl std::ops::DerefMut for NfVec {
+fn deref_mut( self) ->  Self::Target {
+ self.0
+}
+}
+
+impl Serialize for NfVec {
+fn serialize(, serializer: S) -> Result
+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(self, value: $($ty)+) -> Result
+where
+E: Error,
+{
+T::deserialize(value.into_deserializer()).map(NfVec::one)
+}
+)+
+};
+}
+
+impl<'de, T: Deserialize<'de>> Deserialize<'de> for NfVec {
+fn deserialize(deserializer: D) -> Result
+where
+D: serde::Deserializer<'de>,
+{
+use serde::de::{Error, IntoDeserializer};
+
+struct V(PhantomData);
+
+impl<'de, T: Deserialize<'de>> serde::de::Visitor<'de> for V {
+type Value = NfVec;
+
+fn expecting(, f:  fmt::Formatter) -> fmt::Result {
+f.write_str("an array or single element")
+}
+
+fn visit_seq(self, seq: A) -> Result
+where
+A: serde::de::SeqAccess<'de>,
+{
+
Vecdeserialize(serde::de::value::SeqAcce

[pve-devel] [PATCH proxmox-firewall v3 13/39] config: firewall: add host specific config + option types

2024-04-18 Thread Stefan Hanreich
Reviewed-by: Lukas Wagner 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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,
+
+#[serde(default, with = "parse::serde_option_bool")]
+nftables: Option,
+
+log_level_in: Option,
+log_level_out: Option,
+
+#[serde(default, with = "parse::serde_option_bool")]
+log_nf_conntrack: Option,
+#[serde(default, with = "parse::serde_option_bool")]
+ndp: Option,
+
+#[serde(default, with = "parse::serde_option_bool")]
+nf_conntrack_allow_invalid: Option,
+
+// is Option> for easier deserialization
+#[serde(default, with = "parse::serde_option_conntrack_helpers")]
+nf_conntrack_helpers: Option>,
+
+#[serde(default, with = "parse::serde_option_number")]
+nf_conntrack_max: Option,
+#[serde(default, with = "parse::serde_option_number")]
+nf_conntrack_tcp_timeout_established: Option,
+#[serde(default, with = "parse::serde_option_number")]
+nf_conntrack_tcp_timeout_syn_recv: Option,
+
+#[serde(default, with = "parse::serde_option_bool")]
+nosmurfs: Option,
+
+#[serde(default, with = "parse::serde_option_bool")]
+protection_synflood: Option,
+#[serde(default, with = "parse::serde_option_number")]
+protection_synflood_burst: Option,
+#[serde(default, with = "parse::serde_option_number")]
+protection_synflood_rate: Option,
+
+smurf_log_level: Option,
+tcp_flags_log_level: Option,
+
+#[serde(default, with = "parse::serde_option_bool")]
+tcpflags: Option,
+}
+
+#[derive(Debug, Default)]
+pub struct Config {
+pub(crate) config: super::common::Config,
+}
+
+impl Config {
+pub fn new() -> Self {
+Self {
+config: Default::default(),
+}
+}
+
+pub fn parse(input: R) -> Result {
+let config = super::common::Config::parse(input, ::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() -> &[Rule] {
+
+}
+
+pub fn management_ips() -> Result, 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() {
+managemen

[pve-devel] [PATCH proxmox-firewall v3 14/39] config: firewall: add guest-specific config + option types

2024-04-18 Thread Stefan Hanreich
Reviewed-by: Lukas Wagner 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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,
+
+#[serde(default, with = "serde_option_bool")]
+enable: Option,
+
+#[serde(default, with = "serde_option_bool")]
+ipfilter: Option,
+
+#[serde(default, with = "serde_option_bool")]
+ndp: Option,
+
+#[serde(default, with = "serde_option_bool")]
+radv: Option,
+
+log_level_in: Option,
+log_level_out: Option,
+
+#[serde(default, with = "serde_option_bool")]
+macfilter: Option,
+
+#[serde(rename = "policy_in")]
+policy_in: Option,
+
+#[serde(rename = "policy_out")]
+policy_out: Option,
+}
+
+#[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,
+}
+
+impl Config {
+pub fn parse(
+vmid: ,
+iface_prefix: &'static str,
+firewall_input: T,
+network_input: U,
+) -> Result {
+let parser_cfg = super::common::ParserConfig {
+guest_iface_names: true,
+ipset_scope: Some(IpsetScope::Guest),
+};
+
+let config = super::common::Config::parse(firewall_input, 
_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() -> Vmid {
+self.vmid
+}
+
+pub fn alias(, name: ) -> Option<> {
+self.config.alias(name.name())
+}
+
+pub fn iface_name_by_key(, key: ) -> Result {
+let index = NetworkConfig::index_from_net_key(key)?;
+Ok(format!("{}{}i{index}", self.iface_prefix, self.vmid))
+}
+
+pub fn iface_name_by_index(, 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() -> bool {
+self.config.options.enable.unwrap_or(GUEST_ENABLED_DEFAULT)
+}
+
+pub fn rules() -> &[Rule] {
+
+}
+
+pub fn log_level(, 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() -> bool {
+self.config.options.ndp.unwrap_or(GUEST_ALLOW_NDP_DEFAULT)
+}
+
+/// retur

[pve-devel] [PATCH proxmox-firewall v3 06/39] config: host: add helpers for host network configuration

2024-04-18 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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 000..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 {
+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 {
+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



[pve-devel] [PATCH proxmox-firewall v3 03/39] config: firewall: add types for ports

2024-04-18 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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,
+}
+
+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(, name: ) -> Option {
+self.ports.get(name).copied()
+}
+}
+
+fn named_ports() -> &'static NamedPorts {
+static NAMED_PORTS: OnceLock = 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: ) -> Result {
+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 000..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(, f:  fmt::Formatter) -> fmt::Result {
+match self {
+Self::Port(p) => write!(f, "{p}"),
+Self::Range(beg, end) => write!(f, "{beg}-{end}"),
+}
+}
+}
+
+fn parse_port(port: ) -> Result {
+if let Ok(port) = port.parse::() {
+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: ) -> Result {
+Ok(match s.trim().split_once(':') {
+None => PortEntry::from(pa

[pve-devel] [PATCH proxmox-firewall v3 04/39] config: firewall: add types for log level and rate limit

2024-04-18 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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: ) -> Result {
+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 000..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: ) -> Result {
+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() -> bool {
+self.enabled
+}
+
+pub fn rate() -> i64 {
+self.rate
+}
+
+pub fn burst() -> i64 {
+self.burst
+}
+
+pub fn per() -> 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: ) -> Result {
+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" =&

[pve-devel] [PATCH proxmox-firewall v3 01/39] config: add proxmox-ve-config crate

2024-04-18 Thread Stefan Hanreich
Reviewed-by: Lukas Wagner 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller
Signed-off-by: Stefan Hanreich 
---
 .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 000..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 000..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 000..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 000..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 ",
+"Stefan Hanreich ",
+"Proxmox Support Team ",
+]
+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 000..e69de29
-- 
2.39.2


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



[pve-devel] [PATCH proxmox-firewall v3 29/39] firewall: add rule generation logic

2024-04-18 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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,
+statements: Vec,
+terminal_statements: Vec,
+}
+
+impl NftRule {
+pub fn from_terminal_statements(terminal_statements: Vec) -> 
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: , env: ) -> 
Result, Error> {
+let mut rules = Vec::new();
+
+if rule.disabled() {
+return Ok(rules);
+}
+
+rule.to_nft_rules( rules, env)?;
+
+Ok(rules)
+}
+
+pub fn from_ct_helper(
+ct_helper: ,
+env: ,
+) -> Result, Error> {
+let mut rules = Vec::new();
+ct_helper.to_nft_rules( rules, env)?;
+Ok(rules)
+}
+
+pub fn from_ipfilter(ipfilter: , env: ) -> 
Result, Error> {
+let mut rules = Vec::new();
+ipfilter.to_nft_rules( rules, env)?;
+Ok(rules)
+}
+}
+
+impl Deref for NftRule {
+type Target = Vec;
+
+fn deref() -> ::Target {
+
+}
+}
+
+impl DerefMut for NftRule {
+fn deref_mut( self) ->  Self::Target {
+ 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() -> Option {
+self.family
+}
+
+pub fn set_family( 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,
+}
+
+impl NftRuleEnv<'_> {
+fn alias(, name: ) -> Option<> {
+self.firewall_config.alias(name, self.vmid)
+}
+
+fn iface_name(, rule_iface: ) -> String {
+match  {
+Some(vmid) => {
+if let Some(config) = self.firewall_config.guests().get(vmid) 

[pve-devel] [PATCH proxmox-firewall v3 23/39] nftables: commands: add types

2024-04-18 Thread Stefan Hanreich
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 
---
 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 000..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,
+}
+
+impl Commands {
+pub fn new(commands: Vec) -> Self {
+Self { nftables: commands }
+}
+}
+
+impl Deref for Commands {
+type Target = Vec;
+
+fn deref() -> ::Target {
+
+}
+}
+
+impl DerefMut for Commands {
+fn deref_mut( self) ->  Self::Target {
+ 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) -> Command {
+Command::Add(Add::Table(table.into()))
+}
+
+#[inline]
+pub fn chain(chain: impl Into) -> Command {
+Command::Add(Add::Chain(chain.into()))
+}
+
+#[inline]
+pub fn rule(rule: impl Into) -> Command {
+Command::Add(Add::Rule(rule.into()))
+}
+
+#[inline]
+pub fn set(set: impl Into) -> Command {
+Command::Add(Add::Set(set.into()))
+}
+
+#[inline]
+pub fn map(map: impl Into) -> Command {
+Command::Add(Add::Map(map.into()))
+}
+
+#[inline]
+pub fn limit(limit: impl Into) -> Command {
+Command::Add(Add::Limit(limit.into()))
+}
+
+#[inline]
+pub fn element(element: impl Into) -> Command {
+Command::Add(Add::Element(element.into()))
+}
+
+#[inline]
+pub fn ct_helper(ct_helper: impl Into) -> 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) -> Command {
+Command::Flush(Flush::Table(table.into()))
+}
+
+#[inline]
+pub fn chain(chain: impl Into) -> Command {
+Command::Flush(Flush::Chain(chain.into()))
+}
+
+#[inline]
+pub fn set(set: impl Into) -> Command {
+Command::Flush(Flush::Set(set.into()))
+}
+
+#[inline]
+pub fn map(map: impl Into) -> Command {
+Command::Flush(Flush::Map(map.into()))
+}
+
+#[inline]
+pub fn ruleset() -> Command {
+Command::Flush(Flush::Ruleset(Null))
+}
+}
+
+impl From for Flush {
+#[inline]
+fn from(value: TableName) -> Self {
+Flush::Table(value)
+}
+}
+
+impl From for Flush {
+#[inline]
+fn from(value: ChainName) -> Self {
+Flush::Chain(value)
+}
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum 

[pve-devel] [PATCH pve-docs v3 39/39] firewall: add documentation for proxmox-firewall

2024-04-18 Thread Stefan Hanreich
Add a section that explains how to use the new nftables-based
proxmox-firewall.

Signed-off-by: Stefan Hanreich 
---
 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): 6-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//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 fol

[pve-devel] [PATCH proxmox-firewall v3 27/39] firewall: add base ruleset

2024-04-18 Thread Stefan Hanreich
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 
Signed-off-by: Stefan Hanreich 
---
 .../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 000..67dd8c8
--- /dev/null
+++ b/proxmox-firewall/resources/proxmox-firewall.nft
@@ -0,0 +1,305 @@
+#!/usr/sbin/nft -f
+
+define ipv6_mask = :::::
+
+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

[pve-devel] [PATCH pve-manager v3 38/39] firewall: expose configuration option for new nftables firewall

2024-04-18 Thread Stefan Hanreich
Signed-off-by: Stefan Hanreich 
---
 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



[pve-devel] [PATCH proxmox-firewall v3 32/39] firewall: add proxmox-firewall binary and move existing code into lib

2024-04-18 Thread Stefan Hanreich
Reviewed-by: Lukas Wagner 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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:  = 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() {
+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()?);
+}
+
+let response = NftClient::run_json_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())?;
+signal_hook::flag::register(signal_hook::consts::SIGINT, 
Arc::clone())?;
+
+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 000..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..000
--- 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



[pve-devel] [PATCH proxmox-firewall v3 15/39] config: firewall: add firewall macros

2024-04-18 Thread Stefan Hanreich
Reviewed-by: Lukas Wagner 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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)"
+  },
+  "Dist

[pve-devel] [PATCH proxmox-firewall v3 07/39] config: guest: add helpers for parsing guest network config

2024-04-18 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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: ) -> Option<(, 
)> {
 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: ) -> Option<(, )> {
+let split_position = line.as_bytes().iter().position(|| 
!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: ) -> Result {
 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 000..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() -> &'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() -> bool {
+nodename() == self.node
+}
+
+pub fn ty() ->  {
+
+}
+}
+
+const VMLIST_CONFIG_PATH:  = "/etc/pve/.vmlist";
+
+#[derive(Deserialize)]
+pub struct GuestMap {
+#[serde(rename = "version")]
+_version: usize,
+#[serde(rename = "ids", default)]
+guests: HashMap,
+}
+
+impl From> for GuestMap {
+fn from(guests: HashMap) -> Self {
+Self {
+guests,
+_version: Default::default(),
+}
+}
+}
+
+impl Deref for GuestMap {
+type Target = HashMap;
+
+fn deref() -> ::Target {
+
+}
+}
+
+impl GuestMap {
+pub fn new() -> Result {
+let data = std::fs::read(VMLIST_CONFIG_PATH)
+.with_context(|| format!("failed to read guest map from 
{VMLIST_CONFIG_PATH}"))?;
+
+serde_json::from_slice().with_context(|| "failed to parse guest 
map".to_owned())
+}
+
+pub fn firewall_config_path(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: , entry: ) -> 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 000..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, Deb

[pve-devel] [PATCH proxmox-firewall v3 26/39] firewall: add firewall crate

2024-04-18 Thread Stefan Hanreich
Reviewed-by: Lukas Wagner 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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 ",
+"Stefan Hanreich ",
+"Proxmox Support Team ",
+]
+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 000..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



[pve-devel] [PATCH proxmox-firewall v3 24/39] nftables: types: add conversion traits

2024-04-18 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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() -> Vec {
+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 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.name.as_ref()
+}
+
+#[cfg(feature = "config-ext")]
+pub fn ipset_name(
+family: Family,
+name: ,
+vmid: Option,
+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 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



[pve-devel] [PATCH proxmox-firewall v3 20/39] nftables: expression: implement conversion traits for firewall config

2024-04-18 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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<> 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<> for Expression {
+fn from(value: ) -> Self {
+if value.len() == 1 {
+return Expression::from(value.first().unwrap());
+}
+
+Expression::set(value.iter().map(Expression::from))
+}
+}
+
+#[cfg(feature = "config-ext")]
+impl From<> for Expression {
+fn from(value: ) -> 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<> for Expression {
+fn from(value: ) -> Self {
+match value {
+IcmpType::Numeric(id) => Expression::from(*id),
+IcmpType::Named(name) => Expression::from(*name),
+}
+}
+}
+
+#[cfg(feature = "config-ext")]
+impl From<> for Expression {
+fn from(value: ) -> Self {
+match value {
+IcmpCode::Numeric(id) => Expression::from(*id),
+IcmpCode::Named(name) => Expression::from(*name),
+}
+}
+}
+
+#[cfg(feature = "config-ext")]
+impl From<> for Expression {
+fn from(value: ) -> Self {
+match value {
+Icmpv6Type::Numeric(id) => Expression::from(*id),
+Icmpv6Type::Named(name) => Expression::from(*name),
+}
+}
+}
+
+#[cfg(feature = "config-ext")]
+impl From<> for Expression {
+fn from(value: ) -> Self {
+match value {
+Icmpv6Code::Numeric(id) => Expression::from(*id),
+Icmpv6Code::Named(name) => Expression::from(*name),
+}
+}
+}
+
+#[cfg(feature = "config-ext")]
+impl From<> for Expression {
+fn from(value: ) -> 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<> for Expression {
+fn from(value: ) -> 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 

[pve-devel] [PATCH proxmox-firewall v3 11/39] config: firewall: add generic parser for firewall configs

2024-04-18 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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
+where
+O: Default + std::fmt::Debug + serde::de::DeserializeOwned,
+{
+pub(crate) options: O,
+pub(crate) rules: Vec,
+pub(crate) aliases: BTreeMap,
+pub(crate) ipsets: BTreeMap,
+pub(crate) groups: BTreeMap,
+}
+
+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,
+}
+
+impl Config
+where
+O: Default + std::fmt::Debug + serde::de::DeserializeOwned,
+{
+pub fn new() -> Self {
+Self::default()
+}
+
+pub fn parse(input: R, parser_cfg: ) -> 
Result {
+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( section, Sec::Options)?;
+} else if line.eq_ignore_ascii_case("[ALIASES]") {
+this.set_section( section, Sec::Aliases)?;
+} else if line.eq_ignore_ascii_case("[RULES]") {
+this.set_section( 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( 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( section, Sec::Group(name.to_owned(), 
group))?;
+} else if line.starts_with('[') {
+bail!("invalid section {line:?}");
+} else {
+match  section {
+Sec::None => bail!("config line with no section: 
{line:?}"),
+Sec::Options => Self::parse_option(line,  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( section, Sec::None)?;
+
+this.options = O::deserialize(IntoDeserializer::<
+'_,
+crate::firewall::parse::SerdeStringError,
+>::into_deserializer(options))?;
+
+Ok(this)
+}
+
+fn parse_option(line: , options:  HashMap) -> 
Result<(), Error> {
+let (key, value) = split_key_value(line)
+.ok_or_else(|| format_err!("expected colon separated key and 
value, found {lin

[pve-devel] [PATCH proxmox-firewall v3 12/39] config: firewall: add cluster-specific config + option types

2024-04-18 Thread Stefan Hanreich
Reviewed-by: Lukas Wagner 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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,
+}
+
+/// 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(input: R) -> Result {
+let parser_config = ParserConfig {
+guest_iface_names: false,
+ipset_scope: Some(IpsetScope::Datacenter),
+};
+
+Ok(Self {
+config: super::common::Config::parse(input, _config)?,
+})
+}
+
+pub fn rules() ->  {
+
+}
+
+pub fn groups() ->  {
+
+}
+
+pub fn ipsets() ->  {
+
+}
+
+pub fn alias(, name: ) -> Option<> {
+self.config.alias(name)
+}
+
+pub fn is_enabled() -> 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() -> 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(, 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() -> Option {
+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,
+
+#[serde(default, with = "serde_option_bool")]
+ebtables: Option,
+
+#[serde(default, with = "serde_option_log_ratelimit")]
+log_ratelimit: Option,
+
+policy_in: Option,
+policy_out: Option,
+}
+
+#[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:  = 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 ::/64
+
+[IPSET a-set]
+
+!5.5.5.5
+1.2.3.4/30
+dc/analias # a comment
+dc/wide
+::/96
+
+[RULES]
+
+GROUP tgr -i eth0 # acomm
+IN ACCEPT -p udp 

[pve-devel] [PATCH proxmox-firewall v3 17/39] nftables: add crate for libnftables bindings

2024-04-18 Thread Stefan Hanreich
Reviewed-by: Lukas Wagner 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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 ",
+"Stefan Hanreich ",
+"Proxmox Support Team ",
+]
+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 000..e69de29
-- 
2.39.2


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



[pve-devel] [PATCH proxmox-firewall v3 08/39] config: firewall: add types for ipsets

2024-04-18 Thread Stefan Hanreich
Reviewed-by: Lukas Wagner 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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: ) -> Result {
+Ok(match s {
+"+dc" => IpsetScope::Datacenter,
+"+guest" => IpsetScope::Guest,
+_ => bail!("invalid scope for ipset: {s}"),
+})
+}
+}
+
+impl Display for IpsetScope {
+fn fmt(, f:  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) -> Self {
+Self {
+scope,
+name: name.into(),
+}
+}
+
+pub fn name() ->  {
+
+}
+
+pub fn scope() -> IpsetScope {
+self.scope
+}
+}
+
+impl FromStr for IpsetName {
+type Err = Error;
+
+fn from_str(s: ) -> Result {
+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(, f:  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: ) -> Result {
+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> From 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,
+}
+
+impl> From 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: ) -> Result {
+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() -> i64 {
+self.index
+}
+
+pub fn ipset() ->  {
+self.ipset
+}
+
+pub fn name_for_index(index: i64) -> String {
+format!("ipfilter-

[pve-devel] [PATCH proxmox-firewall v3 02/39] config: firewall: add types for ip addresses

2024-04-18 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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 000..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(, f:  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, mask: u8) -> Result {
+Ok(Cidr::Ipv4(Ipv4Cidr::new(addr, mask)?))
+}
+
+pub fn new_v6(addr: impl Into, mask: u8) -> Result {
+Ok(Cidr::Ipv6(Ipv6Cidr::new(addr, mask)?))
+}
+
+pub const fn family() -> Family {
+match self {
+Cidr::Ipv4(_) => Family::V4,
+Cidr::Ipv6(_) => Family::V6,
+}
+}
+
+pub fn is_ipv4() -> bool {
+matches!(self, Cidr::Ipv4(_))
+}
+
+pub fn is_ipv6() -> bool {
+matches!(self, Cidr::Ipv6(_))
+}
+}
+
+impl fmt::Display for Cidr {
+fn fmt(, f:  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: ) -> Result {
+if let Ok(ip) = s.parse::() {
+return Ok(Cidr::Ipv4(ip));
+}
+
+if let Ok(ip) = s.parse::() {
+return Ok(Cidr::Ipv6(ip));
+}
+
+bail!("invalid ip address or CIDR: {s:?}");
+}
+}
+
+impl From for Cidr {
+fn from(cidr: Ipv4Cidr) -> Self {
+Cidr::Ipv4(cidr)
+}
+}
+
+impl From 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, mask: u8) -> Result {
+if mask > 32 {
+bail!("mask out of range for ipv4 cidr ({mask})");
+}
+
+Ok(Self {
+addr: addr.into(),
+mask,
+})
+}
+
+pub fn contains_address(, other: ) -> 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() ->  {
+
+}
+
+pub fn mask() -> u8 {
+self.mask
+}
+}
+
+impl> From 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: ) -> Result {
+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::()?, mask)?
+}
+})
+}
+}
+
+impl fmt::Display for Ipv4Cidr {
+fn fmt(, f: 

[pve-devel] [PATCH proxmox-firewall v3 05/39] config: firewall: add types for aliases

2024-04-18 Thread Stefan Hanreich
Reviewed-by: Lukas Wagner 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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: ) -> Option<(, )> {
+let end = line
+.as_bytes()
+.iter()
+.position(|| !(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: ) -> Option<(, )> {
+let (text, rest) = line
+.as_bytes()
+.iter()
+.position(|| 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: ) -> Result {
 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 000..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: ) -> Result {
+Ok(match s {
+"dc" => AliasScope::Datacenter,
+"guest" => AliasScope::Guest,
+_ => bail!("invalid scope for alias: {s}"),
+})
+}
+}
+
+impl Display for AliasScope {
+fn fmt(, f:  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(, f:  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: ) -> Result {
+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) -> Self {
+Self {
+scope,
+name: name.into(),
+}
+}
+
+pub fn name() ->  {
+
+}
+
+pub fn scope() ->  {
+
+}
+}
+
+#[derive(Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct Alias {
+name: String,
+address: Cidr,
+comment: Option,
+}
+
+impl Alias {
+pub fn new(
+name: impl Into,
+address: impl Into,
+comment: impl Into>,
+) -> Self {
+Self {
+   

[pve-devel] [PATCH container/docs/firewall/manager/proxmox-firewall/qemu-server v3 00/39] proxmox firewall nftables implementation

2024-04-18 Thread Stefan Hanreich
utomatically 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=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



[pve-devel] [PATCH proxmox-firewall v3 10/39] config: firewall: add types for security groups

2024-04-18 Thread Stefan Hanreich
Reviewed-by: Lukas Wagner 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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,
+comment: Option,
+}
+
+impl Group {
+pub const fn new() -> Self {
+Self {
+rules: Vec::new(),
+comment: None,
+}
+}
+
+pub fn rules() ->  {
+
+}
+
+pub fn comment() -> Option<> {
+self.comment.as_deref()
+}
+
+pub fn set_comment( self, comment: Option) {
+self.comment = comment;
+}
+
+pub(crate) fn parse_entry( self, line: ) -> 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



[pve-devel] [PATCH pve-container v2 36/39] firewall: add handling for new nft firewall

2024-04-17 Thread Stefan Hanreich
When the nftables firewall is enabled, we do not need to create
firewall bridges.

Signed-off-by: Stefan Hanreich 
---
 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



[pve-devel] [PATCH proxmox-firewall v2 16/39] config: firewall: add conntrack helper types

2024-04-17 Thread Stefan Hanreich
Reviewed-by: Lukas Wagner 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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 000..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,
+pub v6: Option,
+pub name: String,
+pub tcp: Option,
+pub udp: Option,
+}
+
+impl TryFrom for CtHelperMacro {
+type Error = Error;
+
+fn try_from(value: CtHelperMacroJson) -> Result {
+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,
+name: String,
+tcp: Option,
+udp: Option,
+}
+
+impl CtHelperMacro {
+fn helper_name(, protocol: ) -> String {
+format!("helper-{}-{protocol}", self.name)
+}
+
+pub fn tcp_helper_name() -> String {
+self.helper_name("tcp")
+}
+
+pub fn udp_helper_name() -> String {
+self.helper_name("udp")
+}
+
+pub fn family() -> Option {
+self.family
+}
+
+pub fn name() ->  {
+self.name.as_ref()
+}
+
+pub fn tcp() -> Option<> {
+self.tcp.as_ref()
+}
+
+pub fn udp() -> Option<> {
+self.udp.as_ref()
+}
+}
+
+fn hashmap() -> &'static HashMap {
+const MACROS:  = include_str!("../../resources/ct_helper.json");
+static HASHMAP: OnceLock> = OnceLock::new();
+
+HASHMAP.get_or_init(|| {
+let macro_data: Vec = 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 

[pve-devel] [PATCH proxmox-firewall v2 33/39] firewall: add files for debian packaging

2024-04-17 Thread Stefan Hanreich
Suggested-By: Fabian Grünbichler 
Signed-off-by: Stefan Hanreich 
---
 .gitignore  |  3 ++
 Makefile| 70 +
 debian/changelog|  5 +++
 debian/control  | 38 ++
 debian/copyright| 16 
 debian/postrm   | 14 +++
 debian/proxmox-firewall.install |  1 +
 debian/proxmox-firewall.service |  9 +
 debian/proxmox-firewall.timer   | 13 ++
 debian/rules| 32 +++
 debian/source/format|  1 +
 defines.mk  | 13 ++
 12 files changed, 215 insertions(+)
 create mode 100644 Makefile
 create mode 100644 debian/changelog
 create mode 100644 debian/control
 create mode 100644 debian/copyright
 create mode 100755 debian/postrm
 create mode 100644 debian/proxmox-firewall.install
 create mode 100644 debian/proxmox-firewall.service
 create mode 100644 debian/proxmox-firewall.timer
 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 000..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 000..3ca5833
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,5 @@
+rust-proxmox-firewall (0.1) UNRELEASED; urgency=medium
+
+  * Initial release.
+
+ -- Stefan Hanreich   Thu, 07 Mar 2024 10:15:10 +0100
diff --git a/debian/control b/debian/control
new file mode 100644
index 000..fe9467b
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,38 @@
+Source: rust-proxmox-firewall
+Section: admin
+Priority: optional
+Maintainer: Proxmox Support Team 
+Build-Depends: cargo:native,
+   debhelper-compat (= 13),
+   libnftables-dev,
+   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-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

[pve-devel] [PATCH proxmox-firewall v2 19/39] nftables: expression: add types

2024-04-17 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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),
+Set(Vec),
+Range(Box<(Expression, Expression)>),
+Map(Box),
+Prefix(Prefix),
+Payload(Payload),
+Meta(Meta),
+Ct(Ct),
+Elem(Box),
+
+#[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),
+
+#[serde(untagged)]
+Verdict(Verdict),
+
+#[serde(untagged)]
+Bool(bool),
+#[serde(untagged)]
+Number(i64),
+#[serde(untagged)]
+String(String),
+}
+
+impl Expression {
+pub fn set(expressions: impl IntoIterator) -> Self {
+Expression::Set(Vec::from_iter(expressions))
+}
+
+pub fn concat(expressions: impl IntoIterator) -> Self {
+Expression::Concat(Vec::from_iter(expressions))
+}
+}
+
+impl From for Expression {
+#[inline]
+fn from(v: bool) -> Self {
+Expression::Bool(v)
+}
+}
+
+impl From for Expression {
+#[inline]
+fn from(v: i64) -> Self {
+Expression::Number(v)
+}
+}
+
+impl From for Expression {
+#[inline]
+fn from(v: u16) -> Self {
+Expression::Number(v.into())
+}
+}
+
+impl From for Expression {
+#[inline]
+fn from(v: u8) -> Self {
+Expression::Number(v.into())
+}
+}
+
+impl From<> for Expression {
+#[inline]
+fn from(v: ) -> Self {
+Expression::String(v.to_string())
+}
+}
+
+impl From for Expression {
+#[inline]
+fn from(v: String) -> Self {
+Expression::String(v)
+}
+}
+
+impl From for Expression {
+#[inline]
+fn from(meta: Meta) -> Self {
+Expression::Meta(meta)
+}
+}
+
+impl From for Expression {
+#[inline]
+fn from(ct: Ct) -> Self {
+Expression::Ct(ct)
+}
+}
+
+impl From for Expression {
+#[inline]
+fn from(payload: Payload) -> Self {
+Expression::Payload(payload)
+}
+}
+
+impl From for Expression {
+#[inline]
+fn from(prefix: Prefix) -> Self {
+Expression::Prefix(prefix)
+}
+}
+
+impl From for Expression {
+#[inline]
+fn from(value: Verdict) -> Self {
+Expression::Verdict(value)
+}
+}
+
+impl From<> for Expression {
+fn from(value: ) -> Self {
+Expression::String(value.to_string())
+}
+}
+
+impl From<> for Expression {
+fn from(address: ) -> Self {
+Expression::String(address.to_string())
+}
+}
+
+impl From<> for Expression {
+fn from(address: ) -> 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) -> Self {
+Self { key: key.into() }
+}
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct Map {
+key: Expres

[pve-devel] [PATCH proxmox-firewall v2 29/39] firewall: add rule generation logic

2024-04-17 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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,
+statements: Vec,
+terminal_statements: Vec,
+}
+
+impl NftRule {
+pub fn from_terminal_statements(terminal_statements: Vec) -> 
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: , env: ) -> 
Result, Error> {
+let mut rules = Vec::new();
+
+if rule.disabled() {
+return Ok(rules);
+}
+
+rule.to_nft_rules( rules, env)?;
+
+Ok(rules)
+}
+
+pub fn from_ct_helper(
+ct_helper: ,
+env: ,
+) -> Result, Error> {
+let mut rules = Vec::new();
+ct_helper.to_nft_rules( rules, env)?;
+Ok(rules)
+}
+
+pub fn from_ipfilter(ipfilter: , env: ) -> 
Result, Error> {
+let mut rules = Vec::new();
+ipfilter.to_nft_rules( rules, env)?;
+Ok(rules)
+}
+}
+
+impl Deref for NftRule {
+type Target = Vec;
+
+fn deref() -> ::Target {
+
+}
+}
+
+impl DerefMut for NftRule {
+fn deref_mut( self) ->  Self::Target {
+ 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() -> Option {
+self.family
+}
+
+pub fn set_family( 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,
+}
+
+impl NftRuleEnv<'_> {
+fn alias(, name: ) -> Option<> {
+self.firewall_config.alias(name, self.vmid)
+}
+
+fn iface_name(, rule_iface: ) -> String {
+match  {
+Some(vmid) => {
+if let Some(config) = self.firewall_config.guests().get(vmid) 

[pve-devel] [PATCH proxmox-firewall v2 28/39] firewall: add config loader

2024-04-17 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 proxmox-firewall/Cargo.toml|   2 +
 proxmox-firewall/src/config.rs | 281 +
 proxmox-firewall/src/main.rs   |   3 +
 3 files changed, 286 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 000..f5df20f
--- /dev/null
+++ b/proxmox-firewall/src/config.rs
@@ -0,0 +1,281 @@
+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::NftCtx;
+
+pub trait FirewallConfigLoader {
+fn cluster() -> Option>;
+fn host() -> Option>;
+fn guest_list() -> GuestMap;
+fn guest_config(, vmid: , guest: ) -> Option>;
+fn guest_firewall_config(, vmid: ) -> Option>;
+}
+
+#[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: ) -> Result, 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:  = "/etc/pve/firewall/cluster.fw";
+const HOST_CONFIG_PATH:  = "/etc/pve/local/host.fw";
+
+impl FirewallConfigLoader for PveFirewallConfigLoader {
+fn cluster() -> Option> {
+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;
+return Some(buf_reader);
+}
+
+None
+}
+
+fn host() -> Option> {
+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;
+return Some(buf_reader);
+}
+
+None
+}
+
+fn guest_list() -> GuestMap {
+log::info!("loading vmlist");
+GuestMap::new().expect("able to read vmlist")
+}
+
+fn guest_config(, vmid: , entry: ) -> Option> {
+log::info!("loading guest #{vmid} config");
+
+let fd = open_config_file(::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;
+return Some(buf_reader);
+}
+
+None
+}
+
+fn guest_firewall_config(, vmid: ) -> Option> {
+log::info!("loading guest #{vmid} firewall config");
+
+let fd = open_config_file(::firewall_config_path(vmid))
+.expect("able to read guest firewall config");
+
+if let Some(file) = fd {
+let buf_reader = Box::new

[pve-devel] [PATCH pve-docs v2 39/39] firewall: add documentation for proxmox-firewall

2024-04-17 Thread Stefan Hanreich
Add a section that explains how to use the new nftables-based
proxmox-firewall.

Signed-off-by: Stefan Hanreich 
---
 pve-firewall.adoc | 162 ++
 1 file changed, 162 insertions(+)

diff --git a/pve-firewall.adoc b/pve-firewall.adoc
index a5e40f9..ac3d9ba 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,167 @@ Ports used by {pve}
 * corosync cluster traffic: 5405-5412 UDP
 * live migration (VM memory and local-disk data): 6-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//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.
+
+Additionally, 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 
examining
+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 to "no" / 0.
+
+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 that is triggered regularly via a
+timer, so you can start and stop it via `systemctl`:
+
+
+systemctl start proxmox-firewall.timer
+systemctl stop proxmox-firewall.timer
+
+
+To query the status of the firewall, you can query the status of the 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 binary once with
+the `RUST_LOG` environment variable set to `trace`. This should provide you 
with
+detailed debugging output as well as an error message in case something goes
+wrong.
+
+
+RUST_LOG=trace proxmox-firewall
+
+
+This writes the log to STDERR, you can redirect it with the following command
+(e.g. for submitting logs to the community forum):
+
+
+RUST_LOG=trace proxmox-firewall 2> firewall_log_$(hostname).txt
+
+
+Other, less verbose, log levels are `info` and `debug`.
+
+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
+
+ta

[pve-devel] [PATCH proxmox-firewall v2 24/39] nftables: types: add conversion traits

2024-04-17 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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() -> Vec {
+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 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.name.as_ref()
+}
+
+#[cfg(feature = "config-ext")]
+pub fn ipset_name(
+family: Family,
+name: ,
+vmid: Option,
+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 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



[pve-devel] [PATCH proxmox-firewall v2 22/39] nftables: statement: add conversion traits for config types

2024-04-17 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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> From for Statement {
 }
 }
 
-#[derive(Clone, Debug, Deserialize, Serialize)]
+#[cfg(feature = "config-ext")]
+impl From 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>,
+log_level: LogLevel,
+chain_name: ,
+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 for LogLevel {
+type Error = Error;
+
+fn try_from(value: ConfigLogLevel) -> Result {
+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() -> u8 {
 match self {
@@ -231,6 +286,20 @@ pub struct AnonymousLimit {
 pub inv: Option,
 }
 
+#[cfg(feature = "config-ext")]
+impl From 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



[pve-devel] [PATCH proxmox-firewall v2 34/39] firewall: add integration test

2024-04-17 Thread Stefan Hanreich
Signed-off-by: Stefan Hanreich 
---
 .gitignore|1 +
 debian/control|1 +
 proxmox-firewall/Cargo.toml   |4 +
 proxmox-firewall/src/lib.rs   |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  |1 +
 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 +
 13 files changed, 3742 insertions(+)
 create mode 100644 proxmox-firewall/src/lib.rs
 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 fe9467b..174375e 100644
--- a/debian/control
+++ b/debian/control
@@ -19,6 +19,7 @@ Build-Depends: cargo:native,
librust-serde-with+default-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 1e6a4b8..686aa16 100644
--- a/proxmox-firewall/Cargo.toml
+++ b/proxmox-firewall/Cargo.toml
@@ -20,3 +20,7 @@ serde_json = "1"
 
 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/src/lib.rs b/proxmox-firewall/src/lib.rs
new file mode 100644
index 000..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/tests/input/100.conf 
b/proxmox-firewall/tests/input/100.conf
new file mode 100644
index 000..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 000..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 000..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-18b721a6
+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 000..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 000..327c295
--- /dev/null
+++ b/proxmox-firewall/tests/input/chains.json
@@ -0,0 +1 @@
+{"nfta

[pve-devel] [PATCH proxmox-firewall v2 23/39] nftables: commands: add types

2024-04-17 Thread Stefan Hanreich
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 
---
 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 000..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,
+}
+
+impl Commands {
+pub fn new(commands: Vec) -> Self {
+Self { nftables: commands }
+}
+}
+
+impl Deref for Commands {
+type Target = Vec;
+
+fn deref() -> ::Target {
+
+}
+}
+
+impl DerefMut for Commands {
+fn deref_mut( self) ->  Self::Target {
+ 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) -> Command {
+Command::Add(Add::Table(table.into()))
+}
+
+#[inline]
+pub fn chain(chain: impl Into) -> Command {
+Command::Add(Add::Chain(chain.into()))
+}
+
+#[inline]
+pub fn rule(rule: impl Into) -> Command {
+Command::Add(Add::Rule(rule.into()))
+}
+
+#[inline]
+pub fn set(set: impl Into) -> Command {
+Command::Add(Add::Set(set.into()))
+}
+
+#[inline]
+pub fn map(map: impl Into) -> Command {
+Command::Add(Add::Map(map.into()))
+}
+
+#[inline]
+pub fn limit(limit: impl Into) -> Command {
+Command::Add(Add::Limit(limit.into()))
+}
+
+#[inline]
+pub fn element(element: impl Into) -> Command {
+Command::Add(Add::Element(element.into()))
+}
+
+#[inline]
+pub fn ct_helper(ct_helper: impl Into) -> 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) -> Command {
+Command::Flush(Flush::Table(table.into()))
+}
+
+#[inline]
+pub fn chain(chain: impl Into) -> Command {
+Command::Flush(Flush::Chain(chain.into()))
+}
+
+#[inline]
+pub fn set(set: impl Into) -> Command {
+Command::Flush(Flush::Set(set.into()))
+}
+
+#[inline]
+pub fn map(map: impl Into) -> Command {
+Command::Flush(Flush::Map(map.into()))
+}
+
+#[inline]
+pub fn ruleset() -> Command {
+Command::Flush(Flush::Ruleset(Null))
+}
+}
+
+impl From for Flush {
+#[inline]
+fn from(value: TableName) -> Self {
+Flush::Table(value)
+}
+}
+
+impl From for Flush {
+#[inline]
+fn from(value: ChainName) -> Self {
+Flush::Chain(value)
+}
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum 

[pve-devel] [PATCH qemu-server v2 35/39] firewall: add handling for new nft firewall

2024-04-17 Thread Stefan Hanreich
When the nftables firewall is enabled, we do not need to create
firewall bridges.

Signed-off-by: Stefan Hanreich 
---
 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



[pve-devel] [PATCH proxmox-firewall v2 09/39] config: firewall: add types for rules

2024-04-17 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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: ) -> Result {
 },
 )
 }
+
+/// `` 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(self, visitor: V) -> Result
+where
+V: serde::de::Visitor<'de>,
+{
+self.0.deserialize_any(visitor)
+}
+
+fn deserialize_option(self, visitor: V) -> Result
+where
+V: serde::de::Visitor<'de>,
+{
+visitor.visit_some(self.0)
+}
+
+fn deserialize_str(self, visitor: V) -> Result
+where
+V: serde::de::Visitor<'de>,
+{
+self.0.deserialize_str(visitor)
+}
+
+fn deserialize_string(self, visitor: V) -> Result
+where
+V: serde::de::Visitor<'de>,
+{
+self.0.deserialize_string(visitor)
+}
+
+fn deserialize_enum(
+self,
+_name: ,
+_variants: &'static [&'static str],
+visitor: V,
+) -> Result
+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
+}
+}
+
+/// `` 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(serde::de::value::StringDeserializer);
+
+impl<'de, E> serde::de::Deserializer<'de> for SomeStringDeserializer
+where
+E: serde::de::Error,
+{
+type Error = E;
+
+fn deserialize_any(self, visitor: V) -> Result
+where
+V: serde::de::Visitor<'de>,
+{
+self.0.deserialize_any(visitor)
+}
+
+fn deserialize_option(self, visitor: V) -> Result
+where
+V: serde::de::Visitor<'de>,
+{
+visitor.visit_some(self.0)
+}
+
+fn deserialize_str(self, visitor: V) -> Result
+where
+V: serde::de::Visitor<'de>,
+{
+self.0.deserialize_str(visitor)
+}
+
+fn deserialize_string(self, visitor: V) -> Result
+where
+V: serde::de::Visitor<'de>,
+{
+self.0.deserialize_string(visitor)
+}
+
+fn deserialize_enum(
+self,
+_name: ,
+_variants: &'static [&'static str],
+visitor: V,
+) -> Result
+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

[pve-devel] [PATCH proxmox-firewall v2 07/39] config: guest: add helpers for parsing guest network config

2024-04-17 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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: ) -> Option<(, 
)> {
 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: ) -> Option<(, )> {
+let split_position = line.as_bytes().iter().position(|| 
!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: ) -> Result {
 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 000..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() -> &'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() -> bool {
+nodename() == self.node
+}
+
+pub fn ty() ->  {
+
+}
+}
+
+const VMLIST_CONFIG_PATH:  = "/etc/pve/.vmlist";
+
+#[derive(Deserialize)]
+pub struct GuestMap {
+#[serde(rename = "version")]
+_version: usize,
+#[serde(rename = "ids", default)]
+guests: HashMap,
+}
+
+impl From> for GuestMap {
+fn from(guests: HashMap) -> Self {
+Self {
+guests,
+_version: Default::default(),
+}
+}
+}
+
+impl Deref for GuestMap {
+type Target = HashMap;
+
+fn deref() -> ::Target {
+
+}
+}
+
+impl GuestMap {
+pub fn new() -> Result {
+let data = std::fs::read(VMLIST_CONFIG_PATH)
+.with_context(|| format!("failed to read guest map from 
{VMLIST_CONFIG_PATH}"))?;
+
+serde_json::from_slice().with_context(|| "failed to parse guest 
map".to_owned())
+}
+
+pub fn firewall_config_path(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: , entry: ) -> 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 000..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, Deb

[pve-devel] [PATCH proxmox-firewall v2 15/39] config: firewall: add firewall macros

2024-04-17 Thread Stefan Hanreich
Reviewed-by: Lukas Wagner 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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)"
+  },
+  "Dist

[pve-devel] [PATCH proxmox-firewall v2 20/39] nftables: expression: implement conversion traits for firewall config

2024-04-17 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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<> 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<> for Expression {
+fn from(value: ) -> Self {
+if value.len() == 1 {
+return Expression::from(value.first().unwrap());
+}
+
+Expression::set(value.iter().map(Expression::from))
+}
+}
+
+#[cfg(feature = "config-ext")]
+impl From<> for Expression {
+fn from(value: ) -> 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<> for Expression {
+fn from(value: ) -> Self {
+match value {
+IcmpType::Numeric(id) => Expression::from(*id),
+IcmpType::Named(name) => Expression::from(*name),
+}
+}
+}
+
+#[cfg(feature = "config-ext")]
+impl From<> for Expression {
+fn from(value: ) -> Self {
+match value {
+IcmpCode::Numeric(id) => Expression::from(*id),
+IcmpCode::Named(name) => Expression::from(*name),
+}
+}
+}
+
+#[cfg(feature = "config-ext")]
+impl From<> for Expression {
+fn from(value: ) -> Self {
+match value {
+Icmpv6Type::Numeric(id) => Expression::from(*id),
+Icmpv6Type::Named(name) => Expression::from(*name),
+}
+}
+}
+
+#[cfg(feature = "config-ext")]
+impl From<> for Expression {
+fn from(value: ) -> Self {
+match value {
+Icmpv6Code::Numeric(id) => Expression::from(*id),
+Icmpv6Code::Named(name) => Expression::from(*name),
+}
+}
+}
+
+#[cfg(feature = "config-ext")]
+impl From<> for Expression {
+fn from(value: ) -> 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<> for Expression {
+fn from(value: ) -> 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 

[pve-devel] [PATCH proxmox-firewall v2 26/39] firewall: add firewall crate

2024-04-17 Thread Stefan Hanreich
Reviewed-by: Lukas Wagner 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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 ",
+"Stefan Hanreich ",
+"Proxmox Support Team ",
+]
+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 000..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



[pve-devel] [PATCH proxmox-firewall v2 11/39] config: firewall: add generic parser for firewall configs

2024-04-17 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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
+where
+O: Default + std::fmt::Debug + serde::de::DeserializeOwned,
+{
+pub(crate) options: O,
+pub(crate) rules: Vec,
+pub(crate) aliases: BTreeMap,
+pub(crate) ipsets: BTreeMap,
+pub(crate) groups: BTreeMap,
+}
+
+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,
+}
+
+impl Config
+where
+O: Default + std::fmt::Debug + serde::de::DeserializeOwned,
+{
+pub fn new() -> Self {
+Self::default()
+}
+
+pub fn parse(input: R, parser_cfg: ) -> 
Result {
+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( section, Sec::Options)?;
+} else if line.eq_ignore_ascii_case("[ALIASES]") {
+this.set_section( section, Sec::Aliases)?;
+} else if line.eq_ignore_ascii_case("[RULES]") {
+this.set_section( 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( 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( section, Sec::Group(name.to_owned(), 
group))?;
+} else if line.starts_with('[') {
+bail!("invalid section {line:?}");
+} else {
+match  section {
+Sec::None => bail!("config line with no section: 
{line:?}"),
+Sec::Options => Self::parse_option(line,  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( section, Sec::None)?;
+
+this.options = O::deserialize(IntoDeserializer::<
+'_,
+crate::firewall::parse::SerdeStringError,
+>::into_deserializer(options))?;
+
+Ok(this)
+}
+
+fn parse_option(line: , options:  HashMap) -> 
Result<(), Error> {
+let (key, value) = split_key_value(line)
+.ok_or_else(|| format_err!("expected colon separated key and 
value, found {lin

[pve-devel] [PATCH proxmox-firewall v2 13/39] config: firewall: add host specific config + option types

2024-04-17 Thread Stefan Hanreich
Reviewed-by: Lukas Wagner 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..2fd1f36
--- /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,
+
+#[serde(default, with = "parse::serde_option_bool")]
+nftables: Option,
+
+log_level_in: Option,
+log_level_out: Option,
+
+#[serde(default, with = "parse::serde_option_bool")]
+log_nf_conntrack: Option,
+#[serde(default, with = "parse::serde_option_bool")]
+ndp: Option,
+
+#[serde(default, with = "parse::serde_option_bool")]
+nf_conntrack_allow_invalid: Option,
+
+// is Option> for easier deserialization
+#[serde(default, with = "parse::serde_option_conntrack_helpers")]
+nf_conntrack_helpers: Option>,
+
+#[serde(default, with = "parse::serde_option_number")]
+nf_conntrack_max: Option,
+#[serde(default, with = "parse::serde_option_number")]
+nf_conntrack_tcp_timeout_established: Option,
+#[serde(default, with = "parse::serde_option_number")]
+nf_conntrack_tcp_timeout_syn_recv: Option,
+
+#[serde(default, with = "parse::serde_option_bool")]
+nosmurfs: Option,
+
+#[serde(default, with = "parse::serde_option_bool")]
+protection_synflood: Option,
+#[serde(default, with = "parse::serde_option_number")]
+protection_synflood_burst: Option,
+#[serde(default, with = "parse::serde_option_number")]
+protection_synflood_rate: Option,
+
+smurf_log_level: Option,
+tcp_flags_log_level: Option,
+
+#[serde(default, with = "parse::serde_option_bool")]
+tcpflags: Option,
+}
+
+#[derive(Debug, Default)]
+pub struct Config {
+pub(crate) config: super::common::Config,
+}
+
+impl Config {
+pub fn new() -> Self {
+Self {
+config: Default::default(),
+}
+}
+
+pub fn parse(input: R) -> Result {
+let config = super::common::Config::parse(input, ::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() -> &[Rule] {
+
+}
+
+pub fn management_ips() -> Result, 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.conta

[pve-devel] [PATCH proxmox-firewall v2 14/39] config: firewall: add guest-specific config + option types

2024-04-17 Thread Stefan Hanreich
Reviewed-by: Lukas Wagner 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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,
+
+#[serde(default, with = "serde_option_bool")]
+enable: Option,
+
+#[serde(default, with = "serde_option_bool")]
+ipfilter: Option,
+
+#[serde(default, with = "serde_option_bool")]
+ndp: Option,
+
+#[serde(default, with = "serde_option_bool")]
+radv: Option,
+
+log_level_in: Option,
+log_level_out: Option,
+
+#[serde(default, with = "serde_option_bool")]
+macfilter: Option,
+
+#[serde(rename = "policy_in")]
+policy_in: Option,
+
+#[serde(rename = "policy_out")]
+policy_out: Option,
+}
+
+#[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,
+}
+
+impl Config {
+pub fn parse(
+vmid: ,
+iface_prefix: &'static str,
+firewall_input: T,
+network_input: U,
+) -> Result {
+let parser_cfg = super::common::ParserConfig {
+guest_iface_names: true,
+ipset_scope: Some(IpsetScope::Guest),
+};
+
+let config = super::common::Config::parse(firewall_input, 
_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() -> Vmid {
+self.vmid
+}
+
+pub fn alias(, name: ) -> Option<> {
+self.config.alias(name.name())
+}
+
+pub fn iface_name_by_key(, key: ) -> Result {
+let index = NetworkConfig::index_from_net_key(key)?;
+Ok(format!("{}{}i{index}", self.iface_prefix, self.vmid))
+}
+
+pub fn iface_name_by_index(, 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() -> bool {
+self.config.options.enable.unwrap_or(GUEST_ENABLED_DEFAULT)
+}
+
+pub fn rules() -> &[Rule] {
+
+}
+
+pub fn log_level(, 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() -> bool {
+self.config.options.ndp.unwrap_or(GUEST_ALLOW_NDP_DEFAULT)
+}
+
+/// retur

[pve-devel] [PATCH proxmox-firewall v2 08/39] config: firewall: add types for ipsets

2024-04-17 Thread Stefan Hanreich
Reviewed-by: Lukas Wagner 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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: ) -> Result {
+Ok(match s {
+"+dc" => IpsetScope::Datacenter,
+"+guest" => IpsetScope::Guest,
+_ => bail!("invalid scope for ipset: {s}"),
+})
+}
+}
+
+impl Display for IpsetScope {
+fn fmt(, f:  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) -> Self {
+Self {
+scope,
+name: name.into(),
+}
+}
+
+pub fn name() ->  {
+
+}
+
+pub fn scope() -> IpsetScope {
+self.scope
+}
+}
+
+impl FromStr for IpsetName {
+type Err = Error;
+
+fn from_str(s: ) -> Result {
+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(, f:  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: ) -> Result {
+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> From 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,
+}
+
+impl> From 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: ) -> Result {
+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() -> i64 {
+self.index
+}
+
+pub fn ipset() ->  {
+self.ipset
+}
+
+pub fn name_for_index(index: i64) -> String {
+format!("ipfilter-

[pve-devel] [PATCH proxmox-firewall v2 12/39] config: firewall: add cluster-specific config + option types

2024-04-17 Thread Stefan Hanreich
Reviewed-by: Lukas Wagner 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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,
+}
+
+/// 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(input: R) -> Result {
+let parser_config = ParserConfig {
+guest_iface_names: false,
+ipset_scope: Some(IpsetScope::Datacenter),
+};
+
+Ok(Self {
+config: super::common::Config::parse(input, _config)?,
+})
+}
+
+pub fn rules() ->  {
+
+}
+
+pub fn groups() ->  {
+
+}
+
+pub fn ipsets() ->  {
+
+}
+
+pub fn alias(, name: ) -> Option<> {
+self.config.alias(name)
+}
+
+pub fn is_enabled() -> 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() -> 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(, 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() -> Option {
+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,
+
+#[serde(default, with = "serde_option_bool")]
+ebtables: Option,
+
+#[serde(default, with = "serde_option_log_ratelimit")]
+log_ratelimit: Option,
+
+policy_in: Option,
+policy_out: Option,
+}
+
+#[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:  = 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 ::/64
+
+[IPSET a-set]
+
+!5.5.5.5
+1.2.3.4/30
+dc/analias # a comment
+dc/wide
+::/96
+
+[RULES]
+
+GROUP tgr -i eth0 # acomm
+IN ACCEPT -p udp 

[pve-devel] [PATCH proxmox-firewall v2 17/39] nftables: add crate for libnftables bindings

2024-04-17 Thread Stefan Hanreich
Reviewed-by: Lukas Wagner 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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 ",
+"Stefan Hanreich ",
+"Proxmox Support Team ",
+]
+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 000..e69de29
-- 
2.39.2


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



[pve-devel] [PATCH proxmox-firewall v2 27/39] firewall: add base ruleset

2024-04-17 Thread Stefan Hanreich
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 
Signed-off-by: Stefan Hanreich 
---
 .../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 000..67dd8c8
--- /dev/null
+++ b/proxmox-firewall/resources/proxmox-firewall.nft
@@ -0,0 +1,305 @@
+#!/usr/sbin/nft -f
+
+define ipv6_mask = :::::
+
+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

[pve-devel] [PATCH proxmox-firewall v2 31/39] firewall: add ruleset generation logic

2024-04-17 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 proxmox-firewall/Cargo.toml  |   3 +
 proxmox-firewall/src/firewall.rs | 899 +++
 proxmox-firewall/src/main.rs |   1 +
 3 files changed, 903 insertions(+)
 create mode 100644 proxmox-firewall/src/firewall.rs

diff --git a/proxmox-firewall/Cargo.toml b/proxmox-firewall/Cargo.toml
index 431e71a..1e6a4b8 100644
--- a/proxmox-firewall/Cargo.toml
+++ b/proxmox-firewall/Cargo.toml
@@ -15,5 +15,8 @@ log = "0.4"
 env_logger = "0.10"
 anyhow = "1"
 
+serde = { version = "1", features = [ "derive" ] }
+serde_json = "1"
+
 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 000..1279a81
--- /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:  = "proxmox-firewall";
+static HOST_TABLE_NAME:  = "proxmox-firewall";
+static GUEST_TABLE_NAME:  = "proxmox-firewall-guests";
+
+static NF_CONNTRACK_MAX_FILE:  = 
"/proc/sys/net/netfilter/nf_conntrack_max";
+static NF_CONNTRACK_TCP_TIMEOUT_ESTABLISHED:  =
+"/proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established";
+static NF_CONNTRACK_TCP_TIMEOUT_SYN_RECV:  =
+"/proc/sys/net/netfilter/nf_conntrack_tcp_timeout_syn_recv";
+static LOG_CONNTRACK_FILE:  = "/var/lib/pve-firewall/log_nf_conntrack";
+
+#[derive(Default)]
+pub struct Firewall {
+config: FirewallConfig,
+}
+
+impl From for Firewall {
+fn from(config: FirewallConfig) -> Self {
+Self { config }
+}
+}
+
+impl Firewall {
+pub fn new() -> Self {
+Self {
+..Default::default()
+}
+}
+
+pub fn is_enabled() -> 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: , 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 {
+ChainPa

[pve-devel] [PATCH pve-firewall v2 37/39] add configuration option for new nftables firewall

2024-04-17 Thread Stefan Hanreich
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 
---
 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



[pve-devel] [PATCH pve-manager v2 38/39] firewall: expose configuration option for new nftables firewall

2024-04-17 Thread Stefan Hanreich
Signed-off-by: Stefan Hanreich 
---
 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



[pve-devel] [PATCH proxmox-firewall v2 25/39] nftables: add libnftables bindings

2024-04-17 Thread Stefan Hanreich
Add a thin wrapper around libnftables, which can be used to run
commands defined by the rust types.

Reviewed-by: Lukas Wagner 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 proxmox-nftables/src/context.rs | 243 
 proxmox-nftables/src/error.rs   |  43 ++
 proxmox-nftables/src/lib.rs |   3 +
 3 files changed, 289 insertions(+)
 create mode 100644 proxmox-nftables/src/context.rs
 create mode 100644 proxmox-nftables/src/error.rs

diff --git a/proxmox-nftables/src/context.rs b/proxmox-nftables/src/context.rs
new file mode 100644
index 000..9ab51fb
--- /dev/null
+++ b/proxmox-nftables/src/context.rs
@@ -0,0 +1,243 @@
+use std::ffi::CString;
+use std::os::raw::{c_int, c_uint};
+use std::path::Path;
+
+use crate::command::{CommandOutput, Commands};
+use crate::error::NftError;
+
+#[rustfmt::skip]
+pub mod debug {
+use super::c_uint;
+
+pub const SCANNER: c_uint = 0x1;
+pub const PARSER : c_uint = 0x2;
+pub const EVALUATION : c_uint = 0x4;
+pub const NETLINK: c_uint = 0x8;
+pub const MNL: c_uint = 0x10;
+pub const PROTO_CTX  : c_uint = 0x20;
+pub const SEGTREE: c_uint = 0x40;
+}
+
+#[rustfmt::skip]
+pub mod output {
+use super::c_uint;
+
+pub const REVERSEDNS : c_uint = 1;
+pub const SERVICE: c_uint = 1 << 1;
+pub const STATELESS  : c_uint = 1 << 2;
+pub const HANDLE : c_uint = 1 << 3;
+pub const JSON   : c_uint = 1 << 4;
+pub const ECHO   : c_uint = 1 << 5;
+pub const GUID   : c_uint = 1 << 6;
+pub const NUMERIC_PROTO  : c_uint = 1 << 7;
+pub const NUMERIC_PRIO   : c_uint = 1 << 8;
+pub const NUMERIC_SYMBOL : c_uint = 1 << 9;
+pub const NUMERIC_TIME   : c_uint = 1 << 10;
+pub const TERSE  : c_uint = 1 << 11;
+
+pub const NUMERIC_ALL: c_uint = NUMERIC_PROTO | NUMERIC_PRIO | 
NUMERIC_SYMBOL;
+}
+
+#[link(name = "nftables")]
+extern "C" {
+fn nft_ctx_new(flags: u32) -> RawNftCtx;
+fn nft_ctx_free(ctx: RawNftCtx);
+
+//fn nft_ctx_get_dry_run(ctx: RawNftCtx) -> bool;
+fn nft_ctx_set_dry_run(ctx: RawNftCtx, dry: bool);
+
+fn nft_ctx_output_get_flags(ctx: RawNftCtx) -> c_uint;
+fn nft_ctx_output_set_flags(ctx: RawNftCtx, flags: c_uint);
+
+// fn nft_ctx_output_get_debug(ctx: RawNftCtx) -> c_uint;
+fn nft_ctx_output_set_debug(ctx: RawNftCtx, mask: c_uint);
+
+//fn nft_ctx_set_output(ctx: RawNftCtx, file: RawCFile) -> RawCFile;
+fn nft_ctx_buffer_output(ctx: RawNftCtx) -> c_int;
+fn nft_ctx_unbuffer_output(ctx: RawNftCtx) -> c_int;
+fn nft_ctx_get_output_buffer(ctx: RawNftCtx) -> *const i8;
+
+fn nft_ctx_buffer_error(ctx: RawNftCtx) -> c_int;
+fn nft_ctx_unbuffer_error(ctx: RawNftCtx) -> c_int;
+fn nft_ctx_get_error_buffer(ctx: RawNftCtx) -> *const i8;
+
+fn nft_run_cmd_from_buffer(ctx: RawNftCtx, buf: *const i8) -> c_int;
+fn nft_run_cmd_from_filename(ctx: RawNftCtx, filename: *const i8) -> c_int;
+}
+
+#[derive(Clone, Copy)]
+#[repr(transparent)]
+struct RawNftCtx(*mut u8);
+
+pub struct NftCtx(RawNftCtx);
+
+impl Drop for NftCtx {
+fn drop( self) {
+if !self.0 .0.is_null() {
+unsafe {
+nft_ctx_free(self.0);
+}
+}
+}
+}
+
+impl NftCtx {
+pub fn new() -> Result {
+let mut this = Self(unsafe { nft_ctx_new(0) });
+
+if this.0 .0.is_null() {
+return Err(NftError::msg("failed to instantiate nft context"));
+}
+
+this.enable_json();
+
+Ok(this)
+}
+
+fn modify_flags( self, func: impl FnOnce(c_uint) -> c_uint) {
+unsafe { nft_ctx_output_set_flags(self.0, 
func(nft_ctx_output_get_flags(self.0))) }
+}
+
+pub fn enable_debug( self) {
+unsafe { nft_ctx_output_set_debug(self.0, debug::PARSER | 
debug::SCANNER) }
+}
+
+pub fn disable_debug( self) {
+unsafe { nft_ctx_output_set_debug(self.0, 0) }
+}
+
+fn enable_json( self) {
+self.modify_flags(|flags| flags | output::JSON);
+}
+
+pub fn set_dry_run( self, on: bool) {
+unsafe { nft_ctx_set_dry_run(self.0, on) }
+}
+
+fn start_output_buffering( self) -> Result<(), NftError> {
+let rc = unsafe { nft_ctx_buffer_output(self.0) };
+NftError::expect_zero(rc, || "failed to start output buffering")
+}
+
+fn stop_output_buffering( self) {
+let _ = unsafe { nft_ctx_unbuffer_output(self.0) };
+// ignore errors
+}
+
+fn get_output_buffer( self) -> Result {
+let buf = unsafe { nft_ctx_get_output_buffer(self.0) };
+
+if buf.is_null() {
+return Err(NftError::msg("failed to get output buffer"));
+}
+
+

[pve-devel] [PATCH proxmox-firewall v2 30/39] firewall: add object generation logic

2024-04-17 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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,
+}
+
+impl NftObjectEnv<'_, '_> {
+pub(crate) fn alias(, name: ) -> Option<> {
+self.firewall_config.alias(name, self.vmid)
+}
+}
+
+pub(crate) trait ToNftObjects {
+fn to_nft_objects(, env: ) -> Result, 
Error>;
+}
+
+impl ToNftObjects for CtHelperMacro {
+fn to_nft_objects(, env: ) -> Result, 
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(, env: ) -> Result, 
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  {
+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( vec![
+Add::set(set_config),
+Flush::set(set_name.clone()),
+

[pve-devel] [PATCH proxmox-firewall v2 21/39] nftables: statement: add types

2024-04-17 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 proxmox-nftables/Cargo.toml   |   1 +
 proxmox-nftables/src/lib.rs   |   2 +
 proxmox-nftables/src/statement.rs | 321 ++
 proxmox-nftables/src/types.rs |  18 +-
 4 files changed, 341 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..153716d 100644
--- a/proxmox-nftables/Cargo.toml
+++ b/proxmox-nftables/Cargo.toml
@@ -15,6 +15,7 @@ config-ext = ["dep:proxmox-ve-config"]
 
 [dependencies]
 log = "0.4"
+anyhow = "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 000..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) -> Self {
+Statement::Verdict(Verdict::Jump {
+target: target.into(),
+})
+}
+
+pub fn goto(target: impl Into) -> Self {
+Statement::Verdict(Verdict::Goto {
+target: target.into(),
+})
+}
+}
+
+impl From for Statement {
+#[inline]
+fn from(m: Match) -> Statement {
+Statement::Match(m)
+}
+}
+
+impl From for Statement {
+#[inline]
+fn from(m: Mangle) -> Statement {
+Statement::Mangle(m)
+}
+}
+
+impl From for Statement {
+#[inline]
+fn from(m: Reject) -> Statement {
+Statement::Reject(m)
+}
+}
+
+impl From for Statement {
+#[inline]
+fn from(m: Set) -> Statement {
+Statement::Set(m)
+}
+}
+
+impl From for Statement {
+#[inline]
+fn from(m: Vmap) -> Statement {
+Statement::Vmap(m)
+}
+}
+
+impl From for Statement {
+#[inline]
+fn from(log: Log) -> Statement {
+Statement::Log(log)
+}
+}
+
+impl> From 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,
+#[serde(skip_serializing_if = "Option::is_none")]
+expr: Option,
+}
+
+#[derive(Clone, Debug, Default, Deserialize, Serialize)]
+#[serde(rename_all = "kebab-case")]
+pub struct Log {
+#[serde(skip_serializing_if = "Option::is_none")]
+prefix: Option,
+
+#[serde(skip_serializing_if = "Option::is_none")]
+group: Option,
+
+#[serde(skip_serializing_if = "Option::is_none")]
+snaplen: Option,
+
+#[serde(skip_serializing_if = "Option::is_none")]
+queue_threshold: Option,
+
+#[serde(skip_serializing_if = "Option::is_none")]
+level: Option,
+
+#[serde(default, skip_serializing_if = "Vec::is_empty")]
+flags: NfVec,
+}
+
+impl Log {
+pub fn new_nflog(prefix: Str

[pve-devel] [PATCH proxmox-firewall v2 32/39] firewall: add proxmox-firewall binary

2024-04-17 Thread Stefan Hanreich
Reviewed-by: Lukas Wagner 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 proxmox-firewall/src/main.rs | 34 ++
 1 file changed, 34 insertions(+)

diff --git a/proxmox-firewall/src/main.rs b/proxmox-firewall/src/main.rs
index 53c1289..bff71b9 100644
--- a/proxmox-firewall/src/main.rs
+++ b/proxmox-firewall/src/main.rs
@@ -5,7 +5,41 @@ mod firewall;
 mod object;
 mod rule;
 
+use firewall::Firewall;
+use proxmox_nftables::NftCtx;
+
+const RULE_BASE:  = include_str!("../resources/proxmox-firewall.nft");
+
 fn main() -> Result<(), Error> {
 env_logger::init();
+
+let mut nft = NftCtx::new()?;
+let firewall = Firewall::new();
+
+if !firewall.is_enabled() {
+log::info!("Removing existing firewall rules");
+let commands = firewall.remove_firewall();
+
+// can ignore failures, since it fails when table does not exist
+let _ = nft.run_commands();
+
+return Ok(());
+}
+
+let commands = firewall.full_host_fw()?;
+
+log::info!("Running proxmox-firewall.nft");
+let got = nft.run_nft_commands(RULE_BASE)?;
+log::info!("got response from nftables: {got:?}");
+
+log::info!("Running proxmox-firewall commands");
+
+for (idx, c) in commands.iter().enumerate() {
+log::debug!("cmd #{idx} {}", serde_json::to_string()?);
+}
+
+let got = nft.run_commands()?;
+log::info!("got response from nftables: {got:?}");
+
 Ok(())
 }
-- 
2.39.2


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



[pve-devel] [PATCH proxmox-firewall v2 02/39] config: firewall: add types for ip addresses

2024-04-17 Thread Stefan Hanreich
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 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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 000..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 000..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(, f:  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, mask: u8) -> Result {
+Ok(Cidr::Ipv4(Ipv4Cidr::new(addr, mask)?))
+}
+
+pub fn new_v6(addr: impl Into, mask: u8) -> Result {
+Ok(Cidr::Ipv6(Ipv6Cidr::new(addr, mask)?))
+}
+
+pub const fn family() -> Family {
+match self {
+Cidr::Ipv4(_) => Family::V4,
+Cidr::Ipv6(_) => Family::V6,
+}
+}
+
+pub fn is_ipv4() -> bool {
+matches!(self, Cidr::Ipv4(_))
+}
+
+pub fn is_ipv6() -> bool {
+matches!(self, Cidr::Ipv6(_))
+}
+}
+
+impl fmt::Display for Cidr {
+fn fmt(, f:  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: ) -> Result {
+if let Ok(ip) = s.parse::() {
+return Ok(Cidr::Ipv4(ip));
+}
+
+if let Ok(ip) = s.parse::() {
+return Ok(Cidr::Ipv6(ip));
+}
+
+bail!("invalid ip address or CIDR: {s:?}");
+}
+}
+
+impl From for Cidr {
+fn from(cidr: Ipv4Cidr) -> Self {
+Cidr::Ipv4(cidr)
+}
+}
+
+impl From 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, mask: u8) -> Result {
+if mask > 32 {
+bail!("mask out of range for ipv4 cidr ({mask})");
+}
+
+Ok(Self {
+addr: addr.into(),
+mask,
+})
+}
+
+pub fn contains_address(, other: ) -> 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() ->  {
+
+}
+
+pub fn mask() -> u8 {
+self.mask
+}
+}
+
+impl> From 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: ) -> Result {
+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::()?, mask)?
+}
+})
+}
+}
+
+impl fmt::Display for Ipv4Cidr {
+fn fmt(, f: 

[pve-devel] [PATCH proxmox-firewall v2 05/39] config: firewall: add types for aliases

2024-04-17 Thread Stefan Hanreich
Reviewed-by: Lukas Wagner 
Reviewed-by: Max Carrara 
Co-authored-by: Wolfgang Bumiller 
Signed-off-by: Stefan Hanreich 
---
 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: ) -> Option<(, )> {
+let end = line
+.as_bytes()
+.iter()
+.position(|| !(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: ) -> Option<(, )> {
+let (text, rest) = line
+.as_bytes()
+.iter()
+.position(|| 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: ) -> Result {
 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 000..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: ) -> Result {
+Ok(match s {
+"dc" => AliasScope::Datacenter,
+"guest" => AliasScope::Guest,
+_ => bail!("invalid scope for alias: {s}"),
+})
+}
+}
+
+impl Display for AliasScope {
+fn fmt(, f:  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(, f:  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: ) -> Result {
+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) -> Self {
+Self {
+scope,
+name: name.into(),
+}
+}
+
+pub fn name() ->  {
+
+}
+
+pub fn scope() ->  {
+
+}
+}
+
+#[derive(Debug)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+pub struct Alias {
+name: String,
+address: Cidr,
+comment: Option,
+}
+
+impl Alias {
+pub fn new(
+name: impl Into,
+address: impl Into,
+comment: impl Into>,
+) -> Self {
+Self {
+   

  1   2   3   4   5   >