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