Name update
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
//! Client architecture detection from DHCP options.
|
||||
//!
|
||||
//! Primary source is DHCP option 93 (Client System Architecture, RFC 4578/5970).
|
||||
//! Some firmwares report `0x0009` ("EFI BC") instead of `0x0007` — aliased here.
|
||||
//! Secondary: option 60 vendor-class with `HTTPClient` signals native UEFI HTTP
|
||||
//! boot, in which case we can skip TFTP and return an http:// URL directly.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum ClientArch {
|
||||
/// Legacy x86 BIOS PXE (option 93 = 0x0000).
|
||||
LegacyX86,
|
||||
/// IA32 UEFI (option 93 = 0x0006).
|
||||
Ia32Uefi,
|
||||
/// x86_64 UEFI (option 93 = 0x0007 or 0x0009).
|
||||
X64Uefi,
|
||||
/// ARM32 UEFI (option 93 = 0x000A).
|
||||
Arm32Uefi,
|
||||
/// ARM64 UEFI (option 93 = 0x000B).
|
||||
Arm64Uefi,
|
||||
/// Unknown / unsupported architecture; caller should log and skip.
|
||||
Unknown(u16),
|
||||
}
|
||||
|
||||
impl ClientArch {
|
||||
#[must_use]
|
||||
pub fn from_option_93(value: u16) -> Self {
|
||||
match value {
|
||||
0x0000 => Self::LegacyX86,
|
||||
0x0006 => Self::Ia32Uefi,
|
||||
0x0007 | 0x0009 => Self::X64Uefi,
|
||||
0x000A => Self::Arm32Uefi,
|
||||
0x000B => Self::Arm64Uefi,
|
||||
other => Self::Unknown(other),
|
||||
}
|
||||
}
|
||||
|
||||
/// Default iPXE binary filename to return via TFTP for this architecture.
|
||||
/// Uses `snponly` variants which reuse the firmware's UNDI/SNP network
|
||||
/// stack — smaller binaries and broader hardware compatibility than the
|
||||
/// all-drivers-included `ipxe.efi`.
|
||||
#[must_use]
|
||||
pub fn ipxe_bootfile(self) -> Option<&'static str> {
|
||||
Some(match self {
|
||||
Self::LegacyX86 => "undionly.kpxe",
|
||||
Self::Ia32Uefi => "snponly-i386.efi",
|
||||
Self::X64Uefi => "snponly.efi",
|
||||
// ARM32 UEFI: upstream boot.ipxe.org does not publish a prebuilt
|
||||
// snponly variant for this arch. We return None so the DHCP
|
||||
// proxy declines rather than advertising a file we can't serve.
|
||||
Self::Arm32Uefi => return None,
|
||||
Self::Arm64Uefi => "snponly-arm64.efi",
|
||||
Self::Unknown(_) => return None,
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::LegacyX86 => "bios",
|
||||
Self::Ia32Uefi => "uefi-ia32",
|
||||
Self::X64Uefi => "uefi-x64",
|
||||
Self::Arm32Uefi => "uefi-arm32",
|
||||
Self::Arm64Uefi => "uefi-arm64",
|
||||
Self::Unknown(_) => "unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Which firmware class issued the DHCP request. Used to decide the reply
|
||||
/// path — PXEClient gets TFTP/iPXE chainload, HTTPClient gets an HTTP URL,
|
||||
/// iPXE itself gets the boot script URL.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FirmwareClass {
|
||||
/// Firmware PXE ROM (option 60 = "PXEClient").
|
||||
PxeClient,
|
||||
/// UEFI HTTP boot (option 60 = "HTTPClient").
|
||||
HttpClient,
|
||||
/// iPXE (option 77 user-class = "iPXE").
|
||||
IpxeUserClass,
|
||||
/// No recognised signature — likely not a PXE client at all.
|
||||
Other,
|
||||
}
|
||||
|
||||
impl FirmwareClass {
|
||||
/// Classify a DHCP request. `vendor_class_60` is option 60 (vendor class
|
||||
/// identifier); `user_class_77` is option 77 (user class). Check user-class
|
||||
/// first because iPXE-that-we-chainloaded will set option 60 to PXEClient
|
||||
/// *and* option 77 to iPXE, and the iPXE classification wins.
|
||||
#[must_use]
|
||||
pub fn classify(vendor_class_60: Option<&[u8]>, user_class_77: Option<&[u8]>) -> Self {
|
||||
if let Some(uc) = user_class_77 {
|
||||
if uc.windows(b"iPXE".len()).any(|w| w == b"iPXE") {
|
||||
return Self::IpxeUserClass;
|
||||
}
|
||||
}
|
||||
match vendor_class_60 {
|
||||
Some(v) if v.starts_with(b"PXEClient") => Self::PxeClient,
|
||||
Some(v) if v.starts_with(b"HTTPClient") => Self::HttpClient,
|
||||
_ => Self::Other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn arch_aliases_0x0009_to_x64() {
|
||||
assert_eq!(ClientArch::from_option_93(0x0007), ClientArch::X64Uefi);
|
||||
assert_eq!(ClientArch::from_option_93(0x0009), ClientArch::X64Uefi);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_and_arm() {
|
||||
assert_eq!(ClientArch::from_option_93(0x0000), ClientArch::LegacyX86);
|
||||
assert_eq!(ClientArch::from_option_93(0x000B), ClientArch::Arm64Uefi);
|
||||
assert!(matches!(
|
||||
ClientArch::from_option_93(0x1234),
|
||||
ClientArch::Unknown(0x1234)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bootfile_names_stable() {
|
||||
assert_eq!(ClientArch::LegacyX86.ipxe_bootfile(), Some("undionly.kpxe"));
|
||||
assert_eq!(ClientArch::X64Uefi.ipxe_bootfile(), Some("snponly.efi"));
|
||||
assert_eq!(ClientArch::Arm64Uefi.ipxe_bootfile(), Some("snponly-arm64.efi"));
|
||||
assert_eq!(ClientArch::Unknown(0xFFFF).ipxe_bootfile(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn firmware_class_detects_ipxe_over_pxeclient() {
|
||||
let c = FirmwareClass::classify(Some(b"PXEClient:Arch:00007"), Some(b"iPXE"));
|
||||
assert_eq!(c, FirmwareClass::IpxeUserClass);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn firmware_class_http() {
|
||||
let c = FirmwareClass::classify(Some(b"HTTPClient:Arch:00016"), None);
|
||||
assert_eq!(c, FirmwareClass::HttpClient);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn firmware_class_plain_pxe() {
|
||||
let c = FirmwareClass::classify(Some(b"PXEClient"), None);
|
||||
assert_eq!(c, FirmwareClass::PxeClient);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user