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 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.