From: Hannes Laimer <h.laimer@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH pve-network v6 10/17] sdn: zones: allow the ebpf DHCP backend on every zone type
Date: Thu, 5 Nov 2026 11:03:09 +0100 [thread overview]
Message-ID: <20261105100316.449163-11-h.laimer@proxmox.com> (raw)
In-Reply-To: <20261105100316.449163-1-h.laimer@proxmox.com>
The ebpf backend answers each guest on the guest's own tap or veth
device, so the zone type makes no difference to it. dnsmasq stays
limited to simple zones, since every node runs its own instance on
the vnet bridge and only a simple zone's bridge is cut off from the
other nodes. Choosing it elsewhere fails on create and on update.
Every zone type now reports the MTU its guests should use. The ebpf
backend hands it out, and until now only simple zones could answer.
A VXLAN or EVPN zone without an MTU setting takes it from the
interface that reaches its peers.
Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
Notes:
v5:
* the VXLAN MTU helper moved into the preceding MTU fix
src/PVE/API2/Network/SDN/Zones.pm | 8 +-
src/PVE/Network/SDN/Zones/EvpnPlugin.pm | 32 +++++++
src/PVE/Network/SDN/Zones/FaucetPlugin.pm | 1 +
src/PVE/Network/SDN/Zones/QinQPlugin.pm | 7 ++
src/PVE/Network/SDN/Zones/SimplePlugin.pm | 2 +-
src/PVE/Network/SDN/Zones/VlanPlugin.pm | 7 ++
src/PVE/Network/SDN/Zones/VxlanPlugin.pm | 13 +++
src/test/run_test_vnets_blackbox.pl | 109 +++++++++++++++++++++-
8 files changed, 175 insertions(+), 4 deletions(-)
diff --git a/src/PVE/API2/Network/SDN/Zones.pm b/src/PVE/API2/Network/SDN/Zones.pm
index 4f61e1e858aa..97ed930fad5a 100644
--- a/src/PVE/API2/Network/SDN/Zones.pm
+++ b/src/PVE/API2/Network/SDN/Zones.pm
@@ -60,7 +60,7 @@ my $ZONE_PROPERTIES = {
type => 'string',
enum => PVE::Network::SDN::Dhcp->plugin_types(),
optional => 1,
- description => 'Name of DHCP server backend for this zone.',
+ description => 'DHCP backend serving the zone. dnsmasq works on simple zones only.',
},
'rt-import' => {
type => 'string',
@@ -406,6 +406,9 @@ __PACKAGE__->register_method({
raise_param_exc({ dnszone => "missing dns server" })
if $dnszone && !$dnsserver;
+ raise_param_exc({ dhcp => "the dnsmasq backend only supports simple zones" })
+ if ($opts->{dhcp} // '') eq 'dnsmasq' && $plugin->type() ne 'simple';
+
$zone_cfg->{ids}->{$id} = $opts;
$plugin->on_update_hook($id, $zone_cfg, $controller_cfg);
@@ -493,6 +496,9 @@ __PACKAGE__->register_method({
my $dnszone = $opts->{dnszone};
raise_param_exc({ dnszone => "missing dns server" }) if $dnszone && !$dnsserver;
+ raise_param_exc({ dhcp => "the dnsmasq backend only supports simple zones" })
+ if ($scfg->{dhcp} // '') eq 'dnsmasq' && $plugin->type() ne 'simple';
+
$plugin->on_update_hook($id, $zone_cfg, $controller_cfg);
PVE::Network::SDN::Zones::write_config($zone_cfg);
diff --git a/src/PVE/Network/SDN/Zones/EvpnPlugin.pm b/src/PVE/Network/SDN/Zones/EvpnPlugin.pm
index 1720e1ff5735..78ab3e8dfe53 100644
--- a/src/PVE/Network/SDN/Zones/EvpnPlugin.pm
+++ b/src/PVE/Network/SDN/Zones/EvpnPlugin.pm
@@ -129,10 +129,42 @@ sub options {
reversedns => { optional => 1 },
dnszone => { optional => 1 },
ipam => { optional => 1 },
+ dhcp => { optional => 1 },
};
}
# Plugin implementation
+# the underlay interface is the one the controller's peers name, found the
+# way the bridge generation finds it
+sub get_mtu {
+ my ($class, $plugin_config) = @_;
+
+ return $plugin_config->{mtu} if $plugin_config->{mtu};
+
+ my $iface;
+ my $controller_cfg = PVE::Network::SDN::running_config()->{controllers};
+ my $controller = $controller_cfg->{ids}->{ $plugin_config->{controller} // '' };
+ if ($controller && $controller->{peers}) {
+ my @peers = PVE::Tools::split_list($controller->{peers});
+ my $local_node = PVE::INotify::nodename();
+ my $bgprouter = PVE::Network::SDN::Controllers::EvpnPlugin::find_bgp_controller(
+ $local_node, $controller_cfg,
+ );
+ my $isisrouter = PVE::Network::SDN::Controllers::EvpnPlugin::find_isis_controller(
+ $local_node, $controller_cfg,
+ );
+ my $loopback = $bgprouter->{loopback} // $isisrouter->{loopback};
+ (undef, $iface) = eval {
+ PVE::Network::SDN::Zones::Plugin::find_local_ip_interface_peers(\@peers, $loopback);
+ };
+ }
+ return PVE::Network::SDN::Zones::VxlanPlugin::vxlan_mtu(
+ $plugin_config,
+ $iface,
+ PVE::INotify::read_file('interfaces'),
+ );
+}
+
sub generate_sdn_config {
my (
$class,
diff --git a/src/PVE/Network/SDN/Zones/FaucetPlugin.pm b/src/PVE/Network/SDN/Zones/FaucetPlugin.pm
index 5f069aea85de..4e3f5eff8a8b 100644
--- a/src/PVE/Network/SDN/Zones/FaucetPlugin.pm
+++ b/src/PVE/Network/SDN/Zones/FaucetPlugin.pm
@@ -30,6 +30,7 @@ sub options {
reversedns => { optional => 1 },
dnszone => { optional => 1 },
ipam => { optional => 1 },
+ dhcp => { optional => 1 },
};
}
diff --git a/src/PVE/Network/SDN/Zones/QinQPlugin.pm b/src/PVE/Network/SDN/Zones/QinQPlugin.pm
index 894bba16a20e..6031e0ad5002 100644
--- a/src/PVE/Network/SDN/Zones/QinQPlugin.pm
+++ b/src/PVE/Network/SDN/Zones/QinQPlugin.pm
@@ -39,6 +39,12 @@ sub properties {
};
}
+sub get_mtu {
+ my ($class, $plugin_config) = @_;
+
+ return $plugin_config->{mtu};
+}
+
sub options {
return {
nodes => { optional => 1 },
@@ -51,6 +57,7 @@ sub options {
reversedns => { optional => 1 },
dnszone => { optional => 1 },
ipam => { optional => 1 },
+ dhcp => { optional => 1 },
};
}
diff --git a/src/PVE/Network/SDN/Zones/SimplePlugin.pm b/src/PVE/Network/SDN/Zones/SimplePlugin.pm
index 347eee9a46ba..655f7e41cf37 100644
--- a/src/PVE/Network/SDN/Zones/SimplePlugin.pm
+++ b/src/PVE/Network/SDN/Zones/SimplePlugin.pm
@@ -31,7 +31,7 @@ sub properties {
description => "dns domain zone ex: mydomain.com",
},
dhcp => {
- description => 'Type of the DHCP backend for this zone',
+ description => 'DHCP backend serving the zone. dnsmasq works on simple zones only.',
type => 'string',
enum => PVE::Network::SDN::Dhcp->plugin_types(),
},
diff --git a/src/PVE/Network/SDN/Zones/VlanPlugin.pm b/src/PVE/Network/SDN/Zones/VlanPlugin.pm
index de19755d04f5..46b00d747b80 100644
--- a/src/PVE/Network/SDN/Zones/VlanPlugin.pm
+++ b/src/PVE/Network/SDN/Zones/VlanPlugin.pm
@@ -38,6 +38,12 @@ sub properties {
};
}
+sub get_mtu {
+ my ($class, $plugin_config) = @_;
+
+ return $plugin_config->{mtu};
+}
+
sub options {
return {
@@ -49,6 +55,7 @@ sub options {
reversedns => { optional => 1 },
dnszone => { optional => 1 },
ipam => { optional => 1 },
+ dhcp => { optional => 1 },
};
}
diff --git a/src/PVE/Network/SDN/Zones/VxlanPlugin.pm b/src/PVE/Network/SDN/Zones/VxlanPlugin.pm
index 4992757c1c5b..16bf0038e7e8 100644
--- a/src/PVE/Network/SDN/Zones/VxlanPlugin.pm
+++ b/src/PVE/Network/SDN/Zones/VxlanPlugin.pm
@@ -60,6 +60,18 @@ sub vxlan_mtu {
return 1450;
}
+sub get_mtu {
+ my ($class, $plugin_config) = @_;
+
+ my $iface;
+ if ($plugin_config->{peers}) {
+ my @peers = PVE::Tools::split_list($plugin_config->{peers});
+ (undef, $iface) =
+ eval { PVE::Network::SDN::Zones::Plugin::find_local_ip_interface_peers(\@peers) };
+ }
+ return vxlan_mtu($plugin_config, $iface, PVE::INotify::read_file('interfaces'));
+}
+
sub options {
return {
nodes => { optional => 1 },
@@ -71,6 +83,7 @@ sub options {
dnszone => { optional => 1 },
ipam => { optional => 1 },
fabric => { optional => 1 },
+ dhcp => { optional => 1 },
};
}
diff --git a/src/test/run_test_vnets_blackbox.pl b/src/test/run_test_vnets_blackbox.pl
index 665013d4c2be..2aefb73f602e 100755
--- a/src/test/run_test_vnets_blackbox.pl
+++ b/src/test/run_test_vnets_blackbox.pl
@@ -1610,6 +1610,55 @@ sub test_mapping_edit_target {
run_test(\&test_mapping_edit_target);
+sub test_zone_dhcp_backends {
+ my $test_name = (split(/::/, (caller(0))[3]))[-1];
+ my $zoneid = "TESTZONE";
+
+ # dnsmasq answers on the vnet bridge, so it only fits zones confined to
+ # one node. The ebpf backend answers on the guest's tap and fits any
+ my $zone = {
+ type => "vlan",
+ bridge => "vmbr0",
+ ipam => "pve",
+ zone => $zoneid,
+ };
+ eval { PVE::API2::Network::SDN::Zones->create({ %$zone, dhcp => "dnsmasq" }); };
+ like(
+ $@,
+ qr/dnsmasq backend only supports simple zones/,
+ "$test_name: dnsmasq is refused on a vlan zone",
+ );
+
+ create_zone({ %$zone, dhcp => "ebpf" });
+ is(get_zone($zoneid)->{dhcp}, "ebpf", "$test_name: ebpf is accepted on a vlan zone");
+
+ eval { update_zone($zoneid, { dhcp => "dnsmasq" }); };
+ like(
+ $@,
+ qr/dnsmasq backend only supports simple zones/,
+ "$test_name: switching a vlan zone to dnsmasq is refused",
+ );
+
+ # every other zone type offers the backend as well
+ $test_state->{controller_config} = {
+ ids => { ctrl => { type => 'evpn', asn => 65000, peers => '10.0.0.1' } },
+ };
+ my $params = {
+ qinq => { bridge => 'vmbr0', tag => 100 },
+ vxlan => { peers => '10.0.0.1' },
+ evpn => { controller => 'ctrl', 'vrf-vxlan' => 1000 },
+ faucet => { 'dp-id' => 1, controller => 'ctrl' },
+ };
+ for my $type (sort keys %$params) {
+ my $id = uc($type) . "Z";
+ my $zone = { type => $type, ipam => "pve", zone => $id, %{ $params->{$type} } };
+ create_zone({ %$zone, dhcp => "ebpf" });
+ is(get_zone($id)->{dhcp}, "ebpf", "$test_name: ebpf is accepted on a $type zone");
+ }
+}
+
+run_test(\&test_zone_dhcp_backends);
+
sub test_ipam_per_vnet {
my $test_name = (split(/::/, (caller(0))[3]))[-1];
my $zoneid = "TESTZONE";
@@ -2259,6 +2308,8 @@ sub test_ebpf_backend {
);
}
+run_test(\&test_ebpf_backend);
+
sub test_ebpf_backend_edge_cases {
my $test_name = (split(/::/, (caller(0))[3]))[-1];
my $zoneid = "TESTZONE";
@@ -2375,7 +2426,7 @@ sub test_ebpf_backend_edge_cases {
);
}
-run_test(\&test_ebpf_backend);
+run_test(\&test_ebpf_backend_edge_cases);
sub test_zone_tap_plug {
my $test_name = (split(/::/, (caller(0))[3]))[-1];
@@ -2526,7 +2577,6 @@ sub test_zone_tap_plug {
}
run_test(\&test_zone_tap_plug);
-run_test(\&test_ebpf_backend_edge_cases);
sub test_ebpf_mac_case {
my $test_name = (split(/::/, (caller(0))[3]))[-1];
@@ -2721,6 +2771,61 @@ sub test_ebpf_zone_mtu {
run_test(\&test_ebpf_zone_mtu);
+sub test_ebpf_vxlan_mtu {
+ my $test_name = (split(/::/, (caller(0))[3]))[-1];
+ my $zoneid = "VXZONE";
+ my $vnetid = "vxvnet";
+ my $mac = "da:65:8f:18:9b:6f";
+
+ # a vxlan zone without an MTU of its own answers with the one its bridge
+ # takes from the underlay, the encapsulation overhead taken off
+ my $mocked_zone_plugin = Test::MockModule->new('PVE::Network::SDN::Zones::Plugin');
+ $mocked_zone_plugin->mock(
+ find_local_ip_interface_peers => sub { return ('10.10.10.1', 'eno1'); });
+ $mocked_pve_inotify->mock(
+ read_file => sub { return { ifaces => { eno1 => { mtu => 1400 } } }; });
+ create_zone({
+ type => "vxlan",
+ peers => "10.10.10.2",
+ dhcp => "ebpf",
+ ipam => "pve",
+ zone => $zoneid,
+ });
+ create_vnet({
+ type => "vnet",
+ zone => $zoneid,
+ vnet => $vnetid,
+ tag => 100,
+ });
+ create_subnet({
+ type => "subnet",
+ vnet => $vnetid,
+ subnet => "10.0.0.0/24",
+ gateway => "10.0.0.1",
+ });
+ $test_state->{vmlist} = { ids => { 999 => { type => 'qemu', node => 'localnode' } } };
+ $test_state->{guest_nets} = { 999 => { net0 => "virtio=$mac,bridge=$vnetid" } };
+ $test_state->{macdb} = { vnets => { $vnetid => { $mac => { ip4 => '10.0.0.100' } } } };
+
+ take_ebpf_calls();
+ PVE::Network::SDN::Dhcp::regenerate_config();
+ is(
+ take_ebpf_calls()->[0]->{args}->[0]->[0]->{record}->{mtu},
+ 1350,
+ "$test_name: the underlay's MTU minus the encapsulation is handed out",
+ );
+ update_zone($zoneid, { mtu => 1500 });
+ PVE::Network::SDN::Dhcp::regenerate_config();
+ is(
+ take_ebpf_calls()->[0]->{args}->[0]->[0]->{record}->{mtu},
+ 1500,
+ "$test_name: an explicit zone MTU wins",
+ );
+ $mocked_pve_inotify->unmock('read_file');
+}
+
+run_test(\&test_ebpf_vxlan_mtu);
+
sub test_ebpf_overlapping_subnets {
my $test_name = (split(/::/, (caller(0))[3]))[-1];
my $zoneid = "TESTZONE";
--
2.47.3
next prev parent reply other threads:[~2026-11-05 10:06 UTC|newest]
Thread overview: 18+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-05 10:02 [PATCH container/docs/manager/network/proxmox{,-perl-rs,-ve-rs}/qemu-server v6 00/17] sdn: implement DHCP for all zones using eBPF Hannes Laimer
2026-11-05 10:03 ` [PATCH proxmox v6 01/17] network-types: mac address: add an accessor for the bytes Hannes Laimer
2026-11-05 10:03 ` [PATCH proxmox v6 02/17] ebpf: dhcp: add the per-tap DHCPv4 responder program Hannes Laimer
2026-11-05 10:03 ` [PATCH proxmox v6 03/17] ebpf: dhcp: add the subsystem that loads the responder and keeps its records Hannes Laimer
2026-11-05 10:03 ` [PATCH proxmox-perl-rs v6 04/17] pve-rs: sdn: add bindings for the eBPF DHCP responder Hannes Laimer
2026-11-05 10:03 ` [PATCH pve-network v6 05/17] sdn: zones: take the VXLAN and EVPN MTU from the underlay interface Hannes Laimer
2026-11-05 10:03 ` [PATCH pve-network v6 06/17] sdn: dhcp: make the lease time configurable per subnet Hannes Laimer
2026-11-05 10:03 ` [PATCH pve-network v6 07/17] sdn: dhcp: require a backend only if a zone on this node uses it Hannes Laimer
2026-11-05 10:03 ` [PATCH pve-network v6 08/17] sdn: dhcp: add an eBPF backend answering on each guest interface Hannes Laimer
2026-11-05 10:03 ` [PATCH pve-network v6 09/17] sdn: zones: report interface plugs and unplugs to the DHCP backends Hannes Laimer
2026-11-05 10:03 ` Hannes Laimer [this message]
2026-11-05 10:03 ` [PATCH qemu-server v6 11/17] network: sdn: keep the DHCP backends current on guest NICs Hannes Laimer
2026-11-05 10:03 ` [PATCH pve-container v6 12/17] net: report NIC plugs and removals to the SDN DHCP backends Hannes Laimer
2026-11-05 10:03 ` [PATCH pve-manager v6 13/17] ui: sdn: add zone DHCP backend selector and subnet DHCP options Hannes Laimer
2026-11-05 10:03 ` [PATCH pve-manager v6 14/17] sdn: bring up the DHCP backends at boot before the guests start Hannes Laimer
2026-11-05 10:03 ` [PATCH proxmox-ve-rs v6 15/17] ve-config: sdn: add the ebpf dhcp backend Hannes Laimer
2026-11-05 10:03 ` [PATCH pve-docs v6 16/17] sdn: zones: document the MTU taken from the underlay interface Hannes Laimer
2026-11-05 10:03 ` [PATCH pve-docs v6 17/17] sdn: dhcp: add eBPF backend section Hannes Laimer
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261105100316.449163-11-h.laimer@proxmox.com \
--to=h.laimer@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.