use std::net::Ipv4Addr;
use crate::MacAddr;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct DhcpMessage {
pub op: DhcpOp,
pub msg_type: Option<DhcpMessageType>,
pub xid: u32,
pub client_mac: Option<MacAddr>,
pub ciaddr: Ipv4Addr,
pub yiaddr: Ipv4Addr,
pub siaddr: Ipv4Addr,
pub giaddr: Ipv4Addr,
pub hostname: Option<String>,
pub requested_ip: Option<Ipv4Addr>,
pub lease_time: Option<u32>,
pub server_id: Option<Ipv4Addr>,
pub param_request_list: Vec<u8>,
pub vendor_class: Option<String>,
pub client_fqdn: Option<String>,
}
impl DhcpMessage {
pub fn fingerprint(&self) -> Option<String> {
if self.param_request_list.is_empty() && self.vendor_class.is_none() {
return None;
}
let mut s = String::new();
for (i, opt) in self.param_request_list.iter().enumerate() {
if i > 0 {
s.push(',');
}
s.push_str(&opt.to_string());
}
s.push('|');
if let Some(vc) = &self.vendor_class {
s.push_str(vc);
}
Some(s)
}
}
impl Default for DhcpMessage {
fn default() -> Self {
Self {
op: DhcpOp::BootRequest,
msg_type: None,
xid: 0,
client_mac: None,
ciaddr: Ipv4Addr::UNSPECIFIED,
yiaddr: Ipv4Addr::UNSPECIFIED,
siaddr: Ipv4Addr::UNSPECIFIED,
giaddr: Ipv4Addr::UNSPECIFIED,
hostname: None,
requested_ip: None,
lease_time: None,
server_id: None,
param_request_list: Vec::new(),
vendor_class: None,
client_fqdn: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "serde",
serde(tag = "kind", content = "value", rename_all = "snake_case")
)]
#[non_exhaustive]
pub enum DhcpOp {
BootRequest,
BootReply,
Other(u8),
}
impl DhcpOp {
pub fn as_str(&self) -> &'static str {
match self {
DhcpOp::BootRequest => "boot_request",
DhcpOp::BootReply => "boot_reply",
DhcpOp::Other(_) => "other",
}
}
}
impl From<u8> for DhcpOp {
fn from(op: u8) -> Self {
match op {
1 => DhcpOp::BootRequest,
2 => DhcpOp::BootReply,
other => DhcpOp::Other(other),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "serde",
serde(tag = "kind", content = "value", rename_all = "snake_case")
)]
#[non_exhaustive]
pub enum DhcpMessageType {
Discover,
Offer,
Request,
Decline,
Ack,
Nak,
Release,
Inform,
Other(u8),
}
impl DhcpMessageType {
pub fn as_str(&self) -> &'static str {
match self {
DhcpMessageType::Discover => "discover",
DhcpMessageType::Offer => "offer",
DhcpMessageType::Request => "request",
DhcpMessageType::Decline => "decline",
DhcpMessageType::Ack => "ack",
DhcpMessageType::Nak => "nak",
DhcpMessageType::Release => "release",
DhcpMessageType::Inform => "inform",
DhcpMessageType::Other(_) => "other",
}
}
}
impl From<u8> for DhcpMessageType {
fn from(b: u8) -> Self {
match b {
1 => DhcpMessageType::Discover,
2 => DhcpMessageType::Offer,
3 => DhcpMessageType::Request,
4 => DhcpMessageType::Decline,
5 => DhcpMessageType::Ack,
6 => DhcpMessageType::Nak,
7 => DhcpMessageType::Release,
8 => DhcpMessageType::Inform,
other => DhcpMessageType::Other(other),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dhcp_op_slugs_and_from_u8() {
assert_eq!(DhcpOp::from(1), DhcpOp::BootRequest);
assert_eq!(DhcpOp::from(2), DhcpOp::BootReply);
assert_eq!(DhcpOp::from(99), DhcpOp::Other(99));
assert_eq!(DhcpOp::BootRequest.as_str(), "boot_request");
assert_eq!(DhcpOp::BootReply.as_str(), "boot_reply");
}
#[test]
fn dhcp_msg_type_slugs() {
assert_eq!(DhcpMessageType::Discover.as_str(), "discover");
assert_eq!(DhcpMessageType::Ack.as_str(), "ack");
assert_eq!(DhcpMessageType::Nak.as_str(), "nak");
assert_eq!(DhcpMessageType::from(1), DhcpMessageType::Discover);
assert_eq!(DhcpMessageType::from(99), DhcpMessageType::Other(99));
}
#[test]
fn fingerprint_combines_opt55_and_opt60() {
let msg = DhcpMessage {
param_request_list: vec![1, 3, 6, 15, 33, 253],
vendor_class: Some("MSFT 5.0".into()),
..DhcpMessage::default()
};
assert_eq!(
msg.fingerprint().as_deref(),
Some("1,3,6,15,33,253|MSFT 5.0")
);
}
#[test]
fn fingerprint_with_only_opt55() {
let msg = DhcpMessage {
param_request_list: vec![1, 3],
..DhcpMessage::default()
};
assert_eq!(msg.fingerprint().as_deref(), Some("1,3|"));
}
#[test]
fn fingerprint_with_only_opt60() {
let msg = DhcpMessage {
vendor_class: Some("android-dhcp-14".into()),
..DhcpMessage::default()
};
assert_eq!(msg.fingerprint().as_deref(), Some("|android-dhcp-14"));
}
#[test]
fn fingerprint_none_when_neither_present() {
let msg = DhcpMessage::default();
assert_eq!(msg.fingerprint(), None);
}
}