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 dhcp_protocol::{CLIENT_PORT, SERVER_PORT};
968 use net_declare::net::prefix_length_v4;
969 use net_declare::{net_ip_v4, net_mac, std_ip_v4};
970 use net_types::ip::{Ip, Ipv4, PrefixLength};
971 use std::net::Ipv4Addr;
972 use test_case::test_case;
973
974 #[test]
975 fn serialize_parse_roundtrip() {
976 let make_message = || dhcp_protocol::Message {
977 op: dhcp_protocol::OpCode::BOOTREQUEST,
978 xid: 124,
979 secs: 99,
980 bdcast_flag: false,
981 ciaddr: net_ip_v4!("1.2.3.4").into(),
982 yiaddr: net_ip_v4!("5.6.7.8").into(),
983 siaddr: net_ip_v4!("9.10.11.12").into(),
984 giaddr: net_ip_v4!("13.14.15.16").into(),
985 chaddr: net_mac!("17:18:19:20:21:22"),
986 sname: "this is a sname".to_owned(),
987 file: "this is the boot filename".to_owned(),
988 options: vec![
989 dhcp_protocol::DhcpOption::DhcpMessageType(
990 dhcp_protocol::MessageType::DHCPDISCOVER,
991 ),
992 dhcp_protocol::DhcpOption::RequestedIpAddress(net_ip_v4!("5.6.7.8").into()),
993 ],
994 };
995 let packet = serialize_dhcp_message_to_ip_packet(
996 make_message(),
997 Ipv4Addr::UNSPECIFIED,
998 CLIENT_PORT,
999 Ipv4Addr::BROADCAST,
1000 SERVER_PORT,
1001 );
1002 let (src_addr, parsed_message) =
1003 parse_dhcp_message_from_ip_packet(packet.as_ref(), SERVER_PORT).unwrap();
1004
1005 assert_eq!(net_types::ip::Ipv4::UNSPECIFIED_ADDRESS, src_addr);
1006 assert_eq!(make_message(), parsed_message);
1007 }
1008
1009 #[test]
1010 fn nonsense() {
1011 assert_matches!(
1012 parse_dhcp_message_from_ip_packet(
1013 &[0xD, 0xE, 0xA, 0xD, 0xB, 0xE, 0xE, 0xF],
1014 NonZeroU16::new(1).unwrap()
1015 ),
1016 Err(ParseError::Ipv4(parse_error)) => {
1017 assert_eq!(parse_error, packet_formats::error::ParseError::Format)
1018 }
1019 )
1020 }
1021
1022 #[test]
1023 fn not_udp() {
1024 let src_ip = Ipv4Addr::UNSPECIFIED.into();
1025 let dst_ip = Ipv4Addr::BROADCAST.into();
1026 let tcp_builder: packet_formats::tcp::TcpSegmentBuilder<net_types::ip::Ipv4Addr> =
1027 packet_formats::tcp::TcpSegmentBuilder::new(
1028 src_ip,
1029 dst_ip,
1030 CLIENT_PORT,
1031 SERVER_PORT,
1032 0,
1033 None,
1034 0,
1035 );
1036 let ipv4_builder = packet_formats::ipv4::Ipv4PacketBuilder::new(
1037 src_ip,
1038 dst_ip,
1039 DEFAULT_TTL,
1040 packet_formats::ip::Ipv4Proto::Proto(packet_formats::ip::IpProto::Tcp),
1041 );
1042 let bytes = vec![1, 2, 3, 4, 5]
1043 .into_serializer()
1044 .wrap_in(tcp_builder)
1045 .wrap_in(ipv4_builder)
1046 .serialize_vec_outer()
1047 .expect("serialize error");
1048
1049 assert_matches!(
1050 parse_dhcp_message_from_ip_packet(bytes.as_ref(), NonZeroU16::new(1).unwrap()),
1051 Err(ParseError::NotUdp)
1052 );
1053 }
1054
1055 #[test]
1056 fn wrong_port() {
1057 let src_ip = Ipv4Addr::UNSPECIFIED.into();
1058 let dst_ip = Ipv4Addr::BROADCAST.into();
1059
1060 let udp_builder: packet_formats::udp::UdpPacketBuilder<net_types::ip::Ipv4Addr> =
1061 packet_formats::udp::UdpPacketBuilder::new(
1062 src_ip,
1063 dst_ip,
1064 Some(CLIENT_PORT),
1065 SERVER_PORT,
1066 );
1067 let ipv4_builder = packet_formats::ipv4::Ipv4PacketBuilder::new(
1068 src_ip,
1069 dst_ip,
1070 DEFAULT_TTL,
1071 packet_formats::ip::Ipv4Proto::Proto(packet_formats::ip::IpProto::Udp),
1072 );
1073
1074 let bytes = "hello_world"
1075 .bytes()
1076 .collect::<Vec<_>>()
1077 .into_serializer()
1078 .wrap_in(udp_builder)
1079 .wrap_in(ipv4_builder)
1080 .serialize_vec_outer()
1081 .expect("serialize error");
1082
1083 let result = parse_dhcp_message_from_ip_packet(bytes.as_ref(), CLIENT_PORT);
1084 assert_matches!(result, Err(ParseError::WrongPort(port)) => assert_eq!(port, SERVER_PORT));
1085 }
1086
1087 struct VaryingOfferFields {
1088 op: dhcp_protocol::OpCode,
1089 yiaddr: Ipv4Addr,
1090 message_type: Option<dhcp_protocol::MessageType>,
1091 server_identifier: Option<Ipv4Addr>,
1092 subnet_mask: Option<PrefixLength<Ipv4>>,
1093 lease_length_secs: Option<u32>,
1094 include_duplicate_option: bool,
1095 }
1096
1097 const SERVER_IP: Ipv4Addr = std_ip_v4!("192.168.1.1");
1098 const TEST_SUBNET_MASK: PrefixLength<Ipv4> = prefix_length_v4!(24);
1099 const LEASE_LENGTH_SECS: u32 = 100;
1100 const LEASE_LENGTH_SECS_NONZERO: NonZeroU32 = NonZeroU32::new(LEASE_LENGTH_SECS).unwrap();
1101 const YIADDR: Ipv4Addr = std_ip_v4!("192.168.1.5");
1102
1103 #[test_case(VaryingOfferFields {
1104 op: dhcp_protocol::OpCode::BOOTREPLY,
1105 yiaddr: YIADDR,
1106 message_type: Some(dhcp_protocol::MessageType::DHCPOFFER),
1107 server_identifier: Some(SERVER_IP),
1108 subnet_mask: Some(TEST_SUBNET_MASK),
1109 lease_length_secs: Some(LEASE_LENGTH_SECS),
1110 include_duplicate_option: false,
1111 } => Ok(FieldsFromOfferToUseInRequest {
1112 server_identifier: net_types::ip::Ipv4Addr::from(SERVER_IP)
1113 .try_into()
1114 .expect("should be specified"),
1115 ip_address_lease_time_secs: Some(LEASE_LENGTH_SECS_NONZERO),
1116 ip_address_to_request: net_types::ip::Ipv4Addr::from(YIADDR)
1117 .try_into()
1118 .expect("should be specified"),
1119 }); "accepts good offer with lease time")]
1120 #[test_case(VaryingOfferFields {
1121 op: dhcp_protocol::OpCode::BOOTREPLY,
1122 yiaddr: YIADDR,
1123 message_type: Some(dhcp_protocol::MessageType::DHCPOFFER),
1124 server_identifier: Some(SERVER_IP),
1125 subnet_mask: Some(TEST_SUBNET_MASK),
1126 lease_length_secs: None,
1127 include_duplicate_option: false,
1128 } => Ok(FieldsFromOfferToUseInRequest {
1129 server_identifier: net_types::ip::Ipv4Addr::from(SERVER_IP)
1130 .try_into()
1131 .expect("should be specified"),
1132 ip_address_lease_time_secs: None,
1133 ip_address_to_request: net_types::ip::Ipv4Addr::from(YIADDR)
1134 .try_into()
1135 .expect("should be specified"),
1136 }); "accepts good offer without lease time")]
1137 #[test_case(VaryingOfferFields {
1138 op: dhcp_protocol::OpCode::BOOTREPLY,
1139 yiaddr: YIADDR,
1140 message_type: Some(dhcp_protocol::MessageType::DHCPOFFER),
1141 server_identifier: Some(Ipv4Addr::UNSPECIFIED),
1142 subnet_mask: Some(TEST_SUBNET_MASK),
1143 lease_length_secs: Some(LEASE_LENGTH_SECS),
1144 include_duplicate_option: false,
1145 } => Err(SelectingIncomingMessageError::CommonError(
1146 CommonIncomingMessageError::UnspecifiedServerIdentifier,
1147 )); "rejects offer with unspecified server identifier")]
1148 #[test_case(VaryingOfferFields {
1149 op: dhcp_protocol::OpCode::BOOTREPLY,
1150 yiaddr: YIADDR,
1151 message_type: Some(dhcp_protocol::MessageType::DHCPOFFER),
1152 server_identifier: Some(SERVER_IP),
1153 subnet_mask: None,
1154 lease_length_secs: Some(LEASE_LENGTH_SECS),
1155 include_duplicate_option: false,
1156 } => Err(SelectingIncomingMessageError::MissingRequiredOption(
1157 dhcp_protocol::OptionCode::SubnetMask,
1158 )); "rejects offer without required subnet mask")]
1159 #[test_case(VaryingOfferFields {
1160 op: dhcp_protocol::OpCode::BOOTREPLY,
1161 yiaddr: YIADDR,
1162 message_type: Some(dhcp_protocol::MessageType::DHCPOFFER),
1163 server_identifier: None,
1164 subnet_mask: Some(TEST_SUBNET_MASK),
1165 lease_length_secs: Some(LEASE_LENGTH_SECS),
1166 include_duplicate_option: false,
1167 } => Err(SelectingIncomingMessageError::NoServerIdentifier); "rejects offer with no server identifier option")]
1168 #[test_case(VaryingOfferFields {
1169 op: dhcp_protocol::OpCode::BOOTREPLY,
1170 yiaddr: Ipv4Addr::UNSPECIFIED,
1171 message_type: Some(dhcp_protocol::MessageType::DHCPOFFER),
1172 server_identifier: Some(SERVER_IP),
1173 subnet_mask: Some(TEST_SUBNET_MASK),
1174 lease_length_secs: Some(LEASE_LENGTH_SECS),
1175 include_duplicate_option: false,
1176 } => Err(SelectingIncomingMessageError::UnspecifiedYiaddr) ; "rejects offer with unspecified yiaddr")]
1177 #[test_case(VaryingOfferFields {
1178 op: dhcp_protocol::OpCode::BOOTREQUEST,
1179 yiaddr: YIADDR,
1180 message_type: Some(dhcp_protocol::MessageType::DHCPOFFER),
1181 server_identifier: Some(SERVER_IP),
1182 subnet_mask: Some(TEST_SUBNET_MASK),
1183 lease_length_secs: Some(LEASE_LENGTH_SECS),
1184 include_duplicate_option: false,
1185 } => Err(SelectingIncomingMessageError::CommonError(
1186 CommonIncomingMessageError::NotBootReply(dhcp_protocol::OpCode::BOOTREQUEST),
1187 )); "rejects offer that isn't a bootreply")]
1188 #[test_case(VaryingOfferFields {
1189 op: dhcp_protocol::OpCode::BOOTREPLY,
1190 yiaddr: YIADDR,
1191 message_type: Some(dhcp_protocol::MessageType::DHCPACK),
1192 server_identifier: Some(SERVER_IP),
1193 subnet_mask: Some(TEST_SUBNET_MASK),
1194 lease_length_secs: Some(LEASE_LENGTH_SECS),
1195 include_duplicate_option: false,
1196 } => Err(
1197 SelectingIncomingMessageError::NotDhcpOffer(dhcp_protocol::MessageType::DHCPACK),
1198 ); "rejects offer with wrong DHCP message type")]
1199 #[test_case(VaryingOfferFields {
1200 op: dhcp_protocol::OpCode::BOOTREPLY,
1201 yiaddr: YIADDR,
1202 message_type: None,
1203 server_identifier: Some(SERVER_IP),
1204 subnet_mask: Some(TEST_SUBNET_MASK),
1205 lease_length_secs: Some(LEASE_LENGTH_SECS),
1206 include_duplicate_option: false,
1207 } => Err(SelectingIncomingMessageError::CommonError(
1208 CommonIncomingMessageError::BuilderMissingField("message_type"),
1209 )); "rejects offer with no DHCP message type option")]
1210 #[test_case(VaryingOfferFields {
1211 op: dhcp_protocol::OpCode::BOOTREPLY,
1212 yiaddr: YIADDR,
1213 message_type: Some(dhcp_protocol::MessageType::DHCPOFFER),
1214 server_identifier: Some(SERVER_IP),
1215 subnet_mask: Some(TEST_SUBNET_MASK),
1216 lease_length_secs: Some(LEASE_LENGTH_SECS),
1217 include_duplicate_option: true,
1218 } => Ok(FieldsFromOfferToUseInRequest {
1219 server_identifier: net_types::ip::Ipv4Addr::from(SERVER_IP)
1220 .try_into()
1221 .expect("should be specified"),
1222 ip_address_lease_time_secs: Some(LEASE_LENGTH_SECS_NONZERO),
1223 ip_address_to_request: net_types::ip::Ipv4Addr::from(YIADDR)
1224 .try_into()
1225 .expect("should be specified"),
1226 }); "accepts good offer with duplicate options")]
1227 fn fields_from_offer_to_use_in_request(
1228 offer_fields: VaryingOfferFields,
1229 ) -> Result<FieldsFromOfferToUseInRequest, SelectingIncomingMessageError> {
1230 use super::fields_to_retain_from_selecting as fields;
1231 use dhcp_protocol::DhcpOption;
1232
1233 let VaryingOfferFields {
1234 op,
1235 yiaddr,
1236 message_type,
1237 server_identifier,
1238 subnet_mask,
1239 lease_length_secs,
1240 include_duplicate_option,
1241 } = offer_fields;
1242
1243 let message = dhcp_protocol::Message {
1244 op,
1245 xid: 1,
1246 secs: 0,
1247 bdcast_flag: false,
1248 ciaddr: Ipv4Addr::UNSPECIFIED,
1249 yiaddr,
1250 siaddr: Ipv4Addr::UNSPECIFIED,
1251 giaddr: Ipv4Addr::UNSPECIFIED,
1252 chaddr: net_mac!("01:02:03:04:05:06"),
1253 sname: String::new(),
1254 file: String::new(),
1255 options: message_type
1256 .map(DhcpOption::DhcpMessageType)
1257 .into_iter()
1258 .chain(server_identifier.map(DhcpOption::ServerIdentifier))
1259 .chain(subnet_mask.map(DhcpOption::SubnetMask))
1260 .chain(lease_length_secs.map(DhcpOption::IpAddressLeaseTime))
1261 .chain(
1262 include_duplicate_option
1263 .then_some([
1264 dhcp_protocol::DhcpOption::DomainName("example.com".to_owned()),
1265 dhcp_protocol::DhcpOption::DomainName("example.com".to_owned()),
1266 ])
1267 .into_iter()
1268 .flatten(),
1269 )
1270 .collect(),
1271 };
1272
1273 fields(
1274 &std::iter::once((dhcp_protocol::OptionCode::SubnetMask, OptionRequested::Required))
1275 .collect(),
1276 message,
1277 )
1278 }
1279
1280 struct VaryingReplyToRequestFields {
1281 op: dhcp_protocol::OpCode,
1282 yiaddr: Ipv4Addr,
1283 message_type: Option<dhcp_protocol::MessageType>,
1284 server_identifier: Option<Ipv4Addr>,
1285 subnet_mask: Option<PrefixLength<Ipv4>>,
1286 lease_length_secs: Option<u32>,
1287 renewal_time_secs: Option<u32>,
1288 rebinding_time_secs: Option<u32>,
1289 message: Option<String>,
1290 include_duplicate_option: bool,
1291 }
1292
1293 const DOMAIN_NAME: &str = "example.com";
1294 const MESSAGE: &str = "message explaining why the DHCPNAK was sent";
1295 const RENEWAL_TIME_SECS: u32 = LEASE_LENGTH_SECS / 2;
1296 const REBINDING_TIME_SECS: u32 = LEASE_LENGTH_SECS * 3 / 4;
1297
1298 #[test_case(
1299 VaryingReplyToRequestFields {
1300 op: dhcp_protocol::OpCode::BOOTREPLY,
1301 yiaddr: YIADDR,
1302 message_type: Some(dhcp_protocol::MessageType::DHCPACK),
1303 server_identifier: Some(SERVER_IP),
1304 subnet_mask: Some(TEST_SUBNET_MASK),
1305 lease_length_secs: Some(LEASE_LENGTH_SECS),
1306 renewal_time_secs: None,
1307 rebinding_time_secs: None,
1308 message: None,
1309 include_duplicate_option: false,
1310 } => Ok(IncomingResponseToRequest::Ack(FieldsToRetainFromAck {
1311 yiaddr: net_types::ip::Ipv4Addr::from(YIADDR)
1312 .try_into()
1313 .expect("should be specified"),
1314 server_identifier: Some(
1315 net_types::ip::Ipv4Addr::from(SERVER_IP)
1316 .try_into()
1317 .expect("should be specified"),
1318 ),
1319 ip_address_lease_time_secs: Some(LEASE_LENGTH_SECS_NONZERO),
1320 parameters: vec![
1321 dhcp_protocol::DhcpOption::SubnetMask(TEST_SUBNET_MASK),
1322 dhcp_protocol::DhcpOption::DomainName(DOMAIN_NAME.to_owned())
1323 ],
1324 renewal_time_value_secs: None,
1325 rebinding_time_value_secs: None,
1326 })); "accepts good DHCPACK")]
1327 #[test_case(VaryingReplyToRequestFields {
1328 op: dhcp_protocol::OpCode::BOOTREPLY,
1329 yiaddr: YIADDR,
1330 message_type: Some(dhcp_protocol::MessageType::DHCPACK),
1331 server_identifier: None,
1332 subnet_mask: Some(TEST_SUBNET_MASK),
1333 lease_length_secs: Some(LEASE_LENGTH_SECS),
1334 renewal_time_secs: None,
1335 rebinding_time_secs: None,
1336 message: None,
1337 include_duplicate_option: false,
1338 } => Ok(IncomingResponseToRequest::Ack(FieldsToRetainFromAck {
1339 yiaddr: net_types::ip::Ipv4Addr::from(YIADDR)
1340 .try_into()
1341 .expect("should be specified"),
1342 server_identifier: None,
1343 ip_address_lease_time_secs: Some(LEASE_LENGTH_SECS_NONZERO),
1344 parameters: vec![
1345 dhcp_protocol::DhcpOption::SubnetMask(TEST_SUBNET_MASK),
1346 dhcp_protocol::DhcpOption::DomainName(DOMAIN_NAME.to_owned())
1347 ],
1348 renewal_time_value_secs: None,
1349 rebinding_time_value_secs: None,
1350 })); "accepts DHCPACK with no server identifier")]
1351 #[test_case(VaryingReplyToRequestFields {
1352 op: dhcp_protocol::OpCode::BOOTREPLY,
1353 yiaddr: YIADDR,
1354 message_type: Some(dhcp_protocol::MessageType::DHCPACK),
1355 server_identifier: Some(SERVER_IP),
1356 subnet_mask: Some(TEST_SUBNET_MASK),
1357 lease_length_secs: Some(LEASE_LENGTH_SECS),
1358 renewal_time_secs: Some(RENEWAL_TIME_SECS),
1359 rebinding_time_secs: Some(REBINDING_TIME_SECS),
1360 message: None,
1361 include_duplicate_option: false,
1362 } => Ok(IncomingResponseToRequest::Ack(FieldsToRetainFromAck {
1363 yiaddr: net_types::ip::Ipv4Addr::from(YIADDR)
1364 .try_into()
1365 .expect("should be specified"),
1366 server_identifier: Some(
1367 net_types::ip::Ipv4Addr::from(SERVER_IP)
1368 .try_into()
1369 .expect("should be specified"),
1370 ),
1371 ip_address_lease_time_secs: Some(LEASE_LENGTH_SECS_NONZERO),
1372 parameters: vec![
1373 dhcp_protocol::DhcpOption::SubnetMask(TEST_SUBNET_MASK),
1374 dhcp_protocol::DhcpOption::DomainName(DOMAIN_NAME.to_owned())
1375 ],
1376 renewal_time_value_secs: Some(RENEWAL_TIME_SECS),
1377 rebinding_time_value_secs: Some(REBINDING_TIME_SECS),
1378 })); "accepts DHCPACK with renew and rebind times")]
1379 #[test_case(VaryingReplyToRequestFields {
1380 op: dhcp_protocol::OpCode::BOOTREPLY,
1381 yiaddr: Ipv4Addr::UNSPECIFIED,
1382 message_type: Some(dhcp_protocol::MessageType::DHCPNAK),
1383 server_identifier: Some(SERVER_IP),
1384 subnet_mask: None,
1385 lease_length_secs: None,
1386 renewal_time_secs: None,
1387 rebinding_time_secs: None,
1388 message: Some(MESSAGE.to_owned()),
1389 include_duplicate_option: false,
1390 } => Ok(IncomingResponseToRequest::Nak(FieldsToRetainFromNak {
1391 server_identifier: net_types::ip::Ipv4Addr::from(SERVER_IP)
1392 .try_into()
1393 .expect("should be specified"),
1394 message: Some(MESSAGE.to_owned()),
1395 client_identifier: None,
1396 })); "accepts good DHCPNAK")]
1397 #[test_case(VaryingReplyToRequestFields {
1398 op: dhcp_protocol::OpCode::BOOTREPLY,
1399 yiaddr: YIADDR,
1400 message_type: Some(dhcp_protocol::MessageType::DHCPACK),
1401 server_identifier: Some(SERVER_IP),
1402 subnet_mask: Some(TEST_SUBNET_MASK),
1403 lease_length_secs: None,
1404 renewal_time_secs: None,
1405 rebinding_time_secs: None,
1406 message: None,
1407 include_duplicate_option: false,
1408 } => Ok(IncomingResponseToRequest::Ack(FieldsToRetainFromAck {
1409 yiaddr: net_types::ip::Ipv4Addr::from(YIADDR).try_into().expect("should be specified"),
1410 server_identifier: Some(
1411 net_types::ip::Ipv4Addr::from(SERVER_IP).try_into().expect("should be specified")
1412 ),
1413 ip_address_lease_time_secs: None,
1414 parameters: vec![
1415 dhcp_protocol::DhcpOption::SubnetMask(TEST_SUBNET_MASK),
1416 dhcp_protocol::DhcpOption::DomainName(DOMAIN_NAME.to_owned())
1417 ],
1418 renewal_time_value_secs: None,
1419 rebinding_time_value_secs: None,
1420 })); "accepts DHCPACK with no lease time")]
1421 #[test_case(
1422 VaryingReplyToRequestFields {
1423 op: dhcp_protocol::OpCode::BOOTREPLY,
1424 yiaddr: YIADDR,
1425 message_type: Some(dhcp_protocol::MessageType::DHCPACK),
1426 server_identifier: Some(SERVER_IP),
1427 subnet_mask: None,
1428 lease_length_secs: Some(LEASE_LENGTH_SECS),
1429 renewal_time_secs: None,
1430 rebinding_time_secs: None,
1431 message: None,
1432 include_duplicate_option: false,
1433 } => Err(IncomingResponseToRequestError::MissingRequiredOption(
1434 dhcp_protocol::OptionCode::SubnetMask
1435 )); "rejects DHCPACK without required subnet mask")]
1436 #[test_case(VaryingReplyToRequestFields {
1437 op: dhcp_protocol::OpCode::BOOTREPLY,
1438 yiaddr: YIADDR,
1439 message_type: Some(dhcp_protocol::MessageType::DHCPACK),
1440 server_identifier: Some(Ipv4Addr::UNSPECIFIED),
1441 subnet_mask: Some(TEST_SUBNET_MASK),
1442 lease_length_secs: Some(LEASE_LENGTH_SECS),
1443 renewal_time_secs: Some(RENEWAL_TIME_SECS),
1444 rebinding_time_secs: Some(REBINDING_TIME_SECS),
1445 message: None,
1446 include_duplicate_option: false,
1447 } => Err(IncomingResponseToRequestError::CommonError(
1448 CommonIncomingMessageError::UnspecifiedServerIdentifier,
1449 )); "rejects DHCPACK with unspecified server identifier")]
1450 #[test_case(VaryingReplyToRequestFields {
1451 op: dhcp_protocol::OpCode::BOOTREPLY,
1452 yiaddr: Ipv4Addr::UNSPECIFIED,
1453 message_type: Some(dhcp_protocol::MessageType::DHCPACK),
1454 server_identifier: Some(SERVER_IP),
1455 subnet_mask: Some(TEST_SUBNET_MASK),
1456 lease_length_secs: Some(LEASE_LENGTH_SECS),
1457 renewal_time_secs: Some(RENEWAL_TIME_SECS),
1458 rebinding_time_secs: Some(REBINDING_TIME_SECS),
1459 message: None,
1460 include_duplicate_option: false,
1461 } => Err(IncomingResponseToRequestError::UnspecifiedYiaddr); "rejects DHCPACK with unspecified yiaddr")]
1462 #[test_case(VaryingReplyToRequestFields {
1463 op: dhcp_protocol::OpCode::BOOTREPLY,
1464 yiaddr: Ipv4Addr::UNSPECIFIED,
1465 message_type: Some(dhcp_protocol::MessageType::DHCPNAK),
1466 server_identifier: Some(Ipv4Addr::UNSPECIFIED),
1467 subnet_mask: None,
1468 lease_length_secs: None,
1469 renewal_time_secs: None,
1470 rebinding_time_secs: None,
1471 message: Some(MESSAGE.to_owned()),
1472 include_duplicate_option: false,
1473 } => Err(IncomingResponseToRequestError::CommonError(
1474 CommonIncomingMessageError::UnspecifiedServerIdentifier,
1475 )); "rejects DHCPNAK with unspecified server identifier")]
1476 #[test_case(VaryingReplyToRequestFields {
1477 op: dhcp_protocol::OpCode::BOOTREPLY,
1478 yiaddr: Ipv4Addr::UNSPECIFIED,
1479 message_type: Some(dhcp_protocol::MessageType::DHCPNAK),
1480 server_identifier: None,
1481 subnet_mask: None,
1482 lease_length_secs: None,
1483 renewal_time_secs: None,
1484 rebinding_time_secs: None,
1485 message: Some(MESSAGE.to_owned()),
1486 include_duplicate_option: false,
1487 } => Err(IncomingResponseToRequestError::NoServerIdentifier) ; "rejects DHCPNAK with no server identifier")]
1488 #[test_case(VaryingReplyToRequestFields {
1489 op: dhcp_protocol::OpCode::BOOTREQUEST,
1490 yiaddr: Ipv4Addr::UNSPECIFIED,
1491 message_type: Some(dhcp_protocol::MessageType::DHCPNAK),
1492 server_identifier: Some(SERVER_IP),
1493 subnet_mask: None,
1494 lease_length_secs: None,
1495 renewal_time_secs: None,
1496 rebinding_time_secs: None,
1497 message: Some(MESSAGE.to_owned()),
1498 include_duplicate_option: false,
1499 } => Err(IncomingResponseToRequestError::CommonError(
1500 CommonIncomingMessageError::NotBootReply(dhcp_protocol::OpCode::BOOTREQUEST),
1501 )) ; "rejects non-bootreply")]
1502 #[test_case(VaryingReplyToRequestFields {
1503 op: dhcp_protocol::OpCode::BOOTREPLY,
1504 yiaddr: Ipv4Addr::UNSPECIFIED,
1505 message_type: Some(dhcp_protocol::MessageType::DHCPOFFER),
1506 server_identifier: Some(SERVER_IP),
1507 subnet_mask: Some(TEST_SUBNET_MASK),
1508 lease_length_secs: None,
1509 renewal_time_secs: None,
1510 rebinding_time_secs: None,
1511 message: Some(MESSAGE.to_owned()),
1512 include_duplicate_option: false,
1513 } => Err(IncomingResponseToRequestError::NotDhcpAckOrNak(
1514 dhcp_protocol::MessageType::DHCPOFFER,
1515 )) ; "rejects non-DHCPACK or DHCPNAK")]
1516 #[test_case(VaryingReplyToRequestFields {
1517 op: dhcp_protocol::OpCode::BOOTREPLY,
1518 yiaddr: Ipv4Addr::UNSPECIFIED,
1519 message_type: None,
1520 server_identifier: Some(SERVER_IP),
1521 subnet_mask: None,
1522 lease_length_secs: None,
1523 renewal_time_secs: None,
1524 rebinding_time_secs: None,
1525 message: Some(MESSAGE.to_owned()),
1526 include_duplicate_option: false,
1527 } => Err(IncomingResponseToRequestError::CommonError(
1528 CommonIncomingMessageError::BuilderMissingField("message_type"),
1529 )) ; "rejects missing DHCP message type")]
1530 #[test_case( VaryingReplyToRequestFields {
1531 op: dhcp_protocol::OpCode::BOOTREPLY,
1532 yiaddr: YIADDR,
1533 message_type: Some(dhcp_protocol::MessageType::DHCPACK),
1534 server_identifier: Some(SERVER_IP),
1535 subnet_mask: Some(TEST_SUBNET_MASK),
1536 lease_length_secs: Some(LEASE_LENGTH_SECS),
1537 renewal_time_secs: Some(RENEWAL_TIME_SECS),
1538 rebinding_time_secs: Some(REBINDING_TIME_SECS),
1539 message: None,
1540 include_duplicate_option: true,
1541 } => Ok(IncomingResponseToRequest::Ack(FieldsToRetainFromAck {
1542 yiaddr: net_types::ip::Ipv4Addr::from(YIADDR)
1543 .try_into()
1544 .expect("should be specified"),
1545 server_identifier: Some(
1546 net_types::ip::Ipv4Addr::from(SERVER_IP)
1547 .try_into()
1548 .expect("should be specified"),
1549 ),
1550 ip_address_lease_time_secs: Some(LEASE_LENGTH_SECS_NONZERO),
1551 parameters: vec![
1552 dhcp_protocol::DhcpOption::SubnetMask(TEST_SUBNET_MASK),
1553 dhcp_protocol::DhcpOption::DomainName(DOMAIN_NAME.to_owned())
1554 ],
1555 renewal_time_value_secs: Some(RENEWAL_TIME_SECS),
1556 rebinding_time_value_secs: Some(REBINDING_TIME_SECS),
1557 })); "accepts good DHCPACK with duplicate option")]
1558 fn fields_to_retain_during_requesting(
1559 incoming_fields: VaryingReplyToRequestFields,
1560 ) -> Result<
1561 IncomingResponseToRequest<Option<net_types::SpecifiedAddr<net_types::ip::Ipv4Addr>>>,
1562 IncomingResponseToRequestError,
1563 > {
1564 use super::fields_to_retain_from_response_to_request as fields;
1565 use dhcp_protocol::DhcpOption;
1566
1567 let VaryingReplyToRequestFields {
1568 op,
1569 yiaddr,
1570 message_type,
1571 server_identifier,
1572 subnet_mask,
1573 lease_length_secs,
1574 renewal_time_secs,
1575 rebinding_time_secs,
1576 message,
1577 include_duplicate_option,
1578 } = incoming_fields;
1579
1580 let message = dhcp_protocol::Message {
1581 op,
1582 xid: 1,
1583 secs: 0,
1584 bdcast_flag: false,
1585 ciaddr: Ipv4Addr::UNSPECIFIED,
1586 yiaddr,
1587 siaddr: Ipv4Addr::UNSPECIFIED,
1588 giaddr: Ipv4Addr::UNSPECIFIED,
1589 chaddr: net_mac!("01:02:03:04:05:06"),
1590 sname: String::new(),
1591 file: String::new(),
1592 options: std::iter::empty()
1593 .chain(message_type.map(DhcpOption::DhcpMessageType))
1594 .chain(server_identifier.map(DhcpOption::ServerIdentifier))
1595 .chain(subnet_mask.map(DhcpOption::SubnetMask))
1596 .chain(lease_length_secs.map(DhcpOption::IpAddressLeaseTime))
1597 .chain(renewal_time_secs.map(DhcpOption::RenewalTimeValue))
1598 .chain(rebinding_time_secs.map(DhcpOption::RebindingTimeValue))
1599 .chain(message.map(DhcpOption::Message))
1600 .chain(std::iter::once(dhcp_protocol::DhcpOption::InterfaceMtu(1)))
1603 .chain(std::iter::once(dhcp_protocol::DhcpOption::DomainName(
1606 DOMAIN_NAME.to_owned(),
1607 )))
1608 .chain(
1609 include_duplicate_option
1610 .then_some(dhcp_protocol::DhcpOption::DomainName(DOMAIN_NAME.to_owned())),
1611 )
1612 .collect(),
1613 };
1614
1615 fields(
1616 &[
1617 (dhcp_protocol::OptionCode::SubnetMask, OptionRequested::Required),
1618 (dhcp_protocol::OptionCode::DomainName, OptionRequested::Optional),
1619 ]
1620 .into_iter()
1621 .collect(),
1622 message,
1623 )
1624 }
1625}