From: Hannes Laimer <h.laimer@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH pve-network v3 22/45] sdn: microseg: add config, API and guest inventory
Date: Tue, 27 Oct 2026 09:55:04 +0100 [thread overview]
Message-ID: <20261027085527.702738-23-h.laimer@proxmox.com> (raw)
In-Reply-To: <20261027085527.702738-1-h.laimer@proxmox.com>
The microseg config holds groups, allow and deny rules between
groups, and assignments of guest NICs to groups. It lives in
sdn/microseg.cfg.
An SDN apply copies the entries into the running config, together
with a 16-bit identity for each class of assigned group sets. Group
sets that no rule tells apart form one class. The apply starts from
the identities of the previous running config, so a class that stays
keeps its identity.
Nothing stores which NICs belong to an assignment, since guests and
their NICs change without an SDN apply. The overview at
/cluster/sdn/microseg/all lists them per assignment, read from the
guest configs of the whole cluster. It shows a guest's NICs only to a
caller with VM.Audit on that guest, as /cluster/resources does.
Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
src/PVE/API2/Network/SDN.pm | 11 +
src/PVE/API2/Network/SDN/Makefile | 2 +
src/PVE/API2/Network/SDN/Microseg.pm | 215 ++++++++++
.../API2/Network/SDN/Microseg/Assignment.pm | 23 ++
src/PVE/API2/Network/SDN/Microseg/Base.pm | 377 ++++++++++++++++++
src/PVE/API2/Network/SDN/Microseg/Group.pm | 17 +
src/PVE/API2/Network/SDN/Microseg/Makefile | 11 +
src/PVE/API2/Network/SDN/Microseg/Rule.pm | 22 +
src/PVE/Network/SDN.pm | 23 ++
src/PVE/Network/SDN/Makefile | 1 +
src/PVE/Network/SDN/Microseg.pm | 282 +++++++++++++
11 files changed, 984 insertions(+)
create mode 100644 src/PVE/API2/Network/SDN/Microseg.pm
create mode 100644 src/PVE/API2/Network/SDN/Microseg/Assignment.pm
create mode 100644 src/PVE/API2/Network/SDN/Microseg/Base.pm
create mode 100644 src/PVE/API2/Network/SDN/Microseg/Group.pm
create mode 100644 src/PVE/API2/Network/SDN/Microseg/Makefile
create mode 100644 src/PVE/API2/Network/SDN/Microseg/Rule.pm
create mode 100644 src/PVE/Network/SDN/Microseg.pm
diff --git a/src/PVE/API2/Network/SDN.pm b/src/PVE/API2/Network/SDN.pm
index e3c8d9ddecdf..8640607a6b5c 100644
--- a/src/PVE/API2/Network/SDN.pm
+++ b/src/PVE/API2/Network/SDN.pm
@@ -22,6 +22,7 @@ use PVE::API2::Network::SDN::Zones;
use PVE::API2::Network::SDN::Ipams;
use PVE::API2::Network::SDN::Dns;
use PVE::API2::Network::SDN::Fabrics;
+use PVE::API2::Network::SDN::Microseg;
use PVE::API2::Network::SDN::PrefixLists;
use PVE::API2::Network::SDN::RouteMaps;
@@ -57,6 +58,11 @@ __PACKAGE__->register_method({
path => 'fabrics',
});
+__PACKAGE__->register_method({
+ subclass => "PVE::API2::Network::SDN::Microseg",
+ path => 'microseg',
+});
+
__PACKAGE__->register_method({
subclass => "PVE::API2::Network::SDN::PrefixLists",
path => 'prefix-lists',
@@ -99,6 +105,7 @@ __PACKAGE__->register_method({
{ id => 'ipams' },
{ id => 'dns' },
{ id => 'fabrics' },
+ { id => 'microseg' },
{ id => 'prefix-lists' },
{ id => 'route-maps' },
];
@@ -271,6 +278,10 @@ __PACKAGE__->register_method({
PVE::RS::SDN::PrefixLists->running_config($prefix_list_config);
PVE::Network::SDN::PrefixLists::write_config($parsed_prefix_list_config);
+ my $parsed_microseg_config =
+ PVE::Network::SDN::Microseg::running_policy($running_config);
+ PVE::Network::SDN::Microseg::write_config($parsed_microseg_config);
+
PVE::Network::SDN::delete_global_lock() if $lock_token && $release_lock;
};
diff --git a/src/PVE/API2/Network/SDN/Makefile b/src/PVE/API2/Network/SDN/Makefile
index 6b91f8cc0127..3ae11b02eac6 100644
--- a/src/PVE/API2/Network/SDN/Makefile
+++ b/src/PVE/API2/Network/SDN/Makefile
@@ -6,6 +6,7 @@ SOURCES=Vnets.pm\
Dns.pm\
Ips.pm\
Fabrics.pm\
+ Microseg.pm\
PrefixLists.pm\
RouteMaps.pm
@@ -18,4 +19,5 @@ install:
make -C Nodes install
make -C RouteMaps install
make -C PrefixLists install
+ make -C Microseg install
diff --git a/src/PVE/API2/Network/SDN/Microseg.pm b/src/PVE/API2/Network/SDN/Microseg.pm
new file mode 100644
index 000000000000..525fa429ae50
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Microseg.pm
@@ -0,0 +1,215 @@
+package PVE::API2::Network::SDN::Microseg;
+
+use strict;
+use warnings;
+
+use PVE::JSONSchema qw(get_standard_option);
+
+use PVE::Network::SDN;
+use PVE::Network::SDN::Microseg;
+use PVE::RS::SDN::Microseg;
+
+use PVE::API2::Network::SDN::Microseg::Base;
+use PVE::API2::Network::SDN::Microseg::Group;
+use PVE::API2::Network::SDN::Microseg::Rule;
+use PVE::API2::Network::SDN::Microseg::Assignment;
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method({
+ subclass => "PVE::API2::Network::SDN::Microseg::Group",
+ path => 'group',
+});
+
+__PACKAGE__->register_method({
+ subclass => "PVE::API2::Network::SDN::Microseg::Rule",
+ path => 'rule',
+});
+
+__PACKAGE__->register_method({
+ subclass => "PVE::API2::Network::SDN::Microseg::Assignment",
+ path => 'assignment',
+});
+
+__PACKAGE__->register_method({
+ name => 'index',
+ path => '',
+ method => 'GET',
+ description => "SDN microseg index.",
+ permissions => {
+ check => ['perm', '/sdn/microseg', ['SDN.Audit']],
+ },
+ parameters => {
+ properties => {},
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => {
+ subdir => { type => 'string' },
+ },
+ },
+ links => [{ rel => 'child', href => "{subdir}" }],
+ },
+ code => sub {
+ return [
+ { subdir => 'group' },
+ { subdir => 'rule' },
+ { subdir => 'assignment' },
+ { subdir => 'all' },
+ ];
+ },
+});
+
+my $OBJECT_PROPERTIES = {
+ map { PVE::Network::SDN::Microseg::optional_properties($_)->%* } (
+ PVE::Network::SDN::Microseg::group_properties(0),
+ PVE::Network::SDN::Microseg::rule_properties(0),
+ PVE::Network::SDN::Microseg::assignment_object_properties(),
+ ),
+};
+delete $OBJECT_PROPERTIES->{type};
+
+__PACKAGE__->register_method({
+ name => 'all',
+ path => 'all',
+ method => 'GET',
+ description =>
+ "Every microseg object with its assigned members, plus the group sets and the"
+ . " id space usage.",
+ permissions => {
+ check => ['perm', '/sdn/microseg', ['SDN.Audit']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => PVE::API2::Network::SDN::Microseg::Base::list_parameters(),
+ },
+ returns => {
+ type => 'object',
+ properties => {
+ objects => {
+ type => 'array',
+ description => 'Every microseg object across all types.',
+ items => {
+ type => "object",
+ properties => {
+ PVE::API2::Network::SDN::Microseg::Base::list_item_properties()->%*,
+ type => {
+ type => 'string',
+ enum => [
+ 'group',
+ 'rule',
+ $PVE::Network::SDN::Microseg::ASSIGNMENT_TYPES->@*,
+ ],
+ },
+ members => {
+ type => 'array',
+ optional => 1,
+ description =>
+ 'The guests and NICs that belong to the assignment, resolved'
+ . ' against the current guest inventory, without the guests the'
+ . ' caller may not audit. Assignment objects only.',
+ items => {
+ type => 'object',
+ properties => {
+ vmid => get_standard_option('pve-vmid'),
+ nics => {
+ type => 'array',
+ items => { type => 'integer' },
+ description =>
+ 'Indices of the covered netN interfaces.',
+ },
+ },
+ },
+ },
+ $OBJECT_PROPERTIES->%*,
+ },
+ },
+ },
+ 'id-space' => {
+ type => 'object',
+ description => 'Usage of the 16-bit wire identity space in the applied config.',
+ properties => {
+ used => {
+ type => 'integer',
+ description =>
+ 'Ids in use, one per class of group sets the rules treat alike.',
+ },
+ retired => {
+ type => 'integer',
+ description => 'Retired ids still in quarantine. One whose quarantine'
+ . ' has passed is counted until the next allocation drops it.',
+ },
+ total => {
+ type => 'integer',
+ description =>
+ 'Allocatable ids, id 0 stays reserved for untagged traffic.',
+ },
+ },
+ },
+ 'group-sets' => {
+ type => 'array',
+ description =>
+ 'The rules that govern each group set an assignment binds, as source'
+ . ' (outbound) and as destination (inbound).',
+ items => {
+ type => "object",
+ properties => {
+ groups => {
+ type => 'array',
+ items => { type => 'string' },
+ description => 'The group set as an assignment names it, sorted.',
+ },
+ outbound => {
+ type => 'array',
+ items => { type => 'string' },
+ description =>
+ 'Ids of the rules that apply with this group set as source, a'
+ . ' bidirectional rule in either direction.',
+ },
+ inbound => {
+ type => 'array',
+ items => { type => 'string' },
+ description =>
+ 'Ids of the rules that apply with this group set as destination,'
+ . ' a bidirectional rule in either direction.',
+ },
+ },
+ },
+ },
+ },
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $running_cfg = PVE::Network::SDN::running_config();
+ my $running = $param->{running} && !$param->{pending};
+ my $config =
+ $running
+ ? PVE::Network::SDN::Microseg::running_policy($running_cfg)
+ : PVE::Network::SDN::Microseg::config();
+ my $objects = PVE::API2::Network::SDN::Microseg::Base::list_objects(
+ undef, $param, $config, $running_cfg,
+ );
+
+ my $members = $config->assigned_members(
+ PVE::Network::SDN::Microseg::audited_guest_inventory(),
+ );
+ for my $object (@$objects) {
+ my $id = $object->{id};
+ $object->{members} = $members->{$id} if defined($members->{$id});
+ }
+
+ my $registry = ($running_cfg->{microseg} // {})->{identities};
+
+ return {
+ objects => $objects,
+ 'group-sets' => $config->rules_by_group_set(),
+ 'id-space' => PVE::RS::SDN::Microseg::id_space($registry),
+ };
+ },
+});
+
+1;
diff --git a/src/PVE/API2/Network/SDN/Microseg/Assignment.pm b/src/PVE/API2/Network/SDN/Microseg/Assignment.pm
new file mode 100644
index 000000000000..0cc3f5baf1f1
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Microseg/Assignment.pm
@@ -0,0 +1,23 @@
+package PVE::API2::Network::SDN::Microseg::Assignment;
+
+use strict;
+use warnings;
+
+use PVE::Network::SDN::Microseg;
+
+use PVE::API2::Network::SDN::Microseg::Base;
+use base qw(PVE::API2::Network::SDN::Microseg::Base);
+
+__PACKAGE__->register_handlers({
+ types => $PVE::Network::SDN::Microseg::ASSIGNMENT_TYPES,
+ name => 'assignment',
+ plural => 'NIC-to-group assignments',
+ properties => \&PVE::Network::SDN::Microseg::assignment_properties,
+ object_properties => \&PVE::Network::SDN::Microseg::assignment_object_properties,
+ create_details => "The id is derived from its matcher and interface, so creating the same"
+ . " assignment again is refused as a duplicate.",
+ generated_id => 1,
+ generated_id_description => "The generated assignment id.",
+});
+
+1;
diff --git a/src/PVE/API2/Network/SDN/Microseg/Base.pm b/src/PVE/API2/Network/SDN/Microseg/Base.pm
new file mode 100644
index 000000000000..dde88d10c7b1
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Microseg/Base.pm
@@ -0,0 +1,377 @@
+package PVE::API2::Network::SDN::Microseg::Base;
+
+use strict;
+use warnings;
+
+use PVE::Cluster;
+use PVE::Exception qw(raise_param_exc);
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::Tools qw(extract_param);
+
+use PVE::Network::SDN;
+use PVE::Network::SDN::Microseg;
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+sub list_parameters {
+ return {
+ running => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display running config.",
+ },
+ pending => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display pending config.",
+ },
+ };
+}
+
+sub list_item_properties {
+ return {
+ id => get_standard_option('pve-sdn-microseg-id'),
+ digest => {
+ type => 'string',
+ description => 'Digest of the microseg configuration.',
+ optional => 1,
+ },
+ state => get_standard_option('pve-sdn-config-state'),
+ pending => {
+ type => 'object',
+ optional => 1,
+ description => 'Changes not yet applied to the running configuration.',
+ },
+ };
+}
+
+my sub typed_entry {
+ my ($config, $types, $id) = @_;
+
+ my $entry = $config->get($id);
+ raise_param_exc({ id => "sdn microseg object '$id' does not exist" })
+ if !$entry || !PVE::Network::SDN::Microseg::type_matches($entry->{type}, $types);
+
+ return $entry;
+}
+
+my sub get_object {
+ my ($types, $id) = @_;
+
+ my $config = PVE::Network::SDN::Microseg::config();
+ my $entry = typed_entry($config, $types, $id);
+
+ $entry->{digest} = $config->digest();
+
+ return $entry;
+}
+
+my sub assert_assignment_kind {
+ my ($type, $param) = @_;
+
+ my $properties = PVE::Network::SDN::Microseg::assignment_properties(0);
+ my $errors = {};
+ for my $property (keys %$properties) {
+ my $kinds = $properties->{$property}->{'instance-types'} or next;
+ $errors->{$property} = "not allowed for type '$type'"
+ if defined($param->{$property}) && !grep { $_ eq $type } @$kinds;
+ }
+ raise_param_exc($errors) if %$errors;
+}
+
+# the config's errors for an empty list do not name the parameter
+my sub assert_non_empty_lists {
+ my ($param) = @_;
+
+ my $errors = {};
+ for my $property (qw(src dst groups)) {
+ my $value = $param->{$property};
+ $errors->{$property} = "must not be empty" if $value && !@$value;
+ }
+ raise_param_exc($errors) if %$errors;
+}
+
+my sub create_object {
+ my ($types, $param) = @_;
+
+ my $type = @$types > 1 ? $param->{type} : $types->[0];
+
+ # the schema does not reject a property of another matcher kind
+ assert_assignment_kind($type, $param)
+ if PVE::Network::SDN::Microseg::type_matches(
+ $type,
+ $PVE::Network::SDN::Microseg::ASSIGNMENT_TYPES,
+ );
+ assert_non_empty_lists($param);
+
+ my $lock_token = extract_param($param, 'lock-token');
+ $param->{type} = $type;
+
+ PVE::Cluster::check_cfs_quorum();
+ mkdir("/etc/pve/sdn");
+
+ return PVE::Network::SDN::lock_sdn_config(
+ sub {
+ my $config = PVE::Network::SDN::Microseg::config();
+ my $id = $config->create($param);
+ PVE::Network::SDN::Microseg::write_config($config);
+ return $id;
+ },
+ "create sdn microseg object failed",
+ $lock_token,
+ );
+}
+
+my sub update_object {
+ my ($types, $id, $param) = @_;
+
+ assert_non_empty_lists($param);
+
+ my $digest = extract_param($param, 'digest');
+ my $lock_token = extract_param($param, 'lock-token');
+
+ PVE::Network::SDN::lock_sdn_config(
+ sub {
+ my $config = PVE::Network::SDN::Microseg::config();
+ my $entry = typed_entry($config, $types, $id);
+
+ PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+
+ $param->{type} = $entry->{type};
+
+ $config->update($id, $param);
+ PVE::Network::SDN::Microseg::write_config($config);
+ },
+ "update sdn microseg object failed",
+ $lock_token,
+ );
+}
+
+my sub delete_object {
+ my ($types, $id, $param) = @_;
+
+ my $lock_token = extract_param($param, 'lock-token');
+
+ PVE::Network::SDN::lock_sdn_config(
+ sub {
+ my $config = PVE::Network::SDN::Microseg::config();
+ typed_entry($config, $types, $id);
+
+ $config->delete($id);
+ PVE::Network::SDN::Microseg::write_config($config);
+ },
+ "delete sdn microseg object failed",
+ $lock_token,
+ );
+}
+
+sub list_objects {
+ my ($types, $param, $config, $running_cfg) = @_;
+
+ my $ids;
+ my $digest;
+ if ($param->{pending}) {
+ $running_cfg //= PVE::Network::SDN::running_config();
+ $config //= PVE::Network::SDN::Microseg::config();
+ my $sections = { ids => $config->to_sections() };
+ my $pending = PVE::Network::SDN::pending_config($running_cfg, $sections, 'microseg');
+ $ids = $pending->{ids};
+ $digest = $config->digest();
+ } elsif ($param->{running}) {
+ $running_cfg //= PVE::Network::SDN::running_config();
+ $ids = ($running_cfg->{microseg} // {})->{ids} // {};
+ } else {
+ $config //= PVE::Network::SDN::Microseg::config();
+ $ids = $config->to_sections();
+ $digest = $config->digest();
+ }
+
+ my $res = [];
+ for my $id (sort keys %$ids) {
+ my $scfg = $ids->{$id};
+ next if defined $types && !PVE::Network::SDN::Microseg::type_matches($scfg->{type}, $types);
+ $scfg->{id} = $id;
+ $scfg->{digest} = $digest if defined $digest;
+ push @$res, $scfg;
+ }
+
+ return $res;
+}
+
+sub register_handlers {
+ my ($class, $def) = @_;
+
+ my ($types, $name, $properties) = $def->@{qw(types name properties)};
+ my $plural = $def->{plural} // "${name}s";
+ my $generated_id = $def->{generated_id};
+
+ my $create_description = "Create a microseg $name.";
+ $create_description .= " $def->{create_details}" if defined($def->{create_details});
+
+ my @create_id = (id => get_standard_option('pve-sdn-microseg-id'));
+ my $create_returns = { type => 'null' };
+ if ($generated_id) {
+ @create_id = ();
+ $create_returns = { type => 'string', description => $def->{generated_id_description} };
+ }
+
+ # with several types the properties declare the type themselves
+ my @type_property;
+ if (@$types == 1) {
+ @type_property = (
+ type => {
+ type => 'string',
+ description => 'Type of the object.',
+ enum => [@$types],
+ },
+ );
+ }
+
+ my $object_properties = ($def->{object_properties} // sub { $properties->(0) })->();
+ my $index_properties = PVE::Network::SDN::Microseg::optional_properties($object_properties);
+
+ $class->register_method({
+ name => 'index',
+ path => '',
+ method => 'GET',
+ description => "List microseg $plural.",
+ permissions => {
+ check => ['perm', '/sdn/microseg', ['SDN.Audit']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => list_parameters(),
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => {
+ list_item_properties()->%*, @type_property, $index_properties->%*,
+ },
+ },
+ links => [{ rel => 'child', href => "{id}" }],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ return list_objects($types, $param);
+ },
+ });
+
+ $class->register_method({
+ name => 'read',
+ path => '{id}',
+ method => 'GET',
+ description => "Read one microseg $name.",
+ permissions => {
+ check => ['perm', '/sdn/microseg', ['SDN.Audit']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ id => get_standard_option('pve-sdn-microseg-id'),
+ },
+ },
+ returns => {
+ type => 'object',
+ properties => {
+ id => get_standard_option('pve-sdn-microseg-id'),
+ digest => get_standard_option('pve-config-digest'),
+ @type_property,
+ $object_properties->%*,
+ },
+ },
+ code => sub {
+ my ($param) = @_;
+
+ return get_object($types, extract_param($param, 'id'));
+ },
+ });
+
+ $class->register_method({
+ name => 'create',
+ protected => 1,
+ path => '',
+ method => 'POST',
+ description => $create_description,
+ permissions => {
+ check => ['perm', '/sdn/microseg', ['SDN.Allocate']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ @create_id,
+ $properties->(0)->%*,
+ 'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ },
+ },
+ returns => $create_returns,
+ code => sub {
+ my ($param) = @_;
+
+ my $id = create_object($types, $param);
+
+ return $generated_id ? $id : undef;
+ },
+ });
+
+ $class->register_method({
+ name => 'update',
+ protected => 1,
+ path => '{id}',
+ method => 'PUT',
+ description => "Update a microseg $name.",
+ permissions => {
+ check => ['perm', '/sdn/microseg', ['SDN.Allocate']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ id => get_standard_option('pve-sdn-microseg-id'),
+ $properties->(1)->%*,
+ digest => get_standard_option('pve-config-digest'),
+ 'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $id = extract_param($param, 'id');
+ update_object($types, $id, $param);
+
+ return undef;
+ },
+ });
+
+ $class->register_method({
+ name => 'delete',
+ protected => 1,
+ path => '{id}',
+ method => 'DELETE',
+ description => "Delete a microseg $name.",
+ permissions => {
+ check => ['perm', '/sdn/microseg', ['SDN.Allocate']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ id => get_standard_option('pve-sdn-microseg-id'),
+ 'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $id = extract_param($param, 'id');
+ delete_object($types, $id, $param);
+
+ return undef;
+ },
+ });
+}
+
+1;
diff --git a/src/PVE/API2/Network/SDN/Microseg/Group.pm b/src/PVE/API2/Network/SDN/Microseg/Group.pm
new file mode 100644
index 000000000000..e1c6a85c5863
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Microseg/Group.pm
@@ -0,0 +1,17 @@
+package PVE::API2::Network::SDN::Microseg::Group;
+
+use strict;
+use warnings;
+
+use PVE::Network::SDN::Microseg;
+
+use PVE::API2::Network::SDN::Microseg::Base;
+use base qw(PVE::API2::Network::SDN::Microseg::Base);
+
+__PACKAGE__->register_handlers({
+ types => ['group'],
+ name => 'group',
+ properties => \&PVE::Network::SDN::Microseg::group_properties,
+});
+
+1;
diff --git a/src/PVE/API2/Network/SDN/Microseg/Makefile b/src/PVE/API2/Network/SDN/Microseg/Makefile
new file mode 100644
index 000000000000..910265f95c57
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Microseg/Makefile
@@ -0,0 +1,11 @@
+SOURCES=Base.pm\
+ Group.pm\
+ Rule.pm\
+ Assignment.pm
+
+
+PERL5DIR=${DESTDIR}/usr/share/perl5
+
+.PHONY: install
+install:
+ for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/API2/Network/SDN/Microseg/$$i; done
diff --git a/src/PVE/API2/Network/SDN/Microseg/Rule.pm b/src/PVE/API2/Network/SDN/Microseg/Rule.pm
new file mode 100644
index 000000000000..575ca1286ed9
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Microseg/Rule.pm
@@ -0,0 +1,22 @@
+package PVE::API2::Network::SDN::Microseg::Rule;
+
+use strict;
+use warnings;
+
+use PVE::Network::SDN::Microseg;
+
+use PVE::API2::Network::SDN::Microseg::Base;
+use base qw(PVE::API2::Network::SDN::Microseg::Base);
+
+__PACKAGE__->register_handlers({
+ types => ['rule'],
+ name => 'rule',
+ properties => \&PVE::Network::SDN::Microseg::rule_properties,
+ create_details => "The id is derived from the src and dst predicates and from the"
+ . " direction. Creating the same rule again is refused as a duplicate. Change an existing"
+ . " rule's priority or verdict by updating it rather than creating a second rule.",
+ generated_id => 1,
+ generated_id_description => "The generated rule id.",
+});
+
+1;
diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
index 2dc4d9cda704..107095e3ca71 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -25,6 +25,7 @@ use PVE::Network::SDN::Subnets;
use PVE::Network::SDN::Dhcp;
use PVE::Network::SDN::Frr;
use PVE::Network::SDN::Fabrics;
+use PVE::Network::SDN::Microseg;
use PVE::Network::SDN::RouteMaps;
use PVE::Network::SDN::PrefixLists;
@@ -40,12 +41,26 @@ my $parse_running_cfg = sub {
eval { $cfg = from_json($raw); };
return {} if $@;
+ for my $entry (values((($cfg->{microseg} // {})->{ids} // {})->%*)) {
+ utf8::decode($entry->{comment}) if defined($entry->{comment});
+ }
+
return $cfg;
};
my $write_running_cfg = sub {
my ($filename, $cfg) = @_;
+ # a microseg comment may carry non-ASCII text
+ if (my $ids = ($cfg->{microseg} // {})->{ids}) {
+ my $encoded = {};
+ for my $id (keys %$ids) {
+ $encoded->{$id} = { $ids->{$id}->%* };
+ utf8::encode($encoded->{$id}->{comment}) if defined($encoded->{$id}->{comment});
+ }
+ $cfg = { %$cfg, microseg => { $cfg->{microseg}->%*, ids => $encoded } };
+ }
+
my $json = to_json($cfg);
return $json;
@@ -213,6 +228,7 @@ sub compile_running_cfg {
my $controllers_cfg = PVE::Network::SDN::Controllers::config();
my $subnets_cfg = PVE::Network::SDN::Subnets::config();
my $fabrics_cfg = PVE::Network::SDN::Fabrics::config();
+ my $microseg_cfg = PVE::Network::SDN::Microseg::config();
my $route_maps_cfg = PVE::Network::SDN::RouteMaps::config();
my $prefix_lists_cfg = PVE::Network::SDN::PrefixLists::config();
@@ -221,6 +237,8 @@ sub compile_running_cfg {
my $controllers = { ids => $controllers_cfg->{ids} };
my $subnets = { ids => $subnets_cfg->{ids} };
my $fabrics = { ids => $fabrics_cfg->to_sections() };
+ # the previous identities keep the ids stable
+ my $microseg = $microseg_cfg->render(($cfg->{microseg} // {})->{identities});
my $route_maps = { ids => $route_maps_cfg->to_sections() };
my $prefix_lists = { ids => $prefix_lists_cfg->to_sections() };
@@ -231,6 +249,7 @@ sub compile_running_cfg {
controllers => $controllers,
subnets => $subnets,
fabrics => $fabrics,
+ microseg => $microseg,
'route-maps' => $route_maps,
'prefix-lists' => $prefix_lists,
};
@@ -254,6 +273,7 @@ sub has_pending_changes {
subnets => PVE::Network::SDN::Subnets::config(),
controllers => PVE::Network::SDN::Controllers::config(),
fabrics => { ids => PVE::Network::SDN::Fabrics::config()->to_sections() },
+ microseg => { ids => PVE::Network::SDN::Microseg::config()->to_sections() },
'route-maps' => { ids => PVE::Network::SDN::RouteMaps::config()->to_sections() },
'prefix-lists' => { ids => PVE::Network::SDN::PrefixLists::config()->to_sections() },
};
@@ -522,6 +542,9 @@ sub encode_value {
|| $key eq 'allowed_ips'
|| $key eq 'secondary-controllers'
|| $key eq 'redistribute'
+ || $key eq 'groups'
+ || $key eq 'src'
+ || $key eq 'dst'
) {
if (ref($value) eq 'HASH') {
return join(',', sort keys(%$value));
diff --git a/src/PVE/Network/SDN/Makefile b/src/PVE/Network/SDN/Makefile
index d0b4bce2d067..3042baba6513 100644
--- a/src/PVE/Network/SDN/Makefile
+++ b/src/PVE/Network/SDN/Makefile
@@ -9,6 +9,7 @@ SOURCES=Vnets.pm\
Dhcp.pm\
Fabrics.pm\
Frr.pm\
+ Microseg.pm\
PrefixLists.pm\
RouteMaps.pm\
WireGuard.pm
diff --git a/src/PVE/Network/SDN/Microseg.pm b/src/PVE/Network/SDN/Microseg.pm
new file mode 100644
index 000000000000..05c432d4eec7
--- /dev/null
+++ b/src/PVE/Network/SDN/Microseg.pm
@@ -0,0 +1,282 @@
+package PVE::Network::SDN::Microseg;
+
+use strict;
+use warnings;
+
+use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_write_file);
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::RPCEnvironment;
+use PVE::Tools;
+
+use PVE::RS::SDN::Microseg;
+
+PVE::JSONSchema::register_format(
+ 'pve-sdn-microseg-id',
+ sub {
+ my ($id) = @_;
+ die "microseg id '$id' contains illegal characters or has the wrong length\n"
+ if $id !~ m/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,30}[a-zA-Z0-9]$/;
+ return $id;
+ },
+);
+
+PVE::JSONSchema::register_standard_option(
+ 'pve-sdn-microseg-id',
+ {
+ description => "The microseg object identifier.",
+ type => 'string',
+ format => 'pve-sdn-microseg-id',
+ },
+);
+
+cfs_register_file('sdn/microseg.cfg', \&parse_microseg_config, \&write_microseg_config);
+
+our $ASSIGNMENT_TYPES = ['guest-assignment'];
+
+sub parse_microseg_config {
+ my ($filename, $raw) = @_;
+ return $raw // '';
+}
+
+sub write_microseg_config {
+ my ($filename, $config) = @_;
+ return $config // '';
+}
+
+sub config {
+ my $microseg_config = cfs_read_file("sdn/microseg.cfg");
+ return PVE::RS::SDN::Microseg->config($microseg_config);
+}
+
+sub running_policy {
+ my ($running_cfg) = @_;
+ my $microseg_config = ($running_cfg->{microseg} // {})->{ids} // {};
+ return PVE::RS::SDN::Microseg->running_config($microseg_config);
+}
+
+sub write_config {
+ my ($config) = @_;
+ cfs_write_file("sdn/microseg.cfg", $config->to_raw(), 1);
+}
+
+my $MAX_NIC_INDEX = 31;
+
+my $COMMENT_PROPERTY = {
+ type => 'string',
+ optional => 1,
+ maxLength => 256,
+ description => "Free-form comment.",
+};
+
+my @MATCH_KINDS = ('any', 'all', 'exact');
+my @RULE_MATCH_KINDS = (@MATCH_KINDS, 'every');
+
+my sub delete_property {
+ my (@keys) = @_;
+
+ return {
+ type => 'array',
+ optional => 1,
+ items => {
+ type => 'string',
+ enum => [@keys],
+ },
+ };
+}
+
+sub group_properties {
+ my ($update) = @_;
+
+ my $properties = {
+ comment => $COMMENT_PROPERTY,
+ };
+
+ if ($update) {
+ $properties->{delete} = delete_property('comment');
+ }
+
+ return $properties;
+}
+
+sub rule_properties {
+ my ($update) = @_;
+
+ my $properties = {
+ allow => {
+ type => 'boolean',
+ optional => $update,
+ description =>
+ "0 = deny, 1 = allow. Traffic no rule matches is denied, and there is"
+ . " no connection tracking.",
+ },
+ prio => {
+ type => 'integer',
+ minimum => 0,
+ maximum => 65535,
+ optional => 1,
+ ($update ? () : (default => 0)),
+ description => "Priority. The highest-priority matching rule wins."
+ . " At a tie, a deny overrides an allow.",
+ },
+ comment => $COMMENT_PROPERTY,
+ };
+
+ # the id is derived from these
+ if (!$update) {
+ $properties->{src} = {
+ type => 'array',
+ items => get_standard_option('pve-sdn-microseg-id'),
+ optional => 1,
+ description => "Groups the source predicate is evaluated against, left out only"
+ . " with 'every'. The built-in 'untagged' group matches traffic that carries no"
+ . " group, for example gateway, DHCP, and ARP replies. Alone it works with each"
+ . " match mode that lists groups, together with other groups only with 'any'.",
+ };
+ $properties->{'src-match'} = {
+ type => 'string',
+ enum => [@RULE_MATCH_KINDS],
+ optional => 1,
+ default => 'any',
+ description => "How the source predicate matches. 'any' shares a listed group,"
+ . " 'all' has every listed group, 'exact' equals the listed set, 'every' lists"
+ . " none and fires on each interface that has a group.",
+ };
+ $properties->{dst} = {
+ type => 'array',
+ items => get_standard_option('pve-sdn-microseg-id'),
+ optional => 1,
+ description =>
+ "Groups the destination predicate is evaluated against, left out only with"
+ . " 'every'. Every enforced interface has a group, so the built-in 'untagged'"
+ . " group never matches a destination. It is accepted only with 'any' next to"
+ . " other groups, where it is ignored.",
+ };
+ $properties->{'dst-match'} = {
+ type => 'string',
+ enum => [@RULE_MATCH_KINDS],
+ optional => 1,
+ default => 'any',
+ description => "How the destination predicate matches, as for the source.",
+ };
+ $properties->{bidirectional} = {
+ type => 'boolean',
+ optional => 1,
+ default => 0,
+ description => "Also apply the rule with the source and destination predicates"
+ . " swapped. It is exactly that pair of rules and adds no connection tracking."
+ . " Not with the 'untagged' group.",
+ };
+ } else {
+ $properties->{delete} = delete_property('comment');
+ }
+
+ return $properties;
+}
+
+sub assignment_properties {
+ my ($update) = @_;
+
+ my $properties = {
+ groups => {
+ type => 'array',
+ items => get_standard_option('pve-sdn-microseg-id'),
+ optional => $update,
+ description => "Groups the matching NICs belong to.",
+ },
+ position => {
+ type => 'integer',
+ minimum => 0,
+ optional => 1,
+ description => "Where to put the assignment in the match order, 0 being the first"
+ . " place. A position past the end puts it last. A NIC belongs to the first"
+ . " assignment that covers it."
+ . ($update ? '' : ' A new assignment without a position goes first.'),
+ },
+ comment => $COMMENT_PROPERTY,
+ };
+
+ if (!$update) {
+ $properties->{type} = {
+ type => 'string',
+ enum => $ASSIGNMENT_TYPES,
+ description => "The matcher kind. 'guest-assignment' binds a specific guest.",
+ };
+ $properties->{vmid} = get_standard_option(
+ 'pve-vmid',
+ {
+ 'type-property' => 'type',
+ 'instance-types' => ['guest-assignment'],
+ description => "The guest whose NICs to bind.",
+ },
+ );
+ $properties->{iface} = {
+ type => 'integer',
+ minimum => 0,
+ maximum => $MAX_NIC_INDEX,
+ optional => 1,
+ description => "The guest's netN interface, or all of the guest's NICs if unset.",
+ };
+ } else {
+ $properties->{delete} = delete_property('comment');
+ }
+
+ return $properties;
+}
+
+sub assignment_object_properties {
+ my $properties = assignment_properties(0);
+
+ delete $properties->{position};
+ $properties->{order} = {
+ type => 'integer',
+ minimum => 0,
+ optional => 1,
+ description => 'Place in the match order, the lowest number first.',
+ };
+
+ return $properties;
+}
+
+# with pending=1 a new object has only its type, state and pending changes
+sub optional_properties {
+ my ($properties) = @_;
+
+ return { map { $_ => { $properties->{$_}->%*, optional => 1 } } keys $properties->%* };
+}
+
+sub guest_inventory {
+ my @props = map { "net$_" } 0 .. $MAX_NIC_INDEX;
+
+ my $by_vmid = PVE::Cluster::get_guest_config_properties(\@props);
+
+ my $inventory = [];
+ for my $vmid (sort { $a <=> $b } keys %$by_vmid) {
+ my $guest = $by_vmid->{$vmid};
+
+ my @nics = grep { defined $guest->{"net$_"} } 0 .. $MAX_NIC_INDEX;
+
+ push @$inventory,
+ {
+ vmid => $vmid,
+ nics => \@nics,
+ };
+ }
+
+ return $inventory;
+}
+
+sub audited_guest_inventory {
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $authuser = $rpcenv->get_user();
+
+ return [grep { $rpcenv->check($authuser, "/vms/$_->{vmid}", ['VM.Audit'], 1) }
+ guest_inventory()->@*];
+}
+
+sub type_matches {
+ my ($got, $want) = @_;
+ return 0 if !defined $got;
+ return (grep { $_ eq $got } @$want) ? 1 : 0;
+}
+
+1;
--
2.47.3
next prev parent reply other threads:[~2026-10-27 9:02 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 ` [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 ` Hannes Laimer [this message]
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-23-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