all lists on 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 v3 14/45] microseg: add a rule option that matches non-first fragments
Date: Tue, 27 Oct 2026 09:54:56 +0100	[thread overview]
Message-ID: <20261027085527.702738-15-h.laimer@proxmox.com> (raw)
In-Reply-To: <20261027085527.702738-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 02ea16d5a2ef..717e02fe3ff6 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)
@@ -210,6 +221,7 @@ mod tests {
             }],
             checks: vec![Check {
                 protocols,
+                fragments: false,
                 allow: true,
             }],
             nics: Vec::new(),
diff --git a/proxmox-ve-config/src/sdn/microseg/api.rs b/proxmox-ve-config/src/sdn/microseg/api.rs
index d9cd22344cb5..650658fe0f13 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 be48b890206f..c3f776d1226a 100644
--- a/proxmox-ve-config/src/sdn/microseg/mod.rs
+++ b/proxmox-ve-config/src/sdn/microseg/mod.rs
@@ -588,28 +588,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,
             })
     }
@@ -644,6 +642,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 {
@@ -653,6 +652,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);
@@ -921,6 +921,7 @@ mod tests {
                 dport: None,
                 icmp_type: None,
                 fw_macro: None,
+                fragments: false,
                 comment: None,
             }),
         )
@@ -1040,6 +1041,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 1392860f5062..ce733de9b37d 100644
--- a/proxmox-ve-config/src/sdn/microseg/state.rs
+++ b/proxmox-ve-config/src/sdn/microseg/state.rs
@@ -43,6 +43,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_pairs(firing: &[(u16, u16, &ResolvedRule)]) -> (Vec<Pair>, Vec<Check>) {
                     .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-10-27  8:57 UTC|newest]

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