1use super::SpaceKind;
2use crate::{Instant, MAX_UDP_PAYLOAD, MtuDiscoveryConfig};
3use std::cmp;
4use tracing::trace;
5
6#[derive(Clone, Debug)]
10pub(crate) struct MtuDiscovery {
11 current_mtu: u16,
13 state: Option<EnabledMtuDiscovery>,
15 black_hole_detector: BlackHoleDetector,
17}
18
19impl MtuDiscovery {
20 pub(crate) fn new(
21 initial_plpmtu: u16,
22 min_mtu: u16,
23 peer_max_udp_payload_size: Option<u16>,
24 config: MtuDiscoveryConfig,
25 ) -> Self {
26 debug_assert!(
27 initial_plpmtu >= min_mtu,
28 "initial_max_udp_payload_size must be at least {min_mtu}"
29 );
30
31 let mut mtud = Self::with_state(
32 initial_plpmtu,
33 min_mtu,
34 Some(EnabledMtuDiscovery::new(config)),
35 );
36
37 if let Some(peer_max_udp_payload_size) = peer_max_udp_payload_size {
41 mtud.on_peer_max_udp_payload_size_received(peer_max_udp_payload_size);
42 }
43
44 mtud
45 }
46
47 pub(crate) fn disabled(plpmtu: u16, min_mtu: u16) -> Self {
49 Self::with_state(plpmtu, min_mtu, None)
50 }
51
52 fn with_state(current_mtu: u16, min_mtu: u16, state: Option<EnabledMtuDiscovery>) -> Self {
53 Self {
54 current_mtu,
55 state,
56 black_hole_detector: BlackHoleDetector::new(min_mtu),
57 }
58 }
59
60 pub(crate) fn current_mtu(&self) -> u16 {
62 self.current_mtu
63 }
64
65 pub(crate) fn poll_transmit(&mut self, now: Instant, next_pn: u64) -> Option<u16> {
70 self.state
71 .as_mut()
72 .and_then(|state| state.poll_transmit(now, self.current_mtu, next_pn))
73 }
74
75 pub(crate) fn on_peer_max_udp_payload_size_received(&mut self, peer_max_udp_payload_size: u16) {
78 self.current_mtu = self.current_mtu.min(peer_max_udp_payload_size);
79
80 if let Some(state) = self.state.as_mut() {
81 debug_assert!(
86 !matches!(state.phase, Phase::Searching(_)),
87 "Transport parameters received after MTU probing started"
88 );
89 state.peer_max_udp_payload_size = peer_max_udp_payload_size;
90 }
91 }
92
93 pub(crate) fn on_acked(&mut self, space: SpaceKind, pn: u64, len: u16) -> bool {
97 if space != SpaceKind::Data {
99 return false;
100 }
101
102 if let Some(new_mtu) = self
104 .state
105 .as_mut()
106 .and_then(|state| state.on_probe_acked(pn))
107 {
108 self.current_mtu = new_mtu;
109 trace!(current_mtu = self.current_mtu, "new MTU detected");
110
111 self.black_hole_detector.on_probe_acked(pn, len);
112 true
113 } else {
114 self.black_hole_detector.on_non_probe_acked(pn, len);
115 false
116 }
117 }
118
119 pub(crate) fn in_flight_mtu_probe(&self) -> Option<u64> {
121 match &self.state {
122 Some(EnabledMtuDiscovery {
123 phase: Phase::Searching(search_state),
124 ..
125 }) => search_state.in_flight_probe,
126 _ => None,
127 }
128 }
129
130 pub(crate) fn on_probe_lost(&mut self) {
132 if let Some(state) = &mut self.state {
133 state.on_probe_lost();
134 }
135 }
136
137 pub(crate) fn on_non_probe_lost(&mut self, pn: u64, len: u16) {
143 self.black_hole_detector.on_non_probe_lost(pn, len);
144 }
145
146 pub(crate) fn black_hole_detected(&mut self, now: Instant) -> bool {
151 if !self.black_hole_detector.black_hole_detected() {
152 return false;
153 }
154
155 self.current_mtu = self.black_hole_detector.min_mtu;
156
157 if let Some(state) = &mut self.state {
158 state.on_black_hole_detected(now);
159 }
160
161 true
162 }
163}
164
165#[derive(Debug, Clone)]
167struct EnabledMtuDiscovery {
168 phase: Phase,
169 peer_max_udp_payload_size: u16,
170 config: MtuDiscoveryConfig,
171}
172
173impl EnabledMtuDiscovery {
174 fn new(config: MtuDiscoveryConfig) -> Self {
175 Self {
176 phase: Phase::Initial,
177 peer_max_udp_payload_size: MAX_UDP_PAYLOAD,
178 config,
179 }
180 }
181
182 fn poll_transmit(&mut self, now: Instant, current_mtu: u16, next_pn: u64) -> Option<u16> {
191 if let Phase::Initial = &self.phase {
192 self.phase = Phase::Searching(SearchState::new(
194 current_mtu,
195 self.peer_max_udp_payload_size,
196 &self.config,
197 ));
198 } else if let Phase::Complete(next_mtud_activation) = &self.phase {
199 if now < *next_mtud_activation {
200 return None;
201 }
202
203 self.phase = Phase::Searching(SearchState::new(
205 current_mtu,
206 self.peer_max_udp_payload_size,
207 &self.config,
208 ));
209 }
210
211 if let Phase::Searching(state) = &mut self.phase {
212 if state.in_flight_probe.is_some() {
214 return None;
215 }
216
217 if 0 < state.lost_probe_count && state.lost_probe_count < MAX_PROBE_RETRANSMITS {
219 state.in_flight_probe = Some(next_pn);
220 return Some(state.last_probed_mtu);
221 }
222
223 let last_probe_succeeded = state.lost_probe_count == 0;
224
225 if !last_probe_succeeded {
227 state.lost_probe_count = 0;
228 state.in_flight_probe = None;
229 }
230
231 if let Some(probe_udp_payload_size) = state.next_mtu_to_probe(last_probe_succeeded) {
232 state.in_flight_probe = Some(next_pn);
233 state.last_probed_mtu = probe_udp_payload_size;
234 return Some(probe_udp_payload_size);
235 } else {
236 let next_mtud_activation = now + self.config.interval;
237 self.phase = Phase::Complete(next_mtud_activation);
238 return None;
239 }
240 }
241
242 None
243 }
244
245 fn on_probe_acked(&mut self, pn: u64) -> Option<u16> {
251 match &mut self.phase {
252 Phase::Searching(state) if state.in_flight_probe == Some(pn) => {
253 state.in_flight_probe = None;
254 state.lost_probe_count = 0;
255 Some(state.last_probed_mtu)
256 }
257 _ => None,
258 }
259 }
260
261 fn on_probe_lost(&mut self) {
263 if let Phase::Searching(state) = &mut self.phase {
265 state.in_flight_probe = None;
266 state.lost_probe_count += 1;
267 }
268 }
269
270 fn on_black_hole_detected(&mut self, now: Instant) {
272 let next_mtud_activation = now + self.config.black_hole_cooldown;
274 self.phase = Phase::Complete(next_mtud_activation);
275 }
276}
277
278#[derive(Debug, Clone, Copy)]
279enum Phase {
280 Initial,
282 Searching(SearchState),
284 Complete(Instant),
286}
287
288#[derive(Debug, Clone, Copy)]
289struct SearchState {
290 lower_bound: u16,
292 upper_bound: u16,
294 minimum_change: u16,
296 last_probed_mtu: u16,
298 in_flight_probe: Option<u64>,
300 lost_probe_count: usize,
302}
303
304impl SearchState {
305 fn new(
308 mut lower_bound: u16,
309 peer_max_udp_payload_size: u16,
310 config: &MtuDiscoveryConfig,
311 ) -> Self {
312 lower_bound = lower_bound.min(peer_max_udp_payload_size);
313 let upper_bound = config
314 .upper_bound
315 .clamp(lower_bound, peer_max_udp_payload_size);
316
317 Self {
318 in_flight_probe: None,
319 lost_probe_count: 0,
320 lower_bound,
321 upper_bound,
322 minimum_change: config.minimum_change,
323 last_probed_mtu: lower_bound,
326 }
327 }
328
329 fn next_mtu_to_probe(&mut self, last_probe_succeeded: bool) -> Option<u16> {
331 debug_assert_eq!(self.in_flight_probe, None);
332
333 if last_probe_succeeded {
334 self.lower_bound = self.last_probed_mtu;
335 } else {
336 self.upper_bound = self.last_probed_mtu - 1;
337 }
338
339 let next_mtu = (self.lower_bound as i32 + self.upper_bound as i32) / 2;
340
341 if ((next_mtu - self.last_probed_mtu as i32).unsigned_abs() as u16) < self.minimum_change {
343 if self.upper_bound.saturating_sub(self.last_probed_mtu) >= self.minimum_change {
346 return Some(self.upper_bound);
347 }
348
349 return None;
350 }
351
352 Some(next_mtu as u16)
353 }
354}
355
356#[derive(Clone, Debug)]
371struct BlackHoleDetector {
372 suspicious_loss_bursts: Vec<LossBurst>,
374 current_loss_burst: Option<CurrentLossBurst>,
376 largest_post_loss_packet: u64,
379 acked_mtu: u16,
382 min_mtu: u16,
384}
385
386impl BlackHoleDetector {
387 fn new(min_mtu: u16) -> Self {
388 Self {
389 suspicious_loss_bursts: Vec::with_capacity(BLACK_HOLE_THRESHOLD + 1),
390 current_loss_burst: None,
391 largest_post_loss_packet: 0,
392 acked_mtu: min_mtu,
393 min_mtu,
394 }
395 }
396
397 fn on_probe_acked(&mut self, pn: u64, len: u16) {
398 self.suspicious_loss_bursts.clear();
402 self.acked_mtu = len;
403 self.largest_post_loss_packet = pn;
407 }
408
409 fn on_non_probe_acked(&mut self, pn: u64, len: u16) {
410 if len <= self.acked_mtu {
411 return;
414 }
415 self.acked_mtu = len;
416 self.largest_post_loss_packet = pn;
418 self.suspicious_loss_bursts
420 .retain(|burst| burst.smallest_packet_size > len);
421 }
422
423 fn on_non_probe_lost(&mut self, pn: u64, len: u16) {
424 let end_last_burst = self
427 .current_loss_burst
428 .as_ref()
429 .is_some_and(|current| pn - current.latest_non_probe != 1);
430
431 if end_last_burst {
432 self.finish_loss_burst();
433 }
434
435 self.current_loss_burst = Some(CurrentLossBurst {
436 latest_non_probe: pn,
437 smallest_packet_size: self
438 .current_loss_burst
439 .map_or(len, |prev| cmp::min(prev.smallest_packet_size, len)),
440 });
441 }
442
443 fn black_hole_detected(&mut self) -> bool {
444 self.finish_loss_burst();
445
446 if self.suspicious_loss_bursts.len() <= BLACK_HOLE_THRESHOLD {
447 return false;
448 }
449
450 self.suspicious_loss_bursts.clear();
451
452 true
453 }
454
455 fn finish_loss_burst(&mut self) {
457 let Some(burst) = self.current_loss_burst.take() else {
458 return;
459 };
460 if burst.smallest_packet_size <= self.min_mtu
463 || (burst.latest_non_probe < self.largest_post_loss_packet
464 && burst.smallest_packet_size <= self.acked_mtu)
465 {
466 return;
467 }
468 if burst.latest_non_probe > self.largest_post_loss_packet {
476 self.acked_mtu = self.min_mtu;
477 }
478
479 let burst = LossBurst {
480 smallest_packet_size: burst.smallest_packet_size,
481 };
482
483 if self.suspicious_loss_bursts.len() <= BLACK_HOLE_THRESHOLD {
484 self.suspicious_loss_bursts.push(burst);
485 return;
486 }
487
488 let smallest = self
490 .suspicious_loss_bursts
491 .iter_mut()
492 .min_by_key(|prev| prev.smallest_packet_size)
493 .filter(|prev| prev.smallest_packet_size < burst.smallest_packet_size);
494 if let Some(smallest) = smallest {
495 *smallest = burst;
496 }
497 }
498
499 #[cfg(test)]
500 fn suspicious_loss_burst_count(&self) -> usize {
501 self.suspicious_loss_bursts.len()
502 }
503
504 #[cfg(test)]
505 fn largest_non_probe_lost(&self) -> Option<u64> {
506 self.current_loss_burst.as_ref().map(|x| x.latest_non_probe)
507 }
508}
509
510#[derive(Copy, Clone, Debug)]
511struct LossBurst {
512 smallest_packet_size: u16,
513}
514
515#[derive(Copy, Clone, Debug)]
516struct CurrentLossBurst {
517 smallest_packet_size: u16,
518 latest_non_probe: u64,
519}
520
521const MAX_PROBE_RETRANSMITS: usize = 3;
524const BLACK_HOLE_THRESHOLD: usize = 3;
526
527#[cfg(test)]
528mod tests {
529 use super::*;
530 use crate::Duration;
531 use crate::MAX_UDP_PAYLOAD;
532 use assert_matches::assert_matches;
533
534 fn default_mtud() -> MtuDiscovery {
535 let config = MtuDiscoveryConfig::default();
536 MtuDiscovery::new(1_200, 1_200, None, config)
537 }
538
539 fn completed(mtud: &MtuDiscovery) -> bool {
540 matches!(mtud.state.as_ref().unwrap().phase, Phase::Complete(_))
541 }
542
543 fn drive_to_completion(
545 mtud: &mut MtuDiscovery,
546 now: Instant,
547 link_payload_size_limit: u16,
548 ) -> Vec<u16> {
549 let mut probed_sizes = Vec::new();
550 for probe_pn in 1..100 {
551 let result = mtud.poll_transmit(now, probe_pn);
552
553 if completed(mtud) {
554 break;
555 }
556
557 assert!(result.is_some());
559 let probe_size = result.unwrap();
560 probed_sizes.push(probe_size);
561
562 if probe_size <= link_payload_size_limit {
563 mtud.on_acked(SpaceKind::Data, probe_pn, probe_size);
564 } else {
565 mtud.on_probe_lost();
566 }
567 }
568 probed_sizes
569 }
570
571 #[test]
572 fn black_hole_detector_ignores_burst_containing_non_suspicious_packet() {
573 let mut mtud = default_mtud();
574 mtud.on_non_probe_lost(2, 1300);
575 mtud.on_non_probe_lost(3, 1300);
576 assert_eq!(mtud.black_hole_detector.largest_non_probe_lost(), Some(3));
577 assert_eq!(mtud.black_hole_detector.suspicious_loss_burst_count(), 0);
578
579 mtud.on_non_probe_lost(4, 800);
580 assert!(!mtud.black_hole_detected(Instant::now()));
581 assert_eq!(mtud.black_hole_detector.largest_non_probe_lost(), None);
582 assert_eq!(mtud.black_hole_detector.suspicious_loss_burst_count(), 0);
583 }
584
585 #[test]
586 fn black_hole_detector_counts_burst_containing_only_suspicious_packets() {
587 let mut mtud = default_mtud();
588 mtud.on_non_probe_lost(2, 1300);
589 mtud.on_non_probe_lost(3, 1300);
590 assert_eq!(mtud.black_hole_detector.largest_non_probe_lost(), Some(3));
591 assert_eq!(mtud.black_hole_detector.suspicious_loss_burst_count(), 0);
592
593 assert!(!mtud.black_hole_detected(Instant::now()));
594 assert_eq!(mtud.black_hole_detector.largest_non_probe_lost(), None);
595 assert_eq!(mtud.black_hole_detector.suspicious_loss_burst_count(), 1);
596 }
597
598 #[test]
599 fn black_hole_detector_ignores_empty_burst() {
600 let mut mtud = default_mtud();
601 assert!(!mtud.black_hole_detected(Instant::now()));
602 assert_eq!(mtud.black_hole_detector.suspicious_loss_burst_count(), 0);
603 }
604
605 #[test]
606 fn mtu_discovery_disabled_does_nothing() {
607 let mut mtud = MtuDiscovery::disabled(1_200, 1_200);
608 let probe_size = mtud.poll_transmit(Instant::now(), 0);
609 assert_eq!(probe_size, None);
610 }
611
612 #[test]
613 fn mtu_discovery_disabled_lost_four_packet_bursts_triggers_black_hole_detection() {
614 let mut mtud = MtuDiscovery::disabled(1_400, 1_250);
615 let now = Instant::now();
616
617 for i in 0..4 {
618 mtud.on_non_probe_lost(i * 2, 1300);
620 }
621
622 assert!(mtud.black_hole_detected(now));
623 assert_eq!(mtud.current_mtu, 1250);
624 assert_matches!(mtud.state, None);
625 }
626
627 #[test]
628 fn mtu_discovery_lost_two_packet_bursts_does_not_trigger_black_hole_detection() {
629 let mut mtud = default_mtud();
630 let now = Instant::now();
631
632 for i in 0..2 {
633 mtud.on_non_probe_lost(i, 1300);
634 assert!(!mtud.black_hole_detected(now));
635 }
636 }
637
638 #[test]
639 fn mtu_discovery_lost_four_packet_bursts_triggers_black_hole_detection_and_resets_timer() {
640 let mut mtud = default_mtud();
641 let now = Instant::now();
642
643 for i in 0..4 {
644 mtud.on_non_probe_lost(i * 2, 1300);
646 }
647
648 assert!(mtud.black_hole_detected(now));
649 assert_eq!(mtud.current_mtu, 1200);
650 if let Phase::Complete(next_mtud_activation) = mtud.state.unwrap().phase {
651 assert_eq!(next_mtud_activation, now + Duration::from_secs(60));
652 } else {
653 panic!("Unexpected MTUD phase!");
654 }
655 }
656
657 #[test]
658 fn mtu_discovery_after_complete_reactivates_when_interval_elapsed() {
659 let mut config = MtuDiscoveryConfig::default();
660 config.upper_bound(9_000);
661 let mut mtud = MtuDiscovery::new(1_200, 1_200, None, config);
662 let now = Instant::now();
663 drive_to_completion(&mut mtud, now, 1_500);
664
665 assert_eq!(mtud.poll_transmit(now, 42), None);
667 assert!(completed(&mtud));
668 assert_eq!(mtud.current_mtu, 1_471);
669
670 assert_eq!(
672 mtud.poll_transmit(now + Duration::from_secs(600), 43),
673 Some(5235)
674 );
675
676 match mtud.state.unwrap().phase {
677 Phase::Searching(state) => {
678 assert_eq!(state.lower_bound, 1_471);
679 assert_eq!(state.upper_bound, 9_000);
680 }
681 _ => {
682 panic!("Unexpected MTUD phase!")
683 }
684 }
685 }
686
687 #[test]
688 fn mtu_discovery_lost_three_probes_lowers_probe_size() {
689 let mut mtud = default_mtud();
690
691 let mut probe_sizes = (0..4).map(|i| {
692 let probe_size = mtud.poll_transmit(Instant::now(), i);
693 assert!(probe_size.is_some(), "no probe returned for packet {i}");
694
695 mtud.on_probe_lost();
696 probe_size.unwrap()
697 });
698
699 let first_probe_size = probe_sizes.next().unwrap();
701 for _ in 0..2 {
702 assert_eq!(probe_sizes.next().unwrap(), first_probe_size)
703 }
704
705 let fourth_probe_size = probe_sizes.next().unwrap();
707 assert!(fourth_probe_size < first_probe_size);
708 assert_eq!(
709 fourth_probe_size,
710 first_probe_size - (first_probe_size - 1_200) / 2 - 1
711 );
712 }
713
714 #[test]
715 fn mtu_discovery_with_peer_max_udp_payload_size_clamps_upper_bound() {
716 let mut mtud = default_mtud();
717
718 mtud.on_peer_max_udp_payload_size_received(1300);
719 let probed_sizes = drive_to_completion(&mut mtud, Instant::now(), 1500);
720
721 assert_eq!(mtud.state.as_ref().unwrap().peer_max_udp_payload_size, 1300);
722 assert_eq!(mtud.current_mtu, 1300);
723 let expected_probed_sizes = &[1250, 1275, 1300];
724 assert_eq!(probed_sizes, expected_probed_sizes);
725 assert!(completed(&mtud));
726 }
727
728 #[test]
729 fn mtu_discovery_with_previous_peer_max_udp_payload_size_clamps_upper_bound() {
730 let mut mtud = MtuDiscovery::new(1500, 1_200, Some(1400), MtuDiscoveryConfig::default());
731
732 assert_eq!(mtud.current_mtu, 1400);
733 assert_eq!(mtud.state.as_ref().unwrap().peer_max_udp_payload_size, 1400);
734
735 let probed_sizes = drive_to_completion(&mut mtud, Instant::now(), 1500);
736
737 assert_eq!(mtud.current_mtu, 1400);
738 assert!(probed_sizes.is_empty());
739 assert!(completed(&mtud));
740 }
741
742 #[cfg(debug_assertions)]
743 #[test]
744 #[should_panic(expected = "Transport parameters received after MTU probing started")]
745 fn mtu_discovery_with_peer_max_udp_payload_size_during_search_panics() {
746 let mut mtud = default_mtud();
747 assert!(mtud.poll_transmit(Instant::now(), 0).is_some());
748 assert!(matches!(
749 mtud.state.as_ref().unwrap().phase,
750 Phase::Searching(_)
751 ));
752 mtud.on_peer_max_udp_payload_size_received(1300);
753 }
754
755 #[test]
756 fn mtu_discovery_with_1500_limit() {
757 let mut mtud = default_mtud();
758
759 let probed_sizes = drive_to_completion(&mut mtud, Instant::now(), 1500);
760
761 let expected_probed_sizes = &[1326, 1389, 1420, 1452];
762 assert_eq!(probed_sizes, expected_probed_sizes);
763 assert_eq!(mtud.current_mtu, 1452);
764 assert!(completed(&mtud));
765 }
766
767 #[test]
768 fn mtu_discovery_with_1500_limit_and_10000_upper_bound() {
769 let mut config = MtuDiscoveryConfig::default();
770 config.upper_bound(10_000);
771 let mut mtud = MtuDiscovery::new(1_200, 1_200, None, config);
772
773 let probed_sizes = drive_to_completion(&mut mtud, Instant::now(), 1500);
774
775 let expected_probed_sizes = &[
776 5600, 5600, 5600, 3399, 3399, 3399, 2299, 2299, 2299, 1749, 1749, 1749, 1474, 1611,
777 1611, 1611, 1542, 1542, 1542, 1507, 1507, 1507,
778 ];
779 assert_eq!(probed_sizes, expected_probed_sizes);
780 assert_eq!(mtud.current_mtu, 1474);
781 assert!(completed(&mtud));
782 }
783
784 #[test]
785 fn mtu_discovery_no_lost_probes_finds_maximum_udp_payload() {
786 let mut config = MtuDiscoveryConfig::default();
787 config.upper_bound(MAX_UDP_PAYLOAD);
788 let mut mtud = MtuDiscovery::new(1200, 1200, None, config);
789
790 drive_to_completion(&mut mtud, Instant::now(), u16::MAX);
791
792 assert_eq!(mtud.current_mtu, 65527);
793 assert!(completed(&mtud));
794 }
795
796 #[test]
797 fn mtu_discovery_lost_half_of_probes_finds_maximum_udp_payload() {
798 let mut config = MtuDiscoveryConfig::default();
799 config.upper_bound(MAX_UDP_PAYLOAD);
800 let mut mtud = MtuDiscovery::new(1200, 1200, None, config);
801
802 let now = Instant::now();
803 let mut iterations = 0;
804 for i in 1..100 {
805 iterations += 1;
806
807 let probe_pn = i * 2 - 1;
808 let other_pn = i * 2;
809
810 let result = mtud.poll_transmit(Instant::now(), probe_pn);
811
812 if completed(&mtud) {
813 break;
814 }
815
816 assert!(result.is_some());
818 assert!(mtud.in_flight_mtu_probe().is_some());
819
820 assert_matches!(mtud.poll_transmit(now, other_pn), None);
822
823 if i % 2 == 0 {
824 let previous_max_size = mtud.current_mtu;
826 mtud.on_acked(SpaceKind::Data, probe_pn, result.unwrap());
827 println!(
828 "ACK packet {}. Previous MTU = {previous_max_size}. New MTU = {}",
829 result.unwrap(),
830 mtud.current_mtu
831 );
832 } else {
834 mtud.on_probe_lost();
835 }
836 }
837
838 assert_eq!(iterations, 25);
839 assert_eq!(mtud.current_mtu, 65527);
840 assert!(completed(&mtud));
841 }
842
843 #[test]
844 fn search_state_lower_bound_higher_than_upper_bound_clamps_upper_bound() {
845 let mut config = MtuDiscoveryConfig::default();
846 config.upper_bound(1400);
847
848 let state = SearchState::new(1500, u16::MAX, &config);
849 assert_eq!(state.lower_bound, 1500);
850 assert_eq!(state.upper_bound, 1500);
851 }
852
853 #[test]
854 fn search_state_lower_bound_higher_than_peer_max_udp_payload_size_clamps_lower_bound() {
855 let mut config = MtuDiscoveryConfig::default();
856 config.upper_bound(9000);
857
858 let state = SearchState::new(1500, 1300, &config);
859 assert_eq!(state.lower_bound, 1300);
860 assert_eq!(state.upper_bound, 1300);
861 }
862
863 #[test]
864 fn search_state_upper_bound_higher_than_peer_max_udp_payload_size_clamps_upper_bound() {
865 let mut config = MtuDiscoveryConfig::default();
866 config.upper_bound(9000);
867
868 let state = SearchState::new(1200, 1450, &config);
869 assert_eq!(state.lower_bound, 1200);
870 assert_eq!(state.upper_bound, 1450);
871 }
872
873 #[test]
875 fn simple_black_hole_detection() {
876 let mut bhd = BlackHoleDetector::new(1200);
877 bhd.on_non_probe_acked((BLACK_HOLE_THRESHOLD + 1) as u64 * 2, 1300);
878 for i in 0..BLACK_HOLE_THRESHOLD {
879 bhd.on_non_probe_lost(i as u64 * 2, 1400);
880 }
881 assert!(!bhd.black_hole_detected());
883 bhd.on_non_probe_lost(BLACK_HOLE_THRESHOLD as u64 * 2, 1400);
884 assert!(bhd.black_hole_detected());
885 }
886
887 #[test]
890 fn non_suspicious_bursts() {
891 let mut bhd = BlackHoleDetector::new(1200);
892 bhd.on_non_probe_acked((BLACK_HOLE_THRESHOLD + 1) as u64 * 2, 1500);
893 for i in 0..(BLACK_HOLE_THRESHOLD + 1) {
894 bhd.on_non_probe_lost(i as u64 * 2, 1400);
895 }
896 assert!(!bhd.black_hole_detected());
897 }
898
899 #[test]
902 fn dynamic_mtu_reduction() {
903 let mut bhd = BlackHoleDetector::new(1200);
904 bhd.on_non_probe_acked(0, 1500);
905 for i in 0..(BLACK_HOLE_THRESHOLD + 1) {
906 bhd.on_non_probe_lost(i as u64 * 2, 1400);
907 }
908 assert!(bhd.black_hole_detected());
909 }
910
911 #[test]
913 fn mixed_non_suspicious_bursts() {
914 let mut bhd = BlackHoleDetector::new(1200);
915 bhd.on_non_probe_acked((BLACK_HOLE_THRESHOLD + 1) as u64 * 3, 1400);
916 for i in 0..(BLACK_HOLE_THRESHOLD + 1) {
917 bhd.on_non_probe_lost(i as u64 * 3, 1500);
918 bhd.on_non_probe_lost(i as u64 * 3 + 1, 1300);
919 }
920 assert!(!bhd.black_hole_detected());
921 }
922
923 #[test]
925 fn bursts_count_once() {
926 let mut bhd = BlackHoleDetector::new(1200);
927 bhd.on_non_probe_acked((BLACK_HOLE_THRESHOLD + 1) as u64 * 3, 1400);
928 for i in 0..(BLACK_HOLE_THRESHOLD) {
929 bhd.on_non_probe_lost(i as u64 * 3, 1500);
930 bhd.on_non_probe_lost(i as u64 * 3 + 1, 1500);
931 }
932 assert!(!bhd.black_hole_detected());
933 bhd.on_non_probe_lost(BLACK_HOLE_THRESHOLD as u64 * 3, 1500);
934 assert!(bhd.black_hole_detected());
935 }
936
937 #[test]
939 fn interleaved_bursts() {
940 let mut bhd = BlackHoleDetector::new(1200);
941 bhd.on_non_probe_acked((BLACK_HOLE_THRESHOLD + 1) as u64 * 4, 1400);
942 for i in 0..(BLACK_HOLE_THRESHOLD + 1) {
943 bhd.on_non_probe_lost(i as u64 * 4, 1500);
944 bhd.on_non_probe_lost(i as u64 * 4 + 2, 1300);
945 }
946 assert!(bhd.black_hole_detected());
947 }
948
949 #[test]
951 fn suspicious_after_acked() {
952 let mut bhd = BlackHoleDetector::new(1200);
953 bhd.on_non_probe_acked((BLACK_HOLE_THRESHOLD + 1) as u64 * 2, 1400);
954 for i in 0..(BLACK_HOLE_THRESHOLD + 1) {
955 bhd.on_non_probe_lost(i as u64 * 2, 1300);
956 }
957 assert!(
958 !bhd.black_hole_detected(),
959 "1300 byte losses preceding a 1400 byte delivery are not suspicious"
960 );
961 for i in 0..(BLACK_HOLE_THRESHOLD + 1) {
962 bhd.on_non_probe_lost((BLACK_HOLE_THRESHOLD as u64 + 1 + i as u64) * 2, 1300);
963 }
964 assert!(
965 bhd.black_hole_detected(),
966 "1300 byte losses following a 1400 byte delivery are suspicious"
967 );
968 }
969
970 #[test]
973 fn retroactively_non_suspicious() {
974 let mut bhd = BlackHoleDetector::new(1200);
975 for i in 0..BLACK_HOLE_THRESHOLD {
976 bhd.on_non_probe_lost(i as u64 * 2, 1400);
977 }
978 bhd.on_non_probe_acked(BLACK_HOLE_THRESHOLD as u64 * 2, 1400);
979 bhd.on_non_probe_lost(BLACK_HOLE_THRESHOLD as u64 * 2 + 1, 1400);
980 assert!(!bhd.black_hole_detected());
981 }
982}