//! Build DHCP proxy replies. //! //! Proxy replies look like a normal DHCPOFFER/ACK except: //! - `yiaddr` (your IP) is 0 — we don't lease. //! - `siaddr` (server IP) is us — the client will TFTP from here. //! - option 60 (vendor class) MUST be echoed as `PXEClient` or clients drop. //! - option 66 (TFTP server name) points at us. //! - option 67 (bootfile name) is per-architecture iPXE binary on first //! pass, or the HTTP URL of the boot script once iPXE has chained. use dhcproto::v4::{DhcpOption, Message, MessageType, Opcode, OptionCode}; use pxeforge_core::{ClientArch, FirmwareClass}; use std::net::Ipv4Addr; /// Where the reply directs the client next. #[derive(Debug, Clone)] pub enum BootDirective { /// Serve an iPXE binary over TFTP (first-stage chainload). TftpIpxe { filename: String }, /// Serve an iPXE boot script directly over HTTP. Used when the client is /// iPXE itself (option 77 = "iPXE") or UEFI HTTP boot (option 60 starts /// with "HTTPClient"). HttpScript { url: String }, /// Refuse to respond (architecture we don't have a binary for, or /// not-a-PXE-client). Caller should skip sending anything. Ignore, } pub struct ReplyContext<'a> { pub request: &'a Message, pub our_ip: Ipv4Addr, pub arch: ClientArch, pub class: FirmwareClass, /// Public base URL (scheme://host[:port]) used in HTTP directives. pub public_base_url: &'a str, } /// Decide what to do for an incoming request. Pure function — easy to unit /// test. Does NOT send anything. #[must_use] pub fn decide(ctx: &ReplyContext<'_>) -> BootDirective { match ctx.class { FirmwareClass::IpxeUserClass => BootDirective::HttpScript { url: format!("{}/boot.ipxe", ctx.public_base_url.trim_end_matches('/')), }, 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"); BootDirective::HttpScript { url: format!("{}/ipxe/{}", ctx.public_base_url.trim_end_matches('/'), name), } } FirmwareClass::PxeClient => match ctx.arch.ipxe_bootfile() { Some(name) => BootDirective::TftpIpxe { filename: name.to_string() }, None => BootDirective::Ignore, }, FirmwareClass::Other => BootDirective::Ignore, } } /// Build the outgoing DHCPOFFER (or ACK, matching request type) for a /// directive. Caller is responsible for sending the bytes on the wire. pub fn build_reply(ctx: &ReplyContext<'_>, directive: &BootDirective) -> Option { let reply_type = match request_message_type(ctx.request)? { MessageType::Discover => MessageType::Offer, MessageType::Request | MessageType::Inform => MessageType::Ack, _ => return None, }; let mut msg = Message::default(); msg.set_opcode(Opcode::BootReply) .set_htype(ctx.request.htype()) .set_hops(0) .set_xid(ctx.request.xid()) .set_secs(0) .set_flags(ctx.request.flags()) .set_ciaddr(Ipv4Addr::UNSPECIFIED) .set_yiaddr(Ipv4Addr::UNSPECIFIED) // proxy does not lease .set_siaddr(ctx.our_ip) .set_giaddr(ctx.request.giaddr()) .set_chaddr(ctx.request.chaddr()); // Set the BOOTP `file` field for legacy PXE stacks before we take the // options borrow (the two borrows can't overlap). if let BootDirective::TftpIpxe { filename } = directive { msg.set_fname_str(filename); } let class_echo: &[u8] = match ctx.class { FirmwareClass::HttpClient => b"HTTPClient", _ => b"PXEClient", }; let opts = msg.opts_mut(); opts.insert(DhcpOption::MessageType(reply_type)); opts.insert(DhcpOption::ServerIdentifier(ctx.our_ip)); // Echo the vendor class — REQUIRED by spec for the client to accept. opts.insert(DhcpOption::ClassIdentifier(class_echo.to_vec())); match directive { BootDirective::TftpIpxe { filename } => { opts.insert(DhcpOption::TFTPServerName(ctx.our_ip.to_string().into_bytes())); opts.insert(DhcpOption::BootfileName(filename.as_bytes().to_vec())); } BootDirective::HttpScript { url } => { opts.insert(DhcpOption::BootfileName(url.as_bytes().to_vec())); opts.insert(DhcpOption::TFTPServerName(ctx.our_ip.to_string().into_bytes())); } BootDirective::Ignore => return None, } opts.insert(DhcpOption::End); Some(msg) } fn request_message_type(m: &Message) -> Option { m.opts().get(OptionCode::MessageType).and_then(|o| match o { DhcpOption::MessageType(t) => Some(*t), _ => None, }) }