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