public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Hannes Laimer <h.laimer@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH proxmox-ve-rs v4 14/45] microseg: add a rule option that matches non-first fragments
Date: Thu,  5 Nov 2026 12:12:53 +0100	[thread overview]
Message-ID: <20261105111324.706922-15-h.laimer@proxmox.com> (raw)
In-Reply-To: <20261105111324.706922-1-h.laimer@proxmox.com>

A narrowed rule matches only frames with a transport header. Only the
first fragment of a datagram carries one. So a pair allowed only
through narrowed rules dropped every later fragment.

A rule can now be narrowed to non-first fragments, of one protocol or
of every protocol. Like any narrowed rule it decides only what it
matches. The only other narrowing it accepts is a protocol, since a
fragment carries no ports and no ICMP type.

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 proxmox-microseg/src/ebpf.rs                | 12 +++++
 proxmox-ve-config/src/sdn/microseg/api.rs   |  7 +++
 proxmox-ve-config/src/sdn/microseg/mod.rs   | 50 ++++++++++++++++-----
 proxmox-ve-config/src/sdn/microseg/state.rs |  4 ++
 proxmox-ve-config/src/sdn/microseg/types.rs | 27 +++++++++--
 5 files changed, 85 insertions(+), 15 deletions(-)

diff --git a/proxmox-microseg/src/ebpf.rs b/proxmox-microseg/src/ebpf.rs
index b057c4c51160..370045a502dc 100644
--- a/proxmox-microseg/src/ebpf.rs
+++ b/proxmox-microseg/src/ebpf.rs
@@ -88,6 +88,17 @@ fn check_entries(state: &DesiredState) -> anyhow::Result<Vec<Vec<CheckEntry>>> {
         for protocol in &check.protocols {
             entries.extend(protocol_entries(protocol, check.allow)?);
         }
+        if check.fragments {
+            if entries.is_empty() {
+                entries.push(CheckEntry {
+                    allow: check.allow,
+                    ..Default::default()
+                });
+            }
+            for fragment_entry in &mut entries {
+                fragment_entry.fragment = true;
+            }
+        }
         checks.push(entries);
     }
     Ok(checks)
@@ -211,6 +222,7 @@ mod tests {
             }],
             checks: vec![Check {
                 protocols,
+                fragments: false,
                 allow: true,
             }],
             lists: vec![vec![0]],
diff --git a/proxmox-ve-config/src/sdn/microseg/api.rs b/proxmox-ve-config/src/sdn/microseg/api.rs
index 5efeb8c09a7b..1f398de67063 100644
--- a/proxmox-ve-config/src/sdn/microseg/api.rs
+++ b/proxmox-ve-config/src/sdn/microseg/api.rs
@@ -63,6 +63,8 @@ pub struct RuleCreate {
     pub icmp_type: Option<String>,
     #[serde(default, rename = "macro")]
     pub fw_macro: Option<String>,
+    #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    pub fragments: bool,
     #[serde(default)]
     pub comment: Option<String>,
 }
