all lists on 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: 15+ 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
2026-09-03  4:26 ` 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 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.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal