public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Hannes Laimer <h.laimer@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH pve-network 07/12] sdn: dhcp: add ebpf plugin
Date: Wed,  2 Sep 2026 14:47:34 +0200	[thread overview]
Message-ID: <20260902124739.750853-8-h.laimer@proxmox.com> (raw)
In-Reply-To: <20260902124739.750853-1-h.laimer@proxmox.com>

A dhcp backend that programs the proxmox-ebpf per-tap DHCP responder
instead of driving a dnsmasq instance, selectable per zone with
dhcp=ebpf. Answers come from the same per-MAC records dnsmasq serves
reservations from, handed in-process to the responder through the
pve-rs bindings as complete records, so each mapping push and the
full regenerate sync are self-contained.

Guests get answers without a DHCP daemon per zone and, once records
are pushed, independent of IPAM reachability. Subnets without a
gateway are skipped, the responder identifies itself with the
gateway address. IPv4 only.

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 src/PVE/API2/Network/SDN/Zones.pm |   2 +-
 src/PVE/Network/SDN/Dhcp.pm       |   4 +
 src/PVE/Network/SDN/Dhcp/Ebpf.pm  | 173 ++++++++++++++++++++++++++++++
 src/PVE/Network/SDN/Dhcp/Makefile |   2 +-
 4 files changed, 179 insertions(+), 2 deletions(-)
 create mode 100644 src/PVE/Network/SDN/Dhcp/Ebpf.pm

diff --git a/src/PVE/API2/Network/SDN/Zones.pm b/src/PVE/API2/Network/SDN/Zones.pm
index b897cbd..ad16bef 100644
--- a/src/PVE/API2/Network/SDN/Zones.pm
+++ b/src/PVE/API2/Network/SDN/Zones.pm
@@ -90,7 +90,7 @@ my $ZONE_PROPERTIES = {
     },
     dhcp => {
         type => 'string',
-        enum => ['dnsmasq'],
+        enum => ['dnsmasq', 'ebpf'],
         optional => 1,
         description => 'Name of DHCP server backend for this zone.',
     },
diff --git a/src/PVE/Network/SDN/Dhcp.pm b/src/PVE/Network/SDN/Dhcp.pm
index 4d937dc..f046dfd 100644
--- a/src/PVE/Network/SDN/Dhcp.pm
+++ b/src/PVE/Network/SDN/Dhcp.pm
@@ -10,6 +10,7 @@ use PVE::Network::SDN::Ipams;
 use PVE::Network::SDN::Subnets;
 use PVE::Network::SDN::Dhcp::Plugin;
 use PVE::Network::SDN::Dhcp::Dnsmasq;
+use PVE::Network::SDN::Dhcp::Ebpf;
 
 use PVE::INotify;
 
@@ -18,6 +19,9 @@ PVE::Network::SDN::Dhcp::Plugin->init();
 PVE::Network::SDN::Dhcp::Dnsmasq->register();
 PVE::Network::SDN::Dhcp::Dnsmasq->init();
 
+PVE::Network::SDN::Dhcp::Ebpf->register();
+PVE::Network::SDN::Dhcp::Ebpf->init();
+
 sub plugin_types {
     return PVE::Network::SDN::Dhcp::Plugin->lookup_types();
 }
