dhcp_client_core/
parse.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Parsing and serialization of DHCP messages
6
7use dhcp_protocol::{AtLeast, AtMostBytes};
8use diagnostics_traits::Inspector;
9use packet::{InnerPacketBuilder, ParseBuffer as _, Serializer};
10use packet_formats::ip::IpPacket as _;
11use std::net::Ipv4Addr;
12use std::num::{NonZeroU16, NonZeroU32, TryFromIntError};
13
14use crate::inspect::Counter;
15
16#[derive(thiserror::Error, Debug)]
17pub(crate) enum ParseError {
18    #[error("parsing IPv4 packet: {0}")]
19    Ipv4(packet_formats::error::IpParseError<net_types::ip::Ipv4>),
20    #[error("IPv4 packet protocol was not UDP")]
21    NotUdp,
22    #[error("parsing UDP datagram: {0}")]
23    Udp(packet_formats::error::ParseError),
24    #[error("incoming packet destined for wrong port: {0}")]
25    WrongPort(NonZeroU16),
26    #[error("incoming packet has wrong source address: {0}")]
27    WrongSource(std::net::SocketAddr),
28    #[error("parsing DHCP message: {0}")]
29    Dhcp(dhcp_protocol::ProtocolError),
30}
31
32/// Parses a DHCP message from the bytes of an IP packet. This function does not
33/// expect to parse a packet with link-layer headers; the buffer may only
34/// include bytes for the IP layer and above.
35/// NOTE: does not handle IP fragmentation.
36pub(crate) fn parse_dhcp_message_from_ip_packet(
37    mut bytes: &[u8],
38    expected_dst_port: NonZeroU16,
39) -> Result<(net_types::ip::Ipv4Addr, dhcp_protocol::Message), ParseError> {
40    let ip_packet =
41        bytes.parse::<packet_formats::ipv4::Ipv4Packet<_>>().map_err(ParseError::Ipv4)?;
42
43    let src_addr = ip_packet.src_ip();
44
45    match ip_packet.proto() {
46        packet_formats::ip::Ipv4Proto::Proto(packet_formats::ip::IpProto::Udp) => (),
47        packet_formats::ip::Ipv4Proto::Proto(packet_formats::ip::IpProto::Tcp)
48        | packet_formats::ip::Ipv4Proto::Icmp
49        | packet_formats::ip::Ipv4Proto::Igmp
50        | packet_formats::ip::Ipv4Proto::Proto(packet_formats::ip::IpProto::Reserved)
51        | packet_formats::ip::Ipv4Proto::Other(_) => return Err(ParseError::NotUdp),
52    };
53    let mut ip_packet_body = ip_packet.body();
54
55    let udp_packet = ip_packet_body
56        .parse_with::<_, packet_formats::udp::UdpPacket<_>>(packet_formats::udp::UdpParseArgs::new(
57            ip_packet.src_ip(),
58            ip_packet.dst_ip(),
59        ))
60        .map_err(ParseError::Udp)?;
61    let dst_port = udp_packet.dst_port();
62    if dst_port != expected_dst_port {
63        return Err(ParseError::WrongPort(dst_port));
64    }
65    dhcp_protocol::Message::from_buffer(udp_packet.body())
66        .map(|msg| (src_addr, msg))
67        .map_err(ParseError::Dhcp)
68}
69
70const DEFAULT_TTL: u8 = 64;
71
72/// Serializes a DHCP message to the bytes of an IP packet. Includes IP header
73/// but not link-layer headers.
74pub(crate) fn serialize_dhcp_message_to_ip_packet(
75    message: dhcp_protocol::Message,
76    src_ip: impl Into<net_types::ip::Ipv4Addr>,
77    src_port: NonZeroU16,
78    dst_ip: impl Into<net_types::ip::Ipv4Addr>,
79    dst_port: NonZeroU16,
80) -> impl AsRef<[u8]> {
81    let message = message.serialize();
82    let src_ip = src_ip.into();
83    let dst_ip = dst_ip.into();
84
85    let udp_builder =
86        packet_formats::udp::UdpPacketBuilder::new(src_ip, dst_ip, Some(src_port), dst_port);
87
88    let ipv4_builder = packet_formats::ipv4::Ipv4PacketBuilder::new(
89        src_ip,
90        dst_ip,
91        DEFAULT_TTL,
92        packet_formats::ip::Ipv4Proto::Proto(packet_formats::ip::IpProto::Udp),
93    );
94
95    match message.into_serializer().wrap_in(udp_builder).wrap_in(ipv4_builder).serialize_vec_outer()
96    {
97        Ok(buf) => buf,
98        Err(e) => {
99            let (e, _serializer) = e;
100            match e {
101                packet::SerializeError::SizeLimitExceeded => {
102                    unreachable!("no MTU constraints on serializer")
103                }
104            }
105        }
106    }
107}
108
109#[derive(derive_builder::Builder, Debug, PartialEq)]
110#[builder(private, build_fn(error = "CommonIncomingMessageError"))]
111struct CommonIncomingMessageFields {
112    message_type: dhcp_protocol::MessageType,
113    #[builder(setter(custom), default)]
114    server_identifier: Option<net_types::SpecifiedAddr<net_types::ip::Ipv4Addr>>,
115    #[builder(setter(custom), default)]
116    yiaddr: Option<net_types::SpecifiedAddr<net_types::ip::Ipv4Addr>>,
117    #[builder(setter(strip_option), default)]
118    ip_address_lease_time_secs: Option<NonZeroU32>,
119    // While it's nonsensical to have a 0-valued lease time, it's somewhat more
120    // reasonable to set the renewal time value to 0 (prompting the client to
121    // begin the renewal process immediately).
122    #[builder(setter(strip_option), default)]
123    renewal_time_value_secs: Option<u32>,
124    // Same holds for the rebinding time.
125    #[builder(setter(strip_option), default)]
126    rebinding_time_value_secs: Option<u32>,
127    #[builder(default)]
128    parameters: Vec<dhcp_protocol::DhcpOption>,
129    #[builder(setter(strip_option), default)]
130    message: Option<String>,
131    #[builder(setter(strip_option), default)]
132    client_identifier: Option<AtLeast<2, AtMostBytes<{ dhcp_protocol::U8_MAX_AS_USIZE }, Vec<u8>>>>,
133    #[builder(setter(custom))]
134    seen_option_codes: OptionCodeSet,
135}
136
137#[derive(thiserror::Error, Debug, PartialEq)]
138pub(crate) enum CommonIncomingMessageError {
139    #[error("got op = {0}, want op = BOOTREPLY")]
140    NotBootReply(dhcp_protocol::OpCode),
141    #[error("server identifier was the unspecified address")]
142    UnspecifiedServerIdentifier,
143    #[error("missing: {0}")]
144    BuilderMissingField(&'static str),
145    #[error("option's inclusion violates protocol: {0:?}")]
146    IllegallyIncludedOption(dhcp_protocol::OptionCode),
147}
148
149impl From<derive_builder::UninitializedFieldError> for CommonIncomingMessageError {
150    fn from(value: derive_builder::UninitializedFieldError) -> Self {
151        // `derive_builder::UninitializedFieldError` cannot be destructured
152        // because its fields are private.
153        Self::BuilderMissingField(value.field_name())
154    }
155}
156
157/// Counters for reasons an incoming message was discarded.
158#[derive(Default, Debug)]
159pub(crate) struct CommonIncomingMessageErrorCounters {
160    /// The incoming message was a BOOTREQUEST rather than a BOOTREPLY.
161    pub(crate) not_boot_reply: Counter,
162    /// The incoming message provided the unspecified address as the server
163    /// identifier.
164    pub(crate) unspecified_server_identifier: Counter,
165    /// The parser was unable to populate a required field while consuming the
166    /// message.
167    pub(crate) parser_missing_field: Counter,
168    /// The incoming message included an option that was illegal to include
169    /// according to spec.
170    pub(crate) illegally_included_option: Counter,
171}
172
173impl CommonIncomingMessageErrorCounters {
174    /// Records the counters.
175    fn record(&self, inspector: &mut impl Inspector) {
176        let Self {
177            not_boot_reply,
178            unspecified_server_identifier,
179            parser_missing_field,
180            illegally_included_option,
181        } = self;
182        inspector.record_usize("NotBootReply", not_boot_reply.load());
183        inspector.record_usize("UnspecifiedServerIdentifier", unspecified_server_identifier.load());
184        inspector.record_usize("ParserMissingField", parser_missing_field.load());
185        inspector.record_usize("IllegallyIncludedOption", illegally_included_option.load());
186    }
187
188    /// Increments the counter corresponding to the error.
189    fn increment(&self, error: &CommonIncomingMessageError) {
190        let Self {
191            not_boot_reply,
192            unspecified_server_identifier,
193            parser_missing_field,
194            illegally_included_option,
195        } = self;
196        match error {
197            CommonIncomingMessageError::NotBootReply(_) => not_boot_reply.increment(),
198            CommonIncomingMessageError::UnspecifiedServerIdentifier => {
199                unspecified_server_identifier.increment()
200            }
201            CommonIncomingMessageError::BuilderMissingField(_) => parser_missing_field.increment(),
202            CommonIncomingMessageError::IllegallyIncludedOption(_) => {
203                illegally_included_option.increment()
204            }
205        }
206    }
207}
208
209impl CommonIncomingMessageFieldsBuilder {
210    fn ignore_unused_result(&mut self) {}
211
212    fn add_requested_parameter(&mut self, option: dhcp_protocol::DhcpOption) {
213        let parameters = self.parameters.get_or_insert_with(Default::default);
214        parameters.push(option)
215    }
216
217    fn add_seen_option_and_return_whether_newly_added(
218        &mut self,
219        option_code: dhcp_protocol::OptionCode,
220    ) -> bool {
221        self.seen_option_codes.get_or_insert_with(Default::default).insert(option_code)
222    }
223
224    fn server_identifier(&mut self, addr: Ipv4Addr) -> Result<(), CommonIncomingMessageError> {
225        self.server_identifier = Some(Some(
226            net_types::SpecifiedAddr::new(net_types::ip::Ipv4Addr::from(addr))
227                .ok_or(CommonIncomingMessageError::UnspecifiedServerIdentifier)?,
228        ));
229        Ok(())
230    }
231
232    fn yiaddr(&mut self, addr: Ipv4Addr) {
233        match net_types::SpecifiedAddr::new(net_types::ip::Ipv4Addr::from(addr)) {
234            None => {
235                // Unlike with the Server Identifier option, it is not an error
236                // to set `yiaddr` to the unspecified address, as there is no
237                // other way to indicate its absence (it has its own field in
238                // the DHCP message rather than appearing in the list of
239                // options).
240            }
241            Some(specified_addr) => {
242                self.yiaddr = Some(Some(specified_addr));
243            }
244        }
245    }
246}
247
248/// Represents a `Map<OptionCode, T>` as an array of booleans.
249#[derive(Clone, PartialEq, Debug)]
250pub struct OptionCodeMap<T> {
251    inner: [Option<T>; dhcp_protocol::U8_MAX_AS_USIZE],
252}
253
254impl<T: Copy> OptionCodeMap<T> {
255    /// Constructs an empty `OptionCodeMap`.
256    pub fn new() -> Self {
257        OptionCodeMap { inner: [None; dhcp_protocol::U8_MAX_AS_USIZE] }
258    }
259
260    /// Puts `(option_code, value)` into the map, returning the previously-associated
261    /// value if is one.
262    pub fn put(&mut self, option_code: dhcp_protocol::OptionCode, value: T) -> Option<T> {
263        std::mem::replace(&mut self.inner[usize::from(u8::from(option_code))], Some(value))
264    }
265
266    /// Gets the value associated with `option_code` from the map, if there is one.
267    pub fn get(&self, option_code: dhcp_protocol::OptionCode) -> Option<T> {
268        self.inner[usize::from(u8::from(option_code))]
269    }
270
271    /// Checks if `option_code` is present in the map.
272    pub fn contains(&self, option_code: dhcp_protocol::OptionCode) -> bool {
273        self.get(option_code).is_some()
274    }
275
276    pub(crate) fn iter(&self) -> impl Iterator<Item = (dhcp_protocol::OptionCode, T)> + '_ {
277        self.inner.iter().enumerate().filter_map(|(index, value)| {
278            let option_code = u8::try_from(index)
279                .ok()
280                .and_then(|i| dhcp_protocol::OptionCode::try_from(i).ok())?;
281            let value = *value.as_ref()?;
282            Some((option_code, value))
283        })
284    }
285
286    pub(crate) fn iter_keys(&self) -> impl Iterator<Item = dhcp_protocol::OptionCode> + '_ {
287        self.iter().map(|(key, _)| key)
288    }
289}
290
291impl<V: Copy> FromIterator<(dhcp_protocol::OptionCode, V)> for OptionCodeMap<V> {
292    fn from_iter<T: IntoIterator<Item = (dhcp_protocol::OptionCode, V)>>(iter: T) -> Self {
293        let mut map = Self::new();
294        for (option_code, value) in iter {
295            let _: Option<_> = map.put(option_code, value);
296        }
297        map
298    }
299}
300
301impl<T: Copy> Default for OptionCodeMap<T> {
302    fn default() -> Self {
303        Self::new()
304    }
305}
306
307impl OptionCodeMap<OptionRequested> {
308    fn iter_required(&self) -> impl Iterator<Item = dhcp_protocol::OptionCode> + '_ {
309        self.iter().filter_map(|(key, val)| match val {
310            OptionRequested::Required => Some(key),
311            OptionRequested::Optional => None,
312        })
313    }
314
315    /// Converts `self` into the representation required for
316    /// `DhcpOption::ParameterRequestList`.
317    ///
318    /// Returns None if `self` is empty.
319    pub(crate) fn try_to_parameter_request_list(
320        &self,
321    ) -> Option<
322        AtLeast<1, AtMostBytes<{ dhcp_protocol::U8_MAX_AS_USIZE }, Vec<dhcp_protocol::OptionCode>>>,
323    > {
324        match AtLeast::try_from(self.iter_keys().collect::<Vec<_>>()) {
325            Ok(parameters) => Some(parameters),
326            Err((dhcp_protocol::SizeConstrainedError::SizeConstraintViolated, parameters)) => {
327                // This can only have happened because parameters is empty.
328                assert_eq!(parameters, Vec::new());
329                // Thus, we must omit the ParameterRequestList option.
330                None
331            }
332        }
333    }
334}
335
336/// Represents a set of OptionCodes as an array of booleans.
337pub type OptionCodeSet = OptionCodeMap<()>;
338
339impl OptionCodeSet {
340    /// Inserts `option_code` into the set, returning whether it was newly added.
341    pub fn insert(&mut self, option_code: dhcp_protocol::OptionCode) -> bool {
342        self.put(option_code, ()).is_none()
343    }
344}
345
346impl FromIterator<dhcp_protocol::OptionCode> for OptionCodeSet {
347    fn from_iter<T: IntoIterator<Item = dhcp_protocol::OptionCode>>(iter: T) -> Self {
348        let mut set = Self::new();
349        for code in iter {
350            let _: bool = set.insert(code);
351        }
352        set
353    }
354}
355
356/// Denotes whether a requested option is required or optional.
357#[derive(Copy, Clone, PartialEq, Debug)]
358pub enum OptionRequested {
359    /// The option is required; incoming DHCPOFFERs and DHCPACKs lacking this
360    /// option will be discarded.
361    Required,
362    /// The option is optional.
363    Optional,
364}
365
366fn collect_common_fields<T: Copy>(
367    requested_parameters: &OptionCodeMap<T>,
368    dhcp_protocol::Message {
369        op,
370        xid: _,
371        secs: _,
372        bdcast_flag: _,
373        ciaddr: _,
374        yiaddr,
375        siaddr: _,
376        giaddr: _,
377        chaddr: _,
378        sname: _,
379        file: _,
380        options,
381    }: dhcp_protocol::Message,
382) -> Result<CommonIncomingMessageFields, CommonIncomingMessageError> {
383    use dhcp_protocol::DhcpOption;
384
385    match op {
386        dhcp_protocol::OpCode::BOOTREQUEST => {
387            return Err(CommonIncomingMessageError::NotBootReply(op));
388        }
389        dhcp_protocol::OpCode::BOOTREPLY => (),
390    };
391
392    let mut builder = CommonIncomingMessageFieldsBuilder::default();
393    builder.yiaddr(yiaddr);
394
395    for option in options {
396        let code = option.code();
397        let newly_seen = builder.add_seen_option_and_return_whether_newly_added(code);
398        if !newly_seen && !option_can_be_duplicated(code) {
399            // Adhere to a principle of maximum conformance: ignore options
400            // that the server unexpectedly repeated rather than rejecting the
401            // message entirely. Should the multiple instance of this option
402            // have different values, we'll use the first instance's value.
403            log::warn!("DHCP option {code} was unexpectedly repeated: {option:?}");
404            continue;
405        }
406
407        // From RFC 2131 section 4.3.1:
408        // """
409        // Option                    DHCPOFFER    DHCPACK               DHCPNAK
410        // ------                    ---------    -------               -------
411        // Requested IP address      MUST NOT     MUST NOT              MUST NOT
412        // IP address lease time     MUST         MUST (DHCPREQUEST)    MUST NOT
413        //                                        MUST NOT (DHCPINFORM)
414        // Use 'file'/'sname' fields MAY          MAY                   MUST NOT
415        // DHCP message type         DHCPOFFER    DHCPACK               DHCPNAK
416        // Parameter request list    MUST NOT     MUST NOT              MUST NOT
417        // Message                   SHOULD       SHOULD                SHOULD
418        // Client identifier         MUST NOT     MUST NOT              MAY
419        // Vendor class identifier   MAY          MAY                   MAY
420        // Server identifier         MUST         MUST                  MUST
421        // Maximum message size      MUST NOT     MUST NOT              MUST NOT
422        // All others                MAY          MAY                   MUST NOT
423        //
424        //            Table 3:  Fields and options used by DHCP servers
425        // """
426
427        match &option {
428            DhcpOption::IpAddressLeaseTime(value) => match NonZeroU32::try_from(*value) {
429                Err(e) => {
430                    let _: TryFromIntError = e;
431                    log::warn!("dropping 0 lease time");
432                }
433                Ok(value) => {
434                    builder.ip_address_lease_time_secs(value).ignore_unused_result();
435                }
436            },
437            DhcpOption::DhcpMessageType(message_type) => {
438                builder.message_type(*message_type).ignore_unused_result()
439            }
440            DhcpOption::ServerIdentifier(value) => {
441                builder.server_identifier(*value)?;
442            }
443            DhcpOption::Message(message) => builder.message(message.clone()).ignore_unused_result(),
444            DhcpOption::RenewalTimeValue(value) => {
445                builder.renewal_time_value_secs(*value).ignore_unused_result()
446            }
447            DhcpOption::RebindingTimeValue(value) => {
448                builder.rebinding_time_value_secs(*value).ignore_unused_result()
449            }
450            DhcpOption::ClientIdentifier(value) => {
451                builder.client_identifier(value.clone()).ignore_unused_result();
452            }
453            DhcpOption::ParameterRequestList(_)
454            | DhcpOption::RequestedIpAddress(_)
455            | DhcpOption::MaxDhcpMessageSize(_) => {
456                return Err(CommonIncomingMessageError::IllegallyIncludedOption(option.code()));
457            }
458            DhcpOption::Pad()
459            | DhcpOption::End()
460            | DhcpOption::SubnetMask(_)
461            | DhcpOption::TimeOffset(_)
462            | DhcpOption::Router(_)
463            | DhcpOption::TimeServer(_)
464            | DhcpOption::NameServer(_)
465            | DhcpOption::DomainNameServer(_)
466            | DhcpOption::LogServer(_)
467            | DhcpOption::CookieServer(_)
468            | DhcpOption::LprServer(_)
469            | DhcpOption::ImpressServer(_)
470            | DhcpOption::ResourceLocationServer(_)
471            | DhcpOption::HostName(_)
472            | DhcpOption::BootFileSize(_)
473            | DhcpOption::MeritDumpFile(_)
474            | DhcpOption::DomainName(_)
475            | DhcpOption::SwapServer(_)
476            | DhcpOption::RootPath(_)
477            | DhcpOption::ExtensionsPath(_)
478            | DhcpOption::IpForwarding(_)
479            | DhcpOption::NonLocalSourceRouting(_)
480            | DhcpOption::PolicyFilter(_)
481            | DhcpOption::MaxDatagramReassemblySize(_)
482            | DhcpOption::DefaultIpTtl(_)
483            | DhcpOption::PathMtuAgingTimeout(_)
484            | DhcpOption::PathMtuPlateauTable(_)
485            | DhcpOption::InterfaceMtu(_)
486            | DhcpOption::AllSubnetsLocal(_)
487            | DhcpOption::BroadcastAddress(_)
488            | DhcpOption::PerformMaskDiscovery(_)
489            | DhcpOption::MaskSupplier(_)
490            | DhcpOption::PerformRouterDiscovery(_)
491            | DhcpOption::RouterSolicitationAddress(_)
492            | DhcpOption::StaticRoute(_)
493            | DhcpOption::TrailerEncapsulation(_)
494            | DhcpOption::ArpCacheTimeout(_)
495            | DhcpOption::EthernetEncapsulation(_)
496            | DhcpOption::TcpDefaultTtl(_)
497            | DhcpOption::TcpKeepaliveInterval(_)
498            | DhcpOption::TcpKeepaliveGarbage(_)
499            | DhcpOption::NetworkInformationServiceDomain(_)
500            | DhcpOption::NetworkInformationServers(_)
501            | DhcpOption::NetworkTimeProtocolServers(_)
502            | DhcpOption::VendorSpecificInformation(_)
503            | DhcpOption::NetBiosOverTcpipNameServer(_)
504            | DhcpOption::NetBiosOverTcpipDatagramDistributionServer(_)
505            | DhcpOption::NetBiosOverTcpipNodeType(_)
506            | DhcpOption::NetBiosOverTcpipScope(_)
507            | DhcpOption::XWindowSystemFontServer(_)
508            | DhcpOption::XWindowSystemDisplayManager(_)
509            | DhcpOption::NetworkInformationServicePlusDomain(_)
510            | DhcpOption::NetworkInformationServicePlusServers(_)
511            | DhcpOption::MobileIpHomeAgent(_)
512            | DhcpOption::SmtpServer(_)
513            | DhcpOption::Pop3Server(_)
514            | DhcpOption::NntpServer(_)
515            | DhcpOption::DefaultWwwServer(_)
516            | DhcpOption::DefaultFingerServer(_)
517            | DhcpOption::DefaultIrcServer(_)
518            | DhcpOption::StreetTalkServer(_)
519            | DhcpOption::StreetTalkDirectoryAssistanceServer(_)
520            | DhcpOption::OptionOverload(_)
521            | DhcpOption::TftpServerName(_)
522            | DhcpOption::BootfileName(_)
523            | DhcpOption::VendorClassIdentifier(_) => (),
524        };
525
526        if requested_parameters.contains(option.code()) {
527            builder.add_requested_parameter(option);
528        }
529    }
530    builder.build()
531}
532
533/// Returns whether the option is allowed to be specified multiple times.
534///
535/// Note: The DHCP RFC doesn't take a stance on whether options may or may not
536/// be repeated. We take a pragmatic approach and allow options to be repeated
537/// where appropriate (e.g. options that have list semantics). Repeats are
538/// rejected on options for which they are nonsensical (e.g. the
539/// DhcpMessageType).
540fn option_can_be_duplicated(code: dhcp_protocol::OptionCode) -> bool {
541    use dhcp_protocol::OptionCode::*;
542    match code {
543        SubnetMask
544        | TimeOffset
545        | HostName
546        | BootFileSize
547        | MeritDumpFile
548        | DomainName
549        | SwapServer
550        | RootPath
551        | ExtensionsPath
552        | IpForwarding
553        | NonLocalSourceRouting
554        | MaxDatagramReassemblySize
555        | DefaultIpTtl
556        | PathMtuAgingTimeout
557        | InterfaceMtu
558        | AllSubnetsLocal
559        | BroadcastAddress
560        | PerformMaskDiscovery
561        | MaskSupplier
562        | PerformRouterDiscovery
563        | RouterSolicitationAddress
564        | TrailerEncapsulation
565        | ArpCacheTimeout
566        | EthernetEncapsulation
567        | TcpDefaultTtl
568        | TcpKeepaliveInterval
569        | TcpKeepaliveGarbage
570        | NetworkInformationServiceDomain
571        | NetBiosOverTcpipNodeType
572        | NetBiosOverTcpipScope
573        | RequestedIpAddress
574        | IpAddressLeaseTime
575        | OptionOverload
576        | DhcpMessageType
577        | ServerIdentifier
578        | Message
579        | MaxDhcpMessageSize
580        | RenewalTimeValue
581        | RebindingTimeValue
582        | VendorClassIdentifier
583        | ClientIdentifier
584        | NetworkInformationServicePlusDomain
585        | TftpServerName
586        | BootfileName
587        | End => false,
588        Pad
589        | Router
590        | TimeServer
591        | NameServer
592        | DomainNameServer
593        | LogServer
594        | CookieServer
595        | LprServer
596        | ImpressServer
597        | ResourceLocationServer
598        | PolicyFilter
599        | PathMtuPlateauTable
600        | StaticRoute
601        | NetworkInformationServers
602        | NetworkTimeProtocolServers
603        | VendorSpecificInformation
604        | NetBiosOverTcpipNameServer
605        | NetBiosOverTcpipDatagramDistributionServer
606        | XWindowSystemFontServer
607        | XWindowSystemDisplayManager
608        | NetworkInformationServicePlusServers
609        | MobileIpHomeAgent
610        | SmtpServer
611        | Pop3Server
612        | NntpServer
613        | DefaultWwwServer
614        | DefaultFingerServer
615        | DefaultIrcServer
616        | StreetTalkServer
617        | StreetTalkDirectoryAssistanceServer
618        | ParameterRequestList => true,
619    }
620}
621
622/// Reasons that an incoming DHCP message might be discarded during Selecting
623/// state.
624#[derive(thiserror::Error, Debug, PartialEq)]
625pub(crate) enum SelectingIncomingMessageError {
626    #[error("{0}")]
627    CommonError(#[from] CommonIncomingMessageError),
628    /// Note that `NoServerIdentifier` is intentionally distinct from
629    /// `CommonIncomingMessageError::UnspecifiedServerIdentifier`, as the latter
630    /// refers to the Server Identifier being explicitly populated as the
631    /// unspecified address, rather than simply omitted.
632    #[error("no server identifier")]
633    NoServerIdentifier,
634    #[error("got DHCP message type = {0}, wanted DHCPOFFER")]
635    NotDhcpOffer(dhcp_protocol::MessageType),
636    #[error("yiaddr was the unspecified address")]
637    UnspecifiedYiaddr,
638    #[error("missing required option: {0:?}")]
639    MissingRequiredOption(dhcp_protocol::OptionCode),
640}
641
642/// Counters for reasons a message was discarded while receiving in Selecting
643/// state.
644#[derive(Default, Debug)]
645pub(crate) struct SelectingIncomingMessageErrorCounters {
646    /// Common reasons across all states.
647    pub(crate) common: CommonIncomingMessageErrorCounters,
648    /// The message omitted the Server Identifier option.
649    pub(crate) no_server_identifier: Counter,
650    /// The message was not a DHCPOFFER.
651    pub(crate) not_dhcp_offer: Counter,
652    /// The message had yiaddr set to the unspecified address.
653    pub(crate) unspecified_yiaddr: Counter,
654    /// The message was missing a required option.
655    pub(crate) missing_required_option: Counter,
656}
657
658impl SelectingIncomingMessageErrorCounters {
659    /// Records the counters.
660    pub(crate) fn record(&self, inspector: &mut impl Inspector) {
661        let Self {
662            common,
663            no_server_identifier,
664            not_dhcp_offer,
665            unspecified_yiaddr,
666            missing_required_option,
667        } = self;
668        common.record(inspector);
669        inspector.record_usize("NoServerIdentifier", no_server_identifier.load());
670        inspector.record_usize("NotDhcpOffer", not_dhcp_offer.load());
671        inspector.record_usize("UnspecifiedYiaddr", unspecified_yiaddr.load());
672        inspector.record_usize("MissingRequiredOption", missing_required_option.load());
673    }
674
675    /// Increments the counter corresponding to the error.
676    pub(crate) fn increment(&self, error: &SelectingIncomingMessageError) {
677        let Self {
678            common,
679            no_server_identifier,
680            not_dhcp_offer,
681            unspecified_yiaddr,
682            missing_required_option,
683        } = self;
684        match error {
685            SelectingIncomingMessageError::CommonError(common_incoming_message_error) => {
686                common.increment(common_incoming_message_error)
687            }
688            SelectingIncomingMessageError::NoServerIdentifier => no_server_identifier.increment(),
689            SelectingIncomingMessageError::NotDhcpOffer(_) => not_dhcp_offer.increment(),
690            SelectingIncomingMessageError::UnspecifiedYiaddr => unspecified_yiaddr.increment(),
691            SelectingIncomingMessageError::MissingRequiredOption(_) => {
692                missing_required_option.increment()
693            }
694        }
695    }
696}
697
698/// Extracts the fields from a DHCP message incoming during Selecting state that
699/// should be used during Requesting state.
700pub(crate) fn fields_to_retain_from_selecting(
701    requested_parameters: &OptionCodeMap<OptionRequested>,
702    message: dhcp_protocol::Message,
703) -> Result<FieldsFromOfferToUseInRequest, SelectingIncomingMessageError> {
704    let CommonIncomingMessageFields {
705        message_type,
706        server_identifier,
707        yiaddr,
708        ip_address_lease_time_secs,
709        renewal_time_value_secs: _,
710        rebinding_time_value_secs: _,
711        parameters: _,
712        seen_option_codes,
713        message: _,
714        client_identifier: _,
715    } = collect_common_fields(requested_parameters, message)?;
716
717    match message_type {
718        dhcp_protocol::MessageType::DHCPOFFER => (),
719        dhcp_protocol::MessageType::DHCPDISCOVER
720        | dhcp_protocol::MessageType::DHCPREQUEST
721        | dhcp_protocol::MessageType::DHCPDECLINE
722        | dhcp_protocol::MessageType::DHCPACK
723        | dhcp_protocol::MessageType::DHCPNAK
724        | dhcp_protocol::MessageType::DHCPRELEASE
725        | dhcp_protocol::MessageType::DHCPINFORM => {
726            return Err(SelectingIncomingMessageError::NotDhcpOffer(message_type));
727        }
728    };
729
730    if let Some(missing_option_code) =
731        requested_parameters.iter_required().find(|code| !seen_option_codes.contains(*code))
732    {
733        return Err(SelectingIncomingMessageError::MissingRequiredOption(missing_option_code));
734    }
735
736    Ok(FieldsFromOfferToUseInRequest {
737        server_identifier: server_identifier
738            .ok_or(SelectingIncomingMessageError::NoServerIdentifier)?,
739        ip_address_lease_time_secs,
740        ip_address_to_request: yiaddr.ok_or(SelectingIncomingMessageError::UnspecifiedYiaddr)?,
741    })
742}
743
744#[derive(Debug, Clone, Copy, PartialEq)]
745/// Fields from a DHCPOFFER that should be used while building a DHCPREQUEST.
746pub(crate) struct FieldsFromOfferToUseInRequest {
747    pub(crate) server_identifier: net_types::SpecifiedAddr<net_types::ip::Ipv4Addr>,
748    pub(crate) ip_address_lease_time_secs: Option<NonZeroU32>,
749    pub(crate) ip_address_to_request: net_types::SpecifiedAddr<net_types::ip::Ipv4Addr>,
750}
751
752impl FieldsFromOfferToUseInRequest {
753    pub(crate) fn record(&self, inspector: &mut impl Inspector) {
754        let Self { server_identifier, ip_address_lease_time_secs, ip_address_to_request } = self;
755        inspector.record_ip_addr("ServerIdentifier", **server_identifier);
756        if let Some(value) = ip_address_lease_time_secs {
757            inspector.record_uint("IpAddressLeaseTimeSecs", value.get());
758        }
759        inspector.record_ip_addr("IpAddressToRequest", **ip_address_to_request);
760    }
761}
762
763#[derive(Debug, PartialEq)]
764// `ServerIdentifier`` is generic in order to allow for it to be optional for
765// DHCPACKs received while in RENEWING state (since we already know the
766// identifier of the server we're directly communicating with) but required in
767// REBINDING state (because we've broadcast the request and need to record
768// which server to send renewal requests to in the future).
769pub(crate) enum IncomingResponseToRequest<ServerIdentifier> {
770    Ack(FieldsToRetainFromAck<ServerIdentifier>),
771    Nak(FieldsToRetainFromNak),
772}
773
774/// Reasons that an incoming response to a DHCPREQUEST might be discarded.
775#[derive(thiserror::Error, Debug, PartialEq)]
776pub(crate) enum IncomingResponseToRequestError {
777    #[error("{0}")]
778    CommonError(#[from] CommonIncomingMessageError),
779    #[error("got DHCP message type = {0}, wanted DHCPACK or DHCPNAK")]
780    NotDhcpAckOrNak(dhcp_protocol::MessageType),
781    #[error("yiaddr was the unspecified address")]
782    UnspecifiedYiaddr,
783    #[error("no IP address lease time")]
784    NoLeaseTime,
785    #[error("no server identifier")]
786    NoServerIdentifier,
787    #[error("missing required option: {0:?}")]
788    MissingRequiredOption(dhcp_protocol::OptionCode),
789}
790
791/// Counters for reasons a message was discarded while receiving in Requesting
792/// state.
793#[derive(Default, Debug)]
794pub(crate) struct IncomingResponseToRequestErrorCounters {
795    /// Common reasons across all states.
796    pub(crate) common: CommonIncomingMessageErrorCounters,
797    /// The message was not a DHCPACK or DHCPNAK.
798    pub(crate) not_dhcp_ack_or_nak: Counter,
799    /// The message had yiaddr set to the unspecified address.
800    pub(crate) unspecified_yiaddr: Counter,
801    /// The message had no IP address lease time.
802    pub(crate) no_lease_time: Counter,
803    /// The message had no server identifier.
804    pub(crate) no_server_identifier: Counter,
805    /// The message was missing a required option.
806    pub(crate) missing_required_option: Counter,
807}
808
809impl IncomingResponseToRequestErrorCounters {
810    /// Records the counters.
811    pub(crate) fn record(&self, inspector: &mut impl Inspector) {
812        let Self {
813            common,
814            not_dhcp_ack_or_nak,
815            unspecified_yiaddr,
816            no_lease_time,
817            no_server_identifier,
818            missing_required_option,
819        } = self;
820        common.record(inspector);
821        inspector.record_usize("NotDhcpAckOrNak", not_dhcp_ack_or_nak.load());
822        inspector.record_usize("UnspecifiedYiaddr", unspecified_yiaddr.load());
823        inspector.record_usize("NoLeaseTime", no_lease_time.load());
824        inspector.record_usize("NoServerIdentifier", no_server_identifier.load());
825        inspector.record_usize("MissingRequiredOption", missing_required_option.load());
826    }
827
828    /// Increments the counter corresponding to the error.
829    pub(crate) fn increment(&self, error: &IncomingResponseToRequestError) {
830        let Self {
831            common,
832            not_dhcp_ack_or_nak,
833            unspecified_yiaddr,
834            no_lease_time,
835            no_server_identifier,
836            missing_required_option,
837        } = self;
838        match error {
839            IncomingResponseToRequestError::CommonError(common_incoming_message_error) => {
840                common.increment(common_incoming_message_error)
841            }
842            IncomingResponseToRequestError::NotDhcpAckOrNak(_) => not_dhcp_ack_or_nak.increment(),
843            IncomingResponseToRequestError::UnspecifiedYiaddr => unspecified_yiaddr.increment(),
844            IncomingResponseToRequestError::NoLeaseTime => no_lease_time.increment(),
845            IncomingResponseToRequestError::NoServerIdentifier => no_server_identifier.increment(),
846            IncomingResponseToRequestError::MissingRequiredOption(_) => {
847                missing_required_option.increment()
848            }
849        }
850    }
851}
852
853#[derive(Debug, PartialEq)]
854pub(crate) struct FieldsToRetainFromAck<ServerIdentifier> {
855    pub(crate) yiaddr: net_types::SpecifiedAddr<net_types::ip::Ipv4Addr>,
856    pub(crate) server_identifier: ServerIdentifier,
857    pub(crate) ip_address_lease_time_secs: NonZeroU32,
858    pub(crate) renewal_time_value_secs: Option<u32>,
859    pub(crate) rebinding_time_value_secs: Option<u32>,
860    // Note: Options with list semantics may be repeated.
861    pub(crate) parameters: Vec<dhcp_protocol::DhcpOption>,
862}
863
864impl<ServerIdentifier> FieldsToRetainFromAck<ServerIdentifier> {
865    pub(crate) fn map_server_identifier<T, E>(
866        self,
867        f: impl FnOnce(ServerIdentifier) -> Result<T, E>,
868    ) -> Result<FieldsToRetainFromAck<T>, E> {
869        let Self {
870            yiaddr,
871            server_identifier,
872            ip_address_lease_time_secs,
873            renewal_time_value_secs,
874            rebinding_time_value_secs,
875            parameters,
876        } = self;
877        Ok(FieldsToRetainFromAck {
878            yiaddr,
879            server_identifier: f(server_identifier)?,
880            ip_address_lease_time_secs,
881            renewal_time_value_secs,
882            rebinding_time_value_secs,
883            parameters,
884        })
885    }
886}
887
888#[derive(Debug, PartialEq)]
889pub(crate) struct FieldsToRetainFromNak {
890    pub(crate) server_identifier: net_types::SpecifiedAddr<net_types::ip::Ipv4Addr>,
891    pub(crate) message: Option<String>,
892    pub(crate) client_identifier: Option<
893        AtLeast<
894            { dhcp_protocol::CLIENT_IDENTIFIER_MINIMUM_LENGTH },
895            AtMostBytes<{ dhcp_protocol::U8_MAX_AS_USIZE }, Vec<u8>>,
896        >,
897    >,
898}
899
900pub(crate) fn fields_to_retain_from_response_to_request(
901    requested_parameters: &OptionCodeMap<OptionRequested>,
902    message: dhcp_protocol::Message,
903) -> Result<
904    IncomingResponseToRequest<
905        // Strictly according to RFC 2131, the Server Identifier MUST be included in
906        // the DHCPACK. However, we've observed DHCP servers in the field fail to
907        // set the Server Identifier, instead expecting the client to remember it
908        // from the DHCPOFFER (https://fxbug.dev/42064504). Thus, we treat Server
909        // Identifier as optional for DHCPACK.
910        Option<net_types::SpecifiedAddr<net_types::ip::Ipv4Addr>>,
911    >,
912    IncomingResponseToRequestError,
913> {
914    let CommonIncomingMessageFields {
915        message_type,
916        server_identifier,
917        yiaddr,
918        ip_address_lease_time_secs,
919        renewal_time_value_secs,
920        rebinding_time_value_secs,
921        parameters,
922        seen_option_codes,
923        message,
924        client_identifier,
925    } = collect_common_fields(requested_parameters, message)?;
926
927    match message_type {
928        dhcp_protocol::MessageType::DHCPACK => {
929            // Only enforce required parameters for ACKs, since NAKs aren't
930            // expected to include any configuration at all.
931
932            if let Some(missing_option_code) =
933                requested_parameters.iter_required().find(|code| !seen_option_codes.contains(*code))
934            {
935                return Err(IncomingResponseToRequestError::MissingRequiredOption(
936                    missing_option_code,
937                ));
938            }
939            Ok(IncomingResponseToRequest::Ack(FieldsToRetainFromAck {
940                yiaddr: yiaddr.ok_or(IncomingResponseToRequestError::UnspecifiedYiaddr)?,
941                server_identifier,
942                ip_address_lease_time_secs: ip_address_lease_time_secs
943                    .ok_or(IncomingResponseToRequestError::NoLeaseTime)?,
944                renewal_time_value_secs,
945                rebinding_time_value_secs,
946                parameters,
947            }))
948        }
949        dhcp_protocol::MessageType::DHCPNAK => {
950            Ok(IncomingResponseToRequest::Nak(FieldsToRetainFromNak {
951                server_identifier: server_identifier
952                    .ok_or(IncomingResponseToRequestError::NoServerIdentifier)?,
953                message,
954                client_identifier,
955            }))
956        }
957        dhcp_protocol::MessageType::DHCPDISCOVER
958        | dhcp_protocol::MessageType::DHCPOFFER
959        | dhcp_protocol::MessageType::DHCPREQUEST
960        | dhcp_protocol::MessageType::DHCPDECLINE
961        | dhcp_protocol::MessageType::DHCPRELEASE
962        | dhcp_protocol::MessageType::DHCPINFORM => {
963            Err(IncomingResponseToRequestError::NotDhcpAckOrNak(message_type))
964        }
965    }
966}
967
968#[cfg(test)]
969mod test {
970    use super::*;
971    use assert_matches::assert_matches;
972    use dhcp_protocol::{CLIENT_PORT, SERVER_PORT};
973    use net_declare::net::prefix_length_v4;
974    use net_declare::{net_ip_v4, net_mac, std_ip_v4};
975    use net_types::ip::{Ip, Ipv4, PrefixLength};
976    use std::net::Ipv4Addr;
977    use test_case::test_case;
978
979    #[test]
980    fn serialize_parse_roundtrip() {
981        let make_message = || dhcp_protocol::Message {
982            op: dhcp_protocol::OpCode::BOOTREQUEST,
983            xid: 124,
984            secs: 99,
985            bdcast_flag: false,
986            ciaddr: net_ip_v4!("1.2.3.4").into(),
987            yiaddr: net_ip_v4!("5.6.7.8").into(),
988            siaddr: net_ip_v4!("9.10.11.12").into(),
989            giaddr: net_ip_v4!("13.14.15.16").into(),
990            chaddr: net_mac!("17:18:19:20:21:22"),
991            sname: "this is a sname".to_owned(),
992            file: "this is the boot filename".to_owned(),
993            options: vec![
994                dhcp_protocol::DhcpOption::DhcpMessageType(
995                    dhcp_protocol::MessageType::DHCPDISCOVER,
996                ),
997                dhcp_protocol::DhcpOption::RequestedIpAddress(net_ip_v4!("5.6.7.8").into()),
998            ],
999        };
1000        let packet = serialize_dhcp_message_to_ip_packet(
1001            make_message(),
1002            Ipv4Addr::UNSPECIFIED,
1003            CLIENT_PORT,
1004            Ipv4Addr::BROADCAST,
1005            SERVER_PORT,
1006        );
1007        let (src_addr, parsed_message) =
1008            parse_dhcp_message_from_ip_packet(packet.as_ref(), SERVER_PORT).unwrap();
1009
1010        assert_eq!(net_types::ip::Ipv4::UNSPECIFIED_ADDRESS, src_addr);
1011        assert_eq!(make_message(), parsed_message);
1012    }
1013
1014    #[test]
1015    fn nonsense() {
1016        assert_matches!(
1017            parse_dhcp_message_from_ip_packet(
1018                &[0xD, 0xE, 0xA, 0xD, 0xB, 0xE, 0xE, 0xF],
1019                NonZeroU16::new(1).unwrap()
1020            ),
1021            Err(ParseError::Ipv4(parse_error)) => {
1022                assert_eq!(parse_error, packet_formats::error::IpParseError::Parse { error: packet_formats::error::ParseError::Format })
1023            }
1024        )
1025    }
1026
1027    #[test]
1028    fn not_udp() {
1029        let src_ip = Ipv4Addr::UNSPECIFIED.into();
1030        let dst_ip = Ipv4Addr::BROADCAST.into();
1031        let tcp_builder: packet_formats::tcp::TcpSegmentBuilder<net_types::ip::Ipv4Addr> =
1032            packet_formats::tcp::TcpSegmentBuilder::new(
1033                src_ip,
1034                dst_ip,
1035                CLIENT_PORT,
1036                SERVER_PORT,
1037                0,
1038                None,
1039                0,
1040            );
1041        let ipv4_builder = packet_formats::ipv4::Ipv4PacketBuilder::new(
1042            src_ip,
1043            dst_ip,
1044            DEFAULT_TTL,
1045            packet_formats::ip::Ipv4Proto::Proto(packet_formats::ip::IpProto::Tcp),
1046        );
1047        let bytes = vec![1, 2, 3, 4, 5]
1048            .into_serializer()
1049            .wrap_in(tcp_builder)
1050            .wrap_in(ipv4_builder)
1051            .serialize_vec_outer()
1052            .expect("serialize error");
1053
1054        assert_matches!(
1055            parse_dhcp_message_from_ip_packet(bytes.as_ref(), NonZeroU16::new(1).unwrap()),
1056            Err(ParseError::NotUdp)
1057        );
1058    }
1059
1060    #[test]
1061    fn wrong_port() {
1062        let src_ip = Ipv4Addr::UNSPECIFIED.into();
1063        let dst_ip = Ipv4Addr::BROADCAST.into();
1064
1065        let udp_builder: packet_formats::udp::UdpPacketBuilder<net_types::ip::Ipv4Addr> =
1066            packet_formats::udp::UdpPacketBuilder::new(
1067                src_ip,
1068                dst_ip,
1069                Some(CLIENT_PORT),
1070                SERVER_PORT,
1071            );
1072        let ipv4_builder = packet_formats::ipv4::Ipv4PacketBuilder::new(
1073            src_ip,
1074            dst_ip,
1075            DEFAULT_TTL,
1076            packet_formats::ip::Ipv4Proto::Proto(packet_formats::ip::IpProto::Udp),
1077        );
1078
1079        let bytes = "hello_world"
1080            .bytes()
1081            .collect::<Vec<_>>()
1082            .into_serializer()
1083            .wrap_in(udp_builder)
1084            .wrap_in(ipv4_builder)
1085            .serialize_vec_outer()
1086            .expect("serialize error");
1087
1088        let result = parse_dhcp_message_from_ip_packet(bytes.as_ref(), CLIENT_PORT);
1089        assert_matches!(result, Err(ParseError::WrongPort(port)) => assert_eq!(port, SERVER_PORT));
1090    }
1091
1092    struct VaryingOfferFields {
1093        op: dhcp_protocol::OpCode,
1094        yiaddr: Ipv4Addr,
1095        message_type: Option<dhcp_protocol::MessageType>,
1096        server_identifier: Option<Ipv4Addr>,
1097        subnet_mask: Option<PrefixLength<Ipv4>>,
1098        lease_length_secs: Option<u32>,
1099        include_duplicate_option: bool,
1100    }
1101
1102    const SERVER_IP: Ipv4Addr = std_ip_v4!("192.168.1.1");
1103    const TEST_SUBNET_MASK: PrefixLength<Ipv4> = prefix_length_v4!(24);
1104    const LEASE_LENGTH_SECS: u32 = 100;
1105    const LEASE_LENGTH_SECS_NONZERO: NonZeroU32 = NonZeroU32::new(LEASE_LENGTH_SECS).unwrap();
1106    const YIADDR: Ipv4Addr = std_ip_v4!("192.168.1.5");
1107
1108    #[test_case(VaryingOfferFields {
1109        op: dhcp_protocol::OpCode::BOOTREPLY,
1110        yiaddr: YIADDR,
1111        message_type: Some(dhcp_protocol::MessageType::DHCPOFFER),
1112        server_identifier: Some(SERVER_IP),
1113        subnet_mask: Some(TEST_SUBNET_MASK),
1114        lease_length_secs: Some(LEASE_LENGTH_SECS),
1115        include_duplicate_option: false,
1116    } => Ok(FieldsFromOfferToUseInRequest {
1117        server_identifier: net_types::ip::Ipv4Addr::from(SERVER_IP)
1118            .try_into()
1119            .expect("should be specified"),
1120        ip_address_lease_time_secs: Some(LEASE_LENGTH_SECS_NONZERO),
1121        ip_address_to_request: net_types::ip::Ipv4Addr::from(YIADDR)
1122            .try_into()
1123            .expect("should be specified"),
1124    }); "accepts good offer with lease time")]
1125    #[test_case(VaryingOfferFields {
1126        op: dhcp_protocol::OpCode::BOOTREPLY,
1127        yiaddr: YIADDR,
1128        message_type: Some(dhcp_protocol::MessageType::DHCPOFFER),
1129        server_identifier: Some(SERVER_IP),
1130        subnet_mask: Some(TEST_SUBNET_MASK),
1131        lease_length_secs: None,
1132        include_duplicate_option: false,
1133    } => Ok(FieldsFromOfferToUseInRequest {
1134        server_identifier: net_types::ip::Ipv4Addr::from(SERVER_IP)
1135            .try_into()
1136            .expect("should be specified"),
1137        ip_address_lease_time_secs: None,
1138        ip_address_to_request: net_types::ip::Ipv4Addr::from(YIADDR)
1139            .try_into()
1140            .expect("should be specified"),
1141    }); "accepts good offer without lease time")]
1142    #[test_case(VaryingOfferFields {
1143        op: dhcp_protocol::OpCode::BOOTREPLY,
1144        yiaddr: YIADDR,
1145        message_type: Some(dhcp_protocol::MessageType::DHCPOFFER),
1146        server_identifier: Some(Ipv4Addr::UNSPECIFIED),
1147        subnet_mask: Some(TEST_SUBNET_MASK),
1148        lease_length_secs: Some(LEASE_LENGTH_SECS),
1149        include_duplicate_option: false,
1150    } => Err(SelectingIncomingMessageError::CommonError(
1151        CommonIncomingMessageError::UnspecifiedServerIdentifier,
1152    )); "rejects offer with unspecified server identifier")]
1153    #[test_case(VaryingOfferFields {
1154        op: dhcp_protocol::OpCode::BOOTREPLY,
1155        yiaddr: YIADDR,
1156        message_type: Some(dhcp_protocol::MessageType::DHCPOFFER),
1157        server_identifier: Some(SERVER_IP),
1158        subnet_mask: None,
1159        lease_length_secs: Some(LEASE_LENGTH_SECS),
1160        include_duplicate_option: false,
1161    } => Err(SelectingIncomingMessageError::MissingRequiredOption(
1162        dhcp_protocol::OptionCode::SubnetMask,
1163    )); "rejects offer without required subnet mask")]
1164    #[test_case(VaryingOfferFields {
1165        op: dhcp_protocol::OpCode::BOOTREPLY,
1166        yiaddr: YIADDR,
1167        message_type: Some(dhcp_protocol::MessageType::DHCPOFFER),
1168        server_identifier: None,
1169        subnet_mask: Some(TEST_SUBNET_MASK),
1170        lease_length_secs: Some(LEASE_LENGTH_SECS),
1171        include_duplicate_option: false,
1172    } => Err(SelectingIncomingMessageError::NoServerIdentifier); "rejects offer with no server identifier option")]
1173    #[test_case(VaryingOfferFields {
1174        op: dhcp_protocol::OpCode::BOOTREPLY,
1175        yiaddr: Ipv4Addr::UNSPECIFIED,
1176        message_type: Some(dhcp_protocol::MessageType::DHCPOFFER),
1177        server_identifier: Some(SERVER_IP),
1178        subnet_mask: Some(TEST_SUBNET_MASK),
1179        lease_length_secs: Some(LEASE_LENGTH_SECS),
1180        include_duplicate_option: false,
1181    } => Err(SelectingIncomingMessageError::UnspecifiedYiaddr) ; "rejects offer with unspecified yiaddr")]
1182    #[test_case(VaryingOfferFields {
1183        op: dhcp_protocol::OpCode::BOOTREQUEST,
1184        yiaddr: YIADDR,
1185        message_type: Some(dhcp_protocol::MessageType::DHCPOFFER),
1186        server_identifier: Some(SERVER_IP),
1187        subnet_mask: Some(TEST_SUBNET_MASK),
1188        lease_length_secs: Some(LEASE_LENGTH_SECS),
1189        include_duplicate_option: false,
1190    } => Err(SelectingIncomingMessageError::CommonError(
1191        CommonIncomingMessageError::NotBootReply(dhcp_protocol::OpCode::BOOTREQUEST),
1192    )); "rejects offer that isn't a bootreply")]
1193    #[test_case(VaryingOfferFields {
1194        op: dhcp_protocol::OpCode::BOOTREPLY,
1195        yiaddr: YIADDR,
1196        message_type: Some(dhcp_protocol::MessageType::DHCPACK),
1197        server_identifier: Some(SERVER_IP),
1198        subnet_mask: Some(TEST_SUBNET_MASK),
1199        lease_length_secs: Some(LEASE_LENGTH_SECS),
1200        include_duplicate_option: false,
1201    } => Err(
1202        SelectingIncomingMessageError::NotDhcpOffer(dhcp_protocol::MessageType::DHCPACK),
1203    ); "rejects offer with wrong DHCP message type")]
1204    #[test_case(VaryingOfferFields {
1205        op: dhcp_protocol::OpCode::BOOTREPLY,
1206        yiaddr: YIADDR,
1207        message_type: None,
1208        server_identifier: Some(SERVER_IP),
1209        subnet_mask: Some(TEST_SUBNET_MASK),
1210        lease_length_secs: Some(LEASE_LENGTH_SECS),
1211        include_duplicate_option: false,
1212    } => Err(SelectingIncomingMessageError::CommonError(
1213        CommonIncomingMessageError::BuilderMissingField("message_type"),
1214    )); "rejects offer with no DHCP message type option")]
1215    #[test_case(VaryingOfferFields {
1216        op: dhcp_protocol::OpCode::BOOTREPLY,
1217        yiaddr: YIADDR,
1218        message_type: Some(dhcp_protocol::MessageType::DHCPOFFER),
1219        server_identifier: Some(SERVER_IP),
1220        subnet_mask: Some(TEST_SUBNET_MASK),
1221        lease_length_secs: Some(LEASE_LENGTH_SECS),
1222        include_duplicate_option: true,
1223    } => Ok(FieldsFromOfferToUseInRequest {
1224        server_identifier: net_types::ip::Ipv4Addr::from(SERVER_IP)
1225            .try_into()
1226            .expect("should be specified"),
1227        ip_address_lease_time_secs: Some(LEASE_LENGTH_SECS_NONZERO),
1228        ip_address_to_request: net_types::ip::Ipv4Addr::from(YIADDR)
1229            .try_into()
1230            .expect("should be specified"),
1231    }); "accepts good offer with duplicate options")]
1232    fn fields_from_offer_to_use_in_request(
1233        offer_fields: VaryingOfferFields,
1234    ) -> Result<FieldsFromOfferToUseInRequest, SelectingIncomingMessageError> {
1235        use super::fields_to_retain_from_selecting as fields;
1236        use dhcp_protocol::DhcpOption;
1237
1238        let VaryingOfferFields {
1239            op,
1240            yiaddr,
1241            message_type,
1242            server_identifier,
1243            subnet_mask,
1244            lease_length_secs,
1245            include_duplicate_option,
1246        } = offer_fields;
1247
1248        let message = dhcp_protocol::Message {
1249            op,
1250            xid: 1,
1251            secs: 0,
1252            bdcast_flag: false,
1253            ciaddr: Ipv4Addr::UNSPECIFIED,
1254            yiaddr,
1255            siaddr: Ipv4Addr::UNSPECIFIED,
1256            giaddr: Ipv4Addr::UNSPECIFIED,
1257            chaddr: net_mac!("01:02:03:04:05:06"),
1258            sname: String::new(),
1259            file: String::new(),
1260            options: message_type
1261                .map(DhcpOption::DhcpMessageType)
1262                .into_iter()
1263                .chain(server_identifier.map(DhcpOption::ServerIdentifier))
1264                .chain(subnet_mask.map(DhcpOption::SubnetMask))
1265                .chain(lease_length_secs.map(DhcpOption::IpAddressLeaseTime))
1266                .chain(
1267                    include_duplicate_option
1268                        .then_some([
1269                            dhcp_protocol::DhcpOption::DomainName("example.com".to_owned()),
1270                            dhcp_protocol::DhcpOption::DomainName("example.com".to_owned()),
1271                        ])
1272                        .into_iter()
1273                        .flatten(),
1274                )
1275                .collect(),
1276        };
1277
1278        fields(
1279            &std::iter::once((dhcp_protocol::OptionCode::SubnetMask, OptionRequested::Required))
1280                .collect(),
1281            message,
1282        )
1283    }
1284
1285    struct VaryingReplyToRequestFields {
1286        op: dhcp_protocol::OpCode,
1287        yiaddr: Ipv4Addr,
1288        message_type: Option<dhcp_protocol::MessageType>,
1289        server_identifier: Option<Ipv4Addr>,
1290        subnet_mask: Option<PrefixLength<Ipv4>>,
1291        lease_length_secs: Option<u32>,
1292        renewal_time_secs: Option<u32>,
1293        rebinding_time_secs: Option<u32>,
1294        message: Option<String>,
1295        include_duplicate_option: bool,
1296    }
1297
1298    const DOMAIN_NAME: &str = "example.com";
1299    const MESSAGE: &str = "message explaining why the DHCPNAK was sent";
1300    const RENEWAL_TIME_SECS: u32 = LEASE_LENGTH_SECS / 2;
1301    const REBINDING_TIME_SECS: u32 = LEASE_LENGTH_SECS * 3 / 4;
1302
1303    #[test_case(
1304        VaryingReplyToRequestFields {
1305            op: dhcp_protocol::OpCode::BOOTREPLY,
1306            yiaddr: YIADDR,
1307            message_type: Some(dhcp_protocol::MessageType::DHCPACK),
1308            server_identifier: Some(SERVER_IP),
1309            subnet_mask: Some(TEST_SUBNET_MASK),
1310            lease_length_secs: Some(LEASE_LENGTH_SECS),
1311            renewal_time_secs: None,
1312            rebinding_time_secs: None,
1313            message: None,
1314            include_duplicate_option: false,
1315        } => Ok(IncomingResponseToRequest::Ack(FieldsToRetainFromAck {
1316            yiaddr: net_types::ip::Ipv4Addr::from(YIADDR)
1317                .try_into()
1318                .expect("should be specified"),
1319            server_identifier: Some(
1320                net_types::ip::Ipv4Addr::from(SERVER_IP)
1321                    .try_into()
1322                    .expect("should be specified"),
1323            ),
1324            ip_address_lease_time_secs: LEASE_LENGTH_SECS_NONZERO,
1325            parameters: vec![
1326                dhcp_protocol::DhcpOption::SubnetMask(TEST_SUBNET_MASK),
1327                dhcp_protocol::DhcpOption::DomainName(DOMAIN_NAME.to_owned())
1328            ],
1329            renewal_time_value_secs: None,
1330            rebinding_time_value_secs: None,
1331        })); "accepts good DHCPACK")]
1332    #[test_case(VaryingReplyToRequestFields {
1333        op: dhcp_protocol::OpCode::BOOTREPLY,
1334        yiaddr: YIADDR,
1335        message_type: Some(dhcp_protocol::MessageType::DHCPACK),
1336        server_identifier: None,
1337        subnet_mask: Some(TEST_SUBNET_MASK),
1338        lease_length_secs: Some(LEASE_LENGTH_SECS),
1339        renewal_time_secs: None,
1340        rebinding_time_secs: None,
1341        message: None,
1342        include_duplicate_option: false,
1343    } => Ok(IncomingResponseToRequest::Ack(FieldsToRetainFromAck {
1344        yiaddr: net_types::ip::Ipv4Addr::from(YIADDR)
1345            .try_into()
1346            .expect("should be specified"),
1347        server_identifier: None,
1348        ip_address_lease_time_secs: LEASE_LENGTH_SECS_NONZERO,
1349        parameters: vec![
1350            dhcp_protocol::DhcpOption::SubnetMask(TEST_SUBNET_MASK),
1351            dhcp_protocol::DhcpOption::DomainName(DOMAIN_NAME.to_owned())
1352        ],
1353        renewal_time_value_secs: None,
1354        rebinding_time_value_secs: None,
1355    })); "accepts DHCPACK with no server identifier")]
1356    #[test_case(VaryingReplyToRequestFields {
1357        op: dhcp_protocol::OpCode::BOOTREPLY,
1358        yiaddr: YIADDR,
1359        message_type: Some(dhcp_protocol::MessageType::DHCPACK),
1360        server_identifier: Some(SERVER_IP),
1361        subnet_mask: Some(TEST_SUBNET_MASK),
1362        lease_length_secs: Some(LEASE_LENGTH_SECS),
1363        renewal_time_secs: Some(RENEWAL_TIME_SECS),
1364        rebinding_time_secs: Some(REBINDING_TIME_SECS),
1365        message: None,
1366        include_duplicate_option: false,
1367    } => Ok(IncomingResponseToRequest::Ack(FieldsToRetainFromAck {
1368        yiaddr: net_types::ip::Ipv4Addr::from(YIADDR)
1369            .try_into()
1370            .expect("should be specified"),
1371        server_identifier: Some(
1372            net_types::ip::Ipv4Addr::from(SERVER_IP)
1373                .try_into()
1374                .expect("should be specified"),
1375        ),
1376        ip_address_lease_time_secs: LEASE_LENGTH_SECS_NONZERO,
1377        parameters: vec![
1378            dhcp_protocol::DhcpOption::SubnetMask(TEST_SUBNET_MASK),
1379            dhcp_protocol::DhcpOption::DomainName(DOMAIN_NAME.to_owned())
1380        ],
1381        renewal_time_value_secs: Some(RENEWAL_TIME_SECS),
1382        rebinding_time_value_secs: Some(REBINDING_TIME_SECS),
1383    })); "accepts DHCPACK with renew and rebind times")]
1384    #[test_case(VaryingReplyToRequestFields {
1385        op: dhcp_protocol::OpCode::BOOTREPLY,
1386        yiaddr: Ipv4Addr::UNSPECIFIED,
1387        message_type: Some(dhcp_protocol::MessageType::DHCPNAK),
1388        server_identifier: Some(SERVER_IP),
1389        subnet_mask: None,
1390        lease_length_secs: None,
1391        renewal_time_secs: None,
1392        rebinding_time_secs: None,
1393        message: Some(MESSAGE.to_owned()),
1394        include_duplicate_option: false,
1395    } => Ok(IncomingResponseToRequest::Nak(FieldsToRetainFromNak {
1396        server_identifier: net_types::ip::Ipv4Addr::from(SERVER_IP)
1397            .try_into()
1398            .expect("should be specified"),
1399        message: Some(MESSAGE.to_owned()),
1400        client_identifier: None,
1401    })); "accepts good DHCPNAK")]
1402    #[test_case(VaryingReplyToRequestFields {
1403        op: dhcp_protocol::OpCode::BOOTREPLY,
1404        yiaddr: YIADDR,
1405        message_type: Some(dhcp_protocol::MessageType::DHCPACK),
1406        server_identifier: Some(SERVER_IP),
1407        subnet_mask: Some(TEST_SUBNET_MASK),
1408        lease_length_secs: None,
1409        renewal_time_secs: Some(RENEWAL_TIME_SECS),
1410        rebinding_time_secs: Some(REBINDING_TIME_SECS),
1411        message: None,
1412        include_duplicate_option: false,
1413    } =>  Err(IncomingResponseToRequestError::NoLeaseTime); "rejects DHCPACK with no lease time")]
1414    #[test_case(
1415        VaryingReplyToRequestFields {
1416            op: dhcp_protocol::OpCode::BOOTREPLY,
1417            yiaddr: YIADDR,
1418            message_type: Some(dhcp_protocol::MessageType::DHCPACK),
1419            server_identifier: Some(SERVER_IP),
1420            subnet_mask: None,
1421            lease_length_secs: Some(LEASE_LENGTH_SECS),
1422            renewal_time_secs: None,
1423            rebinding_time_secs: None,
1424            message: None,
1425            include_duplicate_option: false,
1426        } => Err(IncomingResponseToRequestError::MissingRequiredOption(
1427            dhcp_protocol::OptionCode::SubnetMask
1428        )); "rejects DHCPACK without required subnet mask")]
1429    #[test_case(VaryingReplyToRequestFields {
1430        op: dhcp_protocol::OpCode::BOOTREPLY,
1431        yiaddr: YIADDR,
1432        message_type: Some(dhcp_protocol::MessageType::DHCPACK),
1433        server_identifier: Some(Ipv4Addr::UNSPECIFIED),
1434        subnet_mask: Some(TEST_SUBNET_MASK),
1435        lease_length_secs: Some(LEASE_LENGTH_SECS),
1436        renewal_time_secs: Some(RENEWAL_TIME_SECS),
1437        rebinding_time_secs: Some(REBINDING_TIME_SECS),
1438        message: None,
1439        include_duplicate_option: false,
1440    } => Err(IncomingResponseToRequestError::CommonError(
1441        CommonIncomingMessageError::UnspecifiedServerIdentifier,
1442    )); "rejects DHCPACK with unspecified server identifier")]
1443    #[test_case(VaryingReplyToRequestFields {
1444        op: dhcp_protocol::OpCode::BOOTREPLY,
1445        yiaddr: Ipv4Addr::UNSPECIFIED,
1446        message_type: Some(dhcp_protocol::MessageType::DHCPACK),
1447        server_identifier: Some(SERVER_IP),
1448        subnet_mask: Some(TEST_SUBNET_MASK),
1449        lease_length_secs: Some(LEASE_LENGTH_SECS),
1450        renewal_time_secs: Some(RENEWAL_TIME_SECS),
1451        rebinding_time_secs: Some(REBINDING_TIME_SECS),
1452        message: None,
1453        include_duplicate_option: false,
1454    } => Err(IncomingResponseToRequestError::UnspecifiedYiaddr); "rejects DHCPACK with unspecified yiaddr")]
1455    #[test_case(VaryingReplyToRequestFields {
1456        op: dhcp_protocol::OpCode::BOOTREPLY,
1457        yiaddr: Ipv4Addr::UNSPECIFIED,
1458        message_type: Some(dhcp_protocol::MessageType::DHCPNAK),
1459        server_identifier: Some(Ipv4Addr::UNSPECIFIED),
1460        subnet_mask: None,
1461        lease_length_secs: None,
1462        renewal_time_secs: None,
1463        rebinding_time_secs: None,
1464        message: Some(MESSAGE.to_owned()),
1465        include_duplicate_option: false,
1466    } => Err(IncomingResponseToRequestError::CommonError(
1467        CommonIncomingMessageError::UnspecifiedServerIdentifier,
1468    )); "rejects DHCPNAK with unspecified server identifier")]
1469    #[test_case(VaryingReplyToRequestFields {
1470        op: dhcp_protocol::OpCode::BOOTREPLY,
1471        yiaddr: Ipv4Addr::UNSPECIFIED,
1472        message_type: Some(dhcp_protocol::MessageType::DHCPNAK),
1473        server_identifier: None,
1474        subnet_mask: None,
1475        lease_length_secs: None,
1476        renewal_time_secs: None,
1477        rebinding_time_secs: None,
1478        message: Some(MESSAGE.to_owned()),
1479        include_duplicate_option: false,
1480    } => Err(IncomingResponseToRequestError::NoServerIdentifier) ; "rejects DHCPNAK with no server identifier")]
1481    #[test_case(VaryingReplyToRequestFields {
1482        op: dhcp_protocol::OpCode::BOOTREQUEST,
1483        yiaddr: Ipv4Addr::UNSPECIFIED,
1484        message_type: Some(dhcp_protocol::MessageType::DHCPNAK),
1485        server_identifier: Some(SERVER_IP),
1486        subnet_mask: None,
1487        lease_length_secs: None,
1488        renewal_time_secs: None,
1489        rebinding_time_secs: None,
1490        message: Some(MESSAGE.to_owned()),
1491        include_duplicate_option: false,
1492    } => Err(IncomingResponseToRequestError::CommonError(
1493        CommonIncomingMessageError::NotBootReply(dhcp_protocol::OpCode::BOOTREQUEST),
1494    )) ; "rejects non-bootreply")]
1495    #[test_case(VaryingReplyToRequestFields {
1496        op: dhcp_protocol::OpCode::BOOTREPLY,
1497        yiaddr: Ipv4Addr::UNSPECIFIED,
1498        message_type: Some(dhcp_protocol::MessageType::DHCPOFFER),
1499        server_identifier: Some(SERVER_IP),
1500        subnet_mask: Some(TEST_SUBNET_MASK),
1501        lease_length_secs: None,
1502        renewal_time_secs: None,
1503        rebinding_time_secs: None,
1504        message: Some(MESSAGE.to_owned()),
1505        include_duplicate_option: false,
1506    } => Err(IncomingResponseToRequestError::NotDhcpAckOrNak(
1507        dhcp_protocol::MessageType::DHCPOFFER,
1508    )) ; "rejects non-DHCPACK or DHCPNAK")]
1509    #[test_case(VaryingReplyToRequestFields {
1510        op: dhcp_protocol::OpCode::BOOTREPLY,
1511        yiaddr: Ipv4Addr::UNSPECIFIED,
1512        message_type: None,
1513        server_identifier: Some(SERVER_IP),
1514        subnet_mask: None,
1515        lease_length_secs: None,
1516        renewal_time_secs: None,
1517        rebinding_time_secs: None,
1518        message: Some(MESSAGE.to_owned()),
1519        include_duplicate_option: false,
1520    } => Err(IncomingResponseToRequestError::CommonError(
1521        CommonIncomingMessageError::BuilderMissingField("message_type"),
1522    )) ; "rejects missing DHCP message type")]
1523    #[test_case( VaryingReplyToRequestFields {
1524            op: dhcp_protocol::OpCode::BOOTREPLY,
1525            yiaddr: YIADDR,
1526            message_type: Some(dhcp_protocol::MessageType::DHCPACK),
1527            server_identifier: Some(SERVER_IP),
1528            subnet_mask: Some(TEST_SUBNET_MASK),
1529            lease_length_secs: Some(LEASE_LENGTH_SECS),
1530            renewal_time_secs: Some(RENEWAL_TIME_SECS),
1531            rebinding_time_secs: Some(REBINDING_TIME_SECS),
1532            message: None,
1533            include_duplicate_option: true,
1534        } => Ok(IncomingResponseToRequest::Ack(FieldsToRetainFromAck {
1535            yiaddr: net_types::ip::Ipv4Addr::from(YIADDR)
1536                .try_into()
1537                .expect("should be specified"),
1538            server_identifier: Some(
1539                net_types::ip::Ipv4Addr::from(SERVER_IP)
1540                    .try_into()
1541                    .expect("should be specified"),
1542            ),
1543            ip_address_lease_time_secs: LEASE_LENGTH_SECS_NONZERO,
1544            parameters: vec![
1545                dhcp_protocol::DhcpOption::SubnetMask(TEST_SUBNET_MASK),
1546                dhcp_protocol::DhcpOption::DomainName(DOMAIN_NAME.to_owned())
1547            ],
1548            renewal_time_value_secs: Some(RENEWAL_TIME_SECS),
1549            rebinding_time_value_secs: Some(REBINDING_TIME_SECS),
1550        })); "accepts good DHCPACK with duplicate option")]
1551    fn fields_to_retain_during_requesting(
1552        incoming_fields: VaryingReplyToRequestFields,
1553    ) -> Result<
1554        IncomingResponseToRequest<Option<net_types::SpecifiedAddr<net_types::ip::Ipv4Addr>>>,
1555        IncomingResponseToRequestError,
1556    > {
1557        use super::fields_to_retain_from_response_to_request as fields;
1558        use dhcp_protocol::DhcpOption;
1559
1560        let VaryingReplyToRequestFields {
1561            op,
1562            yiaddr,
1563            message_type,
1564            server_identifier,
1565            subnet_mask,
1566            lease_length_secs,
1567            renewal_time_secs,
1568            rebinding_time_secs,
1569            message,
1570            include_duplicate_option,
1571        } = incoming_fields;
1572
1573        let message = dhcp_protocol::Message {
1574            op,
1575            xid: 1,
1576            secs: 0,
1577            bdcast_flag: false,
1578            ciaddr: Ipv4Addr::UNSPECIFIED,
1579            yiaddr,
1580            siaddr: Ipv4Addr::UNSPECIFIED,
1581            giaddr: Ipv4Addr::UNSPECIFIED,
1582            chaddr: net_mac!("01:02:03:04:05:06"),
1583            sname: String::new(),
1584            file: String::new(),
1585            options: std::iter::empty()
1586                .chain(message_type.map(DhcpOption::DhcpMessageType))
1587                .chain(server_identifier.map(DhcpOption::ServerIdentifier))
1588                .chain(subnet_mask.map(DhcpOption::SubnetMask))
1589                .chain(lease_length_secs.map(DhcpOption::IpAddressLeaseTime))
1590                .chain(renewal_time_secs.map(DhcpOption::RenewalTimeValue))
1591                .chain(rebinding_time_secs.map(DhcpOption::RebindingTimeValue))
1592                .chain(message.map(DhcpOption::Message))
1593                // Include a parameter that the client didn't request so that we can
1594                // assert that the client ignored it.
1595                .chain(std::iter::once(dhcp_protocol::DhcpOption::InterfaceMtu(1)))
1596                // Include a parameter that the client did request so that we can
1597                // check that it's included in the acquired parameters map.
1598                .chain(std::iter::once(dhcp_protocol::DhcpOption::DomainName(
1599                    DOMAIN_NAME.to_owned(),
1600                )))
1601                .chain(
1602                    include_duplicate_option
1603                        .then_some(dhcp_protocol::DhcpOption::DomainName(DOMAIN_NAME.to_owned())),
1604                )
1605                .collect(),
1606        };
1607
1608        fields(
1609            &[
1610                (dhcp_protocol::OptionCode::SubnetMask, OptionRequested::Required),
1611                (dhcp_protocol::OptionCode::DomainName, OptionRequested::Optional),
1612            ]
1613            .into_iter()
1614            .collect(),
1615            message,
1616        )
1617    }
1618}