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