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