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 proxmox-perl-rs v6 04/17] pve-rs: sdn: add bindings for the eBPF DHCP responder
Date: Thu,  5 Nov 2026 11:03:03 +0100	[thread overview]
Message-ID: <20261105100316.449163-5-h.laimer@proxmox.com> (raw)
In-Reply-To: <20261105100316.449163-1-h.laimer@proxmox.com>

The responder answers with the records the caller hands in. The
caller has to build them under the responder's lock, or a full pass
could write older records over those a plug or record change built
later. So the lock is an object the caller takes before it builds the
records, and every call that hands in records is a method of that
lock.

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---

Notes:
    v6:
     * pass on whether an interface sits on the bridge its config names
    
    v5:
     * take the MAC with the interface and as an argument of the plug, not
       in the record

 pve-rs/Cargo.toml               |   2 +
 pve-rs/Makefile                 |   3 +
 pve-rs/debian/control           |   2 +
 pve-rs/src/bindings/sdn/dhcp.rs | 217 ++++++++++++++++++++++++++++++++
 pve-rs/src/bindings/sdn/mod.rs  |   1 +
 5 files changed, 225 insertions(+)
 create mode 100644 pve-rs/src/bindings/sdn/dhcp.rs

diff --git a/pve-rs/Cargo.toml b/pve-rs/Cargo.toml
index ecedb1ebe747..f7d6abf4efd7 100644
--- a/pve-rs/Cargo.toml
+++ b/pve-rs/Cargo.toml
@@ -34,6 +34,7 @@ proxmox-apt = { version = "1.0.1", features = ["cache"] }
 proxmox-apt-api-types = "3"
 proxmox-base64 = "1"
 proxmox-config-digest = "1"
+proxmox-ebpf = { version = "0.1", features = ["dhcp"] }
 proxmox-frr = { version = "0.6.0" }
 proxmox-http = { version = "1.0.2", features = ["client-sync", "client-trait"] }
 proxmox-http-error = "1"
@@ -71,6 +72,7 @@ proxmox-wireguard = { version = "0.1.2" }
 # proxmox-config-digest = { path = "../../proxmox/proxmox-config-digest" }
 # proxmox-daemon = { path = "../../proxmox/proxmox-daemon" }
 # proxmox-dns-api = { path = "../../proxmox/proxmox-dns-api" }
+# proxmox-ebpf = { path = "../../proxmox/proxmox-ebpf" }
 # proxmox-http-error = { path = "../../proxmox/proxmox-http-error" }
 # proxmox-http = { path = "../../proxmox/proxmox-http" }
 # proxmox-human-byte = { path = "../../proxmox/proxmox-human-byte" }
diff --git a/pve-rs/Makefile b/pve-rs/Makefile
index 60f68c7a21be..c597401f789b 100644
--- a/pve-rs/Makefile
+++ b/pve-rs/Makefile
@@ -33,6 +33,9 @@ PERLMOD_PACKAGES := \
 	  PVE::RS::OpenId \
 	  PVE::RS::ResourceScheduling::Static \
 	  PVE::RS::ResourceScheduling::Dynamic \
+	  PVE::RS::SDN::Dhcp \
+	  PVE::RS::SDN::Dhcp::ExclusiveLock \
+	  PVE::RS::SDN::Dhcp::SharedLock \
 	  PVE::RS::SDN::Fabrics \
 	  PVE::RS::SDN::PrefixLists \
 	  PVE::RS::SDN::RouteMaps \
diff --git a/pve-rs/debian/control b/pve-rs/debian/control
index ad8611eb1b69..2972f98a91d1 100644
--- a/pve-rs/debian/control
+++ b/pve-rs/debian/control
@@ -20,6 +20,8 @@ Build-Depends: debhelper-compat (= 13),
                librust-proxmox-apt-api-types-3+default-dev (>= 3.0.0-~~),
                librust-proxmox-base64-1+default-dev,
                librust-proxmox-config-digest-1+default-dev,
+               librust-proxmox-ebpf-0.1+default-dev,
+               librust-proxmox-ebpf-0.1+dhcp-dev,
                librust-proxmox-frr-0.6+default-dev (>= 0.6.0-~~),
                librust-proxmox-http-1+client-sync-dev (>= 1.0.2-~~),
                librust-proxmox-http-1+client-trait-dev (>= 1.0.2-~~),
