The consumer hands the responder subsystem of proxmox-ebpf its full state on every trigger, the interfaces to serve and all records, and the subsystem diffs that against the kernel state.
Signed-off-by: Hannes Laimer <[email protected]> --- pve-rs/Cargo.toml | 2 + pve-rs/Makefile | 1 + pve-rs/debian/control | 2 + pve-rs/src/bindings/sdn/dhcp.rs | 81 +++++++++++++++++++++++++++++++++ pve-rs/src/bindings/sdn/mod.rs | 1 + 5 files changed, 87 insertions(+) create mode 100644 pve-rs/src/bindings/sdn/dhcp.rs diff --git a/pve-rs/Cargo.toml b/pve-rs/Cargo.toml index 5ae9082..0adf00b 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.5.1" } proxmox-http = { version = "1.0.2", features = ["client-sync", "client-trait"] } proxmox-http-error = "1" @@ -70,6 +71,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-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 bb1cd2d..7609a1e 100644 --- a/pve-rs/Makefile +++ b/pve-rs/Makefile @@ -32,6 +32,7 @@ PERLMOD_PACKAGES := \ PVE::RS::OpenId \ PVE::RS::ResourceScheduling::Static \ PVE::RS::ResourceScheduling::Dynamic \ + PVE::RS::SDN::Dhcp \ 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 70367dc..521d966 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.5+default-dev (>= 0.5.1-~~), 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 0000000..510e115 --- /dev/null +++ b/pve-rs/src/bindings/sdn/dhcp.rs @@ -0,0 +1,81 @@ +#[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. Records handed in here are + //! answered directly from the kernel. + + use std::net::Ipv4Addr; + + use anyhow::{Context, Error, bail}; + use serde::Deserialize; + + use proxmox_ebpf::dhcp::{DhcpSubsystem, Record}; + + /// One MAC's DHCP answer, the address and every option the reply carries. + #[derive(Deserialize)] + pub struct DhcpRecord { + mac: String, + ip: Ipv4Addr, + prefixlen: u8, + server_id: Ipv4Addr, + lease: u32, + router: Option<Ipv4Addr>, + dns: Option<Ipv4Addr>, + mtu: Option<u16>, + } + + fn parse_mac(s: &str) -> Result<[u8; 6], Error> { + let mut addr = [0u8; 6]; + let mut n = 0; + for part in s.split(':') { + if n >= 6 || part.len() != 2 { + bail!("invalid MAC {s:?}"); + } + addr[n] = u8::from_str_radix(part, 16).with_context(|| format!("invalid MAC {s:?}"))?; + n += 1; + } + if n != 6 { + bail!("invalid MAC {s:?}"); + } + Ok(addr) + } + + fn to_records(records: Vec<DhcpRecord>) -> Result<Vec<Record>, Error> { + records + .into_iter() + .map(|r| { + Ok(Record { + mac: parse_mac(&r.mac)?, + ip: r.ip, + prefixlen: r.prefixlen, + server_id: r.server_id, + lease: r.lease, + router: r.router, + dns: r.dns, + mtu: r.mtu, + }) + }) + .collect() + } + + /// The full pass. Make the responder programs current, attach them to exactly the given + /// interfaces and make the record map hold exactly the given records. + #[export] + pub fn apply(ifaces: Vec<String>, records: Vec<DhcpRecord>) -> Result<(), Error> { + let ifaces: Vec<&str> = ifaces.iter().map(String::as_str).collect(); + DhcpSubsystem::new().apply(&ifaces, &to_records(records)?) + } + + /// Attach the responder to a guest interface. + #[export] + pub fn attach(iface: &str) -> Result<(), Error> { + DhcpSubsystem::new().attach(iface) + } + + /// Detach the responder everywhere and drop its pinned state. + #[export] + pub fn clear() -> Result<(), Error> { + DhcpSubsystem::new().clear() + } +} diff --git a/pve-rs/src/bindings/sdn/mod.rs b/pve-rs/src/bindings/sdn/mod.rs index dcae046..4b99b8f 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
