Skip to main content

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