From: Hannes Laimer <h.laimer@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH proxmox-ve-rs v3 13/45] microseg: add a crate that applies the policy to a node's guest NICs
Date: Tue, 27 Oct 2026 09:54:55 +0100 [thread overview]
Message-ID: <20261027085527.702738-14-h.laimer@proxmox.com> (raw)
In-Reply-To: <20261027085527.702738-1-h.laimer@proxmox.com>
The crate hands the microseg policy of the SDN running config to the
policy subsystem of proxmox-ebpf. A full pass applies it to every
guest NIC on the node, a plug to a single NIC that is about to come
up. A validation needs only the config and tells whether the data
plane can hold it.
Only the microseg block of the running config is parsed. A plug runs
for every guest NIC, so a zone or DHCP type that this node's
proxmox-ve-config does not know yet must not keep guests from
starting. The SDN apply on such a node fails either way.
A guest's NICs come from the interfaces on the node and not from its
config, since the config names a hot-plugged NIC only after the
hotplug.
Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
Cargo.toml | 2 +
proxmox-microseg/Cargo.toml | 20 +++
proxmox-microseg/debian/changelog | 5 +
proxmox-microseg/debian/control | 46 +++++
proxmox-microseg/debian/copyright | 18 ++
proxmox-microseg/debian/debcargo.toml | 7 +
proxmox-microseg/debian/source/format | 1 +
proxmox-microseg/src/ebpf.rs | 235 +++++++++++++++++++++++++
proxmox-microseg/src/lib.rs | 66 +++++++
proxmox-microseg/src/nic.rs | 120 +++++++++++++
proxmox-microseg/src/running_config.rs | 26 +++
11 files changed, 546 insertions(+)
create mode 100644 proxmox-microseg/Cargo.toml
create mode 100644 proxmox-microseg/debian/changelog
create mode 100644 proxmox-microseg/debian/control
create mode 100644 proxmox-microseg/debian/copyright
create mode 100644 proxmox-microseg/debian/debcargo.toml
create mode 100644 proxmox-microseg/debian/source/format
create mode 100644 proxmox-microseg/src/ebpf.rs
create mode 100644 proxmox-microseg/src/lib.rs
create mode 100644 proxmox-microseg/src/nic.rs
create mode 100644 proxmox-microseg/src/running_config.rs
diff --git a/Cargo.toml b/Cargo.toml
index 4655b178b47a..d38e48897216 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,6 +3,7 @@ members = [
"proxmox-ve-config",
"proxmox-frr",
"proxmox-sdn-types",
+ "proxmox-microseg",
]
exclude = [
"build",
@@ -29,3 +30,4 @@ proxmox-network-types = { version = "1.1.2" }
proxmox-schema = { version = "5" }
proxmox-sdn-types = { version = "0.2", path = "proxmox-sdn-types" }
proxmox-serde = { version = "1.0.0" }
+proxmox-ve-config = { version = "0.10", path = "proxmox-ve-config" }
diff --git a/proxmox-microseg/Cargo.toml b/proxmox-microseg/Cargo.toml
new file mode 100644
index 000000000000..067b0f593990
--- /dev/null
+++ b/proxmox-microseg/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "proxmox-microseg"
+description = "Proxmox VE SDN microsegmentation data plane"
+version = "0.1.0"
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+homepage.workspace = true
+exclude.workspace = true
+# proxmox-ebpf needs it
+rust-version = "1.88"
+
+[dependencies]
+anyhow = { workspace = true }
+log = "0.4"
+serde = { workspace = true, features = [ "derive" ] }
+serde_json = "1"
+
+proxmox-ebpf = { version = "0.1", features = [ "policy" ] }
+proxmox-ve-config = { workspace = true }
diff --git a/proxmox-microseg/debian/changelog b/proxmox-microseg/debian/changelog
new file mode 100644
index 000000000000..e8f8ee14dcf9
--- /dev/null
+++ b/proxmox-microseg/debian/changelog
@@ -0,0 +1,5 @@
+rust-proxmox-microseg (0.1.0) trixie; urgency=medium
+
+ * Initial release.
+
+ -- Proxmox Support Team <support@proxmox.com> Tue, 22 Sep 2026 16:02:32 +0200
diff --git a/proxmox-microseg/debian/control b/proxmox-microseg/debian/control
new file mode 100644
index 000000000000..35487025c2b7
--- /dev/null
+++ b/proxmox-microseg/debian/control
@@ -0,0 +1,46 @@
+Source: rust-proxmox-microseg
+Section: rust
+Priority: optional
+Build-Depends: debhelper-compat (= 13),
+ dh-sequence-cargo
+Build-Depends-Arch: cargo:native <!nocheck>,
+ rustc:native (>= 1.88) <!nocheck>,
+ libstd-rust-dev <!nocheck>,
+ librust-anyhow-1+default-dev <!nocheck>,
+ librust-log-0.4+default-dev <!nocheck>,
+ librust-proxmox-ebpf-0.1+default-dev <!nocheck>,
+ librust-proxmox-ebpf-0.1+policy-dev <!nocheck>,
+ librust-proxmox-ve-config-0.10+default-dev <!nocheck>,
+ librust-serde-1+default-dev <!nocheck>,
+ librust-serde-1+derive-dev <!nocheck>,
+ librust-serde-json-1+default-dev <!nocheck>
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Standards-Version: 4.7.2
+Vcs-Git: git://git.proxmox.com/git/proxmox-ve-rs.git
+Vcs-Browser: https://git.proxmox.com/?p=proxmox-ve-rs.git
+Homepage: https://proxmox.com
+X-Cargo-Crate: proxmox-microseg
+
+Package: librust-proxmox-microseg-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-anyhow-1+default-dev,
+ librust-log-0.4+default-dev,
+ librust-proxmox-ebpf-0.1+default-dev,
+ librust-proxmox-ebpf-0.1+policy-dev,
+ librust-proxmox-ve-config-0.10+default-dev,
+ librust-serde-1+default-dev,
+ librust-serde-1+derive-dev,
+ librust-serde-json-1+default-dev
+Provides:
+ librust-proxmox-microseg+default-dev (= ${binary:Version}),
+ librust-proxmox-microseg-0-dev (= ${binary:Version}),
+ librust-proxmox-microseg-0+default-dev (= ${binary:Version}),
+ librust-proxmox-microseg-0.1-dev (= ${binary:Version}),
+ librust-proxmox-microseg-0.1+default-dev (= ${binary:Version}),
+ librust-proxmox-microseg-0.1.0-dev (= ${binary:Version}),
+ librust-proxmox-microseg-0.1.0+default-dev (= ${binary:Version})
+Description: Proxmox VE SDN microsegmentation data plane - Rust source code
+ Source code for Debianized Rust crate "proxmox-microseg"
diff --git a/proxmox-microseg/debian/copyright b/proxmox-microseg/debian/copyright
new file mode 100644
index 000000000000..77952eba571e
--- /dev/null
+++ b/proxmox-microseg/debian/copyright
@@ -0,0 +1,18 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+
+Files:
+ *
+Copyright: 2019 - 2026 Proxmox Server Solutions GmbH <support@proxmox.com>
+License: AGPL-3.0-or-later
+ This program is free software: you can redistribute it and/or modify it under
+ the terms of the GNU Affero General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or (at your option) any
+ later version.
+ .
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ details.
+ .
+ You should have received a copy of the GNU Affero General Public License along
+ with this program. If not, see <https://www.gnu.org/licenses/>.
diff --git a/proxmox-microseg/debian/debcargo.toml b/proxmox-microseg/debian/debcargo.toml
new file mode 100644
index 000000000000..87a787e6d03e
--- /dev/null
+++ b/proxmox-microseg/debian/debcargo.toml
@@ -0,0 +1,7 @@
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support@proxmox.com>"
+
+[source]
+vcs_git = "git://git.proxmox.com/git/proxmox-ve-rs.git"
+vcs_browser = "https://git.proxmox.com/?p=proxmox-ve-rs.git"
diff --git a/proxmox-microseg/debian/source/format b/proxmox-microseg/debian/source/format
new file mode 100644
index 000000000000..89ae9db8f88b
--- /dev/null
+++ b/proxmox-microseg/debian/source/format
@@ -0,0 +1 @@
+3.0 (native)
diff --git a/proxmox-microseg/src/ebpf.rs b/proxmox-microseg/src/ebpf.rs
new file mode 100644
index 000000000000..02ea16d5a2ef
--- /dev/null
+++ b/proxmox-microseg/src/ebpf.rs
@@ -0,0 +1,235 @@
+use anyhow::{Context, bail};
+use proxmox_ebpf::policy::{self, Assignment, CHECK_MAX, CheckEntry, Family, Pair, Pass};
+use proxmox_ve_config::firewall::types::port::{PortEntry, PortList};
+use proxmox_ve_config::firewall::types::rule_match::{
+ IcmpCode, IcmpType, Icmpv6Code, Icmpv6Type, Ports, Protocol,
+};
+use proxmox_ve_config::sdn::microseg::MicrosegRunningConfig;
+use proxmox_ve_config::sdn::microseg::state::{self, DesiredState};
+
+use crate::nic::{self, LocalNics};
+
+pub use proxmox_ebpf::policy::{ExclusiveLock, PAIRS_MAX, SharedLock, lock_exclusive, lock_shared};
+
+fn port_bounds(list: Option<&PortList>) -> Vec<Option<(u16, u16)>> {
+ let Some(list) = list else {
+ // without a port list every port matches
+ return vec![None];
+ };
+ list.iter()
+ .map(|entry| {
+ Some(match entry {
+ PortEntry::Port(port) => (*port, *port),
+ PortEntry::Range(from, to) => (*from, *to),
+ })
+ })
+ .collect()
+}
+
+/// One entry per combination of source and destination range, since an entry carries a single range
+/// of each and both have to match.
+fn port_entries(ip_proto: u8, ports: &Ports, allow: bool) -> Vec<CheckEntry> {
+ let dports = port_bounds(ports.dport());
+ let mut entries = Vec::new();
+ for sport in port_bounds(ports.sport()) {
+ for &dport in &dports {
+ entries.push(CheckEntry {
+ ip_proto: Some(ip_proto),
+ sport,
+ dport,
+ allow,
+ ..Default::default()
+ });
+ }
+ }
+ entries
+}
+
+fn protocol_entries(protocol: &Protocol, allow: bool) -> anyhow::Result<Vec<CheckEntry>> {
+ let Some(ip_proto) = protocol.number() else {
+ bail!("protocol {protocol:?} was not resolved to a number");
+ };
+ Ok(match protocol {
+ Protocol::Tcp(tcp) => port_entries(ip_proto, tcp.ports(), allow),
+ Protocol::Udp(udp) => port_entries(ip_proto, udp.ports(), allow),
+ Protocol::Sctp(sctp) => port_entries(ip_proto, sctp.ports(), allow),
+ Protocol::Dccp(ports) | Protocol::UdpLite(ports) => port_entries(ip_proto, ports, allow),
+ // an icmp match covers IPv4 frames and an icmpv6 match IPv6 frames, which the protocol
+ // number alone does not say
+ Protocol::Icmp(icmp) => vec![CheckEntry {
+ ip_proto: Some(ip_proto),
+ family: Family::V4,
+ icmp_type: icmp.ty().and_then(IcmpType::number),
+ icmp_code: icmp.code().map(IcmpCode::number),
+ allow,
+ ..Default::default()
+ }],
+ Protocol::Icmpv6(icmpv6) => vec![CheckEntry {
+ ip_proto: Some(ip_proto),
+ family: Family::V6,
+ icmp_type: icmpv6.ty().and_then(Icmpv6Type::number),
+ icmp_code: icmpv6.code().map(Icmpv6Code::number),
+ allow,
+ ..Default::default()
+ }],
+ Protocol::Named(_) | Protocol::Numeric(_) => vec![CheckEntry {
+ ip_proto: Some(ip_proto),
+ allow,
+ ..Default::default()
+ }],
+ })
+}
+
+/// Pairs share checks, so translate each check once up front instead of once per pair.
+fn check_entries(state: &DesiredState) -> anyhow::Result<Vec<Vec<CheckEntry>>> {
+ let mut checks = Vec::with_capacity(state.checks.len());
+ for check in &state.checks {
+ let mut entries = Vec::new();
+ for protocol in &check.protocols {
+ entries.extend(protocol_entries(protocol, check.allow)?);
+ }
+ checks.push(entries);
+ }
+ Ok(checks)
+}
+
+fn pair_entries(
+ checks: &[Vec<CheckEntry>],
+ pair: &state::Pair,
+ cfg: &MicrosegRunningConfig,
+) -> anyhow::Result<Vec<CheckEntry>> {
+ let mut entries = Vec::new();
+ for &check in &pair.checks {
+ entries.extend_from_slice(&checks[check]);
+ }
+ // proxmox-ebpf refuses such a pair as well, but cannot name the classes by their groups
+ if entries.len() > CHECK_MAX {
+ bail!(
+ "pair {} -> {} needs {} checks, a pair holds at most {CHECK_MAX}",
+ cfg.describe_class(pair.src_id),
+ cfg.describe_class(pair.dst_id),
+ entries.len(),
+ );
+ }
+ Ok(entries)
+}
+
+fn pairs(state: &DesiredState, cfg: &MicrosegRunningConfig) -> anyhow::Result<Vec<Pair>> {
+ let checks = check_entries(state)?;
+ let mut pairs = Vec::with_capacity(state.pairs.len());
+ for pair in &state.pairs {
+ pairs.push(Pair {
+ src_id: pair.src_id,
+ dst_id: pair.dst_id,
+ checks: pair_entries(&checks, pair, cfg)?,
+ allow: pair.allow,
+ });
+ }
+ Ok(pairs)
+}
+
+pub struct FullPass {
+ configured: bool,
+ assignments: Vec<Assignment>,
+ pairs: Vec<Pair>,
+}
+
+pub fn full_pass(
+ nics: &LocalNics,
+ state: &DesiredState,
+ cfg: &MicrosegRunningConfig,
+) -> anyhow::Result<FullPass> {
+ let assignments = state
+ .nics
+ .iter()
+ .map(|resolved| {
+ let Some(name) = nics.name(resolved.vmid, resolved.iface) else {
+ bail!(
+ "NIC vm{} net{} has no interface on this node",
+ resolved.vmid,
+ resolved.iface
+ );
+ };
+ Ok(Assignment {
+ iface: name.to_owned(),
+ id: resolved.id,
+ firewall_bridge: nic::behind_firewall_bridge(name),
+ })
+ })
+ .collect::<anyhow::Result<Vec<_>>>()?;
+ Ok(FullPass {
+ configured: state.configured,
+ assignments,
+ pairs: pairs(state, cfg)?,
+ })
+}
+
+pub fn check(state: &DesiredState, cfg: &MicrosegRunningConfig) -> anyhow::Result<()> {
+ policy::check(&pairs(state, cfg)?).context("the data plane cannot take the microseg policy")
+}
+
+pub fn apply(lock: &ExclusiveLock, pass: &FullPass) -> anyhow::Result<()> {
+ let applied = lock.apply(pass.configured, &pass.assignments, &pass.pairs)?;
+ if applied != Pass::Applied {
+ return Ok(());
+ }
+ let level = if pass.assignments.is_empty() {
+ log::Level::Debug
+ } else {
+ log::Level::Info
+ };
+ log::log!(
+ level,
+ "microseg: applied {} assigned NICs on this node, {} pairs",
+ pass.assignments.len(),
+ pass.pairs.len()
+ );
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ use proxmox_ve_config::firewall::types::rule_match::Tcp;
+ use proxmox_ve_config::sdn::microseg::state::Check;
+
+ use super::*;
+
+ fn ports(list: &str) -> PortList {
+ list.parse().expect("valid port list")
+ }
+
+ /// The entries depend on nothing but the one check.
+ fn state(protocols: Vec<Protocol>) -> DesiredState {
+ DesiredState {
+ configured: true,
+ pairs: vec![state::Pair {
+ src_id: 1,
+ dst_id: 2,
+ checks: vec![0],
+ allow: false,
+ }],
+ checks: vec![Check {
+ protocols,
+ allow: true,
+ }],
+ nics: Vec::new(),
+ }
+ }
+
+ #[test]
+ fn check_rejects_what_the_apply_would() {
+ let elements: Vec<String> = (1..=CHECK_MAX + 1).map(|port| port.to_string()).collect();
+ let tcp = Tcp::new(Ports::new(None, ports(&elements.join(","))));
+ let over = state(vec![Protocol::Tcp(tcp)]);
+ let cfg: MicrosegRunningConfig = serde_json::from_str(
+ r#"{"ids":{},"identities":{"next":3,"classes":{"1":["web"],"2":["db"]}}}"#,
+ )
+ .expect("parse running config");
+ let error = check(&over, &cfg).expect_err("past the bound");
+ let message = format!("{error:#}");
+ assert!(message.contains("pair 1 (web) -> 2 (db)"), "{message}");
+
+ let tcp = Tcp::new(Ports::new(None, ports("22")));
+ check(&state(vec![Protocol::Tcp(tcp)]), &cfg).expect("an ordinary pair");
+ }
+}
diff --git a/proxmox-microseg/src/lib.rs b/proxmox-microseg/src/lib.rs
new file mode 100644
index 000000000000..4646d70982d1
--- /dev/null
+++ b/proxmox-microseg/src/lib.rs
@@ -0,0 +1,66 @@
+mod ebpf;
+mod nic;
+mod running_config;
+
+use anyhow::{Context, bail};
+use proxmox_ve_config::sdn::microseg::state::{DesiredState, resolve_iface};
+use proxmox_ve_config::sdn::microseg::{GuestInfo, MicrosegRunningConfig};
+
+pub use crate::ebpf::{ExclusiveLock, SharedLock, lock_exclusive, lock_shared};
+pub use crate::running_config::parse as parse_running_config;
+
+fn full_pass(
+ cfg: &MicrosegRunningConfig,
+ inventory: &[GuestInfo],
+) -> anyhow::Result<ebpf::FullPass> {
+ let nics = nic::LocalNics::list()?;
+ let state = DesiredState::build(cfg, &nics.guests(inventory), ebpf::PAIRS_MAX)
+ .context("desired state")?;
+ ebpf::full_pass(&nics, &state, cfg)
+}
+
+pub fn apply(
+ lock: &ExclusiveLock,
+ cfg: &MicrosegRunningConfig,
+ inventory: &[GuestInfo],
+) -> anyhow::Result<()> {
+ ebpf::apply(lock, &full_pass(cfg, inventory)?)
+}
+
+/// Fails for a config the data plane could not install. The pairs alone decide that, and they
+/// cover every allocated class whatever the guests match, so no inventory is needed.
+pub fn check(cfg: &MicrosegRunningConfig) -> anyhow::Result<()> {
+ let state = DesiredState::build(cfg, &[], ebpf::PAIRS_MAX).context("desired state")?;
+ ebpf::check(&state, cfg)
+}
+
+/// Enforce the policy on a guest NIC that is about to come up. An assigned NIC gets its class. A
+/// NIC no assignment covers loses what an earlier plug or pass placed for its index, so one that
+/// comes up again in place drops its old identity. A failure in either case is an error rather
+/// than a skip, since the NIC would come up unenforced or as a class it left. `guest` is the
+/// plugged guest as the inventory lists it. `firewall_bridge` says the NIC is about to be put
+/// behind a firewall bridge.
+pub fn apply_iface(
+ lock: &SharedLock,
+ cfg: &MicrosegRunningConfig,
+ guest: &GuestInfo,
+ ifname: &str,
+ firewall_bridge: bool,
+) -> anyhow::Result<()> {
+ let Some((vmid, iface)) = nic::parse(ifname) else {
+ log::warn!("microseg: {ifname} is not a guest NIC name, skipping");
+ return Ok(());
+ };
+ if vmid != guest.vmid {
+ bail!(
+ "{ifname} is a NIC of guest {vmid}, not of guest {}",
+ guest.vmid
+ );
+ }
+ let Some(id) = resolve_iface(cfg, guest, iface)? else {
+ log::debug!("microseg: {ifname} is unassigned");
+ lock.release_iface(ifname)?;
+ return Ok(());
+ };
+ lock.apply_iface(ifname, id, firewall_bridge)
+}
diff --git a/proxmox-microseg/src/nic.rs b/proxmox-microseg/src/nic.rs
new file mode 100644
index 000000000000..6a0b974b0f49
--- /dev/null
+++ b/proxmox-microseg/src/nic.rs
@@ -0,0 +1,120 @@
+use std::collections::{BTreeMap, BTreeSet, HashMap};
+
+use anyhow::Context;
+use proxmox_ve_config::sdn::microseg::GuestInfo;
+
+/// The guest and the NIC index a host interface name stands for.
+pub fn parse(name: &str) -> Option<(u32, u32)> {
+ let rest = name
+ .strip_prefix("tap")
+ .or_else(|| name.strip_prefix("veth"))?;
+ let (vmid, iface) = rest.split_once('i')?;
+ // the integer parser alone would also take a sign
+ let number = |digits: &str| -> Option<u32> {
+ digits
+ .bytes()
+ .all(|byte| byte.is_ascii_digit())
+ .then(|| digits.parse().ok())
+ .flatten()
+ };
+ Some((number(vmid)?, number(iface)?))
+}
+
+/// A guest without a name or tags may be missing from the inventory. An assignment by VMID covers
+/// it all the same, so it is known by its VMID alone.
+fn guest(known: Option<&GuestInfo>, vmid: u32) -> GuestInfo {
+ known.cloned().unwrap_or(GuestInfo {
+ vmid,
+ ..Default::default()
+ })
+}
+
+pub fn behind_firewall_bridge(name: &str) -> bool {
+ let Some((vmid, iface)) = parse(name) else {
+ return false;
+ };
+ std::fs::read_link(format!("/sys/class/net/{name}/master")).is_ok_and(|bridge| {
+ bridge
+ .file_name()
+ .is_some_and(|master| master.to_string_lossy() == format!("fwbr{vmid}i{iface}"))
+ })
+}
+
+/// The node says which NICs a guest has, since its config does not name a hot-plugged NIC until the
+/// plug is through.
+pub struct LocalNics(BTreeMap<(u32, u32), String>);
+
+impl LocalNics {
+ pub fn list() -> anyhow::Result<Self> {
+ // a failure must not read as a node without guest NICs, a full pass would then take
+ // enforcement off every one of them
+ let names = std::fs::read_dir("/sys/class/net")
+ .and_then(|entries| {
+ entries
+ .map(|entry| Ok(entry?.file_name().to_string_lossy().into_owned()))
+ .collect::<std::io::Result<Vec<_>>>()
+ })
+ .context("list the interfaces of this node")?;
+ Ok(Self::from_names(names))
+ }
+
+ fn from_names(names: impl IntoIterator<Item = String>) -> Self {
+ let nics = names
+ .into_iter()
+ .filter_map(|name| Some((parse(&name)?, name)));
+ Self(nics.collect())
+ }
+
+ pub fn name(&self, vmid: u32, iface: u32) -> Option<&str> {
+ self.0.get(&(vmid, iface)).map(String::as_str)
+ }
+
+ pub fn guests(&self, inventory: &[GuestInfo]) -> Vec<GuestInfo> {
+ let mut nics: BTreeMap<u32, BTreeSet<u32>> = BTreeMap::new();
+ for &(vmid, iface) in self.0.keys() {
+ nics.entry(vmid).or_default().insert(iface);
+ }
+ let known: HashMap<u32, &GuestInfo> = inventory.iter().map(|g| (g.vmid, g)).collect();
+ nics.into_iter()
+ .map(|(vmid, nics)| GuestInfo {
+ nics: nics.into_iter().collect(),
+ ..guest(known.get(&vmid).copied(), vmid)
+ })
+ .collect()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn a_guest_has_the_nics_the_node_has() {
+ let inventory = [
+ GuestInfo {
+ vmid: 100,
+ name: Some("web".to_string()),
+ nics: vec![0, 5],
+ ..Default::default()
+ },
+ GuestInfo {
+ vmid: 300,
+ nics: vec![0],
+ ..Default::default()
+ },
+ ];
+ let names = ["lo", "tap100i1", "fwln100i0", "veth200i0", "tap100i0"];
+ let nics = LocalNics::from_names(names.map(str::to_string));
+ let guests = nics.guests(&inventory);
+ assert_eq!(nics.name(200, 0), Some("veth200i0"));
+
+ // net1 is not in the config of vm100 yet and net5 is not plugged, vm300 runs elsewhere
+ assert_eq!(guests.len(), 2);
+ assert_eq!(guests[0].vmid, 100);
+ assert_eq!(guests[0].name.as_deref(), Some("web"));
+ assert_eq!(guests[0].nics, [0, 1]);
+ assert_eq!(guests[1].vmid, 200);
+ assert_eq!(guests[1].name, None);
+ assert_eq!(guests[1].nics, [0]);
+ }
+}
diff --git a/proxmox-microseg/src/running_config.rs b/proxmox-microseg/src/running_config.rs
new file mode 100644
index 000000000000..dbf8160de196
--- /dev/null
+++ b/proxmox-microseg/src/running_config.rs
@@ -0,0 +1,26 @@
+use proxmox_ve_config::sdn::microseg::MicrosegRunningConfig;
+use serde::Deserialize;
+
+#[derive(Deserialize)]
+struct Sections {
+ microseg: Option<MicrosegRunningConfig>,
+}
+
+/// `None` for a running config without a microseg block. Every apply writes the block, even an
+/// empty one, so only a node without microseg support leaves it out. Such a document says nothing
+/// about the policy, it does not declare an empty one.
+pub fn parse(raw: &str) -> anyhow::Result<Option<MicrosegRunningConfig>> {
+ let sections: Sections = serde_json::from_str(raw)?;
+ Ok(sections.microseg)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn a_missing_block_is_absent() {
+ let cfg = parse(r#"{"zones": {"ids": {}}}"#).expect("parse running config");
+ assert_eq!(cfg, None);
+ }
+}
--
2.47.3
next prev parent reply other threads:[~2026-10-27 8:59 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 ` Hannes Laimer [this message]
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-14-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