packet_strata/packet/
dhcp.rs

1//! DHCP (Dynamic Host Configuration Protocol) packet parsing
2//!
3//! This module implements parsing for DHCP packets as defined in RFC 2131.
4//! DHCP is used to automatically assign IP addresses and network configuration
5//! to hosts on a network.
6//!
7//! # DHCP Header Format
8//!
9//! ```text
10//!  0                   1                   2                   3
11//!  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
12//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
13//! |     op (1)    |   htype (1)   |   hlen (1)    |   hops (1)    |
14//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
15//! |                            xid (4)                            |
16//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
17//! |           secs (2)            |           flags (2)           |
18//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
19//! |                          ciaddr (4)                           |
20//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
21//! |                          yiaddr (4)                           |
22//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
23//! |                          siaddr (4)                           |
24//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
25//! |                          giaddr (4)                           |
26//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
27//! |                          chaddr (16)                          |
28//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
29//! |                          sname (64)                           |
30//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
31//! |                          file (128)                           |
32//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
33//! |                       options (variable)                      |
34//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
35//! ```
36//!
37//! # Key characteristics
38//!
39//! - Fixed header size: 236 bytes (before options)
40//! - op: 1 (BOOTREQUEST), 2 (BOOTREPLY)
41//! - Magic cookie: 0x63825363 marks start of options
42//! - Common ports: 67 (server), 68 (client)
43//!
44//! # Examples
45//!
46//! ```no_run
47//! use packet_strata::packet::dhcp::{DhcpHeader, DhcpOpCode, DhcpMessageType, DhcpOption};
48//! use packet_strata::packet::HeaderParser;
49//! use std::net::Ipv4Addr;
50//!
51//! # fn main() {
52//! # let packet_bytes: Vec<u8> = vec![]; // DHCP packet data
53//! let (dhcp, _) = DhcpHeader::from_bytes(&packet_bytes).unwrap();
54//!
55//! // Check if it's a request or reply
56//! match dhcp.op() {
57//!     DhcpOpCode::BOOTREQUEST => println!("DHCP Request"),
58//!     DhcpOpCode::BOOTREPLY => println!("DHCP Reply"),
59//!     _ => println!("Unknown"),
60//! }
61//!
62//! // Access DHCP options
63//! for option in dhcp.options() {
64//!     if let DhcpOption::MessageType(msg_type) = option {
65//!         println!("Message Type: {:?}", msg_type);
66//!     }
67//! }
68//! # }
69//! ```
70
71use core::fmt;
72use std::fmt::{Display, Formatter};
73use std::net::Ipv4Addr;
74use std::ops::Deref;
75use zerocopy::byteorder::{BigEndian, U16, U32};
76use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
77
78use crate::packet::ether::EthAddr;
79use crate::packet::{HeaderParser, PacketHeader};
80
81/// DHCP Magic Cookie (0x63825363)
82pub const DHCP_MAGIC_COOKIE: u32 = 0x63825363;
83
84/// DHCP server port
85pub const DHCP_SERVER_PORT: u16 = 67;
86
87/// DHCP client port
88pub const DHCP_CLIENT_PORT: u16 = 68;
89
90/// Maximum DHCP packet size
91pub const DHCP_MAX_PACKET_SIZE: usize = 576;
92
93/// DHCP Operation Code
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
95#[repr(u8)]
96pub enum DhcpOpCode {
97    BOOTREQUEST = 1, // Client to Server
98    BOOTREPLY = 2,   // Server to Client
99}
100
101impl DhcpOpCode {
102    pub fn from_u8(value: u8) -> Option<Self> {
103        match value {
104            1 => Some(DhcpOpCode::BOOTREQUEST),
105            2 => Some(DhcpOpCode::BOOTREPLY),
106            _ => None,
107        }
108    }
109}
110
111impl Display for DhcpOpCode {
112    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
113        match self {
114            DhcpOpCode::BOOTREQUEST => write!(f, "BOOTREQUEST"),
115            DhcpOpCode::BOOTREPLY => write!(f, "BOOTREPLY"),
116        }
117    }
118}
119
120/// DHCP Hardware Type (same as ARP)
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
122#[repr(u8)]
123pub enum DhcpHardwareType {
124    ETHERNET = 1,
125    IEEE802 = 6,
126}
127
128impl DhcpHardwareType {
129    pub fn from_u8(value: u8) -> Option<Self> {
130        match value {
131            1 => Some(DhcpHardwareType::ETHERNET),
132            6 => Some(DhcpHardwareType::IEEE802),
133            _ => None,
134        }
135    }
136}
137
138impl Display for DhcpHardwareType {
139    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
140        match self {
141            DhcpHardwareType::ETHERNET => write!(f, "ethernet"),
142            DhcpHardwareType::IEEE802 => write!(f, "ieee802"),
143        }
144    }
145}
146
147/// DHCP Message Type (Option 53)
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
149#[repr(u8)]
150pub enum DhcpMessageType {
151    DISCOVER = 1,
152    OFFER = 2,
153    REQUEST = 3,
154    DECLINE = 4,
155    ACK = 5,
156    NAK = 6,
157    RELEASE = 7,
158    INFORM = 8,
159}
160
161impl DhcpMessageType {
162    pub fn from_u8(value: u8) -> Option<Self> {
163        match value {
164            1 => Some(DhcpMessageType::DISCOVER),
165            2 => Some(DhcpMessageType::OFFER),
166            3 => Some(DhcpMessageType::REQUEST),
167            4 => Some(DhcpMessageType::DECLINE),
168            5 => Some(DhcpMessageType::ACK),
169            6 => Some(DhcpMessageType::NAK),
170            7 => Some(DhcpMessageType::RELEASE),
171            8 => Some(DhcpMessageType::INFORM),
172            _ => None,
173        }
174    }
175}
176
177impl Display for DhcpMessageType {
178    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
179        match self {
180            DhcpMessageType::DISCOVER => write!(f, "DISCOVER"),
181            DhcpMessageType::OFFER => write!(f, "OFFER"),
182            DhcpMessageType::REQUEST => write!(f, "REQUEST"),
183            DhcpMessageType::DECLINE => write!(f, "DECLINE"),
184            DhcpMessageType::ACK => write!(f, "ACK"),
185            DhcpMessageType::NAK => write!(f, "NAK"),
186            DhcpMessageType::RELEASE => write!(f, "RELEASE"),
187            DhcpMessageType::INFORM => write!(f, "INFORM"),
188        }
189    }
190}
191
192/// DHCP Fixed Header (236 bytes)
193///
194/// This represents the fixed portion of a DHCP packet as defined in RFC 2131.
195/// The options field follows this header and is variable length.
196#[repr(C, packed)]
197#[derive(FromBytes, IntoBytes, Unaligned, Immutable, KnownLayout, Debug, Clone, Copy)]
198pub struct DhcpHeader {
199    op: u8,                 // Message op code / message type (1 = BOOTREQUEST, 2 = BOOTREPLY)
200    htype: u8,              // Hardware address type (1 = Ethernet)
201    hlen: u8,               // Hardware address length (6 for Ethernet)
202    hops: u8,               // Client sets to zero, optionally used by relay agents
203    xid: U32<BigEndian>,    // Transaction ID, a random number chosen by the client
204    secs: U16<BigEndian>,   // Seconds elapsed since client began address acquisition/renewal
205    flags: U16<BigEndian>,  // Flags (bit 0 = broadcast flag)
206    ciaddr: U32<BigEndian>, // Client IP address (if client is in BOUND, RENEW or REBINDING)
207    yiaddr: U32<BigEndian>, // 'Your' (client) IP address
208    siaddr: U32<BigEndian>, // IP address of next server to use in bootstrap
209    giaddr: U32<BigEndian>, // Relay agent IP address
210    chaddr: [u8; 16],       // Client hardware address (padded to 16 bytes)
211    sname: [u8; 64],        // Optional server host name, null terminated string
212    file: [u8; 128],        // Boot file name, null terminated string
213    magic_cookie: U32<BigEndian>, // Magic cookie: 0x63825363
214}
215
216impl DhcpHeader {
217    /// Returns the operation code
218    #[inline]
219    pub fn op(&self) -> DhcpOpCode {
220        DhcpOpCode::from_u8(self.op).unwrap_or(DhcpOpCode::BOOTREQUEST)
221    }
222
223    /// Returns the hardware type
224    #[inline]
225    pub fn htype(&self) -> u8 {
226        self.htype
227    }
228
229    /// Returns the hardware address length
230    #[inline]
231    pub fn hlen(&self) -> u8 {
232        self.hlen
233    }
234
235    /// Returns the hop count
236    #[inline]
237    pub fn hops(&self) -> u8 {
238        self.hops
239    }
240
241    /// Returns the transaction ID
242    #[inline]
243    pub fn xid(&self) -> u32 {
244        self.xid.get()
245    }
246
247    /// Returns the seconds elapsed
248    #[inline]
249    pub fn secs(&self) -> u16 {
250        self.secs.get()
251    }
252
253    /// Returns the flags
254    #[inline]
255    pub fn flags(&self) -> u16 {
256        self.flags.get()
257    }
258
259    /// Returns true if broadcast flag is set
260    #[inline]
261    pub fn is_broadcast(&self) -> bool {
262        (self.flags.get() & 0x8000) != 0
263    }
264
265    /// Returns the client IP address
266    #[inline]
267    pub fn ciaddr(&self) -> Ipv4Addr {
268        Ipv4Addr::from(self.ciaddr.get())
269    }
270
271    /// Returns the 'your' (client) IP address
272    #[inline]
273    pub fn yiaddr(&self) -> Ipv4Addr {
274        Ipv4Addr::from(self.yiaddr.get())
275    }
276
277    /// Returns the server IP address
278    #[inline]
279    pub fn siaddr(&self) -> Ipv4Addr {
280        Ipv4Addr::from(self.siaddr.get())
281    }
282
283    /// Returns the relay agent IP address
284    #[inline]
285    pub fn giaddr(&self) -> Ipv4Addr {
286        Ipv4Addr::from(self.giaddr.get())
287    }
288
289    /// Returns the client hardware address (raw bytes)
290    #[inline]
291    pub fn chaddr_raw(&self) -> &[u8] {
292        &self.chaddr[..self.hlen as usize]
293    }
294
295    /// For Ethernet, returns the client MAC address
296    #[inline]
297    pub fn chaddr_eth(&self) -> Option<&EthAddr> {
298        if self.htype == 1 && self.hlen == 6 {
299            zerocopy::Ref::<_, EthAddr>::from_prefix(&self.chaddr[..6])
300                .ok()
301                .map(|(r, _)| zerocopy::Ref::into_ref(r))
302        } else {
303            None
304        }
305    }
306
307    /// Returns the server name (null-terminated)
308    #[inline]
309    pub fn sname(&self) -> &[u8] {
310        // Find null terminator
311        let end = self.sname.iter().position(|&b| b == 0).unwrap_or(64);
312        &self.sname[..end]
313    }
314
315    /// Returns the boot file name (null-terminated)
316    #[inline]
317    pub fn file(&self) -> &[u8] {
318        // Find null terminator
319        let end = self.file.iter().position(|&b| b == 0).unwrap_or(128);
320        &self.file[..end]
321    }
322
323    /// Returns the magic cookie
324    #[inline]
325    pub fn magic_cookie(&self) -> u32 {
326        self.magic_cookie.get()
327    }
328
329    /// Validates the DHCP header
330    #[inline]
331    fn is_valid(&self) -> bool {
332        // Check magic cookie
333        if self.magic_cookie.get() != DHCP_MAGIC_COOKIE {
334            return false;
335        }
336
337        // Check op code
338        if self.op != 1 && self.op != 2 {
339            return false;
340        }
341
342        // Check hardware address length is reasonable
343        if self.hlen > 16 {
344            return false;
345        }
346
347        true
348    }
349}
350
351impl PacketHeader for DhcpHeader {
352    const NAME: &'static str = "DhcpHeader";
353    type InnerType = ();
354
355    #[inline]
356    fn inner_type(&self) -> Self::InnerType {}
357
358    #[inline]
359    fn is_valid(&self) -> bool {
360        self.is_valid()
361    }
362}
363
364impl HeaderParser for DhcpHeader {
365    type Output<'a> = DhcpHeaderOpt<'a>;
366
367    #[inline]
368    fn into_view<'a>(header: &'a Self, options: &'a [u8]) -> Self::Output<'a> {
369        DhcpHeaderOpt { header, options }
370    }
371
372    // Custom implementation to consume all remaining bytes as options
373    fn from_bytes<'a>(
374        buf: &'a [u8],
375    ) -> Result<(Self::Output<'a>, &'a [u8]), crate::packet::PacketHeaderError> {
376        // Parse fixed header
377        let (header_ref, rest) = zerocopy::Ref::<_, Self>::from_prefix(buf)
378            .map_err(|_| crate::packet::PacketHeaderError::TooShort(Self::NAME))?;
379
380        let header = zerocopy::Ref::into_ref(header_ref);
381
382        if !header.is_valid() {
383            return Err(crate::packet::PacketHeaderError::Invalid(Self::NAME));
384        }
385
386        // All remaining bytes are DHCP options
387        let view = Self::into_view(header, rest);
388
389        // Return empty slice as payload since DHCP options consumed everything
390        Ok((view, &[]))
391    }
392}
393
394/// DHCP Header with options
395///
396/// This is the proxy object returned by the parser. It provides access to both
397/// the fixed header and the variable-length options field.
398pub struct DhcpHeaderOpt<'a> {
399    pub header: &'a DhcpHeader,
400    pub options: &'a [u8],
401}
402
403impl<'a> DhcpHeaderOpt<'a> {
404    /// Returns an iterator over DHCP options
405    pub fn options(&self) -> DhcpOptionsIter<'a> {
406        DhcpOptionsIter::new(self.options)
407    }
408
409    /// Find a specific option by code
410    pub fn find_option(&self, code: u8) -> Option<DhcpOption<'a>> {
411        self.options().find(|opt| opt.code() == Some(code))
412    }
413
414    /// Get the DHCP message type (option 53)
415    pub fn message_type(&self) -> Option<DhcpMessageType> {
416        self.find_option(53).and_then(|opt| {
417            if let DhcpOption::MessageType(mt) = opt {
418                Some(mt)
419            } else {
420                None
421            }
422        })
423    }
424
425    /// Get the requested IP address (option 50)
426    pub fn requested_ip(&self) -> Option<Ipv4Addr> {
427        self.find_option(50).and_then(|opt| {
428            if let DhcpOption::RequestedIpAddress(ip) = opt {
429                Some(ip)
430            } else {
431                None
432            }
433        })
434    }
435
436    /// Get the subnet mask (option 1)
437    pub fn subnet_mask(&self) -> Option<Ipv4Addr> {
438        self.find_option(1).and_then(|opt| {
439            if let DhcpOption::SubnetMask(mask) = opt {
440                Some(mask)
441            } else {
442                None
443            }
444        })
445    }
446
447    /// Get the router/gateway (option 3)
448    pub fn router(&self) -> Option<Vec<Ipv4Addr>> {
449        self.find_option(3).and_then(|opt| {
450            if let DhcpOption::Router(routers) = opt {
451                Some(routers)
452            } else {
453                None
454            }
455        })
456    }
457
458    /// Get DNS servers (option 6)
459    pub fn dns_servers(&self) -> Option<Vec<Ipv4Addr>> {
460        self.find_option(6).and_then(|opt| {
461            if let DhcpOption::DomainNameServer(dns) = opt {
462                Some(dns)
463            } else {
464                None
465            }
466        })
467    }
468
469    /// Get the lease time (option 51)
470    pub fn lease_time(&self) -> Option<u32> {
471        self.find_option(51).and_then(|opt| {
472            if let DhcpOption::IpAddressLeaseTime(time) = opt {
473                Some(time)
474            } else {
475                None
476            }
477        })
478    }
479
480    /// Get the DHCP server identifier (option 54)
481    pub fn server_identifier(&self) -> Option<Ipv4Addr> {
482        self.find_option(54).and_then(|opt| {
483            if let DhcpOption::ServerIdentifier(ip) = opt {
484                Some(ip)
485            } else {
486                None
487            }
488        })
489    }
490}
491
492impl Deref for DhcpHeaderOpt<'_> {
493    type Target = DhcpHeader;
494
495    #[inline]
496    fn deref(&self) -> &Self::Target {
497        self.header
498    }
499}
500
501impl Display for DhcpHeaderOpt<'_> {
502    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
503        write!(f, "DHCP {} xid=0x{:08x}", self.op(), self.xid())?;
504
505        if let Some(msg_type) = self.message_type() {
506            write!(f, " type={}", msg_type)?;
507        }
508
509        if let Some(chaddr) = self.chaddr_eth() {
510            write!(f, " chaddr={}", chaddr)?;
511        }
512
513        Ok(())
514    }
515}
516
517/// DHCP Option
518#[derive(Debug, Clone, PartialEq)]
519pub enum DhcpOption<'a> {
520    Pad,
521    End,
522    SubnetMask(Ipv4Addr),
523    Router(Vec<Ipv4Addr>),
524    DomainNameServer(Vec<Ipv4Addr>),
525    HostName(&'a [u8]),
526    DomainName(&'a [u8]),
527    RequestedIpAddress(Ipv4Addr),
528    IpAddressLeaseTime(u32),
529    MessageType(DhcpMessageType),
530    ServerIdentifier(Ipv4Addr),
531    ParameterRequestList(&'a [u8]),
532    RenewalTime(u32),
533    RebindingTime(u32),
534    ClientIdentifier(&'a [u8]),
535    Unknown { code: u8, data: &'a [u8] },
536}
537
538impl<'a> DhcpOption<'a> {
539    /// Returns the option code, if available
540    pub fn code(&self) -> Option<u8> {
541        match self {
542            DhcpOption::Pad => Some(0),
543            DhcpOption::End => Some(255),
544            DhcpOption::SubnetMask(_) => Some(1),
545            DhcpOption::Router(_) => Some(3),
546            DhcpOption::DomainNameServer(_) => Some(6),
547            DhcpOption::HostName(_) => Some(12),
548            DhcpOption::DomainName(_) => Some(15),
549            DhcpOption::RequestedIpAddress(_) => Some(50),
550            DhcpOption::IpAddressLeaseTime(_) => Some(51),
551            DhcpOption::MessageType(_) => Some(53),
552            DhcpOption::ServerIdentifier(_) => Some(54),
553            DhcpOption::ParameterRequestList(_) => Some(55),
554            DhcpOption::RenewalTime(_) => Some(58),
555            DhcpOption::RebindingTime(_) => Some(59),
556            DhcpOption::ClientIdentifier(_) => Some(61),
557            DhcpOption::Unknown { code, .. } => Some(*code),
558        }
559    }
560
561    /// Parse a single DHCP option from bytes
562    fn parse(data: &'a [u8]) -> Option<(Self, &'a [u8])> {
563        if data.is_empty() {
564            return None;
565        }
566
567        let code = data[0];
568
569        // Pad and End have no length field
570        if code == 0 {
571            return Some((DhcpOption::Pad, &data[1..]));
572        }
573        if code == 255 {
574            return Some((DhcpOption::End, &data[1..]));
575        }
576
577        if data.len() < 2 {
578            return None;
579        }
580
581        let length = data[1] as usize;
582        if data.len() < 2 + length {
583            return None;
584        }
585
586        let option_data = &data[2..2 + length];
587        let remaining = &data[2 + length..];
588
589        let option = match code {
590            1 if length == 4 => DhcpOption::SubnetMask(Ipv4Addr::from(u32::from_be_bytes(
591                option_data.try_into().ok()?,
592            ))),
593            3 if length.is_multiple_of(4) => {
594                let routers = option_data
595                    .chunks_exact(4)
596                    .map(|chunk| Ipv4Addr::from(u32::from_be_bytes(chunk.try_into().unwrap())))
597                    .collect();
598                DhcpOption::Router(routers)
599            }
600            6 if length.is_multiple_of(4) => {
601                let servers = option_data
602                    .chunks_exact(4)
603                    .map(|chunk| Ipv4Addr::from(u32::from_be_bytes(chunk.try_into().unwrap())))
604                    .collect();
605                DhcpOption::DomainNameServer(servers)
606            }
607            12 => DhcpOption::HostName(option_data),
608            15 => DhcpOption::DomainName(option_data),
609            50 if length == 4 => DhcpOption::RequestedIpAddress(Ipv4Addr::from(
610                u32::from_be_bytes(option_data.try_into().ok()?),
611            )),
612            51 if length == 4 => {
613                DhcpOption::IpAddressLeaseTime(u32::from_be_bytes(option_data.try_into().ok()?))
614            }
615            53 if length == 1 => DhcpOption::MessageType(DhcpMessageType::from_u8(option_data[0])?),
616            54 if length == 4 => DhcpOption::ServerIdentifier(Ipv4Addr::from(u32::from_be_bytes(
617                option_data.try_into().ok()?,
618            ))),
619            55 => DhcpOption::ParameterRequestList(option_data),
620            58 if length == 4 => {
621                DhcpOption::RenewalTime(u32::from_be_bytes(option_data.try_into().ok()?))
622            }
623            59 if length == 4 => {
624                DhcpOption::RebindingTime(u32::from_be_bytes(option_data.try_into().ok()?))
625            }
626            61 => DhcpOption::ClientIdentifier(option_data),
627            _ => DhcpOption::Unknown {
628                code,
629                data: option_data,
630            },
631        };
632
633        Some((option, remaining))
634    }
635}
636
637/// Iterator over DHCP options
638pub struct DhcpOptionsIter<'a> {
639    data: &'a [u8],
640    done: bool,
641}
642
643impl<'a> DhcpOptionsIter<'a> {
644    pub fn new(data: &'a [u8]) -> Self {
645        Self { data, done: false }
646    }
647}
648
649impl<'a> Iterator for DhcpOptionsIter<'a> {
650    type Item = DhcpOption<'a>;
651
652    fn next(&mut self) -> Option<Self::Item> {
653        if self.done || self.data.is_empty() {
654            return None;
655        }
656
657        let (option, remaining) = DhcpOption::parse(self.data)?;
658        self.data = remaining;
659
660        if matches!(option, DhcpOption::End) {
661            self.done = true;
662        }
663
664        Some(option)
665    }
666}
667
668#[cfg(test)]
669mod tests {
670    use super::*;
671    use std::str::FromStr;
672
673    fn create_basic_dhcp_discover() -> Vec<u8> {
674        let mut packet = vec![
675            1, // op: BOOTREQUEST
676            1, // htype: Ethernet
677            6, // hlen: 6
678            0, // hops: 0
679        ];
680
681        // Fixed header
682        packet.extend_from_slice(&0x12345678u32.to_be_bytes()); // xid
683        packet.extend_from_slice(&0u16.to_be_bytes()); // secs
684        packet.extend_from_slice(&0x8000u16.to_be_bytes()); // flags: broadcast
685        packet.extend_from_slice(&[0; 4]); // ciaddr: 0.0.0.0
686        packet.extend_from_slice(&[0; 4]); // yiaddr: 0.0.0.0
687        packet.extend_from_slice(&[0; 4]); // siaddr: 0.0.0.0
688        packet.extend_from_slice(&[0; 4]); // giaddr: 0.0.0.0
689
690        // chaddr: MAC address + padding
691        packet.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]);
692        packet.extend_from_slice(&[0; 10]);
693
694        // sname: server name (64 bytes)
695        packet.extend_from_slice(&[0; 64]);
696
697        // file: boot file name (128 bytes)
698        packet.extend_from_slice(&[0; 128]);
699
700        // magic cookie
701        packet.extend_from_slice(&DHCP_MAGIC_COOKIE.to_be_bytes());
702
703        packet
704    }
705
706    #[test]
707    fn test_dhcp_discover_basic() {
708        let mut packet = create_basic_dhcp_discover();
709
710        // Add DHCP Message Type option (DISCOVER)
711        packet.push(53); // option code
712        packet.push(1); // length
713        packet.push(1); // DISCOVER
714
715        // End option
716        packet.push(255);
717
718        let (dhcp, remaining) = DhcpHeader::from_bytes(&packet).unwrap();
719
720        assert_eq!(remaining.len(), 0); // All options consumed
721        assert_eq!(dhcp.op(), DhcpOpCode::BOOTREQUEST);
722        assert_eq!(dhcp.htype(), 1);
723        assert_eq!(dhcp.hlen(), 6);
724        assert_eq!(dhcp.xid(), 0x12345678);
725        assert!(dhcp.is_broadcast());
726        assert_eq!(dhcp.ciaddr(), Ipv4Addr::new(0, 0, 0, 0));
727
728        // Check MAC address
729        assert_eq!(
730            dhcp.chaddr_eth().unwrap(),
731            &EthAddr::from_str("aa:bb:cc:dd:ee:ff").unwrap()
732        );
733
734        // Check message type from options
735        let msg_type = dhcp.options().find_map(|opt| {
736            if let DhcpOption::MessageType(mt) = opt {
737                Some(mt)
738            } else {
739                None
740            }
741        });
742        assert_eq!(msg_type, Some(DhcpMessageType::DISCOVER));
743    }
744
745    #[test]
746    fn test_dhcp_offer() {
747        let mut packet = create_basic_dhcp_discover();
748        packet[0] = 2; // op: BOOTREPLY
749
750        // Set yiaddr: 192.168.1.100
751        packet[16..20].copy_from_slice(&[192, 168, 1, 100]);
752
753        // Add DHCP Message Type option (OFFER)
754        packet.push(53);
755        packet.push(1);
756        packet.push(2); // OFFER
757
758        // Add Server Identifier
759        packet.push(54);
760        packet.push(4);
761        packet.extend_from_slice(&[192, 168, 1, 1]);
762
763        // Add Subnet Mask
764        packet.push(1);
765        packet.push(4);
766        packet.extend_from_slice(&[255, 255, 255, 0]);
767
768        // Add Router
769        packet.push(3);
770        packet.push(4);
771        packet.extend_from_slice(&[192, 168, 1, 1]);
772
773        // Add DNS Server
774        packet.push(6);
775        packet.push(4);
776        packet.extend_from_slice(&[8, 8, 8, 8]);
777
778        // Add Lease Time (86400 seconds = 1 day)
779        packet.push(51);
780        packet.push(4);
781        packet.extend_from_slice(&86400u32.to_be_bytes());
782
783        // End option
784        packet.push(255);
785
786        let (dhcp, _) = DhcpHeader::from_bytes(&packet).unwrap();
787
788        assert_eq!(dhcp.op(), DhcpOpCode::BOOTREPLY);
789        assert_eq!(dhcp.yiaddr(), Ipv4Addr::new(192, 168, 1, 100));
790
791        // Collect all options to verify they're parsed
792        let options: Vec<_> = dhcp.options().collect();
793
794        // Verify message type
795        assert!(options
796            .iter()
797            .any(|opt| matches!(opt, DhcpOption::MessageType(DhcpMessageType::OFFER))));
798
799        // Verify server identifier
800        assert!(options.iter().any(|opt| matches!(opt, DhcpOption::ServerIdentifier(ip) if *ip == Ipv4Addr::new(192, 168, 1, 1))));
801
802        // Verify subnet mask
803        assert!(options.iter().any(|opt| matches!(opt, DhcpOption::SubnetMask(mask) if *mask == Ipv4Addr::new(255, 255, 255, 0))));
804
805        // Verify router
806        assert!(options.iter().any(|opt| matches!(opt, DhcpOption::Router(routers) if routers.len() == 1 && routers[0] == Ipv4Addr::new(192, 168, 1, 1))));
807
808        // Verify DNS
809        assert!(options.iter().any(|opt| matches!(opt, DhcpOption::DomainNameServer(dns) if dns.len() == 1 && dns[0] == Ipv4Addr::new(8, 8, 8, 8))));
810
811        // Verify lease time
812        assert!(options
813            .iter()
814            .any(|opt| matches!(opt, DhcpOption::IpAddressLeaseTime(86400))));
815    }
816
817    #[test]
818    fn test_dhcp_request() {
819        let mut packet = create_basic_dhcp_discover();
820
821        // Add Message Type (REQUEST)
822        packet.push(53);
823        packet.push(1);
824        packet.push(3); // REQUEST
825
826        // Add Requested IP Address
827        packet.push(50);
828        packet.push(4);
829        packet.extend_from_slice(&[192, 168, 1, 100]);
830
831        // Add Server Identifier
832        packet.push(54);
833        packet.push(4);
834        packet.extend_from_slice(&[192, 168, 1, 1]);
835
836        // End option
837        packet.push(255);
838
839        let (dhcp, _) = DhcpHeader::from_bytes(&packet).unwrap();
840
841        let options: Vec<_> = dhcp.options().collect();
842
843        assert!(options
844            .iter()
845            .any(|opt| matches!(opt, DhcpOption::MessageType(DhcpMessageType::REQUEST))));
846        assert!(options.iter().any(|opt| matches!(opt, DhcpOption::RequestedIpAddress(ip) if *ip == Ipv4Addr::new(192, 168, 1, 100))));
847        assert!(options.iter().any(|opt| matches!(opt, DhcpOption::ServerIdentifier(ip) if *ip == Ipv4Addr::new(192, 168, 1, 1))));
848    }
849
850    #[test]
851    fn test_dhcp_ack() {
852        let mut packet = create_basic_dhcp_discover();
853        packet[0] = 2; // BOOTREPLY
854
855        // Set yiaddr
856        packet[16..20].copy_from_slice(&[192, 168, 1, 100]);
857
858        // Add Message Type (ACK)
859        packet.push(53);
860        packet.push(1);
861        packet.push(5); // ACK
862
863        // End option
864        packet.push(255);
865
866        let (dhcp, _) = DhcpHeader::from_bytes(&packet).unwrap();
867
868        assert_eq!(dhcp.op(), DhcpOpCode::BOOTREPLY);
869        assert_eq!(dhcp.yiaddr(), Ipv4Addr::new(192, 168, 1, 100));
870
871        // Check message type from options
872        let msg_type = dhcp.options().find_map(|opt| {
873            if let DhcpOption::MessageType(mt) = opt {
874                Some(mt)
875            } else {
876                None
877            }
878        });
879        assert_eq!(msg_type, Some(DhcpMessageType::ACK));
880    }
881
882    #[test]
883    fn test_dhcp_nak() {
884        let mut packet = create_basic_dhcp_discover();
885        packet[0] = 2; // BOOTREPLY
886
887        // Add Message Type (NAK)
888        packet.push(53);
889        packet.push(1);
890        packet.push(6); // NAK
891
892        // End option
893        packet.push(255);
894
895        let (dhcp, _) = DhcpHeader::from_bytes(&packet).unwrap();
896
897        let msg_type = dhcp.options().find_map(|opt| {
898            if let DhcpOption::MessageType(mt) = opt {
899                Some(mt)
900            } else {
901                None
902            }
903        });
904        assert_eq!(msg_type, Some(DhcpMessageType::NAK));
905    }
906
907    #[test]
908    fn test_dhcp_invalid_magic_cookie() {
909        let mut packet = create_basic_dhcp_discover();
910
911        // Corrupt magic cookie
912        let magic_offset = 236;
913        packet[magic_offset..magic_offset + 4].copy_from_slice(&[0x11, 0x22, 0x33, 0x44]);
914
915        let result = DhcpHeader::from_bytes(&packet);
916        assert!(result.is_err());
917    }
918
919    #[test]
920    fn test_dhcp_packet_too_short() {
921        let packet = vec![0u8; 100]; // Too short
922        let result = DhcpHeader::from_bytes(&packet);
923        assert!(result.is_err());
924    }
925
926    #[test]
927    fn test_dhcp_options_iterator() {
928        let mut packet = create_basic_dhcp_discover();
929
930        // Add multiple options
931        packet.push(53);
932        packet.push(1);
933        packet.push(1); // DISCOVER
934
935        packet.push(12); // Hostname
936        packet.push(4);
937        packet.extend_from_slice(b"test");
938
939        packet.push(61); // Client Identifier
940        packet.push(7);
941        packet.push(1); // hardware type
942        packet.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]);
943
944        packet.push(255); // End
945
946        let (dhcp, _) = DhcpHeader::from_bytes(&packet).unwrap();
947
948        let options: Vec<_> = dhcp.options().collect();
949        // Should have: MessageType, HostName, ClientIdentifier, End = at least 4
950        assert!(options.len() >= 4, "Got {} options", options.len());
951
952        // Check message type
953        assert!(matches!(
954            options.iter().find(|opt| opt.code() == Some(53)),
955            Some(DhcpOption::MessageType(DhcpMessageType::DISCOVER))
956        ));
957
958        // Check hostname
959        assert!(matches!(
960            options.iter().find(|opt| opt.code() == Some(12)),
961            Some(DhcpOption::HostName(b"test"))
962        ));
963    }
964
965    #[test]
966    fn test_dhcp_multiple_dns_servers() {
967        let mut packet = create_basic_dhcp_discover();
968
969        // Add multiple DNS servers
970        packet.push(6);
971        packet.push(8); // 2 DNS servers
972        packet.extend_from_slice(&[8, 8, 8, 8]); // Google DNS
973        packet.extend_from_slice(&[8, 8, 4, 4]); // Google DNS 2
974
975        packet.push(255);
976
977        let (dhcp, _) = DhcpHeader::from_bytes(&packet).unwrap();
978
979        let options: Vec<_> = dhcp.options().collect();
980
981        let dns_opt = options.iter().find_map(|opt| {
982            if let DhcpOption::DomainNameServer(dns) = opt {
983                Some(dns)
984            } else {
985                None
986            }
987        });
988
989        assert!(dns_opt.is_some());
990        let dns = dns_opt.unwrap();
991        assert_eq!(dns.len(), 2);
992        assert_eq!(dns[0], Ipv4Addr::new(8, 8, 8, 8));
993        assert_eq!(dns[1], Ipv4Addr::new(8, 8, 4, 4));
994    }
995
996    #[test]
997    fn test_dhcp_display() {
998        let mut packet = create_basic_dhcp_discover();
999        packet.push(53);
1000        packet.push(1);
1001        packet.push(1); // DISCOVER
1002        packet.push(255);
1003
1004        let (dhcp, _) = DhcpHeader::from_bytes(&packet).unwrap();
1005        let display_str = format!("{}", dhcp);
1006
1007        assert!(
1008            display_str.contains("BOOTREQUEST"),
1009            "Display string: {}",
1010            display_str
1011        );
1012        assert!(
1013            display_str.contains("0x12345678"),
1014            "Display string: {}",
1015            display_str
1016        );
1017        // Message type is displayed from options
1018        let has_discover = dhcp
1019            .options()
1020            .any(|opt| matches!(opt, DhcpOption::MessageType(DhcpMessageType::DISCOVER)));
1021        assert!(has_discover);
1022        assert!(
1023            display_str.contains("aa:bb:cc:dd:ee:ff"),
1024            "Display string: {}",
1025            display_str
1026        );
1027    }
1028
1029    #[test]
1030    fn test_dhcp_broadcast_flag() {
1031        let mut packet = create_basic_dhcp_discover();
1032        packet.push(255);
1033
1034        let (dhcp, _) = DhcpHeader::from_bytes(&packet).unwrap();
1035        assert!(dhcp.is_broadcast());
1036
1037        // Test without broadcast flag
1038        packet[10..12].copy_from_slice(&0u16.to_be_bytes());
1039        let (dhcp2, _) = DhcpHeader::from_bytes(&packet).unwrap();
1040        assert!(!dhcp2.is_broadcast());
1041    }
1042
1043    #[test]
1044    fn test_dhcp_size_constants() {
1045        assert_eq!(std::mem::size_of::<DhcpHeader>(), 240);
1046        assert_eq!(DhcpHeader::FIXED_LEN, 240);
1047    }
1048
1049    #[test]
1050    fn test_dhcp_deref() {
1051        let mut packet = create_basic_dhcp_discover();
1052        packet.push(255);
1053
1054        let (dhcp, _) = DhcpHeader::from_bytes(&packet).unwrap();
1055
1056        // Test that Deref works
1057        assert_eq!(dhcp.op(), DhcpOpCode::BOOTREQUEST);
1058        assert_eq!(dhcp.xid(), 0x12345678);
1059    }
1060
1061    #[test]
1062    fn test_dhcp_unknown_option() {
1063        let mut packet = create_basic_dhcp_discover();
1064
1065        // Add unknown option
1066        packet.push(200); // unknown code
1067        packet.push(3);
1068        packet.extend_from_slice(&[1, 2, 3]);
1069
1070        packet.push(255);
1071
1072        let (dhcp, _) = DhcpHeader::from_bytes(&packet).unwrap();
1073
1074        let options: Vec<_> = dhcp.options().collect();
1075        assert!(options
1076            .iter()
1077            .any(|opt| matches!(opt, DhcpOption::Unknown { code: 200, .. })));
1078    }
1079
1080    #[test]
1081    fn test_dhcp_renewal_rebinding_times() {
1082        let mut packet = create_basic_dhcp_discover();
1083
1084        // Add Renewal Time (T1)
1085        packet.push(58);
1086        packet.push(4);
1087        packet.extend_from_slice(&43200u32.to_be_bytes()); // 12 hours
1088
1089        // Add Rebinding Time (T2)
1090        packet.push(59);
1091        packet.push(4);
1092        packet.extend_from_slice(&75600u32.to_be_bytes()); // 21 hours
1093
1094        packet.push(255);
1095
1096        let (dhcp, _) = DhcpHeader::from_bytes(&packet).unwrap();
1097
1098        let options: Vec<_> = dhcp.options().collect();
1099
1100        assert!(options
1101            .iter()
1102            .any(|opt| matches!(opt, DhcpOption::RenewalTime(43200))));
1103        assert!(options
1104            .iter()
1105            .any(|opt| matches!(opt, DhcpOption::RebindingTime(75600))));
1106    }
1107
1108    #[test]
1109    fn test_dhcp_real_world_discover() {
1110        // Simulate a real DHCP DISCOVER packet
1111        let mut packet = vec![
1112            1, // BOOTREQUEST
1113            1, // Ethernet
1114            6, // hlen
1115            0, // hops
1116        ];
1117
1118        packet.extend_from_slice(&0xABCDEF01u32.to_be_bytes());
1119        packet.extend_from_slice(&0u16.to_be_bytes());
1120        packet.extend_from_slice(&0x8000u16.to_be_bytes());
1121        packet.extend_from_slice(&[0; 4]); // ciaddr
1122        packet.extend_from_slice(&[0; 4]); // yiaddr
1123        packet.extend_from_slice(&[0; 4]); // siaddr
1124        packet.extend_from_slice(&[0; 4]); // giaddr
1125        packet.extend_from_slice(&[0x52, 0x54, 0x00, 0x12, 0x34, 0x56]);
1126        packet.extend_from_slice(&[0; 10]);
1127        packet.extend_from_slice(&[0; 64]);
1128        packet.extend_from_slice(&[0; 128]);
1129        packet.extend_from_slice(&DHCP_MAGIC_COOKIE.to_be_bytes());
1130
1131        // Options
1132        packet.push(53);
1133        packet.push(1);
1134        packet.push(1); // DISCOVER
1135
1136        packet.push(55); // Parameter Request List
1137        packet.push(4);
1138        packet.extend_from_slice(&[1, 3, 6, 15]); // subnet, router, dns, domain
1139
1140        packet.push(255);
1141
1142        let (dhcp, _) = DhcpHeader::from_bytes(&packet).unwrap();
1143
1144        assert_eq!(dhcp.op(), DhcpOpCode::BOOTREQUEST);
1145        assert_eq!(dhcp.xid(), 0xABCDEF01);
1146        assert!(dhcp.is_broadcast());
1147
1148        let msg_type = dhcp.options().find_map(|opt| {
1149            if let DhcpOption::MessageType(mt) = opt {
1150                Some(mt)
1151            } else {
1152                None
1153            }
1154        });
1155        assert_eq!(msg_type, Some(DhcpMessageType::DISCOVER));
1156    }
1157}