Files
OpenPXE/crates/core/src/arch.rs
T
2026-05-21 02:13:08 -04:00

154 lines
5.3 KiB
Rust

//! 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 | Self::Unknown(_) => return None,
Self::Arm64Uefi => "snponly-arm64.efi",
})
}
#[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);
}
}