From 64ea8e2659ff4d66a013528823152d86ce47e1be Mon Sep 17 00:00:00 2001 From: Melora Hugues Date: Tue, 15 Aug 2023 21:31:59 +0200 Subject: [PATCH] add first version of uefi client --- .gitignore | 1 + Cargo.toml | 6 +- Makefile | 12 ++++ README.md | 14 +++++ rust-toolchain.toml | 2 + src/config.rs | 71 ++++++++++++++++++++++ src/main.rs | 45 +++++++++++++- src/multicast.rs | 145 ++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 292 insertions(+), 4 deletions(-) create mode 100644 Makefile create mode 100644 rust-toolchain.toml create mode 100644 src/config.rs create mode 100644 src/multicast.rs diff --git a/.gitignore b/.gitignore index 193d30e..8d66e04 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ Cargo.lock # Added by cargo /target +build/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index fc4b370..05ca0bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,8 @@ name = "http_boot_client" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] +log = "0.4.19" +uefi = { version = "0.24.0", features = ["alloc"] } +uefi-raw = "0.3.0" +uefi-services = "0.21.0" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..77c1a23 --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +.PHONY: build + +build: + cargo build --target x86_64-unknown-uefi + +testvm: + cp target/x86_64-unknown-uefi/debug/http_boot_client.efi build/esp/efi/httpboot/httpboot.efi + qemu-system-x86_64 -enable-kvm \ + -drive if=pflash,format=raw,readonly=on,file=build/OVMF_CODE.fd \ + -drive if=pflash,format=raw,readonly=off,file=build/OVMF_VARS.fd \ + -drive format=raw,file=fat:rw:build/esp \ + -netdev user,id=n1 -device virtio-net-pci,netdev=n1 diff --git a/README.md b/README.md index 20bffb7..f21cecc 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,16 @@ # http-boot-client +Commands to add the correct values to UEFI shell + +``` +# 0x2a corresponds to 42 +setvar -rt -nv -guid 638a24ce-a0ce-4908-8152-b2a4e8b2f29d HTTP_BOOT_MCAST_PORT =2a +setvar -rt -nv -guid 638a24ce-a0ce-4908-8152-b2a4e8b2f29d HTTP_BOOT_MCAST_GROUP =ff0200000000000000000000abcd1234 +setvar -rt -nv -guid 638a24ce-a0ce-4908-8152-b2a4e8b2f29d HTTP_BOOT_UUID ="ffaf8813-fe27-4b1d-9d24-edb1baee9db9" +``` + +View current values + +``` +dmpstore -guid 638a24ce-a0ce-4908-8152-b2a4e8b2f29d +``` diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..e756ee7 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +targets = ["aarch64-unknown-uefi", "i686-unknown-uefi", "x86_64-unknown-uefi"] diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..d35f7f5 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,71 @@ +use alloc::string::String; +use log::{debug, info}; +use uefi::prelude::*; +use uefi::proto::network::IpAddress; +use uefi::CStr16; +use uefi::{Error, Result}; +use uefi_raw::table::runtime::VariableVendor; +use uefi_raw::{Guid, Status}; + +extern crate alloc; + +const HTTP_BOOT_VENDOR: VariableVendor = + VariableVendor(Guid::parse_or_panic("638a24ce-a0ce-4908-8152-b2a4e8b2f29d")); + +pub struct Config { + pub multicast_group: IpAddress, + pub multicast_port: u16, + pub client_uuid: String, +} + +fn get_var<'a>( + runtime_services: &RuntimeServices, + var_name: &'a CStr16, + res: &'a mut [u8], +) -> Result<()> { + match runtime_services.get_variable(var_name, &HTTP_BOOT_VENDOR, res) { + Err(e) => Err(e), + Ok(_r) => Ok(()), + } +} + +pub fn read_config(runtime_services: &RuntimeServices) -> Result { + info!("Reading EFI configuration from EFI vars"); + + let mut conf = Config { + multicast_group: IpAddress::new_v6([ + 0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, + ]), + multicast_port: 42, + client_uuid: String::from("24609104-7ec6-423f-b57f-632b99b7566a"), + }; + + let mut port_buf: [u8; 2] = [0; 2]; // port is 16 bytes encoded + get_var( + runtime_services, + cstr16!("HTTP_BOOT_MCAST_PORT"), + &mut port_buf, + )?; + conf.multicast_port = u16::from_ne_bytes(port_buf); + debug!("Got port {}", conf.multicast_port); + + let mut group_buf: [u8; 16] = [0; 16]; // at most ipv6 is 16 bytes + get_var( + runtime_services, + cstr16!("HTTP_BOOT_MCAST_GROUP"), + &mut group_buf, + )?; + conf.multicast_group = IpAddress::new_v6(group_buf); + debug!("Got group {:?}", conf.multicast_group); + + let mut uid_buf: [u8; 36] = [0; 36]; // uuid is 36 bytes + get_var(runtime_services, cstr16!("HTTP_BOOT_UUID"), &mut uid_buf)?; + conf.client_uuid = match String::from_utf8(uid_buf.to_vec()) { + Err(_e) => return Err(Error::from(Status::INVALID_PARAMETER)), + Ok(s) => s, + }; + debug!("Got uuid {:?}", conf.client_uuid); + + Ok(conf) +} diff --git a/src/main.rs b/src/main.rs index e7a11a9..2fefba1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,44 @@ -fn main() { - println!("Hello, world!"); +#![no_main] +#![no_std] + +pub mod config; +pub mod multicast; + +use log::info; +use uefi::fs::PathBuf; +use uefi::prelude::*; +use uefi::table::boot::LoadImageSource; +use uefi::{Error, Result, Status}; + +fn load_efi_app_from_path(boot_services: &BootServices, app_path: PathBuf) -> Result { + let mut fs_handler = boot_services.get_image_file_system(boot_services.image_handle())?; + let app_content = match fs_handler.read(app_path) { + Err(_e) => return Err(Error::from(Status::PROTOCOL_ERROR)), + Ok(x) => x, + }; + let image_source = LoadImageSource::FromBuffer { + buffer: &app_content, + file_path: None, + }; + let img_handler = boot_services.load_image(boot_services.image_handle(), image_source)?; + boot_services.start_image(img_handler) +} + +#[entry] +fn main(_handle: Handle, mut system_table: SystemTable) -> Status { + info!("init service"); + + uefi_services::init(&mut system_table).unwrap(); + + info!("init boot services"); + let boot_services = system_table.boot_services(); + + info!("init runtime services"); + let runtime_services = system_table.runtime_services(); + + let conf = config::read_config(runtime_services).unwrap(); + let app_path = multicast::send_mcast(boot_services, &conf).unwrap(); + load_efi_app_from_path(boot_services, app_path).unwrap(); + + Status::SUCCESS } diff --git a/src/multicast.rs b/src/multicast.rs new file mode 100644 index 0000000..f7771b1 --- /dev/null +++ b/src/multicast.rs @@ -0,0 +1,145 @@ +use alloc::string::String; +use log::{debug, error, info}; +use uefi::prelude::*; +use uefi::proto::network::pxe::{BaseCode, UdpOpFlags}; +use uefi::proto::network::IpAddress; +use uefi::table::boot::{ScopedProtocol, SearchType}; +use uefi::{Error, Identify, Result}; +extern crate alloc; +use crate::config::Config; +use uefi::data_types::CStr16; +use uefi::fs::{Path, PathBuf}; + +fn split_arg(raw_val: &str) -> Option<(&str, &str)> { + let mut splitted = raw_val.split("="); + let arg_name = match splitted.next() { + None => return None, + Some(v) => v, + }; + let arg_value = match splitted.next() { + None => return None, + Some(v) => v, + }; + return Some((arg_name, arg_value)); +} + +fn get_boot_path<'a>(msg: String) -> Result { + debug!("Splitting message"); + + // a message has the following format: "ACTION arg1=val1,arg2=val2" + let mut splitted_msg_iter = msg.split_ascii_whitespace(); + + debug!("Checking if message is a valid BOOT_ACCEPT message"); + match splitted_msg_iter.next() { + None => return Err(Error::new(Status::UNSUPPORTED, "empty message")), + Some(action) => match action { + "BOOT_ACCEPT" => (), + "BOOT_DENY" => return Err(Error::new(Status::UNSUPPORTED, "boot refused")), + _ => return Err(Error::new(Status::UNSUPPORTED, "unexpected action")), + }, + } + + debug!("Parsing message arguments"); + match splitted_msg_iter.next() { + None => return Err(Error::new(Status::UNSUPPORTED, "empty list of arguments")), + Some(args_list) => { + // split all the arguments in a comma separated list + for arg in args_list.split(",") { + match split_arg(arg) { + None => debug!("unexpected format for argument {}", arg), + Some((arg_name, arg_value)) => match arg_name { + "id" => debug!("found id {}", arg_value), + "efi_app" => { + debug!("found efi app {}", arg_value); + let mut buf = [0; 128]; + let path_raw = CStr16::from_str_with_buf(arg_value, &mut buf) + .expect("invalid str format"); + let p = Path::new(path_raw); + let mut res = PathBuf::new(); + res.push(p); + return Ok(res); + } + _ => debug!("unexpected argument {} with value {}", arg_name, arg_value), + }, + } + } + } + } + Err(Error::new(Status::UNSUPPORTED, "no efi app defined")) +} + +fn read_mcast_response<'a>(base_client: &mut ScopedProtocol<'_, BaseCode>) -> Result { + let mut buf: [u8; 200] = [0; 200]; + + let n = base_client + .udp_read( + UdpOpFlags::ANY_DEST_IP + | UdpOpFlags::ANY_DEST_PORT + | UdpOpFlags::ANY_SRC_IP + | UdpOpFlags::ANY_SRC_PORT, + None, + None, + None, + None, + None, + &mut buf, + ) + .unwrap(); + + info!("Read {} bytes", n); + let received_msg = String::from_utf8(buf[..n].to_vec()).unwrap(); + info!("Received message {}", received_msg); + match get_boot_path(received_msg) { + Ok(r) => Ok(r), + Err(e) => { + error!("failed to get boot path: {}", e.data()); + Err(e.to_err_without_payload()) + } + } +} + +pub fn send_mcast<'a>(boot_service: &BootServices, conf: &Config) -> Result { + debug!("Initializing base bootservice client handle"); + let base_client_handle = *boot_service + .locate_handle_buffer(SearchType::ByProtocol(&BaseCode::GUID))? + .first() + .expect("DevicePathToText is missing"); + + let mut base_client = boot_service.open_protocol_exclusive::(base_client_handle)?; + + debug!("Initializing source IP address"); + let src_bytes: [u8; 16] = [ + 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x02, 0x16, 0x3e, 0xff, 0xfe, 0xc2, 0x16, + 0xb7, + ]; + let src_address: IpAddress = IpAddress::new_v6(src_bytes); + + let mut req_msg = String::from("BOOT_REQUEST id="); + req_msg.push_str(&conf.client_uuid); + + info!("Starting PXE client"); + debug!("Starting PXE client"); + base_client.start(true)?; + + debug!("Setting IP address for PXE client"); + base_client.set_station_ip(Some(&src_address), None)?; + + info!("Sending UDP multicast message"); + base_client.udp_write( + UdpOpFlags::MAY_FRAGMENT, + &conf.multicast_group, + conf.multicast_port, + None, + Some(&src_address), + None, + None, + req_msg.as_bytes(), + )?; + let res = read_mcast_response(&mut base_client); + + info!("Stopping PXE client"); + base_client.stop()?; + + info!("PXE client successfully executed"); + res +}