[pve-devel] [PATCH proxmox-firewall 1/1] rules: allow vital ICMP(v6) types
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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 { +