* [pve-devel] [PATCH docs/firewall/manager/proxmox{-firewall, -perl-rs} v4 0/9] autogenerate ipsets for sdn objects
@ 2024-11-15 12:09 Stefan Hanreich
2024-11-15 12:09 ` [pve-devel] [PATCH proxmox-firewall v4 1/9] add proxmox-ve-rs crate - move proxmox-ve-config there Stefan Hanreich
` (9 more replies)
0 siblings, 10 replies; 24+ messages in thread
From: Stefan Hanreich @ 2024-11-15 12:09 UTC (permalink / raw)
To: pve-devel
This patch series adds support for autogenerating ipsets for SDN objects. It
autogenerates ipsets for every VNet as follows:
* ipset containing all IP ranges of the VNet
* ipset containing all gateways of the VNet
* ipset containing all IP ranges of the subnet - except gateways
* ipset containing all dhcp ranges of the vnet
Additionally it generates an IPSet for every guest that has one or more IPAM
entries in the pve IPAM.
Those can then be used in the cluster / host / guest firewalls. Firewall rules
automatically update on changes of the SDN / IPAM configuration. This patch
series works for the old firewall as well as the new firewall.
The ipsets in nftables currently get generated as named ipsets in every table,
this means that the `nft list ruleset` output can get quite crowded for large
SDN configurations or large IPAM databases. Another option would be to only
include them as anonymous IPsets in the rules, which would make the nft output
far less crowded but this way would use more memory when making extensive use of
the sdn ipsets, since everytime it is used in a rule we create an entirely new
ipset.
The base for proxmox-ve-rs (which is a filtered version of the proxmox-firewall
repository can be found here:)
staff/s.hanreich/proxmox-ve-rs.git master
Dependencies:
* proxmox-perl-rs and proxmox-firewall depend on proxmox-ve-rs
* pve-firewall depends on proxmox-perl-rs
* pve-manager depends on pve-firewall
Changes from v3 to v4:
* omitted proxmox-ve-rs since it is merged
* always load SDN configuration now when loading cluster config
* adapt is_nftables to check the flag file instead of reading the config
* gracefully fail when RPCEnvironment is not available
Changes from v2:
* rename end in IpRange to last to avoid confusion - thanks @Wolfgang
* bump Rust to 1.82 - thanks @Wolfgang
* improvements to the code generating IPSets - thanks @Wolfgang
* implement AsRef<str> for SDN name types - thanks @Wolfgang
* improve docstrings (proper capitalization and punctuation) - thanks @Wolfgang
* included a patch that removes proxmox-ve-config from proxmox-firewall
Changes from RFC:
* added documentation
* added separate SDN scope for IPSets
* rustfmt fixes
proxmox-firewall:
Stefan Hanreich (3):
add proxmox-ve-rs crate - move proxmox-ve-config there
config: tests: add support for loading sdn and ipam config
ipsets: autogenerate ipsets for vnets and ipam
Cargo.toml | 4 +-
Makefile | 2 +-
proxmox-firewall/Cargo.toml | 2 +-
proxmox-firewall/src/config.rs | 69 +
proxmox-firewall/src/firewall.rs | 22 +-
proxmox-firewall/src/object.rs | 41 +-
.../tests/input/.running-config.json | 45 +
proxmox-firewall/tests/input/ipam.db | 32 +
proxmox-firewall/tests/integration_tests.rs | 10 +
.../integration_tests__firewall.snap | 1288 +++++++++++++++++
proxmox-nftables/Cargo.toml | 2 +-
proxmox-nftables/src/expression.rs | 17 +-
proxmox-nftables/src/types.rs | 2 +-
proxmox-ve-config/Cargo.toml | 25 -
proxmox-ve-config/resources/ct_helper.json | 52 -
proxmox-ve-config/resources/macros.json | 923 ------------
proxmox-ve-config/src/firewall/cluster.rs | 374 -----
proxmox-ve-config/src/firewall/common.rs | 184 ---
proxmox-ve-config/src/firewall/ct_helper.rs | 115 --
proxmox-ve-config/src/firewall/fw_macros.rs | 69 -
proxmox-ve-config/src/firewall/guest.rs | 237 ---
proxmox-ve-config/src/firewall/host.rs | 372 -----
proxmox-ve-config/src/firewall/mod.rs | 10 -
proxmox-ve-config/src/firewall/parse.rs | 494 -------
proxmox-ve-config/src/firewall/ports.rs | 80 -
.../src/firewall/types/address.rs | 615 --------
proxmox-ve-config/src/firewall/types/alias.rs | 174 ---
proxmox-ve-config/src/firewall/types/group.rs | 36 -
proxmox-ve-config/src/firewall/types/ipset.rs | 349 -----
proxmox-ve-config/src/firewall/types/log.rs | 222 ---
proxmox-ve-config/src/firewall/types/mod.rs | 14 -
proxmox-ve-config/src/firewall/types/port.rs | 181 ---
proxmox-ve-config/src/firewall/types/rule.rs | 412 ------
.../src/firewall/types/rule_match.rs | 977 -------------
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/host/mod.rs | 1 -
proxmox-ve-config/src/host/utils.rs | 70 -
proxmox-ve-config/src/lib.rs | 3 -
40 files changed, 1517 insertions(+), 6671 deletions(-)
create mode 100644 proxmox-firewall/tests/input/.running-config.json
create mode 100644 proxmox-firewall/tests/input/ipam.db
delete mode 100644 proxmox-ve-config/Cargo.toml
delete mode 100644 proxmox-ve-config/resources/ct_helper.json
delete mode 100644 proxmox-ve-config/resources/macros.json
delete mode 100644 proxmox-ve-config/src/firewall/cluster.rs
delete mode 100644 proxmox-ve-config/src/firewall/common.rs
delete mode 100644 proxmox-ve-config/src/firewall/ct_helper.rs
delete mode 100644 proxmox-ve-config/src/firewall/fw_macros.rs
delete mode 100644 proxmox-ve-config/src/firewall/guest.rs
delete mode 100644 proxmox-ve-config/src/firewall/host.rs
delete mode 100644 proxmox-ve-config/src/firewall/mod.rs
delete mode 100644 proxmox-ve-config/src/firewall/parse.rs
delete mode 100644 proxmox-ve-config/src/firewall/ports.rs
delete mode 100644 proxmox-ve-config/src/firewall/types/address.rs
delete mode 100644 proxmox-ve-config/src/firewall/types/alias.rs
delete mode 100644 proxmox-ve-config/src/firewall/types/group.rs
delete mode 100644 proxmox-ve-config/src/firewall/types/ipset.rs
delete mode 100644 proxmox-ve-config/src/firewall/types/log.rs
delete mode 100644 proxmox-ve-config/src/firewall/types/mod.rs
delete mode 100644 proxmox-ve-config/src/firewall/types/port.rs
delete mode 100644 proxmox-ve-config/src/firewall/types/rule.rs
delete mode 100644 proxmox-ve-config/src/firewall/types/rule_match.rs
delete mode 100644 proxmox-ve-config/src/guest/mod.rs
delete mode 100644 proxmox-ve-config/src/guest/types.rs
delete mode 100644 proxmox-ve-config/src/guest/vm.rs
delete mode 100644 proxmox-ve-config/src/host/mod.rs
delete mode 100644 proxmox-ve-config/src/host/utils.rs
delete mode 100644 proxmox-ve-config/src/lib.rs
pve-firewall:
Stefan Hanreich (3):
add support for loading sdn firewall configuration
nftables: make is_nftables check flag file instead of config
api: load sdn ipsets
src/PVE/API2/Firewall/Aliases.pm | 2 +
src/PVE/API2/Firewall/Cluster.pm | 7 ++-
src/PVE/API2/Firewall/Groups.pm | 1 +
src/PVE/API2/Firewall/Host.pm | 1 +
src/PVE/API2/Firewall/IPSet.pm | 2 +
src/PVE/API2/Firewall/Rules.pm | 2 +
src/PVE/API2/Firewall/VM.pm | 5 ++-
src/PVE/Firewall.pm | 76 +++++++++++++++++++++++++++-----
src/PVE/Service/pve_firewall.pm | 4 +-
9 files changed, 84 insertions(+), 16 deletions(-)
proxmox-perl-rs:
Stefan Hanreich (1):
add PVE::RS::Firewall::SDN module
pve-rs/Cargo.toml | 1 +
pve-rs/Makefile | 1 +
pve-rs/src/firewall/mod.rs | 1 +
pve-rs/src/firewall/sdn.rs | 130 +++++++++++++++++++++++++++++++++++++
pve-rs/src/lib.rs | 1 +
5 files changed, 134 insertions(+)
create mode 100644 pve-rs/src/firewall/mod.rs
create mode 100644 pve-rs/src/firewall/sdn.rs
pve-manager:
Stefan Hanreich (1):
firewall: add sdn scope to IPRefSelector
www/manager6/form/IPRefSelector.js | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
pve-docs:
Stefan Hanreich (1):
sdn: add documentation for firewall integration
pvesdn.adoc | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 92 insertions(+)
Summary over all repositories:
56 files changed, 1834 insertions(+), 6688 deletions(-)
--
Generated by git-murpp 0.6.0
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 24+ messages in thread
* [pve-devel] [PATCH proxmox-firewall v4 1/9] add proxmox-ve-rs crate - move proxmox-ve-config there
2024-11-15 12:09 [pve-devel] [PATCH docs/firewall/manager/proxmox{-firewall, -perl-rs} v4 0/9] autogenerate ipsets for sdn objects Stefan Hanreich
@ 2024-11-15 12:09 ` Stefan Hanreich
2024-11-17 14:08 ` [pve-devel] applied: " Thomas Lamprecht
2024-11-15 12:09 ` [pve-devel] [PATCH proxmox-firewall v4 2/9] config: tests: add support for loading sdn and ipam config Stefan Hanreich
` (8 subsequent siblings)
9 siblings, 1 reply; 24+ messages in thread
From: Stefan Hanreich @ 2024-11-15 12:09 UTC (permalink / raw)
To: pve-devel; +Cc: Wolfgang Bumiller
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Reviewed-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
Cargo.toml | 4 +-
Makefile | 2 +-
proxmox-firewall/Cargo.toml | 2 +-
proxmox-nftables/Cargo.toml | 2 +-
proxmox-ve-config/Cargo.toml | 25 -
proxmox-ve-config/resources/ct_helper.json | 52 -
proxmox-ve-config/resources/macros.json | 923 -----------------
proxmox-ve-config/src/firewall/cluster.rs | 374 -------
proxmox-ve-config/src/firewall/common.rs | 184 ----
proxmox-ve-config/src/firewall/ct_helper.rs | 115 ---
proxmox-ve-config/src/firewall/fw_macros.rs | 69 --
proxmox-ve-config/src/firewall/guest.rs | 237 -----
proxmox-ve-config/src/firewall/host.rs | 372 -------
proxmox-ve-config/src/firewall/mod.rs | 10 -
proxmox-ve-config/src/firewall/parse.rs | 494 ---------
proxmox-ve-config/src/firewall/ports.rs | 80 --
.../src/firewall/types/address.rs | 615 -----------
proxmox-ve-config/src/firewall/types/alias.rs | 174 ----
proxmox-ve-config/src/firewall/types/group.rs | 36 -
proxmox-ve-config/src/firewall/types/ipset.rs | 349 -------
proxmox-ve-config/src/firewall/types/log.rs | 222 ----
proxmox-ve-config/src/firewall/types/mod.rs | 14 -
proxmox-ve-config/src/firewall/types/port.rs | 181 ----
proxmox-ve-config/src/firewall/types/rule.rs | 412 --------
.../src/firewall/types/rule_match.rs | 977 ------------------
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/host/mod.rs | 1 -
proxmox-ve-config/src/host/utils.rs | 70 --
proxmox-ve-config/src/lib.rs | 3 -
31 files changed, 6 insertions(+), 6656 deletions(-)
delete mode 100644 proxmox-ve-config/Cargo.toml
delete mode 100644 proxmox-ve-config/resources/ct_helper.json
delete mode 100644 proxmox-ve-config/resources/macros.json
delete mode 100644 proxmox-ve-config/src/firewall/cluster.rs
delete mode 100644 proxmox-ve-config/src/firewall/common.rs
delete mode 100644 proxmox-ve-config/src/firewall/ct_helper.rs
delete mode 100644 proxmox-ve-config/src/firewall/fw_macros.rs
delete mode 100644 proxmox-ve-config/src/firewall/guest.rs
delete mode 100644 proxmox-ve-config/src/firewall/host.rs
delete mode 100644 proxmox-ve-config/src/firewall/mod.rs
delete mode 100644 proxmox-ve-config/src/firewall/parse.rs
delete mode 100644 proxmox-ve-config/src/firewall/ports.rs
delete mode 100644 proxmox-ve-config/src/firewall/types/address.rs
delete mode 100644 proxmox-ve-config/src/firewall/types/alias.rs
delete mode 100644 proxmox-ve-config/src/firewall/types/group.rs
delete mode 100644 proxmox-ve-config/src/firewall/types/ipset.rs
delete mode 100644 proxmox-ve-config/src/firewall/types/log.rs
delete mode 100644 proxmox-ve-config/src/firewall/types/mod.rs
delete mode 100644 proxmox-ve-config/src/firewall/types/port.rs
delete mode 100644 proxmox-ve-config/src/firewall/types/rule.rs
delete mode 100644 proxmox-ve-config/src/firewall/types/rule_match.rs
delete mode 100644 proxmox-ve-config/src/guest/mod.rs
delete mode 100644 proxmox-ve-config/src/guest/types.rs
delete mode 100644 proxmox-ve-config/src/guest/vm.rs
delete mode 100644 proxmox-ve-config/src/host/mod.rs
delete mode 100644 proxmox-ve-config/src/host/utils.rs
delete mode 100644 proxmox-ve-config/src/lib.rs
diff --git a/Cargo.toml b/Cargo.toml
index 1fbc2e0..3894d9f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,9 @@
[workspace]
members = [
- "proxmox-ve-config",
"proxmox-nftables",
"proxmox-firewall",
]
resolver = "2"
+
+[workspace.dependencies]
+proxmox-ve-config = { version = "0.1.0" }
diff --git a/Makefile b/Makefile
index e49e58f..a134423 100644
--- a/Makefile
+++ b/Makefile
@@ -29,7 +29,7 @@ cargo-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/
+ cp -a proxmox-firewall proxmox-nftables debian Cargo.toml Makefile defines.mk $@.tmp/
mv $@.tmp $@
.PHONY: deb
diff --git a/proxmox-firewall/Cargo.toml b/proxmox-firewall/Cargo.toml
index 6cb1b09..c2adcac 100644
--- a/proxmox-firewall/Cargo.toml
+++ b/proxmox-firewall/Cargo.toml
@@ -21,7 +21,7 @@ serde_json = "1"
signal-hook = "0.3"
proxmox-nftables = { path = "../proxmox-nftables", features = ["config-ext"] }
-proxmox-ve-config = { path = "../proxmox-ve-config" }
+proxmox-ve-config = { workspace = true }
[dev-dependencies]
insta = { version = "1.21", features = ["json"] }
diff --git a/proxmox-nftables/Cargo.toml b/proxmox-nftables/Cargo.toml
index e84509d..4ff6f41 100644
--- a/proxmox-nftables/Cargo.toml
+++ b/proxmox-nftables/Cargo.toml
@@ -22,4 +22,4 @@ serde = { version = "1", features = [ "derive" ] }
serde_json = "1"
serde_plain = "1"
-proxmox-ve-config = { path = "../proxmox-ve-config", optional = true }
+proxmox-ve-config = { workspace = true, optional = true }
diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml
deleted file mode 100644
index 0239c08..0000000
--- a/proxmox-ve-config/Cargo.toml
+++ /dev/null
@@ -1,25 +0,0 @@
-[package]
-name = "proxmox-ve-config"
-version = "0.1.0"
-edition = "2021"
-authors = [
- "Wolfgang Bumiller <w.bumiller@proxmox.com>",
- "Stefan Hanreich <s.hanreich@proxmox.com>",
- "Proxmox Support Team <support@proxmox.com>",
-]
-description = "Proxmox VE config parsing"
-license = "AGPL-3"
-
-[dependencies]
-log = "0.4"
-anyhow = "1"
-nix = "0.26"
-
-serde = { version = "1", features = [ "derive" ] }
-serde_json = "1"
-serde_plain = "1"
-serde_with = "3"
-
-proxmox-schema = "3.1.2"
-proxmox-sys = "0.6"
-proxmox-sortable-macro = "0.1.3"
diff --git a/proxmox-ve-config/resources/ct_helper.json b/proxmox-ve-config/resources/ct_helper.json
deleted file mode 100644
index 5e70a3a..0000000
--- a/proxmox-ve-config/resources/ct_helper.json
+++ /dev/null
@@ -1,52 +0,0 @@
-[
- {
- "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/resources/macros.json b/proxmox-ve-config/resources/macros.json
deleted file mode 100644
index 2fcc0fb..0000000
--- a/proxmox-ve-config/resources/macros.json
+++ /dev/null
@@ -1,923 +0,0 @@
-{
- "Amanda": {
- "code": [
- {
- "dport": "10080",
- "proto": "udp"
- },
- {
- "dport": "10080",
- "proto": "tcp"
- }
- ],
- "desc": "Amanda Backup"
- },
- "Auth": {
- "code": [
- {
- "dport": "113",
- "proto": "tcp"
- }
- ],
- "desc": "Auth (identd) traffic"
- },
- "BGP": {
- "code": [
- {
- "dport": "179",
- "proto": "tcp"
- }
- ],
- "desc": "Border Gateway Protocol traffic"
- },
- "BitTorrent": {
- "code": [
- {
- "dport": "6881:6889",
- "proto": "tcp"
- },
- {
- "dport": "6881",
- "proto": "udp"
- }
- ],
- "desc": "BitTorrent traffic for BitTorrent 3.1 and earlier"
- },
- "BitTorrent32": {
- "code": [
- {
- "dport": "6881:6999",
- "proto": "tcp"
- },
- {
- "dport": "6881",
- "proto": "udp"
- }
- ],
- "desc": "BitTorrent traffic for BitTorrent 3.2 and later"
- },
- "CVS": {
- "code": [
- {
- "dport": "2401",
- "proto": "tcp"
- }
- ],
- "desc": "Concurrent Versions System pserver traffic"
- },
- "Ceph": {
- "code": [
- {
- "dport": "6789",
- "proto": "tcp"
- },
- {
- "dport": "3300",
- "proto": "tcp"
- },
- {
- "dport": "6800:7300",
- "proto": "tcp"
- }
- ],
- "desc": "Ceph Storage Cluster traffic (Ceph Monitors, OSD & MDS Daemons)"
- },
- "Citrix": {
- "code": [
- {
- "dport": "1494",
- "proto": "tcp"
- },
- {
- "dport": "1604",
- "proto": "udp"
- },
- {
- "dport": "2598",
- "proto": "tcp"
- }
- ],
- "desc": "Citrix/ICA traffic (ICA, ICA Browser, CGP)"
- },
- "DAAP": {
- "code": [
- {
- "dport": "3689",
- "proto": "tcp"
- },
- {
- "dport": "3689",
- "proto": "udp"
- }
- ],
- "desc": "Digital Audio Access Protocol traffic (iTunes, Rythmbox daemons)"
- },
- "DCC": {
- "code": [
- {
- "dport": "6277",
- "proto": "tcp"
- }
- ],
- "desc": "Distributed Checksum Clearinghouse spam filtering mechanism"
- },
- "DHCPfwd": {
- "code": [
- {
- "dport": "67:68",
- "proto": "udp",
- "sport": "67:68"
- }
- ],
- "desc": "Forwarded DHCP traffic"
- },
- "DHCPv6": {
- "code": [
- {
- "dport": "546:547",
- "proto": "udp",
- "sport": "546:547"
- }
- ],
- "desc": "DHCPv6 traffic"
- },
- "DNS": {
- "code": [
- {
- "dport": "53",
- "proto": "udp"
- },
- {
- "dport": "53",
- "proto": "tcp"
- }
- ],
- "desc": "Domain Name System traffic (upd and tcp)"
- },
- "Distcc": {
- "code": [
- {
- "dport": "3632",
- "proto": "tcp"
- }
- ],
- "desc": "Distributed Compiler service"
- },
- "FTP": {
- "code": [
- {
- "dport": "21",
- "proto": "tcp"
- }
- ],
- "desc": "File Transfer Protocol"
- },
- "Finger": {
- "code": [
- {
- "dport": "79",
- "proto": "tcp"
- }
- ],
- "desc": "Finger protocol (RFC 742)"
- },
- "GNUnet": {
- "code": [
- {
- "dport": "2086",
- "proto": "tcp"
- },
- {
- "dport": "2086",
- "proto": "udp"
- },
- {
- "dport": "1080",
- "proto": "tcp"
- },
- {
- "dport": "1080",
- "proto": "udp"
- }
- ],
- "desc": "GNUnet secure peer-to-peer networking traffic"
- },
- "GRE": {
- "code": [
- {
- "proto": "47"
- }
- ],
- "desc": "Generic Routing Encapsulation tunneling protocol"
- },
- "Git": {
- "code": [
- {
- "dport": "9418",
- "proto": "tcp"
- }
- ],
- "desc": "Git distributed revision control traffic"
- },
- "HKP": {
- "code": [
- {
- "dport": "11371",
- "proto": "tcp"
- }
- ],
- "desc": "OpenPGP HTTP key server protocol traffic"
- },
- "HTTP": {
- "code": [
- {
- "dport": "80",
- "proto": "tcp"
- }
- ],
- "desc": "Hypertext Transfer Protocol (WWW)"
- },
- "HTTPS": {
- "code": [
- {
- "dport": "443",
- "proto": "tcp"
- }
- ],
- "desc": "Hypertext Transfer Protocol (WWW) over SSL"
- },
- "HTTP/3": {
- "code": [
- {
- "dport": "443",
- "proto": "udp"
- }
- ],
- "desc": "Hypertext Transfer Protocol v3"
- },
- "ICPV2": {
- "code": [
- {
- "dport": "3130",
- "proto": "udp"
- }
- ],
- "desc": "Internet Cache Protocol V2 (Squid) traffic"
- },
- "ICQ": {
- "code": [
- {
- "dport": "5190",
- "proto": "tcp"
- }
- ],
- "desc": "AOL Instant Messenger traffic"
- },
- "IMAP": {
- "code": [
- {
- "dport": "143",
- "proto": "tcp"
- }
- ],
- "desc": "Internet Message Access Protocol"
- },
- "IMAPS": {
- "code": [
- {
- "dport": "993",
- "proto": "tcp"
- }
- ],
- "desc": "Internet Message Access Protocol over SSL"
- },
- "IPIP": {
- "code": [
- {
- "proto": "94"
- }
- ],
- "desc": "IPIP capsulation traffic"
- },
- "IPsec": {
- "code": [
- {
- "dport": "500",
- "proto": "udp",
- "sport": "500"
- },
- {
- "proto": "50"
- }
- ],
- "desc": "IPsec traffic"
- },
- "IPsecah": {
- "code": [
- {
- "dport": "500",
- "proto": "udp",
- "sport": "500"
- },
- {
- "proto": "51"
- }
- ],
- "desc": "IPsec authentication (AH) traffic"
- },
- "IPsecnat": {
- "code": [
- {
- "dport": "500",
- "proto": "udp"
- },
- {
- "dport": "4500",
- "proto": "udp"
- },
- {
- "proto": "50"
- }
- ],
- "desc": "IPsec traffic and Nat-Traversal"
- },
- "IRC": {
- "code": [
- {
- "dport": "6667",
- "proto": "tcp"
- }
- ],
- "desc": "Internet Relay Chat traffic"
- },
- "Jetdirect": {
- "code": [
- {
- "dport": "9100",
- "proto": "tcp"
- }
- ],
- "desc": "HP Jetdirect printing"
- },
- "L2TP": {
- "code": [
- {
- "dport": "1701",
- "proto": "udp"
- }
- ],
- "desc": "Layer 2 Tunneling Protocol traffic"
- },
- "LDAP": {
- "code": [
- {
- "dport": "389",
- "proto": "tcp"
- }
- ],
- "desc": "Lightweight Directory Access Protocol traffic"
- },
- "LDAPS": {
- "code": [
- {
- "dport": "636",
- "proto": "tcp"
- }
- ],
- "desc": "Secure Lightweight Directory Access Protocol traffic"
- },
- "MDNS": {
- "code": [
- {
- "dport": "5353",
- "proto": "udp"
- }
- ],
- "desc": "Multicast DNS"
- },
- "MSNP": {
- "code": [
- {
- "dport": "1863",
- "proto": "tcp"
- }
- ],
- "desc": "Microsoft Notification Protocol"
- },
- "MSSQL": {
- "code": [
- {
- "dport": "1433",
- "proto": "tcp"
- }
- ],
- "desc": "Microsoft SQL Server"
- },
- "Mail": {
- "code": [
- {
- "dport": "25",
- "proto": "tcp"
- },
- {
- "dport": "465",
- "proto": "tcp"
- },
- {
- "dport": "587",
- "proto": "tcp"
- }
- ],
- "desc": "Mail traffic (SMTP, SMTPS, Submission)"
- },
- "Munin": {
- "code": [
- {
- "dport": "4949",
- "proto": "tcp"
- }
- ],
- "desc": "Munin networked resource monitoring traffic"
- },
- "MySQL": {
- "code": [
- {
- "dport": "3306",
- "proto": "tcp"
- }
- ],
- "desc": "MySQL server"
- },
- "NNTP": {
- "code": [
- {
- "dport": "119",
- "proto": "tcp"
- }
- ],
- "desc": "NNTP traffic (Usenet)."
- },
- "NNTPS": {
- "code": [
- {
- "dport": "563",
- "proto": "tcp"
- }
- ],
- "desc": "Encrypted NNTP traffic (Usenet)"
- },
- "NTP": {
- "code": [
- {
- "dport": "123",
- "proto": "udp"
- }
- ],
- "desc": "Network Time Protocol (ntpd)"
- },
- "NeighborDiscovery": {
- "code": [
- {
- "dport": "nd-router-solicit",
- "proto": "icmpv6"
- },
- {
- "dport": "nd-router-advert",
- "proto": "icmpv6"
- },
- {
- "dport": "nd-neighbor-solicit",
- "proto": "icmpv6"
- },
- {
- "dport": "nd-neighbor-advert",
- "proto": "icmpv6"
- }
- ],
- "desc": "IPv6 neighbor solicitation, neighbor and router advertisement"
- },
- "OSPF": {
- "code": [
- {
- "proto": "89"
- }
- ],
- "desc": "OSPF multicast traffic"
- },
- "OpenVPN": {
- "code": [
- {
- "dport": "1194",
- "proto": "udp"
- }
- ],
- "desc": "OpenVPN traffic"
- },
- "PBS": {
- "code": [
- {
- "dport": "8007",
- "proto": "tcp"
- }
- ],
- "desc": "Proxmox Backup Server"
- },
- "PCA": {
- "code": [
- {
- "dport": "5632",
- "proto": "udp"
- },
- {
- "dport": "5631",
- "proto": "tcp"
- }
- ],
- "desc": "Symantec PCAnywere (tm)"
- },
- "PMG": {
- "code": [
- {
- "dport": "8006",
- "proto": "tcp"
- }
- ],
- "desc": "Proxmox Mail Gateway web interface"
- },
- "POP3": {
- "code": [
- {
- "dport": "110",
- "proto": "tcp"
- }
- ],
- "desc": "POP3 traffic"
- },
- "POP3S": {
- "code": [
- {
- "dport": "995",
- "proto": "tcp"
- }
- ],
- "desc": "Encrypted POP3 traffic"
- },
- "PPtP": {
- "code": [
- {
- "proto": "47"
- },
- {
- "dport": "1723",
- "proto": "tcp"
- }
- ],
- "desc": "Point-to-Point Tunneling Protocol"
- },
- "Ping": {
- "code": [
- {
- "dport": "echo-request",
- "proto": "icmp"
- }
- ],
- "desc": "ICMP echo request"
- },
- "PostgreSQL": {
- "code": [
- {
- "dport": "5432",
- "proto": "tcp"
- }
- ],
- "desc": "PostgreSQL server"
- },
- "Printer": {
- "code": [
- {
- "dport": "515",
- "proto": "tcp"
- }
- ],
- "desc": "Line Printer protocol printing"
- },
- "RDP": {
- "code": [
- {
- "dport": "3389",
- "proto": "tcp"
- }
- ],
- "desc": "Microsoft Remote Desktop Protocol traffic"
- },
- "RIP": {
- "code": [
- {
- "dport": "520",
- "proto": "udp"
- }
- ],
- "desc": "Routing Information Protocol (bidirectional)"
- },
- "RNDC": {
- "code": [
- {
- "dport": "953",
- "proto": "tcp"
- }
- ],
- "desc": "BIND remote management protocol"
- },
- "Razor": {
- "code": [
- {
- "dport": "2703",
- "proto": "tcp"
- }
- ],
- "desc": "Razor Antispam System"
- },
- "Rdate": {
- "code": [
- {
- "dport": "37",
- "proto": "tcp"
- }
- ],
- "desc": "Remote time retrieval (rdate)"
- },
- "Rsync": {
- "code": [
- {
- "dport": "873",
- "proto": "tcp"
- }
- ],
- "desc": "Rsync server"
- },
- "SANE": {
- "code": [
- {
- "dport": "6566",
- "proto": "tcp"
- }
- ],
- "desc": "SANE network scanning"
- },
- "SMB": {
- "code": [
- {
- "dport": "135,445",
- "proto": "udp"
- },
- {
- "dport": "137:139",
- "proto": "udp"
- },
- {
- "dport": "1024:65535",
- "proto": "udp",
- "sport": "137"
- },
- {
- "dport": "135,139,445",
- "proto": "tcp"
- }
- ],
- "desc": "Microsoft SMB traffic"
- },
- "SMBswat": {
- "code": [
- {
- "dport": "901",
- "proto": "tcp"
- }
- ],
- "desc": "Samba Web Administration Tool"
- },
- "SMTP": {
- "code": [
- {
- "dport": "25",
- "proto": "tcp"
- }
- ],
- "desc": "Simple Mail Transfer Protocol"
- },
- "SMTPS": {
- "code": [
- {
- "dport": "465",
- "proto": "tcp"
- }
- ],
- "desc": "Encrypted Simple Mail Transfer Protocol"
- },
- "SNMP": {
- "code": [
- {
- "dport": "161:162",
- "proto": "udp"
- },
- {
- "dport": "161",
- "proto": "tcp"
- }
- ],
- "desc": "Simple Network Management Protocol"
- },
- "SPAMD": {
- "code": [
- {
- "dport": "783",
- "proto": "tcp"
- }
- ],
- "desc": "Spam Assassin SPAMD traffic"
- },
- "SPICEproxy": {
- "code": [
- {
- "dport": "3128",
- "proto": "tcp"
- }
- ],
- "desc": "Proxmox VE SPICE display proxy traffic"
- },
- "SSH": {
- "code": [
- {
- "dport": "22",
- "proto": "tcp"
- }
- ],
- "desc": "Secure shell traffic"
- },
- "SVN": {
- "code": [
- {
- "dport": "3690",
- "proto": "tcp"
- }
- ],
- "desc": "Subversion server (svnserve)"
- },
- "SixXS": {
- "code": [
- {
- "dport": "3874",
- "proto": "tcp"
- },
- {
- "dport": "3740",
- "proto": "udp"
- },
- {
- "proto": "41"
- },
- {
- "dport": "5072,8374",
- "proto": "udp"
- }
- ],
- "desc": "SixXS IPv6 Deployment and Tunnel Broker"
- },
- "Squid": {
- "code": [
- {
- "dport": "3128",
- "proto": "tcp"
- }
- ],
- "desc": "Squid web proxy traffic"
- },
- "Submission": {
- "code": [
- {
- "dport": "587",
- "proto": "tcp"
- }
- ],
- "desc": "Mail message submission traffic"
- },
- "Syslog": {
- "code": [
- {
- "dport": "514",
- "proto": "udp"
- },
- {
- "dport": "514",
- "proto": "tcp"
- }
- ],
- "desc": "Syslog protocol (RFC 5424) traffic"
- },
- "TFTP": {
- "code": [
- {
- "dport": "69",
- "proto": "udp"
- }
- ],
- "desc": "Trivial File Transfer Protocol traffic"
- },
- "Telnet": {
- "code": [
- {
- "dport": "23",
- "proto": "tcp"
- }
- ],
- "desc": "Telnet traffic"
- },
- "Telnets": {
- "code": [
- {
- "dport": "992",
- "proto": "tcp"
- }
- ],
- "desc": "Telnet over SSL"
- },
- "Time": {
- "code": [
- {
- "dport": "37",
- "proto": "tcp"
- }
- ],
- "desc": "RFC 868 Time protocol"
- },
- "Trcrt": {
- "code": [
- {
- "dport": "33434:33524",
- "proto": "udp"
- },
- {
- "dport": "echo-request",
- "proto": "icmp"
- }
- ],
- "desc": "Traceroute (for up to 30 hops) traffic"
- },
- "VNC": {
- "code": [
- {
- "dport": "5900:5999",
- "proto": "tcp"
- }
- ],
- "desc": "VNC traffic for VNC display's 0 - 99"
- },
- "VNCL": {
- "code": [
- {
- "dport": "5500",
- "proto": "tcp"
- }
- ],
- "desc": "VNC traffic from Vncservers to Vncviewers in listen mode"
- },
- "Web": {
- "code": [
- {
- "dport": "80",
- "proto": "tcp"
- },
- {
- "dport": "443",
- "proto": "tcp"
- }
- ],
- "desc": "WWW traffic (HTTP and HTTPS)"
- },
- "Webcache": {
- "code": [
- {
- "dport": "8080",
- "proto": "tcp"
- }
- ],
- "desc": "Web Cache/Proxy traffic (port 8080)"
- },
- "Webmin": {
- "code": [
- {
- "dport": "10000",
- "proto": "tcp"
- }
- ],
- "desc": "Webmin traffic"
- },
- "Whois": {
- "code": [
- {
- "dport": "43",
- "proto": "tcp"
- }
- ],
- "desc": "Whois (nicname, RFC 3912) traffic"
- }
-}
diff --git a/proxmox-ve-config/src/firewall/cluster.rs b/proxmox-ve-config/src/firewall/cluster.rs
deleted file mode 100644
index 223124b..0000000
--- a/proxmox-ve-config/src/firewall/cluster.rs
+++ /dev/null
@@ -1,374 +0,0 @@
-use std::collections::BTreeMap;
-use std::io;
-
-use anyhow::Error;
-use serde::Deserialize;
-
-use crate::firewall::common::ParserConfig;
-use crate::firewall::types::ipset::{Ipset, IpsetScope};
-use crate::firewall::types::log::LogRateLimit;
-use crate::firewall::types::rule::{Direction, Verdict};
-use crate::firewall::types::{Alias, Group, Rule};
-
-use crate::firewall::parse::{serde_option_bool, serde_option_log_ratelimit};
-
-#[derive(Debug, Default)]
-pub struct Config {
- pub(crate) config: super::common::Config<Options>,
-}
-
-/// default setting for [`Config::is_enabled()`]
-pub const CLUSTER_ENABLED_DEFAULT: bool = false;
-/// default setting for [`Config::ebtables()`]
-pub const CLUSTER_EBTABLES_DEFAULT: bool = false;
-/// default setting for [`Config::default_policy()`]
-pub const CLUSTER_POLICY_IN_DEFAULT: Verdict = Verdict::Drop;
-/// default setting for [`Config::default_policy()`]
-pub const CLUSTER_POLICY_OUT_DEFAULT: Verdict = Verdict::Accept;
-
-impl Config {
- pub fn parse<R: io::BufRead>(input: R) -> Result<Self, Error> {
- let parser_config = ParserConfig {
- guest_iface_names: false,
- ipset_scope: Some(IpsetScope::Datacenter),
- };
-
- Ok(Self {
- config: super::common::Config::parse(input, &parser_config)?,
- })
- }
-
- pub fn rules(&self) -> &Vec<Rule> {
- &self.config.rules
- }
-
- pub fn groups(&self) -> &BTreeMap<String, Group> {
- &self.config.groups
- }
-
- pub fn ipsets(&self) -> &BTreeMap<String, Ipset> {
- &self.config.ipsets
- }
-
- pub fn alias(&self, name: &str) -> Option<&Alias> {
- self.config.alias(name)
- }
-
- pub fn is_enabled(&self) -> bool {
- self.config
- .options
- .enable
- .unwrap_or(CLUSTER_ENABLED_DEFAULT)
- }
-
- /// returns the ebtables option from the cluster config or [`CLUSTER_EBTABLES_DEFAULT`] if
- /// unset
- ///
- /// this setting is leftover from the old firewall, but has no effect on the nftables firewall
- pub fn ebtables(&self) -> bool {
- self.config
- .options
- .ebtables
- .unwrap_or(CLUSTER_EBTABLES_DEFAULT)
- }
-
- /// returns policy_in / out or [`CLUSTER_POLICY_IN_DEFAULT`] / [`CLUSTER_POLICY_OUT_DEFAULT`] if
- /// unset
- pub fn default_policy(&self, dir: Direction) -> Verdict {
- match dir {
- Direction::In => self
- .config
- .options
- .policy_in
- .unwrap_or(CLUSTER_POLICY_IN_DEFAULT),
- Direction::Out => self
- .config
- .options
- .policy_out
- .unwrap_or(CLUSTER_POLICY_OUT_DEFAULT),
- }
- }
-
- /// returns the rate_limit for logs or [`None`] if rate limiting is disabled
- ///
- /// If there is no rate limit set, then [`LogRateLimit::default`] is used
- pub fn log_ratelimit(&self) -> Option<LogRateLimit> {
- let rate_limit = self
- .config
- .options
- .log_ratelimit
- .clone()
- .unwrap_or_default();
-
- match rate_limit.enabled() {
- true => Some(rate_limit),
- false => None,
- }
- }
-}
-
-#[derive(Debug, Default, Deserialize)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Options {
- #[serde(default, with = "serde_option_bool")]
- enable: Option<bool>,
-
- #[serde(default, with = "serde_option_bool")]
- ebtables: Option<bool>,
-
- #[serde(default, with = "serde_option_log_ratelimit")]
- log_ratelimit: Option<LogRateLimit>,
-
- policy_in: Option<Verdict>,
- policy_out: Option<Verdict>,
-}
-
-#[cfg(test)]
-mod tests {
- use crate::firewall::types::{
- address::IpList,
- alias::{AliasName, AliasScope},
- ipset::{IpsetAddress, IpsetEntry},
- log::{LogLevel, LogRateLimitTimescale},
- rule::{Kind, RuleGroup},
- rule_match::{
- Icmpv6, Icmpv6Code, IpAddrMatch, IpMatch, Ports, Protocol, RuleMatch, Tcp, Udp,
- },
- Cidr,
- };
-
- use super::*;
-
- #[test]
- fn test_parse_config() {
- const CONFIG: &str = r#"
-[OPTIONS]
-enable: 1
-log_ratelimit: 1,rate=10/second,burst=20
-ebtables: 0
-policy_in: REJECT
-policy_out: REJECT
-
-[ALIASES]
-
-another 8.8.8.18
-analias 7.7.0.0/16 # much
-wide cccc::/64
-
-[IPSET a-set]
-
-!5.5.5.5
-1.2.3.4/30
-dc/analias # a comment
-dc/wide
-dddd::/96
-
-[RULES]
-
-GROUP tgr -i eth0 # acomm
-IN ACCEPT -p udp -dport 33 -sport 22 -log warning
-
-[group tgr] # comment for tgr
-
-|OUT ACCEPT -source fe80::1/48 -dest dddd:3:3::9/64 -p icmpv6 -log nolog -icmp-type port-unreachable
-OUT ACCEPT -p tcp -sport 33 -log nolog
-IN BGP(REJECT) -log crit -source 1.2.3.4
-"#;
-
- let mut config = CONFIG.as_bytes();
- let config = Config::parse(&mut config).unwrap();
-
- assert_eq!(
- config.config.options,
- Options {
- ebtables: Some(false),
- enable: Some(true),
- log_ratelimit: Some(LogRateLimit::new(
- true,
- 10,
- LogRateLimitTimescale::Second,
- 20
- )),
- policy_in: Some(Verdict::Reject),
- policy_out: Some(Verdict::Reject),
- }
- );
-
- assert_eq!(config.config.aliases.len(), 3);
-
- assert_eq!(
- config.config.aliases["another"],
- Alias::new("another", Cidr::new_v4([8, 8, 8, 18], 32).unwrap(), None),
- );
-
- assert_eq!(
- config.config.aliases["analias"],
- Alias::new(
- "analias",
- Cidr::new_v4([7, 7, 0, 0], 16).unwrap(),
- "much".to_string()
- ),
- );
-
- assert_eq!(
- config.config.aliases["wide"],
- Alias::new(
- "wide",
- Cidr::new_v6(
- [0xCCCC, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x000],
- 64
- )
- .unwrap(),
- None
- ),
- );
-
- assert_eq!(config.config.ipsets.len(), 1);
-
- let mut ipset_elements = vec![
- IpsetEntry {
- nomatch: true,
- address: Cidr::new_v4([5, 5, 5, 5], 32).unwrap().into(),
- comment: None,
- },
- IpsetEntry {
- nomatch: false,
- address: Cidr::new_v4([1, 2, 3, 4], 30).unwrap().into(),
- comment: None,
- },
- IpsetEntry {
- nomatch: false,
- address: IpsetAddress::Alias(AliasName::new(AliasScope::Datacenter, "analias")),
- comment: Some("a comment".to_string()),
- },
- IpsetEntry {
- nomatch: false,
- address: IpsetAddress::Alias(AliasName::new(AliasScope::Datacenter, "wide")),
- comment: None,
- },
- IpsetEntry {
- nomatch: false,
- address: Cidr::new_v6([0xdd, 0xdd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 96)
- .unwrap()
- .into(),
- comment: None,
- },
- ];
-
- let mut ipset = Ipset::from_parts(IpsetScope::Datacenter, "a-set");
- ipset.append(&mut ipset_elements);
-
- assert_eq!(config.config.ipsets["a-set"], ipset,);
-
- assert_eq!(config.config.rules.len(), 2);
-
- assert_eq!(
- config.config.rules[0],
- Rule {
- disabled: false,
- comment: Some("acomm".to_string()),
- kind: Kind::Group(RuleGroup {
- group: "tgr".to_string(),
- iface: Some("eth0".to_string()),
- }),
- },
- );
-
- assert_eq!(
- config.config.rules[1],
- Rule {
- disabled: false,
- comment: None,
- kind: Kind::Match(RuleMatch {
- dir: Direction::In,
- verdict: Verdict::Accept,
- proto: Some(Protocol::Udp(Udp::new(Ports::from_u16(22, 33)))),
- log: Some(LogLevel::Warning),
- ..Default::default()
- }),
- },
- );
-
- assert_eq!(config.config.groups.len(), 1);
-
- let entry = &config.config.groups["tgr"];
- assert_eq!(entry.comment(), Some("comment for tgr"));
- assert_eq!(entry.rules().len(), 3);
-
- assert_eq!(
- entry.rules()[0],
- Rule {
- disabled: true,
- comment: None,
- kind: Kind::Match(RuleMatch {
- dir: Direction::Out,
- verdict: Verdict::Accept,
- ip: Some(IpMatch {
- src: Some(IpAddrMatch::Ip(IpList::from(
- Cidr::new_v6(
- [0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
- 48
- )
- .unwrap()
- ))),
- dst: Some(IpAddrMatch::Ip(IpList::from(
- Cidr::new_v6(
- [0xdd, 0xdd, 0, 3, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9],
- 64
- )
- .unwrap()
- ))),
- }),
- proto: Some(Protocol::Icmpv6(Icmpv6::new_code(Icmpv6Code::Named(
- "port-unreachable"
- )))),
- log: Some(LogLevel::Nolog),
- ..Default::default()
- }),
- },
- );
- assert_eq!(
- entry.rules()[1],
- Rule {
- disabled: false,
- comment: None,
- kind: Kind::Match(RuleMatch {
- dir: Direction::Out,
- verdict: Verdict::Accept,
- proto: Some(Protocol::Tcp(Tcp::new(Ports::from_u16(33, None)))),
- log: Some(LogLevel::Nolog),
- ..Default::default()
- }),
- },
- );
-
- assert_eq!(
- entry.rules()[2],
- Rule {
- disabled: false,
- comment: None,
- kind: Kind::Match(RuleMatch {
- dir: Direction::In,
- verdict: Verdict::Reject,
- log: Some(LogLevel::Critical),
- fw_macro: Some("BGP".to_string()),
- ip: Some(IpMatch {
- src: Some(IpAddrMatch::Ip(IpList::from(
- Cidr::new_v4([1, 2, 3, 4], 32).unwrap()
- ))),
- dst: None,
- }),
- ..Default::default()
- }),
- },
- );
-
- let empty_config = Config::parse("".as_bytes()).expect("empty config is invalid");
-
- assert_eq!(empty_config.config.options, Options::default());
- assert!(empty_config.config.rules.is_empty());
- assert!(empty_config.config.aliases.is_empty());
- assert!(empty_config.config.ipsets.is_empty());
- assert!(empty_config.config.groups.is_empty());
- }
-}
diff --git a/proxmox-ve-config/src/firewall/common.rs b/proxmox-ve-config/src/firewall/common.rs
deleted file mode 100644
index a08f19c..0000000
--- a/proxmox-ve-config/src/firewall/common.rs
+++ /dev/null
@@ -1,184 +0,0 @@
-use std::collections::{BTreeMap, HashMap};
-use std::io;
-
-use anyhow::{bail, format_err, Error};
-use serde::de::IntoDeserializer;
-
-use crate::firewall::parse::{parse_named_section_tail, split_key_value, SomeString};
-use crate::firewall::types::ipset::{IpsetName, IpsetScope};
-use crate::firewall::types::{Alias, Group, Ipset, Rule};
-
-#[derive(Debug, Default)]
-pub struct Config<O>
-where
- O: Default + std::fmt::Debug + serde::de::DeserializeOwned,
-{
- pub(crate) options: O,
- pub(crate) rules: Vec<Rule>,
- pub(crate) aliases: BTreeMap<String, Alias>,
- pub(crate) ipsets: BTreeMap<String, Ipset>,
- pub(crate) groups: BTreeMap<String, Group>,
-}
-
-enum Sec {
- None,
- Options,
- Aliases,
- Rules,
- Ipset(String, Ipset),
- Group(String, Group),
-}
-
-#[derive(Default)]
-pub struct ParserConfig {
- /// Network interfaces must be of the form `netX`.
- pub guest_iface_names: bool,
- pub ipset_scope: Option<IpsetScope>,
-}
-
-impl<O> Config<O>
-where
- O: Default + std::fmt::Debug + serde::de::DeserializeOwned,
-{
- pub fn new() -> Self {
- Self::default()
- }
-
- pub fn parse<R: io::BufRead>(input: R, parser_cfg: &ParserConfig) -> Result<Self, Error> {
- let mut section = Sec::None;
-
- let mut this = Self::new();
- let mut options = HashMap::new();
-
- for line in input.lines() {
- let line = line?;
- let line = line.trim();
-
- if line.is_empty() || line.starts_with('#') {
- continue;
- }
-
- log::trace!("parsing config line {line}");
-
- if line.eq_ignore_ascii_case("[OPTIONS]") {
- this.set_section(&mut section, Sec::Options)?;
- } else if line.eq_ignore_ascii_case("[ALIASES]") {
- this.set_section(&mut section, Sec::Aliases)?;
- } else if line.eq_ignore_ascii_case("[RULES]") {
- this.set_section(&mut section, Sec::Rules)?;
- } else if let Some(line) = line.strip_prefix("[IPSET") {
- let (name, comment) = parse_named_section_tail("ipset", line)?;
-
- let scope = parser_cfg.ipset_scope.ok_or_else(|| {
- format_err!("IPSET in config, but no scope set in parser config")
- })?;
-
- let ipset_name = IpsetName::new(scope, name.to_string());
- let mut ipset = Ipset::new(ipset_name);
- ipset.comment = comment.map(str::to_owned);
-
- this.set_section(&mut section, Sec::Ipset(name.to_string(), ipset))?;
- } else if let Some(line) = line.strip_prefix("[group") {
- let (name, comment) = parse_named_section_tail("group", line)?;
- let mut group = Group::new();
-
- group.set_comment(comment.map(str::to_owned));
-
- this.set_section(&mut section, Sec::Group(name.to_owned(), group))?;
- } else if line.starts_with('[') {
- bail!("invalid section {line:?}");
- } else {
- match &mut section {
- Sec::None => bail!("config line with no section: {line:?}"),
- Sec::Options => Self::parse_option(line, &mut options)?,
- Sec::Aliases => this.parse_alias(line)?,
- Sec::Rules => this.parse_rule(line, parser_cfg)?,
- Sec::Ipset(_name, ipset) => ipset.parse_entry(line)?,
- Sec::Group(_name, group) => group.parse_entry(line)?,
- }
- }
- }
- this.set_section(&mut section, Sec::None)?;
-
- this.options = O::deserialize(IntoDeserializer::<
- '_,
- crate::firewall::parse::SerdeStringError,
- >::into_deserializer(options))?;
-
- Ok(this)
- }
-
- fn parse_option(line: &str, options: &mut HashMap<String, SomeString>) -> Result<(), Error> {
- let (key, value) = split_key_value(line)
- .ok_or_else(|| format_err!("expected colon separated key and value, found {line:?}"))?;
-
- if options.insert(key.to_string(), value.into()).is_some() {
- bail!("duplicate option {key:?}");
- }
-
- Ok(())
- }
-
- fn parse_alias(&mut self, line: &str) -> Result<(), Error> {
- let alias: Alias = line.parse()?;
-
- if self
- .aliases
- .insert(alias.name().to_string(), alias)
- .is_some()
- {
- bail!("duplicate alias: {line}");
- }
-
- Ok(())
- }
-
- fn parse_rule(&mut self, line: &str, parser_cfg: &ParserConfig) -> Result<(), Error> {
- let rule: Rule = line.parse()?;
-
- if parser_cfg.guest_iface_names {
- if let Some(iface) = rule.iface() {
- let _ = iface
- .strip_prefix("net")
- .ok_or_else(|| {
- format_err!("interface name must be of the form \"net<number>\"")
- })?
- .parse::<u16>()
- .map_err(|_| {
- format_err!("interface name must be of the form \"net<number>\"")
- })?;
- }
- }
-
- self.rules.push(rule);
- Ok(())
- }
-
- fn set_section(&mut self, sec: &mut Sec, to: Sec) -> Result<(), Error> {
- let prev = std::mem::replace(sec, to);
-
- match prev {
- Sec::Ipset(name, ipset) => {
- if self.ipsets.insert(name.clone(), ipset).is_some() {
- bail!("duplicate ipset: {name:?}");
- }
- }
- Sec::Group(name, group) => {
- if self.groups.insert(name.clone(), group).is_some() {
- bail!("duplicate group: {name:?}");
- }
- }
- _ => (),
- }
-
- Ok(())
- }
-
- pub fn ipsets(&self) -> &BTreeMap<String, Ipset> {
- &self.ipsets
- }
-
- pub fn alias(&self, name: &str) -> Option<&Alias> {
- self.aliases.get(name)
- }
-}
diff --git a/proxmox-ve-config/src/firewall/ct_helper.rs b/proxmox-ve-config/src/firewall/ct_helper.rs
deleted file mode 100644
index 40e4fee..0000000
--- a/proxmox-ve-config/src/firewall/ct_helper.rs
+++ /dev/null
@@ -1,115 +0,0 @@
-use anyhow::{bail, Error};
-use serde::Deserialize;
-use std::collections::HashMap;
-use std::sync::OnceLock;
-
-use crate::firewall::types::address::Family;
-use crate::firewall::types::rule_match::{Ports, Protocol, Tcp, Udp};
-
-#[derive(Clone, Debug, Deserialize)]
-pub struct CtHelperMacroJson {
- pub v4: Option<bool>,
- pub v6: Option<bool>,
- pub name: String,
- pub tcp: Option<u16>,
- pub udp: Option<u16>,
-}
-
-impl TryFrom<CtHelperMacroJson> for CtHelperMacro {
- type Error = Error;
-
- fn try_from(value: CtHelperMacroJson) -> Result<Self, Self::Error> {
- if value.tcp.is_none() && value.udp.is_none() {
- bail!("Neither TCP nor UDP port set in CT helper!");
- }
-
- let family = match (value.v4, value.v6) {
- (Some(true), Some(true)) => None,
- (Some(true), _) => Some(Family::V4),
- (_, Some(true)) => Some(Family::V6),
- _ => bail!("Neither v4 nor v6 set in CT Helper Macro!"),
- };
-
- let mut ct_helper = CtHelperMacro {
- family,
- name: value.name,
- tcp: None,
- udp: None,
- };
-
- if let Some(dport) = value.tcp {
- let ports = Ports::from_u16(None, dport);
- ct_helper.tcp = Some(Tcp::new(ports).into());
- }
-
- if let Some(dport) = value.udp {
- let ports = Ports::from_u16(None, dport);
- ct_helper.udp = Some(Udp::new(ports).into());
- }
-
- Ok(ct_helper)
- }
-}
-
-#[derive(Clone, Debug, Deserialize)]
-#[serde(try_from = "CtHelperMacroJson")]
-pub struct CtHelperMacro {
- family: Option<Family>,
- name: String,
- tcp: Option<Protocol>,
- udp: Option<Protocol>,
-}
-
-impl CtHelperMacro {
- fn helper_name(&self, protocol: &str) -> String {
- format!("helper-{}-{protocol}", self.name)
- }
-
- pub fn tcp_helper_name(&self) -> String {
- self.helper_name("tcp")
- }
-
- pub fn udp_helper_name(&self) -> String {
- self.helper_name("udp")
- }
-
- pub fn family(&self) -> Option<Family> {
- self.family
- }
-
- pub fn name(&self) -> &str {
- self.name.as_ref()
- }
-
- pub fn tcp(&self) -> Option<&Protocol> {
- self.tcp.as_ref()
- }
-
- pub fn udp(&self) -> Option<&Protocol> {
- self.udp.as_ref()
- }
-}
-
-fn hashmap() -> &'static HashMap<String, CtHelperMacro> {
- const MACROS: &str = include_str!("../../resources/ct_helper.json");
- static HASHMAP: OnceLock<HashMap<String, CtHelperMacro>> = OnceLock::new();
-
- HASHMAP.get_or_init(|| {
- let macro_data: Vec<CtHelperMacro> = match serde_json::from_str(MACROS) {
- Ok(data) => data,
- Err(err) => {
- log::error!("could not load data for ct helpers: {err}");
- Vec::new()
- }
- };
-
- macro_data
- .into_iter()
- .map(|elem| (elem.name.clone(), elem))
- .collect()
- })
-}
-
-pub fn get_cthelper(name: &str) -> Option<&'static CtHelperMacro> {
- hashmap().get(name)
-}
diff --git a/proxmox-ve-config/src/firewall/fw_macros.rs b/proxmox-ve-config/src/firewall/fw_macros.rs
deleted file mode 100644
index 5fa8dab..0000000
--- a/proxmox-ve-config/src/firewall/fw_macros.rs
+++ /dev/null
@@ -1,69 +0,0 @@
-use std::collections::HashMap;
-
-use serde::Deserialize;
-use std::sync::OnceLock;
-
-use crate::firewall::types::rule_match::Protocol;
-
-use super::types::rule_match::RuleOptions;
-
-#[derive(Clone, Debug, Default, Deserialize)]
-struct FwMacroData {
- #[serde(rename = "desc")]
- pub description: &'static str,
- pub code: Vec<RuleOptions>,
-}
-
-#[derive(Clone, Debug, Default)]
-pub struct FwMacro {
- pub _description: &'static str,
- pub code: Vec<Protocol>,
-}
-
-fn macros() -> &'static HashMap<String, FwMacro> {
- const MACROS: &str = include_str!("../../resources/macros.json");
- static HASHMAP: OnceLock<HashMap<String, FwMacro>> = OnceLock::new();
-
- HASHMAP.get_or_init(|| {
- let macro_data: HashMap<String, FwMacroData> = match serde_json::from_str(MACROS) {
- Ok(m) => m,
- Err(err) => {
- log::error!("could not load data for macros: {err}");
- HashMap::new()
- }
- };
-
- let mut macros = HashMap::new();
-
- 'outer: for (name, data) in macro_data {
- let mut code = Vec::new();
-
- for c in data.code {
- match Protocol::from_options(&c) {
- Ok(Some(p)) => code.push(p),
- Ok(None) => {
- continue 'outer;
- }
- Err(err) => {
- log::error!("could not parse data for macro {name}: {err}");
- continue 'outer;
- }
- }
- }
-
- macros.insert(
- name,
- FwMacro {
- _description: data.description,
- code,
- },
- );
- }
-
- macros
- })
-}
-
-pub fn get_macro(name: &str) -> Option<&'static FwMacro> {
- macros().get(name)
-}
diff --git a/proxmox-ve-config/src/firewall/guest.rs b/proxmox-ve-config/src/firewall/guest.rs
deleted file mode 100644
index c7e282f..0000000
--- a/proxmox-ve-config/src/firewall/guest.rs
+++ /dev/null
@@ -1,237 +0,0 @@
-use std::collections::BTreeMap;
-use std::io;
-
-use crate::guest::types::Vmid;
-use crate::guest::vm::NetworkConfig;
-
-use crate::firewall::types::alias::{Alias, AliasName};
-use crate::firewall::types::ipset::IpsetScope;
-use crate::firewall::types::log::LogLevel;
-use crate::firewall::types::rule::{Direction, Rule, Verdict};
-use crate::firewall::types::Ipset;
-
-use anyhow::{bail, Error};
-use serde::Deserialize;
-
-use crate::firewall::parse::serde_option_bool;
-
-/// default return value for [`Config::is_enabled()`]
-pub const GUEST_ENABLED_DEFAULT: bool = false;
-/// default return value for [`Config::allow_ndp()`]
-pub const GUEST_ALLOW_NDP_DEFAULT: bool = true;
-/// default return value for [`Config::allow_dhcp()`]
-pub const GUEST_ALLOW_DHCP_DEFAULT: bool = true;
-/// default return value for [`Config::allow_ra()`]
-pub const GUEST_ALLOW_RA_DEFAULT: bool = false;
-/// default return value for [`Config::macfilter()`]
-pub const GUEST_MACFILTER_DEFAULT: bool = true;
-/// default return value for [`Config::ipfilter()`]
-pub const GUEST_IPFILTER_DEFAULT: bool = false;
-/// default return value for [`Config::default_policy()`]
-pub const GUEST_POLICY_IN_DEFAULT: Verdict = Verdict::Drop;
-/// default return value for [`Config::default_policy()`]
-pub const GUEST_POLICY_OUT_DEFAULT: Verdict = Verdict::Accept;
-
-#[derive(Debug, Default, Deserialize)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Options {
- #[serde(default, with = "serde_option_bool")]
- dhcp: Option<bool>,
-
- #[serde(default, with = "serde_option_bool")]
- enable: Option<bool>,
-
- #[serde(default, with = "serde_option_bool")]
- ipfilter: Option<bool>,
-
- #[serde(default, with = "serde_option_bool")]
- ndp: Option<bool>,
-
- #[serde(default, with = "serde_option_bool")]
- radv: Option<bool>,
-
- log_level_in: Option<LogLevel>,
- log_level_out: Option<LogLevel>,
-
- #[serde(default, with = "serde_option_bool")]
- macfilter: Option<bool>,
-
- #[serde(rename = "policy_in")]
- policy_in: Option<Verdict>,
-
- #[serde(rename = "policy_out")]
- policy_out: Option<Verdict>,
-}
-
-#[derive(Debug)]
-pub struct Config {
- vmid: Vmid,
-
- /// The interface prefix: "veth" for containers, "tap" for VMs.
- iface_prefix: &'static str,
-
- network_config: NetworkConfig,
- config: super::common::Config<Options>,
-}
-
-impl Config {
- pub fn parse<T: io::BufRead, U: io::BufRead>(
- vmid: &Vmid,
- iface_prefix: &'static str,
- firewall_input: T,
- network_input: U,
- ) -> Result<Self, Error> {
- let parser_cfg = super::common::ParserConfig {
- guest_iface_names: true,
- ipset_scope: Some(IpsetScope::Guest),
- };
-
- let config = super::common::Config::parse(firewall_input, &parser_cfg)?;
- if !config.groups.is_empty() {
- bail!("guest firewall config cannot declare groups");
- }
-
- let network_config = NetworkConfig::parse(network_input)?;
-
- Ok(Self {
- vmid: *vmid,
- iface_prefix,
- config,
- network_config,
- })
- }
-
- pub fn vmid(&self) -> Vmid {
- self.vmid
- }
-
- pub fn alias(&self, name: &AliasName) -> Option<&Alias> {
- self.config.alias(name.name())
- }
-
- pub fn iface_name_by_key(&self, key: &str) -> Result<String, Error> {
- let index = NetworkConfig::index_from_net_key(key)?;
- Ok(format!("{}{}i{index}", self.iface_prefix, self.vmid))
- }
-
- pub fn iface_name_by_index(&self, index: i64) -> String {
- format!("{}{}i{index}", self.iface_prefix, self.vmid)
- }
-
- /// returns the value of the enabled config key or [`GUEST_ENABLED_DEFAULT`] if unset
- pub fn is_enabled(&self) -> bool {
- self.config.options.enable.unwrap_or(GUEST_ENABLED_DEFAULT)
- }
-
- pub fn rules(&self) -> &[Rule] {
- &self.config.rules
- }
-
- pub fn log_level(&self, dir: Direction) -> LogLevel {
- match dir {
- Direction::In => self.config.options.log_level_in.unwrap_or_default(),
- Direction::Out => self.config.options.log_level_out.unwrap_or_default(),
- }
- }
-
- /// returns the value of the ndp config key or [`GUEST_ALLOW_NDP_DEFAULT`] if unset
- pub fn allow_ndp(&self) -> bool {
- self.config.options.ndp.unwrap_or(GUEST_ALLOW_NDP_DEFAULT)
- }
-
- /// returns the value of the dhcp config key or [`GUEST_ALLOW_DHCP_DEFAULT`] if unset
- pub fn allow_dhcp(&self) -> bool {
- self.config.options.dhcp.unwrap_or(GUEST_ALLOW_DHCP_DEFAULT)
- }
-
- /// returns the value of the radv config key or [`GUEST_ALLOW_RA_DEFAULT`] if unset
- pub fn allow_ra(&self) -> bool {
- self.config.options.radv.unwrap_or(GUEST_ALLOW_RA_DEFAULT)
- }
-
- /// returns the value of the macfilter config key or [`GUEST_MACFILTER_DEFAULT`] if unset
- pub fn macfilter(&self) -> bool {
- self.config
- .options
- .macfilter
- .unwrap_or(GUEST_MACFILTER_DEFAULT)
- }
-
- /// returns the value of the ipfilter config key or [`GUEST_IPFILTER_DEFAULT`] if unset
- pub fn ipfilter(&self) -> bool {
- self.config
- .options
- .ipfilter
- .unwrap_or(GUEST_IPFILTER_DEFAULT)
- }
-
- /// returns the value of the policy_in/out config key or
- /// [`GUEST_POLICY_IN_DEFAULT`] / [`GUEST_POLICY_OUT_DEFAULT`] if unset
- pub fn default_policy(&self, dir: Direction) -> Verdict {
- match dir {
- Direction::In => self
- .config
- .options
- .policy_in
- .unwrap_or(GUEST_POLICY_IN_DEFAULT),
- Direction::Out => self
- .config
- .options
- .policy_out
- .unwrap_or(GUEST_POLICY_OUT_DEFAULT),
- }
- }
-
- pub fn network_config(&self) -> &NetworkConfig {
- &self.network_config
- }
-
- pub fn ipsets(&self) -> &BTreeMap<String, Ipset> {
- self.config.ipsets()
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_parse_config() {
- // most of the stuff is already tested in cluster parsing, only testing
- // guest specific options here
- const CONFIG: &str = r#"
-[OPTIONS]
-enable: 1
-dhcp: 1
-ipfilter: 0
-log_level_in: emerg
-log_level_out: crit
-macfilter: 0
-ndp:1
-radv:1
-policy_in: REJECT
-policy_out: REJECT
-"#;
-
- let config = CONFIG.as_bytes();
- let network_config: Vec<u8> = Vec::new();
- let config =
- Config::parse(&Vmid::new(100), "tap", config, network_config.as_slice()).unwrap();
-
- assert_eq!(
- config.config.options,
- Options {
- dhcp: Some(true),
- enable: Some(true),
- ipfilter: Some(false),
- ndp: Some(true),
- radv: Some(true),
- log_level_in: Some(LogLevel::Emergency),
- log_level_out: Some(LogLevel::Critical),
- macfilter: Some(false),
- policy_in: Some(Verdict::Reject),
- policy_out: Some(Verdict::Reject),
- }
- );
- }
-}
diff --git a/proxmox-ve-config/src/firewall/host.rs b/proxmox-ve-config/src/firewall/host.rs
deleted file mode 100644
index 3de6fad..0000000
--- a/proxmox-ve-config/src/firewall/host.rs
+++ /dev/null
@@ -1,372 +0,0 @@
-use std::io;
-use std::net::IpAddr;
-
-use anyhow::{bail, Error};
-use serde::Deserialize;
-
-use crate::host::utils::{host_ips, network_interface_cidrs};
-use proxmox_sys::nodename;
-
-use crate::firewall::parse;
-use crate::firewall::types::log::LogLevel;
-use crate::firewall::types::rule::Direction;
-use crate::firewall::types::{Alias, Cidr, Rule};
-
-/// default setting for the enabled key
-pub const HOST_ENABLED_DEFAULT: bool = true;
-/// default setting for the nftables key
-pub const HOST_NFTABLES_DEFAULT: bool = false;
-/// default return value for [`Config::allow_ndp()`]
-pub const HOST_ALLOW_NDP_DEFAULT: bool = true;
-/// default return value for [`Config::block_smurfs()`]
-pub const HOST_BLOCK_SMURFS_DEFAULT: bool = true;
-/// default return value for [`Config::block_synflood()`]
-pub const HOST_BLOCK_SYNFLOOD_DEFAULT: bool = false;
-/// default rate limit for synflood rule (packets / second)
-pub const HOST_BLOCK_SYNFLOOD_RATE_DEFAULT: i64 = 200;
-/// default rate limit for synflood rule (packets / second)
-pub const HOST_BLOCK_SYNFLOOD_BURST_DEFAULT: i64 = 1000;
-/// default return value for [`Config::block_invalid_tcp()`]
-pub const HOST_BLOCK_INVALID_TCP_DEFAULT: bool = false;
-/// default return value for [`Config::block_invalid_conntrack()`]
-pub const HOST_BLOCK_INVALID_CONNTRACK: bool = false;
-/// default setting for logging of invalid conntrack entries
-pub const HOST_LOG_INVALID_CONNTRACK: bool = false;
-
-#[derive(Debug, Default, Deserialize)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Options {
- #[serde(default, with = "parse::serde_option_bool")]
- enable: Option<bool>,
-
- #[serde(default, with = "parse::serde_option_bool")]
- nftables: Option<bool>,
-
- log_level_in: Option<LogLevel>,
- log_level_out: Option<LogLevel>,
-
- #[serde(default, with = "parse::serde_option_bool")]
- log_nf_conntrack: Option<bool>,
- #[serde(default, with = "parse::serde_option_bool")]
- ndp: Option<bool>,
-
- #[serde(default, with = "parse::serde_option_bool")]
- nf_conntrack_allow_invalid: Option<bool>,
-
- // is Option<Vec<>> for easier deserialization
- #[serde(default, with = "parse::serde_option_conntrack_helpers")]
- nf_conntrack_helpers: Option<Vec<String>>,
-
- #[serde(default, with = "parse::serde_option_number")]
- nf_conntrack_max: Option<i64>,
- #[serde(default, with = "parse::serde_option_number")]
- nf_conntrack_tcp_timeout_established: Option<i64>,
- #[serde(default, with = "parse::serde_option_number")]
- nf_conntrack_tcp_timeout_syn_recv: Option<i64>,
-
- #[serde(default, with = "parse::serde_option_bool")]
- nosmurfs: Option<bool>,
-
- #[serde(default, with = "parse::serde_option_bool")]
- protection_synflood: Option<bool>,
- #[serde(default, with = "parse::serde_option_number")]
- protection_synflood_burst: Option<i64>,
- #[serde(default, with = "parse::serde_option_number")]
- protection_synflood_rate: Option<i64>,
-
- smurf_log_level: Option<LogLevel>,
- tcp_flags_log_level: Option<LogLevel>,
-
- #[serde(default, with = "parse::serde_option_bool")]
- tcpflags: Option<bool>,
-}
-
-#[derive(Debug, Default)]
-pub struct Config {
- pub(crate) config: super::common::Config<Options>,
-}
-
-impl Config {
- pub fn new() -> Self {
- Self {
- config: Default::default(),
- }
- }
-
- pub fn parse<R: io::BufRead>(input: R) -> Result<Self, Error> {
- let config = super::common::Config::parse(input, &Default::default())?;
-
- if !config.groups.is_empty() {
- bail!("host firewall config cannot declare groups");
- }
-
- if !config.aliases.is_empty() {
- bail!("host firewall config cannot declare aliases");
- }
-
- if !config.ipsets.is_empty() {
- bail!("host firewall config cannot declare ipsets");
- }
-
- Ok(Self { config })
- }
-
- pub fn rules(&self) -> &[Rule] {
- &self.config.rules
- }
-
- pub fn management_ips() -> Result<Vec<Cidr>, Error> {
- let mut management_cidrs = Vec::new();
-
- for host_ip in host_ips() {
- for network_interface_cidr in network_interface_cidrs() {
- match (host_ip, network_interface_cidr) {
- (IpAddr::V4(ip), Cidr::Ipv4(cidr)) => {
- if cidr.contains_address(&ip) {
- management_cidrs.push(network_interface_cidr);
- }
- }
- (IpAddr::V6(ip), Cidr::Ipv6(cidr)) => {
- if cidr.contains_address(&ip) {
- management_cidrs.push(network_interface_cidr);
- }
- }
- _ => continue,
- };
- }
- }
-
- Ok(management_cidrs)
- }
-
- pub fn hostname() -> &'static str {
- nodename()
- }
-
- pub fn get_alias(&self, name: &str) -> Option<&Alias> {
- self.config.alias(name)
- }
-
- /// returns value of enabled key or [`HOST_ENABLED_DEFAULT`] if unset
- pub fn is_enabled(&self) -> bool {
- self.config.options.enable.unwrap_or(HOST_ENABLED_DEFAULT)
- }
-
- /// returns value of nftables key or [`HOST_NFTABLES_DEFAULT`] if unset
- pub fn nftables(&self) -> bool {
- self.config
- .options
- .nftables
- .unwrap_or(HOST_NFTABLES_DEFAULT)
- }
-
- /// returns value of ndp key or [`HOST_ALLOW_NDP_DEFAULT`] if unset
- pub fn allow_ndp(&self) -> bool {
- self.config.options.ndp.unwrap_or(HOST_ALLOW_NDP_DEFAULT)
- }
-
- /// returns value of nosmurfs key or [`HOST_BLOCK_SMURFS_DEFAULT`] if unset
- pub fn block_smurfs(&self) -> bool {
- self.config
- .options
- .nosmurfs
- .unwrap_or(HOST_BLOCK_SMURFS_DEFAULT)
- }
-
- /// returns the log level for the smurf protection rule
- ///
- /// If there is no log level set, it returns [`LogLevel::default()`]
- pub fn block_smurfs_log_level(&self) -> LogLevel {
- self.config.options.smurf_log_level.unwrap_or_default()
- }
-
- /// returns value of protection_synflood key or [`HOST_BLOCK_SYNFLOOD_DEFAULT`] if unset
- pub fn block_synflood(&self) -> bool {
- self.config
- .options
- .protection_synflood
- .unwrap_or(HOST_BLOCK_SYNFLOOD_DEFAULT)
- }
-
- /// returns value of protection_synflood_rate key or [`HOST_BLOCK_SYNFLOOD_RATE_DEFAULT`] if
- /// unset
- pub fn synflood_rate(&self) -> i64 {
- self.config
- .options
- .protection_synflood_rate
- .unwrap_or(HOST_BLOCK_SYNFLOOD_RATE_DEFAULT)
- }
-
- /// returns value of protection_synflood_burst key or [`HOST_BLOCK_SYNFLOOD_BURST_DEFAULT`] if
- /// unset
- pub fn synflood_burst(&self) -> i64 {
- self.config
- .options
- .protection_synflood_burst
- .unwrap_or(HOST_BLOCK_SYNFLOOD_BURST_DEFAULT)
- }
-
- /// returns value of tcpflags key or [`HOST_BLOCK_INVALID_TCP_DEFAULT`] if unset
- pub fn block_invalid_tcp(&self) -> bool {
- self.config
- .options
- .tcpflags
- .unwrap_or(HOST_BLOCK_INVALID_TCP_DEFAULT)
- }
-
- /// returns the log level for the block invalid TCP packets rule
- ///
- /// If there is no log level set, it returns [`LogLevel::default()`]
- pub fn block_invalid_tcp_log_level(&self) -> LogLevel {
- self.config.options.tcp_flags_log_level.unwrap_or_default()
- }
-
- /// returns value of nf_conntrack_allow_invalid key or [`HOST_BLOCK_INVALID_CONNTRACK`] if
- /// unset
- pub fn block_invalid_conntrack(&self) -> bool {
- !self
- .config
- .options
- .nf_conntrack_allow_invalid
- .unwrap_or(HOST_BLOCK_INVALID_CONNTRACK)
- }
-
- pub fn nf_conntrack_max(&self) -> Option<i64> {
- self.config.options.nf_conntrack_max
- }
-
- pub fn nf_conntrack_tcp_timeout_established(&self) -> Option<i64> {
- self.config.options.nf_conntrack_tcp_timeout_established
- }
-
- pub fn nf_conntrack_tcp_timeout_syn_recv(&self) -> Option<i64> {
- self.config.options.nf_conntrack_tcp_timeout_syn_recv
- }
-
- /// returns value of log_nf_conntrack key or [`HOST_LOG_INVALID_CONNTRACK`] if unset
- pub fn log_nf_conntrack(&self) -> bool {
- self.config
- .options
- .log_nf_conntrack
- .unwrap_or(HOST_LOG_INVALID_CONNTRACK)
- }
-
- pub fn conntrack_helpers(&self) -> Option<&Vec<String>> {
- self.config.options.nf_conntrack_helpers.as_ref()
- }
-
- /// returns the log level for the given direction
- ///
- /// If there is no log level set it returns [`LogLevel::default()`]
- pub fn log_level(&self, dir: Direction) -> LogLevel {
- match dir {
- Direction::In => self.config.options.log_level_in.unwrap_or_default(),
- Direction::Out => self.config.options.log_level_out.unwrap_or_default(),
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use crate::firewall::types::{
- log::LogLevel,
- rule::{Kind, RuleGroup, Verdict},
- rule_match::{Ports, Protocol, RuleMatch, Udp},
- };
-
- use super::*;
-
- #[test]
- fn test_parse_config() {
- const CONFIG: &str = r#"
-[OPTIONS]
-enable: 1
-nftables: 1
-log_level_in: debug
-log_level_out: emerg
-log_nf_conntrack: 0
-ndp: 1
-nf_conntrack_allow_invalid: yes
-nf_conntrack_helpers: ftp
-nf_conntrack_max: 44000
-nf_conntrack_tcp_timeout_established: 500000
-nf_conntrack_tcp_timeout_syn_recv: 44
-nosmurfs: no
-protection_synflood: 1
-protection_synflood_burst: 2500
-protection_synflood_rate: 300
-smurf_log_level: notice
-tcp_flags_log_level: nolog
-tcpflags: yes
-
-[RULES]
-
-GROUP tgr -i eth0 # acomm
-IN ACCEPT -p udp -dport 33 -sport 22 -log warning
-
-"#;
-
- let mut config = CONFIG.as_bytes();
- let config = Config::parse(&mut config).unwrap();
-
- assert_eq!(
- config.config.options,
- Options {
- enable: Some(true),
- nftables: Some(true),
- log_level_in: Some(LogLevel::Debug),
- log_level_out: Some(LogLevel::Emergency),
- log_nf_conntrack: Some(false),
- ndp: Some(true),
- nf_conntrack_allow_invalid: Some(true),
- nf_conntrack_helpers: Some(vec!["ftp".to_string()]),
- nf_conntrack_max: Some(44000),
- nf_conntrack_tcp_timeout_established: Some(500000),
- nf_conntrack_tcp_timeout_syn_recv: Some(44),
- nosmurfs: Some(false),
- protection_synflood: Some(true),
- protection_synflood_burst: Some(2500),
- protection_synflood_rate: Some(300),
- smurf_log_level: Some(LogLevel::Notice),
- tcp_flags_log_level: Some(LogLevel::Nolog),
- tcpflags: Some(true),
- }
- );
-
- assert_eq!(config.config.rules.len(), 2);
-
- assert_eq!(
- config.config.rules[0],
- Rule {
- disabled: false,
- comment: Some("acomm".to_string()),
- kind: Kind::Group(RuleGroup {
- group: "tgr".to_string(),
- iface: Some("eth0".to_string()),
- }),
- },
- );
-
- assert_eq!(
- config.config.rules[1],
- Rule {
- disabled: false,
- comment: None,
- kind: Kind::Match(RuleMatch {
- dir: Direction::In,
- verdict: Verdict::Accept,
- proto: Some(Protocol::Udp(Udp::new(Ports::from_u16(22, 33)))),
- log: Some(LogLevel::Warning),
- ..Default::default()
- }),
- },
- );
-
- Config::parse("[ALIASES]\ntest 127.0.0.1".as_bytes())
- .expect_err("host config cannot contain aliases");
-
- Config::parse("[GROUP test]".as_bytes()).expect_err("host config cannot contain groups");
-
- Config::parse("[IPSET test]".as_bytes()).expect_err("host config cannot contain ipsets");
- }
-}
diff --git a/proxmox-ve-config/src/firewall/mod.rs b/proxmox-ve-config/src/firewall/mod.rs
deleted file mode 100644
index 2cf57e2..0000000
--- a/proxmox-ve-config/src/firewall/mod.rs
+++ /dev/null
@@ -1,10 +0,0 @@
-pub mod cluster;
-pub mod common;
-pub mod ct_helper;
-pub mod fw_macros;
-pub mod guest;
-pub mod host;
-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
deleted file mode 100644
index 7bf00c0..0000000
--- a/proxmox-ve-config/src/firewall/parse.rs
+++ /dev/null
@@ -1,494 +0,0 @@
-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.
-///
-/// Returns a tuple with the name and the remainder (not trimmed).
-///
-/// # Examples
-/// ```ignore
-/// assert_eq!(match_name("some-name someremainder"), Some(("some-name", " someremainder")));
-/// assert_eq!(match_name("some-name@someremainder"), Some(("some-name", "@someremainder")));
-/// assert_eq!(match_name(""), None);
-/// assert_eq!(match_name(" someremainder"), None);
-/// ```
-pub fn match_name(line: &str) -> Option<(&str, &str)> {
- if !line.starts_with(|c: char| c.is_ascii_alphabetic()) {
- return None;
- }
-
- let end = line
- .as_bytes()
- .iter()
- .position(|&b| !(b.is_ascii_alphanumeric() || NAME_SPECIAL_CHARACTERS.contains(&b)));
-
- let (name, rest) = match end {
- Some(end) => line.split_at(end),
- None => (line, ""),
- };
-
- if name.is_empty() {
- None
- } else {
- Some((name, rest))
- }
-}
-
-/// Parses up to the next whitespace character or end of the string.
-///
-/// Returns `None` if the non-whitespace part would be empty.
-///
-/// Returns a tuple containing the parsed section and the *trimmed* remainder.
-pub fn match_non_whitespace(line: &str) -> Option<(&str, &str)> {
- let (text, rest) = line
- .as_bytes()
- .iter()
- .position(|&b| b.is_ascii_whitespace())
- .map(|pos| {
- let (a, b) = line.split_at(pos);
- (a, b.trim_start())
- })
- .unwrap_or((line, ""));
- if text.is_empty() {
- None
- } else {
- Some((text, rest))
- }
-}
-
-/// parses out all digits and returns the remainder
-///
-/// returns [`None`] if the digit part would be empty
-///
-/// Returns a tuple with the digits and the remainder (not trimmed).
-pub fn match_digits(line: &str) -> Option<(&str, &str)> {
- let split_position = line.as_bytes().iter().position(|&b| !b.is_ascii_digit());
-
- let (digits, rest) = match split_position {
- Some(pos) => line.split_at(pos),
- None => (line, ""),
- };
-
- if !digits.is_empty() {
- return Some((digits, rest));
- }
-
- None
-}
-
-/// Separate a `key: value` line, trimming whitespace.
-///
-/// Returns `None` if the `key` would be empty.
-pub fn split_key_value(line: &str) -> Option<(&str, &str)> {
- line.split_once(':')
- .map(|(key, value)| (key.trim(), value.trim()))
-}
-
-/// Parse a boolean.
-///
-/// values that parse as [`false`]: 0, false, off, no
-/// values that parse as [`true`]: 1, true, on, yes
-///
-/// # Examples
-/// ```ignore
-/// assert_eq!(parse_bool("false"), Ok(false));
-/// assert_eq!(parse_bool("on"), Ok(true));
-/// assert!(parse_bool("proxmox").is_err());
-/// ```
-pub fn parse_bool(value: &str) -> Result<bool, Error> {
- Ok(
- if value == "0"
- || 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:?}");
- },
- )
-}
-
-/// Parse the *remainder* of a section line, that is `<whitespace>NAME] #optional comment`.
-/// The `kind` parameter is used for error messages and should be the section type.
-///
-/// Return the name and the optional comment.
-pub fn parse_named_section_tail<'a>(
- kind: &'static str,
- line: &'a str,
-) -> Result<(&'a str, Option<&'a str>), Error> {
- if line.is_empty() || !line.as_bytes()[0].is_ascii_whitespace() {
- bail!("incomplete {kind} section");
- }
-
- let line = line.trim_start();
- let (name, line) = match_name(line)
- .ok_or_else(|| format_err!("expected a name for the {kind} at {line:?}"))?;
-
- let line = line
- .strip_prefix(']')
- .ok_or_else(|| format_err!("expected closing ']' in {kind} section header"))?
- .trim_start();
-
- Ok(match line.strip_prefix('#') {
- Some(comment) => (name, Some(comment.trim())),
- None if !line.is_empty() => bail!("trailing characters after {kind} section: {line:?}"),
- None => (name, None),
- })
-}
-
-// parses a number from a string OR number
-pub mod serde_option_number {
- use std::fmt;
-
- use serde::de::{Deserializer, Error, Visitor};
-
- pub fn deserialize<'de, D: Deserializer<'de>>(
- deserializer: D,
- ) -> Result<Option<i64>, D::Error> {
- struct V;
-
- impl<'de> Visitor<'de> for V {
- type Value = Option<i64>;
-
- fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
- f.write_str("a numerical value")
- }
-
- fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
- v.parse().map_err(E::custom).map(Some)
- }
-
- fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
- Ok(None)
- }
-
- fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
- where
- D: Deserializer<'de>,
- {
- deserializer.deserialize_any(self)
- }
- }
-
- deserializer.deserialize_any(V)
- }
-}
-
-// parses a bool from a string OR bool
-pub mod serde_option_bool {
- use std::fmt;
-
- use serde::de::{Deserializer, Error, Visitor};
-
- pub fn deserialize<'de, D: Deserializer<'de>>(
- deserializer: D,
- ) -> Result<Option<bool>, D::Error> {
- struct V;
-
- impl<'de> Visitor<'de> for V {
- type Value = Option<bool>;
-
- fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
- f.write_str("a boolean-like value")
- }
-
- fn visit_bool<E: Error>(self, v: bool) -> Result<Self::Value, E> {
- Ok(Some(v))
- }
-
- fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
- super::parse_bool(v).map_err(E::custom).map(Some)
- }
-
- fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
- Ok(None)
- }
-
- fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
- where
- D: Deserializer<'de>,
- {
- deserializer.deserialize_any(self)
- }
- }
-
- deserializer.deserialize_any(V)
- }
-}
-
-// parses a comma_separated list of strings
-pub mod serde_option_conntrack_helpers {
- use std::fmt;
-
- use serde::de::{Deserializer, Error, Visitor};
-
- pub fn deserialize<'de, D: Deserializer<'de>>(
- deserializer: D,
- ) -> Result<Option<Vec<String>>, D::Error> {
- struct V;
-
- impl<'de> Visitor<'de> for V {
- type Value = Option<Vec<String>>;
-
- fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
- f.write_str("A list of conntrack helpers")
- }
-
- fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
- if v.is_empty() {
- return Ok(None);
- }
-
- Ok(Some(v.split(',').map(String::from).collect()))
- }
-
- fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
- Ok(None)
- }
-
- fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
- where
- D: Deserializer<'de>,
- {
- deserializer.deserialize_any(self)
- }
- }
-
- deserializer.deserialize_any(V)
- }
-}
-
-// parses a log_ratelimit string: '[enable=]<1|0> [,burst=<integer>] [,rate=<rate>]'
-pub mod serde_option_log_ratelimit {
- use std::fmt;
-
- use serde::de::{Deserializer, Error, Visitor};
-
- use crate::firewall::types::log::LogRateLimit;
-
- pub fn deserialize<'de, D: Deserializer<'de>>(
- deserializer: D,
- ) -> Result<Option<LogRateLimit>, D::Error> {
- struct V;
-
- impl<'de> Visitor<'de> for V {
- type Value = Option<LogRateLimit>;
-
- fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
- f.write_str("a boolean-like value")
- }
-
- fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
- v.parse().map_err(E::custom).map(Some)
- }
-
- fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
- Ok(None)
- }
-
- fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
- where
- D: Deserializer<'de>,
- {
- deserializer.deserialize_any(self)
- }
- }
-
- deserializer.deserialize_any(V)
- }
-}
-
-/// `&str` deserializer which also accepts an `Option`.
-///
-/// Serde's `StringDeserializer` does not.
-#[derive(Clone, Copy, Debug)]
-pub struct SomeStrDeserializer<'a, E>(serde::de::value::StrDeserializer<'a, E>);
-
-impl<'de, 'a, E> serde::de::Deserializer<'de> for SomeStrDeserializer<'a, E>
-where
- E: serde::de::Error,
-{
- type Error = E;
-
- fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
- where
- V: serde::de::Visitor<'de>,
- {
- self.0.deserialize_any(visitor)
- }
-
- fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
- where
- V: serde::de::Visitor<'de>,
- {
- visitor.visit_some(self.0)
- }
-
- fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
- where
- V: serde::de::Visitor<'de>,
- {
- self.0.deserialize_str(visitor)
- }
-
- fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
- where
- V: serde::de::Visitor<'de>,
- {
- self.0.deserialize_string(visitor)
- }
-
- fn deserialize_enum<V>(
- self,
- _name: &str,
- _variants: &'static [&'static str],
- visitor: V,
- ) -> Result<V::Value, Self::Error>
- where
- V: serde::de::Visitor<'de>,
- {
- visitor.visit_enum(self.0)
- }
-
- serde::forward_to_deserialize_any! {
- bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char
- bytes byte_buf unit unit_struct newtype_struct seq tuple
- tuple_struct map struct identifier ignored_any
- }
-}
-
-/// `&str` wrapper which implements `IntoDeserializer` via `SomeStrDeserializer`.
-#[derive(Clone, Debug)]
-pub struct SomeStr<'a>(pub &'a str);
-
-impl<'a> From<&'a str> for SomeStr<'a> {
- fn from(s: &'a str) -> Self {
- Self(s)
- }
-}
-
-impl<'de, 'a, E> serde::de::IntoDeserializer<'de, E> for SomeStr<'a>
-where
- E: serde::de::Error,
-{
- type Deserializer = SomeStrDeserializer<'a, E>;
-
- fn into_deserializer(self) -> Self::Deserializer {
- SomeStrDeserializer(self.0.into_deserializer())
- }
-}
-
-/// `String` deserializer which also accepts an `Option`.
-///
-/// Serde's `StringDeserializer` does not.
-#[derive(Clone, Debug)]
-pub struct SomeStringDeserializer<E>(serde::de::value::StringDeserializer<E>);
-
-impl<'de, E> serde::de::Deserializer<'de> for SomeStringDeserializer<E>
-where
- E: serde::de::Error,
-{
- type Error = E;
-
- fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
- where
- V: serde::de::Visitor<'de>,
- {
- self.0.deserialize_any(visitor)
- }
-
- fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
- where
- V: serde::de::Visitor<'de>,
- {
- visitor.visit_some(self.0)
- }
-
- fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
- where
- V: serde::de::Visitor<'de>,
- {
- self.0.deserialize_str(visitor)
- }
-
- fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
- where
- V: serde::de::Visitor<'de>,
- {
- self.0.deserialize_string(visitor)
- }
-
- fn deserialize_enum<V>(
- self,
- _name: &str,
- _variants: &'static [&'static str],
- visitor: V,
- ) -> Result<V::Value, Self::Error>
- where
- V: serde::de::Visitor<'de>,
- {
- visitor.visit_enum(self.0)
- }
-
- serde::forward_to_deserialize_any! {
- bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char
- bytes byte_buf unit unit_struct newtype_struct seq tuple
- tuple_struct map struct identifier ignored_any
- }
-}
-
-/// `&str` wrapper which implements `IntoDeserializer` via `SomeStringDeserializer`.
-#[derive(Clone, Debug)]
-pub struct SomeString(pub String);
-
-impl From<&str> for SomeString {
- fn from(s: &str) -> Self {
- Self::from(s.to_string())
- }
-}
-
-impl From<String> for SomeString {
- fn from(s: String) -> Self {
- Self(s)
- }
-}
-
-impl<'de, E> serde::de::IntoDeserializer<'de, E> for SomeString
-where
- E: serde::de::Error,
-{
- type Deserializer = SomeStringDeserializer<E>;
-
- fn into_deserializer(self) -> Self::Deserializer {
- SomeStringDeserializer(self.0.into_deserializer())
- }
-}
-
-#[derive(Debug)]
-pub struct SerdeStringError(String);
-
-impl std::error::Error for SerdeStringError {}
-
-impl fmt::Display for SerdeStringError {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- f.write_str(&self.0)
- }
-}
-
-impl serde::de::Error for SerdeStringError {
- fn custom<T: fmt::Display>(msg: T) -> Self {
- Self(msg.to_string())
- }
-}
diff --git a/proxmox-ve-config/src/firewall/ports.rs b/proxmox-ve-config/src/firewall/ports.rs
deleted file mode 100644
index 9d5d1be..0000000
--- a/proxmox-ve-config/src/firewall/ports.rs
+++ /dev/null
@@ -1,80 +0,0 @@
-use anyhow::{format_err, Error};
-use std::sync::OnceLock;
-
-#[derive(Default)]
-struct NamedPorts {
- ports: std::collections::HashMap<String, u16>,
-}
-
-impl NamedPorts {
- fn new() -> Self {
- use std::io::BufRead;
-
- log::trace!("loading /etc/services");
-
- let mut this = Self::default();
-
- let file = match std::fs::File::open("/etc/services") {
- Ok(file) => file,
- Err(_) => return this,
- };
-
- for line in std::io::BufReader::new(file).lines() {
- let line = match line {
- Ok(line) => line,
- Err(_) => break,
- };
-
- let line = line.trim_start();
-
- if line.is_empty() || line.starts_with('#') {
- continue;
- }
-
- let mut parts = line.split_ascii_whitespace();
-
- let name = match parts.next() {
- None => continue,
- Some(name) => name.to_string(),
- };
-
- let proto: u16 = match parts.next() {
- None => continue,
- Some(proto) => match proto.split('/').next() {
- None => continue,
- Some(num) => match num.parse() {
- Ok(num) => num,
- Err(_) => continue,
- },
- },
- };
-
- this.ports.insert(name, proto);
- for alias in parts {
- if alias.starts_with('#') {
- break;
- }
- this.ports.insert(alias.to_string(), proto);
- }
- }
-
- this
- }
-
- fn find(&self, name: &str) -> Option<u16> {
- self.ports.get(name).copied()
- }
-}
-
-fn named_ports() -> &'static NamedPorts {
- static NAMED_PORTS: OnceLock<NamedPorts> = OnceLock::new();
-
- NAMED_PORTS.get_or_init(NamedPorts::new)
-}
-
-/// Parse a named port with the help of `/etc/services`.
-pub fn parse_named_port(name: &str) -> Result<u16, Error> {
- named_ports()
- .find(name)
- .ok_or_else(|| format_err!("unknown port name {name:?}"))
-}
diff --git a/proxmox-ve-config/src/firewall/types/address.rs b/proxmox-ve-config/src/firewall/types/address.rs
deleted file mode 100644
index e48ac1b..0000000
--- a/proxmox-ve-config/src/firewall/types/address.rs
+++ /dev/null
@@ -1,615 +0,0 @@
-use std::fmt;
-use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
-use std::ops::Deref;
-
-use anyhow::{bail, format_err, Error};
-use serde_with::DeserializeFromStr;
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub enum Family {
- V4,
- V6,
-}
-
-impl fmt::Display for Family {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- match self {
- Family::V4 => f.write_str("Ipv4"),
- Family::V6 => f.write_str("Ipv6"),
- }
- }
-}
-
-#[derive(Clone, Copy, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum Cidr {
- Ipv4(Ipv4Cidr),
- Ipv6(Ipv6Cidr),
-}
-
-impl Cidr {
- pub fn new_v4(addr: impl Into<Ipv4Addr>, mask: u8) -> Result<Self, Error> {
- Ok(Cidr::Ipv4(Ipv4Cidr::new(addr, mask)?))
- }
-
- pub fn new_v6(addr: impl Into<Ipv6Addr>, mask: u8) -> Result<Self, Error> {
- Ok(Cidr::Ipv6(Ipv6Cidr::new(addr, mask)?))
- }
-
- pub const fn family(&self) -> Family {
- match self {
- Cidr::Ipv4(_) => Family::V4,
- Cidr::Ipv6(_) => Family::V6,
- }
- }
-
- pub fn is_ipv4(&self) -> bool {
- matches!(self, Cidr::Ipv4(_))
- }
-
- pub fn is_ipv6(&self) -> bool {
- matches!(self, Cidr::Ipv6(_))
- }
-}
-
-impl fmt::Display for Cidr {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- match self {
- Self::Ipv4(ip) => f.write_str(ip.to_string().as_str()),
- Self::Ipv6(ip) => f.write_str(ip.to_string().as_str()),
- }
- }
-}
-
-impl std::str::FromStr for Cidr {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Error> {
- if let Ok(ip) = s.parse::<Ipv4Cidr>() {
- return Ok(Cidr::Ipv4(ip));
- }
-
- if let Ok(ip) = s.parse::<Ipv6Cidr>() {
- return Ok(Cidr::Ipv6(ip));
- }
-
- bail!("invalid ip address or CIDR: {s:?}");
- }
-}
-
-impl From<Ipv4Cidr> for Cidr {
- fn from(cidr: Ipv4Cidr) -> Self {
- Cidr::Ipv4(cidr)
- }
-}
-
-impl From<Ipv6Cidr> for Cidr {
- fn from(cidr: Ipv6Cidr) -> Self {
- Cidr::Ipv6(cidr)
- }
-}
-
-const IPV4_LENGTH: u8 = 32;
-
-#[derive(Clone, Copy, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Ipv4Cidr {
- addr: Ipv4Addr,
- mask: u8,
-}
-
-impl Ipv4Cidr {
- pub fn new(addr: impl Into<Ipv4Addr>, mask: u8) -> Result<Self, Error> {
- if mask > 32 {
- bail!("mask out of range for ipv4 cidr ({mask})");
- }
-
- Ok(Self {
- addr: addr.into(),
- mask,
- })
- }
-
- pub fn contains_address(&self, other: &Ipv4Addr) -> bool {
- let bits = u32::from_be_bytes(self.addr.octets());
- let other_bits = u32::from_be_bytes(other.octets());
-
- let shift_amount: u32 = IPV4_LENGTH.saturating_sub(self.mask).into();
-
- bits.checked_shr(shift_amount).unwrap_or(0)
- == other_bits.checked_shr(shift_amount).unwrap_or(0)
- }
-
- pub fn address(&self) -> &Ipv4Addr {
- &self.addr
- }
-
- pub fn mask(&self) -> u8 {
- self.mask
- }
-}
-
-impl<T: Into<Ipv4Addr>> From<T> for Ipv4Cidr {
- fn from(value: T) -> Self {
- Self {
- addr: value.into(),
- mask: 32,
- }
- }
-}
-
-impl std::str::FromStr for Ipv4Cidr {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Error> {
- Ok(match s.find('/') {
- None => Self {
- addr: s.parse()?,
- mask: 32,
- },
- Some(pos) => {
- let mask: u8 = s[(pos + 1)..]
- .parse()
- .map_err(|_| format_err!("invalid mask in ipv4 cidr: {s:?}"))?;
-
- Self::new(s[..pos].parse::<Ipv4Addr>()?, mask)?
- }
- })
- }
-}
-
-impl fmt::Display for Ipv4Cidr {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(f, "{}/{}", &self.addr, self.mask)
- }
-}
-
-const IPV6_LENGTH: u8 = 128;
-
-#[derive(Clone, Copy, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Ipv6Cidr {
- addr: Ipv6Addr,
- mask: u8,
-}
-
-impl Ipv6Cidr {
- pub fn new(addr: impl Into<Ipv6Addr>, mask: u8) -> Result<Self, Error> {
- if mask > IPV6_LENGTH {
- bail!("mask out of range for ipv6 cidr");
- }
-
- Ok(Self {
- addr: addr.into(),
- mask,
- })
- }
-
- pub fn contains_address(&self, other: &Ipv6Addr) -> bool {
- let bits = u128::from_be_bytes(self.addr.octets());
- let other_bits = u128::from_be_bytes(other.octets());
-
- let shift_amount: u32 = IPV6_LENGTH.saturating_sub(self.mask).into();
-
- bits.checked_shr(shift_amount).unwrap_or(0)
- == other_bits.checked_shr(shift_amount).unwrap_or(0)
- }
-
- pub fn address(&self) -> &Ipv6Addr {
- &self.addr
- }
-
- pub fn mask(&self) -> u8 {
- self.mask
- }
-}
-
-impl std::str::FromStr for Ipv6Cidr {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Error> {
- Ok(match s.find('/') {
- None => Self {
- addr: s.parse()?,
- mask: 128,
- },
- Some(pos) => {
- let mask: u8 = s[(pos + 1)..]
- .parse()
- .map_err(|_| format_err!("invalid mask in ipv6 cidr: {s:?}"))?;
-
- Self::new(s[..pos].parse::<Ipv6Addr>()?, mask)?
- }
- })
- }
-}
-
-impl fmt::Display for Ipv6Cidr {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(f, "{}/{}", &self.addr, self.mask)
- }
-}
-
-impl<T: Into<Ipv6Addr>> From<T> for Ipv6Cidr {
- fn from(addr: T) -> Self {
- Self {
- addr: addr.into(),
- mask: 128,
- }
- }
-}
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum IpEntry {
- Cidr(Cidr),
- Range(IpAddr, IpAddr),
-}
-
-impl std::str::FromStr for IpEntry {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Error> {
- if s.is_empty() {
- bail!("Empty IP specification!")
- }
-
- let entries: Vec<&str> = s
- .split('-')
- .take(3) // so we can check whether there are too many
- .collect();
-
- match entries.as_slice() {
- [cidr] => Ok(IpEntry::Cidr(cidr.parse()?)),
- [beg, end] => {
- if let Ok(beg) = beg.parse::<Ipv4Addr>() {
- if let Ok(end) = end.parse::<Ipv4Addr>() {
- if beg < end {
- return Ok(IpEntry::Range(beg.into(), end.into()));
- }
-
- bail!("start address is greater than end address!");
- }
- }
-
- if let Ok(beg) = beg.parse::<Ipv6Addr>() {
- if let Ok(end) = end.parse::<Ipv6Addr>() {
- if beg < end {
- return Ok(IpEntry::Range(beg.into(), end.into()));
- }
-
- bail!("start address is greater than end address!");
- }
- }
-
- bail!("start and end are not valid IP addresses of the same type!")
- }
- _ => bail!("Invalid amount of elements in IpEntry!"),
- }
- }
-}
-
-impl fmt::Display for IpEntry {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- match self {
- Self::Cidr(ip) => write!(f, "{ip}"),
- Self::Range(beg, end) => write!(f, "{beg}-{end}"),
- }
- }
-}
-
-impl IpEntry {
- fn family(&self) -> Family {
- match self {
- Self::Cidr(cidr) => cidr.family(),
- Self::Range(start, end) => {
- if start.is_ipv4() && end.is_ipv4() {
- return Family::V4;
- }
-
- if start.is_ipv6() && end.is_ipv6() {
- return Family::V6;
- }
-
- // should never be reached due to constructors validating that
- // start type == end type
- unreachable!("invalid IP entry")
- }
- }
- }
-}
-
-impl From<Cidr> for IpEntry {
- fn from(value: Cidr) -> Self {
- IpEntry::Cidr(value)
- }
-}
-
-#[derive(Clone, Debug, DeserializeFromStr)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct IpList {
- // guaranteed to have the same family
- entries: Vec<IpEntry>,
- family: Family,
-}
-
-impl Deref for IpList {
- type Target = Vec<IpEntry>;
-
- fn deref(&self) -> &Self::Target {
- &self.entries
- }
-}
-
-impl<T: Into<IpEntry>> From<T> for IpList {
- fn from(value: T) -> Self {
- let entry = value.into();
-
- Self {
- family: entry.family(),
- entries: vec![entry],
- }
- }
-}
-
-impl std::str::FromStr for IpList {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Error> {
- if s.is_empty() {
- bail!("Empty IP specification!")
- }
-
- let mut entries = Vec::new();
- let mut current_family = None;
-
- for element in s.split(',') {
- let entry: IpEntry = element.parse()?;
-
- if let Some(family) = current_family {
- if family != entry.family() {
- bail!("Incompatible families in IPList!")
- }
- } else {
- current_family = Some(entry.family());
- }
-
- entries.push(entry);
- }
-
- if entries.is_empty() {
- bail!("empty ip list")
- }
-
- Ok(IpList {
- entries,
- family: current_family.unwrap(), // must be set due to length check above
- })
- }
-}
-
-impl IpList {
- pub fn new(entries: Vec<IpEntry>) -> Result<Self, Error> {
- let family = entries.iter().try_fold(None, |result, entry| {
- if let Some(family) = result {
- if entry.family() != family {
- bail!("non-matching families in entries list");
- }
-
- Ok(Some(family))
- } else {
- Ok(Some(entry.family()))
- }
- })?;
-
- if let Some(family) = family {
- return Ok(Self { entries, family });
- }
-
- bail!("no elements in ip list entries");
- }
-
- pub fn family(&self) -> Family {
- self.family
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use std::net::{Ipv4Addr, Ipv6Addr};
-
- #[test]
- fn test_v4_cidr() {
- let mut cidr: Ipv4Cidr = "0.0.0.0/0".parse().expect("valid IPv4 CIDR");
-
- assert_eq!(cidr.addr, Ipv4Addr::new(0, 0, 0, 0));
- assert_eq!(cidr.mask, 0);
-
- assert!(cidr.contains_address(&Ipv4Addr::new(0, 0, 0, 0)));
- assert!(cidr.contains_address(&Ipv4Addr::new(255, 255, 255, 255)));
-
- cidr = "192.168.100.1".parse().expect("valid IPv4 CIDR");
-
- assert_eq!(cidr.addr, Ipv4Addr::new(192, 168, 100, 1));
- assert_eq!(cidr.mask, 32);
-
- assert!(cidr.contains_address(&Ipv4Addr::new(192, 168, 100, 1)));
- assert!(!cidr.contains_address(&Ipv4Addr::new(192, 168, 100, 2)));
- assert!(!cidr.contains_address(&Ipv4Addr::new(192, 168, 100, 0)));
-
- cidr = "10.100.5.0/24".parse().expect("valid IPv4 CIDR");
-
- assert_eq!(cidr.mask, 24);
-
- assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 0)));
- assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 1)));
- assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 100)));
- assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 255)));
- assert!(!cidr.contains_address(&Ipv4Addr::new(10, 100, 4, 255)));
- assert!(!cidr.contains_address(&Ipv4Addr::new(10, 100, 6, 0)));
-
- "0.0.0.0/-1".parse::<Ipv4Cidr>().unwrap_err();
- "0.0.0.0/33".parse::<Ipv4Cidr>().unwrap_err();
- "256.256.256.256/10".parse::<Ipv4Cidr>().unwrap_err();
-
- "fe80::1/64".parse::<Ipv4Cidr>().unwrap_err();
- "qweasd".parse::<Ipv4Cidr>().unwrap_err();
- "".parse::<Ipv4Cidr>().unwrap_err();
- }
-
- #[test]
- fn test_v6_cidr() {
- let mut cidr: Ipv6Cidr = "abab::1/64".parse().expect("valid IPv6 CIDR");
-
- assert_eq!(cidr.addr, Ipv6Addr::new(0xABAB, 0, 0, 0, 0, 0, 0, 1));
- assert_eq!(cidr.mask, 64);
-
- assert!(cidr.contains_address(&Ipv6Addr::new(0xABAB, 0, 0, 0, 0, 0, 0, 0)));
- assert!(cidr.contains_address(&Ipv6Addr::new(
- 0xABAB, 0, 0, 0, 0xAAAA, 0xAAAA, 0xAAAA, 0xAAAA
- )));
- assert!(cidr.contains_address(&Ipv6Addr::new(
- 0xABAB, 0, 0, 0, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
- )));
- assert!(!cidr.contains_address(&Ipv6Addr::new(0xABAB, 0, 0, 1, 0, 0, 0, 0)));
- assert!(!cidr.contains_address(&Ipv6Addr::new(
- 0xABAA, 0, 0, 0, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
- )));
-
- cidr = "eeee::1".parse().expect("valid IPv6 CIDR");
-
- assert_eq!(cidr.mask, 128);
-
- assert!(cidr.contains_address(&Ipv6Addr::new(0xEEEE, 0, 0, 0, 0, 0, 0, 1)));
- assert!(!cidr.contains_address(&Ipv6Addr::new(
- 0xEEED, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
- )));
- assert!(!cidr.contains_address(&Ipv6Addr::new(0xEEEE, 0, 0, 0, 0, 0, 0, 0)));
-
- "eeee::1/-1".parse::<Ipv6Cidr>().unwrap_err();
- "eeee::1/129".parse::<Ipv6Cidr>().unwrap_err();
- "gggg::1/64".parse::<Ipv6Cidr>().unwrap_err();
-
- "192.168.0.1".parse::<Ipv6Cidr>().unwrap_err();
- "qweasd".parse::<Ipv6Cidr>().unwrap_err();
- "".parse::<Ipv6Cidr>().unwrap_err();
- }
-
- #[test]
- fn test_parse_ip_entry() {
- let mut entry: IpEntry = "10.0.0.1".parse().expect("valid IP entry");
-
- assert_eq!(entry, Cidr::new_v4([10, 0, 0, 1], 32).unwrap().into());
-
- entry = "10.0.0.0/16".parse().expect("valid IP entry");
-
- assert_eq!(entry, Cidr::new_v4([10, 0, 0, 0], 16).unwrap().into());
-
- entry = "192.168.0.1-192.168.99.255"
- .parse()
- .expect("valid IP entry");
-
- assert_eq!(
- entry,
- IpEntry::Range([192, 168, 0, 1].into(), [192, 168, 99, 255].into())
- );
-
- entry = "fe80::1".parse().expect("valid IP entry");
-
- assert_eq!(
- entry,
- Cidr::new_v6([0xFE80, 0, 0, 0, 0, 0, 0, 1], 128)
- .unwrap()
- .into()
- );
-
- entry = "fe80::1/48".parse().expect("valid IP entry");
-
- assert_eq!(
- entry,
- Cidr::new_v6([0xFE80, 0, 0, 0, 0, 0, 0, 1], 48)
- .unwrap()
- .into()
- );
-
- entry = "fd80::1-fd80::ffff".parse().expect("valid IP entry");
-
- assert_eq!(
- entry,
- IpEntry::Range(
- [0xFD80, 0, 0, 0, 0, 0, 0, 1].into(),
- [0xFD80, 0, 0, 0, 0, 0, 0, 0xFFFF].into(),
- )
- );
-
- "192.168.100.0-192.168.99.255"
- .parse::<IpEntry>()
- .unwrap_err();
- "192.168.100.0-fe80::1".parse::<IpEntry>().unwrap_err();
- "192.168.100.0-192.168.200.0/16"
- .parse::<IpEntry>()
- .unwrap_err();
- "192.168.100.0-192.168.200.0-192.168.250.0"
- .parse::<IpEntry>()
- .unwrap_err();
- "qweasd".parse::<IpEntry>().unwrap_err();
- }
-
- #[test]
- fn test_parse_ip_list() {
- let mut ip_list: IpList = "192.168.0.1,192.168.100.0/24,172.16.0.0-172.32.255.255"
- .parse()
- .expect("valid IP list");
-
- assert_eq!(
- ip_list,
- IpList {
- entries: vec![
- IpEntry::Cidr(Cidr::new_v4([192, 168, 0, 1], 32).unwrap()),
- IpEntry::Cidr(Cidr::new_v4([192, 168, 100, 0], 24).unwrap()),
- IpEntry::Range([172, 16, 0, 0].into(), [172, 32, 255, 255].into()),
- ],
- family: Family::V4,
- }
- );
-
- ip_list = "fe80::1/64".parse().expect("valid IP list");
-
- assert_eq!(
- ip_list,
- IpList {
- entries: vec![IpEntry::Cidr(
- Cidr::new_v6([0xFE80, 0, 0, 0, 0, 0, 0, 1], 64).unwrap()
- ),],
- family: Family::V6,
- }
- );
-
- "192.168.0.1,fe80::1".parse::<IpList>().unwrap_err();
-
- "".parse::<IpList>().unwrap_err();
- "proxmox".parse::<IpList>().unwrap_err();
- }
-
- #[test]
- fn test_construct_ip_list() {
- let mut ip_list = IpList::new(vec![Cidr::new_v4([10, 0, 0, 0], 8).unwrap().into()])
- .expect("valid ip list");
-
- assert_eq!(ip_list.family(), Family::V4);
-
- ip_list =
- IpList::new(vec![Cidr::new_v6([0x000; 8], 8).unwrap().into()]).expect("valid ip list");
-
- assert_eq!(ip_list.family(), Family::V6);
-
- IpList::new(vec![]).expect_err("empty ip list is invalid");
-
- IpList::new(vec![
- Cidr::new_v4([10, 0, 0, 0], 8).unwrap().into(),
- Cidr::new_v6([0x0000; 8], 8).unwrap().into(),
- ])
- .expect_err("cannot mix ip families in ip list");
- }
-}
diff --git a/proxmox-ve-config/src/firewall/types/alias.rs b/proxmox-ve-config/src/firewall/types/alias.rs
deleted file mode 100644
index e6aa30d..0000000
--- a/proxmox-ve-config/src/firewall/types/alias.rs
+++ /dev/null
@@ -1,174 +0,0 @@
-use std::fmt::Display;
-use std::str::FromStr;
-
-use anyhow::{bail, format_err, Error};
-use serde_with::DeserializeFromStr;
-
-use crate::firewall::parse::{match_name, match_non_whitespace};
-use crate::firewall::types::address::Cidr;
-
-#[derive(Debug, Clone)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum AliasScope {
- Datacenter,
- Guest,
-}
-
-impl FromStr for AliasScope {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- Ok(match s {
- "dc" => AliasScope::Datacenter,
- "guest" => AliasScope::Guest,
- _ => bail!("invalid scope for alias: {s}"),
- })
- }
-}
-
-impl Display for AliasScope {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_str(match self {
- AliasScope::Datacenter => "dc",
- AliasScope::Guest => "guest",
- })
- }
-}
-
-#[derive(Debug, Clone, DeserializeFromStr)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct AliasName {
- scope: AliasScope,
- name: String,
-}
-
-impl Display for AliasName {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_fmt(format_args!("{}/{}", self.scope, self.name))
- }
-}
-
-impl FromStr for AliasName {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- match s.split_once('/') {
- Some((prefix, name)) if !name.is_empty() => Ok(Self {
- scope: prefix.parse()?,
- name: name.to_string(),
- }),
- _ => {
- bail!("Invalid Alias name!")
- }
- }
- }
-}
-
-impl AliasName {
- pub fn new(scope: AliasScope, name: impl Into<String>) -> Self {
- Self {
- scope,
- name: name.into(),
- }
- }
-
- pub fn name(&self) -> &str {
- &self.name
- }
-
- pub fn scope(&self) -> &AliasScope {
- &self.scope
- }
-}
-
-#[derive(Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Alias {
- name: String,
- address: Cidr,
- comment: Option<String>,
-}
-
-impl Alias {
- pub fn new(
- name: impl Into<String>,
- address: impl Into<Cidr>,
- comment: impl Into<Option<String>>,
- ) -> Self {
- Self {
- name: name.into(),
- address: address.into(),
- comment: comment.into(),
- }
- }
-
- pub fn name(&self) -> &str {
- &self.name
- }
-
- pub fn address(&self) -> &Cidr {
- &self.address
- }
-
- pub fn comment(&self) -> Option<&str> {
- self.comment.as_deref()
- }
-}
-
-impl FromStr for Alias {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- let (name, line) =
- match_name(s.trim_start()).ok_or_else(|| format_err!("expected an alias name"))?;
-
- let (address, line) = match_non_whitespace(line.trim_start())
- .ok_or_else(|| format_err!("expected a value for alias {name:?}"))?;
-
- let address: Cidr = address.parse()?;
-
- let line = line.trim_start();
-
- let comment = match line.strip_prefix('#') {
- Some(comment) => Some(comment.trim().to_string()),
- None if !line.is_empty() => bail!("trailing characters in alias: {line:?}"),
- None => None,
- };
-
- Ok(Alias {
- name: name.to_string(),
- address,
- comment,
- })
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_parse_alias() {
- for alias in [
- "local_network 10.0.0.0/32",
- "test-_123-___-a---- 10.0.0.1/32",
- ] {
- alias.parse::<Alias>().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::<Alias>().expect_err("invalid alias");
- }
- }
-
- #[test]
- fn test_parse_alias_name() {
- for name in ["dc/proxmox_123", "guest/proxmox-123"] {
- name.parse::<AliasName>().expect("valid alias name");
- }
-
- for name in ["proxmox/proxmox_123", "guests/proxmox-123", "dc/", "/name"] {
- name.parse::<AliasName>().expect_err("invalid alias name");
- }
- }
-}
diff --git a/proxmox-ve-config/src/firewall/types/group.rs b/proxmox-ve-config/src/firewall/types/group.rs
deleted file mode 100644
index 7455268..0000000
--- a/proxmox-ve-config/src/firewall/types/group.rs
+++ /dev/null
@@ -1,36 +0,0 @@
-use anyhow::Error;
-
-use crate::firewall::types::Rule;
-
-#[derive(Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Group {
- rules: Vec<Rule>,
- comment: Option<String>,
-}
-
-impl Group {
- pub const fn new() -> Self {
- Self {
- rules: Vec::new(),
- comment: None,
- }
- }
-
- pub fn rules(&self) -> &Vec<Rule> {
- &self.rules
- }
-
- pub fn comment(&self) -> Option<&str> {
- self.comment.as_deref()
- }
-
- pub fn set_comment(&mut self, comment: Option<String>) {
- self.comment = comment;
- }
-
- pub(crate) fn parse_entry(&mut self, line: &str) -> Result<(), Error> {
- self.rules.push(line.parse()?);
- Ok(())
- }
-}
diff --git a/proxmox-ve-config/src/firewall/types/ipset.rs b/proxmox-ve-config/src/firewall/types/ipset.rs
deleted file mode 100644
index c1af642..0000000
--- a/proxmox-ve-config/src/firewall/types/ipset.rs
+++ /dev/null
@@ -1,349 +0,0 @@
-use core::fmt::Display;
-use std::ops::{Deref, DerefMut};
-use std::str::FromStr;
-
-use anyhow::{bail, format_err, Error};
-use serde_with::DeserializeFromStr;
-
-use crate::firewall::parse::match_non_whitespace;
-use crate::firewall::types::address::Cidr;
-use crate::firewall::types::alias::AliasName;
-use crate::guest::vm::NetworkConfig;
-
-#[derive(Debug, Clone, Copy, Eq, PartialEq)]
-pub enum IpsetScope {
- Datacenter,
- Guest,
-}
-
-impl FromStr for IpsetScope {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- Ok(match s {
- "+dc" => IpsetScope::Datacenter,
- "+guest" => IpsetScope::Guest,
- _ => bail!("invalid scope for ipset: {s}"),
- })
- }
-}
-
-impl Display for IpsetScope {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let prefix = match self {
- Self::Datacenter => "dc",
- Self::Guest => "guest",
- };
-
- f.write_str(prefix)
- }
-}
-
-#[derive(Debug, Clone, DeserializeFromStr)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct IpsetName {
- pub scope: IpsetScope,
- pub name: String,
-}
-
-impl IpsetName {
- pub fn new(scope: IpsetScope, name: impl Into<String>) -> Self {
- Self {
- scope,
- name: name.into(),
- }
- }
-
- pub fn name(&self) -> &str {
- &self.name
- }
-
- pub fn scope(&self) -> IpsetScope {
- self.scope
- }
-}
-
-impl FromStr for IpsetName {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- match s.split_once('/') {
- Some((prefix, name)) if !name.is_empty() => Ok(Self {
- scope: prefix.parse()?,
- name: name.to_string(),
- }),
- _ => {
- bail!("Invalid IPSet name: {s}")
- }
- }
- }
-}
-
-impl Display for IpsetName {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}/{}", self.scope, self.name)
- }
-}
-
-#[derive(Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum IpsetAddress {
- Alias(AliasName),
- Cidr(Cidr),
-}
-
-impl FromStr for IpsetAddress {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Error> {
- if let Ok(cidr) = s.parse() {
- return Ok(IpsetAddress::Cidr(cidr));
- }
-
- if let Ok(name) = s.parse() {
- return Ok(IpsetAddress::Alias(name));
- }
-
- bail!("Invalid address in IPSet: {s}")
- }
-}
-
-impl<T: Into<Cidr>> From<T> for IpsetAddress {
- fn from(cidr: T) -> Self {
- IpsetAddress::Cidr(cidr.into())
- }
-}
-
-#[derive(Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct IpsetEntry {
- pub nomatch: bool,
- pub address: IpsetAddress,
- pub comment: Option<String>,
-}
-
-impl<T: Into<IpsetAddress>> From<T> for IpsetEntry {
- fn from(value: T) -> Self {
- Self {
- nomatch: false,
- address: value.into(),
- comment: None,
- }
- }
-}
-
-impl FromStr for IpsetEntry {
- type Err = Error;
-
- fn from_str(line: &str) -> Result<Self, Error> {
- let line = line.trim_start();
-
- let (nomatch, line) = match line.strip_prefix('!') {
- Some(line) => (true, line),
- None => (false, line),
- };
-
- let (address, line) =
- match_non_whitespace(line.trim_start()).ok_or_else(|| format_err!("missing value"))?;
-
- let address: IpsetAddress = address.parse()?;
- let line = line.trim_start();
-
- let comment = match line.strip_prefix('#') {
- Some(comment) => Some(comment.trim().to_string()),
- None if !line.is_empty() => bail!("trailing characters in ipset entry: {line:?}"),
- None => None,
- };
-
- Ok(Self {
- nomatch,
- address,
- comment,
- })
- }
-}
-
-#[derive(Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Ipfilter<'a> {
- index: i64,
- ipset: &'a Ipset,
-}
-
-impl Ipfilter<'_> {
- pub fn index(&self) -> i64 {
- self.index
- }
-
- pub fn ipset(&self) -> &Ipset {
- self.ipset
- }
-
- pub fn name_for_index(index: i64) -> String {
- format!("ipfilter-net{index}")
- }
-}
-
-#[derive(Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Ipset {
- pub name: IpsetName,
- set: Vec<IpsetEntry>,
- pub comment: Option<String>,
-}
-
-impl Ipset {
- pub const fn new(name: IpsetName) -> Self {
- Self {
- name,
- set: Vec::new(),
- comment: None,
- }
- }
-
- pub fn name(&self) -> &IpsetName {
- &self.name
- }
-
- pub fn from_parts(scope: IpsetScope, name: impl Into<String>) -> Self {
- Self::new(IpsetName::new(scope, name))
- }
-
- pub(crate) fn parse_entry(&mut self, line: &str) -> Result<(), Error> {
- self.set.push(line.parse()?);
- Ok(())
- }
-
- pub fn ipfilter(&self) -> Option<Ipfilter> {
- if self.name.scope() != IpsetScope::Guest {
- return None;
- }
-
- let name = self.name.name();
-
- if let Some(key) = name.strip_prefix("ipfilter-") {
- let id = NetworkConfig::index_from_net_key(key);
-
- if let Ok(id) = id {
- return Some(Ipfilter {
- index: id,
- ipset: self,
- });
- }
- }
-
- None
- }
-}
-
-impl Deref for Ipset {
- type Target = Vec<IpsetEntry>;
-
- #[inline]
- fn deref(&self) -> &Self::Target {
- &self.set
- }
-}
-
-impl DerefMut for Ipset {
- #[inline]
- fn deref_mut(&mut self) -> &mut Vec<IpsetEntry> {
- &mut self.set
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_parse_ipset_name() {
- for test_case in [
- ("+dc/proxmox-123", IpsetScope::Datacenter, "proxmox-123"),
- ("+guest/proxmox_123", IpsetScope::Guest, "proxmox_123"),
- ] {
- let ipset_name = test_case.0.parse::<IpsetName>().expect("valid ipset name");
-
- assert_eq!(
- ipset_name,
- IpsetName {
- scope: test_case.1,
- name: test_case.2.to_string(),
- }
- )
- }
-
- for name in ["+dc/", "+guests/proxmox_123", "guest/proxmox_123"] {
- name.parse::<IpsetName>().expect_err("invalid ipset name");
- }
- }
-
- #[test]
- fn test_parse_ipset_address() {
- let mut ipset_address = "10.0.0.1"
- .parse::<IpsetAddress>()
- .expect("valid ipset address");
- assert!(matches!(ipset_address, IpsetAddress::Cidr(Cidr::Ipv4(..))));
-
- ipset_address = "fe80::1/64"
- .parse::<IpsetAddress>()
- .expect("valid ipset address");
- assert!(matches!(ipset_address, IpsetAddress::Cidr(Cidr::Ipv6(..))));
-
- ipset_address = "dc/proxmox-123"
- .parse::<IpsetAddress>()
- .expect("valid ipset address");
- assert!(matches!(ipset_address, IpsetAddress::Alias(..)));
-
- ipset_address = "guest/proxmox_123"
- .parse::<IpsetAddress>()
- .expect("valid ipset address");
- assert!(matches!(ipset_address, IpsetAddress::Alias(..)));
- }
-
- #[test]
- fn test_ipfilter() {
- let mut ipset = Ipset::from_parts(IpsetScope::Guest, "ipfilter-net0");
- ipset.ipfilter().expect("is an ipfilter");
-
- ipset = Ipset::from_parts(IpsetScope::Guest, "ipfilter-qwe");
- assert!(ipset.ipfilter().is_none());
-
- ipset = Ipset::from_parts(IpsetScope::Guest, "proxmox");
- assert!(ipset.ipfilter().is_none());
-
- ipset = Ipset::from_parts(IpsetScope::Datacenter, "ipfilter-net0");
- assert!(ipset.ipfilter().is_none());
- }
-
- #[test]
- fn test_parse_ipset_entry() {
- let mut entry = "!10.0.0.1 # qweqweasd"
- .parse::<IpsetEntry>()
- .expect("valid ipset entry");
-
- assert_eq!(
- entry,
- IpsetEntry {
- nomatch: true,
- comment: Some("qweqweasd".to_string()),
- address: IpsetAddress::Cidr(Cidr::new_v4([10, 0, 0, 1], 32).unwrap())
- }
- );
-
- entry = "fe80::1/48"
- .parse::<IpsetEntry>()
- .expect("valid ipset entry");
-
- assert_eq!(
- entry,
- IpsetEntry {
- nomatch: false,
- comment: None,
- address: IpsetAddress::Cidr(
- Cidr::new_v6([0xFE80, 0, 0, 0, 0, 0, 0, 1], 48).unwrap()
- )
- }
- )
- }
-}
diff --git a/proxmox-ve-config/src/firewall/types/log.rs b/proxmox-ve-config/src/firewall/types/log.rs
deleted file mode 100644
index 72344e4..0000000
--- a/proxmox-ve-config/src/firewall/types/log.rs
+++ /dev/null
@@ -1,222 +0,0 @@
-use std::fmt;
-use std::str::FromStr;
-
-use crate::firewall::parse::parse_bool;
-use anyhow::{bail, Error};
-use serde::{Deserialize, Serialize};
-
-#[derive(Copy, Clone, Debug, Deserialize, Serialize, Default)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-#[serde(rename_all = "lowercase")]
-pub enum LogRateLimitTimescale {
- #[default]
- Second,
- Minute,
- Hour,
- Day,
-}
-
-impl FromStr for LogRateLimitTimescale {
- type Err = Error;
-
- fn from_str(str: &str) -> Result<Self, Error> {
- match str {
- "second" => Ok(LogRateLimitTimescale::Second),
- "minute" => Ok(LogRateLimitTimescale::Minute),
- "hour" => Ok(LogRateLimitTimescale::Hour),
- "day" => Ok(LogRateLimitTimescale::Day),
- _ => bail!("Invalid time scale provided"),
- }
- }
-}
-
-#[derive(Debug, Deserialize, Clone)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct LogRateLimit {
- enabled: bool,
- rate: i64, // in packets
- per: LogRateLimitTimescale,
- burst: i64, // in packets
-}
-
-impl LogRateLimit {
- pub fn new(enabled: bool, rate: i64, per: LogRateLimitTimescale, burst: i64) -> Self {
- Self {
- enabled,
- rate,
- per,
- burst,
- }
- }
-
- pub fn enabled(&self) -> bool {
- self.enabled
- }
-
- pub fn rate(&self) -> i64 {
- self.rate
- }
-
- pub fn burst(&self) -> i64 {
- self.burst
- }
-
- pub fn per(&self) -> LogRateLimitTimescale {
- self.per
- }
-}
-
-impl Default for LogRateLimit {
- fn default() -> Self {
- Self {
- enabled: true,
- rate: 1,
- burst: 5,
- per: LogRateLimitTimescale::Second,
- }
- }
-}
-
-impl FromStr for LogRateLimit {
- type Err = Error;
-
- fn from_str(str: &str) -> Result<Self, Error> {
- let mut limit = Self::default();
-
- for element in str.split(',') {
- match element.split_once('=') {
- None => {
- limit.enabled = parse_bool(element)?;
- }
- Some((key, value)) if !key.is_empty() && !value.is_empty() => match key {
- "enable" => limit.enabled = parse_bool(value)?,
- "burst" => limit.burst = i64::from_str(value)?,
- "rate" => match value.split_once('/') {
- None => {
- limit.rate = i64::from_str(value)?;
- }
- Some((rate, unit)) => {
- if unit.is_empty() {
- bail!("empty unit specification")
- }
-
- limit.rate = i64::from_str(rate)?;
- limit.per = LogRateLimitTimescale::from_str(unit)?;
- }
- },
- _ => bail!("Invalid value for Key found in log_ratelimit!"),
- },
- _ => bail!("invalid value in log_ratelimit"),
- }
- }
-
- Ok(limit)
- }
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
-pub enum LogLevel {
- #[default]
- Nolog,
- Emergency,
- Alert,
- Critical,
- Error,
- Warning,
- Notice,
- Info,
- Debug,
-}
-
-impl std::str::FromStr for LogLevel {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Error> {
- Ok(match s {
- "nolog" => LogLevel::Nolog,
- "emerg" => LogLevel::Emergency,
- "alert" => LogLevel::Alert,
- "crit" => LogLevel::Critical,
- "err" => LogLevel::Error,
- "warn" => LogLevel::Warning,
- "warning" => LogLevel::Warning,
- "notice" => LogLevel::Notice,
- "info" => LogLevel::Info,
- "debug" => LogLevel::Debug,
- _ => bail!("invalid log level {s:?}"),
- })
- }
-}
-
-impl fmt::Display for LogLevel {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- f.write_str(match self {
- LogLevel::Nolog => "nolog",
- LogLevel::Emergency => "emerg",
- LogLevel::Alert => "alert",
- LogLevel::Critical => "crit",
- LogLevel::Error => "err",
- LogLevel::Warning => "warn",
- LogLevel::Notice => "notice",
- LogLevel::Info => "info",
- LogLevel::Debug => "debug",
- })
- }
-}
-
-serde_plain::derive_deserialize_from_fromstr!(LogLevel, "valid log level");
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_parse_rate_limit() {
- let mut parsed_rate_limit = "1,burst=123,rate=44"
- .parse::<LogRateLimit>()
- .expect("valid rate limit");
-
- assert_eq!(
- parsed_rate_limit,
- LogRateLimit {
- enabled: true,
- burst: 123,
- rate: 44,
- per: LogRateLimitTimescale::Second,
- }
- );
-
- parsed_rate_limit = "1".parse::<LogRateLimit>().expect("valid rate limit");
-
- assert_eq!(parsed_rate_limit, LogRateLimit::default());
-
- parsed_rate_limit = "enable=0,rate=123/hour"
- .parse::<LogRateLimit>()
- .expect("valid rate limit");
-
- assert_eq!(
- parsed_rate_limit,
- LogRateLimit {
- enabled: false,
- burst: 5,
- rate: 123,
- per: LogRateLimitTimescale::Hour,
- }
- );
-
- "2".parse::<LogRateLimit>()
- .expect_err("invalid value for enable");
-
- "enabled=0,rate=123"
- .parse::<LogRateLimit>()
- .expect_err("invalid key in log ratelimit");
-
- "enable=0,rate=123,"
- .parse::<LogRateLimit>()
- .expect_err("trailing comma in log rate limit specification");
-
- "enable=0,rate=123/proxmox,"
- .parse::<LogRateLimit>()
- .expect_err("invalid unit for rate");
- }
-}
diff --git a/proxmox-ve-config/src/firewall/types/mod.rs b/proxmox-ve-config/src/firewall/types/mod.rs
deleted file mode 100644
index 8fd551e..0000000
--- a/proxmox-ve-config/src/firewall/types/mod.rs
+++ /dev/null
@@ -1,14 +0,0 @@
-pub mod address;
-pub mod alias;
-pub mod group;
-pub mod ipset;
-pub mod log;
-pub mod port;
-pub mod rule;
-pub mod rule_match;
-
-pub use address::Cidr;
-pub use alias::Alias;
-pub use group::Group;
-pub use ipset::Ipset;
-pub use rule::Rule;
diff --git a/proxmox-ve-config/src/firewall/types/port.rs b/proxmox-ve-config/src/firewall/types/port.rs
deleted file mode 100644
index c1252d9..0000000
--- a/proxmox-ve-config/src/firewall/types/port.rs
+++ /dev/null
@@ -1,181 +0,0 @@
-use std::fmt;
-use std::ops::Deref;
-
-use anyhow::{bail, Error};
-use serde_with::DeserializeFromStr;
-
-use crate::firewall::ports::parse_named_port;
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum PortEntry {
- Port(u16),
- Range(u16, u16),
-}
-
-impl fmt::Display for PortEntry {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- match self {
- Self::Port(p) => write!(f, "{p}"),
- Self::Range(beg, end) => write!(f, "{beg}-{end}"),
- }
- }
-}
-
-fn parse_port(port: &str) -> Result<u16, Error> {
- if let Ok(port) = port.parse::<u16>() {
- return Ok(port);
- }
-
- if let Ok(port) = parse_named_port(port) {
- return Ok(port);
- }
-
- bail!("invalid port specification: {port}")
-}
-
-impl std::str::FromStr for PortEntry {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- Ok(match s.trim().split_once(':') {
- None => PortEntry::from(parse_port(s)?),
- Some((first, second)) => {
- PortEntry::try_from((parse_port(first)?, parse_port(second)?))?
- }
- })
- }
-}
-
-impl From<u16> for PortEntry {
- fn from(port: u16) -> Self {
- PortEntry::Port(port)
- }
-}
-
-impl TryFrom<(u16, u16)> for PortEntry {
- type Error = Error;
-
- fn try_from(ports: (u16, u16)) -> Result<Self, Error> {
- if ports.0 > ports.1 {
- bail!("start port is greater than end port!");
- }
-
- Ok(PortEntry::Range(ports.0, ports.1))
- }
-}
-
-#[derive(Clone, Debug, DeserializeFromStr)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct PortList(pub(crate) Vec<PortEntry>);
-
-impl FromIterator<PortEntry> for PortList {
- fn from_iter<T: IntoIterator<Item = PortEntry>>(iter: T) -> Self {
- Self(iter.into_iter().collect())
- }
-}
-
-impl<T: Into<PortEntry>> From<T> for PortList {
- fn from(value: T) -> Self {
- Self(vec![value.into()])
- }
-}
-
-impl Deref for PortList {
- type Target = Vec<PortEntry>;
-
- fn deref(&self) -> &Self::Target {
- &self.0
- }
-}
-
-impl std::str::FromStr for PortList {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Error> {
- if s.is_empty() {
- bail!("empty port specification");
- }
-
- let mut entries = Vec::new();
-
- for entry in s.trim().split(',') {
- entries.push(entry.parse()?);
- }
-
- if entries.is_empty() {
- bail!("invalid empty port list");
- }
-
- Ok(Self(entries))
- }
-}
-
-impl fmt::Display for PortList {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- use fmt::Write;
- if self.0.len() > 1 {
- f.write_char('{')?;
- }
-
- let mut comma = '\0';
- for entry in &self.0 {
- if std::mem::replace(&mut comma, ',') != '\0' {
- f.write_char(comma)?;
- }
- fmt::Display::fmt(entry, f)?;
- }
-
- if self.0.len() > 1 {
- f.write_char('}')?;
- }
-
- Ok(())
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_parse_port_entry() {
- let mut port_entry: PortEntry = "12345".parse().expect("valid port entry");
- assert_eq!(port_entry, PortEntry::from(12345));
-
- port_entry = "0:65535".parse().expect("valid port entry");
- assert_eq!(port_entry, PortEntry::try_from((0, 65535)).unwrap());
-
- "65536".parse::<PortEntry>().unwrap_err();
- "100:100000".parse::<PortEntry>().unwrap_err();
- "qweasd".parse::<PortEntry>().unwrap_err();
- "".parse::<PortEntry>().unwrap_err();
- }
-
- #[test]
- fn test_parse_port_list() {
- let mut port_list: PortList = "12345".parse().expect("valid port list");
- assert_eq!(port_list, PortList::from(12345));
-
- port_list = "12345,0:65535,1337,ssh:80,https"
- .parse()
- .expect("valid port list");
-
- assert_eq!(
- port_list,
- PortList(vec![
- PortEntry::from(12345),
- PortEntry::try_from((0, 65535)).unwrap(),
- PortEntry::from(1337),
- PortEntry::try_from((22, 80)).unwrap(),
- PortEntry::from(443),
- ])
- );
-
- "0::1337".parse::<PortList>().unwrap_err();
- "0:1337,".parse::<PortList>().unwrap_err();
- "70000".parse::<PortList>().unwrap_err();
- "qweasd".parse::<PortList>().unwrap_err();
- "".parse::<PortList>().unwrap_err();
- }
-}
diff --git a/proxmox-ve-config/src/firewall/types/rule.rs b/proxmox-ve-config/src/firewall/types/rule.rs
deleted file mode 100644
index 20deb3a..0000000
--- a/proxmox-ve-config/src/firewall/types/rule.rs
+++ /dev/null
@@ -1,412 +0,0 @@
-use core::fmt::Display;
-use std::fmt;
-use std::str::FromStr;
-
-use anyhow::{bail, ensure, format_err, Error};
-
-use crate::firewall::parse::match_name;
-use crate::firewall::types::rule_match::RuleMatch;
-use crate::firewall::types::rule_match::RuleOptions;
-
-#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
-pub enum Direction {
- #[default]
- In,
- Out,
-}
-
-impl std::str::FromStr for Direction {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Error> {
- for (name, dir) in [("IN", Direction::In), ("OUT", Direction::Out)] {
- if s.eq_ignore_ascii_case(name) {
- return Ok(dir);
- }
- }
-
- bail!("invalid direction: {s:?}, expect 'IN' or 'OUT'");
- }
-}
-
-serde_plain::derive_deserialize_from_fromstr!(Direction, "valid packet direction");
-
-impl fmt::Display for Direction {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- match self {
- Direction::In => f.write_str("in"),
- Direction::Out => f.write_str("out"),
- }
- }
-}
-
-#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
-pub enum Verdict {
- Accept,
- Reject,
- #[default]
- Drop,
-}
-
-impl std::str::FromStr for Verdict {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Error> {
- for (name, verdict) in [
- ("ACCEPT", Verdict::Accept),
- ("REJECT", Verdict::Reject),
- ("DROP", Verdict::Drop),
- ] {
- if s.eq_ignore_ascii_case(name) {
- return Ok(verdict);
- }
- }
- bail!("invalid verdict {s:?}, expected one of 'ACCEPT', 'REJECT' or 'DROP'");
- }
-}
-
-impl Display for Verdict {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- let string = match self {
- Verdict::Accept => "ACCEPT",
- Verdict::Drop => "DROP",
- Verdict::Reject => "REJECT",
- };
-
- write!(f, "{string}")
- }
-}
-
-serde_plain::derive_deserialize_from_fromstr!(Verdict, "valid verdict");
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Rule {
- pub(crate) disabled: bool,
- pub(crate) kind: Kind,
- pub(crate) comment: Option<String>,
-}
-
-impl std::ops::Deref for Rule {
- type Target = Kind;
-
- fn deref(&self) -> &Self::Target {
- &self.kind
- }
-}
-
-impl std::ops::DerefMut for Rule {
- fn deref_mut(&mut self) -> &mut Self::Target {
- &mut self.kind
- }
-}
-
-impl FromStr for Rule {
- type Err = Error;
-
- fn from_str(input: &str) -> Result<Self, Self::Err> {
- if input.contains(['\n', '\r']) {
- bail!("rule must not contain any newlines!");
- }
-
- let (line, comment) = match input.rsplit_once('#') {
- Some((line, comment)) if !comment.is_empty() => (line.trim(), Some(comment.trim())),
- _ => (input.trim(), None),
- };
-
- let (disabled, line) = match line.strip_prefix('|') {
- Some(line) => (true, line.trim_start()),
- None => (false, line),
- };
-
- // todo: case insensitive?
- let kind = if line.starts_with("GROUP") {
- Kind::from(line.parse::<RuleGroup>()?)
- } else {
- Kind::from(line.parse::<RuleMatch>()?)
- };
-
- Ok(Self {
- disabled,
- comment: comment.map(str::to_string),
- kind,
- })
- }
-}
-
-impl Rule {
- pub fn iface(&self) -> Option<&str> {
- match &self.kind {
- Kind::Group(group) => group.iface(),
- Kind::Match(rule) => rule.iface(),
- }
- }
-
- pub fn disabled(&self) -> bool {
- self.disabled
- }
-
- pub fn kind(&self) -> &Kind {
- &self.kind
- }
-
- pub fn comment(&self) -> Option<&str> {
- self.comment.as_deref()
- }
-}
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum Kind {
- Group(RuleGroup),
- Match(RuleMatch),
-}
-
-impl Kind {
- pub fn is_group(&self) -> bool {
- matches!(self, Kind::Group(_))
- }
-
- pub fn is_match(&self) -> bool {
- matches!(self, Kind::Match(_))
- }
-}
-
-impl From<RuleGroup> for Kind {
- fn from(value: RuleGroup) -> Self {
- Kind::Group(value)
- }
-}
-
-impl From<RuleMatch> for Kind {
- fn from(value: RuleMatch) -> Self {
- Kind::Match(value)
- }
-}
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct RuleGroup {
- pub(crate) group: String,
- pub(crate) iface: Option<String>,
-}
-
-impl RuleGroup {
- pub(crate) fn from_options(group: String, options: RuleOptions) -> Result<Self, Error> {
- ensure!(
- options.proto.is_none()
- && options.dport.is_none()
- && options.sport.is_none()
- && options.dest.is_none()
- && options.source.is_none()
- && options.log.is_none()
- && options.icmp_type.is_none(),
- "only interface parameter is permitted for group rules"
- );
-
- Ok(Self {
- group,
- iface: options.iface,
- })
- }
-
- pub fn group(&self) -> &str {
- &self.group
- }
-
- pub fn iface(&self) -> Option<&str> {
- self.iface.as_deref()
- }
-}
-
-impl FromStr for RuleGroup {
- type Err = Error;
-
- fn from_str(input: &str) -> Result<Self, Self::Err> {
- let (keyword, rest) = match_name(input)
- .ok_or_else(|| format_err!("expected a leading keyword in rule group"))?;
-
- if !keyword.eq_ignore_ascii_case("group") {
- bail!("Expected keyword GROUP")
- }
-
- let (name, rest) =
- match_name(rest.trim()).ok_or_else(|| format_err!("expected a name for rule group"))?;
-
- let options = rest.trim_start().parse()?;
-
- Self::from_options(name.to_string(), options)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use crate::firewall::types::{
- address::{IpEntry, IpList},
- alias::{AliasName, AliasScope},
- ipset::{IpsetName, IpsetScope},
- log::LogLevel,
- rule_match::{Icmp, IcmpCode, IpAddrMatch, IpMatch, Ports, Protocol, Udp},
- Cidr,
- };
-
- use super::*;
-
- #[test]
- fn test_parse_rule() {
- let mut rule: Rule = "|GROUP tgr -i eth0 # acomm".parse().expect("valid rule");
-
- assert_eq!(
- rule,
- Rule {
- disabled: true,
- comment: Some("acomm".to_string()),
- kind: Kind::Group(RuleGroup {
- group: "tgr".to_string(),
- iface: Some("eth0".to_string()),
- }),
- },
- );
-
- rule = "IN ACCEPT -p udp -dport 33 -sport 22 -log warning"
- .parse()
- .expect("valid rule");
-
- assert_eq!(
- rule,
- Rule {
- disabled: false,
- comment: None,
- kind: Kind::Match(RuleMatch {
- dir: Direction::In,
- verdict: Verdict::Accept,
- proto: Some(Udp::new(Ports::from_u16(22, 33)).into()),
- log: Some(LogLevel::Warning),
- ..Default::default()
- }),
- }
- );
-
- rule = "IN ACCEPT --proto udp -i eth0".parse().expect("valid rule");
-
- assert_eq!(
- rule,
- Rule {
- disabled: false,
- comment: None,
- kind: Kind::Match(RuleMatch {
- dir: Direction::In,
- verdict: Verdict::Accept,
- proto: Some(Udp::new(Ports::new(None, None)).into()),
- iface: Some("eth0".to_string()),
- ..Default::default()
- }),
- }
- );
-
- rule = " OUT DROP \
- -source 10.0.0.0/24 -dest 20.0.0.0-20.255.255.255,192.168.0.0/16 \
- -p icmp -log nolog -icmp-type port-unreachable "
- .parse()
- .expect("valid rule");
-
- assert_eq!(
- rule,
- Rule {
- disabled: false,
- comment: None,
- kind: Kind::Match(RuleMatch {
- dir: Direction::Out,
- verdict: Verdict::Drop,
- ip: IpMatch::new(
- IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 24).unwrap())),
- IpAddrMatch::Ip(
- IpList::new(vec![
- IpEntry::Range([20, 0, 0, 0].into(), [20, 255, 255, 255].into()),
- IpEntry::Cidr(Cidr::new_v4([192, 168, 0, 0], 16).unwrap()),
- ])
- .unwrap()
- ),
- )
- .ok(),
- proto: Some(Protocol::Icmp(Icmp::new_code(IcmpCode::Named(
- "port-unreachable"
- )))),
- log: Some(LogLevel::Nolog),
- ..Default::default()
- }),
- }
- );
-
- rule = "IN BGP(ACCEPT) --log crit --iface eth0"
- .parse()
- .expect("valid rule");
-
- assert_eq!(
- rule,
- Rule {
- disabled: false,
- comment: None,
- kind: Kind::Match(RuleMatch {
- dir: Direction::In,
- verdict: Verdict::Accept,
- log: Some(LogLevel::Critical),
- fw_macro: Some("BGP".to_string()),
- iface: Some("eth0".to_string()),
- ..Default::default()
- }),
- }
- );
-
- rule = "IN ACCEPT --source dc/test --dest +dc/test"
- .parse()
- .expect("valid rule");
-
- assert_eq!(
- rule,
- Rule {
- disabled: false,
- comment: None,
- kind: Kind::Match(RuleMatch {
- dir: Direction::In,
- verdict: Verdict::Accept,
- ip: Some(
- IpMatch::new(
- IpAddrMatch::Alias(AliasName::new(AliasScope::Datacenter, "test")),
- IpAddrMatch::Set(IpsetName::new(IpsetScope::Datacenter, "test"),),
- )
- .unwrap()
- ),
- ..Default::default()
- }),
- }
- );
-
- rule = "IN REJECT".parse().expect("valid rule");
-
- assert_eq!(
- rule,
- Rule {
- disabled: false,
- comment: None,
- kind: Kind::Match(RuleMatch {
- dir: Direction::In,
- verdict: Verdict::Reject,
- ..Default::default()
- }),
- }
- );
-
- "IN DROP ---log crit"
- .parse::<Rule>()
- .expect_err("too many dashes in option");
-
- "IN DROP --log --iface eth0"
- .parse::<Rule>()
- .expect_err("no value for option");
-
- "IN DROP --log crit --iface"
- .parse::<Rule>()
- .expect_err("no value for option");
- }
-}
diff --git a/proxmox-ve-config/src/firewall/types/rule_match.rs b/proxmox-ve-config/src/firewall/types/rule_match.rs
deleted file mode 100644
index 94d8624..0000000
--- a/proxmox-ve-config/src/firewall/types/rule_match.rs
+++ /dev/null
@@ -1,977 +0,0 @@
-use std::collections::HashMap;
-use std::fmt;
-use std::str::FromStr;
-
-use serde::Deserialize;
-
-use anyhow::{bail, format_err, Error};
-use serde::de::IntoDeserializer;
-
-use proxmox_sortable_macro::sortable;
-
-use crate::firewall::parse::{match_name, match_non_whitespace, SomeStr};
-use crate::firewall::types::address::{Family, IpList};
-use crate::firewall::types::alias::AliasName;
-use crate::firewall::types::ipset::IpsetName;
-use crate::firewall::types::log::LogLevel;
-use crate::firewall::types::port::PortList;
-use crate::firewall::types::rule::{Direction, Verdict};
-
-#[derive(Clone, Debug, Default, Deserialize)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-#[serde(deny_unknown_fields, rename_all = "kebab-case")]
-pub(crate) struct RuleOptions {
- #[serde(alias = "p")]
- pub(crate) proto: Option<String>,
-
- pub(crate) dport: Option<String>,
- pub(crate) sport: Option<String>,
-
- pub(crate) dest: Option<String>,
- pub(crate) source: Option<String>,
-
- #[serde(alias = "i")]
- pub(crate) iface: Option<String>,
-
- pub(crate) log: Option<LogLevel>,
- pub(crate) icmp_type: Option<String>,
-}
-
-impl FromStr for RuleOptions {
- type Err = Error;
-
- fn from_str(mut line: &str) -> Result<Self, Self::Err> {
- let mut options = HashMap::new();
-
- loop {
- line = line.trim_start();
-
- if line.is_empty() {
- break;
- }
-
- line = line
- .strip_prefix('-')
- .ok_or_else(|| format_err!("expected an option starting with '-'"))?;
-
- // second dash is optional
- line = line.strip_prefix('-').unwrap_or(line);
-
- let param;
- (param, line) = match_name(line)
- .ok_or_else(|| format_err!("expected a parameter name after '-'"))?;
-
- let value;
- (value, line) = match_non_whitespace(line.trim_start())
- .ok_or_else(|| format_err!("expected a value for {param:?}"))?;
-
- if options.insert(param, SomeStr(value)).is_some() {
- bail!("duplicate option in rule: {param}")
- }
- }
-
- Ok(RuleOptions::deserialize(IntoDeserializer::<
- '_,
- crate::firewall::parse::SerdeStringError,
- >::into_deserializer(
- options
- ))?)
- }
-}
-
-#[derive(Clone, Debug, Default)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct RuleMatch {
- pub(crate) dir: Direction,
- pub(crate) verdict: Verdict,
- pub(crate) fw_macro: Option<String>,
-
- pub(crate) iface: Option<String>,
- pub(crate) log: Option<LogLevel>,
- pub(crate) ip: Option<IpMatch>,
- pub(crate) proto: Option<Protocol>,
-}
-
-impl RuleMatch {
- pub(crate) fn from_options(
- dir: Direction,
- verdict: Verdict,
- fw_macro: impl Into<Option<String>>,
- options: RuleOptions,
- ) -> Result<Self, Error> {
- if options.dport.is_some() && options.icmp_type.is_some() {
- bail!("dport and icmp-type are mutually exclusive");
- }
-
- let ip = IpMatch::from_options(&options)?;
- let proto = Protocol::from_options(&options)?;
-
- // todo: check protocol & IP Version compatibility
-
- Ok(Self {
- dir,
- verdict,
- fw_macro: fw_macro.into(),
- iface: options.iface,
- log: options.log,
- ip,
- proto,
- })
- }
-
- pub fn direction(&self) -> Direction {
- self.dir
- }
-
- pub fn iface(&self) -> Option<&str> {
- self.iface.as_deref()
- }
-
- pub fn verdict(&self) -> Verdict {
- self.verdict
- }
-
- pub fn fw_macro(&self) -> Option<&str> {
- self.fw_macro.as_deref()
- }
-
- pub fn log(&self) -> Option<LogLevel> {
- self.log
- }
-
- pub fn ip(&self) -> Option<&IpMatch> {
- self.ip.as_ref()
- }
-
- pub fn proto(&self) -> Option<&Protocol> {
- self.proto.as_ref()
- }
-}
-
-/// Returns `(Macro name, Verdict, RestOfTheLine)`.
-fn parse_action(line: &str) -> Result<(Option<&str>, Verdict, &str), Error> {
- let (verdict, line) =
- match_name(line).ok_or_else(|| format_err!("expected a verdict or macro name"))?;
-
- Ok(if let Some(line) = line.strip_prefix('(') {
- // <macro>(<verdict>)
-
- let macro_name = verdict;
- let (verdict, line) = match_name(line).ok_or_else(|| format_err!("expected a verdict"))?;
- let line = line
- .strip_prefix(')')
- .ok_or_else(|| format_err!("expected closing ')' after verdict"))?;
-
- let verdict: Verdict = verdict.parse()?;
-
- (Some(macro_name), verdict, line.trim_start())
- } else {
- (None, verdict.parse()?, line.trim_start())
- })
-}
-
-impl FromStr for RuleMatch {
- type Err = Error;
-
- fn from_str(line: &str) -> Result<Self, Self::Err> {
- let (dir, rest) = match_name(line).ok_or_else(|| format_err!("expected a direction"))?;
-
- let direction: Direction = dir.parse()?;
-
- let (fw_macro, verdict, rest) = parse_action(rest.trim_start())?;
-
- let options: RuleOptions = rest.trim_start().parse()?;
-
- Self::from_options(direction, verdict, fw_macro.map(str::to_string), options)
- }
-}
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct IpMatch {
- pub(crate) src: Option<IpAddrMatch>,
- pub(crate) dst: Option<IpAddrMatch>,
-}
-
-impl IpMatch {
- pub fn new(
- src: impl Into<Option<IpAddrMatch>>,
- dst: impl Into<Option<IpAddrMatch>>,
- ) -> Result<Self, Error> {
- let source = src.into();
- let dest = dst.into();
-
- if source.is_none() && dest.is_none() {
- bail!("either src or dst must be set")
- }
-
- if let (Some(IpAddrMatch::Ip(src)), Some(IpAddrMatch::Ip(dst))) = (&source, &dest) {
- if src.family() != dst.family() {
- bail!("src and dst family must be equal")
- }
- }
-
- let ip_match = Self {
- src: source,
- dst: dest,
- };
-
- Ok(ip_match)
- }
-
- fn from_options(options: &RuleOptions) -> Result<Option<Self>, Error> {
- let src = options
- .source
- .as_ref()
- .map(|elem| elem.parse::<IpAddrMatch>())
- .transpose()?;
-
- let dst = options
- .dest
- .as_ref()
- .map(|elem| elem.parse::<IpAddrMatch>())
- .transpose()?;
-
- if src.is_some() || dst.is_some() {
- Ok(Some(IpMatch::new(src, dst)?))
- } else {
- Ok(None)
- }
- }
-
- pub fn src(&self) -> Option<&IpAddrMatch> {
- self.src.as_ref()
- }
-
- pub fn dst(&self) -> Option<&IpAddrMatch> {
- self.dst.as_ref()
- }
-}
-
-#[derive(Clone, Debug, Deserialize)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum IpAddrMatch {
- Ip(IpList),
- Set(IpsetName),
- Alias(AliasName),
-}
-
-impl IpAddrMatch {
- pub fn family(&self) -> Option<Family> {
- if let IpAddrMatch::Ip(list) = self {
- return Some(list.family());
- }
-
- None
- }
-}
-
-impl FromStr for IpAddrMatch {
- type Err = Error;
-
- fn from_str(value: &str) -> Result<Self, Error> {
- if value.is_empty() {
- bail!("empty IP specification");
- }
-
- if let Ok(ip_list) = value.parse() {
- return Ok(IpAddrMatch::Ip(ip_list));
- }
-
- if let Ok(ipset) = value.parse() {
- return Ok(IpAddrMatch::Set(ipset));
- }
-
- if let Ok(name) = value.parse() {
- return Ok(IpAddrMatch::Alias(name));
- }
-
- bail!("invalid IP specification: {value}")
- }
-}
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum Protocol {
- Dccp(Ports),
- Sctp(Sctp),
- Tcp(Tcp),
- Udp(Udp),
- UdpLite(Ports),
- Icmp(Icmp),
- Icmpv6(Icmpv6),
- Named(String),
- Numeric(u8),
-}
-
-impl Protocol {
- pub(crate) fn from_options(options: &RuleOptions) -> Result<Option<Self>, Error> {
- let proto = match options.proto.as_deref() {
- Some(p) => p,
- None => return Ok(None),
- };
-
- Ok(Some(match proto {
- "dccp" | "33" => Protocol::Dccp(Ports::from_options(options)?),
- "sctp" | "132" => Protocol::Sctp(Sctp::from_options(options)?),
- "tcp" | "6" => Protocol::Tcp(Tcp::from_options(options)?),
- "udp" | "17" => Protocol::Udp(Udp::from_options(options)?),
- "udplite" | "136" => Protocol::UdpLite(Ports::from_options(options)?),
- "icmp" | "1" => Protocol::Icmp(Icmp::from_options(options)?),
- "ipv6-icmp" | "icmpv6" | "58" => Protocol::Icmpv6(Icmpv6::from_options(options)?),
- other => match other.parse::<u8>() {
- Ok(num) => Protocol::Numeric(num),
- Err(_) => Protocol::Named(other.to_string()),
- },
- }))
- }
-
- pub fn family(&self) -> Option<Family> {
- match self {
- Self::Icmp(_) => Some(Family::V4),
- Self::Icmpv6(_) => Some(Family::V6),
- _ => None,
- }
- }
-}
-
-#[derive(Clone, Debug, Default)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Udp {
- ports: Ports,
-}
-
-impl Udp {
- fn from_options(options: &RuleOptions) -> Result<Self, Error> {
- Ok(Self {
- ports: Ports::from_options(options)?,
- })
- }
-
- pub fn new(ports: Ports) -> Self {
- Self { ports }
- }
-
- pub fn ports(&self) -> &Ports {
- &self.ports
- }
-}
-
-impl From<Udp> for Protocol {
- fn from(value: Udp) -> Self {
- Protocol::Udp(value)
- }
-}
-
-#[derive(Clone, Debug, Default)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Ports {
- sport: Option<PortList>,
- dport: Option<PortList>,
-}
-
-impl Ports {
- pub fn new(sport: impl Into<Option<PortList>>, dport: impl Into<Option<PortList>>) -> Self {
- Self {
- sport: sport.into(),
- dport: dport.into(),
- }
- }
-
- fn from_options(options: &RuleOptions) -> Result<Self, Error> {
- Ok(Self {
- sport: options.sport.as_deref().map(|s| s.parse()).transpose()?,
- dport: options.dport.as_deref().map(|s| s.parse()).transpose()?,
- })
- }
-
- pub fn from_u16(sport: impl Into<Option<u16>>, dport: impl Into<Option<u16>>) -> Self {
- Self::new(
- sport.into().map(PortList::from),
- dport.into().map(PortList::from),
- )
- }
-
- pub fn sport(&self) -> Option<&PortList> {
- self.sport.as_ref()
- }
-
- pub fn dport(&self) -> Option<&PortList> {
- self.dport.as_ref()
- }
-}
-
-#[derive(Clone, Debug, Default)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Tcp {
- ports: Ports,
-}
-
-impl Tcp {
- pub fn new(ports: Ports) -> Self {
- Self { ports }
- }
-
- fn from_options(options: &RuleOptions) -> Result<Self, Error> {
- Ok(Self {
- ports: Ports::from_options(options)?,
- })
- }
-
- pub fn ports(&self) -> &Ports {
- &self.ports
- }
-}
-
-impl From<Tcp> for Protocol {
- fn from(value: Tcp) -> Self {
- Protocol::Tcp(value)
- }
-}
-
-#[derive(Clone, Debug, Default)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Sctp {
- ports: Ports,
-}
-
-impl Sctp {
- fn from_options(options: &RuleOptions) -> Result<Self, Error> {
- Ok(Self {
- ports: Ports::from_options(options)?,
- })
- }
-
- pub fn ports(&self) -> &Ports {
- &self.ports
- }
-}
-
-#[derive(Clone, Debug, Default)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Icmp {
- ty: Option<IcmpType>,
- code: Option<IcmpCode>,
-}
-
-impl Icmp {
- pub fn new_ty(ty: IcmpType) -> Self {
- Self {
- ty: Some(ty),
- ..Default::default()
- }
- }
-
- pub fn new_code(code: IcmpCode) -> Self {
- Self {
- code: Some(code),
- ..Default::default()
- }
- }
-
- fn from_options(options: &RuleOptions) -> Result<Self, Error> {
- if let Some(ty) = &options.icmp_type {
- return ty.parse();
- }
-
- Ok(Self::default())
- }
-
- pub fn ty(&self) -> Option<&IcmpType> {
- self.ty.as_ref()
- }
-
- pub fn code(&self) -> Option<&IcmpCode> {
- self.code.as_ref()
- }
-}
-
-impl FromStr for Icmp {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- let mut this = Self::default();
-
- if let Ok(ty) = s.parse() {
- this.ty = Some(ty);
- return Ok(this);
- }
-
- if let Ok(code) = s.parse() {
- this.code = Some(code);
- return Ok(this);
- }
-
- bail!("supplied string is neither a valid icmp type nor code");
- }
-}
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum IcmpType {
- Numeric(u8),
- Named(&'static str),
- Any,
-}
-
-#[sortable]
-const ICMP_TYPES: [(&str, u8); 15] = sorted!([
- ("address-mask-reply", 18),
- ("address-mask-request", 17),
- ("destination-unreachable", 3),
- ("echo-reply", 0),
- ("echo-request", 8),
- ("info-reply", 16),
- ("info-request", 15),
- ("parameter-problem", 12),
- ("redirect", 5),
- ("router-advertisement", 9),
- ("router-solicitation", 10),
- ("source-quench", 4),
- ("time-exceeded", 11),
- ("timestamp-reply", 14),
- ("timestamp-request", 13),
-]);
-
-impl std::str::FromStr for IcmpType {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Error> {
- if s.eq_ignore_ascii_case("any") {
- return Ok(Self::Any);
- }
-
- if let Ok(ty) = s.trim().parse::<u8>() {
- return Ok(Self::Numeric(ty));
- }
-
- if let Ok(index) = ICMP_TYPES.binary_search_by(|v| v.0.cmp(s)) {
- return Ok(Self::Named(ICMP_TYPES[index].0));
- }
-
- bail!("{s:?} is not a valid icmp type");
- }
-}
-
-impl fmt::Display for IcmpType {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- match self {
- IcmpType::Numeric(ty) => write!(f, "{ty}"),
- IcmpType::Named(ty) => write!(f, "{ty}"),
- IcmpType::Any => write!(f, "any"),
- }
- }
-}
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum IcmpCode {
- Numeric(u8),
- Named(&'static str),
-}
-
-#[sortable]
-const ICMP_CODES: [(&str, u8); 7] = sorted!([
- ("admin-prohibited", 13),
- ("host-prohibited", 10),
- ("host-unreachable", 1),
- ("net-prohibited", 9),
- ("net-unreachable", 0),
- ("port-unreachable", 3),
- ("prot-unreachable", 2),
-]);
-
-impl std::str::FromStr for IcmpCode {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Error> {
- if let Ok(code) = s.trim().parse::<u8>() {
- return Ok(Self::Numeric(code));
- }
-
- if let Ok(index) = ICMP_CODES.binary_search_by(|v| v.0.cmp(s)) {
- return Ok(Self::Named(ICMP_CODES[index].0));
- }
-
- bail!("{s:?} is not a valid icmp code");
- }
-}
-
-impl fmt::Display for IcmpCode {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- match self {
- IcmpCode::Numeric(code) => write!(f, "{code}"),
- IcmpCode::Named(code) => write!(f, "{code}"),
- }
- }
-}
-
-#[derive(Clone, Debug, Default)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct Icmpv6 {
- pub ty: Option<Icmpv6Type>,
- pub code: Option<Icmpv6Code>,
-}
-
-impl Icmpv6 {
- pub fn new_ty(ty: Icmpv6Type) -> Self {
- Self {
- ty: Some(ty),
- ..Default::default()
- }
- }
-
- pub fn new_code(code: Icmpv6Code) -> Self {
- Self {
- code: Some(code),
- ..Default::default()
- }
- }
-
- fn from_options(options: &RuleOptions) -> Result<Self, Error> {
- if let Some(ty) = &options.icmp_type {
- return ty.parse();
- }
-
- Ok(Self::default())
- }
-
- pub fn ty(&self) -> Option<&Icmpv6Type> {
- self.ty.as_ref()
- }
-
- pub fn code(&self) -> Option<&Icmpv6Code> {
- self.code.as_ref()
- }
-}
-
-impl FromStr for Icmpv6 {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- let mut this = Self::default();
-
- if let Ok(ty) = s.parse() {
- this.ty = Some(ty);
- return Ok(this);
- }
-
- if let Ok(code) = s.parse() {
- this.code = Some(code);
- return Ok(this);
- }
-
- bail!("supplied string is neither a valid icmpv6 type nor code");
- }
-}
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum Icmpv6Type {
- Numeric(u8),
- Named(&'static str),
- Any,
-}
-
-#[sortable]
-const ICMPV6_TYPES: [(&str, u8); 19] = sorted!([
- ("destination-unreachable", 1),
- ("echo-reply", 129),
- ("echo-request", 128),
- ("ind-neighbor-advert", 142),
- ("ind-neighbor-solicit", 141),
- ("mld-listener-done", 132),
- ("mld-listener-query", 130),
- ("mld-listener-reduction", 132),
- ("mld-listener-report", 131),
- ("mld2-listener-report", 143),
- ("nd-neighbor-advert", 136),
- ("nd-neighbor-solicit", 135),
- ("nd-redirect", 137),
- ("nd-router-advert", 134),
- ("nd-router-solicit", 133),
- ("packet-too-big", 2),
- ("parameter-problem", 4),
- ("router-renumbering", 138),
- ("time-exceeded", 3),
-]);
-
-impl std::str::FromStr for Icmpv6Type {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Error> {
- if s.eq_ignore_ascii_case("any") {
- return Ok(Self::Any);
- }
-
- if let Ok(ty) = s.trim().parse::<u8>() {
- return Ok(Self::Numeric(ty));
- }
-
- if let Ok(index) = ICMPV6_TYPES.binary_search_by(|v| v.0.cmp(s)) {
- return Ok(Self::Named(ICMPV6_TYPES[index].0));
- }
-
- bail!("{s:?} is not a valid icmpv6 type");
- }
-}
-
-impl fmt::Display for Icmpv6Type {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- match self {
- Icmpv6Type::Numeric(ty) => write!(f, "{ty}"),
- Icmpv6Type::Named(ty) => write!(f, "{ty}"),
- Icmpv6Type::Any => write!(f, "any"),
- }
- }
-}
-
-#[derive(Clone, Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum Icmpv6Code {
- Numeric(u8),
- Named(&'static str),
-}
-
-#[sortable]
-const ICMPV6_CODES: [(&str, u8); 6] = sorted!([
- ("addr-unreachable", 3),
- ("admin-prohibited", 1),
- ("no-route", 0),
- ("policy-fail", 5),
- ("port-unreachable", 4),
- ("reject-route", 6),
-]);
-
-impl std::str::FromStr for Icmpv6Code {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Error> {
- if let Ok(code) = s.trim().parse::<u8>() {
- return Ok(Self::Numeric(code));
- }
-
- if let Ok(index) = ICMPV6_CODES.binary_search_by(|v| v.0.cmp(s)) {
- return Ok(Self::Named(ICMPV6_CODES[index].0));
- }
-
- bail!("{s:?} is not a valid icmpv6 code");
- }
-}
-
-impl fmt::Display for Icmpv6Code {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- match self {
- Icmpv6Code::Numeric(code) => write!(f, "{code}"),
- Icmpv6Code::Named(code) => write!(f, "{code}"),
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use crate::firewall::types::{alias::AliasScope::Guest, Cidr};
-
- use super::*;
-
- #[test]
- fn test_parse_action() {
- assert_eq!(parse_action("REJECT").unwrap(), (None, Verdict::Reject, ""));
-
- assert_eq!(
- parse_action("SSH(ACCEPT) qweasd").unwrap(),
- (Some("SSH"), Verdict::Accept, "qweasd")
- );
- }
-
- #[test]
- fn test_parse_ip_addr_match() {
- for input in [
- "10.0.0.0/8",
- "10.0.0.0/8,192.168.0.0-192.168.255.255,172.16.0.1",
- "dc/test",
- "+guest/proxmox",
- ] {
- input.parse::<IpAddrMatch>().expect("valid ip match");
- }
-
- for input in [
- "10.0.0.0/",
- "10.0.0.0/8,192.168.256.0-192.168.255.255,172.16.0.1",
- "dcc/test",
- "+guest/",
- "",
- ] {
- input.parse::<IpAddrMatch>().expect_err("invalid ip match");
- }
- }
-
- #[test]
- fn test_parse_options() {
- let mut options: RuleOptions =
- "-p udp --sport 123 --dport 234 -source 127.0.0.1 --dest 127.0.0.1 -i ens1 --log crit"
- .parse()
- .expect("valid option string");
-
- assert_eq!(
- options,
- RuleOptions {
- proto: Some("udp".to_string()),
- sport: Some("123".to_string()),
- dport: Some("234".to_string()),
- source: Some("127.0.0.1".to_string()),
- dest: Some("127.0.0.1".to_string()),
- iface: Some("ens1".to_string()),
- log: Some(LogLevel::Critical),
- icmp_type: None,
- }
- );
-
- options = "".parse().expect("valid option string");
-
- assert_eq!(options, RuleOptions::default(),);
- }
-
- #[test]
- fn test_construct_ip_match() {
- IpMatch::new(
- IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 8).unwrap())),
- IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 8).unwrap())),
- )
- .expect("valid ip match");
-
- IpMatch::new(
- IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 8).unwrap())),
- IpAddrMatch::Alias(AliasName::new(Guest, "test")),
- )
- .expect("valid ip match");
-
- IpMatch::new(
- IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 8).unwrap())),
- IpAddrMatch::Ip(IpList::from(Cidr::new_v6([0x0000; 8], 8).unwrap())),
- )
- .expect_err("cannot mix ip families");
-
- IpMatch::new(None, None).expect_err("at least one ip must be set");
- }
-
- #[test]
- fn test_from_options() {
- let mut options = RuleOptions {
- proto: Some("tcp".to_string()),
- sport: Some("123".to_string()),
- dport: Some("234".to_string()),
- source: Some("192.168.0.1".to_string()),
- dest: Some("10.0.0.1".to_string()),
- iface: Some("eth123".to_string()),
- log: Some(LogLevel::Error),
- ..Default::default()
- };
-
- assert_eq!(
- Protocol::from_options(&options).unwrap().unwrap(),
- Protocol::Tcp(Tcp::new(Ports::from_u16(123, 234))),
- );
-
- assert_eq!(
- IpMatch::from_options(&options).unwrap().unwrap(),
- IpMatch::new(
- IpAddrMatch::Ip(IpList::from(Cidr::new_v4([192, 168, 0, 1], 32).unwrap()),),
- IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 1], 32).unwrap()),)
- )
- .unwrap(),
- );
-
- options = RuleOptions::default();
-
- assert_eq!(Protocol::from_options(&options).unwrap(), None,);
-
- assert_eq!(IpMatch::from_options(&options).unwrap(), None,);
-
- options = RuleOptions {
- proto: Some("tcp".to_string()),
- sport: Some("qwe".to_string()),
- source: Some("qwe".to_string()),
- ..Default::default()
- };
-
- Protocol::from_options(&options).expect_err("invalid source port");
-
- IpMatch::from_options(&options).expect_err("invalid source address");
-
- options = RuleOptions {
- icmp_type: Some("port-unreachable".to_string()),
- dport: Some("123".to_string()),
- ..Default::default()
- };
-
- RuleMatch::from_options(Direction::In, Verdict::Drop, None, options)
- .expect_err("cannot mix dport and icmp-type");
- }
-
- #[test]
- fn test_parse_icmp() {
- let mut icmp: Icmp = "info-request".parse().expect("valid icmp type");
-
- assert_eq!(
- icmp,
- Icmp {
- ty: Some(IcmpType::Named("info-request")),
- code: None
- }
- );
-
- icmp = "12".parse().expect("valid icmp type");
-
- assert_eq!(
- icmp,
- Icmp {
- ty: Some(IcmpType::Numeric(12)),
- code: None
- }
- );
-
- icmp = "port-unreachable".parse().expect("valid icmp code");
-
- assert_eq!(
- icmp,
- Icmp {
- ty: None,
- code: Some(IcmpCode::Named("port-unreachable"))
- }
- );
- }
-
- #[test]
- fn test_parse_icmp6() {
- let mut icmp: Icmpv6 = "echo-reply".parse().expect("valid icmpv6 type");
-
- assert_eq!(
- icmp,
- Icmpv6 {
- ty: Some(Icmpv6Type::Named("echo-reply")),
- code: None
- }
- );
-
- icmp = "12".parse().expect("valid icmpv6 type");
-
- assert_eq!(
- icmp,
- Icmpv6 {
- ty: Some(Icmpv6Type::Numeric(12)),
- code: None
- }
- );
-
- icmp = "admin-prohibited".parse().expect("valid icmpv6 code");
-
- assert_eq!(
- icmp,
- Icmpv6 {
- ty: None,
- code: Some(Icmpv6Code::Named("admin-prohibited"))
- }
- );
- }
-}
diff --git a/proxmox-ve-config/src/guest/mod.rs b/proxmox-ve-config/src/guest/mod.rs
deleted file mode 100644
index 74fd8ab..0000000
--- a/proxmox-ve-config/src/guest/mod.rs
+++ /dev/null
@@ -1,115 +0,0 @@
-use core::ops::Deref;
-use std::collections::HashMap;
-
-use anyhow::{Context, Error};
-use serde::Deserialize;
-
-use proxmox_sys::nodename;
-use types::Vmid;
-
-pub mod types;
-pub mod vm;
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize)]
-pub enum GuestType {
- #[serde(rename = "qemu")]
- Vm,
- #[serde(rename = "lxc")]
- Ct,
-}
-
-impl GuestType {
- pub fn iface_prefix(self) -> &'static str {
- match self {
- GuestType::Vm => "tap",
- GuestType::Ct => "veth",
- }
- }
-
- fn config_folder(&self) -> &'static str {
- match self {
- GuestType::Vm => "qemu-server",
- GuestType::Ct => "lxc",
- }
- }
-}
-
-#[derive(Deserialize)]
-pub struct GuestEntry {
- node: String,
-
- #[serde(rename = "type")]
- ty: GuestType,
-
- #[serde(rename = "version")]
- _version: usize,
-}
-
-impl GuestEntry {
- pub fn new(node: String, ty: GuestType) -> Self {
- Self {
- node,
- ty,
- _version: Default::default(),
- }
- }
-
- pub fn is_local(&self) -> bool {
- nodename() == self.node
- }
-
- pub fn ty(&self) -> &GuestType {
- &self.ty
- }
-}
-
-const VMLIST_CONFIG_PATH: &str = "/etc/pve/.vmlist";
-
-#[derive(Deserialize)]
-pub struct GuestMap {
- #[serde(rename = "version")]
- _version: usize,
- #[serde(rename = "ids", default)]
- guests: HashMap<Vmid, GuestEntry>,
-}
-
-impl From<HashMap<Vmid, GuestEntry>> for GuestMap {
- fn from(guests: HashMap<Vmid, GuestEntry>) -> Self {
- Self {
- guests,
- _version: Default::default(),
- }
- }
-}
-
-impl Deref for GuestMap {
- type Target = HashMap<Vmid, GuestEntry>;
-
- fn deref(&self) -> &Self::Target {
- &self.guests
- }
-}
-
-impl GuestMap {
- pub fn new() -> Result<Self, Error> {
- let data = std::fs::read(VMLIST_CONFIG_PATH)
- .with_context(|| format!("failed to read guest map from {VMLIST_CONFIG_PATH}"))?;
-
- serde_json::from_slice(&data).with_context(|| "failed to parse guest map".to_owned())
- }
-
- pub fn firewall_config_path(vmid: &Vmid) -> String {
- format!("/etc/pve/firewall/{}.fw", vmid)
- }
-
- /// returns the local configuration path for a given Vmid.
- ///
- /// The caller must ensure that the given Vmid exists and is local to the node
- pub fn config_path(vmid: &Vmid, entry: &GuestEntry) -> String {
- format!(
- "/etc/pve/local/{}/{}.conf",
- entry.ty().config_folder(),
- vmid
- )
- }
-}
diff --git a/proxmox-ve-config/src/guest/types.rs b/proxmox-ve-config/src/guest/types.rs
deleted file mode 100644
index 217c537..0000000
--- a/proxmox-ve-config/src/guest/types.rs
+++ /dev/null
@@ -1,38 +0,0 @@
-use std::fmt;
-use std::str::FromStr;
-
-use anyhow::{format_err, Error};
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)]
-pub struct Vmid(u32);
-
-impl Vmid {
- pub fn new(id: u32) -> Self {
- Vmid(id)
- }
-}
-
-impl From<u32> for Vmid {
- fn from(value: u32) -> Self {
- Self::new(value)
- }
-}
-
-impl fmt::Display for Vmid {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- fmt::Display::fmt(&self.0, f)
- }
-}
-
-impl FromStr for Vmid {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- Ok(Self(
- s.parse()
- .map_err(|_| format_err!("not a valid vmid: {s:?}"))?,
- ))
- }
-}
-
-serde_plain::derive_deserialize_from_fromstr!(Vmid, "valid vmid");
diff --git a/proxmox-ve-config/src/guest/vm.rs b/proxmox-ve-config/src/guest/vm.rs
deleted file mode 100644
index 5b5866a..0000000
--- a/proxmox-ve-config/src/guest/vm.rs
+++ /dev/null
@@ -1,510 +0,0 @@
-use anyhow::{bail, Error};
-use core::fmt::Display;
-use std::io;
-use std::str::FromStr;
-use std::{collections::HashMap, net::Ipv6Addr};
-
-use proxmox_schema::property_string::PropertyIterator;
-
-use crate::firewall::parse::{match_digits, parse_bool};
-use crate::firewall::types::address::{Ipv4Cidr, Ipv6Cidr};
-
-#[derive(Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct MacAddress([u8; 6]);
-
-static LOCAL_PART: [u8; 8] = [0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
-static EUI64_MIDDLE_PART: [u8; 2] = [0xFF, 0xFE];
-
-impl MacAddress {
- /// generates a link local IPv6-address according to RFC 4291 (Appendix A)
- pub fn eui64_link_local_address(&self) -> Ipv6Addr {
- let head = &self.0[..3];
- let tail = &self.0[3..];
-
- let mut eui64_address: Vec<u8> = LOCAL_PART
- .iter()
- .chain(head.iter())
- .chain(EUI64_MIDDLE_PART.iter())
- .chain(tail.iter())
- .copied()
- .collect();
-
- // we need to flip the 7th bit of the first eui64 byte
- eui64_address[8] ^= 0x02;
-
- Ipv6Addr::from(
- TryInto::<[u8; 16]>::try_into(eui64_address).expect("is an u8 array with 16 entries"),
- )
- }
-}
-
-impl FromStr for MacAddress {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- let split = s.split(':');
-
- let parsed = split
- .into_iter()
- .map(|elem| u8::from_str_radix(elem, 16))
- .collect::<Result<Vec<u8>, _>>()
- .map_err(Error::msg)?;
-
- if parsed.len() != 6 {
- bail!("Invalid amount of elements in MAC address!");
- }
-
- let address = &parsed.as_slice()[0..6];
- Ok(Self(address.try_into().unwrap()))
- }
-}
-
-impl Display for MacAddress {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(
- f,
- "{:<02X}:{:<02X}:{:<02X}:{:<02X}:{:<02X}:{:<02X}",
- self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5]
- )
- }
-}
-
-#[derive(Debug, Clone, Copy)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub enum NetworkDeviceModel {
- VirtIO,
- Veth,
- E1000,
- Vmxnet3,
- RTL8139,
-}
-
-impl FromStr for NetworkDeviceModel {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- match s {
- "virtio" => Ok(NetworkDeviceModel::VirtIO),
- "e1000" => Ok(NetworkDeviceModel::E1000),
- "rtl8139" => Ok(NetworkDeviceModel::RTL8139),
- "vmxnet3" => Ok(NetworkDeviceModel::Vmxnet3),
- "veth" => Ok(NetworkDeviceModel::Veth),
- _ => bail!("Invalid network device model: {s}"),
- }
- }
-}
-
-#[derive(Debug)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct NetworkDevice {
- model: NetworkDeviceModel,
- mac_address: MacAddress,
- firewall: bool,
- ip: Option<Ipv4Cidr>,
- ip6: Option<Ipv6Cidr>,
-}
-
-impl NetworkDevice {
- pub fn model(&self) -> NetworkDeviceModel {
- self.model
- }
-
- pub fn mac_address(&self) -> &MacAddress {
- &self.mac_address
- }
-
- pub fn ip(&self) -> Option<&Ipv4Cidr> {
- self.ip.as_ref()
- }
-
- pub fn ip6(&self) -> Option<&Ipv6Cidr> {
- self.ip6.as_ref()
- }
-
- pub fn has_firewall(&self) -> bool {
- self.firewall
- }
-}
-
-impl FromStr for NetworkDevice {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- let (mut ty, mut hwaddr, mut firewall, mut ip, mut ip6) = (None, None, true, None, None);
-
- for entry in PropertyIterator::new(s) {
- let (key, value) = entry.unwrap();
-
- if let Some(key) = key {
- match key {
- "type" | "model" => {
- ty = Some(NetworkDeviceModel::from_str(&value)?);
- }
- "hwaddr" | "macaddr" => {
- hwaddr = Some(MacAddress::from_str(&value)?);
- }
- "firewall" => {
- firewall = parse_bool(&value)?;
- }
- "ip" => {
- if value == "dhcp" {
- continue;
- }
-
- ip = Some(Ipv4Cidr::from_str(&value)?);
- }
- "ip6" => {
- if value == "dhcp" || value == "auto" {
- continue;
- }
-
- ip6 = Some(Ipv6Cidr::from_str(&value)?);
- }
- _ => {
- if let Ok(model) = NetworkDeviceModel::from_str(key) {
- ty = Some(model);
- hwaddr = Some(MacAddress::from_str(&value)?);
- }
- }
- }
- }
- }
-
- if let (Some(ty), Some(hwaddr)) = (ty, hwaddr) {
- return Ok(NetworkDevice {
- model: ty,
- mac_address: hwaddr,
- firewall,
- ip,
- ip6,
- });
- }
-
- bail!("No valid network device detected in string {s}");
- }
-}
-
-#[derive(Debug, Default)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
-pub struct NetworkConfig {
- network_devices: HashMap<i64, NetworkDevice>,
-}
-
-impl NetworkConfig {
- pub fn new() -> Self {
- Self::default()
- }
-
- pub fn index_from_net_key(key: &str) -> Result<i64, Error> {
- if let Some(digits) = key.strip_prefix("net") {
- if let Some((digits, rest)) = match_digits(digits) {
- let index: i64 = digits.parse()?;
-
- if (0..31).contains(&index) && rest.is_empty() {
- return Ok(index);
- }
- }
- }
-
- bail!("No index found in net key string: {key}")
- }
-
- pub fn network_devices(&self) -> &HashMap<i64, NetworkDevice> {
- &self.network_devices
- }
-
- pub fn parse<R: io::BufRead>(input: R) -> Result<Self, Error> {
- let mut network_devices = HashMap::new();
-
- for line in input.lines() {
- let line = line?;
- let line = line.trim();
-
- if line.is_empty() || line.starts_with('#') {
- continue;
- }
-
- if line.starts_with('[') {
- break;
- }
-
- if line.starts_with("net") {
- log::trace!("parsing net config line: {line}");
-
- if let Some((mut key, mut value)) = line.split_once(':') {
- if key.is_empty() || value.is_empty() {
- continue;
- }
-
- key = key.trim();
- value = value.trim();
-
- if let Ok(index) = Self::index_from_net_key(key) {
- let network_device = NetworkDevice::from_str(value)?;
-
- let exists = network_devices.insert(index, network_device);
-
- if exists.is_some() {
- bail!("Duplicated config key detected: {key}");
- }
- } else {
- bail!("Encountered invalid net key in cfg: {key}");
- }
- }
- }
- }
-
- Ok(Self { network_devices })
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_parse_mac_address() {
- for input in [
- "aa:aa:aa:11:22:33",
- "AA:BB:FF:11:22:33",
- "bc:24:11:AA:bb:Ef",
- ] {
- let mac_address = input.parse::<MacAddress>().expect("valid mac address");
-
- assert_eq!(input.to_uppercase(), mac_address.to_string());
- }
-
- for input in [
- "aa:aa:aa:11:22:33:aa",
- "AA:BB:FF:11:22",
- "AA:BB:GG:11:22:33",
- "AABBGG112233",
- "",
- ] {
- input
- .parse::<MacAddress>()
- .expect_err("invalid mac address");
- }
- }
-
- #[test]
- fn test_eui64_link_local_address() {
- let mac_address: MacAddress = "BC:24:11:49:8D:75".parse().expect("valid MAC address");
-
- let link_local_address =
- Ipv6Addr::from_str("fe80::be24:11ff:fe49:8d75").expect("valid IPv6 address");
-
- assert_eq!(link_local_address, mac_address.eui64_link_local_address());
- }
-
- #[test]
- fn test_parse_network_device() {
- let mut network_device: NetworkDevice =
- "virtio=AA:AA:AA:17:19:81,bridge=public,firewall=1,queues=4"
- .parse()
- .expect("valid network configuration");
-
- assert_eq!(
- network_device,
- NetworkDevice {
- model: NetworkDeviceModel::VirtIO,
- mac_address: MacAddress([0xAA, 0xAA, 0xAA, 0x17, 0x19, 0x81]),
- firewall: true,
- ip: None,
- ip6: None,
- }
- );
-
- network_device = "model=virtio,macaddr=AA:AA:AA:17:19:81,bridge=public,firewall=1,queues=4"
- .parse()
- .expect("valid network configuration");
-
- assert_eq!(
- network_device,
- NetworkDevice {
- model: NetworkDeviceModel::VirtIO,
- mac_address: MacAddress([0xAA, 0xAA, 0xAA, 0x17, 0x19, 0x81]),
- firewall: true,
- ip: None,
- ip6: None,
- }
- );
-
- network_device =
- "name=eth0,bridge=public,firewall=0,hwaddr=AA:AA:AA:E2:3E:24,ip=dhcp,type=veth"
- .parse()
- .expect("valid network configuration");
-
- assert_eq!(
- network_device,
- NetworkDevice {
- model: NetworkDeviceModel::Veth,
- mac_address: MacAddress([0xAA, 0xAA, 0xAA, 0xE2, 0x3E, 0x24]),
- firewall: false,
- ip: None,
- ip6: None,
- }
- );
-
- "model=virtio"
- .parse::<NetworkDevice>()
- .expect_err("invalid network configuration");
-
- "bridge=public,firewall=0"
- .parse::<NetworkDevice>()
- .expect_err("invalid network configuration");
-
- "".parse::<NetworkDevice>()
- .expect_err("invalid network configuration");
-
- "name=eth0,bridge=public,firewall=0,hwaddr=AA:AA:AG:E2:3E:24,ip=dhcp,type=veth"
- .parse::<NetworkDevice>()
- .expect_err("invalid network configuration");
- }
-
- #[test]
- fn test_parse_network_confg() {
- let mut guest_config = "\
-boot: order=scsi0;net0
-cores: 4
-cpu: host
-memory: 8192
-meta: creation-qemu=8.0.2,ctime=1700141675
-name: hoan-sdn
-net0: virtio=AA:BB:CC:F2:FE:75,bridge=public
-numa: 0
-ostype: l26
-parent: uwu
-scsi0: local-lvm:vm-999-disk-0,discard=on,iothread=1,size=32G
-scsihw: virtio-scsi-single
-smbios1: uuid=addb0cc6-0393-4269-a504-1eb46604cb8a
-sockets: 1
-vmgenid: 13bcbb05-3608-4d74-bf4f-d5d20c3538e8
-
-[snapshot]
-boot: order=scsi0;ide2;net0
-cores: 4
-cpu: x86-64-v2-AES
-ide2: NFS-iso:iso/proxmox-ve_8.0-2.iso,media=cdrom,size=1166488K
-memory: 8192
-meta: creation-qemu=8.0.2,ctime=1700141675
-name: test
-net2: virtio=AA:AA:AA:F2:FE:75,bridge=public,firewall=1
-numa: 0
-ostype: l26
-parent: pre-SDN
-scsi0: local-lvm:vm-999-disk-0,discard=on,iothread=1,size=32G
-scsihw: virtio-scsi-single
-smbios1: uuid=addb0cc6-0393-4269-a504-1eb46604cb8a
-snaptime: 1700143513
-sockets: 1
-vmgenid: 706fbe99-d28b-4047-a9cd-3677c859ca8a
-
-[snapshott]
-boot: order=scsi0;ide2;net0
-cores: 4
-cpu: host
-ide2: NFS-iso:iso/proxmox-ve_8.0-2.iso,media=cdrom,size=1166488K
-memory: 8192
-meta: creation-qemu=8.0.2,ctime=1700141675
-name: hoan-sdn
-net0: virtio=AA:AA:FF:F2:FE:75,bridge=public,firewall=0
-numa: 0
-ostype: l26
-parent: SDN
-scsi0: local-lvm:vm-999-disk-0,discard=on,iothread=1,size=32G
-scsihw: virtio-scsi-single
-smbios1: uuid=addb0cc6-0393-4269-a504-1eb46604cb8a
-snaptime: 1700158473
-sockets: 1
-vmgenid: 706fbe99-d28b-4047-a9cd-3677c859ca8a"
- .as_bytes();
-
- let mut network_config: NetworkConfig =
- NetworkConfig::parse(guest_config).expect("valid network configuration");
-
- assert_eq!(network_config.network_devices().len(), 1);
-
- assert_eq!(
- network_config.network_devices()[&0],
- NetworkDevice {
- model: NetworkDeviceModel::VirtIO,
- mac_address: MacAddress([0xAA, 0xBB, 0xCC, 0xF2, 0xFE, 0x75]),
- firewall: true,
- ip: None,
- ip6: None,
- }
- );
-
- guest_config = "\
-arch: amd64
-cores: 1
-features: nesting=1
-hostname: dnsct
-memory: 512
-net0: name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:11,ip=dhcp,type=veth
-net2: name=eth0,bridge=data,firewall=0,hwaddr=BC:24:11:47:83:12,ip=123.123.123.123/24,type=veth
-net5: name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:13,ip6=fd80::1/64,type=veth
-ostype: alpine
-rootfs: local-lvm:vm-10001-disk-0,size=1G
-swap: 512
-unprivileged: 1"
- .as_bytes();
-
- network_config = NetworkConfig::parse(guest_config).expect("valid network configuration");
-
- assert_eq!(network_config.network_devices().len(), 3);
-
- assert_eq!(
- network_config.network_devices()[&0],
- NetworkDevice {
- model: NetworkDeviceModel::Veth,
- mac_address: MacAddress([0xBC, 0x24, 0x11, 0x47, 0x83, 0x11]),
- firewall: true,
- ip: None,
- ip6: None,
- }
- );
-
- assert_eq!(
- network_config.network_devices()[&2],
- NetworkDevice {
- model: NetworkDeviceModel::Veth,
- mac_address: MacAddress([0xBC, 0x24, 0x11, 0x47, 0x83, 0x12]),
- firewall: false,
- ip: Some(Ipv4Cidr::from_str("123.123.123.123/24").expect("valid ipv4")),
- ip6: None,
- }
- );
-
- assert_eq!(
- network_config.network_devices()[&5],
- NetworkDevice {
- model: NetworkDeviceModel::Veth,
- mac_address: MacAddress([0xBC, 0x24, 0x11, 0x47, 0x83, 0x13]),
- firewall: true,
- ip: None,
- ip6: Some(Ipv6Cidr::from_str("fd80::1/64").expect("valid ipv6")),
- }
- );
-
- NetworkConfig::parse(
- "netqwe: name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:11,ip=dhcp,type=veth"
- .as_bytes(),
- )
- .expect_err("invalid net key");
-
- NetworkConfig::parse(
- "net0 name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:11,ip=dhcp,type=veth"
- .as_bytes(),
- )
- .expect_err("invalid net key");
-
- NetworkConfig::parse(
- "net33: name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:11,ip=dhcp,type=veth"
- .as_bytes(),
- )
- .expect_err("invalid net key");
- }
-}
diff --git a/proxmox-ve-config/src/host/mod.rs b/proxmox-ve-config/src/host/mod.rs
deleted file mode 100644
index b5614dd..0000000
--- a/proxmox-ve-config/src/host/mod.rs
+++ /dev/null
@@ -1 +0,0 @@
-pub mod utils;
diff --git a/proxmox-ve-config/src/host/utils.rs b/proxmox-ve-config/src/host/utils.rs
deleted file mode 100644
index b1dc8e9..0000000
--- a/proxmox-ve-config/src/host/utils.rs
+++ /dev/null
@@ -1,70 +0,0 @@
-use std::net::{IpAddr, ToSocketAddrs};
-
-use crate::firewall::types::Cidr;
-
-use nix::sys::socket::{AddressFamily, SockaddrLike};
-use proxmox_sys::nodename;
-
-/// gets a list of IPs that the hostname of this node resolves to
-///
-/// panics if the local hostname is not resolvable
-pub fn host_ips() -> Vec<IpAddr> {
- let hostname = nodename();
-
- log::trace!("resolving hostname");
-
- format!("{hostname}:0")
- .to_socket_addrs()
- .expect("local hostname is resolvable")
- .map(|addr| addr.ip())
- .collect()
-}
-
-/// gets a list of all configured CIDRs on all network interfaces of this host
-///
-/// panics if unable to query the current network configuration
-pub fn network_interface_cidrs() -> Vec<Cidr> {
- use nix::ifaddrs::getifaddrs;
-
- log::trace!("reading networking interface list");
-
- let mut cidrs = Vec::new();
-
- let interfaces = getifaddrs().expect("should be able to query network interfaces");
-
- for interface in interfaces {
- if let (Some(address), Some(netmask)) = (interface.address, interface.netmask) {
- match (address.family(), netmask.family()) {
- (Some(AddressFamily::Inet), Some(AddressFamily::Inet)) => {
- let address = address.as_sockaddr_in().expect("is an IPv4 address").ip();
-
- let netmask = netmask
- .as_sockaddr_in()
- .expect("is an IPv4 address")
- .ip()
- .count_ones()
- .try_into()
- .expect("count_ones of u32 is < u8_max");
-
- cidrs.push(Cidr::new_v4(address, netmask).expect("netmask is valid"));
- }
- (Some(AddressFamily::Inet6), Some(AddressFamily::Inet6)) => {
- let address = address.as_sockaddr_in6().expect("is an IPv6 address").ip();
-
- let netmask_address =
- netmask.as_sockaddr_in6().expect("is an IPv6 address").ip();
-
- let netmask = u128::from_be_bytes(netmask_address.octets())
- .count_ones()
- .try_into()
- .expect("count_ones of u128 is < u8_max");
-
- cidrs.push(Cidr::new_v6(address, netmask).expect("netmask is valid"));
- }
- _ => continue,
- }
- }
- }
-
- cidrs
-}
diff --git a/proxmox-ve-config/src/lib.rs b/proxmox-ve-config/src/lib.rs
deleted file mode 100644
index 856b14f..0000000
--- a/proxmox-ve-config/src/lib.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-pub mod firewall;
-pub mod guest;
-pub mod host;
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 24+ messages in thread
* [pve-devel] [PATCH proxmox-firewall v4 2/9] config: tests: add support for loading sdn and ipam config
2024-11-15 12:09 [pve-devel] [PATCH docs/firewall/manager/proxmox{-firewall, -perl-rs} v4 0/9] autogenerate ipsets for sdn objects Stefan Hanreich
2024-11-15 12:09 ` [pve-devel] [PATCH proxmox-firewall v4 1/9] add proxmox-ve-rs crate - move proxmox-ve-config there Stefan Hanreich
@ 2024-11-15 12:09 ` Stefan Hanreich
2024-11-17 14:08 ` [pve-devel] applied: " Thomas Lamprecht
2024-11-15 12:09 ` [pve-devel] [PATCH proxmox-firewall v4 3/9] ipsets: autogenerate ipsets for vnets and ipam Stefan Hanreich
` (7 subsequent siblings)
9 siblings, 1 reply; 24+ messages in thread
From: Stefan Hanreich @ 2024-11-15 12:09 UTC (permalink / raw)
To: pve-devel; +Cc: Wolfgang Bumiller
Also add example SDN configuration files that get automatically
loaded, which can be used for future tests.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Reviewed-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Tested-by: Gabriel Goller <g.goller@proxmox.com>
Tested-by: Hannes Dürr <h.duerr@proxmox.com>
---
proxmox-firewall/src/config.rs | 69 +++++++++++++++++++
.../tests/input/.running-config.json | 45 ++++++++++++
proxmox-firewall/tests/input/ipam.db | 32 +++++++++
proxmox-firewall/tests/integration_tests.rs | 10 +++
proxmox-nftables/src/types.rs | 2 +-
5 files changed, 157 insertions(+), 1 deletion(-)
create mode 100644 proxmox-firewall/tests/input/.running-config.json
create mode 100644 proxmox-firewall/tests/input/ipam.db
diff --git a/proxmox-firewall/src/config.rs b/proxmox-firewall/src/config.rs
index 5bd2512..c27aac6 100644
--- a/proxmox-firewall/src/config.rs
+++ b/proxmox-firewall/src/config.rs
@@ -16,6 +16,10 @@ use proxmox_ve_config::guest::{GuestEntry, GuestMap};
use proxmox_nftables::command::{CommandOutput, Commands, List, ListOutput};
use proxmox_nftables::types::ListChain;
use proxmox_nftables::NftClient;
+use proxmox_ve_config::sdn::{
+ config::{RunningConfig, SdnConfig},
+ ipam::{Ipam, IpamJson},
+};
pub trait FirewallConfigLoader {
fn cluster(&self) -> Result<Option<Box<dyn io::BufRead>>, Error>;
@@ -27,6 +31,8 @@ pub trait FirewallConfigLoader {
guest: &GuestEntry,
) -> Result<Option<Box<dyn io::BufRead>>, Error>;
fn guest_firewall_config(&self, vmid: &Vmid) -> Result<Option<Box<dyn io::BufRead>>, Error>;
+ fn sdn_running_config(&self) -> Result<Option<Box<dyn io::BufRead>>, Error>;
+ fn ipam(&self) -> Result<Option<Box<dyn io::BufRead>>, Error>;
}
#[derive(Default)]
@@ -58,6 +64,9 @@ fn open_config_file(path: &str) -> Result<Option<File>, Error> {
const CLUSTER_CONFIG_PATH: &str = "/etc/pve/firewall/cluster.fw";
const HOST_CONFIG_PATH: &str = "/etc/pve/local/host.fw";
+const SDN_RUNNING_CONFIG_PATH: &str = "/etc/pve/sdn/.running-config";
+const SDN_IPAM_PATH: &str = "/etc/pve/priv/ipam.db";
+
impl FirewallConfigLoader for PveFirewallConfigLoader {
fn cluster(&self) -> Result<Option<Box<dyn io::BufRead>>, Error> {
log::info!("loading cluster config");
@@ -119,6 +128,32 @@ impl FirewallConfigLoader for PveFirewallConfigLoader {
Ok(None)
}
+
+ fn sdn_running_config(&self) -> Result<Option<Box<dyn io::BufRead>>, Error> {
+ log::info!("loading SDN running-config");
+
+ let fd = open_config_file(SDN_RUNNING_CONFIG_PATH)?;
+
+ if let Some(file) = fd {
+ let buf_reader = Box::new(BufReader::new(file)) as Box<dyn io::BufRead>;
+ return Ok(Some(buf_reader));
+ }
+
+ Ok(None)
+ }
+
+ fn ipam(&self) -> Result<Option<Box<dyn io::BufRead>>, Error> {
+ log::info!("loading IPAM config");
+
+ let fd = open_config_file(SDN_IPAM_PATH)?;
+
+ if let Some(file) = fd {
+ let buf_reader = Box::new(BufReader::new(file)) as Box<dyn io::BufRead>;
+ return Ok(Some(buf_reader));
+ }
+
+ Ok(None)
+ }
}
pub trait NftConfigLoader {
@@ -150,6 +185,8 @@ pub struct FirewallConfig {
host_config: HostConfig,
guest_config: BTreeMap<Vmid, GuestConfig>,
nft_config: BTreeMap<String, ListChain>,
+ sdn_config: Option<SdnConfig>,
+ ipam_config: Option<Ipam>,
}
impl FirewallConfig {
@@ -207,6 +244,28 @@ impl FirewallConfig {
Ok(guests)
}
+ pub fn parse_sdn(
+ firewall_loader: &dyn FirewallConfigLoader,
+ ) -> Result<Option<SdnConfig>, Error> {
+ Ok(match firewall_loader.sdn_running_config()? {
+ Some(data) => {
+ let running_config: RunningConfig = serde_json::from_reader(data)?;
+ Some(SdnConfig::try_from(running_config)?)
+ }
+ _ => None,
+ })
+ }
+
+ pub fn parse_ipam(firewall_loader: &dyn FirewallConfigLoader) -> Result<Option<Ipam>, Error> {
+ Ok(match firewall_loader.ipam()? {
+ Some(data) => {
+ let raw_ipam: IpamJson = serde_json::from_reader(data)?;
+ Some(Ipam::try_from(raw_ipam)?)
+ }
+ _ => None,
+ })
+ }
+
pub fn parse_nft(
nft_loader: &dyn NftConfigLoader,
) -> Result<BTreeMap<String, ListChain>, Error> {
@@ -233,6 +292,8 @@ impl FirewallConfig {
cluster_config: Self::parse_cluster(firewall_loader)?,
host_config: Self::parse_host(firewall_loader)?,
guest_config: Self::parse_guests(firewall_loader)?,
+ sdn_config: Self::parse_sdn(firewall_loader)?,
+ ipam_config: Self::parse_ipam(firewall_loader)?,
nft_config: Self::parse_nft(nft_loader)?,
})
}
@@ -253,6 +314,14 @@ impl FirewallConfig {
&self.nft_config
}
+ pub fn sdn(&self) -> Option<&SdnConfig> {
+ self.sdn_config.as_ref()
+ }
+
+ pub fn ipam(&self) -> Option<&Ipam> {
+ self.ipam_config.as_ref()
+ }
+
pub fn is_enabled(&self) -> bool {
self.cluster().is_enabled() && self.host().nftables()
}
diff --git a/proxmox-firewall/tests/input/.running-config.json b/proxmox-firewall/tests/input/.running-config.json
new file mode 100644
index 0000000..a4511f0
--- /dev/null
+++ b/proxmox-firewall/tests/input/.running-config.json
@@ -0,0 +1,45 @@
+{
+ "subnets": {
+ "ids": {
+ "test-10.101.0.0-16": {
+ "gateway": "10.101.1.1",
+ "snat": 1,
+ "vnet": "public",
+ "dhcp-range": [
+ "start-address=10.101.99.100,end-address=10.101.99.200"
+ ],
+ "type": "subnet"
+ },
+ "test-fd80::-64": {
+ "snat": 1,
+ "gateway": "fd80::1",
+ "dhcp-range": [
+ "start-address=fd80::1000,end-address=fd80::ffff"
+ ],
+ "vnet": "public",
+ "type": "subnet"
+ }
+ }
+ },
+ "version": 49,
+ "vnets": {
+ "ids": {
+ "public": {
+ "zone": "test",
+ "type": "vnet"
+ }
+ }
+ },
+ "zones": {
+ "ids": {
+ "test": {
+ "dhcp": "dnsmasq",
+ "ipam": "pve",
+ "type": "simple"
+ }
+ }
+ },
+ "controllers": {
+ "ids": {}
+ }
+}
diff --git a/proxmox-firewall/tests/input/ipam.db b/proxmox-firewall/tests/input/ipam.db
new file mode 100644
index 0000000..ac2901e
--- /dev/null
+++ b/proxmox-firewall/tests/input/ipam.db
@@ -0,0 +1,32 @@
+{
+ "zones": {
+ "public": {
+ "subnets": {
+ "10.101.0.0/16": {
+ "ips": {
+ "10.101.1.1": {
+ "gateway": 1
+ },
+ "10.101.1.100": {
+ "vmid": "101",
+ "mac": "BC:24:11:11:22:33",
+ "hostname": null
+ }
+ }
+ },
+ "fd80::/64": {
+ "ips": {
+ "fd80::1": {
+ "gateway": 1
+ },
+ "fd80::1000": {
+ "mac": "BC:24:11:11:22:33",
+ "vmid": "101",
+ "hostname": "test-vm"
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/proxmox-firewall/tests/integration_tests.rs b/proxmox-firewall/tests/integration_tests.rs
index e9baffe..5de1a4e 100644
--- a/proxmox-firewall/tests/integration_tests.rs
+++ b/proxmox-firewall/tests/integration_tests.rs
@@ -69,6 +69,16 @@ impl FirewallConfigLoader for MockFirewallConfigLoader {
Ok(None)
}
+
+ fn sdn_running_config(&self) -> Result<Option<Box<dyn std::io::BufRead>>, Error> {
+ Ok(Some(Box::new(
+ include_str!("input/.running-config.json").as_bytes(),
+ )))
+ }
+
+ fn ipam(&self) -> Result<Option<Box<dyn std::io::BufRead>>, Error> {
+ Ok(Some(Box::new(include_str!("input/ipam.db").as_bytes())))
+ }
}
struct MockNftConfigLoader {}
diff --git a/proxmox-nftables/src/types.rs b/proxmox-nftables/src/types.rs
index a83e958..3101436 100644
--- a/proxmox-nftables/src/types.rs
+++ b/proxmox-nftables/src/types.rs
@@ -636,7 +636,7 @@ impl SetName {
};
let name = match name.scope() {
- IpsetScope::Datacenter => name.to_string(),
+ IpsetScope::Datacenter | IpsetScope::Sdn => name.to_string(),
IpsetScope::Guest => {
if let Some(vmid) = vmid {
format!("guest-{vmid}/{}", name.name())
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 24+ messages in thread
* [pve-devel] [PATCH proxmox-firewall v4 3/9] ipsets: autogenerate ipsets for vnets and ipam
2024-11-15 12:09 [pve-devel] [PATCH docs/firewall/manager/proxmox{-firewall, -perl-rs} v4 0/9] autogenerate ipsets for sdn objects Stefan Hanreich
2024-11-15 12:09 ` [pve-devel] [PATCH proxmox-firewall v4 1/9] add proxmox-ve-rs crate - move proxmox-ve-config there Stefan Hanreich
2024-11-15 12:09 ` [pve-devel] [PATCH proxmox-firewall v4 2/9] config: tests: add support for loading sdn and ipam config Stefan Hanreich
@ 2024-11-15 12:09 ` Stefan Hanreich
2024-11-17 14:08 ` [pve-devel] applied: " Thomas Lamprecht
2024-11-15 12:09 ` [pve-devel] [PATCH pve-firewall v4 4/9] add support for loading sdn firewall configuration Stefan Hanreich
` (6 subsequent siblings)
9 siblings, 1 reply; 24+ messages in thread
From: Stefan Hanreich @ 2024-11-15 12:09 UTC (permalink / raw)
To: pve-devel; +Cc: Wolfgang Bumiller
They act like virtual ipsets, similar to ipfilter-net, that can be
used for defining firewall rules for sdn objects dynamically.
The changes in proxmox-ve-config also introduced a dedicated struct
for representing ip ranges, so we update the existing code, so that it
uses that struct as well.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Reviewed-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Tested-by: Gabriel Goller <g.goller@proxmox.com>
Tested-by: Hannes Dürr <h.duerr@proxmox.com>
---
proxmox-firewall/src/firewall.rs | 22 +-
proxmox-firewall/src/object.rs | 41 +-
.../integration_tests__firewall.snap | 1288 +++++++++++++++++
proxmox-nftables/src/expression.rs | 17 +-
4 files changed, 1354 insertions(+), 14 deletions(-)
diff --git a/proxmox-firewall/src/firewall.rs b/proxmox-firewall/src/firewall.rs
index 941aa20..347f3af 100644
--- a/proxmox-firewall/src/firewall.rs
+++ b/proxmox-firewall/src/firewall.rs
@@ -197,6 +197,27 @@ impl Firewall {
self.reset_firewall(&mut commands);
let cluster_host_table = Self::cluster_table();
+ let guest_table = Self::guest_table();
+
+ if let Some(sdn_config) = self.config.sdn() {
+ let ipsets = sdn_config
+ .ipsets(None)
+ .map(|ipset| (ipset.name().to_string(), ipset))
+ .collect();
+
+ self.create_ipsets(&mut commands, &ipsets, &cluster_host_table, None)?;
+ self.create_ipsets(&mut commands, &ipsets, &guest_table, None)?;
+ }
+
+ if let Some(ipam_config) = self.config.ipam() {
+ let ipsets = ipam_config
+ .ipsets(None)
+ .map(|ipset| (ipset.name().to_string(), ipset))
+ .collect();
+
+ self.create_ipsets(&mut commands, &ipsets, &cluster_host_table, None)?;
+ self.create_ipsets(&mut commands, &ipsets, &guest_table, None)?;
+ }
if self.config.host().is_enabled() {
log::info!("creating cluster / host configuration");
@@ -242,7 +263,6 @@ impl Firewall {
commands.push(Delete::table(TableName::from(Self::cluster_table())));
}
- let guest_table = Self::guest_table();
let enabled_guests: BTreeMap<&Vmid, &GuestConfig> = self
.config
.guests()
diff --git a/proxmox-firewall/src/object.rs b/proxmox-firewall/src/object.rs
index 32c4ddb..cf7e773 100644
--- a/proxmox-firewall/src/object.rs
+++ b/proxmox-firewall/src/object.rs
@@ -72,20 +72,37 @@ impl ToNftObjects for Ipset {
let mut nomatch_elements = Vec::new();
for element in self.iter() {
- let cidr = match &element.address {
- IpsetAddress::Cidr(cidr) => cidr,
- IpsetAddress::Alias(alias) => env
- .alias(alias)
- .ok_or(format_err!("could not find alias {alias} in environment"))?
- .address(),
+ let expression = match &element.address {
+ IpsetAddress::Range(range) => {
+ if family != range.family() {
+ continue;
+ }
+
+ Expression::from(range)
+ }
+ IpsetAddress::Cidr(cidr) => {
+ if family != cidr.family() {
+ continue;
+ }
+
+ Expression::from(Prefix::from(cidr))
+ }
+ IpsetAddress::Alias(alias) => {
+ let cidr = env
+ .alias(alias)
+ .ok_or_else(|| {
+ format_err!("could not find alias {alias} in environment")
+ })?
+ .address();
+
+ if family != cidr.family() {
+ continue;
+ }
+
+ Expression::from(Prefix::from(cidr))
+ }
};
- if family != cidr.family() {
- continue;
- }
-
- let expression = Expression::from(Prefix::from(cidr));
-
if element.nomatch {
nomatch_elements.push(expression);
} else {
diff --git a/proxmox-firewall/tests/snapshots/integration_tests__firewall.snap b/proxmox-firewall/tests/snapshots/integration_tests__firewall.snap
index 40d4405..e1b599c 100644
--- a/proxmox-firewall/tests/snapshots/integration_tests__firewall.snap
+++ b/proxmox-firewall/tests/snapshots/integration_tests__firewall.snap
@@ -202,6 +202,1294 @@ expression: "firewall.full_host_fw().expect(\"firewall can be generated\")"
}
}
},
+ {
+ "add": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v4-sdn/public-all",
+ "type": "ipv4_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v4-sdn/public-all"
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v4-sdn/public-all-nomatch",
+ "type": "ipv4_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v4-sdn/public-all-nomatch"
+ }
+ }
+ },
+ {
+ "add": {
+ "element": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v4-sdn/public-all",
+ "elem": [
+ {
+ "prefix": {
+ "addr": "10.101.0.0",
+ "len": 16
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v6-sdn/public-all",
+ "type": "ipv6_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v6-sdn/public-all"
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v6-sdn/public-all-nomatch",
+ "type": "ipv6_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v6-sdn/public-all-nomatch"
+ }
+ }
+ },
+ {
+ "add": {
+ "element": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v6-sdn/public-all",
+ "elem": [
+ {
+ "prefix": {
+ "addr": "fd80::",
+ "len": 64
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v4-sdn/public-dhcp",
+ "type": "ipv4_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v4-sdn/public-dhcp"
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v4-sdn/public-dhcp-nomatch",
+ "type": "ipv4_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v4-sdn/public-dhcp-nomatch"
+ }
+ }
+ },
+ {
+ "add": {
+ "element": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v4-sdn/public-dhcp",
+ "elem": [
+ {
+ "range": [
+ "10.101.99.100",
+ "10.101.99.200"
+ ]
+ }
+ ]
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v6-sdn/public-dhcp",
+ "type": "ipv6_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v6-sdn/public-dhcp"
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v6-sdn/public-dhcp-nomatch",
+ "type": "ipv6_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v6-sdn/public-dhcp-nomatch"
+ }
+ }
+ },
+ {
+ "add": {
+ "element": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v6-sdn/public-dhcp",
+ "elem": [
+ {
+ "range": [
+ "fd80::1000",
+ "fd80::ffff"
+ ]
+ }
+ ]
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v4-sdn/public-gateway",
+ "type": "ipv4_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v4-sdn/public-gateway"
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v4-sdn/public-gateway-nomatch",
+ "type": "ipv4_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v4-sdn/public-gateway-nomatch"
+ }
+ }
+ },
+ {
+ "add": {
+ "element": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v4-sdn/public-gateway",
+ "elem": [
+ {
+ "prefix": {
+ "addr": "10.101.1.1",
+ "len": 32
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v6-sdn/public-gateway",
+ "type": "ipv6_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v6-sdn/public-gateway"
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v6-sdn/public-gateway-nomatch",
+ "type": "ipv6_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v6-sdn/public-gateway-nomatch"
+ }
+ }
+ },
+ {
+ "add": {
+ "element": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v6-sdn/public-gateway",
+ "elem": [
+ {
+ "prefix": {
+ "addr": "fd80::1",
+ "len": 128
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v4-sdn/public-no-gateway",
+ "type": "ipv4_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v4-sdn/public-no-gateway"
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v4-sdn/public-no-gateway-nomatch",
+ "type": "ipv4_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v4-sdn/public-no-gateway-nomatch"
+ }
+ }
+ },
+ {
+ "add": {
+ "element": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v4-sdn/public-no-gateway",
+ "elem": [
+ {
+ "prefix": {
+ "addr": "10.101.0.0",
+ "len": 16
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "add": {
+ "element": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v4-sdn/public-no-gateway-nomatch",
+ "elem": [
+ {
+ "prefix": {
+ "addr": "10.101.1.1",
+ "len": 32
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v6-sdn/public-no-gateway",
+ "type": "ipv6_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v6-sdn/public-no-gateway"
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v6-sdn/public-no-gateway-nomatch",
+ "type": "ipv6_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v6-sdn/public-no-gateway-nomatch"
+ }
+ }
+ },
+ {
+ "add": {
+ "element": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v6-sdn/public-no-gateway",
+ "elem": [
+ {
+ "prefix": {
+ "addr": "fd80::",
+ "len": 64
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "add": {
+ "element": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v6-sdn/public-no-gateway-nomatch",
+ "elem": [
+ {
+ "prefix": {
+ "addr": "fd80::1",
+ "len": 128
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v4-sdn/public-all",
+ "type": "ipv4_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v4-sdn/public-all"
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v4-sdn/public-all-nomatch",
+ "type": "ipv4_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v4-sdn/public-all-nomatch"
+ }
+ }
+ },
+ {
+ "add": {
+ "element": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v4-sdn/public-all",
+ "elem": [
+ {
+ "prefix": {
+ "addr": "10.101.0.0",
+ "len": 16
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v6-sdn/public-all",
+ "type": "ipv6_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v6-sdn/public-all"
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v6-sdn/public-all-nomatch",
+ "type": "ipv6_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v6-sdn/public-all-nomatch"
+ }
+ }
+ },
+ {
+ "add": {
+ "element": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v6-sdn/public-all",
+ "elem": [
+ {
+ "prefix": {
+ "addr": "fd80::",
+ "len": 64
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v4-sdn/public-dhcp",
+ "type": "ipv4_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v4-sdn/public-dhcp"
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v4-sdn/public-dhcp-nomatch",
+ "type": "ipv4_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v4-sdn/public-dhcp-nomatch"
+ }
+ }
+ },
+ {
+ "add": {
+ "element": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v4-sdn/public-dhcp",
+ "elem": [
+ {
+ "range": [
+ "10.101.99.100",
+ "10.101.99.200"
+ ]
+ }
+ ]
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v6-sdn/public-dhcp",
+ "type": "ipv6_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v6-sdn/public-dhcp"
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v6-sdn/public-dhcp-nomatch",
+ "type": "ipv6_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v6-sdn/public-dhcp-nomatch"
+ }
+ }
+ },
+ {
+ "add": {
+ "element": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v6-sdn/public-dhcp",
+ "elem": [
+ {
+ "range": [
+ "fd80::1000",
+ "fd80::ffff"
+ ]
+ }
+ ]
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v4-sdn/public-gateway",
+ "type": "ipv4_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v4-sdn/public-gateway"
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v4-sdn/public-gateway-nomatch",
+ "type": "ipv4_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v4-sdn/public-gateway-nomatch"
+ }
+ }
+ },
+ {
+ "add": {
+ "element": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v4-sdn/public-gateway",
+ "elem": [
+ {
+ "prefix": {
+ "addr": "10.101.1.1",
+ "len": 32
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v6-sdn/public-gateway",
+ "type": "ipv6_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v6-sdn/public-gateway"
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v6-sdn/public-gateway-nomatch",
+ "type": "ipv6_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v6-sdn/public-gateway-nomatch"
+ }
+ }
+ },
+ {
+ "add": {
+ "element": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v6-sdn/public-gateway",
+ "elem": [
+ {
+ "prefix": {
+ "addr": "fd80::1",
+ "len": 128
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v4-sdn/public-no-gateway",
+ "type": "ipv4_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v4-sdn/public-no-gateway"
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v4-sdn/public-no-gateway-nomatch",
+ "type": "ipv4_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v4-sdn/public-no-gateway-nomatch"
+ }
+ }
+ },
+ {
+ "add": {
+ "element": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v4-sdn/public-no-gateway",
+ "elem": [
+ {
+ "prefix": {
+ "addr": "10.101.0.0",
+ "len": 16
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "add": {
+ "element": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v4-sdn/public-no-gateway-nomatch",
+ "elem": [
+ {
+ "prefix": {
+ "addr": "10.101.1.1",
+ "len": 32
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v6-sdn/public-no-gateway",
+ "type": "ipv6_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v6-sdn/public-no-gateway"
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v6-sdn/public-no-gateway-nomatch",
+ "type": "ipv6_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v6-sdn/public-no-gateway-nomatch"
+ }
+ }
+ },
+ {
+ "add": {
+ "element": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v6-sdn/public-no-gateway",
+ "elem": [
+ {
+ "prefix": {
+ "addr": "fd80::",
+ "len": 64
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "add": {
+ "element": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v6-sdn/public-no-gateway-nomatch",
+ "elem": [
+ {
+ "prefix": {
+ "addr": "fd80::1",
+ "len": 128
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v4-sdn/guest-ipam-101",
+ "type": "ipv4_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v4-sdn/guest-ipam-101"
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v4-sdn/guest-ipam-101-nomatch",
+ "type": "ipv4_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v4-sdn/guest-ipam-101-nomatch"
+ }
+ }
+ },
+ {
+ "add": {
+ "element": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v4-sdn/guest-ipam-101",
+ "elem": [
+ {
+ "prefix": {
+ "addr": "10.101.1.100",
+ "len": 32
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v6-sdn/guest-ipam-101",
+ "type": "ipv6_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v6-sdn/guest-ipam-101"
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v6-sdn/guest-ipam-101-nomatch",
+ "type": "ipv6_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v6-sdn/guest-ipam-101-nomatch"
+ }
+ }
+ },
+ {
+ "add": {
+ "element": {
+ "family": "inet",
+ "table": "proxmox-firewall",
+ "name": "v6-sdn/guest-ipam-101",
+ "elem": [
+ {
+ "prefix": {
+ "addr": "fd80::1000",
+ "len": 128
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v4-sdn/guest-ipam-101",
+ "type": "ipv4_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v4-sdn/guest-ipam-101"
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v4-sdn/guest-ipam-101-nomatch",
+ "type": "ipv4_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v4-sdn/guest-ipam-101-nomatch"
+ }
+ }
+ },
+ {
+ "add": {
+ "element": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v4-sdn/guest-ipam-101",
+ "elem": [
+ {
+ "prefix": {
+ "addr": "10.101.1.100",
+ "len": 32
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v6-sdn/guest-ipam-101",
+ "type": "ipv6_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v6-sdn/guest-ipam-101"
+ }
+ }
+ },
+ {
+ "add": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v6-sdn/guest-ipam-101-nomatch",
+ "type": "ipv6_addr",
+ "flags": [
+ "interval"
+ ]
+ }
+ }
+ },
+ {
+ "flush": {
+ "set": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v6-sdn/guest-ipam-101-nomatch"
+ }
+ }
+ },
+ {
+ "add": {
+ "element": {
+ "family": "bridge",
+ "table": "proxmox-firewall-guests",
+ "name": "v6-sdn/guest-ipam-101",
+ "elem": [
+ {
+ "prefix": {
+ "addr": "fd80::1000",
+ "len": 128
+ }
+ }
+ ]
+ }
+ }
+ },
{
"add": {
"set": {
diff --git a/proxmox-nftables/src/expression.rs b/proxmox-nftables/src/expression.rs
index 18b92d4..e56a15c 100644
--- a/proxmox-nftables/src/expression.rs
+++ b/proxmox-nftables/src/expression.rs
@@ -1,4 +1,5 @@
use crate::types::{ElemConfig, Verdict};
+use proxmox_ve_config::firewall::types::address::IpRange;
use serde::{Deserialize, Serialize};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
@@ -50,6 +51,10 @@ pub enum Expression {
}
impl Expression {
+ pub fn range(start: impl Into<Expression>, last: impl Into<Expression>) -> Self {
+ Expression::Range(Box::new((start.into(), last.into())))
+ }
+
pub fn set(expressions: impl IntoIterator<Item = Expression>) -> Self {
Expression::Set(Vec::from_iter(expressions))
}
@@ -169,12 +174,22 @@ impl From<&IpList> for Expression {
}
}
+#[cfg(feature = "config-ext")]
+impl From<&IpRange> for Expression {
+ fn from(value: &IpRange) -> Self {
+ match value {
+ IpRange::V4(range) => Expression::range(range.start(), range.last()),
+ IpRange::V6(range) => Expression::range(range.start(), range.last()),
+ }
+ }
+}
+
#[cfg(feature = "config-ext")]
impl From<&IpEntry> for Expression {
fn from(value: &IpEntry) -> Self {
match value {
IpEntry::Cidr(cidr) => Expression::from(Prefix::from(cidr)),
- IpEntry::Range(beg, end) => Expression::Range(Box::new((beg.into(), end.into()))),
+ IpEntry::Range(range) => Expression::from(range),
}
}
}
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 24+ messages in thread
* [pve-devel] [PATCH pve-firewall v4 4/9] add support for loading sdn firewall configuration
2024-11-15 12:09 [pve-devel] [PATCH docs/firewall/manager/proxmox{-firewall, -perl-rs} v4 0/9] autogenerate ipsets for sdn objects Stefan Hanreich
` (2 preceding siblings ...)
2024-11-15 12:09 ` [pve-devel] [PATCH proxmox-firewall v4 3/9] ipsets: autogenerate ipsets for vnets and ipam Stefan Hanreich
@ 2024-11-15 12:09 ` Stefan Hanreich
2024-11-17 14:57 ` Thomas Lamprecht
2024-11-15 12:09 ` [pve-devel] [PATCH pve-firewall v4 5/9] nftables: make is_nftables check flag file instead of config Stefan Hanreich
` (5 subsequent siblings)
9 siblings, 1 reply; 24+ messages in thread
From: Stefan Hanreich @ 2024-11-15 12:09 UTC (permalink / raw)
To: pve-devel; +Cc: Wolfgang Bumiller
This also includes support for parsing rules referencing IPSets in the
new SDN scope and generating those IPSets in the firewall.
Loading SDN configuration is optional, since loading it requires root
privileges which we do not have in all call sites. Adding the flag
allows us to selectively load the SDN configuration only where
required and at the same time allows us to only elevate privileges in
the API where absolutely needed.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Reviewed-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Tested-By: Gabriel Goller <g.goller@proxmox.com>
Tested-By: Hannes Dürr <h.duerr@proxmox.com>
---
src/PVE/Firewall.pm | 62 +++++++++++++++++++++++++++++----
src/PVE/Service/pve_firewall.pm | 4 +--
2 files changed, 57 insertions(+), 9 deletions(-)
diff --git a/src/PVE/Firewall.pm b/src/PVE/Firewall.pm
index 09544ba..7642bf6 100644
--- a/src/PVE/Firewall.pm
+++ b/src/PVE/Firewall.pm
@@ -25,6 +25,7 @@ use PVE::Tools qw($IPV4RE $IPV6RE);
use PVE::Tools qw(run_command lock_file dir_glob_foreach);
use PVE::Firewall::Helpers;
+use PVE::RS::Firewall::SDN;
my $pvefw_conf_dir = "/etc/pve/firewall";
my $clusterfw_conf_filename = "$pvefw_conf_dir/cluster.fw";
@@ -1689,9 +1690,12 @@ sub verify_rule {
if (my $value = $rule->{$name}) {
if ($value =~ m/^\+/) {
- if ($value =~ m@^\+(guest/|dc/)?(${ipset_name_pattern})$@) {
+ if ($value =~ m@^\+(guest/|dc/|sdn/)?(${ipset_name_pattern})$@) {
&$add_error($name, "no such ipset '$2'")
- if !($cluster_conf->{ipset}->{$2} || ($fw_conf && $fw_conf->{ipset}->{$2}));
+ if !($cluster_conf->{ipset}->{$2}
+ || ($fw_conf && $fw_conf->{ipset}->{$2})
+ || ($cluster_conf->{sdn} && $cluster_conf->{sdn}->{ipset}->{$2})
+ || ($fw_conf->{sdn} && $fw_conf->{sdn}->{ipset}->{$2}));
} else {
&$add_error($name, "invalid ipset name '$value'");
@@ -2108,13 +2112,15 @@ sub ipt_gen_src_or_dst_match {
my $match;
if ($adr =~ m/^\+/) {
- if ($adr =~ m@^\+(guest/|dc/)?(${ipset_name_pattern})$@) {
+ if ($adr =~ m@^\+(guest/|dc/|sdn/)?(${ipset_name_pattern})$@) {
my $scope = $1 // "";
my $name = $2;
my $ipset_chain;
- if ($scope ne 'dc/' && $fw_conf && $fw_conf->{ipset}->{$name}) {
+ if ((!$scope || $scope eq 'guest/') && $fw_conf && $fw_conf->{ipset}->{$name}) {
$ipset_chain = compute_ipset_chain_name($fw_conf->{vmid}, $name, $ipversion);
- } elsif ($scope ne 'guest/' && $cluster_conf && $cluster_conf->{ipset}->{$name}) {
+ } elsif ((!$scope || $scope eq 'dc/') && $cluster_conf && $cluster_conf->{ipset}->{$name}) {
+ $ipset_chain = compute_ipset_chain_name(0, $name, $ipversion);
+ } elsif ((!$scope || $scope eq 'sdn/') && $cluster_conf->{sdn} && $cluster_conf->{sdn}->{ipset}->{$name}) {
$ipset_chain = compute_ipset_chain_name(0, $name, $ipversion);
} else {
die "no such ipset '$name'\n";
@@ -3644,7 +3650,8 @@ sub lock_clusterfw_conf {
}
sub load_clusterfw_conf {
- my ($filename) = @_;
+ my ($filename, $options) = @_;
+
$filename = $clusterfw_conf_filename if !defined($filename);
my $empty_conf = {
@@ -3655,6 +3662,7 @@ sub load_clusterfw_conf {
group_comments => {},
ipset => {} ,
ipset_comments => {},
+ sdn => load_sdn_conf(),
};
my $cluster_conf = generic_fw_config_parser($filename, $empty_conf, $empty_conf, 'cluster');
@@ -3663,6 +3671,45 @@ sub load_clusterfw_conf {
return $cluster_conf;
}
+sub load_sdn_conf {
+ my $rpcenv = eval { PVE::RPCEnvironment::get(); };
+
+ if ($@) {
+ warn "could not load SDN configuration because RPCEnvironment is not initialized.";
+ return {};
+ }
+
+ my $authuser = $rpcenv->get_user();
+
+ my $guests = PVE::Cluster::get_vmlist();
+ my $allowed_vms = [];
+ foreach my $vmid (sort keys %{$guests->{ids}}) {
+ next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ], 1);
+ push @$allowed_vms, $vmid;
+ }
+
+ my $vnets = PVE::Network::SDN::Vnets::config(1);
+ my $privs = [ 'SDN.Audit', 'SDN.Allocate' ];
+ my $allowed_vnets = [];
+ foreach my $vnet (sort keys %{$vnets->{ids}}) {
+ my $zone = $vnets->{ids}->{$vnet}->{zone};
+ next if !$rpcenv->check_any($authuser, "/sdn/zones/$zone/$vnet", $privs, 1);
+ push @$allowed_vnets, $vnet;
+ }
+
+ my $sdn_config = {
+ ipset => {} ,
+ ipset_comments => {},
+ };
+
+ eval {
+ $sdn_config = PVE::RS::Firewall::SDN::config($allowed_vnets, $allowed_vms);
+ };
+ warn $@ if $@;
+
+ return $sdn_config;
+}
+
sub save_clusterfw_conf {
my ($cluster_conf) = @_;
@@ -3768,7 +3815,7 @@ sub compile {
$vmfw_configs = read_vm_firewall_configs($cluster_conf, $vmdata, $testdir);
} else { # normal operation
- $cluster_conf = load_clusterfw_conf(undef) if !$cluster_conf;
+ $cluster_conf = load_clusterfw_conf() if !$cluster_conf;
$hostfw_conf = load_hostfw_conf($cluster_conf, undef) if !$hostfw_conf;
@@ -4043,6 +4090,7 @@ sub compile_ipsets {
}
generate_ipset_chains($ipset_ruleset, undef, $cluster_conf, undef, $cluster_conf->{ipset});
+ generate_ipset_chains($ipset_ruleset, undef, $cluster_conf, undef, $cluster_conf->{sdn}->{ipset});
return $ipset_ruleset;
}
diff --git a/src/PVE/Service/pve_firewall.pm b/src/PVE/Service/pve_firewall.pm
index 65cb2b8..02b507a 100755
--- a/src/PVE/Service/pve_firewall.pm
+++ b/src/PVE/Service/pve_firewall.pm
@@ -158,7 +158,7 @@ __PACKAGE__->register_method ({
PVE::Firewall::set_verbose(1); # show syntax errors
- my $cluster_conf = PVE::Firewall::load_clusterfw_conf(undef);
+ my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
$res->{enable} = $cluster_conf->{options}->{enable} ? 1 : 0;
if ($status eq 'running') {
@@ -202,7 +202,7 @@ __PACKAGE__->register_method ({
PVE::Firewall::set_verbose(1);
- my $cluster_conf = PVE::Firewall::load_clusterfw_conf(undef);
+ my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
my ($ruleset, $ipset_ruleset, $rulesetv6, $ebtables_ruleset) = PVE::Firewall::compile($cluster_conf, undef, undef);
print "ipset cmdlist:\n";
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 24+ messages in thread
* [pve-devel] [PATCH pve-firewall v4 5/9] nftables: make is_nftables check flag file instead of config
2024-11-15 12:09 [pve-devel] [PATCH docs/firewall/manager/proxmox{-firewall, -perl-rs} v4 0/9] autogenerate ipsets for sdn objects Stefan Hanreich
` (3 preceding siblings ...)
2024-11-15 12:09 ` [pve-devel] [PATCH pve-firewall v4 4/9] add support for loading sdn firewall configuration Stefan Hanreich
@ 2024-11-15 12:09 ` Stefan Hanreich
2024-11-17 14:58 ` [pve-devel] applied: " Thomas Lamprecht
2024-11-15 12:09 ` [pve-devel] [PATCH pve-firewall v4 6/9] api: load sdn ipsets Stefan Hanreich
` (4 subsequent siblings)
9 siblings, 1 reply; 24+ messages in thread
From: Stefan Hanreich @ 2024-11-15 12:09 UTC (permalink / raw)
To: pve-devel; +Cc: Wolfgang Bumiller
is_nftables is used in the VM and CT network startup scripts to
determine whether the nftables firewall is enabled or not. This causes
issues on container and VM startup when loading the SDN config, since
it requires the RPCEnvironment which is not initialized yet. Therefore
change this check to look for the existence of the flag file instead.
It also avoids parsing the entire cluster and host firewall
configuration on VM / CT startup, which means increased performance.
While we're at it, make all methods related to the configuration
parsing private, in order to avoid accidental usage of the expensive
methods.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Reviewed-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
src/PVE/Firewall.pm | 14 +++++++++-----
1 file changed, 9 insertions(+), 5 deletions(-)
diff --git a/src/PVE/Firewall.pm b/src/PVE/Firewall.pm
index 7642bf6..bfaa33a 100644
--- a/src/PVE/Firewall.pm
+++ b/src/PVE/Firewall.pm
@@ -4727,7 +4727,14 @@ sub remove_pvefw_chains_ebtables {
ebtables_restore_cmdlist(get_ebtables_cmdlist({}));
}
+# This is checked in proxmox-firewall to avoid log-spam due to failing to parse the config
+my $FORCE_NFT_DISABLE_FLAG_FILE = "/run/proxmox-nftables-firewall-force-disable";
+
sub is_nftables {
+ return !-e $FORCE_NFT_DISABLE_FLAG_FILE;
+}
+
+my sub get_nftables_option {
my ($cluster_conf, $host_conf) = @_;
if (!-x "/usr/libexec/proxmox/proxmox-firewall") {
@@ -4743,9 +4750,6 @@ sub is_nftables {
my sub update_force_nftables_disable_flag {
my ($cluster_firewall_enabled, $is_nftables) = @_;
- # This is checked in proxmox-firewall to avoid log-spam due to failing to parse the config
- my $FORCE_NFT_DISABLE_FLAG_FILE = "/run/proxmox-nftables-firewall-force-disable";
-
if (!($cluster_firewall_enabled && $is_nftables)) {
if (! -e $FORCE_NFT_DISABLE_FLAG_FILE) {
open(my $_fh, '>', $FORCE_NFT_DISABLE_FLAG_FILE)
@@ -4757,13 +4761,13 @@ my sub update_force_nftables_disable_flag {
}
}
-sub is_enabled_and_not_nftables {
+my sub is_enabled_and_not_nftables {
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);
- my $is_nftables = is_nftables($cluster_conf, $host_conf);
+ my $is_nftables = get_nftables_option($cluster_conf, $host_conf);
update_force_nftables_disable_flag($cluster_conf->{options}->{enable}, $is_nftables);
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 24+ messages in thread
* [pve-devel] [PATCH pve-firewall v4 6/9] api: load sdn ipsets
2024-11-15 12:09 [pve-devel] [PATCH docs/firewall/manager/proxmox{-firewall, -perl-rs} v4 0/9] autogenerate ipsets for sdn objects Stefan Hanreich
` (4 preceding siblings ...)
2024-11-15 12:09 ` [pve-devel] [PATCH pve-firewall v4 5/9] nftables: make is_nftables check flag file instead of config Stefan Hanreich
@ 2024-11-15 12:09 ` Stefan Hanreich
2024-11-17 14:30 ` Thomas Lamprecht
2024-11-15 12:09 ` [pve-devel] [PATCH proxmox-perl-rs v4 7/9] add PVE::RS::Firewall::SDN module Stefan Hanreich
` (3 subsequent siblings)
9 siblings, 1 reply; 24+ messages in thread
From: Stefan Hanreich @ 2024-11-15 12:09 UTC (permalink / raw)
To: pve-devel; +Cc: Wolfgang Bumiller
Since the SDN configuration reads the IPAM config file, which resides
in /etc/pve/priv we need to add the protected flag to several
endpoints.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Reviewed-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Tested-By: Gabriel Goller <g.goller@proxmox.com>
Tested-By: Hannes Dürr <h.duerr@proxmox.com>
---
src/PVE/API2/Firewall/Aliases.pm | 2 ++
src/PVE/API2/Firewall/Cluster.pm | 7 ++++++-
src/PVE/API2/Firewall/Groups.pm | 1 +
src/PVE/API2/Firewall/Host.pm | 1 +
src/PVE/API2/Firewall/IPSet.pm | 2 ++
src/PVE/API2/Firewall/Rules.pm | 2 ++
src/PVE/API2/Firewall/VM.pm | 5 ++++-
7 files changed, 18 insertions(+), 2 deletions(-)
diff --git a/src/PVE/API2/Firewall/Aliases.pm b/src/PVE/API2/Firewall/Aliases.pm
index 33ac669..2f947aa 100644
--- a/src/PVE/API2/Firewall/Aliases.pm
+++ b/src/PVE/API2/Firewall/Aliases.pm
@@ -87,6 +87,7 @@ sub register_get_aliases {
path => '',
method => 'GET',
description => "List aliases",
+ protected => 1,
permissions => PVE::Firewall::rules_audit_permissions($class->rule_env()),
parameters => {
additionalProperties => 0,
@@ -177,6 +178,7 @@ sub register_read_alias {
path => '{name}',
method => 'GET',
description => "Read alias.",
+ protected => 1,
permissions => PVE::Firewall::rules_audit_permissions($class->rule_env()),
parameters => {
additionalProperties => 0,
diff --git a/src/PVE/API2/Firewall/Cluster.pm b/src/PVE/API2/Firewall/Cluster.pm
index 48ad90d..e519ab9 100644
--- a/src/PVE/API2/Firewall/Cluster.pm
+++ b/src/PVE/API2/Firewall/Cluster.pm
@@ -88,6 +88,7 @@ __PACKAGE__->register_method({
path => 'options',
method => 'GET',
description => "Get Firewall options.",
+ protected => 1,
permissions => {
check => ['perm', '/', [ 'Sys.Audit' ]],
},
@@ -214,6 +215,7 @@ __PACKAGE__->register_method({
permissions => {
check => ['perm', '/', [ 'Sys.Audit' ]],
},
+ protected => 1,
parameters => {
additionalProperties => 0,
properties => {
@@ -255,7 +257,10 @@ __PACKAGE__->register_method({
my $conf = PVE::Firewall::load_clusterfw_conf();
- return PVE::Firewall::Helpers::collect_refs($conf, $param->{type}, "dc");
+ my $cluster_refs = PVE::Firewall::Helpers::collect_refs($conf, $param->{type}, "dc");
+ my $sdn_refs = PVE::Firewall::Helpers::collect_refs($conf->{sdn}, $param->{type}, "sdn");
+
+ return [@$sdn_refs, @$cluster_refs];
}});
1;
diff --git a/src/PVE/API2/Firewall/Groups.pm b/src/PVE/API2/Firewall/Groups.pm
index ffdc45c..98b0747 100644
--- a/src/PVE/API2/Firewall/Groups.pm
+++ b/src/PVE/API2/Firewall/Groups.pm
@@ -44,6 +44,7 @@ __PACKAGE__->register_method({
path => '',
method => 'GET',
description => "List security groups.",
+ protected => 1,
permissions => { user => 'all' },
parameters => {
additionalProperties => 0,
diff --git a/src/PVE/API2/Firewall/Host.pm b/src/PVE/API2/Firewall/Host.pm
index 0432de2..8bd5da1 100644
--- a/src/PVE/API2/Firewall/Host.pm
+++ b/src/PVE/API2/Firewall/Host.pm
@@ -68,6 +68,7 @@ __PACKAGE__->register_method({
path => 'options',
method => 'GET',
description => "Get host firewall options.",
+ protected => 1,
proxyto => 'node',
permissions => {
check => ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
diff --git a/src/PVE/API2/Firewall/IPSet.pm b/src/PVE/API2/Firewall/IPSet.pm
index ed92d87..98c5443 100644
--- a/src/PVE/API2/Firewall/IPSet.pm
+++ b/src/PVE/API2/Firewall/IPSet.pm
@@ -91,6 +91,7 @@ sub register_get_ipset {
path => '',
method => 'GET',
description => "List IPSet content",
+ protected => 1,
permissions => PVE::Firewall::rules_audit_permissions($class->rule_env()),
parameters => {
additionalProperties => 0,
@@ -586,6 +587,7 @@ sub register_index {
path => '',
method => 'GET',
description => "List IPSets",
+ protected => 1,
permissions => PVE::Firewall::rules_audit_permissions($class->rule_env()),
parameters => {
additionalProperties => 0,
diff --git a/src/PVE/API2/Firewall/Rules.pm b/src/PVE/API2/Firewall/Rules.pm
index 9fcfb20..9e903d4 100644
--- a/src/PVE/API2/Firewall/Rules.pm
+++ b/src/PVE/API2/Firewall/Rules.pm
@@ -72,6 +72,7 @@ sub register_get_rules {
path => '',
method => 'GET',
description => "List rules.",
+ protected => 1,
permissions => PVE::Firewall::rules_audit_permissions($rule_env),
parameters => {
additionalProperties => 0,
@@ -120,6 +121,7 @@ sub register_get_rule {
path => '{pos}',
method => 'GET',
description => "Get single rule data.",
+ protected => 1,
permissions => PVE::Firewall::rules_audit_permissions($rule_env),
parameters => {
additionalProperties => 0,
diff --git a/src/PVE/API2/Firewall/VM.pm b/src/PVE/API2/Firewall/VM.pm
index 4222103..75b4345 100644
--- a/src/PVE/API2/Firewall/VM.pm
+++ b/src/PVE/API2/Firewall/VM.pm
@@ -69,6 +69,7 @@ sub register_handlers {
path => 'options',
method => 'GET',
description => "Get VM firewall options.",
+ protected => 1,
proxyto => 'node',
permissions => {
check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
@@ -234,6 +235,7 @@ sub register_handlers {
path => 'refs',
method => 'GET',
description => "Lists possible IPSet/Alias reference which are allowed in source/dest properties.",
+ protected => 1,
permissions => {
check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
},
@@ -282,9 +284,10 @@ sub register_handlers {
my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, $rule_env, $param->{vmid});
my $dc_refs = PVE::Firewall::Helpers::collect_refs($cluster_conf, $param->{type}, 'dc');
+ my $sdn_refs = PVE::Firewall::Helpers::collect_refs($cluster_conf->{sdn}, $param->{type}, 'sdn');
my $vm_refs = PVE::Firewall::Helpers::collect_refs($fw_conf, $param->{type}, 'guest');
- return [@$dc_refs, @$vm_refs];
+ return [@$dc_refs, @$sdn_refs, @$vm_refs];
}});
}
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 24+ messages in thread
* [pve-devel] [PATCH proxmox-perl-rs v4 7/9] add PVE::RS::Firewall::SDN module
2024-11-15 12:09 [pve-devel] [PATCH docs/firewall/manager/proxmox{-firewall, -perl-rs} v4 0/9] autogenerate ipsets for sdn objects Stefan Hanreich
` (5 preceding siblings ...)
2024-11-15 12:09 ` [pve-devel] [PATCH pve-firewall v4 6/9] api: load sdn ipsets Stefan Hanreich
@ 2024-11-15 12:09 ` Stefan Hanreich
2024-11-17 14:36 ` [pve-devel] applied: " Thomas Lamprecht
2024-11-15 12:09 ` [pve-devel] [PATCH pve-manager v4 8/9] firewall: add sdn scope to IPRefSelector Stefan Hanreich
` (2 subsequent siblings)
9 siblings, 1 reply; 24+ messages in thread
From: Stefan Hanreich @ 2024-11-15 12:09 UTC (permalink / raw)
To: pve-devel; +Cc: Wolfgang Bumiller
Used for obtaining the IPSets that get autogenerated by the nftables
firewall. The returned configuration has the same format as the
pve-firewall uses internally, making it compatible with the existing
pve-firewall code.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Reviewed-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Tested-by: Gabriel Goller <g.goller@proxmox.com>
Tested-by: Hannes Dürr <h.duerr@proxmox.com>
---
pve-rs/Cargo.toml | 1 +
pve-rs/Makefile | 1 +
pve-rs/src/firewall/mod.rs | 1 +
pve-rs/src/firewall/sdn.rs | 130 +++++++++++++++++++++++++++++++++++++
pve-rs/src/lib.rs | 1 +
5 files changed, 134 insertions(+)
create mode 100644 pve-rs/src/firewall/mod.rs
create mode 100644 pve-rs/src/firewall/sdn.rs
diff --git a/pve-rs/Cargo.toml b/pve-rs/Cargo.toml
index 9470cb7..c0af2b3 100644
--- a/pve-rs/Cargo.toml
+++ b/pve-rs/Cargo.toml
@@ -45,3 +45,4 @@ proxmox-subscription = "0.5"
proxmox-sys = "0.6"
proxmox-tfa = { version = "5", features = ["api"] }
proxmox-time = "2"
+proxmox-ve-config = { version = "0.1.0" }
diff --git a/pve-rs/Makefile b/pve-rs/Makefile
index c6b4e08..d01da69 100644
--- a/pve-rs/Makefile
+++ b/pve-rs/Makefile
@@ -28,6 +28,7 @@ PERLMOD_GENPACKAGE := /usr/lib/perlmod/genpackage.pl \
PERLMOD_PACKAGES := \
PVE::RS::APT::Repositories \
+ PVE::RS::Firewall::SDN \
PVE::RS::OpenId \
PVE::RS::ResourceScheduling::Static \
PVE::RS::TFA
diff --git a/pve-rs/src/firewall/mod.rs b/pve-rs/src/firewall/mod.rs
new file mode 100644
index 0000000..8bd18a8
--- /dev/null
+++ b/pve-rs/src/firewall/mod.rs
@@ -0,0 +1 @@
+pub mod sdn;
diff --git a/pve-rs/src/firewall/sdn.rs b/pve-rs/src/firewall/sdn.rs
new file mode 100644
index 0000000..5049f74
--- /dev/null
+++ b/pve-rs/src/firewall/sdn.rs
@@ -0,0 +1,130 @@
+#[perlmod::package(name = "PVE::RS::Firewall::SDN", lib = "pve_rs")]
+mod export {
+ use std::collections::HashMap;
+ use std::{fs, io};
+
+ use anyhow::{bail, Context, Error};
+ use serde::Serialize;
+
+ use proxmox_ve_config::{
+ common::Allowlist,
+ firewall::types::ipset::{IpsetAddress, IpsetEntry},
+ firewall::types::Ipset,
+ guest::types::Vmid,
+ sdn::{
+ config::{RunningConfig, SdnConfig},
+ ipam::{Ipam, IpamJson},
+ VnetName,
+ },
+ };
+
+ #[derive(Clone, Debug, Default, Serialize)]
+ pub struct LegacyIpsetEntry {
+ nomatch: bool,
+ cidr: String,
+ comment: Option<String>,
+ }
+
+ impl LegacyIpsetEntry {
+ pub fn from_ipset_entry(entry: &IpsetEntry) -> Vec<LegacyIpsetEntry> {
+ let mut entries = Vec::new();
+
+ match &entry.address {
+ IpsetAddress::Alias(name) => {
+ entries.push(Self {
+ nomatch: entry.nomatch,
+ cidr: name.to_string(),
+ comment: entry.comment.clone(),
+ });
+ }
+ IpsetAddress::Cidr(cidr) => {
+ entries.push(Self {
+ nomatch: entry.nomatch,
+ cidr: cidr.to_string(),
+ comment: entry.comment.clone(),
+ });
+ }
+ IpsetAddress::Range(range) => {
+ entries.extend(range.to_cidrs().into_iter().map(|cidr| Self {
+ nomatch: entry.nomatch,
+ cidr: cidr.to_string(),
+ comment: entry.comment.clone(),
+ }))
+ }
+ };
+
+ entries
+ }
+ }
+
+ #[derive(Clone, Debug, Default, Serialize)]
+ pub struct SdnFirewallConfig {
+ ipset: HashMap<String, Vec<LegacyIpsetEntry>>,
+ ipset_comments: HashMap<String, String>,
+ }
+
+ impl SdnFirewallConfig {
+ pub fn new() -> Self {
+ Default::default()
+ }
+
+ pub fn extend_ipsets(&mut self, ipsets: impl IntoIterator<Item = Ipset>) {
+ for ipset in ipsets {
+ let entries = ipset
+ .iter()
+ .flat_map(LegacyIpsetEntry::from_ipset_entry)
+ .collect();
+
+ self.ipset.insert(ipset.name().name().to_string(), entries);
+
+ if let Some(comment) = &ipset.comment {
+ self.ipset_comments
+ .insert(ipset.name().name().to_string(), comment.to_string());
+ }
+ }
+ }
+ }
+
+ const SDN_RUNNING_CONFIG: &str = "/etc/pve/sdn/.running-config";
+ const SDN_IPAM: &str = "/etc/pve/priv/ipam.db";
+
+ #[export]
+ pub fn config(
+ vnet_filter: Option<Vec<VnetName>>,
+ vm_filter: Option<Vec<Vmid>>,
+ ) -> Result<SdnFirewallConfig, Error> {
+ let mut refs = SdnFirewallConfig::new();
+
+ match fs::read_to_string(SDN_RUNNING_CONFIG) {
+ Ok(data) => {
+ let running_config: RunningConfig = serde_json::from_str(&data)?;
+ let sdn_config = SdnConfig::try_from(running_config)
+ .with_context(|| "Failed to parse SDN config".to_string())?;
+
+ let allowlist = vnet_filter.map(Allowlist::from_iter);
+ refs.extend_ipsets(sdn_config.ipsets(allowlist.as_ref()));
+ }
+ Err(e) if e.kind() == io::ErrorKind::NotFound => (),
+ Err(e) => {
+ bail!("Cannot open SDN running config: {e:#}");
+ }
+ };
+
+ match fs::read_to_string(SDN_IPAM) {
+ Ok(data) => {
+ let ipam_json: IpamJson = serde_json::from_str(&data)?;
+ let ipam: Ipam = Ipam::try_from(ipam_json)
+ .with_context(|| "Failed to parse IPAM".to_string())?;
+
+ let allowlist = vm_filter.map(Allowlist::from_iter);
+ refs.extend_ipsets(ipam.ipsets(allowlist.as_ref()));
+ }
+ Err(e) if e.kind() == io::ErrorKind::NotFound => (),
+ Err(e) => {
+ bail!("Cannot open IPAM database: {e:#}");
+ }
+ };
+
+ Ok(refs)
+ }
+}
diff --git a/pve-rs/src/lib.rs b/pve-rs/src/lib.rs
index 5e47ac6..3de37d1 100644
--- a/pve-rs/src/lib.rs
+++ b/pve-rs/src/lib.rs
@@ -12,6 +12,7 @@ use proxmox_notify::{Config, Notification, Severity};
pub mod common;
pub mod apt;
+pub mod firewall;
pub mod openid;
pub mod resource_scheduling;
pub mod tfa;
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 24+ messages in thread
* [pve-devel] [PATCH pve-manager v4 8/9] firewall: add sdn scope to IPRefSelector
2024-11-15 12:09 [pve-devel] [PATCH docs/firewall/manager/proxmox{-firewall, -perl-rs} v4 0/9] autogenerate ipsets for sdn objects Stefan Hanreich
` (6 preceding siblings ...)
2024-11-15 12:09 ` [pve-devel] [PATCH proxmox-perl-rs v4 7/9] add PVE::RS::Firewall::SDN module Stefan Hanreich
@ 2024-11-15 12:09 ` Stefan Hanreich
2024-11-15 12:09 ` [pve-devel] [PATCH pve-docs v4 9/9] sdn: add documentation for firewall integration Stefan Hanreich
2024-11-18 11:47 ` [pve-devel] [PATCH docs/firewall/manager/proxmox{-firewall, -perl-rs} v4 0/9] autogenerate ipsets for sdn objects Stefan Hanreich
9 siblings, 0 replies; 24+ messages in thread
From: Stefan Hanreich @ 2024-11-15 12:09 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Tested-by: Gabriel Goller <g.goller@proxmox.com>
Tested-by: Hannes Dürr <h.duerr@proxmox.com>
---
www/manager6/form/IPRefSelector.js | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/www/manager6/form/IPRefSelector.js b/www/manager6/form/IPRefSelector.js
index d41cde5f5..16078e428 100644
--- a/www/manager6/form/IPRefSelector.js
+++ b/www/manager6/form/IPRefSelector.js
@@ -67,6 +67,12 @@ Ext.define('PVE.form.IPRefSelector', {
});
}
+ let scopes = {
+ 'dc': gettext("Datacenter"),
+ 'guest': gettext("Guest"),
+ 'sdn': gettext("SDN"),
+ };
+
columns.push(
{
header: gettext('Name'),
@@ -80,7 +86,7 @@ Ext.define('PVE.form.IPRefSelector', {
hideable: false,
width: 140,
renderer: function(value) {
- return value === 'dc' ? gettext("Datacenter") : gettext("Guest");
+ return scopes[value] ?? "unknown scope";
},
},
{
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 24+ messages in thread
* [pve-devel] [PATCH pve-docs v4 9/9] sdn: add documentation for firewall integration
2024-11-15 12:09 [pve-devel] [PATCH docs/firewall/manager/proxmox{-firewall, -perl-rs} v4 0/9] autogenerate ipsets for sdn objects Stefan Hanreich
` (7 preceding siblings ...)
2024-11-15 12:09 ` [pve-devel] [PATCH pve-manager v4 8/9] firewall: add sdn scope to IPRefSelector Stefan Hanreich
@ 2024-11-15 12:09 ` Stefan Hanreich
2024-11-18 11:47 ` [pve-devel] [PATCH docs/firewall/manager/proxmox{-firewall, -perl-rs} v4 0/9] autogenerate ipsets for sdn objects Stefan Hanreich
9 siblings, 0 replies; 24+ messages in thread
From: Stefan Hanreich @ 2024-11-15 12:09 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
pvesdn.adoc | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 92 insertions(+)
diff --git a/pvesdn.adoc b/pvesdn.adoc
index 39de80f..c187365 100644
--- a/pvesdn.adoc
+++ b/pvesdn.adoc
@@ -702,6 +702,98 @@ For more information please consult the documentation of
xref:pvesdn_ipam_plugin_pveipam[the PVE IPAM plugin]. Changing DHCP leases is
currently not supported for the other IPAM plugins.
+Firewall Integration
+--------------------
+
+SDN integrates with the Proxmox VE firewall by automatically generating IPSets
+which can then be referenced in the source / destination fields of firewall
+rules. This happens automatically for VNets and IPAM entries.
+
+VNets and Subnets
+~~~~~~~~~~~~~~~~~
+
+The firewall automatically generates the following IPSets in the SDN scope for
+every VNet:
+
+`vnet-all`::
+ Contains the CIDRs of all subnets in a VNet
+`vnet-gateway`::
+ Contains the IPs of the gateways of all subnets in a VNet
+`vnet-no-gateway`::
+ Contains the CIDRs of all subnets in a VNet, but excludes the gateways
+`vnet-dhcp`::
+ Contains all DHCP ranges configured in the subnets in a VNet
+
+When making changes to your configuration, the IPSets update automatically, so
+you do not have to update your firewall rules when changing the configuration of
+your Subnets.
+
+Simple Zone Example
+^^^^^^^^^^^^^^^^^^^
+
+Assuming the configuration below for a VNet and its contained subnets:
+
+----
+# /etc/pve/sdn/vnets.cfg
+
+vnet: vnet0
+ zone simple
+
+# /etc/pve/sdn/subnets.cfg
+
+subnet: simple-192.0.2.0-24
+ vnet vnet0
+ dhcp-range start-address=192.0.2.100,end-address=192.0.2.199
+ gateway 192.0.2.1
+
+subnet: simple-2001:db8::-64
+ vnet vnet0
+ dhcp-range start-address=2001:db8::1000,end-address=2001:db8::1999
+ gateway 2001:db8::1
+----
+
+In this example we configured an IPv4 subnet in the VNet `vnet0`, with
+'192.0.2.0/24' as its IP Range, '192.0.2.1' as the gateway and the DHCP range is
+'192.0.2.100' - '192.0.2.199'.
+
+Additionally we configured an IPv6 subnet with '2001:db8::/64' as the IP range,
+'2001:db8::1' as the gateway and a DHCP range of '2001:db8::1000' -
+'2001:db8::1999'.
+
+The respective auto-generated IPsets for vnet0 would then contain the following
+elements:
+
+`vnet0-all`::
+* '192.0.2.0/24'
+* '2001:db8::/64'
+`vnet0-gateway`::
+* '192.0.2.1'
+* '2001:db8::1'
+`vnet0-no-gateway`::
+* '192.0.2.0/24'
+* '2001:db8::/64'
+* '!192.0.2.1'
+* '!2001:db8::1'
+`vnet0-dhcp`::
+* '192.0.2.100 - 192.0.2.199'
+* '2001:db8::1000 - 2001:db8::1999'
+
+IPAM
+~~~~
+
+If you are using the built-in PVE IPAM, then the firewall automatically
+generates an IPset for every guest that has entries in the IPAM. The respective
+IPset for a guest with ID 100 would be `guest-ipam-100`. It contains all IP
+addresses from all IPAM entries. So if guest 100 is member of multiple VNets,
+then the IPset would contain the IPs from *all* VNets.
+
+When entries get added / updated / deleted, then the respective IPSets will be
+updated accordingly.
+
+WARNING: When removing all entries for a guest and there are firewall rules
+still referencing the auto-generated IPSet then the firewall will fail to update
+the ruleset, since it references a non-existing IPSet.
+
[[pvesdn_setup_examples]]
Examples
--------
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 24+ messages in thread
* [pve-devel] applied: [PATCH proxmox-firewall v4 3/9] ipsets: autogenerate ipsets for vnets and ipam
2024-11-15 12:09 ` [pve-devel] [PATCH proxmox-firewall v4 3/9] ipsets: autogenerate ipsets for vnets and ipam Stefan Hanreich
@ 2024-11-17 14:08 ` Thomas Lamprecht
0 siblings, 0 replies; 24+ messages in thread
From: Thomas Lamprecht @ 2024-11-17 14:08 UTC (permalink / raw)
To: Proxmox VE development discussion, Stefan Hanreich; +Cc: Wolfgang Bumiller
Am 15.11.24 um 13:09 schrieb Stefan Hanreich:
> They act like virtual ipsets, similar to ipfilter-net, that can be
> used for defining firewall rules for sdn objects dynamically.
>
> The changes in proxmox-ve-config also introduced a dedicated struct
> for representing ip ranges, so we update the existing code, so that it
> uses that struct as well.
>
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> Reviewed-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
> Tested-by: Gabriel Goller <g.goller@proxmox.com>
> Tested-by: Hannes Dürr <h.duerr@proxmox.com>
> ---
> proxmox-firewall/src/firewall.rs | 22 +-
> proxmox-firewall/src/object.rs | 41 +-
> .../integration_tests__firewall.snap | 1288 +++++++++++++++++
> proxmox-nftables/src/expression.rs | 17 +-
> 4 files changed, 1354 insertions(+), 14 deletions(-)
>
>
applied, thanks!
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 24+ messages in thread
* [pve-devel] applied: [PATCH proxmox-firewall v4 1/9] add proxmox-ve-rs crate - move proxmox-ve-config there
2024-11-15 12:09 ` [pve-devel] [PATCH proxmox-firewall v4 1/9] add proxmox-ve-rs crate - move proxmox-ve-config there Stefan Hanreich
@ 2024-11-17 14:08 ` Thomas Lamprecht
0 siblings, 0 replies; 24+ messages in thread
From: Thomas Lamprecht @ 2024-11-17 14:08 UTC (permalink / raw)
To: Proxmox VE development discussion, Stefan Hanreich; +Cc: Wolfgang Bumiller
Am 15.11.24 um 13:09 schrieb Stefan Hanreich:
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> Reviewed-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
> ---
> Cargo.toml | 4 +-
> Makefile | 2 +-
> proxmox-firewall/Cargo.toml | 2 +-
> proxmox-nftables/Cargo.toml | 2 +-
> proxmox-ve-config/Cargo.toml | 25 -
> proxmox-ve-config/resources/ct_helper.json | 52 -
> proxmox-ve-config/resources/macros.json | 923 -----------------
> proxmox-ve-config/src/firewall/cluster.rs | 374 -------
> proxmox-ve-config/src/firewall/common.rs | 184 ----
> proxmox-ve-config/src/firewall/ct_helper.rs | 115 ---
> proxmox-ve-config/src/firewall/fw_macros.rs | 69 --
> proxmox-ve-config/src/firewall/guest.rs | 237 -----
> proxmox-ve-config/src/firewall/host.rs | 372 -------
> proxmox-ve-config/src/firewall/mod.rs | 10 -
> proxmox-ve-config/src/firewall/parse.rs | 494 ---------
> proxmox-ve-config/src/firewall/ports.rs | 80 --
> .../src/firewall/types/address.rs | 615 -----------
> proxmox-ve-config/src/firewall/types/alias.rs | 174 ----
> proxmox-ve-config/src/firewall/types/group.rs | 36 -
> proxmox-ve-config/src/firewall/types/ipset.rs | 349 -------
> proxmox-ve-config/src/firewall/types/log.rs | 222 ----
> proxmox-ve-config/src/firewall/types/mod.rs | 14 -
> proxmox-ve-config/src/firewall/types/port.rs | 181 ----
> proxmox-ve-config/src/firewall/types/rule.rs | 412 --------
> .../src/firewall/types/rule_match.rs | 977 ------------------
> 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/host/mod.rs | 1 -
> proxmox-ve-config/src/host/utils.rs | 70 --
> proxmox-ve-config/src/lib.rs | 3 -
> 31 files changed, 6 insertions(+), 6656 deletions(-)
> delete mode 100644 proxmox-ve-config/Cargo.toml
> delete mode 100644 proxmox-ve-config/resources/ct_helper.json
> delete mode 100644 proxmox-ve-config/resources/macros.json
> delete mode 100644 proxmox-ve-config/src/firewall/cluster.rs
> delete mode 100644 proxmox-ve-config/src/firewall/common.rs
> delete mode 100644 proxmox-ve-config/src/firewall/ct_helper.rs
> delete mode 100644 proxmox-ve-config/src/firewall/fw_macros.rs
> delete mode 100644 proxmox-ve-config/src/firewall/guest.rs
> delete mode 100644 proxmox-ve-config/src/firewall/host.rs
> delete mode 100644 proxmox-ve-config/src/firewall/mod.rs
> delete mode 100644 proxmox-ve-config/src/firewall/parse.rs
> delete mode 100644 proxmox-ve-config/src/firewall/ports.rs
> delete mode 100644 proxmox-ve-config/src/firewall/types/address.rs
> delete mode 100644 proxmox-ve-config/src/firewall/types/alias.rs
> delete mode 100644 proxmox-ve-config/src/firewall/types/group.rs
> delete mode 100644 proxmox-ve-config/src/firewall/types/ipset.rs
> delete mode 100644 proxmox-ve-config/src/firewall/types/log.rs
> delete mode 100644 proxmox-ve-config/src/firewall/types/mod.rs
> delete mode 100644 proxmox-ve-config/src/firewall/types/port.rs
> delete mode 100644 proxmox-ve-config/src/firewall/types/rule.rs
> delete mode 100644 proxmox-ve-config/src/firewall/types/rule_match.rs
> delete mode 100644 proxmox-ve-config/src/guest/mod.rs
> delete mode 100644 proxmox-ve-config/src/guest/types.rs
> delete mode 100644 proxmox-ve-config/src/guest/vm.rs
> delete mode 100644 proxmox-ve-config/src/host/mod.rs
> delete mode 100644 proxmox-ve-config/src/host/utils.rs
> delete mode 100644 proxmox-ve-config/src/lib.rs
>
>
applied, thanks!
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 24+ messages in thread
* [pve-devel] applied: [PATCH proxmox-firewall v4 2/9] config: tests: add support for loading sdn and ipam config
2024-11-15 12:09 ` [pve-devel] [PATCH proxmox-firewall v4 2/9] config: tests: add support for loading sdn and ipam config Stefan Hanreich
@ 2024-11-17 14:08 ` Thomas Lamprecht
0 siblings, 0 replies; 24+ messages in thread
From: Thomas Lamprecht @ 2024-11-17 14:08 UTC (permalink / raw)
To: Proxmox VE development discussion, Stefan Hanreich; +Cc: Wolfgang Bumiller
Am 15.11.24 um 13:09 schrieb Stefan Hanreich:
> Also add example SDN configuration files that get automatically
> loaded, which can be used for future tests.
>
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> Reviewed-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
> Tested-by: Gabriel Goller <g.goller@proxmox.com>
> Tested-by: Hannes Dürr <h.duerr@proxmox.com>
> ---
> proxmox-firewall/src/config.rs | 69 +++++++++++++++++++
> .../tests/input/.running-config.json | 45 ++++++++++++
> proxmox-firewall/tests/input/ipam.db | 32 +++++++++
> proxmox-firewall/tests/integration_tests.rs | 10 +++
> proxmox-nftables/src/types.rs | 2 +-
> 5 files changed, 157 insertions(+), 1 deletion(-)
> create mode 100644 proxmox-firewall/tests/input/.running-config.json
> create mode 100644 proxmox-firewall/tests/input/ipam.db
>
>
applied, thanks!
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [pve-devel] [PATCH pve-firewall v4 6/9] api: load sdn ipsets
2024-11-15 12:09 ` [pve-devel] [PATCH pve-firewall v4 6/9] api: load sdn ipsets Stefan Hanreich
@ 2024-11-17 14:30 ` Thomas Lamprecht
2024-11-18 9:02 ` Stefan Hanreich
0 siblings, 1 reply; 24+ messages in thread
From: Thomas Lamprecht @ 2024-11-17 14:30 UTC (permalink / raw)
To: Proxmox VE development discussion, Stefan Hanreich; +Cc: Wolfgang Bumiller
Am 15.11.24 um 13:09 schrieb Stefan Hanreich:
> Since the SDN configuration reads the IPAM config file, which resides
does that mean the earlier patches already require this? They load
the SDN config already FWICT; and if so, it would be great to either
have that change in those patches or upfront as separate patches, this
has rather reaching consequences after all...
> in /etc/pve/priv we need to add the protected flag to several
> endpoints.
That's wrong, the general IPAM config resides in /etc/pve/sdn/ipams.cfg,
the ipam.db from the PVE IPAM Plugin does indeed reside in the private
directory.
But, why's that? The commits adding it weren't really telling, but there
are no secrets in there, so why does it have to be priv? We could move
them over to /etc/pve/sdn/pve-ipam.db with some backward compat handling
(either in pmxcfs directly or in the backend site of things). Just tell
me if that would be fine in general, or what the original reason for having
this file only visible for root, and I can help you here.
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 24+ messages in thread
* [pve-devel] applied: [PATCH proxmox-perl-rs v4 7/9] add PVE::RS::Firewall::SDN module
2024-11-15 12:09 ` [pve-devel] [PATCH proxmox-perl-rs v4 7/9] add PVE::RS::Firewall::SDN module Stefan Hanreich
@ 2024-11-17 14:36 ` Thomas Lamprecht
0 siblings, 0 replies; 24+ messages in thread
From: Thomas Lamprecht @ 2024-11-17 14:36 UTC (permalink / raw)
To: Proxmox VE development discussion, Stefan Hanreich; +Cc: Wolfgang Bumiller
Am 15.11.24 um 13:09 schrieb Stefan Hanreich:
> Used for obtaining the IPSets that get autogenerated by the nftables
> firewall. The returned configuration has the same format as the
> pve-firewall uses internally, making it compatible with the existing
> pve-firewall code.
>
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> Reviewed-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
> Tested-by: Gabriel Goller <g.goller@proxmox.com>
> Tested-by: Hannes Dürr <h.duerr@proxmox.com>
> ---
> pve-rs/Cargo.toml | 1 +
> pve-rs/Makefile | 1 +
> pve-rs/src/firewall/mod.rs | 1 +
> pve-rs/src/firewall/sdn.rs | 130 +++++++++++++++++++++++++++++++++++++
> pve-rs/src/lib.rs | 1 +
> 5 files changed, 134 insertions(+)
> create mode 100644 pve-rs/src/firewall/mod.rs
> create mode 100644 pve-rs/src/firewall/sdn.rs
>
>
applied, thanks!
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [pve-devel] [PATCH pve-firewall v4 4/9] add support for loading sdn firewall configuration
2024-11-15 12:09 ` [pve-devel] [PATCH pve-firewall v4 4/9] add support for loading sdn firewall configuration Stefan Hanreich
@ 2024-11-17 14:57 ` Thomas Lamprecht
2024-11-18 9:22 ` Stefan Hanreich
2024-11-18 9:35 ` Stefan Hanreich
0 siblings, 2 replies; 24+ messages in thread
From: Thomas Lamprecht @ 2024-11-17 14:57 UTC (permalink / raw)
To: Proxmox VE development discussion, Stefan Hanreich; +Cc: Wolfgang Bumiller
Am 15.11.24 um 13:09 schrieb Stefan Hanreich:
> This also includes support for parsing rules referencing IPSets in the
> new SDN scope and generating those IPSets in the firewall.
>
> Loading SDN configuration is optional, since loading it requires root
> privileges which we do not have in all call sites. Adding the flag
> allows us to selectively load the SDN configuration only where
> required and at the same time allows us to only elevate privileges in
> the API where absolutely needed.
>
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> Reviewed-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
> Tested-By: Gabriel Goller <g.goller@proxmox.com>
> Tested-By: Hannes Dürr <h.duerr@proxmox.com>
> ---
> src/PVE/Firewall.pm | 62 +++++++++++++++++++++++++++++----
> src/PVE/Service/pve_firewall.pm | 4 +--
> 2 files changed, 57 insertions(+), 9 deletions(-)
>
> diff --git a/src/PVE/Firewall.pm b/src/PVE/Firewall.pm
> index 09544ba..7642bf6 100644
> --- a/src/PVE/Firewall.pm
> +++ b/src/PVE/Firewall.pm
> @@ -25,6 +25,7 @@ use PVE::Tools qw($IPV4RE $IPV6RE);
> use PVE::Tools qw(run_command lock_file dir_glob_foreach);
>
> use PVE::Firewall::Helpers;
> +use PVE::RS::Firewall::SDN;
>
> my $pvefw_conf_dir = "/etc/pve/firewall";
> my $clusterfw_conf_filename = "$pvefw_conf_dir/cluster.fw";
> @@ -1689,9 +1690,12 @@ sub verify_rule {
>
> if (my $value = $rule->{$name}) {
> if ($value =~ m/^\+/) {
> - if ($value =~ m@^\+(guest/|dc/)?(${ipset_name_pattern})$@) {
> + if ($value =~ m@^\+(guest/|dc/|sdn/)?(${ipset_name_pattern})$@) {
> &$add_error($name, "no such ipset '$2'")
> - if !($cluster_conf->{ipset}->{$2} || ($fw_conf && $fw_conf->{ipset}->{$2}));
> + if !($cluster_conf->{ipset}->{$2}
> + || ($fw_conf && $fw_conf->{ipset}->{$2})
> + || ($cluster_conf->{sdn} && $cluster_conf->{sdn}->{ipset}->{$2})
> + || ($fw_conf->{sdn} && $fw_conf->{sdn}->{ipset}->{$2}));
Avoid multi-line post-ifs:
https://pve.proxmox.com/wiki/Perl_Style_Guide#Wrapping_Post-If
rather use something like
if (
!$cluster_conf->{ipset}->{$2}
&& !($fw_conf && $fw_conf->{ipset}->{$2})
&& !($cluster_conf->{sdn} && $cluster_conf->{sdn}->{ipset}->{$2})
&& !($fw_conf->{sdn} && $fw_conf->{sdn}->{ipset}->{$2}))
) {
$add_error->($name, "no such ipset '$2'")
}
>
> } else {
> &$add_error($name, "invalid ipset name '$value'");
> @@ -2108,13 +2112,15 @@ sub ipt_gen_src_or_dst_match {
>
> my $match;
> if ($adr =~ m/^\+/) {
> - if ($adr =~ m@^\+(guest/|dc/)?(${ipset_name_pattern})$@) {
> + if ($adr =~ m@^\+(guest/|dc/|sdn/)?(${ipset_name_pattern})$@) {
> my $scope = $1 // "";
> my $name = $2;
A small closer could help to reduce bewlow if expressions, e.g. like:
my $is_scope = sub { return !$scope || $scope eq "$_[0]/" };
#...
if ($is_scope->('guest') && $fw_conf && $fw_conf->{ipset}->{$name}) {
#...
> my $ipset_chain;
> - if ($scope ne 'dc/' && $fw_conf && $fw_conf->{ipset}->{$name}) {
> + if ((!$scope || $scope eq 'guest/') && $fw_conf && $fw_conf->{ipset}->{$name}) {
> $ipset_chain = compute_ipset_chain_name($fw_conf->{vmid}, $name, $ipversion);
> - } elsif ($scope ne 'guest/' && $cluster_conf && $cluster_conf->{ipset}->{$name}) {
> + } elsif ((!$scope || $scope eq 'dc/') && $cluster_conf && $cluster_conf->{ipset}->{$name}) {
> + $ipset_chain = compute_ipset_chain_name(0, $name, $ipversion);
> + } elsif ((!$scope || $scope eq 'sdn/') && $cluster_conf->{sdn} && $cluster_conf->{sdn}->{ipset}->{$name}) {
> $ipset_chain = compute_ipset_chain_name(0, $name, $ipversion);
> } else {
> die "no such ipset '$name'\n";
> @@ -3644,7 +3650,8 @@ sub lock_clusterfw_conf {
> }
>
> sub load_clusterfw_conf {
> - my ($filename) = @_;
> + my ($filename, $options) = @_;
> +
>
> $filename = $clusterfw_conf_filename if !defined($filename);
> my $empty_conf = {
> @@ -3655,6 +3662,7 @@ sub load_clusterfw_conf {
> group_comments => {},
> ipset => {} ,
> ipset_comments => {},
> + sdn => load_sdn_conf(),
it's a bit odd to assign the full SDN related config to a variable named
$empty_config, but assigning it after the parser will cause a semantic difference
for the case where the firewall config is empty, not sure if that is fine.
> };
>
> my $cluster_conf = generic_fw_config_parser($filename, $empty_conf, $empty_conf, 'cluster');
> @@ -3663,6 +3671,45 @@ sub load_clusterfw_conf {
> return $cluster_conf;
> }
>
> +sub load_sdn_conf {
> + my $rpcenv = eval { PVE::RPCEnvironment::get(); };
Missing use statement for that module, also the semicolon inside the eval can be dropped.
> +
> + if ($@) {
> + warn "could not load SDN configuration because RPCEnvironment is not initialized.";
> + return {};
> + }
> +
> + my $authuser = $rpcenv->get_user();
> +
> + my $guests = PVE::Cluster::get_vmlist();
> + my $allowed_vms = [];
> + foreach my $vmid (sort keys %{$guests->{ids}}) {
> + next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ], 1);
> + push @$allowed_vms, $vmid;
> + }
Above one could be:
$allowed_vms = [
grep { $rpcenv->check($authuser, "/vms/$_", [ 'VM.Audit' ], 1) } sort keys $guests->{ids}->%*
];
> + my $vnets = PVE::Network::SDN::Vnets::config(1);
> + my $privs = [ 'SDN.Audit', 'SDN.Allocate' ];
> + my $allowed_vnets = [];
> + foreach my $vnet (sort keys %{$vnets->{ids}}) {
> + my $zone = $vnets->{ids}->{$vnet}->{zone};
> + next if !$rpcenv->check_any($authuser, "/sdn/zones/$zone/$vnet", $privs, 1);
> + push @$allowed_vnets, $vnet;
> + }
> +
> + my $sdn_config = {
> + ipset => {} ,
> + ipset_comments => {},
> + };
> +
> + eval {
> + $sdn_config = PVE::RS::Firewall::SDN::config($allowed_vnets, $allowed_vms);
> + };
> + warn $@ if $@;
above might be more readable by doing:
my $empty_sdn_config = { ipset => {} , ipset_comments => {} };
my $sdn_config = eval { PVE::RS::Firewall::SDN::config($allowed_vnets, $allowed_vms) };
warn $@ if $@;
return $sdn_config // $empty_sdn_config;
> +
> + return $sdn_config;
> +}
> +
> sub save_clusterfw_conf {
> my ($cluster_conf) = @_;
>
> @@ -3768,7 +3815,7 @@ sub compile {
>
> $vmfw_configs = read_vm_firewall_configs($cluster_conf, $vmdata, $testdir);
> } else { # normal operation
> - $cluster_conf = load_clusterfw_conf(undef) if !$cluster_conf;
> + $cluster_conf = load_clusterfw_conf() if !$cluster_conf;
>
> $hostfw_conf = load_hostfw_conf($cluster_conf, undef) if !$hostfw_conf;
>
> @@ -4043,6 +4090,7 @@ sub compile_ipsets {
> }
>
> generate_ipset_chains($ipset_ruleset, undef, $cluster_conf, undef, $cluster_conf->{ipset});
> + generate_ipset_chains($ipset_ruleset, undef, $cluster_conf, undef, $cluster_conf->{sdn}->{ipset});
>
> return $ipset_ruleset;
> }
> diff --git a/src/PVE/Service/pve_firewall.pm b/src/PVE/Service/pve_firewall.pm
> index 65cb2b8..02b507a 100755
> --- a/src/PVE/Service/pve_firewall.pm
> +++ b/src/PVE/Service/pve_firewall.pm
> @@ -158,7 +158,7 @@ __PACKAGE__->register_method ({
>
> PVE::Firewall::set_verbose(1); # show syntax errors
>
> - my $cluster_conf = PVE::Firewall::load_clusterfw_conf(undef);
> + my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
just to be sure, above and below does not change anything, or?
> $res->{enable} = $cluster_conf->{options}->{enable} ? 1 : 0;
>
> if ($status eq 'running') {
> @@ -202,7 +202,7 @@ __PACKAGE__->register_method ({
>
> PVE::Firewall::set_verbose(1);
>
> - my $cluster_conf = PVE::Firewall::load_clusterfw_conf(undef);
> + my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
> my ($ruleset, $ipset_ruleset, $rulesetv6, $ebtables_ruleset) = PVE::Firewall::compile($cluster_conf, undef, undef);
>
> print "ipset cmdlist:\n";
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 24+ messages in thread
* [pve-devel] applied: [PATCH pve-firewall v4 5/9] nftables: make is_nftables check flag file instead of config
2024-11-15 12:09 ` [pve-devel] [PATCH pve-firewall v4 5/9] nftables: make is_nftables check flag file instead of config Stefan Hanreich
@ 2024-11-17 14:58 ` Thomas Lamprecht
0 siblings, 0 replies; 24+ messages in thread
From: Thomas Lamprecht @ 2024-11-17 14:58 UTC (permalink / raw)
To: Proxmox VE development discussion, Stefan Hanreich; +Cc: Wolfgang Bumiller
Am 15.11.24 um 13:09 schrieb Stefan Hanreich:
> is_nftables is used in the VM and CT network startup scripts to
> determine whether the nftables firewall is enabled or not. This causes
> issues on container and VM startup when loading the SDN config, since
> it requires the RPCEnvironment which is not initialized yet. Therefore
> change this check to look for the existence of the flag file instead.
>
> It also avoids parsing the entire cluster and host firewall
> configuration on VM / CT startup, which means increased performance.
>
> While we're at it, make all methods related to the configuration
> parsing private, in order to avoid accidental usage of the expensive
> methods.
>
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> Reviewed-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
> ---
> src/PVE/Firewall.pm | 14 +++++++++-----
> 1 file changed, 9 insertions(+), 5 deletions(-)
>
>
applied, thanks!
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [pve-devel] [PATCH pve-firewall v4 6/9] api: load sdn ipsets
2024-11-17 14:30 ` Thomas Lamprecht
@ 2024-11-18 9:02 ` Stefan Hanreich
2024-11-18 11:38 ` Thomas Lamprecht
0 siblings, 1 reply; 24+ messages in thread
From: Stefan Hanreich @ 2024-11-18 9:02 UTC (permalink / raw)
To: Thomas Lamprecht, Proxmox VE development discussion; +Cc: Wolfgang Bumiller
On 11/17/24 15:30, Thomas Lamprecht wrote:
> Am 15.11.24 um 13:09 schrieb Stefan Hanreich:
>> Since the SDN configuration reads the IPAM config file, which resides
>
> does that mean the earlier patches already require this? They load
> the SDN config already FWICT; and if so, it would be great to either
> have that change in those patches or upfront as separate patches, this
> has rather reaching consequences after all...
That's indeed an oversight on my part, the default behavior of
load_clusterfw_conf changed to loading the SDN configuration in v4 so
that patch is actually required if they are not all applied at the same
time. If we stick with /etc/pve/priv (see below) I'll reorder the
commits accordingly.
>> in /etc/pve/priv we need to add the protected flag to several
>> endpoints.
>
> That's wrong, the general IPAM config resides in /etc/pve/sdn/ipams.cfg,
> the ipam.db from the PVE IPAM Plugin does indeed reside in the private
> directory.
>
> But, why's that? The commits adding it weren't really telling, but there
> are no secrets in there, so why does it have to be priv? We could move
> them over to /etc/pve/sdn/pve-ipam.db with some backward compat handling
> (either in pmxcfs directly or in the backend site of things). Just tell
> me if that would be fine in general, or what the original reason for having
> this file only visible for root, and I can help you here.
Depends on if you consider a database of all assigned IPs inside the
cluster as sensitive information, iirc we erred on the side of caution
in this case and stored it in /etc/pve/priv.
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [pve-devel] [PATCH pve-firewall v4 4/9] add support for loading sdn firewall configuration
2024-11-17 14:57 ` Thomas Lamprecht
@ 2024-11-18 9:22 ` Stefan Hanreich
2024-11-18 9:35 ` Stefan Hanreich
1 sibling, 0 replies; 24+ messages in thread
From: Stefan Hanreich @ 2024-11-18 9:22 UTC (permalink / raw)
To: Thomas Lamprecht, Proxmox VE development discussion; +Cc: Wolfgang Bumiller
On 11/17/24 15:57, Thomas Lamprecht wrote:
>> diff --git a/src/PVE/Service/pve_firewall.pm b/src/PVE/Service/pve_firewall.pm
>> index 65cb2b8..02b507a 100755
>> --- a/src/PVE/Service/pve_firewall.pm
>> +++ b/src/PVE/Service/pve_firewall.pm
>> @@ -158,7 +158,7 @@ __PACKAGE__->register_method ({
>>
>> PVE::Firewall::set_verbose(1); # show syntax errors
>>
>> - my $cluster_conf = PVE::Firewall::load_clusterfw_conf(undef);
>> + my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
>
> just to be sure, above and below does not change anything, or?
yes. if the first parameter (= filename) is undef, then it loads the
config from the default path.
I'll look into incorporating the rest of your suggestions - thanks!
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [pve-devel] [PATCH pve-firewall v4 4/9] add support for loading sdn firewall configuration
2024-11-17 14:57 ` Thomas Lamprecht
2024-11-18 9:22 ` Stefan Hanreich
@ 2024-11-18 9:35 ` Stefan Hanreich
1 sibling, 0 replies; 24+ messages in thread
From: Stefan Hanreich @ 2024-11-18 9:35 UTC (permalink / raw)
To: Thomas Lamprecht, Proxmox VE development discussion; +Cc: Wolfgang Bumiller
On 11/17/24 15:57, Thomas Lamprecht wrote:
> it's a bit odd to assign the full SDN related config to a variable named
> $empty_config, but assigning it after the parser will cause a semantic difference
> for the case where the firewall config is empty, not sure if that is fine.
It has to be beforehand, otherwise if you reference the SDN IPsets in
your cluster configuration you will get verification errors since
generic_fw_config_parser() will execute the whole validation /
verification logic - which fails if IPSets do not exist.
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [pve-devel] [PATCH pve-firewall v4 6/9] api: load sdn ipsets
2024-11-18 9:02 ` Stefan Hanreich
@ 2024-11-18 11:38 ` Thomas Lamprecht
2024-11-18 13:23 ` Thomas Lamprecht
0 siblings, 1 reply; 24+ messages in thread
From: Thomas Lamprecht @ 2024-11-18 11:38 UTC (permalink / raw)
To: Proxmox VE development discussion, Stefan Hanreich; +Cc: Wolfgang Bumiller
Am 18.11.24 um 10:02 schrieb Stefan Hanreich:
> Depends on if you consider a database of all assigned IPs inside the
> cluster as sensitive information, iirc we erred on the side of caution
> in this case and stored it in /etc/pve/priv.
We briefly talked off-list about that, but I think it might be worth to
state this on the list too:
Its sensitive information as in "let's not make that broadly available via
the API to unprivileged users" not secrets that can be used to access third
party systems or break encryption, thus let's be extra vigilant to hedge
against the case where a non-root user/process gets taken over.
As /etc/pve/priv is for the latter, not the former; as else we would need
to also move most configs in there too.
I'll take a short look if it's easily possible to add a sane migration path
at pmxcfs level, handling this transparently, otherwise we'll have to add
some compat handling at higher levels.
Korrigieren
Schließen
Rechtschreibung
Possible spelling mistake found.
EveevepiePVPVCIgnorieren
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [pve-devel] [PATCH docs/firewall/manager/proxmox{-firewall, -perl-rs} v4 0/9] autogenerate ipsets for sdn objects
2024-11-15 12:09 [pve-devel] [PATCH docs/firewall/manager/proxmox{-firewall, -perl-rs} v4 0/9] autogenerate ipsets for sdn objects Stefan Hanreich
` (8 preceding siblings ...)
2024-11-15 12:09 ` [pve-devel] [PATCH pve-docs v4 9/9] sdn: add documentation for firewall integration Stefan Hanreich
@ 2024-11-18 11:47 ` Stefan Hanreich
9 siblings, 0 replies; 24+ messages in thread
From: Stefan Hanreich @ 2024-11-18 11:47 UTC (permalink / raw)
To: pve-devel
superseded by
https://lore.proxmox.com/pve-devel/20241118114134.83882-1-s.hanreich@proxmox.com/T/
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [pve-devel] [PATCH pve-firewall v4 6/9] api: load sdn ipsets
2024-11-18 11:38 ` Thomas Lamprecht
@ 2024-11-18 13:23 ` Thomas Lamprecht
2024-11-18 13:32 ` Stefan Hanreich
0 siblings, 1 reply; 24+ messages in thread
From: Thomas Lamprecht @ 2024-11-18 13:23 UTC (permalink / raw)
To: Proxmox VE development discussion, Stefan Hanreich; +Cc: Wolfgang Bumiller
Am 18.11.24 um 12:38 schrieb Thomas Lamprecht:
> I'll take a short look if it's easily possible to add a sane migration path
> at pmxcfs level, handling this transparently, otherwise we'll have to add
> some compat handling at higher levels.
doing this directly in pmxcfs is not trivial as our link-plug is made for
adding links at the top-level, besides inode updates from other members
running older code would need to take special care, so this route is rather
unfeasible as of now.
I will just add some fallback code in pve-network and be done, for the firewall
uses cases this is a new feature anyway and thus needs all new package versions
anyway to be expected to work.
>
> Korrigieren
>
> Schließen
>
> Rechtschreibung
>
> Possible spelling mistake found.
>
> EveevepiePVPVCIgnorieren
>
btw. I did not have a stroke gere, this was a "select the text from the
LanguageTool pop-up and paste it through middle click by mistake" error.
Sorry for that glitch and thanks @HD to notify me about it – albeit now I'm
thinking that I might have also covered this up as test if my mail get read ;-P
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [pve-devel] [PATCH pve-firewall v4 6/9] api: load sdn ipsets
2024-11-18 13:23 ` Thomas Lamprecht
@ 2024-11-18 13:32 ` Stefan Hanreich
0 siblings, 0 replies; 24+ messages in thread
From: Stefan Hanreich @ 2024-11-18 13:32 UTC (permalink / raw)
To: Proxmox VE development discussion
On 11/18/24 14:23, Thomas Lamprecht wrote:
> Am 18.11.24 um 12:38 schrieb Thomas Lamprecht:
>> I'll take a short look if it's easily possible to add a sane migration path
>> at pmxcfs level, handling this transparently, otherwise we'll have to add
>> some compat handling at higher levels.
>
> doing this directly in pmxcfs is not trivial as our link-plug is made for
> adding links at the top-level, besides inode updates from other members
> running older code would need to take special care, so this route is rather
> unfeasible as of now.
>
> I will just add some fallback code in pve-network and be done, for the firewall
> uses cases this is a new feature anyway and thus needs all new package versions
> anyway to be expected to work.
Too bad to hear, if there's anything I can do wrt this don't hesitate to
ping me.
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 24+ messages in thread
end of thread, other threads:[~2024-11-18 13:32 UTC | newest]
Thread overview: 24+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2024-11-15 12:09 [pve-devel] [PATCH docs/firewall/manager/proxmox{-firewall, -perl-rs} v4 0/9] autogenerate ipsets for sdn objects Stefan Hanreich
2024-11-15 12:09 ` [pve-devel] [PATCH proxmox-firewall v4 1/9] add proxmox-ve-rs crate - move proxmox-ve-config there Stefan Hanreich
2024-11-17 14:08 ` [pve-devel] applied: " Thomas Lamprecht
2024-11-15 12:09 ` [pve-devel] [PATCH proxmox-firewall v4 2/9] config: tests: add support for loading sdn and ipam config Stefan Hanreich
2024-11-17 14:08 ` [pve-devel] applied: " Thomas Lamprecht
2024-11-15 12:09 ` [pve-devel] [PATCH proxmox-firewall v4 3/9] ipsets: autogenerate ipsets for vnets and ipam Stefan Hanreich
2024-11-17 14:08 ` [pve-devel] applied: " Thomas Lamprecht
2024-11-15 12:09 ` [pve-devel] [PATCH pve-firewall v4 4/9] add support for loading sdn firewall configuration Stefan Hanreich
2024-11-17 14:57 ` Thomas Lamprecht
2024-11-18 9:22 ` Stefan Hanreich
2024-11-18 9:35 ` Stefan Hanreich
2024-11-15 12:09 ` [pve-devel] [PATCH pve-firewall v4 5/9] nftables: make is_nftables check flag file instead of config Stefan Hanreich
2024-11-17 14:58 ` [pve-devel] applied: " Thomas Lamprecht
2024-11-15 12:09 ` [pve-devel] [PATCH pve-firewall v4 6/9] api: load sdn ipsets Stefan Hanreich
2024-11-17 14:30 ` Thomas Lamprecht
2024-11-18 9:02 ` Stefan Hanreich
2024-11-18 11:38 ` Thomas Lamprecht
2024-11-18 13:23 ` Thomas Lamprecht
2024-11-18 13:32 ` Stefan Hanreich
2024-11-15 12:09 ` [pve-devel] [PATCH proxmox-perl-rs v4 7/9] add PVE::RS::Firewall::SDN module Stefan Hanreich
2024-11-17 14:36 ` [pve-devel] applied: " Thomas Lamprecht
2024-11-15 12:09 ` [pve-devel] [PATCH pve-manager v4 8/9] firewall: add sdn scope to IPRefSelector Stefan Hanreich
2024-11-15 12:09 ` [pve-devel] [PATCH pve-docs v4 9/9] sdn: add documentation for firewall integration Stefan Hanreich
2024-11-18 11:47 ` [pve-devel] [PATCH docs/firewall/manager/proxmox{-firewall, -perl-rs} v4 0/9] autogenerate ipsets for sdn objects Stefan Hanreich
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox