Road to PXE - Day 3
why did this take so long? #
Well you see I started this project right at a very busy time and I may have bitten off more than I could chew. In the past few months I have gotten a promotion, did a MS Cert (AZ-900) , ran my first half marathon (not a race yet, just ran the distance) and lost a few more kg’s (down 25kg this year).
So uh… I had my hands a little full to say the least 😅
Anyway, I’ve settled into things a bit more now and I’m ready to code so let’s get this show on the road.
where was I? #
In the time since the 2nd post in this series System76 released the Pop!_OS COSMIC alpha. Being the massive f*cking nerd I am, I may have switched over to it as my daily driver.
Needless to say in the excited rush to install it I didn’t backup this project.
But why aren’t you using git? I hear you screaming.
This project was in its early stages and I figured I was gonna have to throw it all away and rewrite it again.
The day of judgment has come and now I shall rewrite. Fortunately I had these blog posts to help pick the pieces back up.
back on the grind #
After a little bit of coding I was back to the same place with getting SeaBIOS able to PXE Boot but not UEFI. By switching undionly.kpxe out for ipxe.efi in the TFTP server and my router config I was able to get UEFI machines to boot. Cool…
This is all nice but I have a fancy router (EdgeRouter). What about people that can’t screw with their routers that much?
In my research so far I have found that most people are using dnsmasq to get PXE working without directly controlling the DHCP server. Looking into it more it seems what we are looking for is proxydhcp.
After some more googling I found this awesome go project by Jacob Weinstock that did what I want proxydhcp. This repo really ended up cracking this thing open for me although it took quite a while to decipher.
I will spare you the details of how I worked out things based on that repo, wireshark, and the RFCs. Instead I will just link to the docs that helped me most.
RFC 2131 - Dynamic Host Configuration Protocol
RFC 4578 - Dynamic Host Configuration Protocol (DHCP) Options for the Preboot eXecution Environment (PXE)
RFC 2132 - DHCP Options and BOOTP Vendor Extensions
RFC 1497 - BOOTP Vendor Information Extensions
The link in RFC 4578 -
pix.net - pxespec.pdf
seems to be dead so I found a mirror of it here - dimitrije.website - pxespec.pdf
With that out of the way lets get into how we solve the issue
finally solving the f*cking bootfile dilema #
First things first lets turn off the PXE settings on the edgerouter so it’s just us sending PXE options.
Now that that is done I pretty quickly worked out how to create packets and send them with dhcproto.
What do we send? Packets!
What is in them? uh..
So basically what we need to do is.
If all of this is true we send a response
Packet Opcode is Boot Request
DHCP Option 53 (Message Type) is Discover or Request
DHCP Option 55 (Parameter Request List) is set
DHCP Option 60 (Class Identifier) starts with PXEClient
DHCP Option 93 (Client System Architecture) is set
DHCP Option 94 (Client Network Interface) is set
in rust with dhcproto that looks something like this.
fn handle_packet(message: Message) -> Option<Message> {
let options = message.opts();
let mac_address = message.chaddr();
let opcode = message.opcode();
let architecture = options.get(OptionCode::ClientSystemArchitecture);
let network_interface = options.get(OptionCode::ClientNetworkInterface);
let vendor_class = options.get(OptionCode::ClassIdentifier);
let message_type = options.get(OptionCode::MessageType);
let user_class = options.get(OptionCode::UserClass);
let requested_params = options.get(OptionCode::ParameterRequestList);
match (
opcode,
message_type,
requested_params,
vendor_class,
architecture,
network_interface,
) {
(
Opcode::BootRequest, // is a boot request
Some(DhcpOption::MessageType(message_type)), // option 53 is set
Some(DhcpOption::ParameterRequestList(_)), //option 55 is set
Some(DhcpOption::ClassIdentifier(class_id)), // option 60 is set
Some(DhcpOption::ClientSystemArchitecture(request_architecture)), // option 93 is set
Some(DhcpOption::ClientNetworkInterface(_, _, _)), // option 94 is set
) => {
if message_type != &MessageType::Discover && message_type != &MessageType::Request {
// message_type(opt 53) must be Discover or Request
return None;
}
let class_id_str: &str = str::from_utf8(class_id).unwrap_or_default();
if !class_id_str.starts_with("PXEClient") {
// class_id(opt 60) must start with PXEClient
return None;
}
// Create and return a new response packet here
}
_ => None
}
}
Okay that’s a fair bit… oh well now we just need a response to send back…
How hard can it be…
Well Jacob’s code in proxydhcp seems to hold the keys.
Basically we need to send a message with the following headers/options.
Headers:
Field | Value | Comment |
---|---|---|
Opcode | 2 | Boot Reply |
xid | * | xid from request |
flags | * | Broadcast true |
ciaddr | 0.0.0.0 | Default for server |
siaddr | * | IP address of the TFTP/HTTP server |
chaddr | * | chaddr from request |
sname | * | IP address of the TFTP/HTTP server |
Options:
Tag Name | Tag Number | Length | Data |
---|---|---|---|
Message Type | 53 | 1 | DHCP Offer |
Server Identifier | 54 | 4 | IP address of the TFTP/HTTP server |
Class Identifier | 60 | 9 | PXEClient |
Bootfile Name | 67 | Varies | name of bootfile or http url |
Vendor Options | 43 | Varies | Encapsulated options below |
PXE_DISCOVERY_CONTROL | 6 | 8 | 8 bytes set to 0 |
In rust with dhcproto that looks a little bit like this
info!(
"Responding to {} ({:?},{}) with {} ({})",
HexSlice::new(mac_address),
request_architecture,
request_user_class,
redirect_to.to_string(),
boot_file
);
let mut response = Message::default();
response
.set_flags(Flags::default().set_broadcast())
.set_chaddr(&mac_address)
.set_xid(message.xid())
.set_siaddr(redirect_to)
.set_sname(redirect_to.to_string().as_bytes())
.set_opcode(Opcode::BootReply)
.opts_mut()
.insert(DhcpOption::MessageType(MessageType::Offer));
let mut vendor_options: Vec<u8> = Vec::new();
vendor_options.push(6); // Set Option 6
vendor_options.push(8); // Length 8 Bytes
vendor_options.append(&mut vec![0, 0, 0, 0, 0, 0, 0, 0]); // 8 Empty Bytes
vendor_options.push(255); // PXEClient End
response
.opts_mut()
.insert(DhcpOption::VendorExtensions(vendor_options));
response
.opts_mut()
.insert(DhcpOption::ServerIdentifier(redirect_to));
response
.opts_mut()
.insert(DhcpOption::ClassIdentifier("PXEClient".as_bytes().to_vec()));
response
.opts_mut()
.insert(DhcpOption::BootfileName(boot_file.as_bytes().to_vec()));
Some(response)
Does it work? #
yes 🙂
rusted-pxe on da thinkpad?
What now? #
Well I am really happy with the progress so far but I want to see if I can stretch the limits here.
Ideas:
- Boot Menu with multiple options - linux, windows, recovery environments, etc.
- Automatically install windows - autounattend
- Maybe Automatically autopilot join laptops?
I’m open to hear more so hit me up on my socials below if you have any other ideas.
oh also I did the thing and its on github finally - jordangomes/rusted-pxe