@@ -211,6 +213,7 @@ fn rule_id(
     dst_match: PredicateMatch,
     dst_groups: &[&str],
     narrowing: &[Option<&str>],
+    fragments: bool,
     bidirectional: bool,
 ) -> Result<MicrosegId, Error> {
     let mut hash = Fnv1a::new();
@@ -221,6 +224,7 @@ fn rule_id(
     for field in narrowing {
         hash.text(field.unwrap_or_default());
     }
+    hash.flag(fragments);
     hash.flag(bidirectional);
     format!("r{:016x}", hash.0).parse()
 }
@@ -332,6 +336,7 @@ fn build_entry(
                 rule.dst_match,
                 &dst_groups,
                 &narrowing,
+                rule.fragments,
                 rule.bidirectional,
             )?;
             MicrosegEntry::Rule(RuleSection {
@@ -348,6 +353,7 @@ fn build_entry(
                 dport,
                 icmp_type,
                 fw_macro,
+                fragments: rule.fragments,
                 comment: rule.comment,
             })
         }
@@ -598,6 +604,7 @@ fn narrowing_str(rule: &RuleSection) -> Option<String> {
         rule.sport.as_ref().map(|ports| format!("sport {ports}")),
         rule.dport.as_ref().map(|ports| format!("dport {ports}")),
         rule.icmp_type.as_ref().map(|kind| format!("type {kind}")),
+        rule.fragments.then(|| "fragments".to_string()),
     ];
     let narrowing = parts.into_iter().flatten().collect::<Vec<_>>().join(" ");
     (!narrowing.is_empty()).then_some(narrowing)
diff --git a/proxmox-ve-config/src/sdn/microseg/mod.rs b/proxmox-ve-config/src/sdn/microseg/mod.rs
index e42aecac8bfe..ed8b18b838fe 100644
--- a/proxmox-ve-config/src/sdn/microseg/mod.rs
+++ b/proxmox-ve-config/src/sdn/microseg/mod.rs
@@ -615,28 +615,26 @@ struct ResolvedRule {
     dst: identity::Predicate,
     prio: u16,
     allow: bool,
-    /// The protocol matches narrowing the rule. Empty for a rule deciding the whole pair.
     protocols: Vec<Protocol>,
-    /// Set on the mirrored orientation of a bidirectional rule.
+    fragments: bool,
     reverse: bool,
 }
 
 impl ResolvedRule {
     fn is_narrowed(&self) -> bool {
-        !self.protocols.is_empty()
+        self.fragments || !self.protocols.is_empty()
     }
 
-    /// A narrowed deny that can match neighbor discovery.
+    /// A narrowed deny that can match neighbor discovery. A non-first fragment carries no ICMPv6
+    /// header, so a fragments rule cannot.
     fn can_deny_nd(&self) -> bool {
         !self.allow
+            && !self.fragments
             && self.protocols.iter().any(|protocol| match protocol {
-                Protocol::Icmpv6(icmp) => matches!(
-                    icmp.ty(),
-                    None | Some(Icmpv6Type::Any | Icmpv6Type::Numeric(135 | 136))
-                        | Some(Icmpv6Type::Named(
-                            "nd-neighbor-solicit" | "nd-neighbor-advert"
-                        ))
-                ),
+                Protocol::Icmpv6(icmp) => icmp
+                    .ty()
+                    .and_then(Icmpv6Type::number)
+                    .is_none_or(|ty| ty == 135 || ty == 136),
                 _ => false,
             })
     }
@@ -671,6 +669,7 @@ fn resolved_rules(entries: &BTreeMap<String, MicrosegEntry>) -> Result<Vec<Resol
             prio: rule.prio,
             allow: rule.allow,
             protocols: rule.protocols().with_context(context)?,
+            fragments: rule.fragments,
             reverse: false,
         };
         let mirrored = rule.bidirectional.then(|| ResolvedRule {
@@ -680,6 +679,7 @@ fn resolved_rules(entries: &BTreeMap<String, MicrosegEntry>) -> Result<Vec<Resol
             prio: forward.prio,
             allow: forward.allow,
             protocols: forward.protocols.iter().map(reversed_protocol).collect(),
+            fragments: forward.fragments,
             reverse: true,
         });
         resolved.push(forward);
@@ -948,6 +948,7 @@ mod tests {
                 dport: None,
                 icmp_type: None,
                 fw_macro: None,
+                fragments: false,
                 comment: None,
             }),
         )
@@ -1067,6 +1068,33 @@ mod tests {
         assert!(!folded.allow);
     }
 
