use core::net::Ipv4Addr;
use core::str;
use crate::endian::{read_u16_be, read_u32_be};
use crate::error::{CrafterError, Result};
use super::constants::{
DHCPV4_AUTH_ALGORITHM_HMAC_MD5, DHCPV4_AUTH_HEADER_LEN,
DHCPV4_AUTH_PROTOCOL_CONFIGURATION_TOKEN, DHCPV4_AUTH_PROTOCOL_DELAYED,
DHCPV4_AUTH_PROTOCOL_RECONFIGURE_KEY, DHCPV4_AUTH_RDM_MONOTONIC_COUNTER,
DHCPV4_AUTH_REPLAY_DETECTION_LEN, DHCPV4_CLIENT_ID_TYPE_NONE, DHCPV4_CLIENT_ID_TYPE_RFC4361,
DHCPV4_CLIENT_MACHINE_UUID_TYPE, DHCPV4_CLIENT_NDI_TYPE_UNDI, DHCPV4_DATA_SOURCE_FLAG_REMOTE,
DHCPV4_HTYPE_ETHERNET, DHCPV4_IAID_LEN, DHCPV4_OPTION_ALL_SUBNETS_LOCAL,
DHCPV4_OPTION_ARP_CACHE_TIMEOUT, DHCPV4_OPTION_ASSOCIATED_IP, DHCPV4_OPTION_AUTHENTICATION,
DHCPV4_OPTION_BASE_TIME, DHCPV4_OPTION_BOOTFILE_NAME, DHCPV4_OPTION_BOOT_FILE_SIZE,
DHCPV4_OPTION_BROADCAST_ADDRESS, DHCPV4_OPTION_CAPTIVE_PORTAL,
DHCPV4_OPTION_CLASSLESS_STATIC_ROUTE, DHCPV4_OPTION_CLIENT_IDENTIFIER,
DHCPV4_OPTION_CLIENT_LAST_TRANSACTION_TIME, DHCPV4_OPTION_CLIENT_MACHINE_IDENTIFIER,
DHCPV4_OPTION_CLIENT_NDI, DHCPV4_OPTION_CLIENT_SYSTEM_ARCHITECTURE,
DHCPV4_OPTION_COOKIE_SERVER, DHCPV4_OPTION_DATA_SOURCE, DHCPV4_OPTION_DEFAULT_IP_TTL,
DHCPV4_OPTION_DHCP_STATE, DHCPV4_OPTION_DOMAIN_NAME, DHCPV4_OPTION_DOMAIN_NAME_SERVER,
DHCPV4_OPTION_DOMAIN_SEARCH, DHCPV4_OPTION_END, DHCPV4_OPTION_ETHERNET_ENCAPSULATION,
DHCPV4_OPTION_EXTENSIONS_PATH, DHCPV4_OPTION_FORCERENEW_NONCE_CAPABLE, DHCPV4_OPTION_HOST_NAME,
DHCPV4_OPTION_IMPRESS_SERVER, DHCPV4_OPTION_INTERFACE_MTU, DHCPV4_OPTION_IPV6_ONLY_PREFERRED,
DHCPV4_OPTION_IP_ADDRESS_LEASE_TIME, DHCPV4_OPTION_IP_FORWARDING, DHCPV4_OPTION_LOG_SERVER,
DHCPV4_OPTION_LPR_SERVER, DHCPV4_OPTION_MASK_SUPPLIER, DHCPV4_OPTION_MAX_DATAGRAM_REASSEMBLY,
DHCPV4_OPTION_MAX_MESSAGE_SIZE, DHCPV4_OPTION_MERIT_DUMP_FILE, DHCPV4_OPTION_MESSAGE,
DHCPV4_OPTION_MESSAGE_TYPE, DHCPV4_OPTION_MUD_URL_V4, DHCPV4_OPTION_NAME_SERVER,
DHCPV4_OPTION_NETBIOS_DATAGRAM_SERVER, DHCPV4_OPTION_NETBIOS_NAME_SERVER,
DHCPV4_OPTION_NETBIOS_NODE_TYPE, DHCPV4_OPTION_NETBIOS_SCOPE, DHCPV4_OPTION_NIS_DOMAIN,
DHCPV4_OPTION_NIS_SERVERS, DHCPV4_OPTION_NON_LOCAL_SOURCE_ROUTING, DHCPV4_OPTION_NTP_SERVERS,
DHCPV4_OPTION_OVERLOAD, DHCPV4_OPTION_PAD, DHCPV4_OPTION_PARAMETER_REQUEST_LIST,
DHCPV4_OPTION_PATH_MTU_AGING_TIMEOUT, DHCPV4_OPTION_PATH_MTU_PLATEAU_TABLE,
DHCPV4_OPTION_PERFORM_MASK_DISCOVERY, DHCPV4_OPTION_PERFORM_ROUTER_DISCOVERY,
DHCPV4_OPTION_POLICY_FILTER, DHCPV4_OPTION_PXELINUX_CONFIGFILE, DHCPV4_OPTION_PXELINUX_MAGIC,
DHCPV4_OPTION_PXELINUX_PATHPREFIX, DHCPV4_OPTION_PXELINUX_REBOOTTIME,
DHCPV4_OPTION_QUERY_END_TIME, DHCPV4_OPTION_QUERY_START_TIME, DHCPV4_OPTION_REBINDING_TIME,
DHCPV4_OPTION_RELAY_AGENT_INFORMATION, DHCPV4_OPTION_RENEWAL_TIME,
DHCPV4_OPTION_REQUESTED_IP_ADDRESS, DHCPV4_OPTION_RESOURCE_LOCATION_SERVER,
DHCPV4_OPTION_ROOT_PATH, DHCPV4_OPTION_ROUTER, DHCPV4_OPTION_ROUTER_SOLICITATION_ADDRESS,
DHCPV4_OPTION_SERVER_IDENTIFIER, DHCPV4_OPTION_SIP_SERVERS, DHCPV4_OPTION_START_TIME_OF_STATE,
DHCPV4_OPTION_STATIC_ROUTE, DHCPV4_OPTION_STATUS_CODE, DHCPV4_OPTION_SUBNET_MASK,
DHCPV4_OPTION_SWAP_SERVER, DHCPV4_OPTION_TCP_DEFAULT_TTL, DHCPV4_OPTION_TCP_KEEPALIVE_GARBAGE,
DHCPV4_OPTION_TCP_KEEPALIVE_INTERVAL, DHCPV4_OPTION_TFTP_SERVER_ADDRESS,
DHCPV4_OPTION_TFTP_SERVER_NAME, DHCPV4_OPTION_TIME_OFFSET, DHCPV4_OPTION_TIME_SERVER,
DHCPV4_OPTION_TRAILER_ENCAPSULATION, DHCPV4_OPTION_USER_CLASS,
DHCPV4_OPTION_VENDOR_CLASS_IDENTIFIER, DHCPV4_OPTION_VENDOR_SPECIFIC,
DHCPV4_OPTION_VI_VENDOR_CLASS, DHCPV4_OPTION_VI_VENDOR_SPECIFIC,
DHCPV4_OPTION_X_WINDOW_DISPLAY_MANAGER, DHCPV4_OPTION_X_WINDOW_FONT_SERVER,
DHCPV4_OVERLOAD_BOTH, DHCPV4_OVERLOAD_FILE, DHCPV4_OVERLOAD_SNAME, DHCPV4_PXELINUX_MAGIC_VALUE,
DHCPV4_RELAY_SUBOPTION_AUTHENTICATION, DHCPV4_RELAY_SUBOPTION_CIRCUIT_ID,
DHCPV4_RELAY_SUBOPTION_DOCSIS_DEVICE_CLASS, DHCPV4_RELAY_SUBOPTION_LINK_SELECTION,
DHCPV4_RELAY_SUBOPTION_RADIUS_ATTRIBUTES, DHCPV4_RELAY_SUBOPTION_RELAY_AGENT_ID,
DHCPV4_RELAY_SUBOPTION_RELAY_FLAGS, DHCPV4_RELAY_SUBOPTION_RELAY_SOURCE_PORT,
DHCPV4_RELAY_SUBOPTION_REMOTE_ID, DHCPV4_RELAY_SUBOPTION_SERVER_ID_OVERRIDE,
DHCPV4_RELAY_SUBOPTION_SUBSCRIBER_ID, DHCPV4_RELAY_SUBOPTION_VENDOR_SPECIFIC,
DHCPV4_RELAY_SUBOPTION_VSS, DHCPV4_RELAY_SUBOPTION_VSS_CONTROL, DHCPV4_STATE_ABANDONED,
DHCPV4_STATE_ACTIVE, DHCPV4_STATE_AVAILABLE, DHCPV4_STATE_EXPIRED, DHCPV4_STATE_RELEASED,
DHCPV4_STATE_REMOTE, DHCPV4_STATE_RESERVED, DHCPV4_STATE_RESET, DHCPV4_STATE_TRANSITIONING,
DHCPV4_STATUS_CATCH_UP_COMPLETE, DHCPV4_STATUS_CONNECTION_ACTIVE, DHCPV4_STATUS_DATA_MISSING,
DHCPV4_STATUS_MALFORMED_QUERY, DHCPV4_STATUS_NOT_ALLOWED, DHCPV4_STATUS_QUERY_TERMINATED,
DHCPV4_STATUS_SUCCESS, DHCPV4_STATUS_TLS_CONNECTION_REFUSED, DHCPV4_STATUS_UNSPEC_FAIL,
DHCPV4_VSS_TYPE_GLOBAL_DEFAULT, DHCPV4_VSS_TYPE_NVT_ASCII, DHCPV4_VSS_TYPE_VPN_ID,
};
use super::message::Dhcpv4MessageType;
use super::registry::{dhcpv4_option_name, dhcpv4_option_status, Dhcpv4OptionStatus};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Dhcpv4OptionArea {
Options,
File,
Sname,
}
impl Dhcpv4OptionArea {
pub const fn label(self) -> &'static str {
match self {
Self::Options => "options",
Self::File => "file",
Self::Sname => "sname",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OptionOverload {
File,
Sname,
Both,
Unknown(u8),
}
impl OptionOverload {
pub const fn from_code(code: u8) -> Self {
match code {
DHCPV4_OVERLOAD_FILE => Self::File,
DHCPV4_OVERLOAD_SNAME => Self::Sname,
DHCPV4_OVERLOAD_BOTH => Self::Both,
other => Self::Unknown(other),
}
}
pub const fn code(self) -> u8 {
match self {
Self::File => DHCPV4_OVERLOAD_FILE,
Self::Sname => DHCPV4_OVERLOAD_SNAME,
Self::Both => DHCPV4_OVERLOAD_BOTH,
Self::Unknown(code) => code,
}
}
pub const fn overloads_file(self) -> bool {
matches!(self, Self::File | Self::Both)
}
pub const fn overloads_sname(self) -> bool {
matches!(self, Self::Sname | Self::Both)
}
pub const fn overloads(self, area: Dhcpv4OptionArea) -> bool {
match area {
Dhcpv4OptionArea::Options => true,
Dhcpv4OptionArea::File => self.overloads_file(),
Dhcpv4OptionArea::Sname => self.overloads_sname(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Dhcpv4StaticRoute {
pub destination: Ipv4Addr,
pub router: Ipv4Addr,
}
impl Dhcpv4StaticRoute {
pub const fn new(destination: Ipv4Addr, router: Ipv4Addr) -> Self {
Self {
destination,
router,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Dhcpv4ClasslessRoute {
pub prefix_length: u8,
pub destination: Ipv4Addr,
pub router: Ipv4Addr,
}
impl Dhcpv4ClasslessRoute {
pub const fn new(prefix_length: u8, destination: Ipv4Addr, router: Ipv4Addr) -> Self {
Self {
prefix_length,
destination,
router,
}
}
pub const fn significant_octets(prefix_length: u8) -> usize {
(prefix_length as usize).div_ceil(8)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum SipServers {
DomainNames(Vec<String>),
Addresses(Vec<Ipv4Addr>),
Unknown {
encoding: u8,
data: Vec<u8>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Dhcpv4UserClass {
pub classes: Vec<Vec<u8>>,
}
impl Dhcpv4UserClass {
pub fn new(classes: impl Into<Vec<Vec<u8>>>) -> Self {
Self {
classes: classes.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ClientSystemArchitecture {
pub architectures: Vec<u16>,
}
impl ClientSystemArchitecture {
pub fn new(architectures: impl Into<Vec<u16>>) -> Self {
Self {
architectures: architectures.into(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ClientNetworkDeviceInterface {
pub interface_type: u8,
pub major: u8,
pub minor: u8,
}
impl ClientNetworkDeviceInterface {
pub const fn new(interface_type: u8, major: u8, minor: u8) -> Self {
Self {
interface_type,
major,
minor,
}
}
pub const fn undi(major: u8, minor: u8) -> Self {
Self::new(DHCPV4_CLIENT_NDI_TYPE_UNDI, major, minor)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Dhcpv4ClientIdentifier {
LegacyHardware {
hardware_type: u8,
address: Vec<u8>,
},
NodeSpecific {
iaid: u32,
duid: Vec<u8>,
},
Raw(Vec<u8>),
}
impl Dhcpv4ClientIdentifier {
pub fn legacy_hardware(hardware_type: u8, address: impl Into<Vec<u8>>) -> Self {
Self::LegacyHardware {
hardware_type,
address: address.into(),
}
}
pub fn ethernet_mac(mac: [u8; 6]) -> Self {
Self::LegacyHardware {
hardware_type: DHCPV4_HTYPE_ETHERNET,
address: mac.to_vec(),
}
}
pub fn node_specific(iaid: u32, duid: impl Into<Vec<u8>>) -> Self {
Self::NodeSpecific {
iaid,
duid: duid.into(),
}
}
pub fn raw(bytes: impl Into<Vec<u8>>) -> Self {
Self::Raw(bytes.into())
}
pub fn type_octet(&self) -> Option<u8> {
match self {
Self::LegacyHardware { hardware_type, .. } => Some(*hardware_type),
Self::NodeSpecific { .. } => Some(DHCPV4_CLIENT_ID_TYPE_RFC4361),
Self::Raw(bytes) => bytes.first().copied(),
}
}
pub fn encode(&self) -> Vec<u8> {
match self {
Self::LegacyHardware {
hardware_type,
address,
} => {
let mut bytes = Vec::with_capacity(1 + address.len());
bytes.push(*hardware_type);
bytes.extend_from_slice(address);
bytes
}
Self::NodeSpecific { iaid, duid } => {
let mut bytes = Vec::with_capacity(1 + DHCPV4_IAID_LEN + duid.len());
bytes.push(DHCPV4_CLIENT_ID_TYPE_RFC4361);
bytes.extend_from_slice(&iaid.to_be_bytes());
bytes.extend_from_slice(duid);
bytes
}
Self::Raw(bytes) => bytes.clone(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Dhcpv4AuthProtocol {
ConfigurationToken,
Delayed,
ReconfigureKey,
Unknown(u8),
}
impl Dhcpv4AuthProtocol {
pub const fn from_code(code: u8) -> Self {
match code {
DHCPV4_AUTH_PROTOCOL_CONFIGURATION_TOKEN => Self::ConfigurationToken,
DHCPV4_AUTH_PROTOCOL_DELAYED => Self::Delayed,
DHCPV4_AUTH_PROTOCOL_RECONFIGURE_KEY => Self::ReconfigureKey,
other => Self::Unknown(other),
}
}
pub const fn code(self) -> u8 {
match self {
Self::ConfigurationToken => DHCPV4_AUTH_PROTOCOL_CONFIGURATION_TOKEN,
Self::Delayed => DHCPV4_AUTH_PROTOCOL_DELAYED,
Self::ReconfigureKey => DHCPV4_AUTH_PROTOCOL_RECONFIGURE_KEY,
Self::Unknown(code) => code,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Dhcpv4AuthAlgorithm {
HmacMd5,
Unknown(u8),
}
impl Dhcpv4AuthAlgorithm {
pub const fn from_code(code: u8) -> Self {
match code {
DHCPV4_AUTH_ALGORITHM_HMAC_MD5 => Self::HmacMd5,
other => Self::Unknown(other),
}
}
pub const fn code(self) -> u8 {
match self {
Self::HmacMd5 => DHCPV4_AUTH_ALGORITHM_HMAC_MD5,
Self::Unknown(code) => code,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Dhcpv4ReplayDetectionMethod {
MonotonicCounter,
Unknown(u8),
}
impl Dhcpv4ReplayDetectionMethod {
pub const fn from_code(code: u8) -> Self {
match code {
DHCPV4_AUTH_RDM_MONOTONIC_COUNTER => Self::MonotonicCounter,
other => Self::Unknown(other),
}
}
pub const fn code(self) -> u8 {
match self {
Self::MonotonicCounter => DHCPV4_AUTH_RDM_MONOTONIC_COUNTER,
Self::Unknown(code) => code,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Dhcpv4Authentication {
pub protocol: Dhcpv4AuthProtocol,
pub algorithm: Dhcpv4AuthAlgorithm,
pub rdm: Dhcpv4ReplayDetectionMethod,
pub replay_detection: u64,
pub authentication_information: Vec<u8>,
}
impl Dhcpv4Authentication {
pub fn new(
protocol: Dhcpv4AuthProtocol,
algorithm: Dhcpv4AuthAlgorithm,
rdm: Dhcpv4ReplayDetectionMethod,
replay_detection: u64,
authentication_information: impl Into<Vec<u8>>,
) -> Self {
Self {
protocol,
algorithm,
rdm,
replay_detection,
authentication_information: authentication_information.into(),
}
}
pub fn encode(&self) -> Vec<u8> {
let mut bytes =
Vec::with_capacity(DHCPV4_AUTH_HEADER_LEN + self.authentication_information.len());
bytes.push(self.protocol.code());
bytes.push(self.algorithm.code());
bytes.push(self.rdm.code());
bytes.extend_from_slice(&self.replay_detection.to_be_bytes());
bytes.extend_from_slice(&self.authentication_information);
bytes
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub struct Dhcpv4ForcerenewNonceCapable {
pub algorithms: Vec<u8>,
}
impl Dhcpv4ForcerenewNonceCapable {
pub fn new(algorithms: impl Into<Vec<u8>>) -> Self {
Self {
algorithms: algorithms.into(),
}
}
pub fn hmac_md5() -> Self {
Self {
algorithms: vec![DHCPV4_AUTH_ALGORITHM_HMAC_MD5],
}
}
pub fn encode(&self) -> Vec<u8> {
self.algorithms.clone()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Dhcpv4StatusCode {
Success,
UnspecFail,
QueryTerminated,
MalformedQuery,
NotAllowed,
DataMissing,
ConnectionActive,
CatchUpComplete,
TlsConnectionRefused,
Unknown(u8),
}
impl Dhcpv4StatusCode {
pub const fn from_code(code: u8) -> Self {
match code {
DHCPV4_STATUS_SUCCESS => Self::Success,
DHCPV4_STATUS_UNSPEC_FAIL => Self::UnspecFail,
DHCPV4_STATUS_QUERY_TERMINATED => Self::QueryTerminated,
DHCPV4_STATUS_MALFORMED_QUERY => Self::MalformedQuery,
DHCPV4_STATUS_NOT_ALLOWED => Self::NotAllowed,
DHCPV4_STATUS_DATA_MISSING => Self::DataMissing,
DHCPV4_STATUS_CONNECTION_ACTIVE => Self::ConnectionActive,
DHCPV4_STATUS_CATCH_UP_COMPLETE => Self::CatchUpComplete,
DHCPV4_STATUS_TLS_CONNECTION_REFUSED => Self::TlsConnectionRefused,
other => Self::Unknown(other),
}
}
pub const fn code(self) -> u8 {
match self {
Self::Success => DHCPV4_STATUS_SUCCESS,
Self::UnspecFail => DHCPV4_STATUS_UNSPEC_FAIL,
Self::QueryTerminated => DHCPV4_STATUS_QUERY_TERMINATED,
Self::MalformedQuery => DHCPV4_STATUS_MALFORMED_QUERY,
Self::NotAllowed => DHCPV4_STATUS_NOT_ALLOWED,
Self::DataMissing => DHCPV4_STATUS_DATA_MISSING,
Self::ConnectionActive => DHCPV4_STATUS_CONNECTION_ACTIVE,
Self::CatchUpComplete => DHCPV4_STATUS_CATCH_UP_COMPLETE,
Self::TlsConnectionRefused => DHCPV4_STATUS_TLS_CONNECTION_REFUSED,
Self::Unknown(code) => code,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Dhcpv4StatusCodeOption {
pub status: Dhcpv4StatusCode,
pub message: Vec<u8>,
}
impl Dhcpv4StatusCodeOption {
pub fn new(status: Dhcpv4StatusCode, message: impl Into<Vec<u8>>) -> Self {
Self {
status,
message: message.into(),
}
}
pub fn message_lossy(&self) -> String {
String::from_utf8_lossy(&self.message).into_owned()
}
pub fn encode(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(1 + self.message.len());
bytes.push(self.status.code());
bytes.extend_from_slice(&self.message);
bytes
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Dhcpv4State {
Reserved,
Available,
Active,
Expired,
Released,
Abandoned,
Reset,
Remote,
Transitioning,
Unknown(u8),
}
impl Dhcpv4State {
pub const fn from_code(code: u8) -> Self {
match code {
DHCPV4_STATE_RESERVED => Self::Reserved,
DHCPV4_STATE_AVAILABLE => Self::Available,
DHCPV4_STATE_ACTIVE => Self::Active,
DHCPV4_STATE_EXPIRED => Self::Expired,
DHCPV4_STATE_RELEASED => Self::Released,
DHCPV4_STATE_ABANDONED => Self::Abandoned,
DHCPV4_STATE_RESET => Self::Reset,
DHCPV4_STATE_REMOTE => Self::Remote,
DHCPV4_STATE_TRANSITIONING => Self::Transitioning,
other => Self::Unknown(other),
}
}
pub const fn code(self) -> u8 {
match self {
Self::Reserved => DHCPV4_STATE_RESERVED,
Self::Available => DHCPV4_STATE_AVAILABLE,
Self::Active => DHCPV4_STATE_ACTIVE,
Self::Expired => DHCPV4_STATE_EXPIRED,
Self::Released => DHCPV4_STATE_RELEASED,
Self::Abandoned => DHCPV4_STATE_ABANDONED,
Self::Reset => DHCPV4_STATE_RESET,
Self::Remote => DHCPV4_STATE_REMOTE,
Self::Transitioning => DHCPV4_STATE_TRANSITIONING,
Self::Unknown(code) => code,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Dhcpv4DataSource {
pub flags: u8,
}
impl Dhcpv4DataSource {
pub const fn new(flags: u8) -> Self {
Self { flags }
}
pub const fn from_remote(remote: bool) -> Self {
Self {
flags: if remote {
DHCPV4_DATA_SOURCE_FLAG_REMOTE
} else {
0
},
}
}
pub const fn is_remote(self) -> bool {
self.flags & DHCPV4_DATA_SOURCE_FLAG_REMOTE != 0
}
pub fn encode(self) -> Vec<u8> {
vec![self.flags]
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Dhcpv4ClientUuid {
pub identifier_type: u8,
pub identifier: Vec<u8>,
}
impl Dhcpv4ClientUuid {
pub fn new(identifier_type: u8, identifier: impl Into<Vec<u8>>) -> Self {
Self {
identifier_type,
identifier: identifier.into(),
}
}
pub fn guid(guid: impl Into<Vec<u8>>) -> Self {
Self::new(DHCPV4_CLIENT_MACHINE_UUID_TYPE, guid)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Dhcpv4VendorClassData {
pub enterprise_number: u32,
pub data: Vec<u8>,
}
impl Dhcpv4VendorClassData {
pub fn new(enterprise_number: u32, data: impl Into<Vec<u8>>) -> Self {
Self {
enterprise_number,
data: data.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Dhcpv4VendorSuboption {
pub code: u8,
pub data: Vec<u8>,
}
impl Dhcpv4VendorSuboption {
pub fn new(code: u8, data: impl Into<Vec<u8>>) -> Self {
Self {
code,
data: data.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Dhcpv4VendorIdentifyingOption {
pub enterprise_number: u32,
pub suboptions: Vec<Dhcpv4VendorSuboption>,
}
impl Dhcpv4VendorIdentifyingOption {
pub fn new(enterprise_number: u32, suboptions: impl Into<Vec<Dhcpv4VendorSuboption>>) -> Self {
Self {
enterprise_number,
suboptions: suboptions.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Dhcpv4RelayVendorSpecific {
pub enterprise_number: u32,
pub data: Vec<u8>,
}
impl Dhcpv4RelayVendorSpecific {
pub fn new(enterprise_number: u32, data: impl Into<Vec<u8>>) -> Self {
Self {
enterprise_number,
data: data.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Dhcpv4VssInfo {
pub vss_type: u8,
pub information: Vec<u8>,
}
impl Dhcpv4VssInfo {
pub fn new(vss_type: u8, information: impl Into<Vec<u8>>) -> Self {
Self {
vss_type,
information: information.into(),
}
}
pub fn nvt_ascii(identifier: impl Into<Vec<u8>>) -> Self {
Self::new(DHCPV4_VSS_TYPE_NVT_ASCII, identifier)
}
pub fn vpn_id(vpn_id: impl Into<Vec<u8>>) -> Self {
Self::new(DHCPV4_VSS_TYPE_VPN_ID, vpn_id)
}
pub fn global_default() -> Self {
Self::new(DHCPV4_VSS_TYPE_GLOBAL_DEFAULT, Vec::new())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Dhcpv4RelaySuboption {
CircuitId(Vec<u8>),
RemoteId(Vec<u8>),
DocsisDeviceClass(u32),
LinkSelection(Ipv4Addr),
SubscriberId(Vec<u8>),
RadiusAttributes(Vec<u8>),
Authentication(Vec<u8>),
VendorSpecific(Vec<Dhcpv4RelayVendorSpecific>),
RelayFlags(u8),
ServerIdOverride(Ipv4Addr),
RelayAgentId(Vec<u8>),
RelaySourcePort,
Vss(Dhcpv4VssInfo),
VssControl,
Other {
code: u8,
data: Vec<u8>,
},
}
impl Dhcpv4RelaySuboption {
pub fn circuit_id(data: impl Into<Vec<u8>>) -> Self {
Self::CircuitId(data.into())
}
pub fn remote_id(data: impl Into<Vec<u8>>) -> Self {
Self::RemoteId(data.into())
}
pub fn subscriber_id(data: impl Into<Vec<u8>>) -> Self {
Self::SubscriberId(data.into())
}
pub fn relay_agent_id(data: impl Into<Vec<u8>>) -> Self {
Self::RelayAgentId(data.into())
}
pub fn other(code: u8, data: impl Into<Vec<u8>>) -> Self {
Self::Other {
code,
data: data.into(),
}
}
pub const fn code(&self) -> u8 {
match self {
Self::CircuitId(_) => DHCPV4_RELAY_SUBOPTION_CIRCUIT_ID,
Self::RemoteId(_) => DHCPV4_RELAY_SUBOPTION_REMOTE_ID,
Self::DocsisDeviceClass(_) => DHCPV4_RELAY_SUBOPTION_DOCSIS_DEVICE_CLASS,
Self::LinkSelection(_) => DHCPV4_RELAY_SUBOPTION_LINK_SELECTION,
Self::SubscriberId(_) => DHCPV4_RELAY_SUBOPTION_SUBSCRIBER_ID,
Self::RadiusAttributes(_) => DHCPV4_RELAY_SUBOPTION_RADIUS_ATTRIBUTES,
Self::Authentication(_) => DHCPV4_RELAY_SUBOPTION_AUTHENTICATION,
Self::VendorSpecific(_) => DHCPV4_RELAY_SUBOPTION_VENDOR_SPECIFIC,
Self::RelayFlags(_) => DHCPV4_RELAY_SUBOPTION_RELAY_FLAGS,
Self::ServerIdOverride(_) => DHCPV4_RELAY_SUBOPTION_SERVER_ID_OVERRIDE,
Self::RelayAgentId(_) => DHCPV4_RELAY_SUBOPTION_RELAY_AGENT_ID,
Self::RelaySourcePort => DHCPV4_RELAY_SUBOPTION_RELAY_SOURCE_PORT,
Self::Vss(_) => DHCPV4_RELAY_SUBOPTION_VSS,
Self::VssControl => DHCPV4_RELAY_SUBOPTION_VSS_CONTROL,
Self::Other { code, .. } => *code,
}
}
fn encode_value(&self) -> Vec<u8> {
match self {
Self::CircuitId(data)
| Self::RemoteId(data)
| Self::SubscriberId(data)
| Self::RadiusAttributes(data)
| Self::Authentication(data)
| Self::RelayAgentId(data)
| Self::Other { data, .. } => data.clone(),
Self::DocsisDeviceClass(value) => value.to_be_bytes().to_vec(),
Self::LinkSelection(address) | Self::ServerIdOverride(address) => {
address.octets().to_vec()
}
Self::VendorSpecific(tuples) => encode_relay_vendor_specific(tuples),
Self::RelayFlags(flags) => vec![*flags],
Self::RelaySourcePort | Self::VssControl => Vec::new(),
Self::Vss(vss) => {
let mut bytes = Vec::with_capacity(1 + vss.information.len());
bytes.push(vss.vss_type);
bytes.extend_from_slice(&vss.information);
bytes
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub struct Dhcpv4RelayAgentInfo {
pub suboptions: Vec<Dhcpv4RelaySuboption>,
}
impl Dhcpv4RelayAgentInfo {
pub fn new(suboptions: impl Into<Vec<Dhcpv4RelaySuboption>>) -> Self {
Self {
suboptions: suboptions.into(),
}
}
pub fn with(mut self, suboption: Dhcpv4RelaySuboption) -> Self {
self.suboptions.push(suboption);
self
}
pub fn suboption(&self, code: u8) -> Option<&Dhcpv4RelaySuboption> {
self.suboptions.iter().find(|sub| sub.code() == code)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Dhcpv4OptionCode {
Pad,
End,
Assigned(u8),
Ambiguous(u8),
PrivateUse(u8),
RemovedOrUnassigned(u8),
}
impl Dhcpv4OptionCode {
pub const fn from_code(code: u8) -> Self {
match code {
DHCPV4_OPTION_PAD => Self::Pad,
DHCPV4_OPTION_END => Self::End,
_ => match dhcpv4_option_status(code) {
Dhcpv4OptionStatus::Assigned => Self::Assigned(code),
Dhcpv4OptionStatus::Ambiguous => Self::Ambiguous(code),
Dhcpv4OptionStatus::PrivateUse => Self::PrivateUse(code),
Dhcpv4OptionStatus::RemovedOrUnassigned | Dhcpv4OptionStatus::Unknown => {
Self::RemovedOrUnassigned(code)
}
},
}
}
pub const fn code(self) -> u8 {
match self {
Self::Pad => DHCPV4_OPTION_PAD,
Self::End => DHCPV4_OPTION_END,
Self::Assigned(code)
| Self::Ambiguous(code)
| Self::PrivateUse(code)
| Self::RemovedOrUnassigned(code) => code,
}
}
pub fn name(self) -> Option<&'static str> {
dhcpv4_option_name(self.code())
}
pub const fn is_single_octet(self) -> bool {
matches!(self, Self::Pad | Self::End)
}
}
impl From<u8> for Dhcpv4OptionCode {
fn from(code: u8) -> Self {
Self::from_code(code)
}
}
impl From<Dhcpv4OptionCode> for u8 {
fn from(code: Dhcpv4OptionCode) -> Self {
code.code()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Dhcpv4OptionValue {
Empty,
U8(u8),
U16(u16),
U32(u32),
I32(i32),
Bool(bool),
Ipv4(Ipv4Addr),
Ipv4List(Vec<Ipv4Addr>),
Ipv4Pairs(Vec<(Ipv4Addr, Ipv4Addr)>),
U16List(Vec<u16>),
Text(Vec<u8>),
MessageType(Dhcpv4MessageType),
OptionOverload(OptionOverload),
ParameterRequestList(Vec<u8>),
StaticRoutes(Vec<Dhcpv4StaticRoute>),
ClasslessRoutes(Vec<Dhcpv4ClasslessRoute>),
DomainSearch(Vec<String>),
SipServers(SipServers),
UserClass(Dhcpv4UserClass),
ClientSystemArchitecture(ClientSystemArchitecture),
ClientNetworkDeviceInterface(ClientNetworkDeviceInterface),
ClientUuid(Dhcpv4ClientUuid),
ViVendorClass(Vec<Dhcpv4VendorClassData>),
ViVendorSpecific(Vec<Dhcpv4VendorIdentifyingOption>),
RelayAgentInformation(Dhcpv4RelayAgentInfo),
ClientIdentifier(Dhcpv4ClientIdentifier),
Authentication(Dhcpv4Authentication),
ForcerenewNonceCapable(Dhcpv4ForcerenewNonceCapable),
StatusCode(Dhcpv4StatusCodeOption),
Dhcpv4State(Dhcpv4State),
DataSource(Dhcpv4DataSource),
Opaque(Vec<u8>),
}
impl Dhcpv4OptionValue {
pub fn as_bytes(&self) -> Option<&[u8]> {
match self {
Self::Text(bytes) | Self::ParameterRequestList(bytes) | Self::Opaque(bytes) => {
Some(bytes)
}
_ => None,
}
}
pub fn as_text_lossy(&self) -> Option<String> {
match self {
Self::Text(bytes) => Some(String::from_utf8_lossy(bytes).into_owned()),
_ => None,
}
}
pub fn encode_payload(&self) -> Vec<u8> {
match self {
Self::Empty => Vec::new(),
Self::U8(value) => vec![*value],
Self::U16(value) => value.to_be_bytes().to_vec(),
Self::U32(value) => value.to_be_bytes().to_vec(),
Self::I32(value) => value.to_be_bytes().to_vec(),
Self::Bool(value) => vec![u8::from(*value)],
Self::Ipv4(address) => address.octets().to_vec(),
Self::Ipv4List(addresses) => encode_ipv4_list(addresses),
Self::Ipv4Pairs(pairs) => {
let mut bytes = Vec::with_capacity(pairs.len() * 8);
for (first, second) in pairs {
bytes.extend_from_slice(&first.octets());
bytes.extend_from_slice(&second.octets());
}
bytes
}
Self::U16List(values) => {
let mut bytes = Vec::with_capacity(values.len() * 2);
for value in values {
bytes.extend_from_slice(&value.to_be_bytes());
}
bytes
}
Self::MessageType(message_type) => vec![message_type.code()],
Self::OptionOverload(overload) => vec![overload.code()],
Self::StaticRoutes(routes) => encode_static_routes(routes),
Self::ClasslessRoutes(routes) => encode_classless_routes(routes),
Self::DomainSearch(names) => encode_domain_name_list(names),
Self::SipServers(servers) => encode_sip_servers(servers),
Self::UserClass(user_class) => encode_user_class(user_class),
Self::ClientSystemArchitecture(arch) => encode_client_system_architecture(arch),
Self::ClientNetworkDeviceInterface(ndi) => {
vec![ndi.interface_type, ndi.major, ndi.minor]
}
Self::ClientUuid(uuid) => encode_client_uuid(uuid),
Self::ViVendorClass(instances) => encode_vi_vendor_class(instances),
Self::ViVendorSpecific(instances) => encode_vi_vendor_specific(instances),
Self::RelayAgentInformation(info) => encode_relay_agent_information(info),
Self::ClientIdentifier(identifier) => identifier.encode(),
Self::Authentication(auth) => auth.encode(),
Self::ForcerenewNonceCapable(value) => value.encode(),
Self::StatusCode(status) => status.encode(),
Self::Dhcpv4State(state) => vec![state.code()],
Self::DataSource(source) => source.encode(),
Self::Text(bytes) | Self::ParameterRequestList(bytes) | Self::Opaque(bytes) => {
bytes.clone()
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Dhcpv4OptionFormat {
Ipv4,
Ipv4List,
Ipv4Pairs,
Bool,
U8,
U16,
U16List,
I32,
U32,
Text,
ParameterRequestList,
MessageType,
OptionOverload,
StaticRoutes,
ClasslessRoutes,
DomainSearch,
SipServers,
UserClass,
ClientSystemArchitecture,
ClientNetworkDeviceInterface,
ClientUuid,
ViVendorClass,
ViVendorSpecific,
RelayAgentInformation,
ClientIdentifier,
Authentication,
ForcerenewNonceCapable,
StatusCode,
Dhcpv4State,
DataSource,
Opaque,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[allow(missing_docs)]
pub enum Dhcpv4OptionKind {
SubnetMask,
TimeOffset,
Router,
TimeServer,
NameServer,
DomainNameServer,
LogServer,
CookieServer,
LprServer,
ImpressServer,
ResourceLocationServer,
HostName,
BootFileSize,
MeritDumpFile,
DomainName,
SwapServer,
RootPath,
ExtensionsPath,
IpForwarding,
NonLocalSourceRouting,
PolicyFilter,
MaxDatagramReassembly,
DefaultIpTtl,
PathMtuAgingTimeout,
PathMtuPlateauTable,
InterfaceMtu,
AllSubnetsLocal,
BroadcastAddress,
PerformMaskDiscovery,
MaskSupplier,
PerformRouterDiscovery,
RouterSolicitationAddress,
StaticRoute,
TrailerEncapsulation,
ArpCacheTimeout,
EthernetEncapsulation,
TcpDefaultTtl,
TcpKeepaliveInterval,
TcpKeepaliveGarbage,
NisDomain,
NisServers,
NtpServers,
VendorSpecificInformation,
NetbiosNameServer,
NetbiosDatagramServer,
NetbiosNodeType,
NetbiosScope,
XWindowFontServer,
XWindowDisplayManager,
RequestedIpAddress,
IpAddressLeaseTime,
OptionOverload,
Dhcpv4MessageType,
ServerIdentifier,
ParameterRequestList,
Dhcpv4Message,
MaximumDhcpv4MessageSize,
RenewalTime,
RebindingTime,
VendorClassIdentifier,
ClientIdentifier,
DomainSearch,
SipServers,
ClasslessStaticRoute,
TftpServerName,
BootfileName,
UserClass,
ClientSystemArchitecture,
ClientNetworkDeviceInterface,
ClientMachineIdentifier,
ViVendorClass,
ViVendorSpecificInformation,
RelayAgentInformation,
Authentication,
ForcerenewNonceCapable,
ClientLastTransactionTime,
AssociatedIp,
StatusCode,
BaseTime,
StartTimeOfState,
QueryStartTime,
QueryEndTime,
Dhcpv4State,
DataSource,
PxelinuxMagic,
PxelinuxConfigFile,
PxelinuxPathPrefix,
PxelinuxRebootTime,
Ipv6OnlyPreferred,
CaptivePortal,
MudUrl,
}
impl Dhcpv4OptionKind {
pub const fn from_code(code: u8) -> Option<Self> {
let kind = match code {
DHCPV4_OPTION_SUBNET_MASK => Self::SubnetMask,
DHCPV4_OPTION_TIME_OFFSET => Self::TimeOffset,
DHCPV4_OPTION_ROUTER => Self::Router,
DHCPV4_OPTION_TIME_SERVER => Self::TimeServer,
DHCPV4_OPTION_NAME_SERVER => Self::NameServer,
DHCPV4_OPTION_DOMAIN_NAME_SERVER => Self::DomainNameServer,
DHCPV4_OPTION_LOG_SERVER => Self::LogServer,
DHCPV4_OPTION_COOKIE_SERVER => Self::CookieServer,
DHCPV4_OPTION_LPR_SERVER => Self::LprServer,
DHCPV4_OPTION_IMPRESS_SERVER => Self::ImpressServer,
DHCPV4_OPTION_RESOURCE_LOCATION_SERVER => Self::ResourceLocationServer,
DHCPV4_OPTION_HOST_NAME => Self::HostName,
DHCPV4_OPTION_BOOT_FILE_SIZE => Self::BootFileSize,
DHCPV4_OPTION_MERIT_DUMP_FILE => Self::MeritDumpFile,
DHCPV4_OPTION_DOMAIN_NAME => Self::DomainName,
DHCPV4_OPTION_SWAP_SERVER => Self::SwapServer,
DHCPV4_OPTION_ROOT_PATH => Self::RootPath,
DHCPV4_OPTION_EXTENSIONS_PATH => Self::ExtensionsPath,
DHCPV4_OPTION_IP_FORWARDING => Self::IpForwarding,
DHCPV4_OPTION_NON_LOCAL_SOURCE_ROUTING => Self::NonLocalSourceRouting,
DHCPV4_OPTION_POLICY_FILTER => Self::PolicyFilter,
DHCPV4_OPTION_MAX_DATAGRAM_REASSEMBLY => Self::MaxDatagramReassembly,
DHCPV4_OPTION_DEFAULT_IP_TTL => Self::DefaultIpTtl,
DHCPV4_OPTION_PATH_MTU_AGING_TIMEOUT => Self::PathMtuAgingTimeout,
DHCPV4_OPTION_PATH_MTU_PLATEAU_TABLE => Self::PathMtuPlateauTable,
DHCPV4_OPTION_INTERFACE_MTU => Self::InterfaceMtu,
DHCPV4_OPTION_ALL_SUBNETS_LOCAL => Self::AllSubnetsLocal,
DHCPV4_OPTION_BROADCAST_ADDRESS => Self::BroadcastAddress,
DHCPV4_OPTION_PERFORM_MASK_DISCOVERY => Self::PerformMaskDiscovery,
DHCPV4_OPTION_MASK_SUPPLIER => Self::MaskSupplier,
DHCPV4_OPTION_PERFORM_ROUTER_DISCOVERY => Self::PerformRouterDiscovery,
DHCPV4_OPTION_ROUTER_SOLICITATION_ADDRESS => Self::RouterSolicitationAddress,
DHCPV4_OPTION_STATIC_ROUTE => Self::StaticRoute,
DHCPV4_OPTION_TRAILER_ENCAPSULATION => Self::TrailerEncapsulation,
DHCPV4_OPTION_ARP_CACHE_TIMEOUT => Self::ArpCacheTimeout,
DHCPV4_OPTION_ETHERNET_ENCAPSULATION => Self::EthernetEncapsulation,
DHCPV4_OPTION_TCP_DEFAULT_TTL => Self::TcpDefaultTtl,
DHCPV4_OPTION_TCP_KEEPALIVE_INTERVAL => Self::TcpKeepaliveInterval,
DHCPV4_OPTION_TCP_KEEPALIVE_GARBAGE => Self::TcpKeepaliveGarbage,
DHCPV4_OPTION_NIS_DOMAIN => Self::NisDomain,
DHCPV4_OPTION_NIS_SERVERS => Self::NisServers,
DHCPV4_OPTION_NTP_SERVERS => Self::NtpServers,
DHCPV4_OPTION_VENDOR_SPECIFIC => Self::VendorSpecificInformation,
DHCPV4_OPTION_NETBIOS_NAME_SERVER => Self::NetbiosNameServer,
DHCPV4_OPTION_NETBIOS_DATAGRAM_SERVER => Self::NetbiosDatagramServer,
DHCPV4_OPTION_NETBIOS_NODE_TYPE => Self::NetbiosNodeType,
DHCPV4_OPTION_NETBIOS_SCOPE => Self::NetbiosScope,
DHCPV4_OPTION_X_WINDOW_FONT_SERVER => Self::XWindowFontServer,
DHCPV4_OPTION_X_WINDOW_DISPLAY_MANAGER => Self::XWindowDisplayManager,
DHCPV4_OPTION_REQUESTED_IP_ADDRESS => Self::RequestedIpAddress,
DHCPV4_OPTION_IP_ADDRESS_LEASE_TIME => Self::IpAddressLeaseTime,
DHCPV4_OPTION_OVERLOAD => Self::OptionOverload,
DHCPV4_OPTION_MESSAGE_TYPE => Self::Dhcpv4MessageType,
DHCPV4_OPTION_SERVER_IDENTIFIER => Self::ServerIdentifier,
DHCPV4_OPTION_PARAMETER_REQUEST_LIST => Self::ParameterRequestList,
DHCPV4_OPTION_MESSAGE => Self::Dhcpv4Message,
DHCPV4_OPTION_MAX_MESSAGE_SIZE => Self::MaximumDhcpv4MessageSize,
DHCPV4_OPTION_RENEWAL_TIME => Self::RenewalTime,
DHCPV4_OPTION_REBINDING_TIME => Self::RebindingTime,
DHCPV4_OPTION_VENDOR_CLASS_IDENTIFIER => Self::VendorClassIdentifier,
DHCPV4_OPTION_CLIENT_IDENTIFIER => Self::ClientIdentifier,
DHCPV4_OPTION_DOMAIN_SEARCH => Self::DomainSearch,
DHCPV4_OPTION_SIP_SERVERS => Self::SipServers,
DHCPV4_OPTION_CLASSLESS_STATIC_ROUTE => Self::ClasslessStaticRoute,
DHCPV4_OPTION_TFTP_SERVER_NAME => Self::TftpServerName,
DHCPV4_OPTION_BOOTFILE_NAME => Self::BootfileName,
DHCPV4_OPTION_USER_CLASS => Self::UserClass,
DHCPV4_OPTION_CLIENT_SYSTEM_ARCHITECTURE => Self::ClientSystemArchitecture,
DHCPV4_OPTION_CLIENT_NDI => Self::ClientNetworkDeviceInterface,
DHCPV4_OPTION_CLIENT_MACHINE_IDENTIFIER => Self::ClientMachineIdentifier,
DHCPV4_OPTION_VI_VENDOR_CLASS => Self::ViVendorClass,
DHCPV4_OPTION_VI_VENDOR_SPECIFIC => Self::ViVendorSpecificInformation,
DHCPV4_OPTION_RELAY_AGENT_INFORMATION => Self::RelayAgentInformation,
DHCPV4_OPTION_AUTHENTICATION => Self::Authentication,
DHCPV4_OPTION_FORCERENEW_NONCE_CAPABLE => Self::ForcerenewNonceCapable,
DHCPV4_OPTION_CLIENT_LAST_TRANSACTION_TIME => Self::ClientLastTransactionTime,
DHCPV4_OPTION_ASSOCIATED_IP => Self::AssociatedIp,
DHCPV4_OPTION_STATUS_CODE => Self::StatusCode,
DHCPV4_OPTION_BASE_TIME => Self::BaseTime,
DHCPV4_OPTION_START_TIME_OF_STATE => Self::StartTimeOfState,
DHCPV4_OPTION_QUERY_START_TIME => Self::QueryStartTime,
DHCPV4_OPTION_QUERY_END_TIME => Self::QueryEndTime,
DHCPV4_OPTION_DHCP_STATE => Self::Dhcpv4State,
DHCPV4_OPTION_DATA_SOURCE => Self::DataSource,
DHCPV4_OPTION_PXELINUX_MAGIC => Self::PxelinuxMagic,
DHCPV4_OPTION_PXELINUX_CONFIGFILE => Self::PxelinuxConfigFile,
DHCPV4_OPTION_PXELINUX_PATHPREFIX => Self::PxelinuxPathPrefix,
DHCPV4_OPTION_PXELINUX_REBOOTTIME => Self::PxelinuxRebootTime,
DHCPV4_OPTION_IPV6_ONLY_PREFERRED => Self::Ipv6OnlyPreferred,
DHCPV4_OPTION_CAPTIVE_PORTAL => Self::CaptivePortal,
DHCPV4_OPTION_MUD_URL_V4 => Self::MudUrl,
_ => return None,
};
Some(kind)
}
pub const fn code(self) -> u8 {
match self {
Self::SubnetMask => DHCPV4_OPTION_SUBNET_MASK,
Self::TimeOffset => DHCPV4_OPTION_TIME_OFFSET,
Self::Router => DHCPV4_OPTION_ROUTER,
Self::TimeServer => DHCPV4_OPTION_TIME_SERVER,
Self::NameServer => DHCPV4_OPTION_NAME_SERVER,
Self::DomainNameServer => DHCPV4_OPTION_DOMAIN_NAME_SERVER,
Self::LogServer => DHCPV4_OPTION_LOG_SERVER,
Self::CookieServer => DHCPV4_OPTION_COOKIE_SERVER,
Self::LprServer => DHCPV4_OPTION_LPR_SERVER,
Self::ImpressServer => DHCPV4_OPTION_IMPRESS_SERVER,
Self::ResourceLocationServer => DHCPV4_OPTION_RESOURCE_LOCATION_SERVER,
Self::HostName => DHCPV4_OPTION_HOST_NAME,
Self::BootFileSize => DHCPV4_OPTION_BOOT_FILE_SIZE,
Self::MeritDumpFile => DHCPV4_OPTION_MERIT_DUMP_FILE,
Self::DomainName => DHCPV4_OPTION_DOMAIN_NAME,
Self::SwapServer => DHCPV4_OPTION_SWAP_SERVER,
Self::RootPath => DHCPV4_OPTION_ROOT_PATH,
Self::ExtensionsPath => DHCPV4_OPTION_EXTENSIONS_PATH,
Self::IpForwarding => DHCPV4_OPTION_IP_FORWARDING,
Self::NonLocalSourceRouting => DHCPV4_OPTION_NON_LOCAL_SOURCE_ROUTING,
Self::PolicyFilter => DHCPV4_OPTION_POLICY_FILTER,
Self::MaxDatagramReassembly => DHCPV4_OPTION_MAX_DATAGRAM_REASSEMBLY,
Self::DefaultIpTtl => DHCPV4_OPTION_DEFAULT_IP_TTL,
Self::PathMtuAgingTimeout => DHCPV4_OPTION_PATH_MTU_AGING_TIMEOUT,
Self::PathMtuPlateauTable => DHCPV4_OPTION_PATH_MTU_PLATEAU_TABLE,
Self::InterfaceMtu => DHCPV4_OPTION_INTERFACE_MTU,
Self::AllSubnetsLocal => DHCPV4_OPTION_ALL_SUBNETS_LOCAL,
Self::BroadcastAddress => DHCPV4_OPTION_BROADCAST_ADDRESS,
Self::PerformMaskDiscovery => DHCPV4_OPTION_PERFORM_MASK_DISCOVERY,
Self::MaskSupplier => DHCPV4_OPTION_MASK_SUPPLIER,
Self::PerformRouterDiscovery => DHCPV4_OPTION_PERFORM_ROUTER_DISCOVERY,
Self::RouterSolicitationAddress => DHCPV4_OPTION_ROUTER_SOLICITATION_ADDRESS,
Self::StaticRoute => DHCPV4_OPTION_STATIC_ROUTE,
Self::TrailerEncapsulation => DHCPV4_OPTION_TRAILER_ENCAPSULATION,
Self::ArpCacheTimeout => DHCPV4_OPTION_ARP_CACHE_TIMEOUT,
Self::EthernetEncapsulation => DHCPV4_OPTION_ETHERNET_ENCAPSULATION,
Self::TcpDefaultTtl => DHCPV4_OPTION_TCP_DEFAULT_TTL,
Self::TcpKeepaliveInterval => DHCPV4_OPTION_TCP_KEEPALIVE_INTERVAL,
Self::TcpKeepaliveGarbage => DHCPV4_OPTION_TCP_KEEPALIVE_GARBAGE,
Self::NisDomain => DHCPV4_OPTION_NIS_DOMAIN,
Self::NisServers => DHCPV4_OPTION_NIS_SERVERS,
Self::NtpServers => DHCPV4_OPTION_NTP_SERVERS,
Self::VendorSpecificInformation => DHCPV4_OPTION_VENDOR_SPECIFIC,
Self::NetbiosNameServer => DHCPV4_OPTION_NETBIOS_NAME_SERVER,
Self::NetbiosDatagramServer => DHCPV4_OPTION_NETBIOS_DATAGRAM_SERVER,
Self::NetbiosNodeType => DHCPV4_OPTION_NETBIOS_NODE_TYPE,
Self::NetbiosScope => DHCPV4_OPTION_NETBIOS_SCOPE,
Self::XWindowFontServer => DHCPV4_OPTION_X_WINDOW_FONT_SERVER,
Self::XWindowDisplayManager => DHCPV4_OPTION_X_WINDOW_DISPLAY_MANAGER,
Self::RequestedIpAddress => DHCPV4_OPTION_REQUESTED_IP_ADDRESS,
Self::IpAddressLeaseTime => DHCPV4_OPTION_IP_ADDRESS_LEASE_TIME,
Self::OptionOverload => DHCPV4_OPTION_OVERLOAD,
Self::Dhcpv4MessageType => DHCPV4_OPTION_MESSAGE_TYPE,
Self::ServerIdentifier => DHCPV4_OPTION_SERVER_IDENTIFIER,
Self::ParameterRequestList => DHCPV4_OPTION_PARAMETER_REQUEST_LIST,
Self::Dhcpv4Message => DHCPV4_OPTION_MESSAGE,
Self::MaximumDhcpv4MessageSize => DHCPV4_OPTION_MAX_MESSAGE_SIZE,
Self::RenewalTime => DHCPV4_OPTION_RENEWAL_TIME,
Self::RebindingTime => DHCPV4_OPTION_REBINDING_TIME,
Self::VendorClassIdentifier => DHCPV4_OPTION_VENDOR_CLASS_IDENTIFIER,
Self::ClientIdentifier => DHCPV4_OPTION_CLIENT_IDENTIFIER,
Self::DomainSearch => DHCPV4_OPTION_DOMAIN_SEARCH,
Self::SipServers => DHCPV4_OPTION_SIP_SERVERS,
Self::ClasslessStaticRoute => DHCPV4_OPTION_CLASSLESS_STATIC_ROUTE,
Self::TftpServerName => DHCPV4_OPTION_TFTP_SERVER_NAME,
Self::BootfileName => DHCPV4_OPTION_BOOTFILE_NAME,
Self::UserClass => DHCPV4_OPTION_USER_CLASS,
Self::ClientSystemArchitecture => DHCPV4_OPTION_CLIENT_SYSTEM_ARCHITECTURE,
Self::ClientNetworkDeviceInterface => DHCPV4_OPTION_CLIENT_NDI,
Self::ClientMachineIdentifier => DHCPV4_OPTION_CLIENT_MACHINE_IDENTIFIER,
Self::ViVendorClass => DHCPV4_OPTION_VI_VENDOR_CLASS,
Self::ViVendorSpecificInformation => DHCPV4_OPTION_VI_VENDOR_SPECIFIC,
Self::RelayAgentInformation => DHCPV4_OPTION_RELAY_AGENT_INFORMATION,
Self::Authentication => DHCPV4_OPTION_AUTHENTICATION,
Self::ForcerenewNonceCapable => DHCPV4_OPTION_FORCERENEW_NONCE_CAPABLE,
Self::ClientLastTransactionTime => DHCPV4_OPTION_CLIENT_LAST_TRANSACTION_TIME,
Self::AssociatedIp => DHCPV4_OPTION_ASSOCIATED_IP,
Self::StatusCode => DHCPV4_OPTION_STATUS_CODE,
Self::BaseTime => DHCPV4_OPTION_BASE_TIME,
Self::StartTimeOfState => DHCPV4_OPTION_START_TIME_OF_STATE,
Self::QueryStartTime => DHCPV4_OPTION_QUERY_START_TIME,
Self::QueryEndTime => DHCPV4_OPTION_QUERY_END_TIME,
Self::Dhcpv4State => DHCPV4_OPTION_DHCP_STATE,
Self::DataSource => DHCPV4_OPTION_DATA_SOURCE,
Self::PxelinuxMagic => DHCPV4_OPTION_PXELINUX_MAGIC,
Self::PxelinuxConfigFile => DHCPV4_OPTION_PXELINUX_CONFIGFILE,
Self::PxelinuxPathPrefix => DHCPV4_OPTION_PXELINUX_PATHPREFIX,
Self::PxelinuxRebootTime => DHCPV4_OPTION_PXELINUX_REBOOTTIME,
Self::Ipv6OnlyPreferred => DHCPV4_OPTION_IPV6_ONLY_PREFERRED,
Self::CaptivePortal => DHCPV4_OPTION_CAPTIVE_PORTAL,
Self::MudUrl => DHCPV4_OPTION_MUD_URL_V4,
}
}
pub const fn format(self) -> Dhcpv4OptionFormat {
use Dhcpv4OptionFormat as F;
match self {
Self::SubnetMask
| Self::SwapServer
| Self::BroadcastAddress
| Self::RouterSolicitationAddress
| Self::RequestedIpAddress
| Self::ServerIdentifier => F::Ipv4,
Self::Router
| Self::TimeServer
| Self::NameServer
| Self::DomainNameServer
| Self::LogServer
| Self::CookieServer
| Self::LprServer
| Self::ImpressServer
| Self::ResourceLocationServer
| Self::NisServers
| Self::NtpServers
| Self::NetbiosNameServer
| Self::NetbiosDatagramServer
| Self::XWindowFontServer
| Self::XWindowDisplayManager
| Self::AssociatedIp => F::Ipv4List,
Self::PolicyFilter => F::Ipv4Pairs,
Self::StaticRoute => F::StaticRoutes,
Self::ClasslessStaticRoute => F::ClasslessRoutes,
Self::DomainSearch => F::DomainSearch,
Self::SipServers => F::SipServers,
Self::IpForwarding
| Self::NonLocalSourceRouting
| Self::AllSubnetsLocal
| Self::PerformMaskDiscovery
| Self::MaskSupplier
| Self::PerformRouterDiscovery
| Self::TrailerEncapsulation
| Self::EthernetEncapsulation
| Self::TcpKeepaliveGarbage => F::Bool,
Self::DefaultIpTtl | Self::TcpDefaultTtl | Self::NetbiosNodeType => F::U8,
Self::BootFileSize
| Self::MaxDatagramReassembly
| Self::InterfaceMtu
| Self::MaximumDhcpv4MessageSize => F::U16,
Self::PathMtuPlateauTable => F::U16List,
Self::TimeOffset => F::I32,
Self::PathMtuAgingTimeout
| Self::ArpCacheTimeout
| Self::TcpKeepaliveInterval
| Self::IpAddressLeaseTime
| Self::RenewalTime
| Self::RebindingTime
| Self::ClientLastTransactionTime
| Self::BaseTime
| Self::StartTimeOfState
| Self::QueryStartTime
| Self::QueryEndTime
| Self::Ipv6OnlyPreferred => F::U32,
Self::HostName
| Self::MeritDumpFile
| Self::DomainName
| Self::RootPath
| Self::ExtensionsPath
| Self::NisDomain
| Self::NetbiosScope
| Self::Dhcpv4Message
| Self::TftpServerName
| Self::BootfileName
| Self::PxelinuxConfigFile
| Self::PxelinuxPathPrefix
| Self::CaptivePortal
| Self::MudUrl => F::Text,
Self::PxelinuxRebootTime => F::U32,
Self::ParameterRequestList => F::ParameterRequestList,
Self::Dhcpv4MessageType => F::MessageType,
Self::OptionOverload => F::OptionOverload,
Self::UserClass => F::UserClass,
Self::ClientSystemArchitecture => F::ClientSystemArchitecture,
Self::ClientNetworkDeviceInterface => F::ClientNetworkDeviceInterface,
Self::ClientMachineIdentifier => F::ClientUuid,
Self::ViVendorClass => F::ViVendorClass,
Self::ViVendorSpecificInformation => F::ViVendorSpecific,
Self::RelayAgentInformation => F::RelayAgentInformation,
Self::ClientIdentifier => F::ClientIdentifier,
Self::Authentication => F::Authentication,
Self::ForcerenewNonceCapable => F::ForcerenewNonceCapable,
Self::StatusCode => F::StatusCode,
Self::Dhcpv4State => F::Dhcpv4State,
Self::DataSource => F::DataSource,
Self::VendorSpecificInformation | Self::VendorClassIdentifier | Self::PxelinuxMagic => {
F::Opaque
}
}
}
}
pub fn dhcpv4_typed_option_value(code: u8, data: &[u8]) -> Result<Option<Dhcpv4OptionValue>> {
let Some(kind) = Dhcpv4OptionKind::from_code(code) else {
return Ok(None);
};
let field = "dhcpv4.option.value";
let value = match kind.format() {
Dhcpv4OptionFormat::Ipv4 => Dhcpv4OptionValue::Ipv4(decode_ipv4_option(field, data)?),
Dhcpv4OptionFormat::Ipv4List => Dhcpv4OptionValue::Ipv4List(decode_ipv4_list(field, data)?),
Dhcpv4OptionFormat::Ipv4Pairs => {
Dhcpv4OptionValue::Ipv4Pairs(decode_ipv4_pairs(field, data)?)
}
Dhcpv4OptionFormat::Bool => Dhcpv4OptionValue::Bool(decode_bool_option(field, data)?),
Dhcpv4OptionFormat::U8 => {
validate_fixed_len(field, data.len(), 1)?;
Dhcpv4OptionValue::U8(data[0])
}
Dhcpv4OptionFormat::U16 => Dhcpv4OptionValue::U16(decode_u16_option(field, data)?),
Dhcpv4OptionFormat::U16List => Dhcpv4OptionValue::U16List(decode_u16_list(field, data)?),
Dhcpv4OptionFormat::I32 => {
validate_fixed_len(field, data.len(), 4)?;
Dhcpv4OptionValue::I32(i32::from_be_bytes([data[0], data[1], data[2], data[3]]))
}
Dhcpv4OptionFormat::U32 => Dhcpv4OptionValue::U32(decode_u32_option(field, data)?),
Dhcpv4OptionFormat::Text => Dhcpv4OptionValue::Text(data.to_vec()),
Dhcpv4OptionFormat::ParameterRequestList => {
Dhcpv4OptionValue::ParameterRequestList(data.to_vec())
}
Dhcpv4OptionFormat::MessageType => {
validate_fixed_len(field, data.len(), 1)?;
Dhcpv4OptionValue::MessageType(Dhcpv4MessageType::from_code(data[0]))
}
Dhcpv4OptionFormat::OptionOverload => {
validate_fixed_len(field, data.len(), 1)?;
Dhcpv4OptionValue::OptionOverload(OptionOverload::from_code(data[0]))
}
Dhcpv4OptionFormat::StaticRoutes => {
Dhcpv4OptionValue::StaticRoutes(decode_static_routes(data)?)
}
Dhcpv4OptionFormat::ClasslessRoutes => {
Dhcpv4OptionValue::ClasslessRoutes(decode_classless_routes(data)?)
}
Dhcpv4OptionFormat::DomainSearch => Dhcpv4OptionValue::DomainSearch(
decode_domain_name_list("dhcpv4.option.domain_search", data)?,
),
Dhcpv4OptionFormat::SipServers => Dhcpv4OptionValue::SipServers(decode_sip_servers(data)?),
Dhcpv4OptionFormat::UserClass => Dhcpv4OptionValue::UserClass(decode_user_class(data)?),
Dhcpv4OptionFormat::ClientSystemArchitecture => {
Dhcpv4OptionValue::ClientSystemArchitecture(decode_client_system_architecture(data)?)
}
Dhcpv4OptionFormat::ClientNetworkDeviceInterface => {
Dhcpv4OptionValue::ClientNetworkDeviceInterface(decode_client_ndi(data)?)
}
Dhcpv4OptionFormat::ClientUuid => Dhcpv4OptionValue::ClientUuid(decode_client_uuid(data)?),
Dhcpv4OptionFormat::ViVendorClass => {
Dhcpv4OptionValue::ViVendorClass(decode_vi_vendor_class(data)?)
}
Dhcpv4OptionFormat::ViVendorSpecific => {
Dhcpv4OptionValue::ViVendorSpecific(decode_vi_vendor_specific(data)?)
}
Dhcpv4OptionFormat::RelayAgentInformation => {
Dhcpv4OptionValue::RelayAgentInformation(decode_relay_agent_information(data)?)
}
Dhcpv4OptionFormat::ClientIdentifier => {
Dhcpv4OptionValue::ClientIdentifier(decode_client_identifier(data)?)
}
Dhcpv4OptionFormat::Authentication => {
Dhcpv4OptionValue::Authentication(decode_authentication(data)?)
}
Dhcpv4OptionFormat::ForcerenewNonceCapable => Dhcpv4OptionValue::ForcerenewNonceCapable(
Dhcpv4ForcerenewNonceCapable::new(data.to_vec()),
),
Dhcpv4OptionFormat::StatusCode => {
Dhcpv4OptionValue::StatusCode(decode_status_code(field, data)?)
}
Dhcpv4OptionFormat::Dhcpv4State => {
validate_fixed_len(field, data.len(), 1)?;
Dhcpv4OptionValue::Dhcpv4State(Dhcpv4State::from_code(data[0]))
}
Dhcpv4OptionFormat::DataSource => {
validate_fixed_len(field, data.len(), 1)?;
Dhcpv4OptionValue::DataSource(Dhcpv4DataSource::new(data[0]))
}
Dhcpv4OptionFormat::Opaque => {
if data.is_empty() {
Dhcpv4OptionValue::Empty
} else {
Dhcpv4OptionValue::Opaque(data.to_vec())
}
}
};
Ok(Some(value))
}
pub fn decode_dhcpv4_tftp_server_addresses(data: &[u8]) -> Result<Vec<Ipv4Addr>> {
let field = "dhcpv4.option.tftp_server_address";
if data.is_empty() || data.len() % 4 != 0 {
return Err(CrafterError::invalid_field_value(
field,
"TFTP server address option length must be a non-zero multiple of four",
));
}
decode_ipv4_list(field, data)
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Dhcpv4OptionSegment {
pub area: Dhcpv4OptionArea,
pub code: Dhcpv4OptionCode,
pub declared_len: Option<u8>,
pub offset: usize,
pub data: Vec<u8>,
}
impl Dhcpv4OptionSegment {
pub const fn code_value(&self) -> u8 {
self.code.code()
}
pub const fn is_single_octet(&self) -> bool {
self.code.is_single_octet()
}
}
pub fn scan_dhcpv4_option_segments(
area: Dhcpv4OptionArea,
bytes: &[u8],
) -> Result<Vec<Dhcpv4OptionSegment>> {
let mut segments = Vec::new();
let mut offset = 0usize;
while offset < bytes.len() {
let code = bytes[offset];
let code_offset = offset;
offset += 1;
match code {
DHCPV4_OPTION_PAD => segments.push(Dhcpv4OptionSegment {
area,
code: Dhcpv4OptionCode::Pad,
declared_len: None,
offset: code_offset,
data: Vec::new(),
}),
DHCPV4_OPTION_END => segments.push(Dhcpv4OptionSegment {
area,
code: Dhcpv4OptionCode::End,
declared_len: None,
offset: code_offset,
data: Vec::new(),
}),
_ => {
if offset >= bytes.len() {
return Err(CrafterError::buffer_too_short(
"dhcpv4 option length",
offset + 1,
bytes.len(),
));
}
let len = bytes[offset] as usize;
offset += 1;
let end = offset + len;
if end > bytes.len() {
return Err(CrafterError::buffer_too_short(
"dhcpv4 option data",
end,
bytes.len(),
));
}
segments.push(Dhcpv4OptionSegment {
area,
code: Dhcpv4OptionCode::from_code(code),
declared_len: Some(len as u8),
offset: code_offset,
data: bytes[offset..end].to_vec(),
});
offset = end;
}
}
}
Ok(segments)
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Dhcpv4Option {
Pad,
End,
MessageType(Dhcpv4MessageType),
OptionOverload(OptionOverload),
SubnetMask(Ipv4Addr),
Router(Vec<Ipv4Addr>),
DomainNameServer(Vec<Ipv4Addr>),
HostName(String),
DomainName(String),
Dhcpv4Message(String),
BroadcastAddress(Ipv4Addr),
RequestedIpAddress(Ipv4Addr),
IpAddressLeaseTime(u32),
ServerIdentifier(Ipv4Addr),
ParameterRequestList(Vec<u8>),
RenewalTime(u32),
RebindingTime(u32),
ClientIdentifier(Vec<u8>),
Generic {
code: u8,
data: Vec<u8>,
},
}
impl Dhcpv4Option {
pub const fn message_type(message_type: Dhcpv4MessageType) -> Self {
Self::MessageType(message_type)
}
pub const fn option_overload(overload: OptionOverload) -> Self {
Self::OptionOverload(overload)
}
pub const fn subnet_mask(mask: Ipv4Addr) -> Self {
Self::SubnetMask(mask)
}
pub fn router(routers: impl Into<Vec<Ipv4Addr>>) -> Self {
Self::Router(routers.into())
}
pub fn domain_name_server(servers: impl Into<Vec<Ipv4Addr>>) -> Self {
Self::DomainNameServer(servers.into())
}
pub fn host_name(host_name: impl Into<String>) -> Self {
Self::HostName(host_name.into())
}
pub fn domain_name(domain_name: impl Into<String>) -> Self {
Self::DomainName(domain_name.into())
}
pub fn message(message: impl Into<String>) -> Self {
Self::Dhcpv4Message(message.into())
}
pub const fn requested_ip_address(address: Ipv4Addr) -> Self {
Self::RequestedIpAddress(address)
}
pub const fn lease_time(seconds: u32) -> Self {
Self::IpAddressLeaseTime(seconds)
}
pub const fn renewal_time(seconds: u32) -> Self {
Self::RenewalTime(seconds)
}
pub const fn rebinding_time(seconds: u32) -> Self {
Self::RebindingTime(seconds)
}
pub const fn server_identifier(address: Ipv4Addr) -> Self {
Self::ServerIdentifier(address)
}
pub fn parameter_request_list(requests: impl Into<Vec<u8>>) -> Self {
Self::ParameterRequestList(requests.into())
}
pub fn client_identifier(identifier: impl Into<Vec<u8>>) -> Self {
Self::ClientIdentifier(identifier.into())
}
pub fn client_identifier_value(identifier: Dhcpv4ClientIdentifier) -> Self {
Self::ClientIdentifier(identifier.encode())
}
pub fn generic(code: u8, data: impl Into<Vec<u8>>) -> Self {
Self::Generic {
code,
data: data.into(),
}
}
pub fn typed(kind: Dhcpv4OptionKind, value: Dhcpv4OptionValue) -> Self {
Self::Generic {
code: kind.code(),
data: value.encode_payload(),
}
}
pub fn vendor_specific(data: impl Into<Vec<u8>>) -> Self {
Self::generic(DHCPV4_OPTION_VENDOR_SPECIFIC, data)
}
pub fn vendor_class_identifier(data: impl Into<Vec<u8>>) -> Self {
Self::generic(DHCPV4_OPTION_VENDOR_CLASS_IDENTIFIER, data)
}
pub fn tftp_server_name(name: impl Into<Vec<u8>>) -> Self {
Self::generic(DHCPV4_OPTION_TFTP_SERVER_NAME, name)
}
pub fn bootfile_name(name: impl Into<Vec<u8>>) -> Self {
Self::generic(DHCPV4_OPTION_BOOTFILE_NAME, name)
}
pub fn user_class(user_class: Dhcpv4UserClass) -> Self {
Self::typed(
Dhcpv4OptionKind::UserClass,
Dhcpv4OptionValue::UserClass(user_class),
)
}
pub fn client_system_architecture(arch: ClientSystemArchitecture) -> Self {
Self::typed(
Dhcpv4OptionKind::ClientSystemArchitecture,
Dhcpv4OptionValue::ClientSystemArchitecture(arch),
)
}
pub fn client_network_device_interface(ndi: ClientNetworkDeviceInterface) -> Self {
Self::typed(
Dhcpv4OptionKind::ClientNetworkDeviceInterface,
Dhcpv4OptionValue::ClientNetworkDeviceInterface(ndi),
)
}
pub fn client_uuid(uuid: Dhcpv4ClientUuid) -> Self {
Self::typed(
Dhcpv4OptionKind::ClientMachineIdentifier,
Dhcpv4OptionValue::ClientUuid(uuid),
)
}
pub fn vi_vendor_class(instances: impl Into<Vec<Dhcpv4VendorClassData>>) -> Self {
Self::typed(
Dhcpv4OptionKind::ViVendorClass,
Dhcpv4OptionValue::ViVendorClass(instances.into()),
)
}
pub fn vi_vendor_specific(instances: impl Into<Vec<Dhcpv4VendorIdentifyingOption>>) -> Self {
Self::typed(
Dhcpv4OptionKind::ViVendorSpecificInformation,
Dhcpv4OptionValue::ViVendorSpecific(instances.into()),
)
}
pub fn relay_agent_information(info: Dhcpv4RelayAgentInfo) -> Self {
Self::typed(
Dhcpv4OptionKind::RelayAgentInformation,
Dhcpv4OptionValue::RelayAgentInformation(info),
)
}
pub fn authentication(auth: Dhcpv4Authentication) -> Self {
Self::typed(
Dhcpv4OptionKind::Authentication,
Dhcpv4OptionValue::Authentication(auth),
)
}
pub fn forcerenew_nonce_capable(value: Dhcpv4ForcerenewNonceCapable) -> Self {
Self::typed(
Dhcpv4OptionKind::ForcerenewNonceCapable,
Dhcpv4OptionValue::ForcerenewNonceCapable(value),
)
}
pub fn client_last_transaction_time(seconds: u32) -> Self {
Self::typed(
Dhcpv4OptionKind::ClientLastTransactionTime,
Dhcpv4OptionValue::U32(seconds),
)
}
pub fn associated_ip(addresses: impl Into<Vec<Ipv4Addr>>) -> Self {
Self::typed(
Dhcpv4OptionKind::AssociatedIp,
Dhcpv4OptionValue::Ipv4List(addresses.into()),
)
}
pub fn status_code(status: Dhcpv4StatusCodeOption) -> Self {
Self::typed(
Dhcpv4OptionKind::StatusCode,
Dhcpv4OptionValue::StatusCode(status),
)
}
pub fn base_time(seconds: u32) -> Self {
Self::typed(Dhcpv4OptionKind::BaseTime, Dhcpv4OptionValue::U32(seconds))
}
pub fn start_time_of_state(seconds: u32) -> Self {
Self::typed(
Dhcpv4OptionKind::StartTimeOfState,
Dhcpv4OptionValue::U32(seconds),
)
}
pub fn query_start_time(seconds: u32) -> Self {
Self::typed(
Dhcpv4OptionKind::QueryStartTime,
Dhcpv4OptionValue::U32(seconds),
)
}
pub fn query_end_time(seconds: u32) -> Self {
Self::typed(
Dhcpv4OptionKind::QueryEndTime,
Dhcpv4OptionValue::U32(seconds),
)
}
pub fn dhcp_state(state: Dhcpv4State) -> Self {
Self::typed(
Dhcpv4OptionKind::Dhcpv4State,
Dhcpv4OptionValue::Dhcpv4State(state),
)
}
pub fn data_source(source: Dhcpv4DataSource) -> Self {
Self::typed(
Dhcpv4OptionKind::DataSource,
Dhcpv4OptionValue::DataSource(source),
)
}
pub fn tftp_server_addresses(addresses: impl Into<Vec<Ipv4Addr>>) -> Self {
Self::generic(
DHCPV4_OPTION_TFTP_SERVER_ADDRESS,
encode_ipv4_list(&addresses.into()),
)
}
pub fn pxelinux_magic() -> Self {
Self::generic(
DHCPV4_OPTION_PXELINUX_MAGIC,
DHCPV4_PXELINUX_MAGIC_VALUE.to_vec(),
)
}
pub fn pxelinux_config_file(path: impl Into<Vec<u8>>) -> Self {
Self::generic(DHCPV4_OPTION_PXELINUX_CONFIGFILE, path)
}
pub fn pxelinux_path_prefix(path: impl Into<Vec<u8>>) -> Self {
Self::generic(DHCPV4_OPTION_PXELINUX_PATHPREFIX, path)
}
pub fn pxelinux_reboot_time(seconds: u32) -> Self {
Self::typed(
Dhcpv4OptionKind::PxelinuxRebootTime,
Dhcpv4OptionValue::U32(seconds),
)
}
pub const fn code(&self) -> u8 {
match self {
Self::Pad => DHCPV4_OPTION_PAD,
Self::End => DHCPV4_OPTION_END,
Self::MessageType(_) => DHCPV4_OPTION_MESSAGE_TYPE,
Self::OptionOverload(_) => DHCPV4_OPTION_OVERLOAD,
Self::SubnetMask(_) => DHCPV4_OPTION_SUBNET_MASK,
Self::Router(_) => DHCPV4_OPTION_ROUTER,
Self::DomainNameServer(_) => DHCPV4_OPTION_DOMAIN_NAME_SERVER,
Self::HostName(_) => DHCPV4_OPTION_HOST_NAME,
Self::DomainName(_) => DHCPV4_OPTION_DOMAIN_NAME,
Self::Dhcpv4Message(_) => DHCPV4_OPTION_MESSAGE,
Self::BroadcastAddress(_) => DHCPV4_OPTION_BROADCAST_ADDRESS,
Self::RequestedIpAddress(_) => DHCPV4_OPTION_REQUESTED_IP_ADDRESS,
Self::IpAddressLeaseTime(_) => DHCPV4_OPTION_IP_ADDRESS_LEASE_TIME,
Self::ServerIdentifier(_) => DHCPV4_OPTION_SERVER_IDENTIFIER,
Self::ParameterRequestList(_) => DHCPV4_OPTION_PARAMETER_REQUEST_LIST,
Self::RenewalTime(_) => DHCPV4_OPTION_RENEWAL_TIME,
Self::RebindingTime(_) => DHCPV4_OPTION_REBINDING_TIME,
Self::ClientIdentifier(_) => DHCPV4_OPTION_CLIENT_IDENTIFIER,
Self::Generic { code, .. } => *code,
}
}
pub fn option_code(&self) -> Dhcpv4OptionCode {
Dhcpv4OptionCode::from_code(self.code())
}
pub fn registry_name(&self) -> Option<&'static str> {
dhcpv4_option_name(self.code())
}
pub fn logical_value(&self) -> Option<Dhcpv4OptionValue> {
let value = match self {
Self::Pad | Self::End => return None,
Self::MessageType(message_type) => Dhcpv4OptionValue::MessageType(*message_type),
Self::OptionOverload(overload) => Dhcpv4OptionValue::OptionOverload(*overload),
Self::SubnetMask(address)
| Self::BroadcastAddress(address)
| Self::RequestedIpAddress(address)
| Self::ServerIdentifier(address) => Dhcpv4OptionValue::Ipv4(*address),
Self::Router(addresses) | Self::DomainNameServer(addresses) => {
Dhcpv4OptionValue::Ipv4List(addresses.clone())
}
Self::HostName(text) | Self::DomainName(text) | Self::Dhcpv4Message(text) => {
Dhcpv4OptionValue::Text(text.as_bytes().to_vec())
}
Self::IpAddressLeaseTime(seconds)
| Self::RenewalTime(seconds)
| Self::RebindingTime(seconds) => Dhcpv4OptionValue::U32(*seconds),
Self::ParameterRequestList(requests) => {
Dhcpv4OptionValue::ParameterRequestList(requests.clone())
}
Self::ClientIdentifier(data) | Self::Generic { data, .. } => {
if data.is_empty() {
Dhcpv4OptionValue::Empty
} else {
Dhcpv4OptionValue::Opaque(data.clone())
}
}
};
Some(value)
}
pub fn kind(&self) -> Option<Dhcpv4OptionKind> {
Dhcpv4OptionKind::from_code(self.code())
}
pub fn typed_value(&self) -> Result<Option<Dhcpv4OptionValue>> {
if matches!(self, Self::Pad | Self::End) {
return Ok(None);
}
dhcpv4_typed_option_value(self.code(), &self.payload_bytes()?)
}
pub fn encoded_len(&self) -> usize {
match self {
Self::Pad | Self::End => 1,
Self::MessageType(_) | Self::OptionOverload(_) => 3,
Self::SubnetMask(_)
| Self::BroadcastAddress(_)
| Self::RequestedIpAddress(_)
| Self::ServerIdentifier(_) => 6,
Self::Router(addresses) | Self::DomainNameServer(addresses) => {
split_option_encoded_len(addresses.len() * 4)
}
Self::HostName(name) | Self::DomainName(name) | Self::Dhcpv4Message(name) => {
split_option_encoded_len(name.len())
}
Self::IpAddressLeaseTime(_) | Self::RenewalTime(_) | Self::RebindingTime(_) => 6,
Self::ParameterRequestList(requests)
| Self::ClientIdentifier(requests)
| Self::Generic { data: requests, .. } => split_option_encoded_len(requests.len()),
}
}
pub fn encode(&self) -> Result<Vec<u8>> {
let mut bytes = Vec::with_capacity(self.encoded_len());
self.encode_into(&mut bytes)?;
Ok(bytes)
}
pub fn payload(&self) -> Result<Vec<u8>> {
self.payload_bytes()
}
pub fn decode_all(bytes: &[u8]) -> Result<Vec<Self>> {
decode_dhcpv4_options(bytes)
}
pub(super) fn encode_into(&self, out: &mut Vec<u8>) -> Result<()> {
match self {
Self::Pad => {
out.push(DHCPV4_OPTION_PAD);
Ok(())
}
Self::End => {
out.push(DHCPV4_OPTION_END);
Ok(())
}
Self::MessageType(message_type) => {
encode_split_option(DHCPV4_OPTION_MESSAGE_TYPE, &[message_type.code()], out);
Ok(())
}
Self::OptionOverload(overload) => {
encode_split_option(DHCPV4_OPTION_OVERLOAD, &[overload.code()], out);
Ok(())
}
Self::SubnetMask(address) => {
encode_split_option(DHCPV4_OPTION_SUBNET_MASK, &address.octets(), out);
Ok(())
}
Self::BroadcastAddress(address) => {
encode_split_option(DHCPV4_OPTION_BROADCAST_ADDRESS, &address.octets(), out);
Ok(())
}
Self::RequestedIpAddress(address) => {
encode_split_option(DHCPV4_OPTION_REQUESTED_IP_ADDRESS, &address.octets(), out);
Ok(())
}
Self::ServerIdentifier(address) => {
encode_split_option(DHCPV4_OPTION_SERVER_IDENTIFIER, &address.octets(), out);
Ok(())
}
Self::HostName(host_name) => {
encode_split_option(DHCPV4_OPTION_HOST_NAME, host_name.as_bytes(), out);
Ok(())
}
Self::DomainName(domain_name) => {
encode_split_option(DHCPV4_OPTION_DOMAIN_NAME, domain_name.as_bytes(), out);
Ok(())
}
Self::Dhcpv4Message(message) => {
encode_split_option(DHCPV4_OPTION_MESSAGE, message.as_bytes(), out);
Ok(())
}
Self::IpAddressLeaseTime(seconds) => {
encode_split_option(
DHCPV4_OPTION_IP_ADDRESS_LEASE_TIME,
&seconds.to_be_bytes(),
out,
);
Ok(())
}
Self::RenewalTime(seconds) => {
encode_split_option(DHCPV4_OPTION_RENEWAL_TIME, &seconds.to_be_bytes(), out);
Ok(())
}
Self::RebindingTime(seconds) => {
encode_split_option(DHCPV4_OPTION_REBINDING_TIME, &seconds.to_be_bytes(), out);
Ok(())
}
Self::ParameterRequestList(requests) => {
encode_split_option(DHCPV4_OPTION_PARAMETER_REQUEST_LIST, requests, out);
Ok(())
}
Self::ClientIdentifier(identifier) => {
encode_split_option(DHCPV4_OPTION_CLIENT_IDENTIFIER, identifier, out);
Ok(())
}
Self::Generic { code, data } => {
validate_data_option_code(*code)?;
encode_split_option(*code, data, out);
Ok(())
}
Self::Router(_) | Self::DomainNameServer(_) => {
let data = self.payload_bytes()?;
encode_split_option(self.code(), &data, out);
Ok(())
}
}
}
fn payload_bytes(&self) -> Result<Vec<u8>> {
let bytes = match self {
Self::Pad | Self::End => Vec::new(),
Self::MessageType(message_type) => vec![message_type.code()],
Self::OptionOverload(overload) => vec![overload.code()],
Self::SubnetMask(address)
| Self::BroadcastAddress(address)
| Self::RequestedIpAddress(address)
| Self::ServerIdentifier(address) => address.octets().to_vec(),
Self::Router(addresses) | Self::DomainNameServer(addresses) => {
encode_ipv4_list(addresses)
}
Self::HostName(host_name)
| Self::DomainName(host_name)
| Self::Dhcpv4Message(host_name) => host_name.as_bytes().to_vec(),
Self::IpAddressLeaseTime(seconds)
| Self::RenewalTime(seconds)
| Self::RebindingTime(seconds) => seconds.to_be_bytes().to_vec(),
Self::ParameterRequestList(requests) | Self::ClientIdentifier(requests) => {
requests.clone()
}
Self::Generic { code, data } => {
if matches!(*code, DHCPV4_OPTION_PAD | DHCPV4_OPTION_END) {
return Err(CrafterError::invalid_field_value(
"dhcpv4.option.code",
"generic option code cannot be pad or end",
));
}
data.clone()
}
};
Ok(bytes)
}
}
fn validate_data_option_code(code: u8) -> Result<()> {
if matches!(code, DHCPV4_OPTION_PAD | DHCPV4_OPTION_END) {
return Err(CrafterError::invalid_field_value(
"dhcpv4.option.code",
"pad and end options do not carry a length byte",
));
}
Ok(())
}
pub(super) fn decode_dhcpv4_options(bytes: &[u8]) -> Result<Vec<Dhcpv4Option>> {
let mut options = Vec::with_capacity(8);
let mut seen_content = [false; 256];
let mut saw_end = false;
let mut offset = 0usize;
while offset < bytes.len() {
let code = bytes[offset];
offset += 1;
match code {
DHCPV4_OPTION_PAD => options.push(Dhcpv4Option::Pad),
DHCPV4_OPTION_END if !saw_end => {
options.push(Dhcpv4Option::End);
saw_end = true;
}
DHCPV4_OPTION_END => {
return Err(CrafterError::invalid_field_value(
"dhcpv4.option.end",
"non-padding data follows DHCP end option",
));
}
_ => {
if saw_end {
return Err(CrafterError::invalid_field_value(
"dhcpv4.option.end",
"non-padding data follows DHCP end option",
));
}
if offset >= bytes.len() {
return Err(CrafterError::buffer_too_short(
"dhcpv4 option length",
offset + 1,
bytes.len(),
));
}
let len = bytes[offset] as usize;
offset += 1;
let end = offset + len;
if end > bytes.len() {
return Err(CrafterError::buffer_too_short(
"dhcpv4 option data",
end,
bytes.len(),
));
}
if seen_content[code as usize] {
return decode_segments_to_options(&scan_dhcpv4_option_segments(
Dhcpv4OptionArea::Options,
bytes,
)?);
}
seen_content[code as usize] = true;
options.push(decode_dhcpv4_option(code, &bytes[offset..end])?);
offset = end;
}
}
}
if !saw_end {
return Err(CrafterError::invalid_field_value(
"dhcpv4.options",
"DHCPv4 options are missing an end marker",
));
}
Ok(options)
}
pub(super) fn find_option_overload(options: &[Dhcpv4Option]) -> Option<OptionOverload> {
options.iter().find_map(|option| match option {
Dhcpv4Option::OptionOverload(overload) => Some(*overload),
_ => None,
})
}
pub(super) fn decode_overload_area_options(
area: Dhcpv4OptionArea,
bytes: &[u8],
) -> Result<Vec<Dhcpv4Option>> {
let segments = scan_dhcpv4_option_segments(area, bytes)?;
let mut order = SegmentOrder::new();
let mut saw_end = false;
for segment in &segments {
match segment.code {
Dhcpv4OptionCode::Pad => {
if !saw_end {
order.push_pad();
}
}
Dhcpv4OptionCode::End if !saw_end => {
order.push_end();
saw_end = true;
}
_ => {
if saw_end {
return Err(CrafterError::invalid_field_value(
overload_end_field(area),
"non-padding data follows the DHCP end option in an overloaded field",
));
}
order.push_content(segment.code_value(), &segment.data);
}
}
}
let options = order.into_options()?;
if !saw_end {
return Err(CrafterError::invalid_field_value(
overload_field(area),
"overloaded DHCP field is missing an end marker",
));
}
Ok(options)
}
const fn overload_field(area: Dhcpv4OptionArea) -> &'static str {
match area {
Dhcpv4OptionArea::Options => "dhcpv4.options",
Dhcpv4OptionArea::File => "dhcpv4.file.options",
Dhcpv4OptionArea::Sname => "dhcpv4.sname.options",
}
}
const fn overload_end_field(area: Dhcpv4OptionArea) -> &'static str {
match area {
Dhcpv4OptionArea::Options => "dhcpv4.option.end",
Dhcpv4OptionArea::File => "dhcpv4.file.option.end",
Dhcpv4OptionArea::Sname => "dhcpv4.sname.option.end",
}
}
pub(super) fn encode_overload_area_options(
field: &'static str,
options: &[Dhcpv4Option],
width: usize,
) -> Result<Vec<u8>> {
let mut bytes = encode_dhcpv4_options(options)?;
if bytes.len() > width {
return Err(CrafterError::invalid_field_value(
field,
"overloaded DHCP field options exceed the fixed field width",
));
}
bytes.resize(width, 0);
Ok(bytes)
}
fn decode_segments_to_options(segments: &[Dhcpv4OptionSegment]) -> Result<Vec<Dhcpv4Option>> {
let mut order = SegmentOrder::new();
let mut saw_end = false;
for segment in segments {
match segment.code {
Dhcpv4OptionCode::Pad => order.push_pad(),
Dhcpv4OptionCode::End if !saw_end => {
order.push_end();
saw_end = true;
}
_ => {
if saw_end {
return Err(CrafterError::invalid_field_value(
"dhcpv4.option.end",
"non-padding data follows DHCP end option",
));
}
order.push_content(segment.code_value(), &segment.data);
}
}
}
let options = order.into_options()?;
if !saw_end {
return Err(CrafterError::invalid_field_value(
"dhcpv4.options",
"DHCPv4 options are missing an end marker",
));
}
Ok(options)
}
struct SegmentOrder {
slots: Vec<Slot>,
content_index: std::collections::HashMap<u8, usize>,
}
enum Slot {
Pad,
End,
Content { code: u8, data: Vec<u8> },
}
impl SegmentOrder {
fn new() -> Self {
Self {
slots: Vec::new(),
content_index: std::collections::HashMap::new(),
}
}
fn push_pad(&mut self) {
self.slots.push(Slot::Pad);
}
fn push_end(&mut self) {
self.slots.push(Slot::End);
}
fn push_content(&mut self, code: u8, data: &[u8]) {
if let Some(&index) = self.content_index.get(&code) {
if let Slot::Content { data: existing, .. } = &mut self.slots[index] {
existing.extend_from_slice(data);
return;
}
}
let index = self.slots.len();
self.content_index.insert(code, index);
self.slots.push(Slot::Content {
code,
data: data.to_vec(),
});
}
fn into_options(self) -> Result<Vec<Dhcpv4Option>> {
let mut options = Vec::with_capacity(self.slots.len());
for slot in self.slots {
match slot {
Slot::Pad => options.push(Dhcpv4Option::Pad),
Slot::End => options.push(Dhcpv4Option::End),
Slot::Content { code, data } => options.push(decode_dhcpv4_option(code, &data)?),
}
}
Ok(options)
}
}
pub(super) fn decode_dhcpv4_option(code: u8, data: &[u8]) -> Result<Dhcpv4Option> {
match code {
DHCPV4_OPTION_MESSAGE_TYPE => {
validate_fixed_len("dhcpv4.option.message_type", data.len(), 1)?;
Ok(Dhcpv4Option::MessageType(Dhcpv4MessageType::from_code(
data[0],
)))
}
DHCPV4_OPTION_OVERLOAD => {
validate_fixed_len("dhcpv4.option.overload", data.len(), 1)?;
let overload = OptionOverload::from_code(data[0]);
Ok(Dhcpv4Option::OptionOverload(overload))
}
DHCPV4_OPTION_SUBNET_MASK => Ok(Dhcpv4Option::SubnetMask(decode_ipv4_option(
"dhcpv4.option.subnet_mask",
data,
)?)),
DHCPV4_OPTION_ROUTER => Ok(Dhcpv4Option::Router(decode_ipv4_list(
"dhcpv4.option.router",
data,
)?)),
DHCPV4_OPTION_DOMAIN_NAME_SERVER => Ok(Dhcpv4Option::DomainNameServer(decode_ipv4_list(
"dhcpv4.option.domain_name_server",
data,
)?)),
DHCPV4_OPTION_HOST_NAME => Ok(match decode_optional_text(data) {
Some(text) => Dhcpv4Option::HostName(text),
None => Dhcpv4Option::Generic {
code,
data: data.to_vec(),
},
}),
DHCPV4_OPTION_DOMAIN_NAME => Ok(match decode_optional_text(data) {
Some(text) => Dhcpv4Option::DomainName(text),
None => Dhcpv4Option::Generic {
code,
data: data.to_vec(),
},
}),
DHCPV4_OPTION_MESSAGE => Ok(match decode_optional_text(data) {
Some(text) => Dhcpv4Option::Dhcpv4Message(text),
None => Dhcpv4Option::Generic {
code,
data: data.to_vec(),
},
}),
DHCPV4_OPTION_BROADCAST_ADDRESS => Ok(Dhcpv4Option::BroadcastAddress(decode_ipv4_option(
"dhcpv4.option.broadcast_address",
data,
)?)),
DHCPV4_OPTION_REQUESTED_IP_ADDRESS => Ok(Dhcpv4Option::RequestedIpAddress(
decode_ipv4_option("dhcpv4.option.requested_ip_address", data)?,
)),
DHCPV4_OPTION_IP_ADDRESS_LEASE_TIME => Ok(Dhcpv4Option::IpAddressLeaseTime(
decode_u32_option("dhcpv4.option.lease_time", data)?,
)),
DHCPV4_OPTION_SERVER_IDENTIFIER => Ok(Dhcpv4Option::ServerIdentifier(decode_ipv4_option(
"dhcpv4.option.server_identifier",
data,
)?)),
DHCPV4_OPTION_PARAMETER_REQUEST_LIST => {
Ok(Dhcpv4Option::ParameterRequestList(data.to_vec()))
}
DHCPV4_OPTION_RENEWAL_TIME => Ok(Dhcpv4Option::RenewalTime(decode_u32_option(
"dhcpv4.option.renewal_time",
data,
)?)),
DHCPV4_OPTION_REBINDING_TIME => Ok(Dhcpv4Option::RebindingTime(decode_u32_option(
"dhcpv4.option.rebinding_time",
data,
)?)),
DHCPV4_OPTION_CLIENT_IDENTIFIER => Ok(Dhcpv4Option::ClientIdentifier(data.to_vec())),
_ => Ok(Dhcpv4Option::Generic {
code,
data: data.to_vec(),
}),
}
}
pub(super) fn encode_dhcpv4_options(options: &[Dhcpv4Option]) -> Result<Vec<u8>> {
let mut out = Vec::with_capacity(encoded_options_len_lossy(options));
let mut saw_end = false;
for option in options {
if saw_end && !matches!(option, Dhcpv4Option::Pad) {
return Err(CrafterError::invalid_field_value(
"dhcpv4.options",
"only padding may follow the DHCP end option",
));
}
if matches!(option, Dhcpv4Option::End) {
saw_end = true;
}
option.encode_into(&mut out)?;
}
if !saw_end {
out.push(DHCPV4_OPTION_END);
}
Ok(out)
}
pub(super) fn encoded_options_len_lossy(options: &[Dhcpv4Option]) -> usize {
let len = options.iter().map(Dhcpv4Option::encoded_len).sum::<usize>();
if options
.iter()
.any(|option| matches!(option, Dhcpv4Option::End))
{
len
} else {
len + 1
}
}
fn validate_fixed_len(field: &'static str, actual: usize, expected: usize) -> Result<()> {
if actual != expected {
return Err(CrafterError::invalid_field_value(
field,
"DHCP option has an invalid fixed length",
));
}
Ok(())
}
fn decode_optional_text(data: &[u8]) -> Option<String> {
str::from_utf8(data).map(str::to_string).ok()
}
fn decode_ipv4_option(field: &'static str, data: &[u8]) -> Result<Ipv4Addr> {
validate_fixed_len(field, data.len(), 4)?;
Ok(Ipv4Addr::new(data[0], data[1], data[2], data[3]))
}
fn decode_ipv4_list(field: &'static str, data: &[u8]) -> Result<Vec<Ipv4Addr>> {
if data.len() % 4 != 0 {
return Err(CrafterError::invalid_field_value(
field,
"IPv4 address list option length must be a multiple of four",
));
}
Ok(data
.chunks_exact(4)
.map(|chunk| Ipv4Addr::new(chunk[0], chunk[1], chunk[2], chunk[3]))
.collect())
}
fn decode_u32_option(field: &'static str, data: &[u8]) -> Result<u32> {
validate_fixed_len(field, data.len(), 4)?;
read_u32_be(data)
}
fn decode_u16_option(field: &'static str, data: &[u8]) -> Result<u16> {
validate_fixed_len(field, data.len(), 2)?;
read_u16_be(data)
}
fn decode_u16_list(field: &'static str, data: &[u8]) -> Result<Vec<u16>> {
if data.is_empty() || data.len() % 2 != 0 {
return Err(CrafterError::invalid_field_value(
field,
"16-bit list option length must be a non-zero multiple of two",
));
}
data.chunks_exact(2).map(read_u16_be).collect()
}
fn decode_bool_option(field: &'static str, data: &[u8]) -> Result<bool> {
validate_fixed_len(field, data.len(), 1)?;
match data[0] {
0 => Ok(false),
1 => Ok(true),
_ => Err(CrafterError::invalid_field_value(
field,
"boolean option octet must be 0 or 1",
)),
}
}
fn decode_ipv4_pairs(field: &'static str, data: &[u8]) -> Result<Vec<(Ipv4Addr, Ipv4Addr)>> {
if data.is_empty() || data.len() % 8 != 0 {
return Err(CrafterError::invalid_field_value(
field,
"IPv4 address pair option length must be a non-zero multiple of eight",
));
}
Ok(data
.chunks_exact(8)
.map(|chunk| {
(
Ipv4Addr::new(chunk[0], chunk[1], chunk[2], chunk[3]),
Ipv4Addr::new(chunk[4], chunk[5], chunk[6], chunk[7]),
)
})
.collect())
}
fn encode_ipv4_list(addresses: &[Ipv4Addr]) -> Vec<u8> {
let mut bytes = Vec::with_capacity(addresses.len() * 4);
for address in addresses {
bytes.extend_from_slice(&address.octets());
}
bytes
}
const DHCPV4_STATIC_ROUTE_ENTRY_LEN: usize = 8;
const DHCPV4_CLASSLESS_ROUTER_LEN: usize = 4;
const DHCPV4_CLASSLESS_MAX_PREFIX: u8 = 32;
const DHCPV4_DNS_LABEL_MAX_LEN: usize = 63;
const DHCPV4_DNS_POINTER_MASK: u8 = 0xC0;
const DHCPV4_SIP_ENC_DOMAIN: u8 = 0;
const DHCPV4_SIP_ENC_ADDRESS: u8 = 1;
fn decode_static_routes(data: &[u8]) -> Result<Vec<Dhcpv4StaticRoute>> {
let field = "dhcpv4.option.static_route";
if data.is_empty() || data.len() % DHCPV4_STATIC_ROUTE_ENTRY_LEN != 0 {
return Err(CrafterError::invalid_field_value(
field,
"static route option length must be a non-zero multiple of eight",
));
}
Ok(data
.chunks_exact(DHCPV4_STATIC_ROUTE_ENTRY_LEN)
.map(|chunk| {
Dhcpv4StaticRoute::new(
Ipv4Addr::new(chunk[0], chunk[1], chunk[2], chunk[3]),
Ipv4Addr::new(chunk[4], chunk[5], chunk[6], chunk[7]),
)
})
.collect())
}
fn encode_static_routes(routes: &[Dhcpv4StaticRoute]) -> Vec<u8> {
let mut bytes = Vec::with_capacity(routes.len() * DHCPV4_STATIC_ROUTE_ENTRY_LEN);
for route in routes {
bytes.extend_from_slice(&route.destination.octets());
bytes.extend_from_slice(&route.router.octets());
}
bytes
}
fn decode_classless_routes(data: &[u8]) -> Result<Vec<Dhcpv4ClasslessRoute>> {
let field = "dhcpv4.option.classless_static_route";
let mut routes = Vec::new();
let mut offset = 0usize;
while offset < data.len() {
let prefix_length = data[offset];
offset += 1;
if prefix_length > DHCPV4_CLASSLESS_MAX_PREFIX {
return Err(CrafterError::invalid_field_value(
field,
"classless route prefix length exceeds 32 bits",
));
}
let significant = Dhcpv4ClasslessRoute::significant_octets(prefix_length);
if offset + significant + DHCPV4_CLASSLESS_ROUTER_LEN > data.len() {
return Err(CrafterError::buffer_too_short(
field,
offset + significant + DHCPV4_CLASSLESS_ROUTER_LEN,
data.len(),
));
}
let mut subnet = [0u8; 4];
subnet[..significant].copy_from_slice(&data[offset..offset + significant]);
offset += significant;
let router = Ipv4Addr::new(
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
);
offset += DHCPV4_CLASSLESS_ROUTER_LEN;
routes.push(Dhcpv4ClasslessRoute::new(
prefix_length,
Ipv4Addr::from(subnet),
router,
));
}
Ok(routes)
}
fn encode_classless_routes(routes: &[Dhcpv4ClasslessRoute]) -> Vec<u8> {
let mut bytes = Vec::new();
for route in routes {
let significant = Dhcpv4ClasslessRoute::significant_octets(route.prefix_length).min(4);
bytes.push(route.prefix_length);
bytes.extend_from_slice(&route.destination.octets()[..significant]);
bytes.extend_from_slice(&route.router.octets());
}
bytes
}
fn decode_domain_name_list(field: &'static str, data: &[u8]) -> Result<Vec<String>> {
let mut names = Vec::new();
let mut offset = 0usize;
while offset < data.len() {
let (name, next) = decode_domain_name(field, data, offset)?;
names.push(name);
offset = next;
}
Ok(names)
}
fn decode_domain_name(field: &'static str, data: &[u8], start: usize) -> Result<(String, usize)> {
let mut labels: Vec<String> = Vec::new();
let mut cursor = start;
let mut linear_end: Option<usize> = None;
let mut jumps = 0usize;
loop {
if cursor >= data.len() {
return Err(CrafterError::buffer_too_short(
field,
cursor + 1,
data.len(),
));
}
let length = data[cursor];
if length == 0 {
cursor += 1;
let end = linear_end.unwrap_or(cursor);
return Ok((labels.join("."), end));
}
if length & DHCPV4_DNS_POINTER_MASK == DHCPV4_DNS_POINTER_MASK {
if cursor + 2 > data.len() {
return Err(CrafterError::buffer_too_short(
field,
cursor + 2,
data.len(),
));
}
let pointer = (usize::from(length & !DHCPV4_DNS_POINTER_MASK) << 8)
| usize::from(data[cursor + 1]);
if linear_end.is_none() {
linear_end = Some(cursor + 2);
}
if pointer >= data.len() {
return Err(CrafterError::invalid_field_value(
field,
"domain-name compression pointer points outside the option data",
));
}
jumps += 1;
if jumps > data.len() {
return Err(CrafterError::invalid_field_value(
field,
"domain-name compression pointers form a loop",
));
}
cursor = pointer;
continue;
}
if length & DHCPV4_DNS_POINTER_MASK != 0 {
return Err(CrafterError::invalid_field_value(
field,
"domain-name label length has reserved high bits set",
));
}
let label_len = usize::from(length);
if label_len > DHCPV4_DNS_LABEL_MAX_LEN {
return Err(CrafterError::invalid_field_value(
field,
"domain-name label exceeds 63 octets",
));
}
let label_start = cursor + 1;
let label_end = label_start + label_len;
if label_end > data.len() {
return Err(CrafterError::buffer_too_short(field, label_end, data.len()));
}
labels.push(String::from_utf8_lossy(&data[label_start..label_end]).into_owned());
cursor = label_end;
}
}
fn encode_domain_name_list(names: &[String]) -> Vec<u8> {
let mut bytes = Vec::new();
for name in names {
for label in name.split('.').filter(|label| !label.is_empty()) {
let label_bytes = label.as_bytes();
let len = label_bytes.len().min(DHCPV4_DNS_LABEL_MAX_LEN);
bytes.push(len as u8);
bytes.extend_from_slice(&label_bytes[..len]);
}
bytes.push(0);
}
bytes
}
fn decode_sip_servers(data: &[u8]) -> Result<SipServers> {
let field = "dhcpv4.option.sip_servers";
let Some((&encoding, rest)) = data.split_first() else {
return Err(CrafterError::buffer_too_short(field, 1, 0));
};
match encoding {
DHCPV4_SIP_ENC_DOMAIN => Ok(SipServers::DomainNames(decode_domain_name_list(
field, rest,
)?)),
DHCPV4_SIP_ENC_ADDRESS => {
if rest.is_empty() || rest.len() % 4 != 0 {
return Err(CrafterError::invalid_field_value(
field,
"SIP server address list length must be a non-zero multiple of four",
));
}
Ok(SipServers::Addresses(decode_ipv4_list(field, rest)?))
}
other => Ok(SipServers::Unknown {
encoding: other,
data: rest.to_vec(),
}),
}
}
fn encode_sip_servers(servers: &SipServers) -> Vec<u8> {
match servers {
SipServers::DomainNames(names) => {
let mut bytes = vec![DHCPV4_SIP_ENC_DOMAIN];
bytes.extend(encode_domain_name_list(names));
bytes
}
SipServers::Addresses(addresses) => {
let mut bytes = vec![DHCPV4_SIP_ENC_ADDRESS];
bytes.extend(encode_ipv4_list(addresses));
bytes
}
SipServers::Unknown { encoding, data } => {
let mut bytes = Vec::with_capacity(1 + data.len());
bytes.push(*encoding);
bytes.extend_from_slice(data);
bytes
}
}
}
const DHCPV4_ENTERPRISE_NUMBER_LEN: usize = 4;
fn decode_user_class(data: &[u8]) -> Result<Dhcpv4UserClass> {
let field = "dhcpv4.option.user_class";
let mut classes = Vec::new();
let mut offset = 0usize;
while offset < data.len() {
let len = data[offset] as usize;
offset += 1;
if len == 0 {
return Err(CrafterError::invalid_field_value(
field,
"user class data instance length must be non-zero",
));
}
let end = offset + len;
if end > data.len() {
return Err(CrafterError::buffer_too_short(field, end, data.len()));
}
classes.push(data[offset..end].to_vec());
offset = end;
}
Ok(Dhcpv4UserClass::new(classes))
}
fn encode_user_class(user_class: &Dhcpv4UserClass) -> Vec<u8> {
let mut bytes = Vec::new();
for class in &user_class.classes {
let len = class.len().min(DHCPV4_MAX_OPTION_DATA_LEN);
bytes.push(len as u8);
bytes.extend_from_slice(&class[..len]);
}
bytes
}
fn decode_client_system_architecture(data: &[u8]) -> Result<ClientSystemArchitecture> {
let field = "dhcpv4.option.client_system_architecture";
Ok(ClientSystemArchitecture::new(decode_u16_list(field, data)?))
}
fn encode_client_system_architecture(arch: &ClientSystemArchitecture) -> Vec<u8> {
let mut bytes = Vec::with_capacity(arch.architectures.len() * 2);
for value in &arch.architectures {
bytes.extend_from_slice(&value.to_be_bytes());
}
bytes
}
fn decode_client_ndi(data: &[u8]) -> Result<ClientNetworkDeviceInterface> {
let field = "dhcpv4.option.client_ndi";
validate_fixed_len(field, data.len(), 3)?;
Ok(ClientNetworkDeviceInterface::new(data[0], data[1], data[2]))
}
fn decode_client_uuid(data: &[u8]) -> Result<Dhcpv4ClientUuid> {
let field = "dhcpv4.option.client_uuid";
let Some((&identifier_type, rest)) = data.split_first() else {
return Err(CrafterError::buffer_too_short(field, 1, 0));
};
Ok(Dhcpv4ClientUuid::new(identifier_type, rest.to_vec()))
}
fn decode_client_identifier(data: &[u8]) -> Result<Dhcpv4ClientIdentifier> {
let field = "dhcpv4.option.client_identifier";
let Some((&type_octet, rest)) = data.split_first() else {
return Ok(Dhcpv4ClientIdentifier::Raw(Vec::new()));
};
if type_octet == DHCPV4_CLIENT_ID_TYPE_RFC4361 {
if rest.len() < DHCPV4_IAID_LEN {
return Err(CrafterError::buffer_too_short(
field,
1 + DHCPV4_IAID_LEN,
data.len(),
));
}
let iaid = read_u32_be(&rest[..DHCPV4_IAID_LEN])?;
let duid = rest[DHCPV4_IAID_LEN..].to_vec();
return Ok(Dhcpv4ClientIdentifier::NodeSpecific { iaid, duid });
}
if type_octet == DHCPV4_CLIENT_ID_TYPE_NONE {
return Ok(Dhcpv4ClientIdentifier::Raw(data.to_vec()));
}
Ok(Dhcpv4ClientIdentifier::LegacyHardware {
hardware_type: type_octet,
address: rest.to_vec(),
})
}
fn decode_authentication(data: &[u8]) -> Result<Dhcpv4Authentication> {
let field = "dhcpv4.option.authentication";
if data.len() < DHCPV4_AUTH_HEADER_LEN {
return Err(CrafterError::buffer_too_short(
field,
DHCPV4_AUTH_HEADER_LEN,
data.len(),
));
}
let protocol = Dhcpv4AuthProtocol::from_code(data[0]);
let algorithm = Dhcpv4AuthAlgorithm::from_code(data[1]);
let rdm = Dhcpv4ReplayDetectionMethod::from_code(data[2]);
let replay_end = 3 + DHCPV4_AUTH_REPLAY_DETECTION_LEN;
let mut replay_bytes = [0u8; DHCPV4_AUTH_REPLAY_DETECTION_LEN];
replay_bytes.copy_from_slice(&data[3..replay_end]);
let replay_detection = u64::from_be_bytes(replay_bytes);
Ok(Dhcpv4Authentication {
protocol,
algorithm,
rdm,
replay_detection,
authentication_information: data[replay_end..].to_vec(),
})
}
fn decode_status_code(field: &'static str, data: &[u8]) -> Result<Dhcpv4StatusCodeOption> {
let Some((&status, message)) = data.split_first() else {
return Err(CrafterError::buffer_too_short(field, 1, 0));
};
Ok(Dhcpv4StatusCodeOption::new(
Dhcpv4StatusCode::from_code(status),
message.to_vec(),
))
}
fn encode_client_uuid(uuid: &Dhcpv4ClientUuid) -> Vec<u8> {
let mut bytes = Vec::with_capacity(1 + uuid.identifier.len());
bytes.push(uuid.identifier_type);
bytes.extend_from_slice(&uuid.identifier);
bytes
}
fn decode_vi_vendor_class(data: &[u8]) -> Result<Vec<Dhcpv4VendorClassData>> {
let field = "dhcpv4.option.vi_vendor_class";
let mut instances = Vec::new();
let mut offset = 0usize;
while offset < data.len() {
let enterprise_number = read_enterprise_number(field, data, offset)?;
offset += DHCPV4_ENTERPRISE_NUMBER_LEN;
if offset >= data.len() {
return Err(CrafterError::buffer_too_short(
field,
offset + 1,
data.len(),
));
}
let len = data[offset] as usize;
offset += 1;
let end = offset + len;
if end > data.len() {
return Err(CrafterError::buffer_too_short(field, end, data.len()));
}
instances.push(Dhcpv4VendorClassData::new(
enterprise_number,
data[offset..end].to_vec(),
));
offset = end;
}
Ok(instances)
}
fn encode_vi_vendor_class(instances: &[Dhcpv4VendorClassData]) -> Vec<u8> {
let mut bytes = Vec::new();
for instance in instances {
bytes.extend_from_slice(&instance.enterprise_number.to_be_bytes());
let len = instance.data.len().min(DHCPV4_MAX_OPTION_DATA_LEN);
bytes.push(len as u8);
bytes.extend_from_slice(&instance.data[..len]);
}
bytes
}
fn decode_vi_vendor_specific(data: &[u8]) -> Result<Vec<Dhcpv4VendorIdentifyingOption>> {
let field = "dhcpv4.option.vi_vendor_specific";
let mut instances = Vec::new();
let mut offset = 0usize;
while offset < data.len() {
let enterprise_number = read_enterprise_number(field, data, offset)?;
offset += DHCPV4_ENTERPRISE_NUMBER_LEN;
if offset >= data.len() {
return Err(CrafterError::buffer_too_short(
field,
offset + 1,
data.len(),
));
}
let len = data[offset] as usize;
offset += 1;
let end = offset + len;
if end > data.len() {
return Err(CrafterError::buffer_too_short(field, end, data.len()));
}
let suboptions = decode_vendor_suboptions(field, &data[offset..end])?;
instances.push(Dhcpv4VendorIdentifyingOption::new(
enterprise_number,
suboptions,
));
offset = end;
}
Ok(instances)
}
fn decode_vendor_suboptions(
field: &'static str,
data: &[u8],
) -> Result<Vec<Dhcpv4VendorSuboption>> {
let mut suboptions = Vec::new();
let mut offset = 0usize;
while offset < data.len() {
let code = data[offset];
offset += 1;
if offset >= data.len() {
return Err(CrafterError::buffer_too_short(
field,
offset + 1,
data.len(),
));
}
let len = data[offset] as usize;
offset += 1;
let end = offset + len;
if end > data.len() {
return Err(CrafterError::buffer_too_short(field, end, data.len()));
}
suboptions.push(Dhcpv4VendorSuboption::new(code, data[offset..end].to_vec()));
offset = end;
}
Ok(suboptions)
}
fn encode_vi_vendor_specific(instances: &[Dhcpv4VendorIdentifyingOption]) -> Vec<u8> {
let mut bytes = Vec::new();
for instance in instances {
bytes.extend_from_slice(&instance.enterprise_number.to_be_bytes());
let option_data = encode_vendor_suboptions(&instance.suboptions);
let len = option_data.len().min(DHCPV4_MAX_OPTION_DATA_LEN);
bytes.push(len as u8);
bytes.extend_from_slice(&option_data[..len]);
}
bytes
}
fn encode_vendor_suboptions(suboptions: &[Dhcpv4VendorSuboption]) -> Vec<u8> {
let mut bytes = Vec::new();
for suboption in suboptions {
bytes.push(suboption.code);
let len = suboption.data.len().min(DHCPV4_MAX_OPTION_DATA_LEN);
bytes.push(len as u8);
bytes.extend_from_slice(&suboption.data[..len]);
}
bytes
}
fn read_enterprise_number(field: &'static str, data: &[u8], offset: usize) -> Result<u32> {
let end = offset + DHCPV4_ENTERPRISE_NUMBER_LEN;
if end > data.len() {
return Err(CrafterError::buffer_too_short(field, end, data.len()));
}
read_u32_be(&data[offset..end])
}
fn decode_relay_agent_information(data: &[u8]) -> Result<Dhcpv4RelayAgentInfo> {
let field = "dhcpv4.option.relay_agent_information";
let mut suboptions = Vec::new();
let mut offset = 0usize;
while offset < data.len() {
let code = data[offset];
offset += 1;
if offset >= data.len() {
return Err(CrafterError::buffer_too_short(
field,
offset + 1,
data.len(),
));
}
let len = data[offset] as usize;
offset += 1;
let end = offset + len;
if end > data.len() {
return Err(CrafterError::buffer_too_short(field, end, data.len()));
}
let value = &data[offset..end];
suboptions.push(decode_relay_suboption(field, code, value)?);
offset = end;
}
Ok(Dhcpv4RelayAgentInfo::new(suboptions))
}
fn decode_relay_suboption(
field: &'static str,
code: u8,
value: &[u8],
) -> Result<Dhcpv4RelaySuboption> {
let suboption = match code {
DHCPV4_RELAY_SUBOPTION_CIRCUIT_ID => Dhcpv4RelaySuboption::CircuitId(value.to_vec()),
DHCPV4_RELAY_SUBOPTION_REMOTE_ID => Dhcpv4RelaySuboption::RemoteId(value.to_vec()),
DHCPV4_RELAY_SUBOPTION_DOCSIS_DEVICE_CLASS => {
validate_fixed_len(field, value.len(), 4)?;
Dhcpv4RelaySuboption::DocsisDeviceClass(read_u32_be(value)?)
}
DHCPV4_RELAY_SUBOPTION_LINK_SELECTION => {
Dhcpv4RelaySuboption::LinkSelection(decode_ipv4_option(field, value)?)
}
DHCPV4_RELAY_SUBOPTION_SUBSCRIBER_ID => Dhcpv4RelaySuboption::SubscriberId(value.to_vec()),
DHCPV4_RELAY_SUBOPTION_RADIUS_ATTRIBUTES => {
Dhcpv4RelaySuboption::RadiusAttributes(value.to_vec())
}
DHCPV4_RELAY_SUBOPTION_AUTHENTICATION => {
Dhcpv4RelaySuboption::Authentication(value.to_vec())
}
DHCPV4_RELAY_SUBOPTION_VENDOR_SPECIFIC => {
Dhcpv4RelaySuboption::VendorSpecific(decode_relay_vendor_specific(field, value)?)
}
DHCPV4_RELAY_SUBOPTION_RELAY_FLAGS => {
validate_fixed_len(field, value.len(), 1)?;
Dhcpv4RelaySuboption::RelayFlags(value[0])
}
DHCPV4_RELAY_SUBOPTION_SERVER_ID_OVERRIDE => {
Dhcpv4RelaySuboption::ServerIdOverride(decode_ipv4_option(field, value)?)
}
DHCPV4_RELAY_SUBOPTION_RELAY_AGENT_ID => Dhcpv4RelaySuboption::RelayAgentId(value.to_vec()),
DHCPV4_RELAY_SUBOPTION_RELAY_SOURCE_PORT => {
validate_fixed_len(field, value.len(), 0)?;
Dhcpv4RelaySuboption::RelaySourcePort
}
DHCPV4_RELAY_SUBOPTION_VSS => {
if value.is_empty() {
return Err(CrafterError::buffer_too_short(field, 1, value.len()));
}
Dhcpv4RelaySuboption::Vss(Dhcpv4VssInfo::new(value[0], value[1..].to_vec()))
}
DHCPV4_RELAY_SUBOPTION_VSS_CONTROL => {
validate_fixed_len(field, value.len(), 0)?;
Dhcpv4RelaySuboption::VssControl
}
_ => Dhcpv4RelaySuboption::Other {
code,
data: value.to_vec(),
},
};
Ok(suboption)
}
fn decode_relay_vendor_specific(
field: &'static str,
data: &[u8],
) -> Result<Vec<Dhcpv4RelayVendorSpecific>> {
let mut tuples = Vec::new();
let mut offset = 0usize;
while offset < data.len() {
let enterprise_number = read_enterprise_number(field, data, offset)?;
offset += DHCPV4_ENTERPRISE_NUMBER_LEN;
if offset >= data.len() {
return Err(CrafterError::buffer_too_short(
field,
offset + 1,
data.len(),
));
}
let len = data[offset] as usize;
offset += 1;
let end = offset + len;
if end > data.len() {
return Err(CrafterError::buffer_too_short(field, end, data.len()));
}
tuples.push(Dhcpv4RelayVendorSpecific::new(
enterprise_number,
data[offset..end].to_vec(),
));
offset = end;
}
Ok(tuples)
}
fn encode_relay_agent_information(info: &Dhcpv4RelayAgentInfo) -> Vec<u8> {
let mut bytes = Vec::new();
for suboption in &info.suboptions {
let value = suboption.encode_value();
let len = value.len().min(DHCPV4_MAX_OPTION_DATA_LEN);
bytes.push(suboption.code());
bytes.push(len as u8);
bytes.extend_from_slice(&value[..len]);
}
bytes
}
fn encode_relay_vendor_specific(tuples: &[Dhcpv4RelayVendorSpecific]) -> Vec<u8> {
let mut bytes = Vec::new();
for tuple in tuples {
bytes.extend_from_slice(&tuple.enterprise_number.to_be_bytes());
let len = tuple.data.len().min(DHCPV4_MAX_OPTION_DATA_LEN);
bytes.push(len as u8);
bytes.extend_from_slice(&tuple.data[..len]);
}
bytes
}
pub(super) const DHCPV4_MAX_OPTION_DATA_LEN: usize = u8::MAX as usize;
pub(super) fn encode_split_option(code: u8, data: &[u8], out: &mut Vec<u8>) {
if data.is_empty() {
out.push(code);
out.push(0);
return;
}
for chunk in data.chunks(DHCPV4_MAX_OPTION_DATA_LEN) {
out.push(code);
out.push(chunk.len() as u8);
out.extend_from_slice(chunk);
}
}
pub(super) fn split_option_encoded_len(data_len: usize) -> usize {
if data_len == 0 {
return 2;
}
let segments = data_len.div_ceil(DHCPV4_MAX_OPTION_DATA_LEN);
segments * 2 + data_len
}
#[cfg(test)]
mod dhcpv4_options {
use super::super::{
scan_dhcpv4_option_segments, Dhcpv4, Dhcpv4MessageType, Dhcpv4Option, Dhcpv4OptionArea,
Dhcpv4OptionCode, Dhcpv4OptionSegment, Dhcpv4OptionStatus, Dhcpv4OptionValue,
};
use crate::error::CrafterError;
use core::net::Ipv4Addr;
const OFFER_OPTIONS: &str = fixture_str!("bytes/dhcpv4-offer-options.hex");
#[test]
fn option_fixture_decodes_common_offer_values() {
let options = Dhcpv4Option::decode_all(&hex_fixture(OFFER_OPTIONS)).unwrap();
let dhcpv4 = Dhcpv4::new().options(options);
assert_eq!(dhcpv4.message_type_value(), Some(Dhcpv4MessageType::Offer));
assert_eq!(
dhcpv4.server_identifier_value(),
Some(Ipv4Addr::new(192, 0, 2, 1))
);
assert_eq!(
dhcpv4.subnet_mask_value(),
Some(Ipv4Addr::new(255, 255, 255, 0))
);
assert_eq!(dhcpv4.routers(), vec![Ipv4Addr::new(192, 0, 2, 1)]);
assert_eq!(
dhcpv4.domain_name_servers(),
vec![
Ipv4Addr::new(192, 0, 2, 53),
Ipv4Addr::new(198, 51, 100, 53)
]
);
assert_eq!(dhcpv4.lease_time_value(), Some(3600));
}
#[test]
fn typed_options_roundtrip_and_preserve_unknown_options() {
let options = vec![
Dhcpv4Option::Pad,
Dhcpv4Option::message_type(Dhcpv4MessageType::Ack),
Dhcpv4Option::host_name("agent-host"),
Dhcpv4Option::generic(224, [0xde, 0xad, 0xbe, 0xef]),
Dhcpv4Option::End,
Dhcpv4Option::Pad,
];
let encoded = Dhcpv4::new()
.options(options.clone())
.encoded_options()
.unwrap();
let decoded = Dhcpv4Option::decode_all(&encoded).unwrap();
assert_eq!(decoded, options);
}
#[test]
fn builder_appends_end_marker_deterministically() {
let encoded = Dhcpv4::new()
.message_type(Dhcpv4MessageType::Discover)
.encoded_options()
.unwrap();
assert_eq!(encoded.last(), Some(&super::DHCPV4_OPTION_END));
}
#[test]
fn dhcpv4_offer_options_decode_through_new_model() {
let bytes = hex_fixture(OFFER_OPTIONS);
let segments = scan_dhcpv4_option_segments(Dhcpv4OptionArea::Options, &bytes).unwrap();
let codes: Vec<u8> = segments.iter().map(|s| s.code_value()).collect();
assert_eq!(codes, vec![53, 54, 1, 3, 6, 51, 255]);
assert!(segments.iter().all(|s| s.area == Dhcpv4OptionArea::Options));
let message_type = &segments[0];
assert_eq!(message_type.code, Dhcpv4OptionCode::Assigned(53));
assert_eq!(message_type.declared_len, Some(1));
assert_eq!(message_type.offset, 0);
assert_eq!(message_type.data, vec![Dhcpv4MessageType::Offer.code()]);
let server_id = &segments[1];
assert_eq!(server_id.declared_len, Some(4));
assert_eq!(server_id.offset, 3);
assert_eq!(server_id.data, vec![192, 0, 2, 1]);
let end = segments.last().unwrap();
assert_eq!(end.code, Dhcpv4OptionCode::End);
assert!(end.is_single_octet());
assert_eq!(end.declared_len, None);
let options = Dhcpv4Option::decode_all(&bytes).unwrap();
let logical: Vec<Option<Dhcpv4OptionValue>> =
options.iter().map(Dhcpv4Option::logical_value).collect();
assert_eq!(
logical[0],
Some(Dhcpv4OptionValue::MessageType(Dhcpv4MessageType::Offer))
);
assert_eq!(
logical[2],
Some(Dhcpv4OptionValue::Ipv4(Ipv4Addr::new(255, 255, 255, 0)))
);
assert_eq!(
logical[4],
Some(Dhcpv4OptionValue::Ipv4List(vec![
Ipv4Addr::new(192, 0, 2, 53),
Ipv4Addr::new(198, 51, 100, 53),
]))
);
assert_eq!(logical[5], Some(Dhcpv4OptionValue::U32(3_600)));
assert_eq!(options.last().unwrap().logical_value(), None);
assert_eq!(options[0].registry_name(), Some("DHCP Msg Type"));
assert_eq!(options[1].registry_name(), Some("DHCP Server Id"));
}
#[test]
fn dhcpv4_option_model_preserves_unknown_private_bytes() {
let private_payload = [0xde, 0xad, 0xbe, 0xef];
let removed_payload = [0x01, 0x02];
let options = vec![
Dhcpv4Option::message_type(Dhcpv4MessageType::Ack),
Dhcpv4Option::generic(224, private_payload),
Dhcpv4Option::generic(84, removed_payload),
Dhcpv4Option::End,
];
let encoded = Dhcpv4::new()
.options(options.clone())
.encoded_options()
.unwrap();
let decoded = Dhcpv4Option::decode_all(&encoded).unwrap();
assert_eq!(decoded, options);
assert_eq!(decoded[1].option_code(), Dhcpv4OptionCode::PrivateUse(224));
assert_eq!(
decoded[2].option_code(),
Dhcpv4OptionCode::RemovedOrUnassigned(84)
);
assert_eq!(decoded[1].registry_name(), None);
assert_eq!(decoded[2].registry_name(), Some("REMOVED/Unassigned"));
assert_eq!(
decoded[1].logical_value(),
Some(Dhcpv4OptionValue::Opaque(private_payload.to_vec()))
);
assert_eq!(
decoded[1]
.logical_value()
.and_then(|v| v.as_bytes().map(<[u8]>::to_vec)),
Some(private_payload.to_vec())
);
let segments = scan_dhcpv4_option_segments(Dhcpv4OptionArea::Options, &encoded).unwrap();
let private = segments
.iter()
.find(|s| s.code_value() == 224)
.expect("private-use segment present");
assert_eq!(private.code, Dhcpv4OptionCode::PrivateUse(224));
assert_eq!(private.declared_len, Some(private_payload.len() as u8));
assert_eq!(private.data, private_payload);
let removed = segments
.iter()
.find(|s| s.code_value() == 84)
.expect("removed segment present");
assert_eq!(removed.code, Dhcpv4OptionCode::RemovedOrUnassigned(84));
assert_eq!(removed.data, removed_payload);
assert_eq!(
Dhcpv4OptionCode::from_code(224),
Dhcpv4OptionCode::PrivateUse(224)
);
assert_eq!(
super::dhcpv4_option_status(84),
Dhcpv4OptionStatus::RemovedOrUnassigned
);
}
#[test]
fn dhcpv4_option_codec_preserves_raw_segments() {
let options = vec![
Dhcpv4Option::Pad,
Dhcpv4Option::message_type(Dhcpv4MessageType::Ack),
Dhcpv4Option::subnet_mask(Ipv4Addr::new(255, 255, 255, 0)),
Dhcpv4Option::generic(224, [0xde, 0xad, 0xbe, 0xef]),
Dhcpv4Option::End,
Dhcpv4Option::Pad,
];
let encoded = Dhcpv4::new()
.options(options.clone())
.encoded_options()
.unwrap();
let segments = scan_dhcpv4_option_segments(Dhcpv4OptionArea::Options, &encoded).unwrap();
let codes: Vec<u8> = segments
.iter()
.map(Dhcpv4OptionSegment::code_value)
.collect();
assert_eq!(codes, vec![0, 53, 1, 224, 255, 0]);
assert!(segments.iter().all(|s| s.area == Dhcpv4OptionArea::Options));
let private = &segments[3];
assert_eq!(private.code, Dhcpv4OptionCode::PrivateUse(224));
assert_eq!(private.declared_len, Some(4));
assert_eq!(private.data, vec![0xde, 0xad, 0xbe, 0xef]);
let end = &segments[4];
assert_eq!(end.code, Dhcpv4OptionCode::End);
assert!(end.is_single_octet());
assert_eq!(end.declared_len, None);
assert_eq!(segments[5].code, Dhcpv4OptionCode::Pad);
let decoded = Dhcpv4Option::decode_all(&encoded).unwrap();
assert_eq!(decoded, options);
let re_encoded = Dhcpv4::new().options(decoded).encoded_options().unwrap();
assert_eq!(re_encoded, encoded);
}
#[test]
fn dhcpv4_option_codec_rejects_non_padding_after_end() {
let bytes = [
super::DHCPV4_OPTION_END,
super::DHCPV4_OPTION_MESSAGE_TYPE,
1,
1,
];
let error = Dhcpv4Option::decode_all(&bytes).unwrap_err();
assert!(matches!(
error,
CrafterError::InvalidFieldValue { field, .. } if field == "dhcpv4.option.end"
));
let segments = scan_dhcpv4_option_segments(Dhcpv4OptionArea::Options, &bytes).unwrap();
let codes: Vec<u8> = segments
.iter()
.map(Dhcpv4OptionSegment::code_value)
.collect();
assert_eq!(codes, vec![255, 53]);
}
#[test]
fn dhcpv4_option_codec_rejects_missing_end_marker() {
let bytes = [super::DHCPV4_OPTION_MESSAGE_TYPE, 1, 1];
let error = Dhcpv4Option::decode_all(&bytes).unwrap_err();
assert!(matches!(
error,
CrafterError::InvalidFieldValue { field, .. } if field == "dhcpv4.options"
));
}
#[test]
fn dhcpv4_option_codec_reports_truncated_segments() {
let truncated_length = [super::DHCPV4_OPTION_MESSAGE_TYPE];
let truncated_data = [super::DHCPV4_OPTION_MESSAGE_TYPE, 4, 0x01];
for bytes in [truncated_length.as_slice(), truncated_data.as_slice()] {
let error = scan_dhcpv4_option_segments(Dhcpv4OptionArea::Options, bytes).unwrap_err();
assert!(matches!(error, CrafterError::BufferTooShort { .. }));
assert!(matches!(
Dhcpv4Option::decode_all(bytes),
Err(CrafterError::BufferTooShort { .. })
));
}
}
#[test]
fn dhcpv4_option_codec_preserves_pad_and_unknown_options() {
let options = vec![
Dhcpv4Option::Pad,
Dhcpv4Option::Pad,
Dhcpv4Option::message_type(Dhcpv4MessageType::Discover),
Dhcpv4Option::generic(84, [0x01, 0x02]),
Dhcpv4Option::End,
];
let encoded = Dhcpv4::new()
.options(options.clone())
.encoded_options()
.unwrap();
let decoded = Dhcpv4Option::decode_all(&encoded).unwrap();
assert_eq!(decoded, options);
assert_eq!(
decoded[3].option_code(),
Dhcpv4OptionCode::RemovedOrUnassigned(84)
);
assert_eq!(
decoded[3].logical_value(),
Some(Dhcpv4OptionValue::Opaque(vec![0x01, 0x02]))
);
}
fn hex_fixture(input: &str) -> Vec<u8> {
input
.split_whitespace()
.map(|byte| u8::from_str_radix(byte, 16).unwrap())
.collect()
}
}
#[cfg(test)]
mod dhcpv4_rfc3396 {
use super::super::{
scan_dhcpv4_option_segments, Dhcpv4, Dhcpv4MessageType, Dhcpv4Option, Dhcpv4OptionArea,
};
use super::{encode_split_option, split_option_encoded_len, DHCPV4_MAX_OPTION_DATA_LEN};
use core::net::Ipv4Addr;
const HOST_NAME: u8 = super::super::DHCPV4_OPTION_HOST_NAME;
const DOMAIN_SEARCH: u8 = 119; const VENDOR_CLASS: u8 = 60;
fn build_options(payload_options: Vec<Dhcpv4Option>) -> Vec<u8> {
Dhcpv4::new()
.options(payload_options)
.encoded_options()
.unwrap()
}
#[test]
fn dhcpv4_rfc3396_concatenates_repeated_option_segments() {
let part_one = vec![b'a'; DHCPV4_MAX_OPTION_DATA_LEN]; let part_two = vec![b'b'; 40];
let mut full = part_one.clone();
full.extend_from_slice(&part_two);
let mut wire = Vec::new();
wire.push(HOST_NAME);
wire.push(part_one.len() as u8);
wire.extend_from_slice(&part_one);
wire.push(HOST_NAME);
wire.push(part_two.len() as u8);
wire.extend_from_slice(&part_two);
wire.push(super::super::DHCPV4_OPTION_END);
let segments = scan_dhcpv4_option_segments(Dhcpv4OptionArea::Options, &wire).unwrap();
let host_segments: Vec<&super::Dhcpv4OptionSegment> = segments
.iter()
.filter(|s| s.code_value() == HOST_NAME)
.collect();
assert_eq!(host_segments.len(), 2);
assert_eq!(host_segments[0].data, part_one);
assert_eq!(host_segments[1].data, part_two);
let decoded = Dhcpv4Option::decode_all(&wire).unwrap();
let host_options: Vec<&Dhcpv4Option> =
decoded.iter().filter(|o| o.code() == HOST_NAME).collect();
assert_eq!(host_options.len(), 1);
match host_options[0] {
Dhcpv4Option::HostName(name) => {
assert_eq!(name.as_bytes(), full.as_slice());
assert_eq!(name.len(), DHCPV4_MAX_OPTION_DATA_LEN + 40);
}
other => panic!("expected concatenated host name, got {other:?}"),
}
}
#[test]
fn dhcpv4_rfc3396_encoder_splits_long_payloads() {
let long_name = "x".repeat(600);
let encoded = build_options(vec![
Dhcpv4Option::host_name(long_name.clone()),
Dhcpv4Option::End,
]);
let segments = scan_dhcpv4_option_segments(Dhcpv4OptionArea::Options, &encoded).unwrap();
let host_segments: Vec<&super::Dhcpv4OptionSegment> = segments
.iter()
.filter(|s| s.code_value() == HOST_NAME)
.collect();
assert_eq!(host_segments.len(), 3);
assert_eq!(host_segments[0].data.len(), DHCPV4_MAX_OPTION_DATA_LEN);
assert_eq!(host_segments[1].data.len(), DHCPV4_MAX_OPTION_DATA_LEN);
assert_eq!(host_segments[2].data.len(), 90);
assert!(host_segments
.iter()
.all(|s| s.declared_len.unwrap() as usize <= DHCPV4_MAX_OPTION_DATA_LEN));
let decoded = Dhcpv4Option::decode_all(&encoded).unwrap();
let host = decoded
.iter()
.find(|o| o.code() == HOST_NAME)
.expect("host name present");
assert_eq!(host, &Dhcpv4Option::host_name(long_name));
let re_encoded = Dhcpv4::new().options(decoded).encoded_options().unwrap();
assert_eq!(re_encoded, encoded);
}
#[test]
fn dhcpv4_rfc3396_exact_255_boundary_is_a_single_segment() {
let exactly_255 = vec![0xABu8; DHCPV4_MAX_OPTION_DATA_LEN];
let mut out = Vec::new();
encode_split_option(VENDOR_CLASS, &exactly_255, &mut out);
assert_eq!(out.len(), 2 + DHCPV4_MAX_OPTION_DATA_LEN);
assert_eq!(out[0], VENDOR_CLASS);
assert_eq!(out[1], 255);
let just_over = vec![0xCDu8; DHCPV4_MAX_OPTION_DATA_LEN + 1];
let mut out = Vec::new();
encode_split_option(VENDOR_CLASS, &just_over, &mut out);
let segments = scan_dhcpv4_option_segments(Dhcpv4OptionArea::Options, &out).unwrap();
assert_eq!(segments.len(), 2);
assert_eq!(segments[0].data.len(), DHCPV4_MAX_OPTION_DATA_LEN);
assert_eq!(segments[1].data.len(), 1);
assert_eq!(
split_option_encoded_len(DHCPV4_MAX_OPTION_DATA_LEN),
2 + DHCPV4_MAX_OPTION_DATA_LEN
);
assert_eq!(
split_option_encoded_len(DHCPV4_MAX_OPTION_DATA_LEN + 1),
2 + DHCPV4_MAX_OPTION_DATA_LEN + 2 + 1
);
assert_eq!(split_option_encoded_len(0), 2);
}
#[test]
fn dhcpv4_rfc3396_multi_segment_roundtrip_for_long_vendor_and_message_payloads() {
let payload = (0u16..700).map(|n| n as u8).collect::<Vec<u8>>();
let option = Dhcpv4Option::generic(VENDOR_CLASS, payload.clone());
assert_eq!(option.encode().unwrap().len(), option.encoded_len());
let encoded = build_options(vec![option.clone(), Dhcpv4Option::End]);
let decoded = Dhcpv4Option::decode_all(&encoded).unwrap();
let vendor = decoded
.iter()
.find(|o| o.code() == VENDOR_CLASS)
.expect("vendor option present");
assert_eq!(vendor.payload().unwrap(), payload);
let re_encoded = Dhcpv4::new().options(decoded).encoded_options().unwrap();
assert_eq!(re_encoded, encoded);
}
#[test]
fn dhcpv4_rfc3396_concatenates_across_overloaded_areas() {
let dhcp = Dhcpv4::new()
.message_type(Dhcpv4MessageType::Ack)
.server_identifier(Ipv4Addr::new(192, 0, 2, 1))
.option(Dhcpv4Option::generic(DOMAIN_SEARCH, b"aaa".to_vec()))
.file_option(Dhcpv4Option::generic(DOMAIN_SEARCH, b"bbb".to_vec()))
.file_option(Dhcpv4Option::End)
.sname_option(Dhcpv4Option::generic(DOMAIN_SEARCH, b"ccc".to_vec()))
.sname_option(Dhcpv4Option::End);
let bytes = crate::Packet::from_layer(dhcp)
.compile()
.unwrap()
.as_bytes()
.to_vec();
let parsed = Dhcpv4::decode(&bytes).unwrap();
assert!(parsed
.options_value()
.iter()
.any(|o| o.code() == DOMAIN_SEARCH));
assert!(parsed
.file_options_value()
.iter()
.any(|o| o.code() == DOMAIN_SEARCH));
assert!(parsed
.sname_options_value()
.iter()
.any(|o| o.code() == DOMAIN_SEARCH));
let joined = parsed
.concatenated_option(DOMAIN_SEARCH)
.expect("option present in some area")
.expect("decodes cleanly");
assert_eq!(joined.payload().unwrap(), b"aaabbbccc".to_vec());
assert!(parsed.concatenated_option(200).is_none());
}
}
#[cfg(test)]
mod dhcpv4_rfc2132_base_options {
use super::super::{
Dhcpv4, Dhcpv4ClientIdentifier, Dhcpv4MessageType, Dhcpv4Option, Dhcpv4OptionCode,
Dhcpv4OptionFormat, Dhcpv4OptionKind, Dhcpv4OptionValue, OptionOverload,
};
use super::dhcpv4_typed_option_value;
use crate::error::CrafterError;
use core::net::Ipv4Addr;
fn ip(a: u8, b: u8, c: u8, d: u8) -> Ipv4Addr {
Ipv4Addr::new(a, b, c, d)
}
fn family_samples() -> Vec<(u8, Dhcpv4OptionValue)> {
vec![
(1, Dhcpv4OptionValue::Ipv4(ip(255, 255, 255, 0))),
(16, Dhcpv4OptionValue::Ipv4(ip(192, 0, 2, 9))),
(32, Dhcpv4OptionValue::Ipv4(ip(192, 0, 2, 7))),
(
6,
Dhcpv4OptionValue::Ipv4List(vec![ip(192, 0, 2, 53), ip(198, 51, 100, 53)]),
),
(42, Dhcpv4OptionValue::Ipv4List(vec![ip(192, 0, 2, 123)])),
(
44,
Dhcpv4OptionValue::Ipv4List(vec![ip(192, 0, 2, 200), ip(192, 0, 2, 201)]),
),
(
21,
Dhcpv4OptionValue::Ipv4Pairs(vec![(ip(192, 0, 2, 0), ip(255, 255, 255, 0))]),
),
(
33,
Dhcpv4OptionValue::StaticRoutes(vec![
super::Dhcpv4StaticRoute::new(ip(198, 51, 100, 0), ip(192, 0, 2, 1)),
super::Dhcpv4StaticRoute::new(ip(203, 0, 113, 0), ip(192, 0, 2, 2)),
]),
),
(19, Dhcpv4OptionValue::Bool(true)),
(27, Dhcpv4OptionValue::Bool(false)),
(39, Dhcpv4OptionValue::Bool(true)),
(23, Dhcpv4OptionValue::U8(64)),
(46, Dhcpv4OptionValue::U8(8)),
(13, Dhcpv4OptionValue::U16(1024)),
(22, Dhcpv4OptionValue::U16(576)),
(26, Dhcpv4OptionValue::U16(1500)),
(57, Dhcpv4OptionValue::U16(1400)),
(25, Dhcpv4OptionValue::U16List(vec![68, 296, 1500])),
(2, Dhcpv4OptionValue::I32(-18_000)),
(24, Dhcpv4OptionValue::U32(1_200)),
(35, Dhcpv4OptionValue::U32(60)),
(51, Dhcpv4OptionValue::U32(86_400)),
(12, Dhcpv4OptionValue::Text(b"agent-host".to_vec())),
(40, Dhcpv4OptionValue::Text(b"corp.example".to_vec())),
(56, Dhcpv4OptionValue::Text(b"lease denied".to_vec())),
(
55,
Dhcpv4OptionValue::ParameterRequestList(vec![1, 3, 6, 15, 51, 54]),
),
(
53,
Dhcpv4OptionValue::MessageType(Dhcpv4MessageType::Discover),
),
(52, Dhcpv4OptionValue::OptionOverload(OptionOverload::Both)),
(60, Dhcpv4OptionValue::Opaque(b"MSFT 5.0".to_vec())),
(
61,
Dhcpv4OptionValue::ClientIdentifier(Dhcpv4ClientIdentifier::ethernet_mac([
0x02, 0x00, 0x5e, 0x10, 0x00, 0x01,
])),
),
(43, Dhcpv4OptionValue::Opaque(vec![0xde, 0xad, 0xbe, 0xef])),
]
}
#[test]
fn dhcpv4_rfc2132_base_options_cover_format_families() {
let samples = family_samples();
use std::collections::HashSet;
let covered: HashSet<Dhcpv4OptionFormat> = samples
.iter()
.map(|(code, _)| {
Dhcpv4OptionKind::from_code(*code)
.expect("sample code is a registered base option")
.format()
})
.collect();
let all_families = [
Dhcpv4OptionFormat::Ipv4,
Dhcpv4OptionFormat::Ipv4List,
Dhcpv4OptionFormat::Ipv4Pairs,
Dhcpv4OptionFormat::Bool,
Dhcpv4OptionFormat::U8,
Dhcpv4OptionFormat::U16,
Dhcpv4OptionFormat::U16List,
Dhcpv4OptionFormat::I32,
Dhcpv4OptionFormat::U32,
Dhcpv4OptionFormat::Text,
Dhcpv4OptionFormat::ParameterRequestList,
Dhcpv4OptionFormat::MessageType,
Dhcpv4OptionFormat::OptionOverload,
Dhcpv4OptionFormat::ClientIdentifier,
Dhcpv4OptionFormat::Opaque,
];
for family in all_families {
assert!(
covered.contains(&family),
"format family {family:?} is not exercised by the sample set",
);
}
for (code, value) in samples {
let payload = value.encode_payload();
let decoded = dhcpv4_typed_option_value(code, &payload)
.unwrap()
.unwrap_or_else(|| panic!("code {code} has no typed value"));
assert_eq!(decoded, value, "typed decode mismatch for code {code}");
let kind = Dhcpv4OptionKind::from_code(code).unwrap();
assert_eq!(kind.code(), code);
let option = Dhcpv4Option::typed(kind, value.clone());
assert_eq!(option.code(), code);
assert_eq!(option.kind(), Some(kind));
assert_eq!(option.typed_value().unwrap(), Some(value.clone()));
assert!(matches!(
Dhcpv4OptionCode::from_code(code),
Dhcpv4OptionCode::Assigned(_)
));
}
}
#[test]
fn dhcpv4_rfc2132_base_options_roundtrip() {
let samples = family_samples();
let mut options: Vec<Dhcpv4Option> = samples
.iter()
.filter(|(code, _)| *code != 52)
.map(|(code, value)| {
Dhcpv4Option::typed(Dhcpv4OptionKind::from_code(*code).unwrap(), value.clone())
})
.collect();
options.push(Dhcpv4Option::End);
let dhcp = Dhcpv4::new()
.op(super::super::BOOTP_REPLY)
.options(options.clone());
let bytes = crate::Packet::from_layer(dhcp)
.compile()
.unwrap()
.as_bytes()
.to_vec();
let parsed = Dhcpv4::decode(&bytes).unwrap();
let recompiled = crate::Packet::from_layer(parsed.clone())
.compile()
.unwrap()
.as_bytes()
.to_vec();
assert_eq!(recompiled, bytes);
for (code, value) in samples.iter().filter(|(code, _)| *code != 52) {
let option = parsed
.options_value()
.iter()
.find(|o| o.code() == *code)
.unwrap_or_else(|| panic!("option {code} present after decode"));
assert_eq!(
option.typed_value().unwrap(),
Some(value.clone()),
"typed value lost for code {code} after round-trip",
);
}
let raw_text = Dhcpv4Option::typed(
Dhcpv4OptionKind::HostName,
Dhcpv4OptionValue::Text(vec![0xff, 0xfe, b'x']),
);
let dhcp = Dhcpv4::new().options([raw_text, Dhcpv4Option::End]);
let bytes = crate::Packet::from_layer(dhcp)
.compile()
.unwrap()
.as_bytes()
.to_vec();
let parsed = Dhcpv4::decode(&bytes).unwrap();
let host = parsed
.options_value()
.iter()
.find(|o| o.code() == 12)
.unwrap();
assert_eq!(
host.typed_value().unwrap(),
Some(Dhcpv4OptionValue::Text(vec![0xff, 0xfe, b'x'])),
);
}
#[test]
fn dhcpv4_rfc2132_base_options_reject_malformed_lengths() {
for (code, bad) in [
(19u8, vec![2u8]), (51, vec![0, 0, 1]), (6, vec![192, 0, 2]), (33, vec![192, 0, 2, 1]), (25, vec![0]), ] {
let error = dhcpv4_typed_option_value(code, &bad).unwrap_err();
assert!(
matches!(error, CrafterError::InvalidFieldValue { .. }),
"code {code} should yield a structured field error",
);
}
assert!(dhcpv4_typed_option_value(224, &[0xde, 0xad])
.unwrap()
.is_none());
assert!(dhcpv4_typed_option_value(100, &[0x01, 0x00])
.unwrap()
.is_none());
}
}
#[cfg(test)]
mod dhcpv4_route_domain_service {
use super::super::{
Dhcpv4, Dhcpv4ClasslessRoute, Dhcpv4MessageType, Dhcpv4Option, Dhcpv4OptionKind,
Dhcpv4OptionValue, Dhcpv4StaticRoute, SipServers,
};
use super::{
decode_classless_routes, decode_domain_name_list, decode_static_routes,
dhcpv4_typed_option_value, encode_classless_routes, encode_domain_name_list,
encode_static_routes,
};
use crate::error::CrafterError;
use core::net::Ipv4Addr;
const STATIC_ROUTE: u8 = super::super::DHCPV4_OPTION_STATIC_ROUTE; const DOMAIN_SEARCH: u8 = super::super::DHCPV4_OPTION_DOMAIN_SEARCH; const SIP_SERVERS: u8 = super::super::DHCPV4_OPTION_SIP_SERVERS; const CLASSLESS_ROUTE: u8 = super::super::DHCPV4_OPTION_CLASSLESS_STATIC_ROUTE;
fn ip(a: u8, b: u8, c: u8, d: u8) -> Ipv4Addr {
Ipv4Addr::new(a, b, c, d)
}
fn build_and_decode(option: Dhcpv4Option) -> Dhcpv4 {
let dhcp = Dhcpv4::new()
.op(super::super::BOOTP_REPLY)
.message_type(Dhcpv4MessageType::Ack)
.options([option, Dhcpv4Option::End]);
let bytes = crate::Packet::from_layer(dhcp)
.compile()
.unwrap()
.as_bytes()
.to_vec();
Dhcpv4::decode(&bytes).unwrap()
}
#[test]
fn dhcpv4_classless_routes_roundtrip() {
let routes = vec![
Dhcpv4ClasslessRoute::new(24, ip(198, 51, 100, 0), ip(192, 0, 2, 1)),
Dhcpv4ClasslessRoute::new(0, ip(0, 0, 0, 0), ip(192, 0, 2, 254)),
Dhcpv4ClasslessRoute::new(32, ip(203, 0, 113, 7), ip(192, 0, 2, 9)),
Dhcpv4ClasslessRoute::new(16, ip(172, 16, 0, 0), ip(192, 0, 2, 8)),
];
let value = Dhcpv4OptionValue::ClasslessRoutes(routes.clone());
let payload = value.encode_payload();
let expected: Vec<u8> = vec![
24, 198, 51, 100, 192, 0, 2, 1, 0, 192, 0, 2, 254, 32, 203, 0, 113, 7, 192, 0, 2, 9, 16, 172, 16, 192, 0, 2, 8, ];
assert_eq!(payload, expected);
let decoded = dhcpv4_typed_option_value(CLASSLESS_ROUTE, &payload)
.unwrap()
.unwrap();
assert_eq!(decoded, value);
assert_eq!(
Dhcpv4ClasslessRoute::significant_octets(24),
3,
"ceil(24/8) significant octets",
);
let option = Dhcpv4Option::typed(Dhcpv4OptionKind::ClasslessStaticRoute, value.clone());
assert_eq!(option.code(), CLASSLESS_ROUTE);
let parsed = build_and_decode(option);
assert_eq!(parsed.classless_static_routes().unwrap().unwrap(), routes);
let bytes = crate::Packet::from_layer(parsed.clone())
.compile()
.unwrap()
.as_bytes()
.to_vec();
let recompiled = crate::Packet::from_layer(Dhcpv4::decode(&bytes).unwrap())
.compile()
.unwrap()
.as_bytes()
.to_vec();
assert_eq!(recompiled, bytes);
assert_eq!(decode_classless_routes(&payload).unwrap(), routes);
assert_eq!(encode_classless_routes(&routes), payload);
}
#[test]
fn dhcpv4_static_routes_roundtrip() {
let routes = vec![
Dhcpv4StaticRoute::new(ip(198, 51, 100, 0), ip(192, 0, 2, 1)),
Dhcpv4StaticRoute::new(ip(203, 0, 113, 0), ip(192, 0, 2, 2)),
];
let value = Dhcpv4OptionValue::StaticRoutes(routes.clone());
let payload = value.encode_payload();
assert_eq!(payload.len(), routes.len() * 8);
let decoded = dhcpv4_typed_option_value(STATIC_ROUTE, &payload)
.unwrap()
.unwrap();
assert_eq!(decoded, value);
let option = Dhcpv4Option::typed(Dhcpv4OptionKind::StaticRoute, value);
let parsed = build_and_decode(option);
assert_eq!(parsed.static_routes().unwrap().unwrap(), routes);
assert_eq!(decode_static_routes(&payload).unwrap(), routes);
assert_eq!(encode_static_routes(&routes), payload);
}
#[test]
fn dhcpv4_domain_search_roundtrip() {
let names = vec!["eng.example.com".to_string(), "example.net".to_string()];
let value = Dhcpv4OptionValue::DomainSearch(names.clone());
let payload = value.encode_payload();
let expected: Vec<u8> = {
let mut bytes = Vec::new();
for name in &names {
for label in name.split('.') {
bytes.push(label.len() as u8);
bytes.extend_from_slice(label.as_bytes());
}
bytes.push(0);
}
bytes
};
assert_eq!(payload, expected);
let decoded = dhcpv4_typed_option_value(DOMAIN_SEARCH, &payload)
.unwrap()
.unwrap();
assert_eq!(decoded, value);
let option = Dhcpv4Option::typed(Dhcpv4OptionKind::DomainSearch, value);
let parsed = build_and_decode(option);
assert_eq!(parsed.domain_search().unwrap().unwrap(), names);
let mut compressed = Vec::new();
compressed.extend_from_slice(&[3, b'e', b'n', b'g']); compressed.extend_from_slice(&[7, b'e', b'x', b'a', b'm', b'p', b'l', b'e']); compressed.extend_from_slice(&[3, b'c', b'o', b'm', 0]); let pointer_to_example = compressed.len(); compressed.extend_from_slice(&[9, b'm', b'a', b'r', b'k', b'e', b't', b'i', b'n', b'g']);
compressed.push(0xC0);
compressed.push(4);
let _ = pointer_to_example;
let resolved = decode_domain_name_list("dhcpv4.option.domain_search", &compressed).unwrap();
assert_eq!(
resolved,
vec![
"eng.example.com".to_string(),
"marketing.example.com".to_string(),
],
);
let fqdn = encode_domain_name_list(&["host.example.com.".to_string()]);
assert_eq!(
decode_domain_name_list("dhcpv4.option.domain_search", &fqdn).unwrap(),
vec!["host.example.com".to_string()],
);
}
#[test]
fn dhcpv4_sip_servers_roundtrip_both_encodings() {
let domains = SipServers::DomainNames(vec![
"sip.example.com".to_string(),
"sip.example.net".to_string(),
]);
let domain_payload = Dhcpv4OptionValue::SipServers(domains.clone()).encode_payload();
assert_eq!(domain_payload[0], 0, "enc byte selects domain names");
let decoded = dhcpv4_typed_option_value(SIP_SERVERS, &domain_payload)
.unwrap()
.unwrap();
assert_eq!(decoded, Dhcpv4OptionValue::SipServers(domains.clone()));
let addresses = SipServers::Addresses(vec![ip(192, 0, 2, 10), ip(198, 51, 100, 10)]);
let address_payload = Dhcpv4OptionValue::SipServers(addresses.clone()).encode_payload();
assert_eq!(address_payload[0], 1, "enc byte selects addresses");
let decoded = dhcpv4_typed_option_value(SIP_SERVERS, &address_payload)
.unwrap()
.unwrap();
assert_eq!(decoded, Dhcpv4OptionValue::SipServers(addresses.clone()));
let unknown = dhcpv4_typed_option_value(SIP_SERVERS, &[0x09, 0xde, 0xad])
.unwrap()
.unwrap();
assert_eq!(
unknown,
Dhcpv4OptionValue::SipServers(SipServers::Unknown {
encoding: 0x09,
data: vec![0xde, 0xad],
}),
);
let option = Dhcpv4Option::typed(
Dhcpv4OptionKind::SipServers,
Dhcpv4OptionValue::SipServers(addresses.clone()),
);
let parsed = build_and_decode(option);
assert_eq!(parsed.sip_servers().unwrap().unwrap(), addresses);
}
#[test]
fn dhcpv4_route_domain_service_malformed_inputs() {
let bad_prefix = [33u8, 10, 0, 0, 192, 0, 2, 1];
assert!(matches!(
decode_classless_routes(&bad_prefix),
Err(CrafterError::InvalidFieldValue { field, .. }) if field == "dhcpv4.option.classless_static_route",
));
let truncated_route = [24u8, 10, 0, 0, 192, 0]; assert!(matches!(
decode_classless_routes(&truncated_route),
Err(CrafterError::BufferTooShort { .. }),
));
assert!(dhcpv4_typed_option_value(CLASSLESS_ROUTE, &truncated_route).is_err());
assert!(matches!(
decode_static_routes(&[192, 0, 2, 1, 192, 0, 2]),
Err(CrafterError::InvalidFieldValue { field, .. }) if field == "dhcpv4.option.static_route",
));
let truncated_label = [5u8, b'a', b'b']; assert!(matches!(
decode_domain_name_list("dhcpv4.option.domain_search", &truncated_label),
Err(CrafterError::BufferTooShort { .. }),
));
let bad_pointer = [0xC0u8, 0x40];
assert!(matches!(
decode_domain_name_list("dhcpv4.option.domain_search", &bad_pointer),
Err(CrafterError::InvalidFieldValue { .. }),
));
let pointer_loop = [0xC0u8, 0x00];
assert!(matches!(
decode_domain_name_list("dhcpv4.option.domain_search", &pointer_loop),
Err(CrafterError::InvalidFieldValue { .. }),
));
assert!(matches!(
dhcpv4_typed_option_value(SIP_SERVERS, &[]),
Err(CrafterError::BufferTooShort { .. }),
));
assert!(matches!(
dhcpv4_typed_option_value(SIP_SERVERS, &[1, 192, 0, 2]),
Err(CrafterError::InvalidFieldValue { .. }),
));
}
}
#[cfg(test)]
mod dhcpv4_vendor_user_pxe {
use super::super::{
decode_dhcpv4_tftp_server_addresses, ClientNetworkDeviceInterface,
ClientSystemArchitecture, Dhcpv4, Dhcpv4ClientUuid, Dhcpv4MessageType, Dhcpv4Option,
Dhcpv4UserClass, Dhcpv4VendorClassData, Dhcpv4VendorIdentifyingOption,
Dhcpv4VendorSuboption,
};
use super::{
decode_client_ndi, decode_client_system_architecture, decode_client_uuid,
decode_user_class, decode_vi_vendor_class, decode_vi_vendor_specific,
dhcpv4_typed_option_value, Dhcpv4OptionValue,
};
use crate::error::CrafterError;
use core::net::Ipv4Addr;
const VENDOR_SPECIFIC: u8 = super::super::DHCPV4_OPTION_VENDOR_SPECIFIC; const VENDOR_CLASS_ID: u8 = super::super::DHCPV4_OPTION_VENDOR_CLASS_IDENTIFIER; const TFTP_SERVER_NAME: u8 = super::super::DHCPV4_OPTION_TFTP_SERVER_NAME; const BOOTFILE_NAME: u8 = super::super::DHCPV4_OPTION_BOOTFILE_NAME; const USER_CLASS: u8 = super::super::DHCPV4_OPTION_USER_CLASS; const CLIENT_ARCH: u8 = super::super::DHCPV4_OPTION_CLIENT_SYSTEM_ARCHITECTURE; const CLIENT_NDI: u8 = super::super::DHCPV4_OPTION_CLIENT_NDI; const CLIENT_UUID: u8 = super::super::DHCPV4_OPTION_CLIENT_MACHINE_IDENTIFIER; const VI_VENDOR_CLASS: u8 = super::super::DHCPV4_OPTION_VI_VENDOR_CLASS; const VI_VENDOR_SPECIFIC: u8 = super::super::DHCPV4_OPTION_VI_VENDOR_SPECIFIC; const TFTP_SERVER_ADDRESS: u8 = super::super::DHCPV4_OPTION_TFTP_SERVER_ADDRESS;
fn ip(a: u8, b: u8, c: u8, d: u8) -> Ipv4Addr {
Ipv4Addr::new(a, b, c, d)
}
fn build_and_decode(options: Vec<Dhcpv4Option>) -> Dhcpv4 {
let dhcp = Dhcpv4::new()
.op(super::super::BOOTP_REQUEST)
.message_type(Dhcpv4MessageType::Discover)
.options(options);
let bytes = crate::Packet::from_layer(dhcp)
.compile()
.unwrap()
.as_bytes()
.to_vec();
Dhcpv4::decode(&bytes).unwrap()
}
fn recompile_is_stable(parsed: &Dhcpv4) {
let bytes = crate::Packet::from_layer(parsed.clone())
.compile()
.unwrap()
.as_bytes()
.to_vec();
let recompiled = crate::Packet::from_layer(Dhcpv4::decode(&bytes).unwrap())
.compile()
.unwrap()
.as_bytes()
.to_vec();
assert_eq!(recompiled, bytes);
}
#[test]
fn dhcpv4_vendor_identifying_options_roundtrip() {
let vivco = vec![
Dhcpv4VendorClassData::new(3561, b"PXEClient:Arch:00009".to_vec()),
Dhcpv4VendorClassData::new(311, vec![0xde, 0xad, 0xbe, 0xef]),
];
let vivco_value = Dhcpv4OptionValue::ViVendorClass(vivco.clone());
let payload = vivco_value.encode_payload();
let mut expected: Vec<u8> = Vec::new();
expected.extend_from_slice(&3561u32.to_be_bytes());
expected.push(20);
expected.extend_from_slice(b"PXEClient:Arch:00009");
expected.extend_from_slice(&311u32.to_be_bytes());
expected.push(4);
expected.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]);
assert_eq!(payload, expected);
assert_eq!(
dhcpv4_typed_option_value(VI_VENDOR_CLASS, &payload)
.unwrap()
.unwrap(),
vivco_value,
);
assert_eq!(decode_vi_vendor_class(&payload).unwrap(), vivco);
let vivso = vec![Dhcpv4VendorIdentifyingOption::new(
3561,
vec![
Dhcpv4VendorSuboption::new(1, vec![0x01, 0x02, 0x03]),
Dhcpv4VendorSuboption::new(2, b"opaque".to_vec()),
],
)];
let vivso_value = Dhcpv4OptionValue::ViVendorSpecific(vivso.clone());
let payload = vivso_value.encode_payload();
let mut expected: Vec<u8> = Vec::new();
expected.extend_from_slice(&3561u32.to_be_bytes());
expected.push(13);
expected.extend_from_slice(&[1, 3, 0x01, 0x02, 0x03]);
expected.push(2);
expected.push(6);
expected.extend_from_slice(b"opaque");
assert_eq!(payload, expected);
assert_eq!(
dhcpv4_typed_option_value(VI_VENDOR_SPECIFIC, &payload)
.unwrap()
.unwrap(),
vivso_value,
);
assert_eq!(decode_vi_vendor_specific(&payload).unwrap(), vivso);
let parsed = build_and_decode(vec![
Dhcpv4Option::vi_vendor_class(vivco.clone()),
Dhcpv4Option::vi_vendor_specific(vivso.clone()),
Dhcpv4Option::End,
]);
assert_eq!(parsed.vi_vendor_class().unwrap().unwrap(), vivco);
assert_eq!(parsed.vi_vendor_specific().unwrap().unwrap(), vivso);
recompile_is_stable(&parsed);
let opaque = vec![0x01, 0xff, 0x00, 0xab];
let parsed = build_and_decode(vec![
Dhcpv4Option::vendor_specific(opaque.clone()),
Dhcpv4Option::vendor_class_identifier(b"MSFT 5.0".to_vec()),
Dhcpv4Option::End,
]);
assert_eq!(parsed.vendor_specific_information().unwrap(), opaque);
assert_eq!(
parsed.vendor_class_identifier().unwrap(),
b"MSFT 5.0".to_vec(),
);
assert!(matches!(
dhcpv4_typed_option_value(VI_VENDOR_CLASS, &3561u32.to_be_bytes()),
Err(CrafterError::BufferTooShort { .. }),
));
let bad = {
let mut bytes = 3561u32.to_be_bytes().to_vec();
bytes.push(5);
bytes.extend_from_slice(&[1, 2]);
bytes
};
assert!(matches!(
decode_vi_vendor_class(&bad),
Err(CrafterError::BufferTooShort { .. }),
));
let bad_subopt = {
let mut bytes = 3561u32.to_be_bytes().to_vec();
bytes.push(3); bytes.extend_from_slice(&[7, 9, 0xaa]); bytes
};
assert!(matches!(
decode_vi_vendor_specific(&bad_subopt),
Err(CrafterError::BufferTooShort { .. }),
));
}
#[test]
fn dhcpv4_pxe_architecture_options_roundtrip() {
let arch = ClientSystemArchitecture::new(vec![0u16, 7, 9]);
let arch_value = Dhcpv4OptionValue::ClientSystemArchitecture(arch.clone());
let payload = arch_value.encode_payload();
assert_eq!(payload, vec![0, 0, 0, 7, 0, 9]);
assert_eq!(
dhcpv4_typed_option_value(CLIENT_ARCH, &payload)
.unwrap()
.unwrap(),
arch_value,
);
assert_eq!(decode_client_system_architecture(&payload).unwrap(), arch);
assert!(matches!(
dhcpv4_typed_option_value(CLIENT_ARCH, &[0, 7, 9]),
Err(CrafterError::InvalidFieldValue { .. }),
));
let ndi = ClientNetworkDeviceInterface::undi(2, 1);
let ndi_value = Dhcpv4OptionValue::ClientNetworkDeviceInterface(ndi);
let payload = ndi_value.encode_payload();
assert_eq!(payload, vec![1, 2, 1]);
assert_eq!(
dhcpv4_typed_option_value(CLIENT_NDI, &payload)
.unwrap()
.unwrap(),
ndi_value,
);
assert_eq!(decode_client_ndi(&payload).unwrap(), ndi);
assert!(matches!(
dhcpv4_typed_option_value(CLIENT_NDI, &[1, 2]),
Err(CrafterError::InvalidFieldValue { .. }),
));
let guid: Vec<u8> = (0u8..16).collect();
let uuid = Dhcpv4ClientUuid::guid(guid.clone());
let uuid_value = Dhcpv4OptionValue::ClientUuid(uuid.clone());
let payload = uuid_value.encode_payload();
assert_eq!(payload.len(), 17);
assert_eq!(payload[0], 0);
assert_eq!(&payload[1..], guid.as_slice());
assert_eq!(
dhcpv4_typed_option_value(CLIENT_UUID, &payload)
.unwrap()
.unwrap(),
uuid_value,
);
assert_eq!(decode_client_uuid(&payload).unwrap(), uuid);
assert!(matches!(
dhcpv4_typed_option_value(CLIENT_UUID, &[]),
Err(CrafterError::BufferTooShort { .. }),
));
let parsed = build_and_decode(vec![
Dhcpv4Option::client_system_architecture(arch.clone()),
Dhcpv4Option::client_network_device_interface(ndi),
Dhcpv4Option::client_uuid(uuid.clone()),
Dhcpv4Option::pxelinux_magic(),
Dhcpv4Option::pxelinux_config_file(b"pxelinux.cfg/default".to_vec()),
Dhcpv4Option::pxelinux_path_prefix(b"tftp://192.0.2.1/".to_vec()),
Dhcpv4Option::pxelinux_reboot_time(30),
Dhcpv4Option::End,
]);
assert_eq!(parsed.client_system_architecture().unwrap().unwrap(), arch,);
assert_eq!(
parsed.client_network_device_interface().unwrap().unwrap(),
ndi,
);
assert_eq!(parsed.client_uuid().unwrap().unwrap(), uuid);
assert_eq!(
parsed.pxelinux_magic().unwrap(),
vec![0xF1, 0x00, 0x74, 0x7E]
);
assert_eq!(
parsed.pxelinux_config_file().unwrap(),
b"pxelinux.cfg/default".to_vec(),
);
assert_eq!(
parsed.pxelinux_path_prefix().unwrap(),
b"tftp://192.0.2.1/".to_vec(),
);
assert_eq!(parsed.pxelinux_reboot_time().unwrap().unwrap(), 30);
recompile_is_stable(&parsed);
}
#[test]
fn dhcpv4_user_class_roundtrip() {
let user_class = Dhcpv4UserClass::new(vec![b"iPXE".to_vec(), b"linux-install".to_vec()]);
let value = Dhcpv4OptionValue::UserClass(user_class.clone());
let payload = value.encode_payload();
let mut expected = vec![4u8];
expected.extend_from_slice(b"iPXE");
expected.push(13);
expected.extend_from_slice(b"linux-install");
assert_eq!(payload, expected);
assert_eq!(
dhcpv4_typed_option_value(USER_CLASS, &payload)
.unwrap()
.unwrap(),
value,
);
assert_eq!(decode_user_class(&payload).unwrap(), user_class);
let parsed = build_and_decode(vec![
Dhcpv4Option::user_class(user_class.clone()),
Dhcpv4Option::End,
]);
assert_eq!(parsed.user_class().unwrap().unwrap(), user_class);
recompile_is_stable(&parsed);
assert!(matches!(
decode_user_class(&[0]),
Err(CrafterError::InvalidFieldValue { .. }),
));
assert!(matches!(
decode_user_class(&[5, b'a', b'b']),
Err(CrafterError::BufferTooShort { .. }),
));
}
#[test]
fn dhcpv4_tftp_and_bootfile_options_roundtrip() {
let parsed = build_and_decode(vec![
Dhcpv4Option::tftp_server_name(b"tftp.example.com".to_vec()),
Dhcpv4Option::bootfile_name(b"undionly.kpxe".to_vec()),
Dhcpv4Option::End,
]);
assert_eq!(
parsed.tftp_server_name().unwrap(),
b"tftp.example.com".to_vec(),
);
assert_eq!(parsed.bootfile_name().unwrap(), b"undionly.kpxe".to_vec());
recompile_is_stable(&parsed);
let addresses = vec![ip(192, 0, 2, 10), ip(198, 51, 100, 10)];
let parsed = build_and_decode(vec![
Dhcpv4Option::tftp_server_addresses(addresses.clone()),
Dhcpv4Option::End,
]);
assert_eq!(parsed.tftp_server_addresses().unwrap().unwrap(), addresses);
assert!(
dhcpv4_typed_option_value(TFTP_SERVER_ADDRESS, &[192, 0, 2, 10])
.unwrap()
.is_none()
);
recompile_is_stable(&parsed);
assert!(matches!(
decode_dhcpv4_tftp_server_addresses(&[192, 0, 2]),
Err(CrafterError::InvalidFieldValue { .. }),
));
assert!(matches!(
decode_dhcpv4_tftp_server_addresses(&[]),
Err(CrafterError::InvalidFieldValue { .. }),
));
assert_eq!(VENDOR_SPECIFIC, 43);
assert_eq!(VENDOR_CLASS_ID, 60);
assert_eq!(TFTP_SERVER_NAME, 66);
assert_eq!(BOOTFILE_NAME, 67);
}
}
#[cfg(test)]
mod dhcpv4_relay_agent {
use super::super::{
scan_dhcpv4_option_segments, Dhcpv4, Dhcpv4MessageType, Dhcpv4Option, Dhcpv4OptionArea,
Dhcpv4RelayAgentInfo, Dhcpv4RelaySuboption, Dhcpv4RelayVendorSpecific, Dhcpv4VssInfo,
};
use super::{decode_relay_agent_information, dhcpv4_typed_option_value, Dhcpv4OptionValue};
use crate::error::CrafterError;
use core::net::Ipv4Addr;
const RELAY_AGENT_INFORMATION: u8 = super::super::DHCPV4_OPTION_RELAY_AGENT_INFORMATION;
fn ip(a: u8, b: u8, c: u8, d: u8) -> Ipv4Addr {
Ipv4Addr::new(a, b, c, d)
}
fn build_and_decode(options: Vec<Dhcpv4Option>) -> Dhcpv4 {
let dhcp = Dhcpv4::new()
.op(super::super::BOOTP_REPLY)
.message_type(Dhcpv4MessageType::Ack)
.options(options);
let bytes = crate::Packet::from_layer(dhcp)
.compile()
.unwrap()
.as_bytes()
.to_vec();
Dhcpv4::decode(&bytes).unwrap()
}
fn recompile_is_stable(parsed: &Dhcpv4) {
let bytes = crate::Packet::from_layer(parsed.clone())
.compile()
.unwrap()
.as_bytes()
.to_vec();
let recompiled = crate::Packet::from_layer(Dhcpv4::decode(&bytes).unwrap())
.compile()
.unwrap()
.as_bytes()
.to_vec();
assert_eq!(recompiled, bytes);
}
#[test]
fn dhcpv4_relay_agent_option82_roundtrips_suboptions() {
let info = Dhcpv4RelayAgentInfo::new(vec![
Dhcpv4RelaySuboption::circuit_id(b"eth0:vlan100".to_vec()),
Dhcpv4RelaySuboption::remote_id(vec![0x00, 0x0c, 0x29, 0xab, 0xcd, 0xef]),
Dhcpv4RelaySuboption::DocsisDeviceClass(0x0000_0001),
Dhcpv4RelaySuboption::LinkSelection(ip(192, 0, 2, 0)),
Dhcpv4RelaySuboption::subscriber_id(b"sub-42".to_vec()),
Dhcpv4RelaySuboption::RadiusAttributes(vec![0x01, 0x06, 0x00, 0x00, 0x00, 0x2a]),
Dhcpv4RelaySuboption::Authentication(vec![0x01, 0x00, 0x00, 0x00]),
Dhcpv4RelaySuboption::VendorSpecific(vec![
Dhcpv4RelayVendorSpecific::new(3561, vec![0xde, 0xad]),
Dhcpv4RelayVendorSpecific::new(311, b"v".to_vec()),
]),
Dhcpv4RelaySuboption::RelayFlags(super::super::DHCPV4_RELAY_FLAG_UNICAST),
Dhcpv4RelaySuboption::ServerIdOverride(ip(192, 0, 2, 1)),
Dhcpv4RelaySuboption::relay_agent_id(vec![0xab, 0xcd]),
Dhcpv4RelaySuboption::RelaySourcePort,
Dhcpv4RelaySuboption::Vss(Dhcpv4VssInfo::nvt_ascii(b"vpn-blue".to_vec())),
Dhcpv4RelaySuboption::VssControl,
Dhcpv4RelaySuboption::other(200, vec![0x01, 0x02, 0x03]),
]);
let value = Dhcpv4OptionValue::RelayAgentInformation(info.clone());
let payload = value.encode_payload();
assert_eq!(
decode_relay_agent_information(&payload).unwrap(),
info,
"relay agent information must round-trip through the codec",
);
assert_eq!(
dhcpv4_typed_option_value(RELAY_AGENT_INFORMATION, &payload)
.unwrap()
.unwrap(),
value,
);
let relay_port = info.suboption(19).unwrap().encode_value();
assert!(relay_port.is_empty(), "relay source port carries no value");
let parsed = build_and_decode(vec![
Dhcpv4Option::relay_agent_information(info.clone()),
Dhcpv4Option::End,
]);
let decoded = parsed.relay_agent_information().unwrap().unwrap();
assert_eq!(decoded, info);
assert_eq!(
decoded.suboption(200),
Some(&Dhcpv4RelaySuboption::other(200, vec![0x01, 0x02, 0x03])),
);
recompile_is_stable(&parsed);
}
#[test]
fn dhcpv4_relay_agent_option82_rejects_truncated_suboption() {
let truncated = [1u8, 5, 0xaa, 0xbb];
assert!(matches!(
decode_relay_agent_information(&truncated),
Err(CrafterError::BufferTooShort { .. }),
));
assert!(dhcpv4_typed_option_value(RELAY_AGENT_INFORMATION, &truncated).is_err());
let no_len = [1u8];
assert!(matches!(
decode_relay_agent_information(&no_len),
Err(CrafterError::BufferTooShort { .. }),
));
assert!(matches!(
decode_relay_agent_information(&[5, 3, 192, 0, 2]),
Err(CrafterError::InvalidFieldValue { .. }),
));
assert!(matches!(
decode_relay_agent_information(&[19, 2, 0x00, 0x43]),
Err(CrafterError::InvalidFieldValue { .. }),
));
}
#[test]
fn dhcpv4_relay_agent_option82_unknown_suboptions_preserved() {
let info = Dhcpv4RelayAgentInfo::new(vec![
Dhcpv4RelaySuboption::other(3, vec![0xca, 0xfe]),
Dhcpv4RelaySuboption::other(99, b"private".to_vec()),
]);
let payload = Dhcpv4OptionValue::RelayAgentInformation(info.clone()).encode_payload();
assert_eq!(
payload,
vec![3, 2, 0xca, 0xfe, 99, 7, b'p', b'r', b'i', b'v', b'a', b't', b'e'],
);
assert_eq!(decode_relay_agent_information(&payload).unwrap(), info);
let parsed = build_and_decode(vec![
Dhcpv4Option::relay_agent_information(info.clone()),
Dhcpv4Option::End,
]);
assert_eq!(parsed.relay_agent_information().unwrap().unwrap(), info);
recompile_is_stable(&parsed);
}
#[test]
fn dhcpv4_relay_agent_option82_overload_and_long_options() {
let info = Dhcpv4RelayAgentInfo::new(vec![
Dhcpv4RelaySuboption::circuit_id(b"port-7".to_vec()),
Dhcpv4RelaySuboption::ServerIdOverride(ip(198, 51, 100, 1)),
]);
let dhcp = Dhcpv4::new()
.op(super::super::BOOTP_REPLY)
.message_type(Dhcpv4MessageType::Ack)
.file_options(vec![Dhcpv4Option::relay_agent_information(info.clone())]);
let bytes = crate::Packet::from_layer(dhcp)
.compile()
.unwrap()
.as_bytes()
.to_vec();
let parsed = Dhcpv4::decode(&bytes).unwrap();
assert_eq!(parsed.relay_agent_information().unwrap().unwrap(), info);
recompile_is_stable(&parsed);
let big_info = Dhcpv4RelayAgentInfo::new(vec![
Dhcpv4RelaySuboption::circuit_id(vec![0x5au8; 200]),
Dhcpv4RelaySuboption::remote_id(vec![0xa5u8; 200]),
]);
let payload = Dhcpv4OptionValue::RelayAgentInformation(big_info.clone()).encode_payload();
assert!(payload.len() > 255, "payload must exceed one segment");
let parsed = build_and_decode(vec![
Dhcpv4Option::relay_agent_information(big_info.clone()),
Dhcpv4Option::End,
]);
let encoded = parsed.encoded_options().unwrap();
let segments = scan_dhcpv4_option_segments(Dhcpv4OptionArea::Options, &encoded)
.unwrap()
.into_iter()
.filter(|seg| seg.code_value() == RELAY_AGENT_INFORMATION)
.count();
assert!(
segments >= 2,
"an over-long relay option must split into multiple wire segments",
);
assert_eq!(parsed.relay_agent_information().unwrap().unwrap(), big_info);
recompile_is_stable(&parsed);
assert_eq!(RELAY_AGENT_INFORMATION, 82);
}
}
#[cfg(test)]
mod dhcpv4_client_identifier {
use super::super::{
Dhcpv4, Dhcpv4ClientIdentifier, Dhcpv4MessageType, Dhcpv4Option, Dhcpv4OptionValue,
};
use super::{decode_client_identifier, dhcpv4_typed_option_value};
use crate::error::CrafterError;
const CLIENT_IDENTIFIER: u8 = super::super::DHCPV4_OPTION_CLIENT_IDENTIFIER;
fn build_and_decode(option: Dhcpv4Option) -> Dhcpv4 {
let dhcp = Dhcpv4::new()
.op(super::super::BOOTP_REQUEST)
.message_type(Dhcpv4MessageType::Request)
.options(vec![option, Dhcpv4Option::End]);
let bytes = crate::Packet::from_layer(dhcp)
.compile()
.unwrap()
.as_bytes()
.to_vec();
Dhcpv4::decode(&bytes).unwrap()
}
fn recompile_is_stable(parsed: &Dhcpv4) {
let bytes = crate::Packet::from_layer(parsed.clone())
.compile()
.unwrap()
.as_bytes()
.to_vec();
let recompiled = crate::Packet::from_layer(Dhcpv4::decode(&bytes).unwrap())
.compile()
.unwrap()
.as_bytes()
.to_vec();
assert_eq!(recompiled, bytes);
}
#[test]
fn dhcpv4_client_identifier_legacy_mac_roundtrip() {
let mac = [0x02, 0x00, 0x5e, 0x10, 0x00, 0x01];
let identifier = Dhcpv4ClientIdentifier::ethernet_mac(mac);
assert_eq!(identifier.type_octet(), Some(1));
let value = Dhcpv4OptionValue::ClientIdentifier(identifier.clone());
let payload = value.encode_payload();
assert_eq!(payload, vec![0x01, 0x02, 0x00, 0x5e, 0x10, 0x00, 0x01]);
assert_eq!(decode_client_identifier(&payload).unwrap(), identifier);
assert_eq!(
dhcpv4_typed_option_value(CLIENT_IDENTIFIER, &payload)
.unwrap()
.unwrap(),
value,
);
let parsed = build_and_decode(Dhcpv4Option::client_identifier_value(identifier.clone()));
assert_eq!(
parsed.client_identifier_value().unwrap().unwrap(),
identifier,
);
recompile_is_stable(&parsed);
let raw = build_and_decode(Dhcpv4Option::client_identifier(payload.clone()));
assert_eq!(raw.client_identifier_value().unwrap().unwrap(), identifier);
}
#[test]
fn dhcpv4_client_identifier_rfc4361_roundtrip() {
let iaid = 0x0102_0304u32;
let duid = vec![
0x00, 0x01, 0x00, 0x01, 0x12, 0x34, 0x56, 0x78, 0x02, 0x00, 0x5e, 0x10, 0x00, 0x01, ];
let identifier = Dhcpv4ClientIdentifier::node_specific(iaid, duid.clone());
assert_eq!(identifier.type_octet(), Some(255));
let value = Dhcpv4OptionValue::ClientIdentifier(identifier.clone());
let payload = value.encode_payload();
assert_eq!(payload[0], 255, "RFC 4361 identifier uses type 255");
assert_eq!(&payload[1..5], &iaid.to_be_bytes(), "IAID is 4 octets");
assert_eq!(&payload[5..], duid.as_slice(), "DUID follows the IAID");
assert_eq!(decode_client_identifier(&payload).unwrap(), identifier);
assert_eq!(
dhcpv4_typed_option_value(CLIENT_IDENTIFIER, &payload)
.unwrap()
.unwrap(),
value,
);
let parsed = build_and_decode(Dhcpv4Option::client_identifier_value(identifier.clone()));
assert_eq!(
parsed.client_identifier_value().unwrap().unwrap(),
identifier,
);
recompile_is_stable(&parsed);
}
#[test]
fn dhcpv4_client_identifier_raw_unknown_forms_preserved() {
let fqdn_payload = {
let mut bytes = vec![0u8];
bytes.extend_from_slice(b"host.example.com");
bytes
};
let decoded = decode_client_identifier(&fqdn_payload).unwrap();
assert_eq!(decoded, Dhcpv4ClientIdentifier::Raw(fqdn_payload.clone()));
let parsed = build_and_decode(Dhcpv4Option::client_identifier(fqdn_payload.clone()));
assert_eq!(
parsed.client_identifier_value().unwrap().unwrap(),
Dhcpv4ClientIdentifier::Raw(fqdn_payload),
);
assert_eq!(
decode_client_identifier(&[]).unwrap(),
Dhcpv4ClientIdentifier::Raw(Vec::new()),
);
let raw = Dhcpv4ClientIdentifier::raw(vec![0x07, 0xaa, 0xbb]);
assert_eq!(raw.encode(), vec![0x07, 0xaa, 0xbb]);
assert_eq!(raw.type_octet(), Some(0x07));
let truncated = [255u8, 0x01, 0x02, 0x03];
assert!(matches!(
decode_client_identifier(&truncated),
Err(CrafterError::BufferTooShort { .. }),
));
}
#[test]
fn dhcpv4_client_identifier_server_reply_echo() {
let identifier =
Dhcpv4ClientIdentifier::node_specific(0xdead_beef, vec![0x00, 0x03, 0x00, 0x01, 0x11]);
let request_payload =
Dhcpv4OptionValue::ClientIdentifier(identifier.clone()).encode_payload();
let reply = Dhcpv4::new()
.op(super::super::BOOTP_REPLY)
.message_type(Dhcpv4MessageType::Ack)
.options(vec![
Dhcpv4Option::client_identifier_value(identifier.clone()),
Dhcpv4Option::End,
]);
let bytes = crate::Packet::from_layer(reply)
.compile()
.unwrap()
.as_bytes()
.to_vec();
let parsed = Dhcpv4::decode(&bytes).unwrap();
let echoed = parsed.client_identifier_value().unwrap().unwrap();
assert_eq!(echoed, identifier);
assert_eq!(echoed.encode(), request_payload);
recompile_is_stable(&parsed);
assert_eq!(CLIENT_IDENTIFIER, 61);
}
}
#[cfg(test)]
mod dhcpv4_authentication {
use super::super::{
Dhcpv4, Dhcpv4AuthAlgorithm, Dhcpv4AuthProtocol, Dhcpv4Authentication,
Dhcpv4ForcerenewNonceCapable, Dhcpv4MessageType, Dhcpv4Option, Dhcpv4OptionValue,
Dhcpv4ReplayDetectionMethod,
};
use super::{decode_authentication, dhcpv4_typed_option_value};
use crate::error::CrafterError;
const AUTHENTICATION: u8 = super::super::DHCPV4_OPTION_AUTHENTICATION; const FORCERENEW_NONCE_CAPABLE: u8 = super::super::DHCPV4_OPTION_FORCERENEW_NONCE_CAPABLE;
fn build_and_decode(options: Vec<Dhcpv4Option>) -> Dhcpv4 {
let dhcp = Dhcpv4::new()
.op(super::super::BOOTP_REQUEST)
.message_type(Dhcpv4MessageType::Request)
.options(options);
let bytes = crate::Packet::from_layer(dhcp)
.compile()
.unwrap()
.as_bytes()
.to_vec();
Dhcpv4::decode(&bytes).unwrap()
}
fn recompile_is_stable(parsed: &Dhcpv4) {
let bytes = crate::Packet::from_layer(parsed.clone())
.compile()
.unwrap()
.as_bytes()
.to_vec();
let recompiled = crate::Packet::from_layer(Dhcpv4::decode(&bytes).unwrap())
.compile()
.unwrap()
.as_bytes()
.to_vec();
assert_eq!(recompiled, bytes);
}
#[test]
fn dhcpv4_authentication_option_roundtrip() {
let secret_id = [0x00, 0x00, 0x00, 0x2a];
let digest = [0xABu8; 16];
let mut auth_info = Vec::new();
auth_info.extend_from_slice(&secret_id);
auth_info.extend_from_slice(&digest);
let auth = Dhcpv4Authentication::new(
Dhcpv4AuthProtocol::Delayed,
Dhcpv4AuthAlgorithm::HmacMd5,
Dhcpv4ReplayDetectionMethod::MonotonicCounter,
0x0102_0304_0506_0708,
auth_info.clone(),
);
let value = Dhcpv4OptionValue::Authentication(auth.clone());
let payload = value.encode_payload();
let mut expected = vec![1u8, 1, 0]; expected.extend_from_slice(&0x0102_0304_0506_0708u64.to_be_bytes());
expected.extend_from_slice(&auth_info);
assert_eq!(payload, expected);
assert_eq!(decode_authentication(&payload).unwrap(), auth);
assert_eq!(
dhcpv4_typed_option_value(AUTHENTICATION, &payload)
.unwrap()
.unwrap(),
value,
);
let parsed = build_and_decode(vec![
Dhcpv4Option::authentication(auth.clone()),
Dhcpv4Option::End,
]);
let decoded = parsed.authentication().unwrap().unwrap();
assert_eq!(decoded, auth);
assert_eq!(decoded.authentication_information, auth_info);
recompile_is_stable(&parsed);
assert_eq!(AUTHENTICATION, 90);
}
#[test]
fn dhcpv4_authentication_unknown_codes_preserved() {
let auth = Dhcpv4Authentication::new(
Dhcpv4AuthProtocol::Unknown(0x7f),
Dhcpv4AuthAlgorithm::Unknown(0x42),
Dhcpv4ReplayDetectionMethod::Unknown(0x99),
0,
vec![0xde, 0xad, 0xbe, 0xef],
);
let payload = Dhcpv4OptionValue::Authentication(auth.clone()).encode_payload();
assert_eq!(payload[0], 0x7f, "unknown Protocol preserved");
assert_eq!(payload[1], 0x42, "unknown Algorithm preserved");
assert_eq!(payload[2], 0x99, "unknown RDM preserved");
let decoded = decode_authentication(&payload).unwrap();
assert_eq!(decoded, auth);
assert_eq!(decoded.protocol.code(), 0x7f);
assert_eq!(decoded.algorithm.code(), 0x42);
assert_eq!(decoded.rdm.code(), 0x99);
let parsed = build_and_decode(vec![
Dhcpv4Option::authentication(auth.clone()),
Dhcpv4Option::End,
]);
assert_eq!(parsed.authentication().unwrap().unwrap(), auth);
recompile_is_stable(&parsed);
}
#[test]
fn dhcpv4_authentication_malformed_lengths_are_structured() {
for len in 0..super::super::DHCPV4_AUTH_HEADER_LEN {
let short = vec![0u8; len];
assert!(
matches!(
decode_authentication(&short),
Err(CrafterError::BufferTooShort { .. }),
),
"len {len} must be rejected",
);
assert!(dhcpv4_typed_option_value(AUTHENTICATION, &short).is_err());
}
let header_only = vec![1u8, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0];
let decoded = decode_authentication(&header_only).unwrap();
assert!(decoded.authentication_information.is_empty());
assert_eq!(decoded.encode(), header_only);
}
#[test]
fn dhcpv4_forcerenew_nonce_capable_option_parsing() {
let value = Dhcpv4ForcerenewNonceCapable::hmac_md5();
assert_eq!(value.algorithms, vec![1]);
let payload = Dhcpv4OptionValue::ForcerenewNonceCapable(value.clone()).encode_payload();
assert_eq!(payload, vec![1]);
assert_eq!(
dhcpv4_typed_option_value(FORCERENEW_NONCE_CAPABLE, &payload)
.unwrap()
.unwrap(),
Dhcpv4OptionValue::ForcerenewNonceCapable(value.clone()),
);
let multi = Dhcpv4ForcerenewNonceCapable::new(vec![1, 2, 0xff]);
let parsed = build_and_decode(vec![
Dhcpv4Option::forcerenew_nonce_capable(multi.clone()),
Dhcpv4Option::End,
]);
assert_eq!(parsed.forcerenew_nonce_capable().unwrap().unwrap(), multi);
recompile_is_stable(&parsed);
assert_eq!(
dhcpv4_typed_option_value(FORCERENEW_NONCE_CAPABLE, &[])
.unwrap()
.unwrap(),
Dhcpv4OptionValue::ForcerenewNonceCapable(Dhcpv4ForcerenewNonceCapable::default()),
);
assert_eq!(FORCERENEW_NONCE_CAPABLE, 145);
}
}
#[cfg(test)]
mod dhcpv4_leasequery {
use super::super::{
Dhcpv4, Dhcpv4DataSource, Dhcpv4MessageType, Dhcpv4Option, Dhcpv4OptionValue, Dhcpv4State,
Dhcpv4StatusCode, Dhcpv4StatusCodeOption,
};
use super::dhcpv4_typed_option_value;
use crate::error::CrafterError;
use core::net::Ipv4Addr;
const CLIENT_LAST_TRANSACTION_TIME: u8 =
super::super::DHCPV4_OPTION_CLIENT_LAST_TRANSACTION_TIME; const ASSOCIATED_IP: u8 = super::super::DHCPV4_OPTION_ASSOCIATED_IP; const STATUS_CODE: u8 = super::super::DHCPV4_OPTION_STATUS_CODE; const BASE_TIME: u8 = super::super::DHCPV4_OPTION_BASE_TIME; const START_TIME_OF_STATE: u8 = super::super::DHCPV4_OPTION_START_TIME_OF_STATE; const QUERY_START_TIME: u8 = super::super::DHCPV4_OPTION_QUERY_START_TIME; const QUERY_END_TIME: u8 = super::super::DHCPV4_OPTION_QUERY_END_TIME; const DHCPV4_STATE: u8 = super::super::DHCPV4_OPTION_DHCP_STATE; const DATA_SOURCE: u8 = super::super::DHCPV4_OPTION_DATA_SOURCE;
fn ip(a: u8, b: u8, c: u8, d: u8) -> Ipv4Addr {
Ipv4Addr::new(a, b, c, d)
}
fn build_and_decode(options: Vec<Dhcpv4Option>) -> Dhcpv4 {
let dhcp = Dhcpv4::new()
.op(super::super::BOOTP_REPLY)
.message_type(Dhcpv4MessageType::LeaseActive)
.options(options);
let bytes = crate::Packet::from_layer(dhcp)
.compile()
.unwrap()
.as_bytes()
.to_vec();
Dhcpv4::decode(&bytes).unwrap()
}
fn recompile_is_stable(parsed: &Dhcpv4) {
let bytes = crate::Packet::from_layer(parsed.clone())
.compile()
.unwrap()
.as_bytes()
.to_vec();
let recompiled = crate::Packet::from_layer(Dhcpv4::decode(&bytes).unwrap())
.compile()
.unwrap()
.as_bytes()
.to_vec();
assert_eq!(recompiled, bytes);
}
#[test]
fn dhcpv4_leasequery_options_roundtrip() {
let status = Dhcpv4StatusCodeOption::new(Dhcpv4StatusCode::Success, b"ok".to_vec());
let cases: Vec<(u8, Dhcpv4OptionValue, Vec<u8>)> = vec![
(
CLIENT_LAST_TRANSACTION_TIME,
Dhcpv4OptionValue::U32(3600),
3600u32.to_be_bytes().to_vec(),
),
(
ASSOCIATED_IP,
Dhcpv4OptionValue::Ipv4List(vec![ip(192, 0, 2, 10), ip(198, 51, 100, 20)]),
vec![192, 0, 2, 10, 198, 51, 100, 20],
),
(
STATUS_CODE,
Dhcpv4OptionValue::StatusCode(status.clone()),
vec![0, b'o', b'k'],
),
(
BASE_TIME,
Dhcpv4OptionValue::U32(1_700_000_000),
1_700_000_000u32.to_be_bytes().to_vec(),
),
(
START_TIME_OF_STATE,
Dhcpv4OptionValue::U32(120),
120u32.to_be_bytes().to_vec(),
),
(
QUERY_START_TIME,
Dhcpv4OptionValue::U32(1_699_999_000),
1_699_999_000u32.to_be_bytes().to_vec(),
),
(
QUERY_END_TIME,
Dhcpv4OptionValue::U32(1_700_000_500),
1_700_000_500u32.to_be_bytes().to_vec(),
),
(
DHCPV4_STATE,
Dhcpv4OptionValue::Dhcpv4State(Dhcpv4State::Active),
vec![2],
),
(
DATA_SOURCE,
Dhcpv4OptionValue::DataSource(Dhcpv4DataSource::from_remote(true)),
vec![1],
),
];
for (code, value, expected) in &cases {
let payload = value.encode_payload();
assert_eq!(&payload, expected, "wire payload for code {code}");
assert_eq!(
dhcpv4_typed_option_value(*code, &payload).unwrap().unwrap(),
*value,
"typed decode mismatch for code {code}",
);
}
let parsed = build_and_decode(vec![
Dhcpv4Option::client_last_transaction_time(3600),
Dhcpv4Option::associated_ip(vec![ip(192, 0, 2, 10), ip(198, 51, 100, 20)]),
Dhcpv4Option::status_code(status.clone()),
Dhcpv4Option::base_time(1_700_000_000),
Dhcpv4Option::start_time_of_state(120),
Dhcpv4Option::query_start_time(1_699_999_000),
Dhcpv4Option::query_end_time(1_700_000_500),
Dhcpv4Option::dhcp_state(Dhcpv4State::Active),
Dhcpv4Option::data_source(Dhcpv4DataSource::from_remote(true)),
Dhcpv4Option::End,
]);
assert_eq!(
parsed.client_last_transaction_time().unwrap().unwrap(),
3600
);
assert_eq!(
parsed.associated_ip().unwrap().unwrap(),
vec![ip(192, 0, 2, 10), ip(198, 51, 100, 20)],
);
let decoded_status = parsed.status_code().unwrap().unwrap();
assert_eq!(decoded_status.status, Dhcpv4StatusCode::Success);
assert_eq!(decoded_status.message, b"ok");
assert_eq!(decoded_status.message_lossy(), "ok");
assert_eq!(parsed.base_time().unwrap().unwrap(), 1_700_000_000);
assert_eq!(parsed.start_time_of_state().unwrap().unwrap(), 120);
assert_eq!(parsed.query_start_time().unwrap().unwrap(), 1_699_999_000);
assert_eq!(parsed.query_end_time().unwrap().unwrap(), 1_700_000_500);
assert_eq!(parsed.dhcp_state().unwrap().unwrap(), Dhcpv4State::Active);
let source = parsed.data_source().unwrap().unwrap();
assert!(source.is_remote());
recompile_is_stable(&parsed);
assert_eq!(CLIENT_LAST_TRANSACTION_TIME, 91);
assert_eq!(ASSOCIATED_IP, 92);
assert_eq!(STATUS_CODE, 151);
assert_eq!(BASE_TIME, 152);
assert_eq!(START_TIME_OF_STATE, 153);
assert_eq!(QUERY_START_TIME, 154);
assert_eq!(QUERY_END_TIME, 155);
assert_eq!(DHCPV4_STATE, 156);
assert_eq!(DATA_SOURCE, 157);
}
#[test]
fn dhcpv4_leasequery_unknown_status_and_state_preserved() {
assert_eq!(
Dhcpv4StatusCode::from_code(200),
Dhcpv4StatusCode::Unknown(200)
);
assert_eq!(Dhcpv4StatusCode::Unknown(200).code(), 200);
assert_eq!(Dhcpv4State::from_code(99), Dhcpv4State::Unknown(99));
assert_eq!(Dhcpv4State::Unknown(99).code(), 99);
let status =
Dhcpv4StatusCodeOption::new(Dhcpv4StatusCode::Unknown(0x40), vec![0xff, 0xfe, 0x00]);
let payload = Dhcpv4OptionValue::StatusCode(status.clone()).encode_payload();
assert_eq!(payload, vec![0x40, 0xff, 0xfe, 0x00]);
assert_eq!(
dhcpv4_typed_option_value(STATUS_CODE, &payload)
.unwrap()
.unwrap(),
Dhcpv4OptionValue::StatusCode(status.clone()),
);
let parsed = build_and_decode(vec![
Dhcpv4Option::status_code(status.clone()),
Dhcpv4Option::End,
]);
assert_eq!(parsed.status_code().unwrap().unwrap(), status);
recompile_is_stable(&parsed);
let parsed_state = build_and_decode(vec![
Dhcpv4Option::dhcp_state(Dhcpv4State::Unknown(0x55)),
Dhcpv4Option::End,
]);
assert_eq!(
parsed_state.dhcp_state().unwrap().unwrap(),
Dhcpv4State::Unknown(0x55),
);
let source = Dhcpv4DataSource::new(0xFE);
assert!(
!source.is_remote(),
"REMOTE bit clear when only UNA bits set"
);
assert_eq!(source.encode(), vec![0xFE]);
let source_remote = Dhcpv4DataSource::new(0xFF);
assert!(source_remote.is_remote());
assert_eq!(
dhcpv4_typed_option_value(DATA_SOURCE, &[0xFE])
.unwrap()
.unwrap(),
Dhcpv4OptionValue::DataSource(Dhcpv4DataSource::new(0xFE)),
);
}
#[test]
fn dhcpv4_leasequery_malformed_lengths_are_structured() {
for code in [
CLIENT_LAST_TRANSACTION_TIME,
BASE_TIME,
START_TIME_OF_STATE,
QUERY_START_TIME,
QUERY_END_TIME,
] {
for len in [0usize, 1, 2, 3, 5, 8] {
assert!(
dhcpv4_typed_option_value(code, &vec![0u8; len]).is_err(),
"code {code} len {len} must be rejected",
);
}
}
for code in [DHCPV4_STATE, DATA_SOURCE] {
for len in [0usize, 2, 3] {
assert!(
dhcpv4_typed_option_value(code, &vec![0u8; len]).is_err(),
"code {code} len {len} must be rejected",
);
}
}
for len in [1usize, 2, 3, 5, 7] {
assert!(dhcpv4_typed_option_value(ASSOCIATED_IP, &vec![0u8; len]).is_err());
}
assert!(matches!(
dhcpv4_typed_option_value(STATUS_CODE, &[]),
Err(CrafterError::BufferTooShort { .. }),
));
let bare = dhcpv4_typed_option_value(STATUS_CODE, &[3])
.unwrap()
.unwrap();
assert_eq!(
bare,
Dhcpv4OptionValue::StatusCode(Dhcpv4StatusCodeOption::new(
Dhcpv4StatusCode::MalformedQuery,
Vec::new(),
)),
);
}
}
#[cfg(test)]
mod dhcpv4_remaining_registry {
use super::super::DHCPV4_IPV6_ONLY_PREFERRED_LEN;
use super::super::{
Dhcpv4, Dhcpv4MessageType, Dhcpv4Option, Dhcpv4OptionCode, Dhcpv4OptionKind,
Dhcpv4OptionStatus, Dhcpv4OptionValue,
};
use super::dhcpv4_typed_option_value;
const IPV6_ONLY_PREFERRED: u8 = super::super::DHCPV4_OPTION_IPV6_ONLY_PREFERRED; const CAPTIVE_PORTAL: u8 = super::super::DHCPV4_OPTION_CAPTIVE_PORTAL; const MUD_URL: u8 = super::super::DHCPV4_OPTION_MUD_URL_V4;
const PCP_SERVER: u8 = super::super::DHCPV4_OPTION_V4_PCP_SERVER; const DNR: u8 = super::super::DHCPV4_OPTION_V4_DNR; const SIX_RD: u8 = super::super::DHCPV4_OPTION_6RD;
fn build_and_decode(options: Vec<Dhcpv4Option>) -> Dhcpv4 {
let dhcp = Dhcpv4::new()
.op(super::super::BOOTP_REPLY)
.message_type(Dhcpv4MessageType::Ack)
.options(options);
let bytes = crate::Packet::from_layer(dhcp)
.compile()
.unwrap()
.as_bytes()
.to_vec();
Dhcpv4::decode(&bytes).unwrap()
}
fn option_payload(parsed: &Dhcpv4, code: u8) -> Vec<u8> {
parsed
.concatenated_option(code)
.expect("option present")
.expect("option decodes")
.payload()
.expect("payload bytes")
.to_vec()
}
#[test]
fn dhcpv4_modern_registry_options_decode_typed() {
let wait = 1800u32;
let payload = wait.to_be_bytes().to_vec();
assert_eq!(payload.len(), DHCPV4_IPV6_ONLY_PREFERRED_LEN);
let value = dhcpv4_typed_option_value(IPV6_ONLY_PREFERRED, &payload)
.unwrap()
.expect("option 108 has a typed value");
assert_eq!(value, Dhcpv4OptionValue::U32(wait));
assert_eq!(value.encode_payload(), payload);
assert_eq!(
Dhcpv4OptionKind::from_code(IPV6_ONLY_PREFERRED),
Some(Dhcpv4OptionKind::Ipv6OnlyPreferred),
);
assert!(dhcpv4_typed_option_value(IPV6_ONLY_PREFERRED, &[0u8; 3]).is_err());
let portal = b"https://portal.example.com/captive".to_vec();
let captive = dhcpv4_typed_option_value(CAPTIVE_PORTAL, &portal)
.unwrap()
.expect("option 114 has a typed value");
assert_eq!(captive, Dhcpv4OptionValue::Text(portal.clone()));
assert_eq!(captive.encode_payload(), portal);
assert_eq!(
Dhcpv4OptionKind::from_code(CAPTIVE_PORTAL),
Some(Dhcpv4OptionKind::CaptivePortal),
);
let mud = b"https://mud.example.com/device.json".to_vec();
let mud_url = dhcpv4_typed_option_value(MUD_URL, &mud)
.unwrap()
.expect("option 161 has a typed value");
assert_eq!(mud_url, Dhcpv4OptionValue::Text(mud.clone()));
assert_eq!(
Dhcpv4OptionKind::from_code(MUD_URL),
Some(Dhcpv4OptionKind::MudUrl),
);
let raw_text = vec![0xff, 0x00, 0x80, 0x41];
let preserved = dhcpv4_typed_option_value(CAPTIVE_PORTAL, &raw_text)
.unwrap()
.unwrap();
assert_eq!(preserved, Dhcpv4OptionValue::Text(raw_text));
let parsed = build_and_decode(vec![
Dhcpv4Option::message_type(Dhcpv4MessageType::Ack),
Dhcpv4Option::generic(IPV6_ONLY_PREFERRED, payload.clone()),
Dhcpv4Option::generic(CAPTIVE_PORTAL, portal.clone()),
Dhcpv4Option::generic(MUD_URL, mud.clone()),
Dhcpv4Option::End,
]);
assert_eq!(option_payload(&parsed, IPV6_ONLY_PREFERRED), payload);
assert_eq!(option_payload(&parsed, CAPTIVE_PORTAL), portal);
assert_eq!(option_payload(&parsed, MUD_URL), mud);
}
#[test]
fn dhcpv4_remaining_registry_options_preserve_raw_payloads() {
let raw_cases: &[(u8, Vec<u8>)] = &[
(PCP_SERVER, vec![0x04, 192, 0, 2, 1]),
(DNR, vec![0x00, 0x01, 0x03, b'd', b'n', b's']),
(
SIX_RD,
vec![
0x10, 0x20, 0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 0,
2, 1,
],
),
(110, vec![0xde, 0xad]),
(130, vec![0x01, 0x02, 0x03]),
(240, vec![0xbe, 0xef]),
];
for (code, payload) in raw_cases {
let code = *code;
assert!(
dhcpv4_typed_option_value(code, payload).unwrap().is_none(),
"code {code} must stay raw (Ok(None) from the typed dispatch)",
);
let parsed = build_and_decode(vec![
Dhcpv4Option::message_type(Dhcpv4MessageType::Ack),
Dhcpv4Option::generic(code, payload.clone()),
Dhcpv4Option::End,
]);
assert_eq!(
option_payload(&parsed, code),
*payload,
"code {code} payload must round-trip through a full packet",
);
let option = Dhcpv4Option::generic(code, payload.clone());
assert_eq!(option.code(), code);
assert_eq!(
option.logical_value(),
Some(Dhcpv4OptionValue::Opaque(payload.clone())),
);
assert_eq!(option.typed_value().unwrap(), None);
}
for code in [PCP_SERVER, DNR, SIX_RD] {
assert_eq!(
super::super::dhcpv4_option_status(code),
Dhcpv4OptionStatus::Assigned
);
assert!(matches!(
Dhcpv4OptionCode::from_code(code),
Dhcpv4OptionCode::Assigned(_)
));
}
assert_eq!(
super::super::dhcpv4_option_status(130),
Dhcpv4OptionStatus::Ambiguous
);
assert_eq!(
Dhcpv4OptionCode::from_code(110),
Dhcpv4OptionCode::RemovedOrUnassigned(110),
);
assert_eq!(
Dhcpv4OptionCode::from_code(240),
Dhcpv4OptionCode::PrivateUse(240)
);
}
#[test]
fn dhcpv4_remaining_registry_option_metadata_is_inspectable() {
use super::super::{dhcpv4_option_meta, dhcpv4_option_name, Dhcpv4OptionMeta};
let assigned_names: &[(u8, &str)] = &[
(IPV6_ONLY_PREFERRED, "IPv6-Only Preferred"),
(CAPTIVE_PORTAL, "DHCP Captive-Portal"),
(MUD_URL, "OPTION_MUD_URL_V4"),
(PCP_SERVER, "OPTION_V4_PCP_SERVER"),
(DNR, "OPTION_V4_DNR"),
(SIX_RD, "OPTION_6RD"),
];
for &(code, name) in assigned_names {
assert_eq!(
dhcpv4_option_name(code),
Some(name),
"option {code} must expose its registered name",
);
let meta: Dhcpv4OptionMeta = dhcpv4_option_meta(code);
assert_eq!(meta.code, code);
assert_eq!(meta.name, name);
assert_eq!(meta.status, Dhcpv4OptionStatus::Assigned);
let classified = Dhcpv4OptionCode::from_code(code);
assert_eq!(classified.code(), code);
assert_eq!(classified.name(), Some(name));
assert!(
!classified.is_single_octet(),
"option {code} is a length-prefixed option, not pad/end",
);
let option = Dhcpv4Option::generic(code, vec![0u8; 4]);
assert_eq!(option.registry_name(), Some(name));
assert_eq!(option.option_code(), classified);
}
let ambiguous = dhcpv4_option_meta(130);
assert_eq!(ambiguous.status, Dhcpv4OptionStatus::Ambiguous);
assert!(!ambiguous.name.is_empty());
assert_eq!(dhcpv4_option_name(130), Some(ambiguous.name));
let removed = dhcpv4_option_meta(110);
assert_eq!(removed.status, Dhcpv4OptionStatus::RemovedOrUnassigned);
assert_eq!(removed.name, "REMOVED/Unassigned");
assert_eq!(dhcpv4_option_name(110), Some("REMOVED/Unassigned"));
assert_eq!(
Dhcpv4OptionCode::from_code(110).name(),
Some("REMOVED/Unassigned"),
);
let private = dhcpv4_option_meta(240);
assert_eq!(private.status, Dhcpv4OptionStatus::PrivateUse);
assert_eq!(private.name, "Reserved (Private Use)");
assert_eq!(dhcpv4_option_name(240), None);
assert_eq!(Dhcpv4OptionCode::from_code(240).name(), None);
}
}