v0.6.1: latest iPXE + automatic NIC driver fallback (more devices, zero toggle)
Mirrors the worthwhile device-support wins from iVentoy 1.0.24→1.0.35 onto our
(very different) proxy-DHCP + iPXE-chainload architecture. iVentoy's other
changes are inapplicable (arm64-server / distro-display fixes live in its
injected Linux, which we don't have), niche (iSCSI), or closed-source
(Matrix Boot).
iPXE refreshed (mirrors 1.0.35 "Update iPXE")
- Pin the from-source build to ipxe/ipxe master @ 2026-06-09
(95ffbf4745553e8a207922389929e1943c0237c0) — newer NIC drivers + EFI fixes.
The pin also busts the cached ipxe-build Docker layer so the release
actually recompiles iPXE; build-ipxe.sh now shallow-fetches an exact SHA.
Automatic NIC driver fallback (mirrors 1.0.34 "driver/boot-file mode" — but
no operator toggle, per request)
- New DriverMode {Firmware, Builtin} in core; ClientArch::ipxe_bootfile_mode
maps each arch to either the firmware-net build (snponly/undionly, default)
or the all-drivers build (ipxe.efi/ipxe.pxe/ipxe-i386.efi/ipxe-arm64.efi).
- The DHCP proxy serves Firmware by default — byte-for-byte unchanged, so
hardware that boots today never regresses. A new DriverEscalation state
machine watches for the tell-tale failure: a MAC re-PXE-boots (fresh
firmware DISCOVER) without ever completing the iPXE-user-class handoff that
proves the firmware NIC stack worked. That MAC is automatically escalated to
iPXE's own NIC drivers, and the choice is sticky after a confirmed handoff
(debounced for the :67/:4011 same-boot pair, TTL-pruned, capped). It just
works — no settings, no UI.
- All-drivers binaries fetched per arch (ipxe.pxe + i386/arm64 native EFI;
x86_64 ipxe.efi already built from source with PNG); ipxe-assets embeds
*.pxe and logs availability per (arch, mode).
Core principles intact: DHCP-proxy-only, container-first, Rust-focused (the
logic is all Rust; only the iPXE fetch/build stays shell), Windows hard-rules
untouched (this never goes near Windows boot).
Validation: clippy clean; full workspace test suite green (core 99 incl. new
DriverMode tests, dhcp-proxy +4 escalation tests, http-api 31+68, iso-store
61, tftp 6, bin 2); fmt-clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
24879fcc90
commit
4f193cac05
@@ -0,0 +1,202 @@
|
||||
//! Automatic per-MAC NIC driver-mode escalation (v0.6.1).
|
||||
//!
|
||||
//! OpenPXE serves the firmware-net iPXE build (`snponly`/`undionly`) by
|
||||
//! default — it's the most reliable choice for chainloading because the
|
||||
//! firmware just proved its network works by downloading the NBP. A minority
|
||||
//! of NICs have a missing or buggy firmware UNDI/SNP stack; those clients
|
||||
//! TFTP the binary fine, but then iPXE can't bring the link up, so the
|
||||
//! tell-tale second DHCP DISCOVER carrying the `iPXE` user-class never arrives
|
||||
//! and the machine eventually re-PXE-boots.
|
||||
//!
|
||||
//! We detect exactly that: a *fresh* firmware DISCOVER from a MAC whose
|
||||
//! previous firmware attempt was never confirmed by an iPXE handoff means the
|
||||
//! firmware-net build failed → escalate that MAC to [`DriverMode::Builtin`]
|
||||
//! (iPXE's own NIC drivers). The decision is sticky — once a MAC settles on a
|
||||
//! mode that completes the handoff, later boots go straight to it. There is no
|
||||
//! operator toggle; it just works, and the default (firmware) path is
|
||||
//! unchanged so hardware that already boots never regresses.
|
||||
|
||||
use openpxe_core::DriverMode;
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::HashMap;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Multiple DISCOVERs within this window belong to the *same* boot (DHCP
|
||||
/// retransmits, plus the :4011 PXE Boot Server query that follows the :67
|
||||
/// DISCOVER). They must not be mistaken for a failed-and-retried boot.
|
||||
const SAME_BOOT_DEBOUNCE: Duration = Duration::from_secs(8);
|
||||
|
||||
/// Forget a MAC's state after this long with no activity, so a transient
|
||||
/// escalation doesn't pin a client to Builtin forever and the map stays
|
||||
/// bounded over a long-running deployment.
|
||||
const ENTRY_TTL: Duration = Duration::from_mins(30);
|
||||
|
||||
/// Hard cap on tracked MACs. Past this we evict the least-recently-seen
|
||||
/// entry — escalation is best-effort, never a memory-growth vector.
|
||||
const MAX_ENTRIES: usize = 4096;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Entry {
|
||||
mode: DriverMode,
|
||||
/// True once we've served `mode` and are waiting for the iPXE handoff to
|
||||
/// confirm it worked. A *new* boot arriving while this is still true means
|
||||
/// the previous attempt failed and we should escalate.
|
||||
awaiting_confirm: bool,
|
||||
last_seen: Instant,
|
||||
}
|
||||
|
||||
/// Tracks per-MAC driver-mode escalation. Cheap to share via `Arc`.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DriverEscalation {
|
||||
inner: Mutex<HashMap<String, Entry>>,
|
||||
}
|
||||
|
||||
impl DriverEscalation {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Decide the driver mode for a firmware (PXEClient/HTTPClient) boot from
|
||||
/// `mac`. `primary` is true for the main DHCP DISCOVER (:67) and false for
|
||||
/// the PXE Boot Server query (:4011); only the primary path drives
|
||||
/// escalation, and only when it's clearly a *new* boot (outside the
|
||||
/// same-boot debounce). The :4011 path just echoes the current mode.
|
||||
pub fn mode_for_firmware_attempt(&self, mac: &str, primary: bool) -> DriverMode {
|
||||
self.decide_at(mac, primary, Instant::now())
|
||||
}
|
||||
|
||||
/// Record that `mac` completed the iPXE handoff (a DISCOVER carrying the
|
||||
/// `iPXE` user-class). The mode we last served worked, so stop awaiting
|
||||
/// confirmation and keep it sticky for next time.
|
||||
pub fn mark_ipxe_success(&self, mac: &str) {
|
||||
self.confirm_at(mac, Instant::now());
|
||||
}
|
||||
|
||||
fn decide_at(&self, mac: &str, primary: bool, now: Instant) -> DriverMode {
|
||||
let mut g = self.inner.lock();
|
||||
g.retain(|_, e| now.duration_since(e.last_seen) < ENTRY_TTL);
|
||||
|
||||
match g.get_mut(mac) {
|
||||
None => {
|
||||
g.insert(
|
||||
mac.to_owned(),
|
||||
Entry {
|
||||
mode: DriverMode::Firmware,
|
||||
// Only the primary DISCOVER opens a confirmation window.
|
||||
awaiting_confirm: primary,
|
||||
last_seen: now,
|
||||
},
|
||||
);
|
||||
if g.len() > MAX_ENTRIES {
|
||||
evict_oldest(&mut g);
|
||||
}
|
||||
DriverMode::Firmware
|
||||
}
|
||||
Some(entry) => {
|
||||
let recent = now.duration_since(entry.last_seen) < SAME_BOOT_DEBOUNCE;
|
||||
if primary && !recent {
|
||||
// A genuinely new boot. If the previous attempt was never
|
||||
// confirmed, the firmware-net build failed → escalate to
|
||||
// the all-drivers build. Builtin is the most capable build
|
||||
// we have, so it's the single escalation target (and a MAC
|
||||
// already on Builtin simply stays there).
|
||||
if entry.awaiting_confirm {
|
||||
entry.mode = DriverMode::Builtin;
|
||||
}
|
||||
entry.awaiting_confirm = true;
|
||||
}
|
||||
entry.last_seen = now;
|
||||
entry.mode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm_at(&self, mac: &str, now: Instant) {
|
||||
let mut g = self.inner.lock();
|
||||
if let Some(e) = g.get_mut(mac) {
|
||||
e.awaiting_confirm = false;
|
||||
e.last_seen = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn evict_oldest(map: &mut HashMap<String, Entry>) {
|
||||
if let Some(oldest) = map
|
||||
.iter()
|
||||
.min_by_key(|(_, e)| e.last_seen)
|
||||
.map(|(k, _)| k.clone())
|
||||
{
|
||||
map.remove(&oldest);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn firmware_first_then_escalates_on_unconfirmed_retry() {
|
||||
let e = DriverEscalation::new();
|
||||
let t0 = Instant::now();
|
||||
// Boot 1, primary DISCOVER: firmware.
|
||||
assert_eq!(e.decide_at("aa", true, t0), DriverMode::Firmware);
|
||||
// Same boot's :4011 query (+1s, within debounce): still firmware, no escalation.
|
||||
assert_eq!(
|
||||
e.decide_at("aa", false, t0 + Duration::from_secs(1)),
|
||||
DriverMode::Firmware
|
||||
);
|
||||
// Firmware net failed → no iPXE handoff → machine re-PXE-boots much
|
||||
// later: escalate to builtin drivers.
|
||||
assert_eq!(
|
||||
e.decide_at("aa", true, t0 + Duration::from_mins(1)),
|
||||
DriverMode::Builtin
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_is_sticky_after_success() {
|
||||
let e = DriverEscalation::new();
|
||||
let t0 = Instant::now();
|
||||
assert_eq!(e.decide_at("bb", true, t0), DriverMode::Firmware);
|
||||
assert_eq!(
|
||||
e.decide_at("bb", true, t0 + Duration::from_mins(1)),
|
||||
DriverMode::Builtin
|
||||
);
|
||||
// Builtin worked this time — confirm the handoff.
|
||||
e.confirm_at("bb", t0 + Duration::from_secs(61));
|
||||
// Next cold boot goes straight to builtin (no wasted firmware attempt).
|
||||
assert_eq!(
|
||||
e.decide_at("bb", true, t0 + Duration::from_mins(2)),
|
||||
DriverMode::Builtin
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirmed_firmware_never_escalates() {
|
||||
let e = DriverEscalation::new();
|
||||
let t0 = Instant::now();
|
||||
assert_eq!(e.decide_at("cc", true, t0), DriverMode::Firmware);
|
||||
// snponly worked: handoff confirmed.
|
||||
e.confirm_at("cc", t0 + Duration::from_secs(2));
|
||||
// A later boot stays on firmware — no spurious escalation.
|
||||
assert_eq!(
|
||||
e.decide_at("cc", true, t0 + Duration::from_mins(5)),
|
||||
DriverMode::Firmware
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_entry_is_forgotten_and_resets_to_firmware() {
|
||||
let e = DriverEscalation::new();
|
||||
let t0 = Instant::now();
|
||||
assert_eq!(e.decide_at("dd", true, t0), DriverMode::Firmware);
|
||||
assert_eq!(
|
||||
e.decide_at("dd", true, t0 + Duration::from_mins(1)),
|
||||
DriverMode::Builtin
|
||||
);
|
||||
// After the TTL with no activity the entry is pruned → fresh firmware.
|
||||
let later = t0 + Duration::from_mins(1) + ENTRY_TTL + Duration::from_secs(1);
|
||||
assert_eq!(e.decide_at("dd", true, later), DriverMode::Firmware);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,9 @@
|
||||
//! clients silently drop them.
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod escalation;
|
||||
pub mod reply;
|
||||
pub mod server;
|
||||
|
||||
pub use escalation::DriverEscalation;
|
||||
pub use server::DhcpProxyServer;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
//! pass, or the HTTP URL of the boot script once iPXE has chained.
|
||||
|
||||
use dhcproto::v4::{DhcpOption, Message, MessageType, Opcode, OptionCode};
|
||||
use openpxe_core::{ClientArch, FirmwareClass};
|
||||
use openpxe_core::{ClientArch, DriverMode, FirmwareClass};
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
/// Where the reply directs the client next.
|
||||
@@ -31,6 +31,11 @@ pub struct ReplyContext<'a> {
|
||||
pub our_ip: Ipv4Addr,
|
||||
pub arch: ClientArch,
|
||||
pub class: FirmwareClass,
|
||||
/// Which iPXE network backend to advertise for this client. The DHCP
|
||||
/// proxy fills this from the automatic per-MAC escalation state: normally
|
||||
/// [`DriverMode::Firmware`], escalated to [`DriverMode::Builtin`] for a
|
||||
/// MAC whose firmware-net boot failed to chainload (v0.6.1).
|
||||
pub driver_mode: DriverMode,
|
||||
/// Public base URL (scheme://host[:port]) used in HTTP directives.
|
||||
pub public_base_url: &'a str,
|
||||
}
|
||||
@@ -52,9 +57,13 @@ pub fn decide(ctx: &ReplyContext<'_>) -> BootDirective {
|
||||
},
|
||||
FirmwareClass::HttpClient => {
|
||||
// UEFI HTTP boot: client wants an http:// URL in option 67
|
||||
// pointing at an EFI executable. We serve ipxe.efi over HTTP;
|
||||
// it'll then do the same script-fetch the iPXE path does.
|
||||
let name = ctx.arch.ipxe_bootfile().unwrap_or("snponly.efi");
|
||||
// pointing at an EFI executable. We serve the iPXE EFI build for
|
||||
// the negotiated driver mode over HTTP; it'll then do the same
|
||||
// script-fetch the iPXE path does.
|
||||
let name = ctx
|
||||
.arch
|
||||
.ipxe_bootfile_mode(ctx.driver_mode)
|
||||
.unwrap_or("snponly.efi");
|
||||
BootDirective::HttpScript {
|
||||
url: format!(
|
||||
"{}/ipxe/{}",
|
||||
@@ -63,7 +72,7 @@ pub fn decide(ctx: &ReplyContext<'_>) -> BootDirective {
|
||||
),
|
||||
}
|
||||
}
|
||||
FirmwareClass::PxeClient => match ctx.arch.ipxe_bootfile() {
|
||||
FirmwareClass::PxeClient => match ctx.arch.ipxe_bootfile_mode(ctx.driver_mode) {
|
||||
Some(name) => BootDirective::TftpIpxe {
|
||||
filename: name.to_string(),
|
||||
},
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
//! UDP listener loop for the DHCP proxy. Accepts on :67 (and :4011 on a
|
||||
//! second socket) and dispatches each datagram through the pure reply logic.
|
||||
|
||||
use crate::escalation::DriverEscalation;
|
||||
use crate::reply::{build_reply, decide, BootDirective, ReplyContext};
|
||||
use dhcproto::v4::{DhcpOption, Message, OptionCode};
|
||||
use dhcproto::{Decodable, Decoder, Encodable, Encoder};
|
||||
use openpxe_core::{ClientArch, ClientEvent, ClientRegistry, FirmwareClass};
|
||||
use openpxe_core::{ClientArch, ClientEvent, ClientRegistry, DriverMode, FirmwareClass};
|
||||
use socket2::{Domain, Protocol, Socket, Type};
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
|
||||
use std::sync::Arc;
|
||||
@@ -18,6 +19,9 @@ pub struct DhcpProxyServer {
|
||||
public_base_url: String,
|
||||
clients: Arc<ClientRegistry>,
|
||||
metrics: openpxe_core::Metrics,
|
||||
/// Automatic per-MAC NIC driver-mode escalation (v0.6.1). Shared across
|
||||
/// the :67 and :4011 listener tasks via the server `Arc`.
|
||||
escalation: DriverEscalation,
|
||||
}
|
||||
|
||||
impl DhcpProxyServer {
|
||||
@@ -38,6 +42,7 @@ impl DhcpProxyServer {
|
||||
public_base_url,
|
||||
clients,
|
||||
metrics,
|
||||
escalation: DriverEscalation::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,11 +131,30 @@ impl DhcpProxyServer {
|
||||
},
|
||||
);
|
||||
|
||||
// Automatic NIC driver-mode selection (v0.6.1). The default is
|
||||
// firmware-net (snponly/undionly). A successful iPXE handoff confirms
|
||||
// the current mode works for this MAC; a fresh firmware boot whose
|
||||
// predecessor never handed off escalates the MAC to iPXE's built-in
|
||||
// NIC drivers. No operator toggle — the firmware path is unchanged so
|
||||
// hardware that already boots never regresses.
|
||||
let driver_mode = match class {
|
||||
FirmwareClass::IpxeUserClass => {
|
||||
self.escalation.mark_ipxe_success(&mac);
|
||||
DriverMode::Firmware // unused: this path serves the HTTP script
|
||||
}
|
||||
FirmwareClass::PxeClient | FirmwareClass::HttpClient => self
|
||||
.escalation
|
||||
.mode_for_firmware_attempt(&mac, label == "67"),
|
||||
// Unreachable: FirmwareClass::Other returned above.
|
||||
FirmwareClass::Other => DriverMode::Firmware,
|
||||
};
|
||||
|
||||
let ctx = ReplyContext {
|
||||
request: &request,
|
||||
our_ip: self.our_ip,
|
||||
arch,
|
||||
class,
|
||||
driver_mode,
|
||||
public_base_url: &self.public_base_url,
|
||||
};
|
||||
let directive = decide(&ctx);
|
||||
@@ -154,7 +178,7 @@ impl DhcpProxyServer {
|
||||
sock.send_to(&out, dest).await?;
|
||||
tracing::info!(
|
||||
target: "openpxe::dhcp",
|
||||
mac=%mac, arch=arch.as_str(), class=?class, dest=%dest, directive=?directive,
|
||||
mac=%mac, arch=arch.as_str(), class=?class, driver=?driver_mode, dest=%dest, directive=?directive,
|
||||
"PXE reply sent"
|
||||
);
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user