diff --git a/pve-rs/src/bindings/sdn/dhcp.rs b/pve-rs/src/bindings/sdn/dhcp.rs
new file mode 100644
index 000000000000..b9ea41e6998d
--- /dev/null
+++ b/pve-rs/src/bindings/sdn/dhcp.rs
@@ -0,0 +1,217 @@
+#[perlmod::package(name = "PVE::RS::SDN::Dhcp", lib = "pve_rs")]
+pub mod pve_rs_sdn_dhcp {
+    //! The `PVE::RS::SDN::Dhcp` package.
+    //!
+    //! Bindings for the per-tap eBPF DHCP responder. Interfaces and the records they answer with
+    //! are handed in here and served directly from the kernel. The calls that change the responder
+    //! are methods of a lock object, so nothing writes to it without holding the lock in the mode
+    //! the change needs.
+
+    use std::net::Ipv4Addr;
+
+    use anyhow::Error;
+    use serde::Deserialize;
+
+    use proxmox_ebpf::dhcp::{DhcpSubsystem, Iface, Record, Unplugged};
+    use proxmox_network_types::mac_address::MacAddress;
+
+    /// One interface's DHCP answer, the address and every option the reply carries.
+    #[derive(Deserialize)]
+    pub struct DhcpRecord {
+        ip: Ipv4Addr,
+        prefixlen: u8,
+        server_id: Ipv4Addr,
+        lease: u32,
+        #[serde(default)]
+        router: Option<Ipv4Addr>,
+        #[serde(default)]
+        dns: Option<Ipv4Addr>,
+        #[serde(default)]
+        mtu: Option<u16>,
+    }
+
+    /// One guest interface, the vnet it sits on if any, its MAC, whether the responder runs on it
+    /// and what it answers there, and whether it already sits on the bridge the configs name.
+    #[derive(Deserialize)]
+    pub struct DhcpIface {
+        name: String,
+        #[serde(default)]
+        vnet: Option<String>,
+        #[serde(default)]
+        mac: Option<MacAddress>,
+        serve: bool,
+        #[serde(default)]
+        record: Option<DhcpRecord>,
+        #[serde(default)]
+        on_config_bridge: bool,
+    }
+
+    impl From<DhcpRecord> for Record {
+        fn from(r: DhcpRecord) -> Self {
+            Record {
+                ip: r.ip,
+                prefixlen: r.prefixlen,
+                server_id: r.server_id,
+                lease: r.lease,
+                router: r.router,
+                dns: r.dns,
+                mtu: r.mtu,
+            }
+        }
+    }
+
+    impl From<DhcpIface> for Iface {
+        fn from(i: DhcpIface) -> Self {
+            Iface {
+                name: i.name,
+                vnet: i.vnet,
+                mac: i.mac.map(|mac| mac.octets()),
+                serve: i.serve,
+                record: i.record.map(Record::from),
+                on_config_bridge: i.on_config_bridge,
+            }
+        }
+    }
+
+    pub(super) fn to_ifaces(ifaces: Vec<DhcpIface>) -> Vec<Iface> {
+        ifaces.into_iter().map(Iface::from).collect()
+    }
+
+    /// A tap unplug. The interface is gone or going and takes its record with it.
+    #[export]
+    pub fn detach(iface: &str) -> Result<(), Error> {
+        DhcpSubsystem::new().detach(iface, Unplugged::Gone)
+    }
+}
+
+#[perlmod::package(name = "PVE::RS::SDN::Dhcp::SharedLock", lib = "pve_rs")]
+pub mod pve_rs_sdn_dhcp_shared_lock {
+    //! The `PVE::RS::SDN::Dhcp::SharedLock` package.
+    //!
+    //! The responder's lock taken shared, what a tap plug and a guest's own record change hold.
+    //! Released when the object goes away.
+
+    use anyhow::Error;
+    use perlmod::Value;
+
+    use proxmox_ebpf::dhcp::{DhcpSubsystem, Iface, Outcome, Record, SharedLock, Unplugged};
+    use proxmox_network_types::mac_address::MacAddress;
+
+    use super::pve_rs_sdn_dhcp::{DhcpIface, DhcpRecord, to_ifaces};
+
+    /// The lock as Perl holds it.
+    pub struct PerlSharedLock {
+        lock: SharedLock,
+    }
+
+    perlmod::declare_magic!(Box<PerlSharedLock> : &PerlSharedLock as "PVE::RS::SDN::Dhcp::SharedLock");
+
+    /// Class method: take the lock. Plugs hold it next to each other and wait only behind a full
+    /// pass or a record change.
+    #[export]
+    pub fn new(#[raw] class: Value) -> Result<Value, Error> {
+        let lock = DhcpSubsystem::new().lock_shared()?;
+        Ok(perlmod::instantiate_magic!(&class, MAGIC => Box::new(PerlSharedLock { lock })))
+    }
+
+    /// Method: a tap plug onto a vnet with the NIC's MAC and the record it answers. Without a
+    /// record the interface gets no answer. Returns false when nothing is loaded yet. An interface
+    /// gone already counts as done.
+    #[export]
+    pub fn attach(
+        #[try_from_ref] this: &PerlSharedLock,
+        iface: String,
+        vnet: String,
+        mac: Option<MacAddress>,
+        record: Option<DhcpRecord>,
+    ) -> Result<bool, Error> {
+        let iface = Iface {
+            name: iface,
+            vnet: Some(vnet),
+            mac: mac.map(|mac| mac.octets()),
+            serve: true,
+            record: record.map(Record::from),
+            on_config_bridge: false,
+        };
+        Ok(this.lock.attach(&iface)? == Outcome::Done)
+    }
+
+    /// Method: a plug onto a bridge the responder does not serve, the vnet it sits on now, none
+    /// for a plain bridge. The interface keeps a marker naming that place.
+    #[export]
+    pub fn detach(
+        #[try_from_ref] this: &PerlSharedLock,
+        iface: String,
+        vnet: Option<String>,
+    ) -> Result<(), Error> {
+        this.lock.detach(&iface, Unplugged::MovedTo(vnet))
+    }
+
+    /// Method: a record change for the listed interfaces by a caller holding the guest lock.
+    /// Returns false when nothing is loaded to write into, so a full pass is due.
+    #[export]
+    pub fn update(
+        #[try_from_ref] this: &PerlSharedLock,
+        ifaces: Vec<DhcpIface>,
+    ) -> Result<bool, Error> {
+        Ok(this.lock.update(&to_ifaces(ifaces))? == Outcome::Done)
+    }
+}
+
+#[perlmod::package(name = "PVE::RS::SDN::Dhcp::ExclusiveLock", lib = "pve_rs")]
+pub mod pve_rs_sdn_dhcp_exclusive_lock {
+    //! The `PVE::RS::SDN::Dhcp::ExclusiveLock` package.
+    //!
+    //! The responder's lock taken exclusively, what a full pass and a record change hold. Released
+    //! when the object goes away.
+
+    use std::sync::Mutex;
+
+    use anyhow::Error;
+    use perlmod::Value;
+
+    use proxmox_ebpf::dhcp::{DhcpSubsystem, ExclusiveLock, Outcome};
+
+    use super::pve_rs_sdn_dhcp::{DhcpIface, to_ifaces};
+
+    /// The lock as Perl holds it. A full pass and a clear take it mutably, and Perl hands out
+    /// shared references only.
+    pub struct PerlExclusiveLock {
+        lock: Mutex<ExclusiveLock>,
+    }
+
+    perlmod::declare_magic!(Box<PerlExclusiveLock> : &PerlExclusiveLock as "PVE::RS::SDN::Dhcp::ExclusiveLock");
+
+    /// Class method: take the lock, waiting out every plug in flight.
+    #[export]
+    pub fn new(#[raw] class: Value) -> Result<Value, Error> {
+        let lock = Mutex::new(DhcpSubsystem::new().lock_exclusive()?);
+        Ok(perlmod::instantiate_magic!(&class, MAGIC => Box::new(PerlExclusiveLock { lock })))
+    }
+
+    /// Method: the full pass over every guest interface of the cluster. Those found here get
+    /// their entry and a link where served.
+    #[export]
+    pub fn apply(
+        #[try_from_ref] this: &PerlExclusiveLock,
+        ifaces: Vec<DhcpIface>,
+    ) -> Result<(), Error> {
+        this.lock.lock().unwrap().apply(&to_ifaces(ifaces))
+    }
+
+    /// Method: a record change for the listed interfaces. Those not found here are skipped.
+    /// Returns false when nothing is loaded to write into, so a full pass is due.
+    #[export]
+    pub fn update(
+        #[try_from_ref] this: &PerlExclusiveLock,
+        ifaces: Vec<DhcpIface>,
+    ) -> Result<bool, Error> {
+        Ok(this.lock.lock().unwrap().update(&to_ifaces(ifaces))? == Outcome::Done)
+    }
+
+    /// Method: detach the responder everywhere and drop its pinned state.
+    #[export]
+    pub fn clear(#[try_from_ref] this: &PerlExclusiveLock) -> Result<(), Error> {
+        this.lock.lock().unwrap().clear()
+    }
+}
diff --git a/pve-rs/src/bindings/sdn/mod.rs b/pve-rs/src/bindings/sdn/mod.rs
index dcae046a7e46..4b99b8fa10e5 100644
--- a/pve-rs/src/bindings/sdn/mod.rs
+++ b/pve-rs/src/bindings/sdn/mod.rs
@@ -1,3 +1,4 @@
+pub(crate) mod dhcp;
 pub(crate) mod fabrics;
 pub(crate) mod prefix_lists;
 pub(crate) mod route_maps;
-- 
2.47.3





  parent reply	other threads:[~2026-11-05 10:04 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 ` Hannes Laimer [this message]
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 ` [PATCH pve-network v6 10/17] sdn: zones: allow the ebpf DHCP backend on every zone type Hannes Laimer
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-5-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