From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id 5AB901FF0DF for ; Thu, 05 Nov 2026 12:19:58 +0100 (CET) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id EC99321AB7; Thu, 05 Nov 2026 12:15:51 +0100 (CET) From: Hannes Laimer To: pve-devel@lists.proxmox.com Subject: [PATCH pve-container v4 43/45] lxc: update the microseg enforcement on a tag or hostname change Date: Thu, 5 Nov 2026 12:13:22 +0100 Message-ID: <20261105111324.706922-44-h.laimer@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20261105111324.706922-1-h.laimer@proxmox.com> References: <20261105111324.706922-1-h.laimer@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1793877254166 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.291 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: P4YSNBFB3FF3SQGLDJQPYPV7RTFVOFAC X-Message-ID-Hash: P4YSNBFB3FF3SQGLDJQPYPV7RTFVOFAC X-MailFrom: h.laimer@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: The groups of a container's NICs depend on its tags and hostname, but a change of those took effect at the next start of the container or the next network reload of the node. Update the NICs of a running container once the new config is written, so the change applies right away. The update reads the config under the same lock as a network reload, so a reload running at the same time cannot leave the old groups in place. A failed update only warns, and the next reload or start catches up. Moving a container between groups is an SDN change like putting a NIC on a vnet, which needs SDN.Use on the vnet. So a tag or hostname change that can move the container also needs SDN.Use on /sdn/microseg, and so does creating, cloning or restoring a container with such a tag or hostname. A destroy with purge removes the container's own assignments from the policy, as it removes the container from its backup and replication jobs. Assignments by tag or hostname are not the container's and stay. Signed-off-by: Hannes Laimer --- src/PVE/API2/LXC.pm | 14 ++++++++++++++ src/PVE/API2/LXC/Config.pm | 3 +++ src/PVE/LXC.pm | 28 ++++++++++++++++++++++++++++ src/PVE/LXC/Create.pm | 4 ++++ 4 files changed, 49 insertions(+) diff --git a/src/PVE/API2/LXC.pm b/src/PVE/API2/LXC.pm index 63d30d17dfaf..bca4c26c914d 100644 --- a/src/PVE/API2/LXC.pm +++ b/src/PVE/API2/LXC.pm @@ -334,6 +334,8 @@ __PACKAGE__->register_method({ PVE::LXC::check_ct_modify_config_perm( $rpcenv, $authuser, $vmid, $pool, undef, $param, [], $unprivileged, ); + PVE::Network::SDN::Microseg::check_name_access($rpcenv, $authuser, undef, "CT$vmid") + if !$restore && !defined($param->{hostname}); my $storage_cfg = cfs_read_file("storage.cfg"); @@ -919,6 +921,12 @@ __PACKAGE__->register_method({ print "purging CT $vmid from related configurations..\n"; PVE::ReplicationConfig::remove_vmid_jobs($vmid); PVE::VZDump::Plugin::remove_vmid_from_backup_jobs($vmid); + my $assignments = + eval { PVE::Network::SDN::Microseg::remove_guest_assignments($vmid) }; + warn "unable to remove the microseg assignments of CT $vmid: $@" if $@; + print + "NOTE: removed the microseg assignments of CT $vmid, pending an SDN apply.\n" + if $assignments; if ($ha_managed) { PVE::HA::Config::delete_service_from_config("ct:$vmid", $param->{purge}); @@ -2138,6 +2146,9 @@ __PACKAGE__->register_method({ $newconf->{$opt} = $value; } } + PVE::Network::SDN::Microseg::check_tag_access( + $rpcenv, $authuser, undef, $newconf->{tags}, + ) if defined($newconf->{tags}); die "can't clone CT to node '$target' (CT uses local storage)\n" if $target && !$sharedvm; @@ -2151,6 +2162,9 @@ __PACKAGE__->register_method({ delete $newconf->{template}; $newconf->{hostname} = $param->{hostname} if $param->{hostname}; + PVE::Network::SDN::Microseg::check_name_access( + $rpcenv, $authuser, undef, $newconf->{hostname}, + ) if defined($newconf->{hostname}); $newconf->{description} = $param->{description} if $param->{description}; $lock_and_reload->( diff --git a/src/PVE/API2/LXC/Config.pm b/src/PVE/API2/LXC/Config.pm index 805eac0eac38..1ba8c5d865de 100644 --- a/src/PVE/API2/LXC/Config.pm +++ b/src/PVE/API2/LXC/Config.pm @@ -262,6 +262,9 @@ __PACKAGE__->register_method({ $conf = PVE::LXC::Config->load_config($vmid); PVE::LXC::update_lxc_config($vmid, $conf); + # reads the tags and hostname from pmxcfs, so after the write + PVE::LXC::update_nets_enforcement($conf, $vmid) + if $running && grep { $_ eq 'tags' || $_ eq 'hostname' } (keys %$param, @delete); raise_param_exc($errors) if scalar(keys %$errors); }; diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm index 59d803d2592c..c8fa96ca81c6 100644 --- a/src/PVE/LXC.pm +++ b/src/PVE/LXC.pm @@ -25,6 +25,7 @@ use PVE::GuestHelpers qw(check_vnet_access safe_string_ne safe_num_ne safe_boole use PVE::INotify; use PVE::JSONSchema qw(get_standard_option); use PVE::Network; +use PVE::Network::SDN::Microseg; use PVE::Network::SDN::Vnets; use PVE::Network::SDN::Zones; use PVE::ProcFSTools; @@ -1097,6 +1098,21 @@ sub net_tap_plug : prototype($$) { PVE::Tools::run_command(['/sbin/ip', 'link', 'set', 'dev', $iface, 'up']); } +# the groups of a NIC depend on the container's tags and hostname +sub update_nets_enforcement { + my ($conf, $vmid) = @_; + + for my $opt (keys %$conf) { + next if $opt !~ m/^net(\d+)$/; + my $iface = "veth${vmid}i$1"; + next if !-d "/sys/class/net/$iface"; + my $net = PVE::LXC::Config->parse_lxc_network($conf->{$opt}); + next if ($net->{type} // '') ne 'veth'; + my $firewall = $net->{firewall} && PVE::Firewall::Helpers::needs_fwbr($net->{bridge}); + PVE::Network::SDN::Zones::tap_update($iface, $firewall); + } +} + sub update_net { my ($vmid, $conf, $opt, $newnet, $netid, $rootdir) = @_; @@ -1656,6 +1672,12 @@ sub check_ct_modify_config_perm { raise_perm_exc("configuring device passthrough is only allowed for root\@pam"); } elsif ($opt eq 'nameserver' || $opt eq 'searchdomain' || $opt eq 'hostname') { $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Network']); + PVE::Network::SDN::Microseg::check_name_access( + $rpcenv, + $authuser, + $oldconf->{$opt}, + $delete ? undef : $newconf->{$opt}, + ) if $opt eq 'hostname'; } elsif ($opt eq 'features') { raise_perm_exc( "changing feature flags for privileged container is only allowed for root\@pam") @@ -1709,6 +1731,12 @@ sub check_ct_modify_config_perm { my $old = $oldconf->{$opt}; my $new = $delete ? '' : $newconf->{$opt}; PVE::GuestHelpers::assert_tag_permissions($vmid, $old, $new, $rpcenv, $authuser); + PVE::Network::SDN::Microseg::check_tag_access( + $rpcenv, + $authuser, + $old, + PVE::GuestHelpers::get_unique_tags($new), + ); } else { $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Options']); } diff --git a/src/PVE/LXC/Create.pm b/src/PVE/LXC/Create.pm index 08b65cab7677..c4751b11b424 100644 --- a/src/PVE/LXC/Create.pm +++ b/src/PVE/LXC/Create.pm @@ -578,6 +578,10 @@ sub sanitize_and_merge_config { } $conf->{$key} = $oldconf->{$key} if !defined($conf->{$key}); } + PVE::Network::SDN::Microseg::check_tag_access($rpcenv, $authuser, undef, $conf->{tags}) + if defined($conf->{tags}); + PVE::Network::SDN::Microseg::check_name_access($rpcenv, $authuser, undef, $conf->{hostname}) + if defined($conf->{hostname}); } sub restore_configuration_from_etc_vzdump { -- 2.47.3