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