//! 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); } }