From: Hannes Laimer <h.laimer@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH proxmox-ve-rs v4 08/45] ve-config: sdn: microseg: add name regex matcher
Date: Thu, 5 Nov 2026 12:12:47 +0100 [thread overview]
Message-ID: <20261105111324.706922-9-h.laimer@proxmox.com> (raw)
In-Reply-To: <20261105111324.706922-1-h.laimer@proxmox.com>
Assign groups to the NICs of every guest whose name matches a regular
expression. Like guest tags, the names come from the guest inventory
and never enter the running config. A rename takes effect the next
time the NICs are resolved. The expression has to be ASCII, like the
names it is matched against.
A query tells whether renaming a guest changes which expressions match
its name. It uses the same regex engine as the matcher, so a caller
does not have to match the expressions in a dialect of its own.
Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
proxmox-ve-config/src/sdn/microseg/api.rs | 42 +++++++++++++++-
proxmox-ve-config/src/sdn/microseg/mod.rs | 53 ++++++++++++++++++++-
proxmox-ve-config/src/sdn/microseg/types.rs | 30 +++++++++++-
3 files changed, 120 insertions(+), 5 deletions(-)
diff --git a/proxmox-ve-config/src/sdn/microseg/api.rs b/proxmox-ve-config/src/sdn/microseg/api.rs
index 977711778d48..305ecce3828b 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, Tag, TagAssignmentSection,
+ MicrosegRunningConfig, RegexAssignmentSection, RuleSection, Tag, TagAssignmentSection,
};
use super::{PredicateMatch, assigned_nics, assignments, sorted_rules, validate, validate_entry};
@@ -21,6 +21,7 @@ pub enum MicrosegCreate {
Rule(RuleCreate),
GuestAssignment(GuestAssignmentCreate),
TagAssignment(TagAssignmentCreate),
+ RegexAssignment(RegexAssignmentCreate),
}
impl MicrosegCreate {
@@ -28,6 +29,7 @@ impl MicrosegCreate {
match self {
Self::GuestAssignment(a) => Some(&a.binding),
Self::TagAssignment(a) => Some(&a.binding),
+ Self::RegexAssignment(a) => Some(&a.binding),
Self::Group(_) | Self::Rule(_) => None,
}
}
@@ -95,6 +97,14 @@ pub struct TagAssignmentCreate {
pub binding: BindingCreate,
}
+#[derive(Debug, Clone, Deserialize)]
+pub struct RegexAssignmentCreate {
+ #[serde(default, rename = "name-regex")]
+ pub name_regex: String,
+ #[serde(flatten)]
+ pub binding: BindingCreate,
+}
+
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum MicrosegUpdate {
@@ -102,6 +112,7 @@ pub enum MicrosegUpdate {
Rule(RuleUpdate),
GuestAssignment(AssignmentUpdate),
TagAssignment(AssignmentUpdate),
+ RegexAssignment(AssignmentUpdate),
}
#[derive(Debug, Clone, Deserialize)]
@@ -262,6 +273,13 @@ fn tag_assignment_id(
format!("tag{:016x}", hash.0).parse()
}
+fn regex_assignment_id(name_regex: &str, iface: Option<u32>) -> Result<MicrosegId, Error> {
+ let mut hash = Fnv1a::new();
+ hash.iface(iface);
+ hash.text(name_regex);
+ format!("re{:016x}", hash.0).parse()
+}
+
fn build_entry(
payload: MicrosegCreate,
entries: &BTreeMap<String, MicrosegEntry>,
@@ -315,6 +333,17 @@ fn build_entry(
binding: assignment.binding.into(),
})
}
+ MicrosegCreate::RegexAssignment(assignment) => {
+ // section-config trims every property value on read, so an untrimmed regex would
+ // come back under an id derived from text the entry no longer holds
+ let name_regex = assignment.name_regex.trim().to_string();
+ let id = regex_assignment_id(&name_regex, assignment.binding.iface)?;
+ MicrosegEntry::RegexAssignment(RegexAssignmentSection {
+ id,
+ name_regex,
+ binding: assignment.binding.into(),
+ })
+ }
})
}
@@ -469,7 +498,8 @@ pub fn update(
None
}
(entry @ MicrosegEntry::GuestAssignment(_), MicrosegUpdate::GuestAssignment(update))
- | (entry @ MicrosegEntry::TagAssignment(_), MicrosegUpdate::TagAssignment(update)) => {
+ | (entry @ MicrosegEntry::TagAssignment(_), MicrosegUpdate::TagAssignment(update))
+ | (entry @ MicrosegEntry::RegexAssignment(_), MicrosegUpdate::RegexAssignment(update)) => {
apply_assignment_update(entry, update)
}
_ => bail!("update type does not match the existing object's type"),
@@ -544,6 +574,14 @@ fn describe_referrer(entry: &MicrosegEntry) -> String {
),
assignment.binding.comment.as_deref(),
),
+ MicrosegEntry::RegexAssignment(assignment) => (
+ format!(
+ "regex assignment /{}/{}",
+ assignment.name_regex,
+ 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 7ff7ced3306c..43e87449b039 100644
--- a/proxmox-ve-config/src/sdn/microseg/mod.rs
+++ b/proxmox-ve-config/src/sdn/microseg/mod.rs
@@ -15,13 +15,14 @@ mod types;
pub use identity::PredicateMatch;
pub use types::{
Binding, GroupName, GroupSection, GuestAssignmentSection, GuestInfo, MicrosegEntry, MicrosegId,
- MicrosegRunningConfig, RuleSection, Tag, TagAssignmentSection,
+ MicrosegRunningConfig, RegexAssignmentSection, RuleSection, Tag, TagAssignmentSection,
};
use std::collections::{BTreeMap, BTreeSet, HashMap};
use anyhow::{Context, Error, bail};
use identity::UNTAGGED_GROUP;
+use regex::Regex;
use serde::Serialize;
/// Validate a microseg config before anything renders from it, so resolving group names and
@@ -31,7 +32,9 @@ pub fn validate(entries: &BTreeMap<String, MicrosegEntry>) -> Result<(), Error>
for (name, entry) in entries {
let kind = match entry {
MicrosegEntry::Rule(_) => "rule",
- MicrosegEntry::GuestAssignment(_) | MicrosegEntry::TagAssignment(_) => "assignment",
+ MicrosegEntry::GuestAssignment(_)
+ | MicrosegEntry::TagAssignment(_)
+ | MicrosegEntry::RegexAssignment(_) => "assignment",
MicrosegEntry::Group(_) => "group",
};
validate_entry(entries, &format!("{kind} '{name}'"), entry)?;
@@ -55,6 +58,17 @@ fn validate_entry(
bail!("{subject} matches tags with 'every', which only a rule predicate takes");
}
}
+ MicrosegEntry::RegexAssignment(assignment) => {
+ if assignment.name_regex.is_empty() {
+ bail!("{subject} has an empty name regex");
+ }
+ if !assignment.name_regex.is_ascii() {
+ bail!("{subject} has a name regex with non-ASCII characters");
+ }
+ if let Err(err) = Regex::new(&assignment.name_regex) {
+ bail!("{subject} has an invalid name regex: {err}");
+ }
+ }
MicrosegEntry::GuestAssignment(_) | MicrosegEntry::Group(_) => {}
}
if let Some(groups) = entry.assignment_groups() {
@@ -212,6 +226,9 @@ fn tags_fire(tags: &[Tag], tag_match: PredicateMatch, guest_tags: &[Tag]) -> boo
enum Matcher<'a> {
Guest(u32),
Tags(&'a [Tag], PredicateMatch),
+ /// `None` for a regex that does not compile, which validation rejects, so it fires on nothing
+ /// rather than failing the resolve.
+ Name(Option<Regex>),
}
struct Assignment<'a> {
@@ -227,6 +244,12 @@ impl Assignment<'_> {
let on_guest = match &self.matcher {
Matcher::Guest(vmid) => *vmid == guest.vmid,
Matcher::Tags(tags, tag_match) => tags_fire(tags, *tag_match, &guest.tags),
+ Matcher::Name(regex) => regex.as_ref().is_some_and(|regex| {
+ guest
+ .name
+ .as_deref()
+ .is_some_and(|name| regex.is_match(name))
+ }),
};
on_guest && self.iface.is_none_or(|scoped| scoped == iface)
}
@@ -247,6 +270,9 @@ fn assignments(entries: &BTreeMap<String, MicrosegEntry>) -> Vec<Assignment<'_>>
MicrosegEntry::TagAssignment(a) => {
(&a.binding, Matcher::Tags(&a.tags, a.tag_match))
}
+ MicrosegEntry::RegexAssignment(a) => {
+ (&a.binding, Matcher::Name(Regex::new(&a.name_regex).ok()))
+ }
MicrosegEntry::Group(_) | MicrosegEntry::Rule(_) => return None,
};
Some(Assignment {
@@ -388,6 +414,29 @@ pub fn render(
})
}
+/// Whether a guest renamed from `old` to `new` enters or leaves the names a regex assignment
+/// matches, decided by the same expressions that resolve the NICs.
+pub fn name_match_changed(
+ entries: &BTreeMap<String, MicrosegEntry>,
+ old: Option<&str>,
+ new: Option<&str>,
+) -> bool {
+ assignments(entries)
+ .iter()
+ .any(|assignment| match &assignment.matcher {
+ Matcher::Name(regex) => {
+ let matches = |name: Option<&str>| {
+ regex
+ .as_ref()
+ .zip(name)
+ .is_some_and(|(regex, name)| regex.is_match(name))
+ };
+ matches(old) != matches(new)
+ }
+ _ => false,
+ })
+}
+
/// Whether a guest whose tags change from `old` to `new` enters or leaves the guests a tag
/// assignment matches, decided by the same match that resolves the NICs, so an exact match sees
/// a tag the assignment does not name.
diff --git a/proxmox-ve-config/src/sdn/microseg/types.rs b/proxmox-ve-config/src/sdn/microseg/types.rs
index cf6886267e95..18367490e076 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|tag)[0-9a-f]{16})";
+const GENERATED_ID_REGEX_STR: &str = r"(?:vm[0-9]+(?:i[0-9]+)?|(?:r|tag|re)[0-9a-f]{16})";
const_regex! {
GENERATED_ID_REGEX = concatcp!(r"^", GENERATED_ID_REGEX_STR, r"$");
@@ -264,6 +264,27 @@ pub struct TagAssignmentSection {
pub(crate) binding: Binding,
}
+#[api(
+ properties: {
+ "name-regex": {
+ type: String,
+ max_length: 1024,
+ description: "Regular expression matched against each guest's name, ASCII only. It matches anywhere in the name, so anchor it with '^' and '$' to match the whole name. A guest without a name is never matched.",
+ },
+ binding: { flatten: true, type: Binding },
+ },
+)]
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+/// Binds the NICs of every guest whose name matches `name_regex` to a set of groups. Stored as the
+/// `regex-assignment` section type.
+pub struct RegexAssignmentSection {
+ pub(crate) id: MicrosegId,
+ #[serde(default, rename = "name-regex")]
+ pub(crate) name_regex: String,
+ #[serde(flatten)]
+ pub(crate) binding: Binding,
+}
+
#[api(
"id-property": "id",
"id-schema": {
@@ -285,6 +306,8 @@ pub enum MicrosegEntry {
GuestAssignment(GuestAssignmentSection),
/// An assignment by guest tags.
TagAssignment(TagAssignmentSection),
+ /// An assignment by guest name regex.
+ RegexAssignment(RegexAssignmentSection),
}
impl MicrosegEntry {
@@ -295,6 +318,7 @@ impl MicrosegEntry {
Self::Rule(r) => &r.id,
Self::GuestAssignment(a) => &a.id,
Self::TagAssignment(a) => &a.id,
+ Self::RegexAssignment(a) => &a.id,
}
}
@@ -302,6 +326,7 @@ impl MicrosegEntry {
match self {
Self::GuestAssignment(a) => Some(&a.binding),
Self::TagAssignment(a) => Some(&a.binding),
+ Self::RegexAssignment(a) => Some(&a.binding),
Self::Group(_) | Self::Rule(_) => None,
}
}
@@ -310,6 +335,7 @@ impl MicrosegEntry {
match self {
Self::GuestAssignment(a) => Some(&mut a.binding),
Self::TagAssignment(a) => Some(&mut a.binding),
+ Self::RegexAssignment(a) => Some(&mut a.binding),
Self::Group(_) | Self::Rule(_) => None,
}
}
@@ -339,6 +365,8 @@ pub struct GuestInfo {
pub vmid: u32,
#[serde(default)]
pub tags: Vec<Tag>,
+ #[serde(default)]
+ pub name: Option<String>,
/// 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-11-05 11:14 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 ` Hannes Laimer [this message]
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 ` [PATCH proxmox-ve-rs v4 14/45] microseg: add a rule option that matches non-first fragments Hannes Laimer
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-9-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.