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 A812C1FF0AA for ; Tue, 20 Oct 2026 15:18:03 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 4B783216BA; Tue, 20 Oct 2026 15:18:02 +0200 (CEST) Message-ID: Date: Tue, 20 Oct 2026 15:17:57 +0200 MIME-Version: 1.0 User-Agent: Mozilla Thunderbird Subject: Re: [PATCH pve-network v3 10/17] sdn: zones: report interface plugs and unplugs to the DHCP backends To: pve-devel@lists.proxmox.com References: <20261020080005.470757-1-h.laimer@proxmox.com> <20261020080005.470757-11-h.laimer@proxmox.com> Content-Language: en-US From: Stefan Hanreich In-Reply-To: <20261020080005.470757-11-h.laimer@proxmox.com> Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 7bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.363 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: 4JTJY46U3FHIEOXZO7KHXN2U3TFMFO3Z X-Message-ID-Hash: 4JTJY46U3FHIEOXZO7KHXN2U3TFMFO3Z X-MailFrom: s.hanreich@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: Reviewed-by: Stefan Hanreich Tested-by: Stefan Hanreich On 10/20/26 10:01 AM, Hannes Laimer wrote: > 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 > --- > 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 db6fc472..783d0d47 100755 > --- a/src/test/run_test_vnets_blackbox.pl > +++ b/src/test/run_test_vnets_blackbox.pl > @@ -1844,6 +1844,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 {