+    #[test]
+    fn a_fragments_rule_decides_the_fragments_only() {
+        let with = |modify: fn(&mut RuleSection)| {
+            BTreeMap::from([
+                group("web"),
+                group("db"),
+                with_rule(rule("frag", &["web"], &["db"], true), modify),
+            ])
+        };
+        let entries = with(|rule| rule.fragments = true);
+        validate(&entries).expect("valid");
+        let rules = resolved(&entries);
+        let folded = fold(firing_rules(&rules, &set(&["web"]), &set(&["db"])));
+        assert!(folded.checks[0].fragments);
+        assert!(
+            !folded.allow,
+            "the allow is for the fragments, not the pair"
+        );
+
+        let with_ports = with(|rule| {
+            rule.fragments = true;
+            rule.proto = Some("tcp".to_string());
+            rule.dport = Some("22".to_string());
+        });
+        validate(&with_ports).expect_err("a fragment carries no ports");
+    }
+
     #[test]
     fn bidirectional_narrowing_swaps_the_ports() {
         let tcp = |sport: Option<&str>, dport: Option<&str>| {
diff --git a/proxmox-ve-config/src/sdn/microseg/state.rs b/proxmox-ve-config/src/sdn/microseg/state.rs
index 8e1c14f3d102..a1a2b705c11f 100644
--- a/proxmox-ve-config/src/sdn/microseg/state.rs
+++ b/proxmox-ve-config/src/sdn/microseg/state.rs
@@ -47,6 +47,9 @@ pub struct Pair {
 #[derive(Debug)]
 pub struct Check {
     pub protocols: Vec<Protocol>,
+    /// The check matches non-first fragments, those of `protocols` or of every protocol when there
+    /// is none.
+    pub fragments: bool,
     pub allow: bool,
 }
 
@@ -273,6 +276,7 @@ fn fold_candidates(
                     .or_insert_with(|| {
                         checks.push(Check {
                             protocols: rule.protocols.clone(),
+                            fragments: rule.fragments,
                             allow: rule.allow,
                         });
                         checks.len() - 1
diff --git a/proxmox-ve-config/src/sdn/microseg/types.rs b/proxmox-ve-config/src/sdn/microseg/types.rs
index 39f80764e5e0..a8c3fcb0557b 100644
--- a/proxmox-ve-config/src/sdn/microseg/types.rs
+++ b/proxmox-ve-config/src/sdn/microseg/types.rs
@@ -165,6 +165,10 @@ pub struct GroupSection {
             max_length: 128,
             description: "Firewall macro standing in for the protocol and ports.",
         },
+        fragments: {
+            type: Boolean,
+            optional: true,
+        },
         comment: {
             type: String,
             optional: true,
@@ -179,7 +183,8 @@ pub struct GroupSection {
 /// the evaluation order decides first. Traffic no rule decides is denied.
 ///
 /// A rule may be narrowed to a protocol with ports or an ICMP type, or to a firewall macro that
-/// stands in for them.
+/// stands in for them. A non-first fragment carries no transport header, so among the
+/// narrowed rules only one narrowed to fragments matches it.
 pub struct RuleSection {
     pub(crate) id: MicrosegId,
     /// Groups the source predicate lists.
@@ -218,18 +223,32 @@ pub struct RuleSection {
     pub(crate) icmp_type: Option<String>,
     #[serde(default, rename = "macro", skip_serializing_if = "Option::is_none")]
     pub(crate) fw_macro: Option<String>,
+    /// Whether the rule matches non-first fragments, those of `proto` or of every protocol.
+    #[serde(
+        default,
+        deserialize_with = "proxmox_serde::perl::deserialize_bool",
+        skip_serializing_if = "is_false"
+    )]
+    pub(crate) fragments: bool,
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub(crate) comment: Option<String>,
 }
 
 impl RuleSection {
-    /// The protocol matches narrowing this rule, empty for a rule that decides the whole pair.
-    /// Ports are only accepted with a port-carrying protocol and an ICMP type only with an ICMP
-    /// protocol, since either would otherwise be silently ignored.
+    /// The protocol matches narrowing this rule, empty for a rule that decides the whole pair or
+    /// matches the fragments of every protocol. Ports without a port-carrying protocol and an
+    /// ICMP type without an ICMP protocol return Err, since the protocol match would ignore
+    /// them.
     pub(super) fn protocols(&self) -> Result<Vec<Protocol>, Error> {
         let explicit = [&self.proto, &self.sport, &self.dport, &self.icmp_type]
             .iter()
             .any(|field| field.is_some());
+        let beyond_proto = [&self.sport, &self.dport, &self.icmp_type, &self.fw_macro]
+            .iter()
+            .any(|field| field.is_some());
+        if self.fragments && beyond_proto {
+            bail!("a fragment carries no ports and no ICMP type, so only a protocol can narrow it");
+        }
         if let Some(name) = &self.fw_macro {
             if explicit {
                 bail!("a macro cannot be combined with a protocol, ports or an ICMP type");
-- 
2.47.3





  parent reply	other threads:[~2026-11-05 11:15 UTC|newest]

Thread overview: 46+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-05 11:12 [PATCH access-control/cluster/container/docs/manager/network/proxmox{,-ve-rs,-perl-rs}/qemu-server v4 00/45] sdn: add microsegmentation support Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox v4 01/45] ebpf: build: keep the build directory out of the BPF objects Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox v4 02/45] ebpf: policy: add the enforcement subsystem for guest interfaces Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox v4 03/45] ebpf: policy: add a check for non-first fragments Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox-ve-rs v4 04/45] ve-config: firewall: add a constructor for sctp matches Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox-ve-rs v4 05/45] ve-config: sdn: microseg: add identity allocation for group sets Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox-ve-rs v4 06/45] ve-config: sdn: microseg: add config types Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox-ve-rs v4 07/45] ve-config: sdn: microseg: add tag matcher Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox-ve-rs v4 08/45] ve-config: sdn: microseg: add name regex matcher Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox-ve-rs v4 09/45] ve-config: sdn: microseg: narrow rules by protocol and ports Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox-ve-rs v4 10/45] ve-config: sdn: microseg: add group hierarchy Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox-ve-rs v4 11/45] ve-config: firewall: expose protocol, icmp type and code numbers Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox-ve-rs v4 12/45] ve-config: sdn: microseg: add the desired state Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox-ve-rs v4 13/45] microseg: add a crate that applies the policy to a node's guest NICs Hannes Laimer
2026-11-05 11:12 ` Hannes Laimer [this message]
2026-11-05 11:12 ` [PATCH pve-cluster v4 15/45] cfs: add 'sdn/microseg.cfg' to observed files Hannes Laimer
2026-11-05 11:12 ` [PATCH pve-access-control v4 16/45] permissions: add ACL path for SDN microsegmentation Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox-perl-rs v4 17/45] pve-rs: sdn: add microseg config binding Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox-perl-rs v4 18/45] pve-rs: sdn: microseg: bind the data plane Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox-perl-rs v4 19/45] pve-rs: sdn: microseg: check the data plane limits on render Hannes Laimer
2026-11-05 11:12 ` [PATCH proxmox-perl-rs v4 20/45] pve-rs: debian: fire the pve-microseg-update trigger on upgrade Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-network v4 21/45] sdn: zones: add vxlan-gbp option to vxlan and evpn zones Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-network v4 22/45] sdn: microseg: add config, API and guest inventory Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-network v4 23/45] sdn: dry-run: show pending microseg changes Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-network v4 24/45] sdn: microseg: enforce the policy on guest NICs Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-network v4 25/45] sdn: microseg: add tag matcher Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-network v4 26/45] sdn: microseg: add name regex matcher Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-network v4 27/45] sdn: microseg: add an update for a running NIC Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-network v4 28/45] sdn: microseg: expose protocol, port and macro rule options Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-network v4 29/45] sdn: microseg: allow nesting groups under a parent Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-network v4 30/45] sdn: microseg: expose the fragments rule option Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-manager v4 31/45] network: apply the microseg policy on reload Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-manager v4 32/45] pve-sdn-commit: apply the microseg policy at boot Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-manager v4 33/45] ui: sdn: zones: add vxlan-gbp checkbox to vxlan and evpn Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-manager v4 34/45] ui: sdn: add microsegmentation panel Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-manager v4 35/45] ui: sdn: dry-run: show pending microseg diff Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-manager v4 36/45] ui: sdn: microseg: add tag matcher Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-manager v4 37/45] ui: sdn: microseg: add name regex matcher Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-manager v4 38/45] ui: sdn: microseg: add narrowing to the rule form Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-manager v4 39/45] ui: sdn: microseg: show and edit the group hierarchy Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-manager v4 40/45] ui: sdn: microseg: add fragments option to the rule form Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-manager v4 41/45] ui: sdn: microseg: add a guest tab with its groups and rules Hannes Laimer
2026-11-05 11:13 ` [PATCH qemu-server v4 42/45] network: update the microseg enforcement on a tag or name change Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-container v4 43/45] lxc: update the microseg enforcement on a tag or hostname change Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-docs v4 44/45] sdn: zones: document the VXLAN-GBP option Hannes Laimer
2026-11-05 11:13 ` [PATCH pve-docs v4 45/45] sdn: add microsegmentation section Hannes Laimer

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261105111324.706922-15-h.laimer@proxmox.com \
    --to=h.laimer@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal