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 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 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