From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 953D51FF0A7 for ; Wed, 02 Sep 2026 14:49:11 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 09C99215F0; Wed, 02 Sep 2026 14:48:34 +0200 (CEST) From: Hannes Laimer To: pve-devel@lists.proxmox.com Subject: [PATCH pve-network 09/12] sdn: dhcp: apply mapping edits on the node serving the guest Date: Wed, 2 Sep 2026 14:47:36 +0200 Message-ID: <20260902124739.750853-10-h.laimer@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260902124739.750853-1-h.laimer@proxmox.com> References: <20260902124739.750853-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: 1788353266200 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.669 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: VG52TAULWA5NF37Y2YDUZIOV5IMCQINQ X-Message-ID-Hash: VG52TAULWA5NF37Y2YDUZIOV5IMCQINQ 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: Mapping edits change cluster-wide records, but the ebpf backend answers from a per-node map, so an edit made through another node's API left the map of the node running the guest stale until the next apply or guest start there. Resolve the MAC to its guest and re-apply the mapping on that node through a node-scoped call, the same remove-and-add the editing node runs locally. No other node needs the record, each picks it up at that guest's next start there, so the cost of an edit stays one call however large the cluster is. The push runs detached from the edit request, an unreachable node cannot hold the edit up, it just catches up on its next apply or the guest's next start. Signed-off-by: Hannes Laimer --- src/PVE/API2/Network/SDN/Ips.pm | 5 +- src/PVE/API2/Network/SDN/Nodes/Status.pm | 37 +++++++++++- src/PVE/Network/SDN/Dhcp.pm | 74 ++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 4 deletions(-) diff --git a/src/PVE/API2/Network/SDN/Ips.pm b/src/PVE/API2/Network/SDN/Ips.pm index fc76c90..63bfce6 100644 --- a/src/PVE/API2/Network/SDN/Ips.pm +++ b/src/PVE/API2/Network/SDN/Ips.pm @@ -17,9 +17,8 @@ my sub update_dhcp_mapping { my ($vnet, $mac) = @_; eval { - my ($ip4, $ip6) = PVE::Network::SDN::Vnets::get_ips_from_mac($vnet, $mac); - PVE::Network::SDN::Dhcp::remove_mapping($vnet, $mac); - PVE::Network::SDN::Dhcp::add_mapping($vnet, $mac, $ip4, $ip6) if $ip4 || $ip6; + PVE::Network::SDN::Dhcp::update_mapping($vnet, $mac); + PVE::Network::SDN::Dhcp::notify_guest_node($vnet, $mac); }; warn "could not update dhcp mapping for $mac: $@" if $@; } diff --git a/src/PVE/API2/Network/SDN/Nodes/Status.pm b/src/PVE/API2/Network/SDN/Nodes/Status.pm index 7977e0c..f11fd48 100644 --- a/src/PVE/API2/Network/SDN/Nodes/Status.pm +++ b/src/PVE/API2/Network/SDN/Nodes/Status.pm @@ -9,6 +9,8 @@ use PVE::API2::Network::SDN::Nodes::Vnets; use PVE::JSONSchema qw(get_standard_option); +use PVE::Network::SDN::Dhcp; + use PVE::RESTHandler; use base qw(PVE::RESTHandler); @@ -27,6 +29,36 @@ __PACKAGE__->register_method({ path => 'vnets', }); +__PACKAGE__->register_method({ + name => 'dhcp_mapping', + path => 'dhcp-mapping', + method => 'POST', + description => + 'Re-apply the DHCP mapping of a MAC address on this node from the current records.', + permissions => { + check => ['perm', '/sdn/zones/{zone}/{vnet}', ['SDN.Allocate']], + }, + protected => 1, + proxyto => 'node', + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + zone => get_standard_option('pve-sdn-zone-id'), + vnet => get_standard_option('pve-sdn-vnet-id'), + mac => get_standard_option('mac-addr'), + }, + }, + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + PVE::Network::SDN::Dhcp::update_mapping($param->{vnet}, $param->{mac}); + + return undef; + }, +}); + __PACKAGE__->register_method({ name => 'sdnindex', path => '', @@ -52,7 +84,10 @@ __PACKAGE__->register_method({ my ($param) = @_; my $result = [ - { name => 'fabrics' }, { name => 'vnets' }, { name => 'zones' }, + { name => 'dhcp-mapping' }, + { name => 'fabrics' }, + { name => 'vnets' }, + { name => 'zones' }, ]; return $result; }, diff --git a/src/PVE/Network/SDN/Dhcp.pm b/src/PVE/Network/SDN/Dhcp.pm index f046dfd..6bc9bad 100644 --- a/src/PVE/Network/SDN/Dhcp.pm +++ b/src/PVE/Network/SDN/Dhcp.pm @@ -3,6 +3,8 @@ package PVE::Network::SDN::Dhcp; use strict; use warnings; +use POSIX qw(); + use PVE::Cluster; use PVE::Network::SDN; @@ -59,6 +61,78 @@ sub remove_mapping { $dhcp_plugin->del_ip_mapping($zoneid, $mac); } +# re-apply a MAC's mapping on this node from the current records +sub update_mapping { + my ($vnetid, $mac) = @_; + + my ($ip4, $ip6) = PVE::Network::SDN::Vnets::get_ips_from_mac($vnetid, $mac); + remove_mapping($vnetid, $mac); + add_mapping($vnetid, $mac, $ip4, $ip6) if $ip4 || $ip6; +} + +# the guest config lines come from pmxcfs in one go, no guest config +# gets parsed for this +my sub guest_node_by_mac { + my ($mac) = @_; + + my $vmlist = PVE::Cluster::get_vmlist(); + my $nets = PVE::Cluster::get_guest_config_properties([map { "net$_" } 0 .. 31]); + for my $vmid (keys %$nets) { + for my $net (values %{ $nets->{$vmid} }) { + return $vmlist->{ids}->{$vmid}->{node} if $net =~ m/=\Q$mac\E(?:,|$)/i; + } + } + + return undef; +} + +# The records are cluster-wide, but a node answers from its own map, +# so an edit has to reach the node running the guest behind the MAC. +# Every other node picks the record up at that guest's next start +# there. The push runs detached from the request, an unreachable node +# must not hold the edit up. +sub notify_guest_node { + my ($vnetid, $mac) = @_; + + my $vnet = PVE::Network::SDN::Vnets::get_vnet($vnetid, 1); + return if !$vnet; + + my $zone = PVE::Network::SDN::Zones::get_zone($vnet->{zone}, 1); + return if !$zone || !$zone->{dhcp} || $zone->{dhcp} ne 'ebpf'; + + my $node = guest_node_by_mac($mac); + return if !$node || $node eq PVE::INotify::nodename(); + + # double fork, the grandchild is reparented to init so nothing on + # the request path ever waits on it + my $pid = fork(); + if (!defined($pid)) { + warn "could not fork for the dhcp mapping push to $node: $!\n"; + return; + } + if ($pid) { + waitpid($pid, 0); + return; + } + + POSIX::setsid(); + my $child = fork(); + POSIX::_exit(1) if !defined($child); + POSIX::_exit(0) if $child; + + exec( + 'pvesh', + 'create', + "/nodes/$node/sdn/dhcp-mapping", + '--zone', + $vnet->{zone}, + '--vnet', + $vnetid, + '--mac', + $mac, + ) or POSIX::_exit(1); +} + sub regenerate_config { my ($reload) = @_; -- 2.47.3