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