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 A66991FF0C0 for ; Wed, 21 Oct 2026 10:47:01 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id AFABF2171E; Wed, 21 Oct 2026 10:45:51 +0200 (CEST) From: Hannes Laimer To: pve-devel@lists.proxmox.com Subject: [PATCH pve-network v4 12/20] sdn: zones: report interface plugs and unplugs to the DHCP backends Date: Wed, 21 Oct 2026 10:44:37 +0200 Message-ID: <20261021084445.462195-13-h.laimer@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20261021084445.462195-1-h.laimer@proxmox.com> References: <20261021084445.462195-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: 1792572314195 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.364 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: 3RYMLPE7AJWWPQPPJVEQLSURMSIIJ7T7 X-Message-ID-Hash: 3RYMLPE7AJWWPQPPJVEQLSURMSIIJ7T7 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 ebpf DHCP backend answers on each guest interface rather than on the bridge, so the zone layer now reports every plug to the DHCP backends. The plug takes the guest's MAC along, since the address to hand out is recorded per MAC. A plug onto a plain bridge is reported too, since a running guest's interface can move there from a vnet. A failing backend only warns and the plug goes ahead. The zone layer also gains an unplug and a veth removal, which report the interface to the backends as gone. Signed-off-by: Hannes Laimer Reviewed-by: Stefan Hanreich Tested-by: Stefan Hanreich --- src/PVE/Network/SDN/Zones.pm | 29 ++++++- src/test/run_test_vnets_blackbox.pl | 129 ++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+), 4 deletions(-) diff --git a/src/PVE/Network/SDN/Zones.pm b/src/PVE/Network/SDN/Zones.pm index 4229feda..10fc0731 100644 --- a/src/PVE/Network/SDN/Zones.pm +++ b/src/PVE/Network/SDN/Zones.pm @@ -326,16 +326,18 @@ sub veth_create { } sub tap_plug { - my ($iface, $bridge, $tag, $firewall, $trunks, $rate) = @_; + my ($iface, $bridge, $tag, $firewall, $trunks, $rate, $opts) = @_; my $vnet = PVE::Network::SDN::Vnets::get_vnet($bridge, 1); if (!$vnet) { # fallback for classic bridge my $interfaces_config = PVE::INotify::read_file('interfaces'); - my $opts = {}; - $opts->{learning} = 0 + my $bridge_opts = {}; + $bridge_opts->{learning} = 0 if $interfaces_config->{ifaces}->{$bridge} && $interfaces_config->{ifaces}->{$bridge}->{'bridge-disable-mac-learning'}; - PVE::Network::tap_plug($iface, $bridge, $tag, $firewall, $trunks, $rate, $opts); + # the dhcp backends hear of an interface moving onto a plain bridge as well + PVE::Network::SDN::Dhcp::tap_plug($bridge, $iface, $opts->{mac}); + PVE::Network::tap_plug($iface, $bridge, $tag, $firewall, $trunks, $rate, $bridge_opts); return; } @@ -346,9 +348,28 @@ sub tap_plug { if $plugin_config->{nodes} && !defined($plugin_config->{nodes}->{$nodename}); my $plugin = PVE::Network::SDN::Zones::Plugin->lookup($plugin_config->{type}); + + # the dhcp backends act before the interface joins the bridge, so no request slips past + # them. Dhcp.pm comes in through Vnets.pm, since a use of it here would create a load-order + # cycle + PVE::Network::SDN::Dhcp::tap_plug($bridge, $iface, $opts->{mac}); $plugin->tap_plug($plugin_config, $vnet, $tag, $iface, $bridge, $firewall, $trunks, $rate); } +sub tap_unplug { + my ($iface) = @_; + + PVE::Network::SDN::Dhcp::tap_unplug($iface); + PVE::Network::tap_unplug($iface); +} + +sub veth_delete { + my ($veth) = @_; + + PVE::Network::SDN::Dhcp::tap_unplug($veth); + PVE::Network::veth_delete($veth); +} + sub add_bridge_fdb { my ($iface, $macaddr, $bridge) = @_; diff --git a/src/test/run_test_vnets_blackbox.pl b/src/test/run_test_vnets_blackbox.pl index 30a4ab82..516b13c6 100755 --- a/src/test/run_test_vnets_blackbox.pl +++ b/src/test/run_test_vnets_blackbox.pl @@ -1858,6 +1858,135 @@ sub test_ebpf_backend_edge_cases { } run_test(\&test_ebpf_backend); + +sub test_zone_tap_plug { + my $test_name = (split(/::/, (caller(0))[3]))[-1]; + my $zoneid = "TESTZONE"; + my $vnetid = "testvnet"; + my $mac = "da:65:8f:18:9b:6f"; + + # the zone plug hands the MAC on to the dhcp backends, and a plug onto a + # plain bridge tells them where the interface went + create_zone({ + type => "simple", + dhcp => "ebpf", + ipam => "pve", + zone => $zoneid, + }); + create_vnet({ + type => "vnet", + zone => $zoneid, + vnet => $vnetid, + }); + create_subnet({ + type => "subnet", + vnet => $vnetid, + subnet => "10.0.0.0/24", + gateway => "10.0.0.1", + 'dhcp-range' => ["start-address=10.0.0.100,end-address=10.0.0.200"], + }); + create_ip({ + zone => $zoneid, + vnet => $vnetid, + mac => $mac, + ip => "10.0.0.100", + }); + my $bridged = []; + my $unplugged = []; + $mocked_sdn_zones_super_plugin->mock( + tap_plug => sub { + push @$bridged, $_[5]; + push $test_state->{order}->@*, 'bridge'; + }, + ); + my $mocked_pve_network = Test::MockModule->new('PVE::Network'); + $mocked_pve_network->mock( + tap_plug => sub { + push @$bridged, $_[1]; + push $test_state->{order}->@*, 'bridge'; + }, + tap_unplug => sub { push @$unplugged, $_[0]; }, + veth_delete => sub { push @$unplugged, $_[0]; }, + ); + $mocked_pve_inotify->mock(read_file => sub { return { ifaces => {} }; }); + take_ebpf_calls(); + take_order(); + + PVE::Network::SDN::Zones::tap_plug('tap999i0', $vnetid, undef, 0, undef, undef, + { mac => $mac }); + eq_or_diff($bridged, [$vnetid], "$test_name: the zone plugin plugs the interface"); + eq_or_diff( + take_ebpf_calls(), + [{ method => 'attach', args => ['tap999i0', $vnetid, $ebpf_record->("10.0.0.100")] }], + "$test_name: and the dhcp backend attaches it with the MAC's record", + ); + is_deeply( + take_order(), + ['lock', 'refresh', 'write', 'bridge'], + "$test_name: the responder attaches before the interface joins the bridge", + ); + + @$bridged = (); + PVE::Network::SDN::Zones::tap_plug('tap999i0', 'vmbr0', undef, 0, undef, undef, + { mac => $mac }); + eq_or_diff($bridged, ['vmbr0'], "$test_name: a plain bridge is plugged the classic way"); + eq_or_diff( + take_ebpf_calls(), + [{ method => 'detach', args => ['tap999i0', { vnet => undef }] }], + "$test_name: and the dhcp backends hear where the interface went", + ); + + # a vnet of another backend is a place the ebpf backend hears of as well + create_zone({ + type => "simple", + dhcp => "dnsmasq", + ipam => "pve", + zone => "DNSZONE", + }); + create_vnet({ + type => "vnet", + zone => "DNSZONE", + vnet => "dnsvnet", + }); + @$bridged = (); + PVE::Network::SDN::Zones::tap_plug( + 'tap999i0', + 'dnsvnet', + undef, + 0, + undef, + undef, + { mac => $mac }, + ); + eq_or_diff( + $bridged, + ['dnsvnet'], + "$test_name: a vnet of another backend is plugged by its zone", + ); + eq_or_diff( + take_ebpf_calls(), + [{ method => 'detach', args => ['tap999i0', { vnet => 'dnsvnet' }] }], + "$test_name: and the ebpf backend hears which vnet the interface went to", + ); + + # the zone unplug and the veth delete report the interface gone before + # the classic ones run + PVE::Network::SDN::Zones::tap_unplug('tap999i0'); + PVE::Network::SDN::Zones::veth_delete('veth999i0'); + eq_or_diff($unplugged, ['tap999i0', 'veth999i0'], "$test_name: the classic unplugs run"); + eq_or_diff( + take_ebpf_calls(), + [ + { method => 'detach', args => ['tap999i0', undef] }, + { method => 'detach', args => ['veth999i0', undef] }, + ], + "$test_name: and the dhcp backends hear the interfaces are gone", + ); + $mocked_sdn_zones_super_plugin->unmock('tap_plug'); + $mocked_pve_inotify->unmock('read_file'); +} + +run_test(\&test_zone_tap_plug); run_test(\&test_ebpf_backend_edge_cases); sub test_ebpf_mac_case { -- 2.47.3