diff --git a/Cargo.toml b/Cargo.toml index e8f9d58..1fea2a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,3 +8,6 @@ edition = "2021" [dependencies.nom] version = "7" default-features = false + +[dependencies] +thiserror = "1" \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 8e1cd88..45c6982 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,19 @@ #![allow(unused)] use std::{net::Ipv4Addr, path::Path}; +//#![no_std] +use alloc::vec::Vec; +use core::net::Ipv4Addr; +use options::{DhcpOption, RawDhcpOption, DhcpOptionError}; use nom::{ - bytes::complete::{take, take_until}, + bytes::complete::{take, take_until, take_while}, + multi::many0_count, number::complete::{be_u16, be_u32, be_u8}, IResult, }; #[derive(Debug)] -struct DhcpMessage<'a> { +struct Dhcpv4Message<'a> { operation: Operation, hardware_type: HardwareType, hardware_address_length: u8, @@ -22,6 +27,10 @@ struct DhcpMessage<'a> { client_hardware_address: &'a [u8], server_name: &'a str, file_name: &'a Path, + { + let opt = self.options.iter().find(|a| a.code == T::CODE)?; + Some(T::parse(opt)) + } } #[derive(Debug)] @@ -61,11 +70,10 @@ impl From for HardwareType { } } -fn parse_dhcp_operation(i: &[u8]) -> IResult<&[u8], Operation> { be_u8(i).map(|(i, v)| (i, v.into())) } -fn parse_dhcp_flags(i: &[u8]) -> IResult<&[u8], Flags> { +fn parse_flags(i: &[u8]) -> IResult<&[u8], Flags> { be_u16(i).map(|(i, v)| (i, Flags(v))) } @@ -86,8 +94,9 @@ fn parse_client_hardware_address(i: &[u8], hardware_address_length: u8) -> IResu } fn parse_server_name(i: &[u8]) -> IResult<&[u8], &str> { - let nullbyte_position = i.iter().position(|v| *v == b'\0').unwrap_or(64); - let val = unsafe { std::str::from_utf8_unchecked(&i[..nullbyte_position]) }; + let (i, val) = take(64usize)(i)?; + let nullbyte_position = val.iter().position(|v| *v == b'\0').unwrap_or(64); + let val = unsafe { core::str::from_utf8_unchecked(&val[..nullbyte_position]) }; Ok((i, val)) } @@ -95,18 +104,28 @@ fn parse_server_name(i: &[u8]) -> IResult<&[u8], &str> { fn parse_file(i: &[u8]) -> IResult<&[u8], &Path> { let nullbyte_position = i.iter().position(|v| *v == b'\0').unwrap_or(128); let val = unsafe { std::str::from_utf8_unchecked(&i[..nullbyte_position]) }; +fn parse_option(i: &[u8]) -> IResult<&[u8], RawDhcpOption> { + let (i, code) = be_u8(i)?; + let (i, length) = be_u8(i)?; + let (i, data) = take(length)(i)?; + + Ok((i, RawDhcpOption { code, length, data })) +} + +fn parse_file(i: &[u8]) -> IResult<&[u8], &str> { + let (i, val) = take(128usize)(i)?; Ok((i, Path::new(val))) } -fn parse_dhcp_packet(packet: &[u8]) -> IResult<&[u8], DhcpMessage> { - let (packet, operation) = parse_dhcp_operation(packet)?; +fn parse_dhcp_packet(packet: &[u8]) -> IResult<&[u8], Dhcpv4Message> { + let (packet, operation) = parse_operation(packet)?; let (packet, hardware_type) = parse_dhcp_hardware_type(packet)?; let (packet, hardware_address_length) = be_u8(packet)?; let (packet, hops) = be_u8(packet)?; let (packet, transaction_id) = be_u32(packet)?; let (packet, seconds) = be_u16(packet)?; - let (packet, flags) = parse_dhcp_flags(packet)?; + let (packet, flags) = parse_flags(packet)?; let (packet, client_address) = parse_ipv4_address(packet)?; let (packet, server_address) = parse_ipv4_address(packet)?; let (packet, gateway_address) = parse_ipv4_address(packet)?; @@ -117,10 +136,21 @@ fn parse_dhcp_packet(packet: &[u8]) -> IResult<&[u8], DhcpMessage> { let (_, server_name) = parse_server_name(server_name_bytes)?; let (_, file_name) = parse_file(file_name_bytes)?; + if let Ok((mut packet, magic)) = take::<_, _, ()>(4usize)(packet) { + while !packet.is_empty() && packet[0] != 0xff { + let (remaining, option) = parse_option(packet)?; + options.push(option); + let (remaining, _) = take_while(|x| x == 0x00)(remaining)?; + packet = remaining; + } + let (_, _server_name) = parse_server_name(server_name_bytes)?; + server_name = Some(_server_name); + let (_, _file_name) = parse_file(file_name_bytes)?; + } Ok(( packet, - DhcpMessage { + Dhcpv4Message { operation, hardware_type, hardware_address_length, @@ -168,11 +198,18 @@ mod tests { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x63, 0x82, 0x53, 0x63, // Magic cookie + 0x01, 0x04, 0x7f, 0x00, 0x00, 0x01, // Subnet Mask Option + 0x00, 0x00, 0x00, // Paddings + 0x03, 0x08, 0x7f, 0x00, 0x00, 0x01, 0x7f, 0x00, 0x00, 0x02// Router Option ]; #[test] fn parses() { - let res = super::parse_dhcp_packet(DHCP_PACKET).unwrap(); - dbg!(res); + use crate::options; + let (buf, res) = super::parse_dhcp_packet(DHCP_PACKET).unwrap(); + dbg!(&res); + dbg!(res.get_option::()); + dbg!(res.get_option::()); } } diff --git a/src/options/boot_file_size_option.rs b/src/options/boot_file_size_option.rs new file mode 100644 index 0000000..02f7181 --- /dev/null +++ b/src/options/boot_file_size_option.rs @@ -0,0 +1,17 @@ +use super::{DhcpOptionError, RawDhcpOption}; +use crate::options::DhcpOption; + +pub struct BootFileSizeOption; + +impl DhcpOption for BootFileSizeOption { + const CODE: u8 = 13; + type Item = u16; + + fn parse(option: &RawDhcpOption) -> Result { + (option.length == 2) + .then_some(()) + .ok_or(DhcpOptionError::InvalidLength(option.length, 2))?; + let data = option.data[..2].try_into().expect("Parser pulled 2 bytes"); + Ok(u16::from_be_bytes(data)) + } +} diff --git a/src/options/cookie_server_option.rs b/src/options/cookie_server_option.rs new file mode 100644 index 0000000..3235185 --- /dev/null +++ b/src/options/cookie_server_option.rs @@ -0,0 +1,22 @@ +use super::{DhcpOptionError, RawDhcpOption}; +use crate::options::DhcpOption; +use core::net::Ipv4Addr; + +pub struct CookieServerOption; + +impl DhcpOption for CookieServerOption { + const CODE: u8 = 8; + type Item = Vec; + + fn parse(option: &RawDhcpOption) -> Result { + let rem = option.length % 4; + (rem == 0) + .then_some(()) + .ok_or(DhcpOptionError::InvalidLength(option.length, option.length + 4 - rem))?; + Ok(option + .data + .chunks(4) + .map(|c| Ipv4Addr::new(c[0], c[1], c[2], c[3])) + .collect()) + } +} \ No newline at end of file diff --git a/src/options/domain_name.rs b/src/options/domain_name.rs new file mode 100644 index 0000000..886e37a --- /dev/null +++ b/src/options/domain_name.rs @@ -0,0 +1,15 @@ +use super::{RawDhcpOption, DhcpOptionError}; +use crate::options::DhcpOption; +use core::net::Ipv4Addr; + +pub struct DomainName; + +impl DhcpOption for DomainName { + const CODE: u8 = 15; + type Item = String; + + fn parse(option: &RawDhcpOption) -> Result { + let data = core::str::from_utf8(&option.data)?; + Ok(data.to_string()) + } +} diff --git a/src/options/domain_name_server_option.rs b/src/options/domain_name_server_option.rs new file mode 100644 index 0000000..05ea70d --- /dev/null +++ b/src/options/domain_name_server_option.rs @@ -0,0 +1,22 @@ +use super::{DhcpOptionError, RawDhcpOption}; +use crate::options::DhcpOption; +use core::net::Ipv4Addr; + +pub struct DomainNameServerOption; + +impl DhcpOption for DomainNameServerOption { + const CODE: u8 = 6; + type Item = Vec; + + fn parse(option: &RawDhcpOption) -> Result { + let rem = option.length % 4; + (rem == 0) + .then_some(()) + .ok_or(DhcpOptionError::InvalidLength(option.length, option.length + 4 - rem))?; + Ok(option + .data + .chunks(4) + .map(|c| Ipv4Addr::new(c[0], c[1], c[2], c[3])) + .collect()) + } +} diff --git a/src/options/extensions_path.rs b/src/options/extensions_path.rs new file mode 100644 index 0000000..59a901a --- /dev/null +++ b/src/options/extensions_path.rs @@ -0,0 +1,15 @@ +use super::{RawDhcpOption, DhcpOptionError}; +use crate::options::DhcpOption; +use core::net::Ipv4Addr; + +pub struct ExtensionsPath; + +impl DhcpOption for ExtensionsPath { + const CODE: u8 = 18; + type Item = String; + + fn parse(option: &RawDhcpOption) -> Result { + let data = core::str::from_utf8(&option.data)?; + Ok(data.to_string()) + } +} diff --git a/src/options/host_name_option.rs b/src/options/host_name_option.rs new file mode 100644 index 0000000..1dd585d --- /dev/null +++ b/src/options/host_name_option.rs @@ -0,0 +1,15 @@ +use super::{RawDhcpOption, DhcpOptionError}; +use crate::options::DhcpOption; +use core::net::Ipv4Addr; + +pub struct HostNameOption; + +impl DhcpOption for HostNameOption { + const CODE: u8 = 12; + type Item = String; + + fn parse(option: &RawDhcpOption) -> Result { + let data = core::str::from_utf8(&option.data)?; + Ok(data.to_string()) + } +} diff --git a/src/options/impress_server_option.rs b/src/options/impress_server_option.rs new file mode 100644 index 0000000..1d49474 --- /dev/null +++ b/src/options/impress_server_option.rs @@ -0,0 +1,22 @@ +use super::{RawDhcpOption, DhcpOptionError}; +use crate::options::DhcpOption; +use core::net::Ipv4Addr; + +pub struct ImpressServerOption; + +impl DhcpOption for ImpressServerOption { + const CODE: u8 = 10; + type Item = Vec; + + fn parse(option: &RawDhcpOption) -> Result { + let rem = option.length % 4; + (rem == 0) + .then_some(()) + .ok_or(DhcpOptionError::InvalidLength(option.length, option.length + 4 - rem))?; + Ok(option + .data + .chunks(4) + .map(|c| Ipv4Addr::new(c[0], c[1], c[2], c[3])) + .collect()) + } +} diff --git a/src/options/log_server_option.rs b/src/options/log_server_option.rs new file mode 100644 index 0000000..b49ddb7 --- /dev/null +++ b/src/options/log_server_option.rs @@ -0,0 +1,22 @@ +use super::{RawDhcpOption, DhcpOptionError}; +use crate::options::DhcpOption; +use core::net::Ipv4Addr; + +pub struct LogServerOption; + +impl DhcpOption for LogServerOption { + const CODE: u8 = 7; + type Item = Vec; + + fn parse(option: &RawDhcpOption) -> Result { + let rem = option.length % 4; + (rem == 0) + .then_some(()) + .ok_or(DhcpOptionError::InvalidLength(option.length, option.length + 4 - rem))?; + Ok(option + .data + .chunks(4) + .map(|c| Ipv4Addr::new(c[0], c[1], c[2], c[3])) + .collect()) + } +} diff --git a/src/options/lpr_server_option.rs b/src/options/lpr_server_option.rs new file mode 100644 index 0000000..914e56c --- /dev/null +++ b/src/options/lpr_server_option.rs @@ -0,0 +1,22 @@ +use super::{RawDhcpOption, DhcpOptionError}; +use crate::options::DhcpOption; +use core::net::Ipv4Addr; + +pub struct LPRServerOption; + +impl DhcpOption for LPRServerOption { + const CODE: u8 = 9; + type Item = Vec; + + fn parse(option: &RawDhcpOption) -> Result { + let rem = option.length % 4; + (rem == 0) + .then_some(()) + .ok_or(DhcpOptionError::InvalidLength(option.length, option.length + 4 - rem))?; + Ok(option + .data + .chunks(4) + .map(|c| Ipv4Addr::new(c[0], c[1], c[2], c[3])) + .collect()) + } +} diff --git a/src/options/merit_dump_file.rs b/src/options/merit_dump_file.rs new file mode 100644 index 0000000..ec0dfd4 --- /dev/null +++ b/src/options/merit_dump_file.rs @@ -0,0 +1,15 @@ +use super::{RawDhcpOption, DhcpOptionError}; +use crate::options::DhcpOption; +use core::net::Ipv4Addr; + +pub struct MeritDumpFile; + +impl DhcpOption for MeritDumpFile { + const CODE: u8 = 14; + type Item = String; + + fn parse(option: &RawDhcpOption) -> Result { + let data = core::str::from_utf8(&option.data)?; + Ok(data.to_string()) + } +} diff --git a/src/options/mod.rs b/src/options/mod.rs new file mode 100644 index 0000000..9de9f87 --- /dev/null +++ b/src/options/mod.rs @@ -0,0 +1,74 @@ +mod boot_file_size_option; +mod cookie_server_option; +mod domain_name_server_option; +mod host_name_option; +mod impress_server_option; +mod log_server_option; +mod lpr_server_option; +mod name_server_option; +mod resource_location_server_option; +mod router_option; +mod subnet_mask; +mod time_offset; +mod time_server_option; +mod merit_dump_file; +mod domain_name; +mod swap_server; +mod root_path; +mod extensions_path; + +pub use boot_file_size_option::BootFileSizeOption; +pub use cookie_server_option::CookieServerOption; +pub use domain_name_server_option::DomainNameServerOption; +pub use impress_server_option::ImpressServerOption; +pub use log_server_option::LogServerOption; +pub use lpr_server_option::LPRServerOption; +pub use name_server_option::NameServerOption; +pub use resource_location_server_option::ResourceLocationServerOption; +pub use router_option::RouterOption; +pub use subnet_mask::SubnetMask; +pub use time_offset::TimeOffset; +pub use merit_dump_file::MeritDumpFile; +pub use domain_name::DomainName; +pub use swap_server::SwapServer; +pub use root_path::RootPath; +pub use extensions_path::ExtensionsPath; + +#[derive(Debug)] +pub struct RawDhcpOption<'a> { + pub code: u8, + pub length: u8, + pub data: &'a [u8], +} + +pub trait DhcpOption { + const CODE: u8; + type Item; + + fn parse(option: &RawDhcpOption) -> Result; +} + +#[derive(Debug)] +pub enum DhcpOptionError { + InvalidLength(u8, u8), + UTF8Error, +} + +impl core::fmt::Display for DhcpOptionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DhcpOptionError::InvalidLength(got, should) => { + write!(f, "Invalid length got {got}, should be {should}") + } + DhcpOptionError::UTF8Error => write!(f, "Error parsing option data as UTF-8"), + } + } +} + +impl core::error::Error for DhcpOptionError {} + +impl From for DhcpOptionError { + fn from(value: core::str::Utf8Error) -> Self { + DhcpOptionError::UTF8Error + } +} \ No newline at end of file diff --git a/src/options/name_server_option.rs b/src/options/name_server_option.rs new file mode 100644 index 0000000..ba6699d --- /dev/null +++ b/src/options/name_server_option.rs @@ -0,0 +1,22 @@ +use super::{RawDhcpOption, DhcpOptionError}; +use crate::options::DhcpOption; +use core::net::Ipv4Addr; + +pub struct NameServerOption; + +impl DhcpOption for NameServerOption { + const CODE: u8 = 5; + type Item = Vec; + + fn parse(option: &RawDhcpOption) -> Result { + let rem = option.length % 4; + (rem == 0) + .then_some(()) + .ok_or(DhcpOptionError::InvalidLength(option.length, option.length + 4 - rem))?; + Ok(option + .data + .chunks(4) + .map(|c| Ipv4Addr::new(c[0], c[1], c[2], c[3])) + .collect()) + } +} diff --git a/src/options/resource_location_server_option.rs b/src/options/resource_location_server_option.rs new file mode 100644 index 0000000..2ed71a3 --- /dev/null +++ b/src/options/resource_location_server_option.rs @@ -0,0 +1,22 @@ +use super::{RawDhcpOption, DhcpOptionError}; +use crate::options::DhcpOption; +use core::net::Ipv4Addr; + +pub struct ResourceLocationServerOption; + +impl DhcpOption for ResourceLocationServerOption { + const CODE: u8 = 11; + type Item = Vec; + + fn parse(option: &RawDhcpOption) -> Result { + let rem = option.length % 4; + (rem == 0) + .then_some(()) + .ok_or(DhcpOptionError::InvalidLength(option.length, option.length + 4 - rem))?; + Ok(option + .data + .chunks(4) + .map(|c| Ipv4Addr::new(c[0], c[1], c[2], c[3])) + .collect()) + } +} diff --git a/src/options/root_path.rs b/src/options/root_path.rs new file mode 100644 index 0000000..b8047fd --- /dev/null +++ b/src/options/root_path.rs @@ -0,0 +1,15 @@ +use super::{RawDhcpOption, DhcpOptionError}; +use crate::options::DhcpOption; +use core::net::Ipv4Addr; + +pub struct RootPath; + +impl DhcpOption for RootPath { + const CODE: u8 = 17; + type Item = String; + + fn parse(option: &RawDhcpOption) -> Result { + let data = core::str::from_utf8(&option.data)?; + Ok(data.to_string()) + } +} diff --git a/src/options/router_option.rs b/src/options/router_option.rs new file mode 100644 index 0000000..ea80258 --- /dev/null +++ b/src/options/router_option.rs @@ -0,0 +1,22 @@ +use super::{RawDhcpOption, DhcpOptionError}; +use crate::options::DhcpOption; +use core::net::Ipv4Addr; + +pub struct RouterOption; + +impl DhcpOption for RouterOption { + const CODE: u8 = 3; + type Item = Vec; + + fn parse(option: &RawDhcpOption) -> Result { + let rem = option.length % 4; + (rem == 0) + .then_some(()) + .ok_or(DhcpOptionError::InvalidLength(option.length, option.length + 4 - rem))?; + Ok(option + .data + .chunks(4) + .map(|c| Ipv4Addr::new(c[0], c[1], c[2], c[3])) + .collect()) + } +} diff --git a/src/options/subnet_mask.rs b/src/options/subnet_mask.rs new file mode 100644 index 0000000..8f81c69 --- /dev/null +++ b/src/options/subnet_mask.rs @@ -0,0 +1,22 @@ +use super::{DhcpOptionError, RawDhcpOption}; +use crate::options::DhcpOption; +use core::net::Ipv4Addr; + +pub struct SubnetMask; + +impl DhcpOption for SubnetMask { + const CODE: u8 = 1; + type Item = Ipv4Addr; + + fn parse(option: &RawDhcpOption) -> Result { + (option.length == 4) + .then_some(()) + .ok_or(DhcpOptionError::InvalidLength(option.length, 4))?; + Ok(Ipv4Addr::new( + option.data[0], + option.data[1], + option.data[2], + option.data[3], + )) + } +} diff --git a/src/options/swap_server.rs b/src/options/swap_server.rs new file mode 100644 index 0000000..7cefca6 --- /dev/null +++ b/src/options/swap_server.rs @@ -0,0 +1,22 @@ +use super::{DhcpOptionError, RawDhcpOption}; +use crate::options::DhcpOption; +use core::net::Ipv4Addr; + +pub struct SwapServer; + +impl DhcpOption for SwapServer { + const CODE: u8 = 16; + type Item = Ipv4Addr; + + fn parse(option: &RawDhcpOption) -> Result { + (option.length == 4) + .then_some(()) + .ok_or(DhcpOptionError::InvalidLength(option.length, 4))?; + Ok(Ipv4Addr::new( + option.data[0], + option.data[1], + option.data[2], + option.data[3], + )) + } +} diff --git a/src/options/time_offset.rs b/src/options/time_offset.rs new file mode 100644 index 0000000..7e921f0 --- /dev/null +++ b/src/options/time_offset.rs @@ -0,0 +1,20 @@ +use super::{RawDhcpOption, DhcpOptionError}; +use crate::options::DhcpOption; + +pub struct TimeOffset; + +impl DhcpOption for TimeOffset { + const CODE: u8 = 2; + type Item = u32; + + fn parse(option: &RawDhcpOption) -> Result { + (option.length == 4) + .then_some(()) + .ok_or(DhcpOptionError::InvalidLength(option.length, 4))?; + Ok(u32::from_be_bytes( + option.data[..4] + .try_into() + .expect("The parser pulled exactly 4 bytes"), + )) + } +} diff --git a/src/options/time_server_option.rs b/src/options/time_server_option.rs new file mode 100644 index 0000000..5a5a04d --- /dev/null +++ b/src/options/time_server_option.rs @@ -0,0 +1,22 @@ +use super::{RawDhcpOption, DhcpOptionError}; +use crate::options::DhcpOption; +use core::net::Ipv4Addr; + +pub struct TimeServerOption; + +impl DhcpOption for TimeServerOption { + const CODE: u8 = 4; + type Item = Vec; + + fn parse(option: &RawDhcpOption) -> Result { + let rem = option.length % 4; + (rem == 0) + .then_some(()) + .ok_or(DhcpOptionError::InvalidLength(option.length, option.length + 4 - rem))?; + Ok(option + .data + .chunks(4) + .map(|c| Ipv4Addr::new(c[0], c[1], c[2], c[3])) + .collect()) + } +}