From: Hannes Laimer <h.laimer@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH proxmox-ve-rs v3 07/45] ve-config: sdn: microseg: add tag matcher
Date: Tue, 27 Oct 2026 09:54:49 +0100 [thread overview]
Message-ID: <20261027085527.702738-8-h.laimer@proxmox.com> (raw)
In-Reply-To: <20261027085527.702738-1-h.laimer@proxmox.com>
Bind the NICs of every guest matching a set of tags to a set of
groups. The tags are read from the guest inventory whenever NICs are
resolved, so a tag change needs no new render of the running config.
Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
proxmox-ve-config/src/sdn/microseg/api.rs | 70 ++++++++++++++++++++-
proxmox-ve-config/src/sdn/microseg/mod.rs | 36 +++++++++--
proxmox-ve-config/src/sdn/microseg/types.rs | 51 ++++++++++++++-
3 files changed, 150 insertions(+), 7 deletions(-)
diff --git a/proxmox-ve-config/src/sdn/microseg/api.rs b/proxmox-ve-config/src/sdn/microseg/api.rs
index 0c89ba27c21e..4f47c1a3ed80 100644
--- a/proxmox-ve-config/src/sdn/microseg/api.rs
+++ b/proxmox-ve-config/src/sdn/microseg/api.rs
@@ -10,7 +10,7 @@ use serde::Deserialize;
use super::types::{
Binding, GroupSection, GuestAssignmentSection, GuestInfo, MicrosegEntry, MicrosegId,
- MicrosegRunningConfig, RuleSection,
+ MicrosegRunningConfig, RuleSection, Tag, TagAssignmentSection,
};
use super::{PredicateMatch, assigned_nics, assignments, sorted_rules, validate, validate_entry};
@@ -20,12 +20,14 @@ pub enum MicrosegCreate {
Group(GroupSection),
Rule(RuleCreate),
GuestAssignment(GuestAssignmentCreate),
+ TagAssignment(TagAssignmentCreate),
}
impl MicrosegCreate {
fn binding(&self) -> Option<&BindingCreate> {
match self {
Self::GuestAssignment(a) => Some(&a.binding),
+ Self::TagAssignment(a) => Some(&a.binding),
Self::Group(_) | Self::Rule(_) => None,
}
}
@@ -83,12 +85,23 @@ pub struct GuestAssignmentCreate {
pub binding: BindingCreate,
}
+#[derive(Debug, Clone, Deserialize)]
+pub struct TagAssignmentCreate {
+ #[serde(default)]
+ pub tags: Vec<Tag>,
+ #[serde(default, rename = "tag-match")]
+ pub tag_match: PredicateMatch,
+ #[serde(flatten)]
+ pub binding: BindingCreate,
+}
+
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum MicrosegUpdate {
Group(GroupUpdate),
Rule(RuleUpdate),
GuestAssignment(AssignmentUpdate),
+ TagAssignment(AssignmentUpdate),
}
#[derive(Debug, Clone, Deserialize)]
@@ -221,6 +234,32 @@ impl Fnv1a {
self.text(group);
}
}
+
+ fn iface(&mut self, iface: Option<u32>) {
+ match iface {
+ Some(iface) => {
+ self.byte(1);
+ self.bytes(&iface.to_be_bytes());
+ }
+ None => self.byte(0),
+ }
+ }
+}
+
+/// FNV-1a over the matcher and the interface, so the bound groups can change under the same id.
+fn tag_assignment_id(
+ tags: &[Tag],
+ tag_match: PredicateMatch,
+ iface: Option<u32>,
+) -> Result<MicrosegId, Error> {
+ let mut sorted: Vec<&str> = tags.iter().map(AsRef::as_ref).collect();
+ sorted.sort_unstable();
+ sorted.dedup();
+ let mut hash = Fnv1a::new();
+ hash.match_kind(tag_match);
+ hash.iface(iface);
+ hash.groups(&sorted);
+ format!("tag{:016x}", hash.0).parse()
}
fn build_entry(
@@ -263,6 +302,19 @@ fn build_entry(
binding: assignment.binding.into(),
})
}
+ MicrosegCreate::TagAssignment(assignment) => {
+ let id = tag_assignment_id(
+ &assignment.tags,
+ assignment.tag_match,
+ assignment.binding.iface,
+ )?;
+ MicrosegEntry::TagAssignment(TagAssignmentSection {
+ id,
+ tags: assignment.tags,
+ tag_match: assignment.tag_match,
+ binding: assignment.binding.into(),
+ })
+ }
})
}
@@ -416,7 +468,8 @@ pub fn update(
}
None
}
- (entry @ MicrosegEntry::GuestAssignment(_), MicrosegUpdate::GuestAssignment(update)) => {
+ (entry @ MicrosegEntry::GuestAssignment(_), MicrosegUpdate::GuestAssignment(update))
+ | (entry @ MicrosegEntry::TagAssignment(_), MicrosegUpdate::TagAssignment(update)) => {
apply_assignment_update(entry, update)
}
_ => bail!("update type does not match the existing object's type"),
@@ -469,6 +522,11 @@ fn describe_rule(rule: &RuleSection) -> String {
/// A human-readable description of an entry for diagnostics, by what a user would recognize rather
/// than by its id.
fn describe_referrer(entry: &MicrosegEntry) -> String {
+ let on_net = |iface: Option<u32>| {
+ iface
+ .map(|iface| format!(" on net{iface}"))
+ .unwrap_or_default()
+ };
let (desc, comment) = match entry {
MicrosegEntry::Rule(rule) => (describe_rule(rule), rule.comment.as_deref()),
MicrosegEntry::GuestAssignment(assignment) => (
@@ -478,6 +536,14 @@ fn describe_referrer(entry: &MicrosegEntry) -> String {
},
assignment.binding.comment.as_deref(),
),
+ MicrosegEntry::TagAssignment(assignment) => (
+ format!(
+ "tag assignment {}{}",
+ predicate_str(assignment.tag_match, &assignment.tags),
+ on_net(assignment.binding.iface),
+ ),
+ assignment.binding.comment.as_deref(),
+ ),
MicrosegEntry::Group(_) => ("object".to_string(), None),
};
match comment {
diff --git a/proxmox-ve-config/src/sdn/microseg/mod.rs b/proxmox-ve-config/src/sdn/microseg/mod.rs
index 0a1b1b69225c..0688d4e58180 100644
--- a/proxmox-ve-config/src/sdn/microseg/mod.rs
+++ b/proxmox-ve-config/src/sdn/microseg/mod.rs
@@ -15,7 +15,7 @@ mod types;
pub use identity::PredicateMatch;
pub use types::{
Binding, GroupName, GroupSection, GuestAssignmentSection, GuestInfo, MicrosegEntry, MicrosegId,
- MicrosegRunningConfig, RuleSection,
+ MicrosegRunningConfig, RuleSection, Tag, TagAssignmentSection,
};
use std::collections::{BTreeMap, BTreeSet, HashMap};
@@ -31,7 +31,7 @@ pub fn validate(entries: &BTreeMap<String, MicrosegEntry>) -> Result<(), Error>
for (name, entry) in entries {
let kind = match entry {
MicrosegEntry::Rule(_) => "rule",
- MicrosegEntry::GuestAssignment(_) => "assignment",
+ MicrosegEntry::GuestAssignment(_) | MicrosegEntry::TagAssignment(_) => "assignment",
MicrosegEntry::Group(_) => "group",
};
validate_entry(entries, &format!("{kind} '{name}'"), entry)?;
@@ -47,6 +47,14 @@ fn validate_entry(
) -> Result<(), Error> {
match entry {
MicrosegEntry::Rule(rule) => validate_rule(entries, subject, rule)?,
+ MicrosegEntry::TagAssignment(assignment) => {
+ if assignment.tags.is_empty() {
+ bail!("{subject} has an empty tag matcher");
+ }
+ if assignment.tag_match == PredicateMatch::Every {
+ bail!("{subject} matches tags with 'every', which only a rule predicate takes");
+ }
+ }
MicrosegEntry::GuestAssignment(_) | MicrosegEntry::Group(_) => {}
}
if let Some(groups) = entry.assignment_groups() {
@@ -186,8 +194,24 @@ fn build_policies(
.collect())
}
-enum Matcher {
+fn tags_fire(tags: &[Tag], tag_match: PredicateMatch, guest: &GuestInfo) -> bool {
+ let has = |tag: &Tag| guest.tags.contains(tag);
+ match tag_match {
+ PredicateMatch::Any => tags.iter().any(has),
+ PredicateMatch::All => tags.iter().all(has),
+ PredicateMatch::Exact => {
+ let want: BTreeSet<&Tag> = tags.iter().collect();
+ let have: BTreeSet<&Tag> = guest.tags.iter().collect();
+ want == have
+ }
+ // validation rejects it on an assignment
+ PredicateMatch::Every => false,
+ }
+}
+
+enum Matcher<'a> {
Guest(u32),
+ Tags(&'a [Tag], PredicateMatch),
}
struct Assignment<'a> {
@@ -195,13 +219,14 @@ struct Assignment<'a> {
order: Option<u32>,
iface: Option<u32>,
groups: &'a [MicrosegId],
- matcher: Matcher,
+ matcher: Matcher<'a>,
}
impl Assignment<'_> {
fn covers(&self, guest: &GuestInfo, iface: u32) -> bool {
let on_guest = match &self.matcher {
Matcher::Guest(vmid) => *vmid == guest.vmid,
+ Matcher::Tags(tags, tag_match) => tags_fire(tags, *tag_match, guest),
};
on_guest && self.iface.is_none_or(|scoped| scoped == iface)
}
@@ -219,6 +244,9 @@ fn assignments(entries: &BTreeMap<String, MicrosegEntry>) -> Vec<Assignment<'_>>
.filter_map(|(id, entry)| {
let (binding, matcher) = match entry {
MicrosegEntry::GuestAssignment(a) => (&a.binding, Matcher::Guest(a.vmid)),
+ MicrosegEntry::TagAssignment(a) => {
+ (&a.binding, Matcher::Tags(&a.tags, a.tag_match))
+ }
MicrosegEntry::Group(_) | MicrosegEntry::Rule(_) => return None,
};
Some(Assignment {
diff --git a/proxmox-ve-config/src/sdn/microseg/types.rs b/proxmox-ve-config/src/sdn/microseg/types.rs
index 5e20ee6ca2df..cf6886267e95 100644
--- a/proxmox-ve-config/src/sdn/microseg/types.rs
+++ b/proxmox-ve-config/src/sdn/microseg/types.rs
@@ -27,7 +27,7 @@ api_string_type! {
pub struct MicrosegId(String);
}
-const GENERATED_ID_REGEX_STR: &str = r"(?:vm[0-9]+(?:i[0-9]+)?|r[0-9a-f]{16})";
+const GENERATED_ID_REGEX_STR: &str = r"(?:vm[0-9]+(?:i[0-9]+)?|(?:r|tag)[0-9a-f]{16})";
const_regex! {
GENERATED_ID_REGEX = concatcp!(r"^", GENERATED_ID_REGEX_STR, r"$");
@@ -56,6 +56,21 @@ api_string_type! {
pub struct GroupName(String);
}
+const MICROSEG_TAG_REGEX_STR: &str = r"[a-zA-Z0-9_][a-zA-Z0-9_+.\-]*";
+
+const_regex! {
+ pub MICROSEG_TAG_REGEX = concatcp!(r"^", MICROSEG_TAG_REGEX_STR, r"$");
+}
+
+pub const MICROSEG_TAG_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&MICROSEG_TAG_REGEX);
+
+api_string_type! {
+ /// A guest tag a tag assignment matches on.
+ #[api(format: &MICROSEG_TAG_FORMAT)]
+ #[derive(Debug, Deserialize, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
+ pub struct Tag(String);
+}
+
#[api(
properties: {
comment: {
@@ -222,6 +237,33 @@ pub struct GuestAssignmentSection {
pub(crate) binding: Binding,
}
+#[api(
+ properties: {
+ tags: {
+ type: Array,
+ description: "Guest tags to match on, compared exactly as the guest carries them.",
+ items: { type: Tag },
+ },
+ "tag-match": {
+ type: PredicateMatch,
+ optional: true,
+ },
+ binding: { flatten: true, type: Binding },
+ },
+)]
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+/// Binds the NICs of every guest matching `tags` (by `tag_match`) to a set of groups. Stored as the
+/// `tag-assignment` section type.
+pub struct TagAssignmentSection {
+ pub(crate) id: MicrosegId,
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ pub(crate) tags: Vec<Tag>,
+ #[serde(default, rename = "tag-match")]
+ pub(crate) tag_match: PredicateMatch,
+ #[serde(flatten)]
+ pub(crate) binding: Binding,
+}
+
#[api(
"id-property": "id",
"id-schema": {
@@ -241,6 +283,8 @@ pub enum MicrosegEntry {
Rule(RuleSection),
/// An assignment by guest id.
GuestAssignment(GuestAssignmentSection),
+ /// An assignment by guest tags.
+ TagAssignment(TagAssignmentSection),
}
impl MicrosegEntry {
@@ -250,12 +294,14 @@ impl MicrosegEntry {
Self::Group(g) => &g.id,
Self::Rule(r) => &r.id,
Self::GuestAssignment(a) => &a.id,
+ Self::TagAssignment(a) => &a.id,
}
}
pub(super) fn binding(&self) -> Option<&Binding> {
match self {
Self::GuestAssignment(a) => Some(&a.binding),
+ Self::TagAssignment(a) => Some(&a.binding),
Self::Group(_) | Self::Rule(_) => None,
}
}
@@ -263,6 +309,7 @@ impl MicrosegEntry {
pub(super) fn binding_mut(&mut self) -> Option<&mut Binding> {
match self {
Self::GuestAssignment(a) => Some(&mut a.binding),
+ Self::TagAssignment(a) => Some(&mut a.binding),
Self::Group(_) | Self::Rule(_) => None,
}
}
@@ -290,6 +337,8 @@ where
pub struct GuestInfo {
#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u32")]
pub vmid: u32,
+ #[serde(default)]
+ pub tags: Vec<Tag>,
/// The present netN interface indices.
#[serde(default, deserialize_with = "deserialize_perl_nics")]
pub nics: Vec<u32>,
--
2.47.3
next prev parent reply other threads:[~2026-10-27 8:56 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 ` Hannes Laimer [this message]
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 ` [PATCH proxmox-ve-rs v3 14/45] microseg: add a rule option that matches non-first fragments Hannes Laimer
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-8-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.