From: Hannes Laimer <h.laimer@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: superseded: [PATCH container/docs/manager/network/proxmox{-ebpf,-perl-rs}/qemu-server v2 00/16] sdn: implement DHCP for all zones using eBPF
Date: Tue, 20 Oct 2026 10:01:08 +0200 [thread overview]
Message-ID: <a3beb954-df1e-4090-8c66-95e1f77e58c3@proxmox.com> (raw)
In-Reply-To: <20260909104144.1110031-1-h.laimer@proxmox.com>
superseded-by:
https://lore.proxmox.com/pve-devel/20261020080005.470757-1-h.laimer@proxmox.com/
On 2026-09-09 12:42, Hannes Laimer wrote:
> Adds a second DHCP backend, `ebpf`, next to dnsmasq, selectable per
> zone. It aims to replace dnsmasq eventually and keeping both for now
> makes a migration simple. Every zone type can enable DHCP and the UI
> offers the selector on the zone types it edits. `dnsmasq` stays limited
> to simple zones.
>
> The responder lives in proxmox-ebpf [1]. The two patches for it apply
> on top of the v3 of that series, which follows separately. pve-rs gains
> a build dependency on the librust-proxmox-ebpf-dev it provides.
> pve-network needs this series' pve-rs at build time and at run time.
> qemu-server needs this series' pve-network as a Depends. pve-container
> works without the package and needs a Breaks on the older versions.
> Both pve-manager patches need it as well.
>
> # On the tap
>
> An eBPF program on the ingress of every guest tap on a vnet of an ebpf
> zone parses DHCP requests. It looks the interface up in an
> interface -> ip+options map and turns the request into the reply in
> place, back out of the tap. The exchange never reaches the bridge.
> Everything else passes untouched to the bridge and through the firewall
> as before. The answer is the interface's and only a request carrying
> its NIC's MAC gets it. Another MAC is passed on unanswered. So no guest
> can read another one's record by asking with its MAC, and a nested
> client is not handed the outer guest's address. Equal subnets in two
> zones do not mix either, each interface is answered from its own vnet's
> subnet.
>
> # The records and who writes them
>
> IPAM is the source and the map holds the answer of every guest
> interface on the node. A record change writes the records of the MAC's
> interfaces here. A tap plug attaches its interface with its record and
> an unplug drops it. All of that runs under a shared lock. The full pass
> builds the desired state from the configs, every guest NIC in the
> cluster with the vnet it sits on and the answer its MAC gets there from
> the MAC cache. The responder gives every listed interface present here
> its record, and a link where it is served. An interface the configs
> serve without a mapping gets a marker, so a plug that read the cache
> before the mapping went loses to it. It leaves a linked interface the
> configs do not name yet alone, since a hotplug plugs before it writes
> the config. It drops what a crashed guest left. A link on an interface
> without a record is harmless, the program passes everything there. So
> nothing is lost by attaching first and answering once the record is in.
> A pass failing midway warns in its task, and what was served keeps
> being served meanwhile. A change drawn before it is dropped, the next
> apply carries its input.
>
> - a guest start, NIC hotplug or migration allocates the record before
> the interface is plugged. Then a new tap plug hook of the dhcp
> plugins attaches the program, and a stop or unplug detaches it
> - a mapping created, updated or deleted through the API is written by
> the editing node. That node pokes the node running the guest through
> a new node endpoint (POST /nodes/{node}/sdn/dhcp-mapping) with a
> detached pvesh call, proxied over ssh like pvesh does. An
> unreachable node catches up on its next apply
> - an SDN apply runs the full pass and refreshes the programs, so a
> schema rebuild is refilled and a zone switching its backend takes
> effect for its running guests that hold a mapping as well
> - after a boot nothing is pinned, so the SDN commit brings the
> responder back before the guests start. A change that still finds
> nothing loaded runs the full pass itself
>
> # Ordering
>
> Nothing forces the collection of that state into one order, so every
> change carries a generation, a per-node counter the responder hands
> out. There is one rule. A change draws after its input is written and
> a reader before it reads. So two changes are ordered by the input they
> saw and not by when they reach the kernel.
>
> Who draws when:
> - record change after the IPAM record and the cache are written
> - full pass before it reads the configs and the cache
> - tap plug before it reads the cache. Its number orders the
> cache read and says nothing about the guest config,
> which is not written yet
>
> What the number decides:
> - record keeps the generation that wrote it, an older change
> never overwrites it. A removal by a record change
> leaves a marker with its generation for the same
> reason
> - full pass records its generation before it runs, so an older
> pass is dropped. It leaves alone every record a
> newer change wrote since it read its input
>
> Links carry no generation. The configs decide about the interfaces they
> name, a linked interface they do not name is left alone. So a hotplug's
> plug survives a pass that read the config before the write.
>
> A generation cannot order a pass against a hotplug that moves a tap
> between bridges. The plug runs before the guest config names the new
> bridge, so a pass in between reads the old one. That is why every
> record also names the vnet its writer saw the tap on.
>
> What the vnet decides:
> - pass, record change leave an entry alone that names another vnet
> than their config does, their config is stale
> for that tap. The pass lets the link follow
> the entry
> - tap plug replaces such an entry. Plugs of one tap are
> serialized by the guest lock, so the plug is
> the latest word on where it sits. On its own
> vnet it compares by generation like any other
> change. A plug finding no entry writes, a pass
> that stamped meanwhile never saw its interface
> - plug elsewhere a tap moved onto a plain bridge or a zone of
> another backend trades its entry for a marker
> naming that place. So a pass still reading
> the old vnet leaves it alone, and one reading
> the new place keeps it as it is. A tap without
> an entry gets none
> - tap unplug drops the entry with the link, the tap is
> gone or about to be. A tap with neither a
> link nor an interface left leaves its entry
> to the next pass
>
> The counter lives under /run and restarts with the node like
> everything it orders.
>
> # What a guest sees
>
> A changed mapping reaches a running guest at its next renewal, so the
> lease time bounds how long an old answer survives. A deleted one is no
> longer answered, the guest keeps its address until the lease runs out.
> Subnets get a `dhcp-lease-time` property, used by both backends.
> Without it dnsmasq keeps handing out infinite leases and the responder
> defaults to ten minutes.
>
> Since the request is answered on the tap, neither the firewall nor the
> rate limit of the interface sees it. So neither the guest's rules nor
> its DHCP firewall option nor the rate limit apply to an answered
> request. The reply is put on the tap's egress, so the rate limit shapes
> it and the firewall never sees it.
>
> The responder identifies itself with the subnet gateway. A subnet
> without one is served under a link-local identifier and without a
> default route, its guests renew by broadcast at rebinding time. It
> hands out an IPv4 DNS server only, an IPv6 one on a v4 subnet is left
> out. A guest tagging its own requests is not answered either, so a
> trunk port keeps working as before. Only DHCPv4 for now, but adding v6
> is very possible once we are happy with the design.
>
> Changes made directly on an external IPAM service are not detectable
> and the answers are cached per MAC. So an apply does not pick them up
> either, exactly like with dnsmasq today.
>
> note on the earlier versions: The main problem was that a tap plug ran
> on information not in the config yet, only pending. This allowed a race
> between the tap plug hook updating the kernel state and a fast enough
> SDN apply undoing it again, and a few other subtleties. This version
> should address all of these properly. Sorry for the noise with the other
> two versions, both the result of me spotting the problem too late and
> being too fast in assuming I had solved it.
>
>
> [1] https://lore.proxmox.com/pve-devel/20260909103952.1108084-1-h.laimer@proxmox.com/T/#t
>
>
> proxmox-ebpf:
>
> Hannes Laimer (2):
> dhcp: add per-tap responder BPF program
> dhcp: add responder subsystem
>
> Cargo.toml | 5 +
> debian/control | 6 +-
> src/bpf-shim/bpf/bpf_helpers.h | 3 +
> src/bpf-shim/vmlinux.h | 1 +
> src/dhcp/bpf/dhcp.bpf.c | 405 ++++++++++++
> src/dhcp/bpf/types.h | 25 +
> src/dhcp/mod.rs | 1113 ++++++++++++++++++++++++++++++++
> src/dhcp/types.rs | 80 +++
> src/lib.rs | 3 +
> tests/common/mod.rs | 63 +-
> tests/dhcp.rs | 827 ++++++++++++++++++++++++
> 11 files changed, 2526 insertions(+), 5 deletions(-)
> create mode 100644 src/dhcp/bpf/dhcp.bpf.c
> create mode 100644 src/dhcp/bpf/types.h
> create mode 100644 src/dhcp/mod.rs
> create mode 100644 src/dhcp/types.rs
> create mode 100644 tests/dhcp.rs
>
>
> proxmox-perl-rs:
>
> Hannes Laimer (1):
> pve-rs: sdn: add dhcp responder bindings
>
> pve-rs/Cargo.toml | 2 +
> pve-rs/Makefile | 1 +
> pve-rs/debian/control | 2 +
> pve-rs/src/bindings/sdn/dhcp.rs | 162 ++++++++++++++++++++++++++++++++
> pve-rs/src/bindings/sdn/mod.rs | 1 +
> 5 files changed, 168 insertions(+)
> create mode 100644 pve-rs/src/bindings/sdn/dhcp.rs
>
>
> pve-network:
>
> Hannes Laimer (8):
> sdn: push mapping changes from the ipam API to the dhcp backend
> sdn: ipam: do not cache negative per-MAC answers, lock the write
> sdn: subnets: add dhcp-lease-time property
> sdn: dhcp: only assert a backend's availability for zones using it
> sdn: dhcp: add ebpf plugin
> sdn: zones: attach the dhcp responder on tap plug, detach on unplug
> sdn: dhcp: apply mapping edits on the node serving the guest
> sdn: zones: offer dhcp on all zone types, keep dnsmasq simple-only
>
> debian/libpve-network-perl.prerm | 13 +
> src/PVE/API2/Network/SDN/Ips.pm | 10 +
> src/PVE/API2/Network/SDN/Nodes/Status.pm | 37 +
> src/PVE/API2/Network/SDN/Zones.pm | 11 +-
> src/PVE/Network/SDN/Dhcp.pm | 202 ++-
> src/PVE/Network/SDN/Dhcp/Dnsmasq.pm | 104 +-
> src/PVE/Network/SDN/Dhcp/Ebpf.pm | 355 ++++++
> src/PVE/Network/SDN/Dhcp/Makefile | 2 +-
> src/PVE/Network/SDN/Dhcp/Plugin.pm | 25 +-
> src/PVE/Network/SDN/Ipams.pm | 61 +-
> src/PVE/Network/SDN/SubnetPlugin.pm | 11 +
> src/PVE/Network/SDN/Vnets.pm | 5 +-
> src/PVE/Network/SDN/Zones.pm | 31 +-
> 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 | 25 +
> src/test/run_test_vnets_blackbox.pl | 1361 ++++++++++++++++++++-
> 20 files changed, 2223 insertions(+), 79 deletions(-)
> create mode 100755 debian/libpve-network-perl.prerm
> create mode 100644 src/PVE/Network/SDN/Dhcp/Ebpf.pm
>
>
> qemu-server:
>
> Hannes Laimer (1):
> network: report NIC plug and unplug to SDN with the MAC
>
> src/PVE/CLI/qm.pm | 4 ++--
> src/PVE/QemuServer.pm | 4 ++++
> src/PVE/QemuServer/Network.pm | 4 ++--
> src/usr/pve-bridge | 8 +++++++-
> src/usr/pve-bridgedown | 4 ++--
> 5 files changed, 17 insertions(+), 7 deletions(-)
>
>
> pve-container:
>
> Hannes Laimer (1):
> net: report veth plug and unplug to SDN with the hwaddr
>
> src/PVE/LXC.pm | 24 ++++++++++++++++++++++--
> src/PVE/LXC/Config.pm | 2 +-
> src/lxc-pve-poststop-hook | 5 ++---
> 3 files changed, 25 insertions(+), 6 deletions(-)
>
>
> pve-manager:
>
> Hannes Laimer (2):
> ui: sdn: dhcp backend selector on all zones, expose dhcp options
> sdn: bring the dhcp backends up at boot before the guests start
>
> bin/pve-sdn-commit | 3 +++
> services/pve-sdn-commit.service | 3 ++-
> www/manager6/sdn/SubnetEdit.js | 23 +++++++++++++++++++++++
> www/manager6/sdn/zones/Base.js | 17 +++++++++++++++++
> www/manager6/sdn/zones/SimpleEdit.js | 11 -----------
> 5 files changed, 45 insertions(+), 12 deletions(-)
>
>
> pve-docs:
>
> Hannes Laimer (1):
> sdn: dhcp: document the ebpf backend
>
> pvesdn.adoc | 113 +++++++++++++++++++++++++++++++++++++++++++---------
> 1 file changed, 95 insertions(+), 18 deletions(-)
>
>
> Summary over all repositories:
> 50 files changed, 5099 insertions(+), 127 deletions(-)
>
prev parent reply other threads:[~2026-10-20 8:03 UTC|newest]
Thread overview: 18+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-09 10:41 [PATCH container/docs/manager/network/proxmox{-ebpf,-perl-rs}/qemu-server v2 00/16] sdn: implement DHCP for all zones using eBPF Hannes Laimer
2026-09-09 10:41 ` [PATCH proxmox-ebpf v2 01/16] dhcp: add per-tap responder BPF program Hannes Laimer
2026-09-09 10:41 ` [PATCH proxmox-ebpf v2 02/16] dhcp: add responder subsystem Hannes Laimer
2026-09-09 10:41 ` [PATCH proxmox-perl-rs v2 03/16] pve-rs: sdn: add dhcp responder bindings Hannes Laimer
2026-09-09 10:41 ` [PATCH pve-network v2 04/16] sdn: push mapping changes from the ipam API to the dhcp backend Hannes Laimer
2026-09-09 10:41 ` [PATCH pve-network v2 05/16] sdn: ipam: do not cache negative per-MAC answers, lock the write Hannes Laimer
2026-09-09 10:41 ` [PATCH pve-network v2 06/16] sdn: subnets: add dhcp-lease-time property Hannes Laimer
2026-09-09 10:41 ` [PATCH pve-network v2 07/16] sdn: dhcp: only assert a backend's availability for zones using it Hannes Laimer
2026-09-09 10:41 ` [PATCH pve-network v2 08/16] sdn: dhcp: add ebpf plugin Hannes Laimer
2026-09-09 10:41 ` [PATCH pve-network v2 09/16] sdn: zones: attach the dhcp responder on tap plug, detach on unplug Hannes Laimer
2026-09-09 10:41 ` [PATCH pve-network v2 10/16] sdn: dhcp: apply mapping edits on the node serving the guest Hannes Laimer
2026-09-09 10:41 ` [PATCH pve-network v2 11/16] sdn: zones: offer dhcp on all zone types, keep dnsmasq simple-only Hannes Laimer
2026-09-09 10:41 ` [PATCH qemu-server v2 12/16] network: report NIC plug and unplug to SDN with the MAC Hannes Laimer
2026-09-09 10:41 ` [PATCH pve-container v2 13/16] net: report veth plug and unplug to SDN with the hwaddr Hannes Laimer
2026-09-09 10:41 ` [PATCH pve-manager v2 14/16] ui: sdn: dhcp backend selector on all zones, expose dhcp options Hannes Laimer
2026-09-09 10:41 ` [PATCH pve-manager v2 15/16] sdn: bring the dhcp backends up at boot before the guests start Hannes Laimer
2026-09-09 10:41 ` [PATCH pve-docs v2 16/16] sdn: dhcp: document the ebpf backend Hannes Laimer
2026-10-20 8:01 ` Hannes Laimer [this message]
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=a3beb954-df1e-4090-8c66-95e1f77e58c3@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