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: 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(-)
> 





      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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal