Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f193cac05 | ||
|
|
24879fcc90 |
Generated
+9
-8
@@ -2669,7 +2669,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe"
|
name = "openpxe"
|
||||||
version = "0.6.0"
|
version = "0.6.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -2691,7 +2691,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-core"
|
name = "openpxe-core"
|
||||||
version = "0.6.0"
|
version = "0.6.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -2718,12 +2718,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-dhcp-proxy"
|
name = "openpxe-dhcp-proxy"
|
||||||
version = "0.6.0"
|
version = "0.6.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
"dhcproto",
|
"dhcproto",
|
||||||
"openpxe-core",
|
"openpxe-core",
|
||||||
|
"parking_lot",
|
||||||
"socket2 0.5.10",
|
"socket2 0.5.10",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -2732,7 +2733,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-http-api"
|
name = "openpxe-http-api"
|
||||||
version = "0.6.0"
|
version = "0.6.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -2768,7 +2769,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-ipxe-assets"
|
name = "openpxe-ipxe-assets"
|
||||||
version = "0.6.0"
|
version = "0.6.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"openpxe-core",
|
"openpxe-core",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
@@ -2778,7 +2779,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-iso-store"
|
name = "openpxe-iso-store"
|
||||||
version = "0.6.0"
|
version = "0.6.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bcrypt",
|
"bcrypt",
|
||||||
@@ -2807,7 +2808,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-tftp"
|
name = "openpxe-tftp"
|
||||||
version = "0.6.0"
|
version = "0.6.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -2821,7 +2822,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-webui"
|
name = "openpxe-webui"
|
||||||
version = "0.6.0"
|
version = "0.6.1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "p256"
|
name = "p256"
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.6.0"
|
version = "0.6.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.95"
|
rust-version = "1.95"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img alt="release" src="https://img.shields.io/badge/release-v0.5.8-2874d7" />
|
<img alt="release" src="https://img.shields.io/badge/release-v0.6.0-2874d7" />
|
||||||
<img alt="license" src="https://img.shields.io/badge/license-MIT%20%7C%20Apache--2.0-59824f" />
|
<img alt="license" src="https://img.shields.io/badge/license-MIT%20%7C%20Apache--2.0-59824f" />
|
||||||
<img alt="rust" src="https://img.shields.io/badge/built%20with-Rust-fb8841?logo=rust&logoColor=white" />
|
<img alt="rust" src="https://img.shields.io/badge/built%20with-Rust-fb8841?logo=rust&logoColor=white" />
|
||||||
<img alt="container" src="https://img.shields.io/badge/container--native-OCI%20%C2%B7%20OpenShift-2496ED?logo=docker&logoColor=white" />
|
<img alt="container" src="https://img.shields.io/badge/container--native-OCI%20%C2%B7%20OpenShift-2496ED?logo=docker&logoColor=white" />
|
||||||
@@ -32,7 +32,7 @@ Upload `.iso` files (or point at a remote share), and any machine on the network
|
|||||||
them — Linux installers, live tools, or stock Windows setup — with **zero iPXE knowledge
|
them — Linux installers, live tools, or stock Windows setup — with **zero iPXE knowledge
|
||||||
required by the operator.**
|
required by the operator.**
|
||||||
|
|
||||||
> **Status — v0.5.5, late pre-beta.** The full PXE stack, web UI, remote ISO libraries
|
> **Status — v0.6.0, late pre-beta.** The full PXE stack, web UI, remote ISO libraries
|
||||||
> (SMB/NFS/SFTP), Windows deployment, queued fleet rollout, SAML SSO, and Prometheus
|
> (SMB/NFS/SFTP), Windows deployment, queued fleet rollout, SAML SSO, and Prometheus
|
||||||
> metrics are implemented and test-covered. The release checklist gates every tag on the
|
> metrics are implemented and test-covered. The release checklist gates every tag on the
|
||||||
> full test suite + `clippy`. Currently in real-hardware validation.
|
> full test suite + `clippy`. Currently in real-hardware validation.
|
||||||
|
|||||||
+110
-10
@@ -23,6 +23,25 @@ pub enum ClientArch {
|
|||||||
Unknown(u16),
|
Unknown(u16),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Which iPXE network backend to advertise to a client (v0.6.1).
|
||||||
|
///
|
||||||
|
/// OpenPXE serves [`DriverMode::Firmware`] first (the firmware's own NIC
|
||||||
|
/// stack, via `snponly`/`undionly`) and only escalates a specific MAC to
|
||||||
|
/// [`DriverMode::Builtin`] (iPXE's bundled NIC drivers) automatically, when a
|
||||||
|
/// firmware-net boot fails to chainload. There is no operator toggle — the
|
||||||
|
/// DHCP proxy decides per client.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum DriverMode {
|
||||||
|
/// Reuse the firmware UNDI/SNP NIC stack (`snponly.efi`, `undionly.kpxe`).
|
||||||
|
/// Default, smallest, most reliable for chainloading.
|
||||||
|
#[default]
|
||||||
|
Firmware,
|
||||||
|
/// iPXE's own bundled NIC drivers (`ipxe.efi`, `ipxe.pxe`). Fallback for
|
||||||
|
/// hardware whose firmware NIC stack is missing or buggy.
|
||||||
|
Builtin,
|
||||||
|
}
|
||||||
|
|
||||||
impl ClientArch {
|
impl ClientArch {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn from_option_93(value: u16) -> Self {
|
pub fn from_option_93(value: u16) -> Self {
|
||||||
@@ -39,18 +58,47 @@ impl ClientArch {
|
|||||||
/// Default iPXE binary filename to return via TFTP for this architecture.
|
/// Default iPXE binary filename to return via TFTP for this architecture.
|
||||||
/// Uses `snponly` variants which reuse the firmware's UNDI/SNP network
|
/// Uses `snponly` variants which reuse the firmware's UNDI/SNP network
|
||||||
/// stack — smaller binaries and broader hardware compatibility than the
|
/// stack — smaller binaries and broader hardware compatibility than the
|
||||||
/// all-drivers-included `ipxe.efi`.
|
/// all-drivers-included `ipxe.efi`. Equivalent to
|
||||||
|
/// [`Self::ipxe_bootfile_mode`] with [`DriverMode::Firmware`]; kept as a
|
||||||
|
/// convenience for the common firmware-net path.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn ipxe_bootfile(self) -> Option<&'static str> {
|
pub fn ipxe_bootfile(self) -> Option<&'static str> {
|
||||||
Some(match self {
|
self.ipxe_bootfile_mode(DriverMode::Firmware)
|
||||||
Self::LegacyX86 => "undionly.kpxe",
|
}
|
||||||
Self::Ia32Uefi => "snponly-i386.efi",
|
|
||||||
Self::X64Uefi => "snponly.efi",
|
/// iPXE binary filename for this architecture under a given network
|
||||||
// ARM32 UEFI: upstream boot.ipxe.org does not publish a prebuilt
|
/// [`DriverMode`].
|
||||||
// snponly variant for this arch. We return None so the DHCP
|
///
|
||||||
// proxy declines rather than advertising a file we can't serve.
|
/// * [`DriverMode::Firmware`] — the `snponly`/`undionly` builds that reuse
|
||||||
Self::Arm32Uefi | Self::Unknown(_) => return None,
|
/// the firmware's UNDI/SNP NIC stack. Smallest, and the most reliable
|
||||||
Self::Arm64Uefi => "snponly-arm64.efi",
|
/// choice for chainloading because the firmware just proved its network
|
||||||
|
/// works by downloading the NBP. This is the default first attempt.
|
||||||
|
/// * [`DriverMode::Builtin`] — the all-drivers `ipxe.efi`/`ipxe.pxe`
|
||||||
|
/// builds that carry iPXE's *own* NIC drivers. The automatic fallback
|
||||||
|
/// for clients whose firmware NIC stack is missing or buggy (v0.6.1):
|
||||||
|
/// the DHCP proxy escalates a MAC to this mode when a firmware-net boot
|
||||||
|
/// never completes the iPXE handoff. iPXE still includes the `snp`
|
||||||
|
/// driver here too, so it degrades gracefully.
|
||||||
|
#[must_use]
|
||||||
|
pub fn ipxe_bootfile_mode(self, mode: DriverMode) -> Option<&'static str> {
|
||||||
|
Some(match (self, mode) {
|
||||||
|
// Legacy x86 BIOS: UNDI (firmware) vs full native-driver build.
|
||||||
|
(Self::LegacyX86, DriverMode::Firmware) => "undionly.kpxe",
|
||||||
|
(Self::LegacyX86, DriverMode::Builtin) => "ipxe.pxe",
|
||||||
|
// IA32 UEFI.
|
||||||
|
(Self::Ia32Uefi, DriverMode::Firmware) => "snponly-i386.efi",
|
||||||
|
(Self::Ia32Uefi, DriverMode::Builtin) => "ipxe-i386.efi",
|
||||||
|
// x86_64 UEFI — the overwhelmingly common modern client.
|
||||||
|
(Self::X64Uefi, DriverMode::Firmware) => "snponly.efi",
|
||||||
|
(Self::X64Uefi, DriverMode::Builtin) => "ipxe.efi",
|
||||||
|
// ARM64 UEFI.
|
||||||
|
(Self::Arm64Uefi, DriverMode::Firmware) => "snponly-arm64.efi",
|
||||||
|
(Self::Arm64Uefi, DriverMode::Builtin) => "ipxe-arm64.efi",
|
||||||
|
// ARM32 UEFI: upstream boot.ipxe.org publishes no prebuilt binary
|
||||||
|
// for this arch in either mode. Unknown arches likewise. Return
|
||||||
|
// None so the DHCP proxy declines rather than advertising a file
|
||||||
|
// we can't serve.
|
||||||
|
(Self::Arm32Uefi | Self::Unknown(_), _) => return None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,6 +181,58 @@ mod tests {
|
|||||||
assert_eq!(ClientArch::Unknown(0xFFFF).ipxe_bootfile(), None);
|
assert_eq!(ClientArch::Unknown(0xFFFF).ipxe_bootfile(), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bootfile_default_is_firmware_mode() {
|
||||||
|
// The convenience method must equal the explicit Firmware mode.
|
||||||
|
for a in [
|
||||||
|
ClientArch::LegacyX86,
|
||||||
|
ClientArch::Ia32Uefi,
|
||||||
|
ClientArch::X64Uefi,
|
||||||
|
ClientArch::Arm64Uefi,
|
||||||
|
ClientArch::Arm32Uefi,
|
||||||
|
ClientArch::Unknown(0x99),
|
||||||
|
] {
|
||||||
|
assert_eq!(
|
||||||
|
a.ipxe_bootfile(),
|
||||||
|
a.ipxe_bootfile_mode(DriverMode::Firmware)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn builtin_mode_maps_to_all_drivers_binaries() {
|
||||||
|
assert_eq!(
|
||||||
|
ClientArch::LegacyX86.ipxe_bootfile_mode(DriverMode::Builtin),
|
||||||
|
Some("ipxe.pxe")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ClientArch::X64Uefi.ipxe_bootfile_mode(DriverMode::Builtin),
|
||||||
|
Some("ipxe.efi")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ClientArch::Ia32Uefi.ipxe_bootfile_mode(DriverMode::Builtin),
|
||||||
|
Some("ipxe-i386.efi")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ClientArch::Arm64Uefi.ipxe_bootfile_mode(DriverMode::Builtin),
|
||||||
|
Some("ipxe-arm64.efi")
|
||||||
|
);
|
||||||
|
// No binary for ARM32 / unknown in either mode.
|
||||||
|
assert_eq!(
|
||||||
|
ClientArch::Arm32Uefi.ipxe_bootfile_mode(DriverMode::Builtin),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ClientArch::Unknown(0x99).ipxe_bootfile_mode(DriverMode::Builtin),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn driver_mode_default_is_firmware() {
|
||||||
|
assert_eq!(DriverMode::default(), DriverMode::Firmware);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn firmware_class_detects_ipxe_over_pxeclient() {
|
fn firmware_class_detects_ipxe_over_pxeclient() {
|
||||||
let c = FirmwareClass::classify(Some(b"PXEClient:Arch:00007"), Some(b"iPXE"));
|
let c = FirmwareClass::classify(Some(b"PXEClient:Arch:00007"), Some(b"iPXE"));
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ pub mod settings;
|
|||||||
pub mod sso;
|
pub mod sso;
|
||||||
pub mod wol;
|
pub mod wol;
|
||||||
|
|
||||||
pub use arch::{ClientArch, FirmwareClass};
|
pub use arch::{ClientArch, DriverMode, FirmwareClass};
|
||||||
pub use auth::{AdminAccount, AdminPublic, AdminStore};
|
pub use auth::{AdminAccount, AdminPublic, AdminStore};
|
||||||
pub use boot_log::{BootEvent, BootLog};
|
pub use boot_log::{BootEvent, BootLog};
|
||||||
pub use branding::{ext_for_mime, BrandingStore, LogoSlot, ALLOWED_LOGO_MIMES, MAX_LOGO_BYTES};
|
pub use branding::{ext_for_mime, BrandingStore, LogoSlot, ALLOWED_LOGO_MIMES, MAX_LOGO_BYTES};
|
||||||
|
|||||||
@@ -18,3 +18,4 @@ tracing.workspace = true
|
|||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
bytes.workspace = true
|
bytes.workspace = true
|
||||||
|
parking_lot.workspace = true
|
||||||
|
|||||||
@@ -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.
|
//! clients silently drop them.
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
pub mod escalation;
|
||||||
pub mod reply;
|
pub mod reply;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
|
|
||||||
|
pub use escalation::DriverEscalation;
|
||||||
pub use server::DhcpProxyServer;
|
pub use server::DhcpProxyServer;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
//! pass, or the HTTP URL of the boot script once iPXE has chained.
|
//! pass, or the HTTP URL of the boot script once iPXE has chained.
|
||||||
|
|
||||||
use dhcproto::v4::{DhcpOption, Message, MessageType, Opcode, OptionCode};
|
use dhcproto::v4::{DhcpOption, Message, MessageType, Opcode, OptionCode};
|
||||||
use openpxe_core::{ClientArch, FirmwareClass};
|
use openpxe_core::{ClientArch, DriverMode, FirmwareClass};
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
|
|
||||||
/// Where the reply directs the client next.
|
/// Where the reply directs the client next.
|
||||||
@@ -31,6 +31,11 @@ pub struct ReplyContext<'a> {
|
|||||||
pub our_ip: Ipv4Addr,
|
pub our_ip: Ipv4Addr,
|
||||||
pub arch: ClientArch,
|
pub arch: ClientArch,
|
||||||
pub class: FirmwareClass,
|
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.
|
/// Public base URL (scheme://host[:port]) used in HTTP directives.
|
||||||
pub public_base_url: &'a str,
|
pub public_base_url: &'a str,
|
||||||
}
|
}
|
||||||
@@ -52,9 +57,13 @@ pub fn decide(ctx: &ReplyContext<'_>) -> BootDirective {
|
|||||||
},
|
},
|
||||||
FirmwareClass::HttpClient => {
|
FirmwareClass::HttpClient => {
|
||||||
// UEFI HTTP boot: client wants an http:// URL in option 67
|
// UEFI HTTP boot: client wants an http:// URL in option 67
|
||||||
// pointing at an EFI executable. We serve ipxe.efi over HTTP;
|
// pointing at an EFI executable. We serve the iPXE EFI build for
|
||||||
// it'll then do the same script-fetch the iPXE path does.
|
// the negotiated driver mode over HTTP; it'll then do the same
|
||||||
let name = ctx.arch.ipxe_bootfile().unwrap_or("snponly.efi");
|
// script-fetch the iPXE path does.
|
||||||
|
let name = ctx
|
||||||
|
.arch
|
||||||
|
.ipxe_bootfile_mode(ctx.driver_mode)
|
||||||
|
.unwrap_or("snponly.efi");
|
||||||
BootDirective::HttpScript {
|
BootDirective::HttpScript {
|
||||||
url: format!(
|
url: format!(
|
||||||
"{}/ipxe/{}",
|
"{}/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 {
|
Some(name) => BootDirective::TftpIpxe {
|
||||||
filename: name.to_string(),
|
filename: name.to_string(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
//! UDP listener loop for the DHCP proxy. Accepts on :67 (and :4011 on a
|
//! 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.
|
//! second socket) and dispatches each datagram through the pure reply logic.
|
||||||
|
|
||||||
|
use crate::escalation::DriverEscalation;
|
||||||
use crate::reply::{build_reply, decide, BootDirective, ReplyContext};
|
use crate::reply::{build_reply, decide, BootDirective, ReplyContext};
|
||||||
use dhcproto::v4::{DhcpOption, Message, OptionCode};
|
use dhcproto::v4::{DhcpOption, Message, OptionCode};
|
||||||
use dhcproto::{Decodable, Decoder, Encodable, Encoder};
|
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 socket2::{Domain, Protocol, Socket, Type};
|
||||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
|
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -18,6 +19,9 @@ pub struct DhcpProxyServer {
|
|||||||
public_base_url: String,
|
public_base_url: String,
|
||||||
clients: Arc<ClientRegistry>,
|
clients: Arc<ClientRegistry>,
|
||||||
metrics: openpxe_core::Metrics,
|
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 {
|
impl DhcpProxyServer {
|
||||||
@@ -38,6 +42,7 @@ impl DhcpProxyServer {
|
|||||||
public_base_url,
|
public_base_url,
|
||||||
clients,
|
clients,
|
||||||
metrics,
|
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 {
|
let ctx = ReplyContext {
|
||||||
request: &request,
|
request: &request,
|
||||||
our_ip: self.our_ip,
|
our_ip: self.our_ip,
|
||||||
arch,
|
arch,
|
||||||
class,
|
class,
|
||||||
|
driver_mode,
|
||||||
public_base_url: &self.public_base_url,
|
public_base_url: &self.public_base_url,
|
||||||
};
|
};
|
||||||
let directive = decide(&ctx);
|
let directive = decide(&ctx);
|
||||||
@@ -154,7 +178,7 @@ impl DhcpProxyServer {
|
|||||||
sock.send_to(&out, dest).await?;
|
sock.send_to(&out, dest).await?;
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
target: "openpxe::dhcp",
|
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"
|
"PXE reply sent"
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -6,23 +6,32 @@
|
|||||||
//! missing, that architecture simply won't have PXE support — we log at
|
//! missing, that architecture simply won't have PXE support — we log at
|
||||||
//! startup and serve what we have.
|
//! startup and serve what we have.
|
||||||
//!
|
//!
|
||||||
//! Filename convention (matches `ClientArch::ipxe_bootfile`):
|
//! Filename convention (matches `ClientArch::ipxe_bootfile_mode`):
|
||||||
|
//!
|
||||||
|
//! DriverMode::Firmware (default — reuse the firmware UNDI/SNP NIC stack):
|
||||||
//! - `undionly.kpxe` — Legacy x86 BIOS
|
//! - `undionly.kpxe` — Legacy x86 BIOS
|
||||||
//! - `snponly-i386.efi` — IA32 UEFI
|
//! - `snponly-i386.efi` — IA32 UEFI
|
||||||
//! - `snponly.efi` — x86_64 UEFI
|
//! - `snponly.efi` — x86_64 UEFI
|
||||||
//! - `snponly-arm32.efi` — ARM32 UEFI
|
|
||||||
//! - `snponly-arm64.efi` — ARM64 UEFI
|
//! - `snponly-arm64.efi` — ARM64 UEFI
|
||||||
//! - `ipxe.efi` (fallback) — UEFI with bundled drivers, if snponly fails on a NIC
|
//!
|
||||||
|
//! DriverMode::Builtin (v0.6.1 automatic fallback — iPXE's own NIC drivers,
|
||||||
|
//! advertised when a firmware-net boot fails to chainload):
|
||||||
|
//! - `ipxe.pxe` — Legacy x86 BIOS
|
||||||
|
//! - `ipxe-i386.efi` — IA32 UEFI
|
||||||
|
//! - `ipxe.efi` — x86_64 UEFI (built from source with PNG)
|
||||||
|
//! - `ipxe-arm64.efi` — ARM64 UEFI
|
||||||
|
//!
|
||||||
//! - `wimboot` — Windows boot shim (fetched separately for WIM chains)
|
//! - `wimboot` — Windows boot shim (fetched separately for WIM chains)
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
use openpxe_core::ClientArch;
|
use openpxe_core::{ClientArch, DriverMode};
|
||||||
use rust_embed::Embed;
|
use rust_embed::Embed;
|
||||||
|
|
||||||
#[derive(Embed)]
|
#[derive(Embed)]
|
||||||
#[folder = "../../assets/ipxe/"]
|
#[folder = "../../assets/ipxe/"]
|
||||||
#[include = "*.kpxe"]
|
#[include = "*.kpxe"]
|
||||||
#[include = "*.efi"]
|
#[include = "*.efi"]
|
||||||
|
#[include = "*.pxe"]
|
||||||
#[include = "wimboot"]
|
#[include = "wimboot"]
|
||||||
pub struct IpxeAssets;
|
pub struct IpxeAssets;
|
||||||
|
|
||||||
@@ -56,25 +65,42 @@ pub fn list_assets() -> Vec<String> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Log at startup which iPXE binaries are present and which are missing.
|
/// Log at startup which iPXE binaries are present and which are missing, for
|
||||||
|
/// both driver modes. The Firmware-mode binaries are required for PXE on each
|
||||||
|
/// arch; the Builtin-mode binaries are the optional automatic NIC-driver
|
||||||
|
/// fallback (v0.6.1) — without one, escalation simply can't help that arch.
|
||||||
pub fn log_availability() {
|
pub fn log_availability() {
|
||||||
let have: std::collections::HashSet<String> = list_assets().into_iter().collect();
|
let have: std::collections::HashSet<String> = list_assets().into_iter().collect();
|
||||||
let needed = [
|
let arches = [
|
||||||
(ClientArch::LegacyX86, "undionly.kpxe"),
|
ClientArch::LegacyX86,
|
||||||
(ClientArch::Ia32Uefi, "snponly-i386.efi"),
|
ClientArch::Ia32Uefi,
|
||||||
(ClientArch::X64Uefi, "snponly.efi"),
|
ClientArch::X64Uefi,
|
||||||
// ARM32 UEFI deferred — no upstream snponly binary published.
|
// ARM32 UEFI deferred — no upstream binary published in either mode.
|
||||||
(ClientArch::Arm64Uefi, "snponly-arm64.efi"),
|
ClientArch::Arm64Uefi,
|
||||||
];
|
];
|
||||||
for (arch, name) in needed {
|
for arch in arches {
|
||||||
|
for mode in [DriverMode::Firmware, DriverMode::Builtin] {
|
||||||
|
let Some(name) = arch.ipxe_bootfile_mode(mode) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
if have.contains(name) {
|
if have.contains(name) {
|
||||||
tracing::info!(target: "openpxe::ipxe", "bundled iPXE for {}: {}", arch.as_str(), name);
|
tracing::info!(
|
||||||
} else {
|
target: "openpxe::ipxe",
|
||||||
|
"bundled iPXE for {} [{mode:?}]: {name}", arch.as_str()
|
||||||
|
);
|
||||||
|
} else if mode == DriverMode::Firmware {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
target: "openpxe::ipxe",
|
target: "openpxe::ipxe",
|
||||||
"MISSING iPXE binary for {}: {} — clients of this arch will not PXE boot",
|
"MISSING iPXE binary for {} [{mode:?}]: {name} — clients of this arch will not PXE boot",
|
||||||
arch.as_str(), name
|
arch.as_str()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
tracing::info!(
|
||||||
|
target: "openpxe::ipxe",
|
||||||
|
"no built-in-driver fallback for {} [{mode:?}]: {name} — auto NIC driver escalation unavailable for this arch",
|
||||||
|
arch.as_str()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-7
@@ -41,15 +41,31 @@ DEST="${1:-$ROOT/assets/ipxe}"
|
|||||||
WORK="$(mktemp -d)"
|
WORK="$(mktemp -d)"
|
||||||
trap 'rm -rf "$WORK"' EXIT
|
trap 'rm -rf "$WORK"' EXIT
|
||||||
|
|
||||||
# Pinned upstream iPXE. Rolling master is fine functionally, but a pin
|
# Pinned upstream iPXE. Rolling master is fine functionally, but a pin keeps
|
||||||
# keeps builds reproducible and protects against a transient master
|
# builds reproducible, protects against a transient master breakage, and —
|
||||||
# breakage. Bump deliberately.
|
# crucially for the Docker image — busting this value invalidates the cached
|
||||||
|
# ipxe-build layer so an "update iPXE" release actually recompiles from the
|
||||||
|
# new upstream. Bump deliberately to a recent master commit.
|
||||||
|
#
|
||||||
|
# v0.6.1: ipxe/ipxe master @ 2026-06-09 (newer NIC drivers + EFI fixes;
|
||||||
|
# mirrors iVentoy 1.0.35 "Update iPXE").
|
||||||
IPXE_REPO="https://github.com/ipxe/ipxe.git"
|
IPXE_REPO="https://github.com/ipxe/ipxe.git"
|
||||||
IPXE_REF="${IPXE_REF:-master}"
|
IPXE_REF="${IPXE_REF:-95ffbf4745553e8a207922389929e1943c0237c0}"
|
||||||
|
|
||||||
echo ">> cloning iPXE ($IPXE_REF)"
|
echo ">> fetching iPXE ($IPXE_REF)"
|
||||||
git clone --depth 1 --branch "$IPXE_REF" "$IPXE_REPO" "$WORK/ipxe" 2>/dev/null \
|
# Shallow-fetch the exact ref: works for a full commit SHA (GitHub allows
|
||||||
|| git clone "$IPXE_REPO" "$WORK/ipxe"
|
# reachable-SHA1-in-want) and for branch/tag names. Fall back to a full
|
||||||
|
# clone + checkout if the server refuses a direct fetch of this ref.
|
||||||
|
git init -q "$WORK/ipxe"
|
||||||
|
git -C "$WORK/ipxe" remote add origin "$IPXE_REPO"
|
||||||
|
if git -C "$WORK/ipxe" fetch -q --depth 1 origin "$IPXE_REF"; then
|
||||||
|
git -C "$WORK/ipxe" checkout -q FETCH_HEAD
|
||||||
|
else
|
||||||
|
echo " direct fetch failed; falling back to full clone + checkout"
|
||||||
|
rm -rf "$WORK/ipxe"
|
||||||
|
git clone -q "$IPXE_REPO" "$WORK/ipxe"
|
||||||
|
git -C "$WORK/ipxe" checkout -q "$IPXE_REF"
|
||||||
|
fi
|
||||||
SRC="$WORK/ipxe/src"
|
SRC="$WORK/ipxe/src"
|
||||||
|
|
||||||
echo ">> applying OpenPXE config overrides (PNG + framebuffer + console cmd)"
|
echo ">> applying OpenPXE config overrides (PNG + framebuffer + console cmd)"
|
||||||
|
|||||||
@@ -29,11 +29,19 @@ mkdir -p "$DEST"
|
|||||||
# Upstream uses arch-scoped subdirectories; we flatten to the names our
|
# Upstream uses arch-scoped subdirectories; we flatten to the names our
|
||||||
# ClientArch::ipxe_bootfile() expects.
|
# ClientArch::ipxe_bootfile() expects.
|
||||||
declare -a MAP=(
|
declare -a MAP=(
|
||||||
|
# DriverMode::Firmware (default) — reuse the firmware UNDI/SNP NIC stack.
|
||||||
"undionly.kpxe=undionly.kpxe"
|
"undionly.kpxe=undionly.kpxe"
|
||||||
"snponly.efi=x86_64-efi/snponly.efi"
|
"snponly.efi=x86_64-efi/snponly.efi"
|
||||||
"snponly-i386.efi=i386-efi/snponly.efi"
|
"snponly-i386.efi=i386-efi/snponly.efi"
|
||||||
"snponly-arm64.efi=arm64-efi/snponly.efi"
|
"snponly-arm64.efi=arm64-efi/snponly.efi"
|
||||||
"ipxe.efi=x86_64-efi/ipxe.efi" # fallback with bundled drivers
|
# DriverMode::Builtin (v0.6.1 automatic fallback) — iPXE's own all-drivers
|
||||||
|
# builds, advertised by the DHCP proxy to a MAC whose firmware NIC stack
|
||||||
|
# failed to chainload. (x86_64 ipxe.efi is rebuilt from source with PNG in
|
||||||
|
# build-ipxe.sh and overlaid on top of this fetched baseline.)
|
||||||
|
"ipxe.efi=x86_64-efi/ipxe.efi"
|
||||||
|
"ipxe.pxe=ipxe.pxe"
|
||||||
|
"ipxe-i386.efi=i386-efi/ipxe.efi"
|
||||||
|
"ipxe-arm64.efi=arm64-efi/ipxe.efi"
|
||||||
)
|
)
|
||||||
|
|
||||||
BASE="https://boot.ipxe.org"
|
BASE="https://boot.ipxe.org"
|
||||||
|
|||||||
Reference in New Issue
Block a user