diff --git a/src/PVE/Network/SDN/Dhcp/Ebpf.pm b/src/PVE/Network/SDN/Dhcp/Ebpf.pm
new file mode 100644
index 0000000..d6a85a5
--- /dev/null
+++ b/src/PVE/Network/SDN/Dhcp/Ebpf.pm
@@ -0,0 +1,173 @@
+package PVE::Network::SDN::Dhcp::Ebpf;
+
+use strict;
+use warnings;
+
+use base qw(PVE::Network::SDN::Dhcp::Plugin);
+
+use Net::IP qw(:PROC);
+use Net::Subnet qw(subnet_matcher);
+
+use PVE::RESTEnvironment qw(log_warn);
+
+use PVE::RS::SDN::Dhcp;
+
+my $DEFAULT_LEASE_TIME = 600;
+
+sub type {
+    return 'ebpf';
+}
+
+# The responder identifies itself with the subnet gateway, a subnet
+# without one cannot be served.
+my sub dhcp_record {
+    my ($mac, $ip4, $subnet, $mtu) = @_;
+
+    my $gateway = $subnet->{gateway};
+    return undef if !$gateway;
+
+    # the config hands its numbers over as strings, the bindings take integers only
+    return {
+        mac => $mac,
+        ip => $ip4,
+        prefixlen => int($subnet->{mask}),
+        server_id => $gateway,
+        lease => int($subnet->{'dhcp-lease-time'} // $DEFAULT_LEASE_TIME),
+        router => $gateway,
+        dns => $subnet->{'dhcp-dns-server'},
+        mtu => defined($mtu) ? int($mtu) : undef,
+    };
+}
+
+my sub zone_subnets {
+    my ($zoneid) = @_;
+
+    my $cfg = PVE::Network::SDN::Subnets::config();
+
+    my $subnets = {};
+    for my $id (keys %{ $cfg->{ids} }) {
+        my $subnet = PVE::Network::SDN::Subnets::sdn_subnets_config($cfg, $id);
+        next if $subnet->{zone} ne $zoneid;
+        $subnets->{$id} = $subnet;
+    }
+
+    return $subnets;
+}
+
+my sub zone_mtu {
+    my ($zoneid) = @_;
+
+    my $zone = PVE::Network::SDN::Zones::get_zone($zoneid, 1);
+    return if !$zone;
+
+    return PVE::Network::SDN::Zones::get_mtu($zone);
+}
+
+sub add_ip_mapping {
+    my ($class, $dhcpid, $macdb, $mac, $ip4, $ip6) = @_;
+
+    return if !$ip4; # v4 answers only
+
+    my $subnets = zone_subnets($dhcpid);
+    my ($subnetid, $subnet) = eval { PVE::Network::SDN::Subnets::find_ip_subnet($ip4, $subnets) };
+    if (!$subnet) {
+        warn "could not find subnet for $ip4 in zone $dhcpid: $@";
+        return;
+    }
+
+    my $record = dhcp_record($mac, $ip4, $subnet, zone_mtu($dhcpid));
+    if (!defined($record)) {
+        warn "subnet $subnetid has no gateway, cannot serve DHCP for $mac\n";
+        return;
+    }
+
+    eval { PVE::RS::SDN::Dhcp::update([$record]) };
+    warn "could not update DHCP record for $mac: $@" if $@;
+}
+
+sub del_ip_mapping {
+    my ($class, $dhcpid, $mac) = @_;
+
+    eval { PVE::RS::SDN::Dhcp::remove($mac) };
+    warn "could not remove DHCP record for $mac: $@" if $@;
+}
+
+# regenerate translates the full record set into one responder sync, the
+# dispatcher collects per-vnet records through the configure hooks
+my $sync_records = undef;
+my $current_mtu = undef;
+
+sub before_regenerate {
+    my ($class, $noerr) = @_;
+
+    $sync_records = [];
+}
+
+sub before_configure {
+    my ($class, $dhcpid, $zone_cfg) = @_;
+
+    $current_mtu = PVE::Network::SDN::Zones::get_mtu($zone_cfg);
+}
+
+sub configure_subnet {
+    my ($class, $config, $dhcpid, $vnetid, $subnet_config) = @_;
+
+    return if !Net::IP::ip_is_ipv4($subnet_config->{network});
+
+    if (!$subnet_config->{gateway}) {
+        warn "subnet $subnet_config->{id} has no gateway, not serving DHCP for it\n";
+        return;
+    }
+
+    my $macdb = PVE::Network::SDN::Ipams::read_macdb();
+    my $matcher = subnet_matcher($subnet_config->{cidr});
+
+    for my $mac (sort keys %{ $macdb->{macs} }) {
+        my $ip4 = $macdb->{macs}->{$mac}->{ip4};
+        next if !$ip4 || !$matcher->($ip4);
+        # the vnet's own gateway address is cached too and never a lease
+        next if $ip4 eq $subnet_config->{gateway};
+        push @$config, dhcp_record($mac, $ip4, $subnet_config, $current_mtu);
+    }
+}
+
+sub configure_range {
+    # noop, static answers only
+}
+
+sub configure_vnet {
+    my ($class, $config, $dhcpid, $vnetid, $vnet_config) = @_;
+
+    push @$sync_records, @$config;
+}
+
+sub after_configure {
+    my ($class, $dhcpid, $noerr) = @_;
+
+    $current_mtu = undef;
+}
+
+sub after_regenerate {
+    my ($class) = @_;
+
+    my $records = $sync_records // [];
+    $sync_records = undef;
+
+    # the full pass also drops the link pins of departed guests
+    eval { PVE::RS::SDN::Dhcp::apply() };
+    warn "could not refresh the DHCP responder: $@" if $@;
+
+    eval { PVE::RS::SDN::Dhcp::sync($records) };
+    warn "could not sync DHCP records: $@" if $@;
+}
+
+# tap plug hook, attaches the responder program to a guest interface.
+# Best effort, a guest start must not fail on it.
+sub attach_iface {
+    my ($iface) = @_;
+
+    eval { PVE::RS::SDN::Dhcp::attach($iface) };
+    log_warn("could not attach DHCP responder to $iface: $@") if $@;
+}
+
+1;
diff --git a/src/PVE/Network/SDN/Dhcp/Makefile b/src/PVE/Network/SDN/Dhcp/Makefile
index 6546513..ce86aae 100644
--- a/src/PVE/Network/SDN/Dhcp/Makefile
+++ b/src/PVE/Network/SDN/Dhcp/Makefile
@@ -1,4 +1,4 @@
-SOURCES=Plugin.pm Dnsmasq.pm
+SOURCES=Plugin.pm Dnsmasq.pm Ebpf.pm
 
 
 PERL5DIR=${DESTDIR}/usr/share/perl5
-- 
2.47.3





  parent reply	other threads:[~2026-09-02 12:49 UTC|newest]

Thread overview: 14+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-02 12:47 [RFC manager/network/proxmox{-ebpf,-perl-rs} 00/12] sdn: implement DHCP for all zones using eBPF Hannes Laimer
2026-09-02 12:47 ` [PATCH proxmox-ebpf 01/12] dhcp: add per-tap responder BPF program Hannes Laimer
2026-09-02 12:47 ` [PATCH proxmox-ebpf 02/12] dhcp: add responder subsystem Hannes Laimer
2026-09-02 12:47 ` [PATCH proxmox-perl-rs 03/12] pve-rs: sdn: add dhcp responder bindings Hannes Laimer
2026-09-02 12:47 ` [PATCH pve-network 04/12] sdn: ipam: do not cache negative per-MAC answers, lock the write Hannes Laimer
2026-09-02 12:47 ` [PATCH pve-network 05/12] sdn: subnets: add dhcp-lease-time property Hannes Laimer
2026-09-02 12:47 ` [PATCH pve-network 06/12] sdn: dhcp: only assert a backend's availability for zones using it Hannes Laimer
2026-09-02 12:47 ` Hannes Laimer [this message]
2026-09-02 12:47 ` [PATCH pve-network 08/12] sdn: zones: attach the dhcp responder on tap plug Hannes Laimer
2026-09-02 12:47 ` [PATCH pve-network 09/12] sdn: dhcp: apply mapping edits on the node serving the guest Hannes Laimer
2026-09-02 12:47 ` [PATCH pve-network 10/12] sdn: zones: offer dhcp on all zone types, keep dnsmasq simple-only Hannes Laimer
2026-09-02 12:47 ` [PATCH pve-network 11/12] tests: cover the ebpf dhcp backend and ipam API mapping pushes Hannes Laimer
2026-09-02 12:47 ` [PATCH pve-manager 12/12] ui: sdn: dhcp backend selector on all zones, expose dhcp options Hannes Laimer
2026-09-02 12:54 ` [RFC manager/network/proxmox{-ebpf,-perl-rs} 00/12] sdn: implement DHCP for all zones using eBPF 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=20260902124739.750853-8-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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal