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 ABB9D1FF0A5 for ; Fri, 04 Sep 2026 11:40:27 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 0C2E62163F; Fri, 04 Sep 2026 11:39:51 +0200 (CEST) From: Hannes Laimer To: pve-devel@lists.proxmox.com Subject: [PATCH pve-network 11/12] tests: cover the ebpf dhcp backend and ipam API mapping pushes Date: Fri, 4 Sep 2026 11:38:34 +0200 Message-ID: <20260904093835.1050030-12-h.laimer@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260904093835.1050030-1-h.laimer@proxmox.com> References: <20260904093835.1050030-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: 1788514726092 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.594 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: Z66RLGFAJHDZT4E7LB2X6DKIAMNU6HDW X-Message-ID-Hash: Z66RLGFAJHDZT4E7LB2X6DKIAMNU6HDW 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: Signed-off-by: Hannes Laimer --- src/test/run_test_vnets_blackbox.pl | 231 ++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) diff --git a/src/test/run_test_vnets_blackbox.pl b/src/test/run_test_vnets_blackbox.pl index 9f4c424..5b35320 100755 --- a/src/test/run_test_vnets_blackbox.pl +++ b/src/test/run_test_vnets_blackbox.pl @@ -46,6 +46,7 @@ sub clear_test_state { vnets_config => {}, macdb => {}, ipamdb => {}, + ebpf_calls => [], ipam_config => { 'ids' => { 'pve' => { @@ -103,6 +104,13 @@ my $mocked_pve_sdn; $mocked_pve_sdn = Test::MockModule->new('PVE::Network::SDN'); $mocked_pve_sdn->mock( cfs_lock_file => $mocked_cfs_lock_file, + running_config => sub { + return { + zones => $test_state->{zones_config}, + vnets => $test_state->{vnets_config}, + subnets => $test_state->{subnets_config}, + }; + }, ); my $mocked_pve_tools = Test::MockModule->new('PVE::Tools'); @@ -224,6 +232,26 @@ $mocked_sdn_dhcp_dnsmasq->mock( update_lease => sub { }, ); +my $mocked_sdn_dhcp = Test::MockModule->new('PVE::Network::SDN::Dhcp'); +$mocked_sdn_dhcp->mock( + notify_guest_node => sub { }, +); + +my $mocked_sdn_dhcp_ebpf = Test::MockModule->new('PVE::Network::SDN::Dhcp::Ebpf'); +$mocked_sdn_dhcp_ebpf->mock( + cfs_lock_file => $mocked_cfs_lock_file, +); + +my $mocked_pve_rs_dhcp = Test::MockModule->new('PVE::RS::SDN::Dhcp'); +$mocked_pve_rs_dhcp->mock( + map { + my $method = $_; + $method => sub { + push $test_state->{ebpf_calls}->@*, { method => $method, args => [@_] }; + }; + } qw(apply attach clear) +); + my $mocked_api_zones = Test::MockModule->new('PVE::API2::Network::SDN::Zones'); $mocked_api_zones->mock( create_etc_interfaces_sdn_dir => sub { }, @@ -321,6 +349,16 @@ sub create_subnet { PVE::API2::Network::SDN::Subnets->create($params); } +sub update_zone { + my ($zoneid, $params) = @_; + PVE::API2::Network::SDN::Zones->update({ zone => $zoneid, %$params }); +} + +sub update_subnet { + my ($params) = @_; + PVE::API2::Network::SDN::Subnets->update($params); +} + sub get_ipam_entries { return PVE::API2::Network::SDN::Ipams->ipamindex({ ipam => "pve" }); } @@ -330,6 +368,22 @@ sub create_ip { return PVE::API2::Network::SDN::Ips->ipcreate($param); } +sub update_ip { + my ($param) = @_; + return PVE::API2::Network::SDN::Ips->ipupdate($param); +} + +sub delete_ip { + my ($param) = @_; + return PVE::API2::Network::SDN::Ips->ipdelete($param); +} + +sub take_ebpf_calls { + my $calls = $test_state->{ebpf_calls}; + $test_state->{ebpf_calls} = []; + return $calls; +} + sub run_test { my $test = shift; clear_test_state(); @@ -963,4 +1017,181 @@ run_test( 2, ); +# -------------- ebpf dhcp backend +sub test_ebpf_backend { + my $test_name = (split(/::/, (caller(0))[3]))[-1]; + my $zoneid = "TESTZONE"; + my $vnetid = "testvnet"; + my $mac = "da:65:8f:18:9b:6f"; + + 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"], + 'dhcp-lease-time' => 300, + }); + + take_ebpf_calls(); + + # guest start allocates from the range and applies the full state + eval { nic_start($vnetid, $mac, "999", "testhostname"); }; + if ($@) { + fail("$test_name: nic_start: $@"); + return; + } + + my $calls = take_ebpf_calls(); + my $record = sub { + my ($ip) = @_; + return { + mac => $mac, + ip => $ip, + prefixlen => 24, + server_id => "10.0.0.1", + lease => 300, + router => "10.0.0.1", + dns => undef, + mtu => 1500, + }; + }; + + eq_or_diff( + $calls, + [{ method => 'apply', args => [[], [$record->("10.0.0.100")]] }], + "$test_name: guest start applies the full state", + ); + + # a mapping edit through the API applies the full state once + update_ip({ + zone => $zoneid, + vnet => $vnetid, + mac => $mac, + ip => "10.0.0.150", + }); + + $calls = take_ebpf_calls(); + eq_or_diff( + $calls, + [{ method => 'apply', args => [[], [$record->("10.0.0.150")]] }], + "$test_name: mapping edit applies the new state", + ); + + # a full regenerate is the same full pass + PVE::Network::SDN::Dhcp::regenerate_config(); + + $calls = take_ebpf_calls(); + eq_or_diff( + $calls, + [{ method => 'apply', args => [[], [$record->("10.0.0.150")]] }], + "$test_name: regenerate applies the full state", + ); + + # deleting the mapping drops the record + delete_ip({ + zone => $zoneid, + vnet => $vnetid, + mac => $mac, + ip => "10.0.0.150", + }); + + $calls = take_ebpf_calls(); + eq_or_diff( + $calls, + [{ method => 'apply', args => [[], []] }], + "$test_name: mapping delete applies the emptied state", + ); +} + +sub test_ebpf_backend_edge_cases { + my $test_name = (split(/::/, (caller(0))[3]))[-1]; + my $zoneid = "TESTZONE"; + my $vnetid = "testvnet"; + my $mac = "da:65:8f:18:9b:6f"; + + # a regenerate without any ebpf zone tears the responder state down + create_zone({ + type => "simple", + ipam => "pve", + zone => $zoneid, + }); + take_ebpf_calls(); + PVE::Network::SDN::Dhcp::regenerate_config(); + eq_or_diff( + take_ebpf_calls(), + [{ method => 'clear', args => [] }], + "$test_name: regenerate without an ebpf zone clears the responder", + ); + + # the zone switches to ebpf, its subnets lack what a record needs + update_zone($zoneid, { dhcp => "ebpf" }); + create_vnet({ + type => "vnet", + zone => $zoneid, + vnet => $vnetid, + }); + create_subnet({ + type => "subnet", + vnet => $vnetid, + subnet => "10.0.0.0/24", + 'dhcp-range' => ["start-address=10.0.0.100,end-address=10.0.0.200"], + }); + create_subnet({ + type => "subnet", + vnet => $vnetid, + subnet => "fd00::/64", + gateway => "fd00::1", + 'dhcp-range' => ["start-address=fd00::100,end-address=fd00::200"], + }); + + take_ebpf_calls(); + eval { nic_start($vnetid, $mac, "999", "testhostname"); }; + if ($@) { + fail("$test_name: nic_start: $@"); + return; + } + + # a gateway-less v4 subnet and a v6 subnet produce no record, the pass + # still runs once per allocated family with the guest interfaces alone + my $calls = take_ebpf_calls(); + eq_or_diff( + $calls, + [{ method => 'apply', args => [[], []] }, { method => 'apply', args => [[], []] }], + "$test_name: subnets without a gateway or over IPv6 yield no record", + ); + + # a v6 resolver on a v4 subnet is dropped from the record instead of + # failing the whole set + update_subnet({ + vnet => $vnetid, + subnet => "$zoneid-10.0.0.0-24", + gateway => "10.0.0.1", + 'dhcp-dns-server' => "fd00::53", + }); + take_ebpf_calls(); + PVE::Network::SDN::Dhcp::regenerate_config(); + $calls = take_ebpf_calls(); + is(scalar(@$calls), 1, "$test_name: regenerate applies once"); + my $records = $calls->[0]->{args}->[1]; + is(scalar(@$records), 1, "$test_name: the v4 subnet now yields the record"); + is($records->[0]->{dns}, undef, "$test_name: the IPv6 resolver is not handed out"); +} + +run_test(\&test_ebpf_backend); +run_test(\&test_ebpf_backend_edge_cases); + done_testing(); -- 2.47.3