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::ParseError),
20 #[error("IPv4 packet protocol was not UDP")]
21 NotUdp,
22 #[error("parsing UDP datagram: {0}")]
23 Udp(packet_formats::error::ParseError),
24 #[error("incoming packet destined for wrong port: {0}")]
25 WrongPort(NonZeroU16),
26 #[error("incoming packet has wrong source address: {0}")]
27 WrongSource(std::net::SocketAddr),
28 #[error("parsing DHCP message: {0}")]
29 Dhcp(dhcp_protocol::ProtocolError),
30}
31
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 server identifier")]
784 NoServerIdentifier,
785 #[error("missing required option: {0:?}")]
786 MissingRequiredOption(dhcp_protocol::OptionCode),
787}
788
789#[derive(Default, Debug)]
792pub(crate) struct IncomingResponseToRequestErrorCounters {
793 pub(crate) common: CommonIncomingMessageErrorCounters,
795 pub(crate) not_dhcp_ack_or_nak: Counter,
797 pub(crate) unspecified_yiaddr: Counter,
799 pub(crate) no_server_identifier: Counter,
801 pub(crate) missing_required_option: Counter,
803}
804
805impl IncomingResponseToRequestErrorCounters {
806 pub(crate) fn record(&self, inspector: &mut impl Inspector) {
808 let Self {
809 common,
810 not_dhcp_ack_or_nak,
811 unspecified_yiaddr,
812 no_server_identifier,
813 missing_required_option,
814 } = self;
815 common.record(inspector);
816 inspector.record_usize("NotDhcpAckOrNak", not_dhcp_ack_or_nak.load());
817 inspector.record_usize("UnspecifiedYiaddr", unspecified_yiaddr.load());
818 inspector.record_usize("NoServerIdentifier", no_server_identifier.load());
819 inspector.record_usize("MissingRequiredOption", missing_required_option.load());
820 }
821
822 pub(crate) fn increment(&self, error: &IncomingResponseToRequestError) {
824 let Self {
825 common,
826 not_dhcp_ack_or_nak,
827 unspecified_yiaddr,
828 no_server_identifier,
829 missing_required_option,
830 } = self;
831 match error {
832 IncomingResponseToRequestError::CommonError(common_incoming_message_error) => {
833 common.increment(common_incoming_message_error)
834 }
835 IncomingResponseToRequestError::NotDhcpAckOrNak(_) => not_dhcp_ack_or_nak.increment(),
836 IncomingResponseToRequestError::UnspecifiedYiaddr => unspecified_yiaddr.increment(),
837 IncomingResponseToRequestError::NoServerIdentifier => no_server_identifier.increment(),
838 IncomingResponseToRequestError::MissingRequiredOption(_) => {
839 missing_required_option.increment()
840 }
841 }
842 }
843}
844
845#[derive(Debug, PartialEq)]
846pub(crate) struct FieldsToRetainFromAck<ServerIdentifier> {
847 pub(crate) yiaddr: net_types::SpecifiedAddr<net_types::ip::Ipv4Addr>,
848 pub(crate) server_identifier: ServerIdentifier,
849 pub(crate) ip_address_lease_time_secs: Option<NonZeroU32>,
854 pub(crate) renewal_time_value_secs: Option<u32>,
855 pub(crate) rebinding_time_value_secs: Option<u32>,
856 pub(crate) parameters: Vec<dhcp_protocol::DhcpOption>,
858}
859
860impl<ServerIdentifier> FieldsToRetainFromAck<ServerIdentifier> {
861 pub(crate) fn map_server_identifier<T, E>(
862 self,
863 f: impl FnOnce(ServerIdentifier) -> Result<T, E>,
864 ) -> Result<FieldsToRetainFromAck<T>, E> {
865 let Self {
866 yiaddr,
867 server_identifier,
868 ip_address_lease_time_secs,
869 renewal_time_value_secs,
870 rebinding_time_value_secs,
871 parameters,
872 } = self;
873 Ok(FieldsToRetainFromAck {
874 yiaddr,
875 server_identifier: f(server_identifier)?,
876 ip_address_lease_time_secs,
877 renewal_time_value_secs,
878 rebinding_time_value_secs,
879 parameters,
880 })
881 }
882}
883
884#[derive(Debug, PartialEq)]
885pub(crate) struct FieldsToRetainFromNak {
886 pub(crate) server_identifier: net_types::SpecifiedAddr<net_types::ip::Ipv4Addr>,
887 pub(crate) message: Option<String>,
888 pub(crate) client_identifier: Option<
889 AtLeast<
890 { dhcp_protocol::CLIENT_IDENTIFIER_MINIMUM_LENGTH },
891 AtMostBytes<{ dhcp_protocol::U8_MAX_AS_USIZE }, Vec<u8>>,
892 >,
893 >,
894}
895
896pub(crate) fn fields_to_retain_from_response_to_request(
897 requested_parameters: &OptionCodeMap<OptionRequested>,
898 message: dhcp_protocol::Message,
899) -> Result<
900 IncomingResponseToRequest<
901 Option<net_types::SpecifiedAddr<net_types::ip::Ipv4Addr>>,
907 >,
908 IncomingResponseToRequestError,
909> {
910 let CommonIncomingMessageFields {
911 message_type,
912 server_identifier,
913 yiaddr,
914 ip_address_lease_time_secs,
915 renewal_time_value_secs,
916 rebinding_time_value_secs,
917 parameters,
918 seen_option_codes,
919 message,
920 client_identifier,
921 } = collect_common_fields(requested_parameters, message)?;
922
923 match message_type {
924 dhcp_protocol::MessageType::DHCPACK => {
925 if let Some(missing_option_code) =
929 requested_parameters.iter_required().find(|code| !seen_option_codes.contains(*code))
930 {
931 return Err(IncomingResponseToRequestError::MissingRequiredOption(
932 missing_option_code,
933 ));
934 }
935 Ok(IncomingResponseToRequest::Ack(FieldsToRetainFromAck {
936 yiaddr: yiaddr.ok_or(IncomingResponseToRequestError::UnspecifiedYiaddr)?,
937 server_identifier,
938 ip_address_lease_time_secs: ip_address_lease_time_secs,
939 renewal_time_value_secs,
940 rebinding_time_value_secs,
941 parameters,
942 }))
943 }
944 dhcp_protocol::MessageType::DHCPNAK => {
945 Ok(IncomingResponseToRequest::Nak(FieldsToRetainFromNak {
946 server_identifier: server_identifier
947 .ok_or(IncomingResponseToRequestError::NoServerIdentifier)?,
948 message,
949 client_identifier,
950 }))
951 }
952 dhcp_protocol::MessageType::DHCPDISCOVER
953 | dhcp_protocol::MessageType::DHCPOFFER
954 | dhcp_protocol::MessageType::DHCPREQUEST
955 | dhcp_protocol::MessageType::DHCPDECLINE
956 | dhcp_protocol::MessageType::DHCPRELEASE
957 | dhcp_protocol::MessageType::DHCPINFORM => {
958 Err(IncomingResponseToRequestError::NotDhcpAckOrNak(message_type))
959 }
960 }
961}
962
963#[cfg(test)]
964mod test {
965 use super::*;
966 use assert_matches::assert_matches;
967 use bstr::BString;
968 use dhcp_protocol::{CLIENT_PORT, SERVER_PORT};
969 use net_declare::net::prefix_length_v4;
970 use net_declare::{net_ip_v4, net_mac, std_ip_v4};
971 use net_types::ip::{Ip, Ipv4, PrefixLength};
972 use std::net::Ipv4Addr;
973 use test_case::test_case;
974
975 #[test]
976 fn serialize_parse_roundtrip() {
977 let make_message = || dhcp_protocol::Message {
978 op: dhcp_protocol::OpCode::BOOTREQUEST,
979 xid: 124,
980 secs: 99,
981 bdcast_flag: false,
982 ciaddr: net_ip_v4!("1.2.3.4").into(),
983 yiaddr: net_ip_v4!("5.6.7.8").into(),
984 siaddr: net_ip_v4!("9.10.11.12").into(),
985 giaddr: net_ip_v4!("13.14.15.16").into(),
986 chaddr: net_mac!("17:18:19:20:21:22"),
987 sname: BString::from("this is a sname"),
988 file: BString::from("this is the boot filename"),
989 options: vec![
990 dhcp_protocol::DhcpOption::DhcpMessageType(
991 dhcp_protocol::MessageType::DHCPDISCOVER,
992 ),
993 dhcp_protocol::DhcpOption::RequestedIpAddress(net_ip_v4!("5.6.7.8").into()),
994 ],
995 };
996 let packet = serialize_dhcp_message_to_ip_packet(
997 make_message(),
998 Ipv4Addr::UNSPECIFIED,
999 CLIENT_PORT,
1000 Ipv4Addr::BROADCAST,
1001 SERVER_PORT,
1002 );
1003 let (src_addr, parsed_message) =
1004 parse_dhcp_message_from_ip_packet(packet.as_ref(), SERVER_PORT).unwrap();
1005
1006 assert_eq!(net_types::ip::Ipv4::UNSPECIFIED_ADDRESS, src_addr);
1007 assert_eq!(make_message(), parsed_message);
1008 }
1009
1010 #[test]
1011 fn nonsense() {
1012 assert_matches!(
1013 parse_dhcp_message_from_ip_packet(
1014 &[0xD, 0xE, 0xA, 0xD, 0xB, 0xE, 0xE, 0xF],
1015 NonZeroU16::new(1).unwrap()
1016 ),
1017 Err(ParseError::Ipv4(parse_error)) => {
1018 assert_eq!(parse_error, packet_formats::error::ParseError::Format)
1019 }
1020 )
1021 }
1022
1023 #[test]
1024 fn not_udp() {
1025 let src_ip = Ipv4Addr::UNSPECIFIED.into();
1026 let dst_ip = Ipv4Addr::BROADCAST.into();
1027 let tcp_builder: packet_formats::tcp::TcpSegmentBuilder<net_types::ip::Ipv4Addr> =
1028 packet_formats::tcp::TcpSegmentBuilder::new(
1029 src_ip,
1030 dst_ip,
1031 CLIENT_PORT,
1032 SERVER_PORT,
1033 0,
1034 None,
1035 0,
1036 );
1037 let ipv4_builder = packet_formats::ipv4::Ipv4PacketBuilder::new(
1038 src_ip,
1039 dst_ip,
1040 DEFAULT_TTL,
1041 packet_formats::ip::Ipv4Proto::Proto(packet_formats::ip::IpProto::Tcp),
1042 );
1043 let bytes = vec![1, 2, 3, 4, 5]
1044 .into_serializer()
1045 .wrap_in(tcp_builder)
1046 .wrap_in(ipv4_builder)
1047 .serialize_vec_outer()
1048 .expect("serialize error");
1049
1050 assert_matches!(
1051 parse_dhcp_message_from_ip_packet(bytes.as_ref(), NonZeroU16::new(1).unwrap()),
1052 Err(ParseError::NotUdp)
1053 );
1054 }
1055
1056 #[test]
1057 fn wrong_port() {
1058 let src_ip = Ipv4Addr::UNSPECIFIED.into();
1059 let dst_ip = Ipv4Addr::BROADCAST.into();
1060
1061 let udp_builder: packet_formats::udp::UdpPacketBuilder<net_types::ip::Ipv4Addr> =
1062 packet_formats::udp::UdpPacketBuilder::new(
1063 src_ip,
1064 dst_ip,
1065 Some(CLIENT_PORT),
1066 SERVER_PORT,
1067 );
1068 let ipv4_builder = packet_formats::ipv4::Ipv4PacketBuilder::new(
1069 src_ip,
1070 dst_ip,
1071 DEFAULT_TTL,
1072 packet_formats::ip::Ipv4Proto::Proto(packet_formats::ip::IpProto::Udp),
1073 );
1074
1075 let bytes = "hello_world"
1076 .bytes()
1077 .collect::<Vec<_>>()
1078 .into_serializer()
1079 .wrap_in(udp_builder)
1080 .wrap_in(ipv4_builder)
1081 .serialize_vec_outer()
1082 .expect("serialize error");
1083
1084 let result = parse_dhcp_message_from_ip_packet(bytes.as_ref(), CLIENT_PORT);
1085 assert_matches!(result, Err(ParseError::WrongPort(port)) => assert_eq!(port, SERVER_PORT));
1086 }
1087
1088 struct VaryingOfferFields {
1089 op: dhcp_protocol::OpCode,
1090 yiaddr: Ipv4Addr,
1091 message_type: Option<dhcp_protocol::MessageType>,
1092 server_identifier: Option<Ipv4Addr>,
1093 subnet_mask: Option<PrefixLength<Ipv4>>,
1094 lease_length_secs: Option<u32>,
1095 include_duplicate_option: bool,
1096 }
1097
1098 const SERVER_IP: Ipv4Addr = std_ip_v4!("192.168.1.1");
1099 const TEST_SUBNET_MASK: PrefixLength<Ipv4> = prefix_length_v4!(24);
1100 const LEASE_LENGTH_SECS: u32 = 100;
1101 const LEASE_LENGTH_SECS_NONZERO: NonZeroU32 = NonZeroU32::new(LEASE_LENGTH_SECS).unwrap();
1102 const YIADDR: Ipv4Addr = std_ip_v4!("192.168.1.5");
1103
1104 #[test_case(VaryingOfferFields {
1105 op: dhcp_protocol::OpCode::BOOTREPLY,
1106 yiaddr: YIADDR,
1107 message_type: Some(dhcp_protocol::MessageType::DHCPOFFER),
1108 server_identifier: Some(SERVER_IP),
1109 subnet_mask: Some(TEST_SUBNET_MASK),
1110 lease_length_secs: Some(LEASE_LENGTH_SECS),
1111 include_duplicate_option: false,
1112 } => Ok(FieldsFromOfferToUseInRequest {
1113 server_identifier: net_types::ip::Ipv4Addr::from(SERVER_IP)
1114 .try_into()
1115 .expect("should be specified"),
1116 ip_address_lease_time_secs: Some(LEASE_LENGTH_SECS_NONZERO),
1117 ip_address_to_request: net_types::ip::Ipv4Addr::from(YIADDR)
1118 .try_into()
1119 .expect("should be specified"),
1120 }); "accepts good offer with lease time")]
1121 #[test_case(VaryingOfferFields {
1122 op: dhcp_protocol::OpCode::BOOTREPLY,
1123 yiaddr: YIADDR,
1124 message_type: Some(dhcp_protocol::MessageType::DHCPOFFER),
1125 server_identifier: Some(SERVER_IP),
1126 subnet_mask: Some(TEST_SUBNET_MASK),
1127 lease_length_secs: None,
1128 include_duplicate_option: false,
1129 } => Ok(FieldsFromOfferToUseInRequest {
1130 server_identifier: net_types::ip::Ipv4Addr::from(SERVER_IP)
1131 .try_into()
1132 .expect("should be specified"),
1133 ip_address_lease_time_secs: None,
1134 ip_address_to_request: net_types::ip::Ipv4Addr::from(YIADDR)
1135 .try_into()
1136 .expect("should be specified"),
1137 }); "accepts good offer without lease time")]
1138 #[test_case(VaryingOfferFields {
1139 op: dhcp_protocol::OpCode::BOOTREPLY,
1140 yiaddr: YIADDR,
1141 message_type: Some(dhcp_protocol::MessageType::DHCPOFFER),
1142 server_identifier: Some(Ipv4Addr::UNSPECIFIED),
1143 subnet_mask: Some(TEST_SUBNET_MASK),
1144 lease_length_secs: Some(LEASE_LENGTH_SECS),
1145 include_duplicate_option: false,
1146 } => Err(SelectingIncomingMessageError::CommonError(
1147 CommonIncomingMessageError::UnspecifiedServerIdentifier,
1148 )); "rejects offer with unspecified server identifier")]
1149 #[test_case(VaryingOfferFields {
1150 op: dhcp_protocol::OpCode::BOOTREPLY,
1151 yiaddr: YIADDR,
1152 message_type: Some(dhcp_protocol::MessageType::DHCPOFFER),
1153 server_identifier: Some(SERVER_IP),
1154 subnet_mask: None,
1155 lease_length_secs: Some(LEASE_LENGTH_SECS),
1156 include_duplicate_option: false,
1157 } => Err(SelectingIncomingMessageError::MissingRequiredOption(
1158 dhcp_protocol::OptionCode::SubnetMask,
1159 )); "rejects offer without required subnet mask")]
1160 #[test_case(VaryingOfferFields {
1161 op: dhcp_protocol::OpCode::BOOTREPLY,
1162 yiaddr: YIADDR,
1163 message_type: Some(dhcp_protocol::MessageType::DHCPOFFER),
1164 server_identifier: None,
1165 subnet_mask: Some(TEST_SUBNET_MASK),
1166 lease_length_secs: Some(LEASE_LENGTH_SECS),
1167 include_duplicate_option: false,
1168 } => Err(SelectingIncomingMessageError::NoServerIdentifier); "rejects offer with no server identifier option")]
1169 #[test_case(VaryingOfferFields {
1170 op: dhcp_protocol::OpCode::BOOTREPLY,
1171 yiaddr: Ipv4Addr::UNSPECIFIED,
1172 message_type: Some(dhcp_protocol::MessageType::DHCPOFFER),
1173 server_identifier: Some(SERVER_IP),
1174 subnet_mask: Some(TEST_SUBNET_MASK),
1175 lease_length_secs: Some(LEASE_LENGTH_SECS),
1176 include_duplicate_option: false,
1177 } => Err(SelectingIncomingMessageError::UnspecifiedYiaddr) ; "rejects offer with unspecified yiaddr")]
1178 #[test_case(VaryingOfferFields {
1179 op: dhcp_protocol::OpCode::BOOTREQUEST,
1180 yiaddr: YIADDR,
1181 message_type: Some(dhcp_protocol::MessageType::DHCPOFFER),
1182 server_identifier: Some(SERVER_IP),
1183 subnet_mask: Some(TEST_SUBNET_MASK),
1184 lease_length_secs: Some(LEASE_LENGTH_SECS),
1185 include_duplicate_option: false,
1186 } => Err(SelectingIncomingMessageError::CommonError(
1187 CommonIncomingMessageError::NotBootReply(dhcp_protocol::OpCode::BOOTREQUEST),
1188 )); "rejects offer that isn't a bootreply")]
1189 #[test_case(VaryingOfferFields {
1190 op: dhcp_protocol::OpCode::BOOTREPLY,
1191 yiaddr: YIADDR,
1192 message_type: Some(dhcp_protocol::MessageType::DHCPACK),
1193 server_identifier: Some(SERVER_IP),
1194 subnet_mask: Some(TEST_SUBNET_MASK),
1195 lease_length_secs: Some(LEASE_LENGTH_SECS),
1196 include_duplicate_option: false,
1197 } => Err(
1198 SelectingIncomingMessageError::NotDhcpOffer(dhcp_protocol::MessageType::DHCPACK),
1199 ); "rejects offer with wrong DHCP message type")]
1200 #[test_case(VaryingOfferFields {
1201 op: dhcp_protocol::OpCode::BOOTREPLY,
1202 yiaddr: YIADDR,
1203 message_type: None,
1204 server_identifier: Some(SERVER_IP),
1205 subnet_mask: Some(TEST_SUBNET_MASK),
1206 lease_length_secs: Some(LEASE_LENGTH_SECS),
1207 include_duplicate_option: false,
1208 } => Err(SelectingIncomingMessageError::CommonError(
1209 CommonIncomingMessageError::BuilderMissingField("message_type"),
1210 )); "rejects offer with no DHCP message type option")]
1211 #[test_case(VaryingOfferFields {
1212 op: dhcp_protocol::OpCode::BOOTREPLY,
1213 yiaddr: YIADDR,
1214 message_type: Some(dhcp_protocol::MessageType::DHCPOFFER),
1215 server_identifier: Some(SERVER_IP),
1216 subnet_mask: Some(TEST_SUBNET_MASK),
1217 lease_length_secs: Some(LEASE_LENGTH_SECS),
1218 include_duplicate_option: true,
1219 } => Ok(FieldsFromOfferToUseInRequest {
1220 server_identifier: net_types::ip::Ipv4Addr::from(SERVER_IP)
1221 .try_into()
1222 .expect("should be specified"),
1223 ip_address_lease_time_secs: Some(LEASE_LENGTH_SECS_NONZERO),
1224 ip_address_to_request: net_types::ip::Ipv4Addr::from(YIADDR)
1225 .try_into()
1226 .expect("should be specified"),
1227 }); "accepts good offer with duplicate options")]
1228 fn fields_from_offer_to_use_in_request(
1229 offer_fields: VaryingOfferFields,
1230 ) -> Result<FieldsFromOfferToUseInRequest, SelectingIncomingMessageError> {
1231 use super::fields_to_retain_from_selecting as fields;
1232 use dhcp_protocol::DhcpOption;
1233
1234 let VaryingOfferFields {
1235 op,
1236 yiaddr,
1237 message_type,
1238 server_identifier,
1239 subnet_mask,
1240 lease_length_secs,
1241 include_duplicate_option,
1242 } = offer_fields;
1243
1244 let message = dhcp_protocol::Message {
1245 op,
1246 xid: 1,
1247 secs: 0,
1248 bdcast_flag: false,
1249 ciaddr: Ipv4Addr::UNSPECIFIED,
1250 yiaddr,
1251 siaddr: Ipv4Addr::UNSPECIFIED,
1252 giaddr: Ipv4Addr::UNSPECIFIED,
1253 chaddr: net_mac!("01:02:03:04:05:06"),
1254 sname: BString::default(),
1255 file: BString::default(),
1256 options: message_type
1257 .map(DhcpOption::DhcpMessageType)
1258 .into_iter()
1259 .chain(server_identifier.map(DhcpOption::ServerIdentifier))
1260 .chain(subnet_mask.map(DhcpOption::SubnetMask))
1261 .chain(lease_length_secs.map(DhcpOption::IpAddressLeaseTime))
1262 .chain(
1263 include_duplicate_option
1264 .then_some([
1265 dhcp_protocol::DhcpOption::DomainName("example.com".to_owned()),
1266 dhcp_protocol::DhcpOption::DomainName("example.com".to_owned()),
1267 ])
1268 .into_iter()
1269 .flatten(),
1270 )
1271 .collect(),
1272 };
1273
1274 fields(
1275 &std::iter::once((dhcp_protocol::OptionCode::SubnetMask, OptionRequested::Required))
1276 .collect(),
1277 message,
1278 )
1279 }
1280
1281 struct VaryingReplyToRequestFields {
1282 op: dhcp_protocol::OpCode,
1283 yiaddr: Ipv4Addr,
1284 message_type: Option<dhcp_protocol::MessageType>,
1285 server_identifier: Option<Ipv4Addr>,
1286 subnet_mask: Option<PrefixLength<Ipv4>>,
1287 lease_length_secs: Option<u32>,
1288 renewal_time_secs: Option<u32>,
1289 rebinding_time_secs: Option<u32>,
1290 message: Option<String>,
1291 include_duplicate_option: bool,
1292 }
1293
1294 const DOMAIN_NAME: &str = "example.com";
1295 const MESSAGE: &str = "message explaining why the DHCPNAK was sent";
1296 const RENEWAL_TIME_SECS: u32 = LEASE_LENGTH_SECS / 2;
1297 const REBINDING_TIME_SECS: u32 = LEASE_LENGTH_SECS * 3 / 4;
1298
1299 #[test_case(
1300 VaryingReplyToRequestFields {
1301 op: dhcp_protocol::OpCode::BOOTREPLY,
1302 yiaddr: YIADDR,
1303 message_type: Some(dhcp_protocol::MessageType::DHCPACK),
1304 server_identifier: Some(SERVER_IP),
1305 subnet_mask: Some(TEST_SUBNET_MASK),
1306 lease_length_secs: Some(LEASE_LENGTH_SECS),
1307 renewal_time_secs: None,
1308 rebinding_time_secs: None,
1309 message: None,
1310 include_duplicate_option: false,
1311 } => Ok(IncomingResponseToRequest::Ack(FieldsToRetainFromAck {
1312 yiaddr: net_types::ip::Ipv4Addr::from(YIADDR)
1313 .try_into()
1314 .expect("should be specified"),
1315 server_identifier: Some(
1316 net_types::ip::Ipv4Addr::from(SERVER_IP)
1317 .try_into()
1318 .expect("should be specified"),
1319 ),
1320 ip_address_lease_time_secs: Some(LEASE_LENGTH_SECS_NONZERO),
1321 parameters: vec![
1322 dhcp_protocol::DhcpOption::SubnetMask(TEST_SUBNET_MASK),
1323 dhcp_protocol::DhcpOption::DomainName(DOMAIN_NAME.to_owned())
1324 ],
1325 renewal_time_value_secs: None,
1326 rebinding_time_value_secs: None,
1327 })); "accepts good DHCPACK")]
1328 #[test_case(VaryingReplyToRequestFields {
1329 op: dhcp_protocol::OpCode::BOOTREPLY,
1330 yiaddr: YIADDR,
1331 message_type: Some(dhcp_protocol::MessageType::DHCPACK),
1332 server_identifier: None,
1333 subnet_mask: Some(TEST_SUBNET_MASK),
1334 lease_length_secs: Some(LEASE_LENGTH_SECS),
1335 renewal_time_secs: None,
1336 rebinding_time_secs: None,
1337 message: None,
1338 include_duplicate_option: false,
1339 } => Ok(IncomingResponseToRequest::Ack(FieldsToRetainFromAck {
1340 yiaddr: net_types::ip::Ipv4Addr::from(YIADDR)
1341 .try_into()
1342 .expect("should be specified"),
1343 server_identifier: None,
1344 ip_address_lease_time_secs: Some(LEASE_LENGTH_SECS_NONZERO),
1345 parameters: vec![
1346 dhcp_protocol::DhcpOption::SubnetMask(TEST_SUBNET_MASK),
1347 dhcp_protocol::DhcpOption::DomainName(DOMAIN_NAME.to_owned())
1348 ],
1349 renewal_time_value_secs: None,
1350 rebinding_time_value_secs: None,
1351 })); "accepts DHCPACK with no server identifier")]
1352 #[test_case(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: Some(RENEWAL_TIME_SECS),
1360 rebinding_time_secs: Some(REBINDING_TIME_SECS),
1361 message: None,
1362 include_duplicate_option: false,
1363 } => Ok(IncomingResponseToRequest::Ack(FieldsToRetainFromAck {
1364 yiaddr: net_types::ip::Ipv4Addr::from(YIADDR)
1365 .try_into()
1366 .expect("should be specified"),
1367 server_identifier: Some(
1368 net_types::ip::Ipv4Addr::from(SERVER_IP)
1369 .try_into()
1370 .expect("should be specified"),
1371 ),
1372 ip_address_lease_time_secs: Some(LEASE_LENGTH_SECS_NONZERO),
1373 parameters: vec![
1374 dhcp_protocol::DhcpOption::SubnetMask(TEST_SUBNET_MASK),
1375 dhcp_protocol::DhcpOption::DomainName(DOMAIN_NAME.to_owned())
1376 ],
1377 renewal_time_value_secs: Some(RENEWAL_TIME_SECS),
1378 rebinding_time_value_secs: Some(REBINDING_TIME_SECS),
1379 })); "accepts DHCPACK with renew and rebind times")]
1380 #[test_case(VaryingReplyToRequestFields {
1381 op: dhcp_protocol::OpCode::BOOTREPLY,
1382 yiaddr: Ipv4Addr::UNSPECIFIED,
1383 message_type: Some(dhcp_protocol::MessageType::DHCPNAK),
1384 server_identifier: Some(SERVER_IP),
1385 subnet_mask: None,
1386 lease_length_secs: None,
1387 renewal_time_secs: None,
1388 rebinding_time_secs: None,
1389 message: Some(MESSAGE.to_owned()),
1390 include_duplicate_option: false,
1391 } => Ok(IncomingResponseToRequest::Nak(FieldsToRetainFromNak {
1392 server_identifier: net_types::ip::Ipv4Addr::from(SERVER_IP)
1393 .try_into()
1394 .expect("should be specified"),
1395 message: Some(MESSAGE.to_owned()),
1396 client_identifier: None,
1397 })); "accepts good DHCPNAK")]
1398 #[test_case(VaryingReplyToRequestFields {
1399 op: dhcp_protocol::OpCode::BOOTREPLY,
1400 yiaddr: YIADDR,
1401 message_type: Some(dhcp_protocol::MessageType::DHCPACK),
1402 server_identifier: Some(SERVER_IP),
1403 subnet_mask: Some(TEST_SUBNET_MASK),
1404 lease_length_secs: None,
1405 renewal_time_secs: None,
1406 rebinding_time_secs: None,
1407 message: None,
1408 include_duplicate_option: false,
1409 } => Ok(IncomingResponseToRequest::Ack(FieldsToRetainFromAck {
1410 yiaddr: net_types::ip::Ipv4Addr::from(YIADDR).try_into().expect("should be specified"),
1411 server_identifier: Some(
1412 net_types::ip::Ipv4Addr::from(SERVER_IP).try_into().expect("should be specified")
1413 ),
1414 ip_address_lease_time_secs: None,
1415 parameters: vec![
1416 dhcp_protocol::DhcpOption::SubnetMask(TEST_SUBNET_MASK),
1417 dhcp_protocol::DhcpOption::DomainName(DOMAIN_NAME.to_owned())
1418 ],
1419 renewal_time_value_secs: None,
1420 rebinding_time_value_secs: None,
1421 })); "accepts DHCPACK with no lease time")]
1422 #[test_case(
1423 VaryingReplyToRequestFields {
1424 op: dhcp_protocol::OpCode::BOOTREPLY,
1425 yiaddr: YIADDR,
1426 message_type: Some(dhcp_protocol::MessageType::DHCPACK),
1427 server_identifier: Some(SERVER_IP),
1428 subnet_mask: None,
1429 lease_length_secs: Some(LEASE_LENGTH_SECS),
1430 renewal_time_secs: None,
1431 rebinding_time_secs: None,
1432 message: None,
1433 include_duplicate_option: false,
1434 } => Err(IncomingResponseToRequestError::MissingRequiredOption(
1435 dhcp_protocol::OptionCode::SubnetMask
1436 )); "rejects DHCPACK without required subnet mask")]
1437 #[test_case(VaryingReplyToRequestFields {
1438 op: dhcp_protocol::OpCode::BOOTREPLY,
1439 yiaddr: YIADDR,
1440 message_type: Some(dhcp_protocol::MessageType::DHCPACK),
1441 server_identifier: Some(Ipv4Addr::UNSPECIFIED),
1442 subnet_mask: Some(TEST_SUBNET_MASK),
1443 lease_length_secs: Some(LEASE_LENGTH_SECS),
1444 renewal_time_secs: Some(RENEWAL_TIME_SECS),
1445 rebinding_time_secs: Some(REBINDING_TIME_SECS),
1446 message: None,
1447 include_duplicate_option: false,
1448 } => Err(IncomingResponseToRequestError::CommonError(
1449 CommonIncomingMessageError::UnspecifiedServerIdentifier,
1450 )); "rejects DHCPACK with unspecified server identifier")]
1451 #[test_case(VaryingReplyToRequestFields {
1452 op: dhcp_protocol::OpCode::BOOTREPLY,
1453 yiaddr: Ipv4Addr::UNSPECIFIED,
1454 message_type: Some(dhcp_protocol::MessageType::DHCPACK),
1455 server_identifier: Some(SERVER_IP),
1456 subnet_mask: Some(TEST_SUBNET_MASK),
1457 lease_length_secs: Some(LEASE_LENGTH_SECS),
1458 renewal_time_secs: Some(RENEWAL_TIME_SECS),
1459 rebinding_time_secs: Some(REBINDING_TIME_SECS),
1460 message: None,
1461 include_duplicate_option: false,
1462 } => Err(IncomingResponseToRequestError::UnspecifiedYiaddr); "rejects DHCPACK with unspecified yiaddr")]
1463 #[test_case(VaryingReplyToRequestFields {
1464 op: dhcp_protocol::OpCode::BOOTREPLY,
1465 yiaddr: Ipv4Addr::UNSPECIFIED,
1466 message_type: Some(dhcp_protocol::MessageType::DHCPNAK),
1467 server_identifier: Some(Ipv4Addr::UNSPECIFIED),
1468 subnet_mask: None,
1469 lease_length_secs: None,
1470 renewal_time_secs: None,
1471 rebinding_time_secs: None,
1472 message: Some(MESSAGE.to_owned()),
1473 include_duplicate_option: false,
1474 } => Err(IncomingResponseToRequestError::CommonError(
1475 CommonIncomingMessageError::UnspecifiedServerIdentifier,
1476 )); "rejects DHCPNAK with unspecified server identifier")]
1477 #[test_case(VaryingReplyToRequestFields {
1478 op: dhcp_protocol::OpCode::BOOTREPLY,
1479 yiaddr: Ipv4Addr::UNSPECIFIED,
1480 message_type: Some(dhcp_protocol::MessageType::DHCPNAK),
1481 server_identifier: None,
1482 subnet_mask: None,
1483 lease_length_secs: None,
1484 renewal_time_secs: None,
1485 rebinding_time_secs: None,
1486 message: Some(MESSAGE.to_owned()),
1487 include_duplicate_option: false,
1488 } => Err(IncomingResponseToRequestError::NoServerIdentifier) ; "rejects DHCPNAK with no server identifier")]
1489 #[test_case(VaryingReplyToRequestFields {
1490 op: dhcp_protocol::OpCode::BOOTREQUEST,
1491 yiaddr: Ipv4Addr::UNSPECIFIED,
1492 message_type: Some(dhcp_protocol::MessageType::DHCPNAK),
1493 server_identifier: Some(SERVER_IP),
1494 subnet_mask: None,
1495 lease_length_secs: None,
1496 renewal_time_secs: None,
1497 rebinding_time_secs: None,
1498 message: Some(MESSAGE.to_owned()),
1499 include_duplicate_option: false,
1500 } => Err(IncomingResponseToRequestError::CommonError(
1501 CommonIncomingMessageError::NotBootReply(dhcp_protocol::OpCode::BOOTREQUEST),
1502 )) ; "rejects non-bootreply")]
1503 #[test_case(VaryingReplyToRequestFields {
1504 op: dhcp_protocol::OpCode::BOOTREPLY,
1505 yiaddr: Ipv4Addr::UNSPECIFIED,
1506 message_type: Some(dhcp_protocol::MessageType::DHCPOFFER),
1507 server_identifier: Some(SERVER_IP),
1508 subnet_mask: Some(TEST_SUBNET_MASK),
1509 lease_length_secs: None,
1510 renewal_time_secs: None,
1511 rebinding_time_secs: None,
1512 message: Some(MESSAGE.to_owned()),
1513 include_duplicate_option: false,
1514 } => Err(IncomingResponseToRequestError::NotDhcpAckOrNak(
1515 dhcp_protocol::MessageType::DHCPOFFER,
1516 )) ; "rejects non-DHCPACK or DHCPNAK")]
1517 #[test_case(VaryingReplyToRequestFields {
1518 op: dhcp_protocol::OpCode::BOOTREPLY,
1519 yiaddr: Ipv4Addr::UNSPECIFIED,
1520 message_type: None,
1521 server_identifier: Some(SERVER_IP),
1522 subnet_mask: None,
1523 lease_length_secs: None,
1524 renewal_time_secs: None,
1525 rebinding_time_secs: None,
1526 message: Some(MESSAGE.to_owned()),
1527 include_duplicate_option: false,
1528 } => Err(IncomingResponseToRequestError::CommonError(
1529 CommonIncomingMessageError::BuilderMissingField("message_type"),
1530 )) ; "rejects missing DHCP message type")]
1531 #[test_case( VaryingReplyToRequestFields {
1532 op: dhcp_protocol::OpCode::BOOTREPLY,
1533 yiaddr: YIADDR,
1534 message_type: Some(dhcp_protocol::MessageType::DHCPACK),
1535 server_identifier: Some(SERVER_IP),
1536 subnet_mask: Some(TEST_SUBNET_MASK),
1537 lease_length_secs: Some(LEASE_LENGTH_SECS),
1538 renewal_time_secs: Some(RENEWAL_TIME_SECS),
1539 rebinding_time_secs: Some(REBINDING_TIME_SECS),
1540 message: None,
1541 include_duplicate_option: true,
1542 } => Ok(IncomingResponseToRequest::Ack(FieldsToRetainFromAck {
1543 yiaddr: net_types::ip::Ipv4Addr::from(YIADDR)
1544 .try_into()
1545 .expect("should be specified"),
1546 server_identifier: Some(
1547 net_types::ip::Ipv4Addr::from(SERVER_IP)
1548 .try_into()
1549 .expect("should be specified"),
1550 ),
1551 ip_address_lease_time_secs: Some(LEASE_LENGTH_SECS_NONZERO),
1552 parameters: vec![
1553 dhcp_protocol::DhcpOption::SubnetMask(TEST_SUBNET_MASK),
1554 dhcp_protocol::DhcpOption::DomainName(DOMAIN_NAME.to_owned())
1555 ],
1556 renewal_time_value_secs: Some(RENEWAL_TIME_SECS),
1557 rebinding_time_value_secs: Some(REBINDING_TIME_SECS),
1558 })); "accepts good DHCPACK with duplicate option")]
1559 fn fields_to_retain_during_requesting(
1560 incoming_fields: VaryingReplyToRequestFields,
1561 ) -> Result<
1562 IncomingResponseToRequest<Option<net_types::SpecifiedAddr<net_types::ip::Ipv4Addr>>>,
1563 IncomingResponseToRequestError,
1564 > {
1565 use super::fields_to_retain_from_response_to_request as fields;
1566 use dhcp_protocol::DhcpOption;
1567
1568 let VaryingReplyToRequestFields {
1569 op,
1570 yiaddr,
1571 message_type,
1572 server_identifier,
1573 subnet_mask,
1574 lease_length_secs,
1575 renewal_time_secs,
1576 rebinding_time_secs,
1577 message,
1578 include_duplicate_option,
1579 } = incoming_fields;
1580
1581 let message = dhcp_protocol::Message {
1582 op,
1583 xid: 1,
1584 secs: 0,
1585 bdcast_flag: false,
1586 ciaddr: Ipv4Addr::UNSPECIFIED,
1587 yiaddr,
1588 siaddr: Ipv4Addr::UNSPECIFIED,
1589 giaddr: Ipv4Addr::UNSPECIFIED,
1590 chaddr: net_mac!("01:02:03:04:05:06"),
1591 sname: BString::default(),
1592 file: BString::default(),
1593 options: std::iter::empty()
1594 .chain(message_type.map(DhcpOption::DhcpMessageType))
1595 .chain(server_identifier.map(DhcpOption::ServerIdentifier))
1596 .chain(subnet_mask.map(DhcpOption::SubnetMask))
1597 .chain(lease_length_secs.map(DhcpOption::IpAddressLeaseTime))
1598 .chain(renewal_time_secs.map(DhcpOption::RenewalTimeValue))
1599 .chain(rebinding_time_secs.map(DhcpOption::RebindingTimeValue))
1600 .chain(message.map(DhcpOption::Message))
1601 .chain(std::iter::once(dhcp_protocol::DhcpOption::InterfaceMtu(1)))
1604 .chain(std::iter::once(dhcp_protocol::DhcpOption::DomainName(
1607 DOMAIN_NAME.to_owned(),
1608 )))
1609 .chain(
1610 include_duplicate_option
1611 .then_some(dhcp_protocol::DhcpOption::DomainName(DOMAIN_NAME.to_owned())),
1612 )
1613 .collect(),
1614 };
1615
1616 fields(
1617 &[
1618 (dhcp_protocol::OptionCode::SubnetMask, OptionRequested::Required),
1619 (dhcp_protocol::OptionCode::DomainName, OptionRequested::Optional),
1620 ]
1621 .into_iter()
1622 .collect(),
1623 message,
1624 )
1625 }
1626}