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