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