edge_dhcp/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2#![warn(clippy::large_futures)]
3#![allow(clippy::uninlined_format_args)]
4#![allow(unknown_lints)]
5
6/// This code is a `no_std` and no-alloc modification of https://github.com/krolaw/dhcp4r
7use core::str::Utf8Error;
8
9pub use core::net::Ipv4Addr;
10
11use num_enum::TryFromPrimitive;
12
13use edge_raw::bytes::{self, BytesIn, BytesOut};
14
15// This mod MUST go first, so that the others see its macros.
16pub(crate) mod fmt;
17
18pub mod client;
19pub mod server;
20
21#[cfg(feature = "io")]
22pub mod io;
23
24#[derive(Copy, Clone, Debug, Eq, PartialEq)]
25pub enum Error {
26    DataUnderflow,
27    BufferOverflow,
28    InvalidPacket,
29    InvalidUtf8Str(Utf8Error),
30    InvalidMessageType,
31    MissingCookie,
32    InvalidHlen,
33}
34
35impl From<bytes::Error> for Error {
36    fn from(value: bytes::Error) -> Self {
37        match value {
38            bytes::Error::BufferOverflow => Self::BufferOverflow,
39            bytes::Error::DataUnderflow => Self::DataUnderflow,
40            bytes::Error::InvalidFormat => Self::InvalidPacket,
41        }
42    }
43}
44
45impl core::fmt::Display for Error {
46    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
47        let str = match self {
48            Self::DataUnderflow => "Data underflow",
49            Self::BufferOverflow => "Buffer overflow",
50            Self::InvalidPacket => "Invalid packet",
51            Self::InvalidUtf8Str(_) => "Invalid Utf8 string",
52            Self::InvalidMessageType => "Invalid message type",
53            Self::MissingCookie => "Missing cookie",
54            Self::InvalidHlen => "Invalid hlen",
55        };
56
57        write!(f, "{}", str)
58    }
59}
60
61#[cfg(feature = "defmt")]
62impl defmt::Format for Error {
63    fn format(&self, f: defmt::Formatter<'_>) {
64        let str = match self {
65            Self::DataUnderflow => "Data underflow",
66            Self::BufferOverflow => "Buffer overflow",
67            Self::InvalidPacket => "Invalid packet",
68            Self::InvalidUtf8Str(_) => "Invalid Utf8 string",
69            Self::InvalidMessageType => "Invalid message type",
70            Self::MissingCookie => "Missing cookie",
71            Self::InvalidHlen => "Invalid hlen",
72        };
73
74        defmt::write!(f, "{}", str)
75    }
76}
77
78impl core::error::Error for Error {}
79
80///
81/// DHCP Message Type.
82///
83/// # Standards
84///
85/// The semantics of the various DHCP message types are described in RFC 2131 (see Table 2).
86/// Their numeric values are described in Section 9.6 of RFC 2132, which begins:
87///
88/// > This option is used to convey the type of the DHCP message.  The code for this option is 53,
89/// > and its length is 1.
90///
91#[derive(Copy, Clone, PartialEq, Eq, Debug, TryFromPrimitive)]
92#[repr(u8)]
93pub enum MessageType {
94    /// Client broadcast to locate available servers.
95    Discover = 1,
96
97    /// Server to client in response to DHCPDISCOVER with offer of configuration parameters.
98    Offer = 2,
99
100    /// Client message to servers either (a) requesting offered parameters from one server and
101    /// implicitly declining offers from all others, (b) confirming correctness of previously
102    /// allocated address after, e.g., system reboot, or (c) extending the lease on a particular
103    /// network address.
104    Request = 3,
105
106    /// Client to server indicating network address is already in use.
107    Decline = 4,
108
109    /// Server to client with configuration parameters, including committed network address.
110    Ack = 5,
111
112    /// Server to client indicating client's notion of network address is incorrect (e.g., client
113    /// has moved to new subnet) or client's lease as expired.
114    Nak = 6,
115
116    /// Client to server relinquishing network address and cancelling remaining lease.
117    Release = 7,
118
119    /// Client to server, asking only for local configuration parameters; client already has
120    /// externally configured network address.
121    Inform = 8,
122}
123
124impl core::fmt::Display for MessageType {
125    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
126        match self {
127            Self::Discover => "DHCPDISCOVER",
128            Self::Offer => "DHCPOFFER",
129            Self::Request => "DHCPREQUEST",
130            Self::Decline => "DHCPDECLINE",
131            Self::Ack => "DHCPACK",
132            Self::Nak => "DHCPNAK",
133            Self::Release => "DHCPRELEASE",
134            Self::Inform => "DHCPINFORM",
135        }
136        .fmt(f)
137    }
138}
139
140#[cfg(feature = "defmt")]
141impl defmt::Format for MessageType {
142    fn format(&self, f: defmt::Formatter<'_>) {
143        match self {
144            Self::Discover => "DHCPDISCOVER",
145            Self::Offer => "DHCPOFFER",
146            Self::Request => "DHCPREQUEST",
147            Self::Decline => "DHCPDECLINE",
148            Self::Ack => "DHCPACK",
149            Self::Nak => "DHCPNAK",
150            Self::Release => "DHCPRELEASE",
151            Self::Inform => "DHCPINFORM",
152        }
153        .format(f)
154    }
155}
156
157/// DHCP Packet Structure
158#[derive(Clone, PartialEq, Eq, Debug)]
159#[cfg_attr(feature = "defmt", derive(defmt::Format))]
160pub struct Packet<'a> {
161    pub reply: bool,
162    pub hops: u8,
163    pub xid: u32,
164    pub secs: u16,
165    pub broadcast: bool,
166    pub ciaddr: Ipv4Addr,
167    pub yiaddr: Ipv4Addr,
168    pub siaddr: Ipv4Addr,
169    pub giaddr: Ipv4Addr,
170    pub chaddr: [u8; 16],
171    pub options: Options<'a>,
172}
173
174impl<'a> Packet<'a> {
175    const COOKIE: [u8; 4] = [99, 130, 83, 99];
176
177    const BOOT_REQUEST: u8 = 1; // From Client
178    const BOOT_REPLY: u8 = 2; // From Server
179
180    const SERVER_NAME_AND_FILE_NAME: usize = 64 + 128;
181
182    const END: u8 = 255;
183    const PAD: u8 = 0;
184
185    pub fn new_request(
186        mac: [u8; 6],
187        xid: u32,
188        secs: u16,
189        our_ip: Option<Ipv4Addr>,
190        broadcast: bool,
191        options: Options<'a>,
192    ) -> Self {
193        let mut chaddr = [0; 16];
194        chaddr[..6].copy_from_slice(&mac);
195
196        Self {
197            reply: false,
198            hops: 0,
199            xid,
200            secs,
201            broadcast,
202            ciaddr: our_ip.unwrap_or(Ipv4Addr::UNSPECIFIED),
203            yiaddr: our_ip.unwrap_or(Ipv4Addr::UNSPECIFIED),
204            siaddr: Ipv4Addr::UNSPECIFIED,
205            giaddr: Ipv4Addr::UNSPECIFIED,
206            chaddr,
207            options,
208        }
209    }
210
211    pub fn new_reply<'b>(&self, ip: Option<Ipv4Addr>, options: Options<'b>) -> Packet<'b> {
212        let mut ciaddr = Ipv4Addr::UNSPECIFIED;
213        if ip.is_some() {
214            for opt in self.options.iter() {
215                if matches!(opt, DhcpOption::MessageType(MessageType::Request)) {
216                    ciaddr = self.ciaddr;
217                    break;
218                }
219            }
220        }
221
222        Packet {
223            reply: true,
224            hops: 0,
225            xid: self.xid,
226            secs: 0,
227            broadcast: self.broadcast,
228            ciaddr,
229            yiaddr: ip.unwrap_or(Ipv4Addr::UNSPECIFIED),
230            siaddr: Ipv4Addr::UNSPECIFIED,
231            giaddr: self.giaddr,
232            chaddr: self.chaddr,
233            options,
234        }
235    }
236
237    pub fn is_for_us(&self, mac: &[u8; 6], xid: u32) -> bool {
238        const MAC_TRAILING_ZEROS: [u8; 10] = [0; 10];
239
240        self.chaddr[0..6] == *mac
241            && self.chaddr[6..16] == MAC_TRAILING_ZEROS
242            && self.xid == xid
243            && self.reply
244    }
245
246    /// Parses the packet from a byte slice
247    pub fn decode(data: &'a [u8]) -> Result<Self, Error> {
248        let mut bytes = BytesIn::new(data);
249
250        Ok(Self {
251            reply: {
252                let reply = bytes.byte()? == Self::BOOT_REPLY;
253                let _htype = bytes.byte()?; // Hardware address type; 1 = 10Mb Ethernet
254                let hlen = bytes.byte()?;
255
256                if hlen != 6 {
257                    Err(Error::InvalidHlen)?;
258                }
259
260                reply
261            },
262            hops: bytes.byte()?,
263            xid: u32::from_be_bytes(bytes.arr()?),
264            secs: u16::from_be_bytes(bytes.arr()?),
265            broadcast: u16::from_be_bytes(bytes.arr()?) & 128 != 0,
266            ciaddr: bytes.arr()?.into(),
267            yiaddr: bytes.arr()?.into(),
268            siaddr: bytes.arr()?.into(),
269            giaddr: bytes.arr()?.into(),
270            chaddr: bytes.arr()?,
271            options: {
272                for _ in 0..Self::SERVER_NAME_AND_FILE_NAME {
273                    bytes.byte()?;
274                }
275
276                if bytes.arr()? != Self::COOKIE {
277                    Err(Error::MissingCookie)?;
278                }
279
280                Options(OptionsInner::decode(bytes.remaining())?)
281            },
282        })
283    }
284
285    /// Encodes the packet into the provided buf slice
286    pub fn encode<'o>(&self, buf: &'o mut [u8]) -> Result<&'o [u8], Error> {
287        let mut bytes = BytesOut::new(buf);
288
289        bytes
290            .push(&[if self.reply {
291                Self::BOOT_REPLY
292            } else {
293                Self::BOOT_REQUEST
294            }])?
295            .byte(1)?
296            .byte(6)?
297            .byte(self.hops)?
298            .push(&u32::to_be_bytes(self.xid))?
299            .push(&u16::to_be_bytes(self.secs))?
300            .push(&u16::to_be_bytes(if self.broadcast { 128 } else { 0 }))?
301            .push(&self.ciaddr.octets())?
302            .push(&self.yiaddr.octets())?
303            .push(&self.siaddr.octets())?
304            .push(&self.giaddr.octets())?
305            .push(&self.chaddr)?;
306
307        for _ in 0..Self::SERVER_NAME_AND_FILE_NAME {
308            bytes.byte(0)?;
309        }
310
311        bytes.push(&Self::COOKIE)?;
312
313        self.options.0.encode(&mut bytes)?;
314
315        bytes.byte(Self::END)?;
316
317        while bytes.len() < 272 {
318            bytes.byte(Self::PAD)?;
319        }
320
321        let len = bytes.len();
322
323        Ok(&buf[..len])
324    }
325}
326
327#[derive(Clone, Debug)]
328#[cfg_attr(feature = "defmt", derive(defmt::Format))]
329#[non_exhaustive]
330pub struct Settings<'a> {
331    pub ip: Ipv4Addr,
332    pub server_ip: Option<Ipv4Addr>,
333    pub lease_time_secs: Option<u32>,
334    pub gateway: Option<Ipv4Addr>,
335    pub subnet: Option<Ipv4Addr>,
336    pub dns1: Option<Ipv4Addr>,
337    pub dns2: Option<Ipv4Addr>,
338    pub captive_url: Option<&'a str>,
339}
340
341impl<'a> Settings<'a> {
342    pub fn new(packet: &Packet<'a>) -> Self {
343        Self {
344            ip: packet.yiaddr,
345            server_ip: packet.options.iter().find_map(|option| {
346                if let DhcpOption::ServerIdentifier(ip) = option {
347                    Some(ip)
348                } else {
349                    None
350                }
351            }),
352            lease_time_secs: packet.options.iter().find_map(|option| {
353                if let DhcpOption::IpAddressLeaseTime(lease_time_secs) = option {
354                    Some(lease_time_secs)
355                } else {
356                    None
357                }
358            }),
359            gateway: packet.options.iter().find_map(|option| {
360                if let DhcpOption::Router(ips) = option {
361                    ips.iter().next()
362                } else {
363                    None
364                }
365            }),
366            subnet: packet.options.iter().find_map(|option| {
367                if let DhcpOption::SubnetMask(subnet) = option {
368                    Some(subnet)
369                } else {
370                    None
371                }
372            }),
373            dns1: packet.options.iter().find_map(|option| {
374                if let DhcpOption::DomainNameServer(ips) = option {
375                    ips.iter().next()
376                } else {
377                    None
378                }
379            }),
380            dns2: packet.options.iter().find_map(|option| {
381                if let DhcpOption::DomainNameServer(ips) = option {
382                    ips.iter().nth(1)
383                } else {
384                    None
385                }
386            }),
387            captive_url: packet.options.iter().find_map(|option| {
388                if let DhcpOption::CaptiveUrl(url) = option {
389                    Some(url)
390                } else {
391                    None
392                }
393            }),
394        }
395    }
396}
397
398#[derive(Clone, PartialEq, Eq)]
399#[cfg_attr(feature = "defmt", derive(defmt::Format))]
400pub struct Options<'a>(OptionsInner<'a>);
401
402impl<'a> Options<'a> {
403    const REQUEST_PARAMS: &'static [u8] = &[
404        DhcpOption::CODE_ROUTER,
405        DhcpOption::CODE_SUBNET,
406        DhcpOption::CODE_DNS,
407    ];
408
409    pub const fn new(options: &'a [DhcpOption<'a>]) -> Self {
410        Self(OptionsInner::DataSlice(options))
411    }
412
413    #[inline(always)]
414    pub const fn buf() -> [DhcpOption<'a>; 8] {
415        [DhcpOption::Message(""); 8]
416    }
417
418    pub fn discover(requested_ip: Option<Ipv4Addr>, buf: &'a mut [DhcpOption<'a>]) -> Self {
419        buf[0] = DhcpOption::MessageType(MessageType::Discover);
420
421        let mut offset = 1;
422
423        if let Some(requested_ip) = requested_ip {
424            buf[1] = DhcpOption::RequestedIpAddress(requested_ip);
425            offset += 1;
426        }
427
428        Self::new(&buf[..offset])
429    }
430
431    pub fn request(ip: Ipv4Addr, buf: &'a mut [DhcpOption<'a>]) -> Self {
432        buf[0] = DhcpOption::MessageType(MessageType::Request);
433        buf[1] = DhcpOption::RequestedIpAddress(ip);
434        buf[2] = DhcpOption::ParameterRequestList(Self::REQUEST_PARAMS);
435
436        Self::new(&buf[..3])
437    }
438
439    pub fn release(buf: &'a mut [DhcpOption<'a>]) -> Self {
440        buf[0] = DhcpOption::MessageType(MessageType::Release);
441
442        Self::new(&buf[..1])
443    }
444
445    pub fn decline(buf: &'a mut [DhcpOption<'a>]) -> Self {
446        buf[0] = DhcpOption::MessageType(MessageType::Decline);
447
448        Self::new(&buf[..1])
449    }
450
451    #[allow(clippy::too_many_arguments)]
452    pub fn reply<'b>(
453        &self,
454        mt: MessageType,
455        server_ip: Ipv4Addr,
456        lease_duration_secs: u32,
457        gateways: &'b [Ipv4Addr],
458        subnet: Option<Ipv4Addr>,
459        dns: &'b [Ipv4Addr],
460        captive_url: Option<&'b str>,
461        buf: &'b mut [DhcpOption<'b>],
462    ) -> Options<'b> {
463        let requested = self.iter().find_map(|option| {
464            if let DhcpOption::ParameterRequestList(requested) = option {
465                Some(requested)
466            } else {
467                None
468            }
469        });
470
471        Options::internal_reply(
472            requested,
473            mt,
474            server_ip,
475            lease_duration_secs,
476            gateways,
477            subnet,
478            dns,
479            captive_url,
480            buf,
481        )
482    }
483
484    #[allow(clippy::too_many_arguments)]
485    fn internal_reply(
486        requested: Option<&[u8]>,
487        mt: MessageType,
488        server_ip: Ipv4Addr,
489        lease_duration_secs: u32,
490        gateways: &'a [Ipv4Addr],
491        subnet: Option<Ipv4Addr>,
492        dns: &'a [Ipv4Addr],
493        captive_url: Option<&'a str>,
494        buf: &'a mut [DhcpOption<'a>],
495    ) -> Self {
496        buf[0] = DhcpOption::MessageType(mt);
497        buf[1] = DhcpOption::ServerIdentifier(server_ip);
498        buf[2] = DhcpOption::IpAddressLeaseTime(lease_duration_secs);
499
500        let mut offset = 3;
501
502        if !matches!(mt, MessageType::Nak) {
503            if let Some(requested) = requested {
504                for code in requested {
505                    if !buf[0..offset].iter().any(|option| option.code() == *code) {
506                        let option = match *code {
507                            DhcpOption::CODE_ROUTER => (!gateways.is_empty())
508                                .then_some(DhcpOption::Router(Ipv4Addrs::new(gateways))),
509                            DhcpOption::CODE_DNS => (!dns.is_empty())
510                                .then_some(DhcpOption::DomainNameServer(Ipv4Addrs::new(dns))),
511                            DhcpOption::CODE_SUBNET => subnet.map(DhcpOption::SubnetMask),
512                            DhcpOption::CODE_CAPTIVE_URL => captive_url.map(DhcpOption::CaptiveUrl),
513                            _ => None,
514                        };
515
516                        if let Some(option) = option {
517                            buf[offset] = option;
518                            offset += 1;
519                        }
520                    }
521
522                    if offset == buf.len() {
523                        break;
524                    }
525                }
526            }
527        }
528
529        Self::new(&buf[..offset])
530    }
531
532    pub fn iter(&self) -> impl Iterator<Item = DhcpOption<'a>> + 'a {
533        self.0.iter()
534    }
535
536    pub(crate) fn requested_ip(&self) -> Option<Ipv4Addr> {
537        self.iter().find_map(|option| {
538            if let DhcpOption::RequestedIpAddress(ip) = option {
539                Some(ip)
540            } else {
541                None
542            }
543        })
544    }
545}
546
547impl core::fmt::Debug for Options<'_> {
548    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
549        f.debug_set().entries(self.iter()).finish()
550    }
551}
552
553#[derive(Clone, PartialEq, Eq, Debug)]
554#[cfg_attr(feature = "defmt", derive(defmt::Format))]
555enum OptionsInner<'a> {
556    ByteSlice(&'a [u8]),
557    DataSlice(&'a [DhcpOption<'a>]),
558}
559
560impl<'a> OptionsInner<'a> {
561    fn decode(data: &'a [u8]) -> Result<Self, Error> {
562        let mut bytes = BytesIn::new(data);
563
564        while DhcpOption::decode(&mut bytes)?.is_some() {}
565
566        Ok(Self::ByteSlice(data))
567    }
568
569    fn encode(&self, buf: &mut BytesOut) -> Result<(), Error> {
570        for option in self.iter() {
571            option.encode(buf)?;
572        }
573
574        Ok(())
575    }
576
577    fn iter(&self) -> impl Iterator<Item = DhcpOption<'a>> + 'a {
578        struct ByteSliceDhcpOptions<'a>(BytesIn<'a>);
579
580        impl<'a> Iterator for ByteSliceDhcpOptions<'a> {
581            type Item = DhcpOption<'a>;
582
583            fn next(&mut self) -> Option<Self::Item> {
584                if self.0.is_empty() {
585                    None
586                } else {
587                    unwrap!(DhcpOption::decode(&mut self.0))
588                }
589            }
590        }
591
592        match self {
593            Self::ByteSlice(data) => {
594                EitherIterator::First(ByteSliceDhcpOptions(BytesIn::new(data)))
595            }
596            Self::DataSlice(data) => EitherIterator::Second(data.iter().cloned()),
597        }
598    }
599}
600
601#[derive(Copy, Clone, PartialEq, Eq, Debug)]
602#[cfg_attr(feature = "defmt", derive(defmt::Format))]
603pub enum DhcpOption<'a> {
604    /// 53: DHCP Message Type
605    MessageType(MessageType),
606    /// 54: Server Identifier
607    ServerIdentifier(Ipv4Addr),
608    /// 55: Parameter Request List
609    ParameterRequestList(&'a [u8]),
610    /// 50: Requested IP Address
611    RequestedIpAddress(Ipv4Addr),
612    /// 12: Host Name Option
613    HostName(&'a str),
614    /// 3: Router Option
615    Router(Ipv4Addrs<'a>),
616    /// 6: Domain Name Server Option
617    DomainNameServer(Ipv4Addrs<'a>),
618    /// 51: IP Address Lease Time
619    IpAddressLeaseTime(u32),
620    /// 1: Subnet Mask
621    SubnetMask(Ipv4Addr),
622    /// 56: Message
623    Message(&'a str),
624    /// 57: Maximum DHCP Message Size
625    MaximumMessageSize(u16),
626    /// 61: Client-identifier
627    ClientIdentifier(&'a [u8]),
628    /// 114: Captive-portal URL
629    CaptiveUrl(&'a str),
630    // Other (unrecognized)
631    Unrecognized(u8, &'a [u8]),
632}
633
634impl DhcpOption<'_> {
635    pub const CODE_ROUTER: u8 = DhcpOption::Router(Ipv4Addrs::new(&[])).code();
636    pub const CODE_DNS: u8 = DhcpOption::DomainNameServer(Ipv4Addrs::new(&[])).code();
637    pub const CODE_SUBNET: u8 = DhcpOption::SubnetMask(Ipv4Addr::new(0, 0, 0, 0)).code();
638    pub const CODE_CAPTIVE_URL: u8 = DhcpOption::CaptiveUrl("").code();
639
640    fn decode<'o>(bytes: &mut BytesIn<'o>) -> Result<Option<DhcpOption<'o>>, Error> {
641        let code = bytes.byte()?;
642        if code == Packet::END {
643            Ok(None)
644        } else {
645            let len = bytes.byte()? as usize;
646            let mut bytes = BytesIn::new(bytes.slice(len)?);
647
648            let option = match code {
649                DHCP_MESSAGE_TYPE => DhcpOption::MessageType(
650                    TryFromPrimitive::try_from_primitive(bytes.remaining_byte()?)
651                        .map_err(|_| Error::InvalidMessageType)?,
652                ),
653                SERVER_IDENTIFIER => {
654                    DhcpOption::ServerIdentifier(Ipv4Addr::from(bytes.remaining_arr()?))
655                }
656                PARAMETER_REQUEST_LIST => DhcpOption::ParameterRequestList(bytes.remaining()),
657                REQUESTED_IP_ADDRESS => {
658                    DhcpOption::RequestedIpAddress(Ipv4Addr::from(bytes.remaining_arr()?))
659                }
660                HOST_NAME => DhcpOption::HostName(
661                    core::str::from_utf8(bytes.remaining()).map_err(Error::InvalidUtf8Str)?,
662                ),
663                MAXIMUM_DHCP_MESSAGE_SIZE => {
664                    DhcpOption::MaximumMessageSize(u16::from_be_bytes(bytes.remaining_arr()?))
665                }
666                ROUTER => {
667                    DhcpOption::Router(Ipv4Addrs(Ipv4AddrsInner::ByteSlice(bytes.remaining())))
668                }
669                DOMAIN_NAME_SERVER => DhcpOption::DomainNameServer(Ipv4Addrs(
670                    Ipv4AddrsInner::ByteSlice(bytes.remaining()),
671                )),
672                IP_ADDRESS_LEASE_TIME => {
673                    DhcpOption::IpAddressLeaseTime(u32::from_be_bytes(bytes.remaining_arr()?))
674                }
675                SUBNET_MASK => DhcpOption::SubnetMask(Ipv4Addr::from(bytes.remaining_arr()?)),
676                MESSAGE => DhcpOption::Message(
677                    core::str::from_utf8(bytes.remaining()).map_err(Error::InvalidUtf8Str)?,
678                ),
679                CLIENT_IDENTIFIER => {
680                    if len < 2 {
681                        return Err(Error::DataUnderflow);
682                    }
683
684                    DhcpOption::ClientIdentifier(bytes.remaining())
685                }
686                CAPTIVE_URL => DhcpOption::HostName(
687                    core::str::from_utf8(bytes.remaining()).map_err(Error::InvalidUtf8Str)?,
688                ),
689                _ => DhcpOption::Unrecognized(code, bytes.remaining()),
690            };
691
692            Ok(Some(option))
693        }
694    }
695
696    fn encode(&self, out: &mut BytesOut) -> Result<(), Error> {
697        out.byte(self.code())?;
698
699        self.data(|data| {
700            out.byte(data.len() as _)?;
701            out.push(data)?;
702
703            Ok(())
704        })
705    }
706
707    pub const fn code(&self) -> u8 {
708        match self {
709            Self::MessageType(_) => DHCP_MESSAGE_TYPE,
710            Self::ServerIdentifier(_) => SERVER_IDENTIFIER,
711            Self::ParameterRequestList(_) => PARAMETER_REQUEST_LIST,
712            Self::RequestedIpAddress(_) => REQUESTED_IP_ADDRESS,
713            Self::HostName(_) => HOST_NAME,
714            Self::Router(_) => ROUTER,
715            Self::DomainNameServer(_) => DOMAIN_NAME_SERVER,
716            Self::IpAddressLeaseTime(_) => IP_ADDRESS_LEASE_TIME,
717            Self::SubnetMask(_) => SUBNET_MASK,
718            Self::MaximumMessageSize(_) => MAXIMUM_DHCP_MESSAGE_SIZE,
719            Self::Message(_) => MESSAGE,
720            Self::ClientIdentifier(_) => CLIENT_IDENTIFIER,
721            Self::CaptiveUrl(_) => CAPTIVE_URL,
722            Self::Unrecognized(code, _) => *code,
723        }
724    }
725
726    fn data(&self, mut f: impl FnMut(&[u8]) -> Result<(), Error>) -> Result<(), Error> {
727        match self {
728            Self::MessageType(mtype) => f(&[*mtype as _]),
729            Self::ServerIdentifier(addr) => f(&addr.octets()),
730            Self::ParameterRequestList(prl) => f(prl),
731            Self::RequestedIpAddress(addr) => f(&addr.octets()),
732            Self::HostName(name) => f(name.as_bytes()),
733            Self::Router(addrs) | Self::DomainNameServer(addrs) => {
734                for addr in addrs.iter() {
735                    f(&addr.octets())?;
736                }
737
738                Ok(())
739            }
740            Self::IpAddressLeaseTime(secs) => f(&secs.to_be_bytes()),
741            Self::SubnetMask(mask) => f(&mask.octets()),
742            Self::Message(msg) => f(msg.as_bytes()),
743            Self::MaximumMessageSize(size) => f(&size.to_be_bytes()),
744            Self::ClientIdentifier(id) => f(id),
745            Self::CaptiveUrl(name) => f(name.as_bytes()),
746            Self::Unrecognized(_, data) => f(data),
747        }
748    }
749}
750
751#[derive(Copy, Clone, PartialEq, Eq, Debug)]
752#[cfg_attr(feature = "defmt", derive(defmt::Format))]
753pub struct Ipv4Addrs<'a>(Ipv4AddrsInner<'a>);
754
755impl<'a> Ipv4Addrs<'a> {
756    pub const fn new(addrs: &'a [Ipv4Addr]) -> Self {
757        Self(Ipv4AddrsInner::DataSlice(addrs))
758    }
759
760    pub fn iter(&self) -> impl Iterator<Item = Ipv4Addr> + 'a {
761        self.0.iter()
762    }
763}
764
765#[derive(Copy, Clone, PartialEq, Eq, Debug)]
766#[cfg_attr(feature = "defmt", derive(defmt::Format))]
767enum Ipv4AddrsInner<'a> {
768    ByteSlice(&'a [u8]),
769    DataSlice(&'a [Ipv4Addr]),
770}
771
772impl<'a> Ipv4AddrsInner<'a> {
773    fn iter(&self) -> impl Iterator<Item = Ipv4Addr> + 'a {
774        match self {
775            Self::ByteSlice(data) => {
776                EitherIterator::First((0..data.len()).step_by(4).map(|offset| {
777                    let octets: [u8; 4] = unwrap!(data[offset..offset + 4].try_into());
778
779                    octets.into()
780                }))
781            }
782            Self::DataSlice(data) => EitherIterator::Second(data.iter().cloned()),
783        }
784    }
785}
786
787enum EitherIterator<F, S> {
788    First(F),
789    Second(S),
790}
791
792impl<F, S> Iterator for EitherIterator<F, S>
793where
794    F: Iterator,
795    S: Iterator<Item = F::Item>,
796{
797    type Item = F::Item;
798
799    fn next(&mut self) -> Option<Self::Item> {
800        match self {
801            Self::First(iter) => iter.next(),
802            Self::Second(iter) => iter.next(),
803        }
804    }
805}
806
807// DHCP Options
808const SUBNET_MASK: u8 = 1;
809const ROUTER: u8 = 3;
810const DOMAIN_NAME_SERVER: u8 = 6;
811const HOST_NAME: u8 = 12;
812
813// DHCP Extensions
814const REQUESTED_IP_ADDRESS: u8 = 50;
815const IP_ADDRESS_LEASE_TIME: u8 = 51;
816const DHCP_MESSAGE_TYPE: u8 = 53;
817const SERVER_IDENTIFIER: u8 = 54;
818const PARAMETER_REQUEST_LIST: u8 = 55;
819const MESSAGE: u8 = 56;
820const MAXIMUM_DHCP_MESSAGE_SIZE: u8 = 57;
821const CLIENT_IDENTIFIER: u8 = 61;
822const CAPTIVE_URL: u8 = 114;