use super::SpaceKind;
use crate::{Instant, MAX_UDP_PAYLOAD, MtuDiscoveryConfig};
use std::cmp;
use tracing::trace;
#[derive(Clone, Debug)]
pub(crate) struct MtuDiscovery {
current_mtu: u16,
state: Option<EnabledMtuDiscovery>,
black_hole_detector: BlackHoleDetector,
}
impl MtuDiscovery {
pub(crate) fn new(
initial_plpmtu: u16,
min_mtu: u16,
peer_max_udp_payload_size: Option<u16>,
config: MtuDiscoveryConfig,
) -> Self {
debug_assert!(
initial_plpmtu >= min_mtu,
"initial_max_udp_payload_size must be at least {min_mtu}"
);
let mut mtud = Self::with_state(
initial_plpmtu,
min_mtu,
Some(EnabledMtuDiscovery::new(config)),
);
if let Some(peer_max_udp_payload_size) = peer_max_udp_payload_size {
mtud.on_peer_max_udp_payload_size_received(peer_max_udp_payload_size);
}
mtud
}
pub(crate) fn disabled(plpmtu: u16, min_mtu: u16) -> Self {
Self::with_state(plpmtu, min_mtu, None)
}
fn with_state(current_mtu: u16, min_mtu: u16, state: Option<EnabledMtuDiscovery>) -> Self {
Self {
current_mtu,
state,
black_hole_detector: BlackHoleDetector::new(min_mtu),
}
}
pub(crate) fn current_mtu(&self) -> u16 {
self.current_mtu
}
pub(crate) fn poll_transmit(&mut self, now: Instant, next_pn: u64) -> Option<u16> {
self.state
.as_mut()
.and_then(|state| state.poll_transmit(now, self.current_mtu, next_pn))
}
pub(crate) fn on_peer_max_udp_payload_size_received(&mut self, peer_max_udp_payload_size: u16) {
self.current_mtu = self.current_mtu.min(peer_max_udp_payload_size);
if let Some(state) = self.state.as_mut() {
debug_assert!(
!matches!(state.phase, Phase::Searching(_)),
"Transport parameters received after MTU probing started"
);
state.peer_max_udp_payload_size = peer_max_udp_payload_size;
}
}
pub(crate) fn on_acked(&mut self, space: SpaceKind, pn: u64, len: u16) -> bool {
if space != SpaceKind::Data {
return false;
}
if let Some(new_mtu) = self
.state
.as_mut()
.and_then(|state| state.on_probe_acked(pn))
{
self.current_mtu = new_mtu;
trace!(current_mtu = self.current_mtu, "new MTU detected");
self.black_hole_detector.on_probe_acked(pn, len);
true
} else {
self.black_hole_detector.on_non_probe_acked(pn, len);
false
}
}
pub(crate) fn in_flight_mtu_probe(&self) -> Option<u64> {
match &self.state {
Some(EnabledMtuDiscovery {
phase: Phase::Searching(search_state),
..
}) => search_state.in_flight_probe,
_ => None,
}
}
pub(crate) fn on_probe_lost(&mut self) {
if let Some(state) = &mut self.state {
state.on_probe_lost();
}
}
pub(crate) fn on_non_probe_lost(&mut self, pn: u64, len: u16) {
self.black_hole_detector.on_non_probe_lost(pn, len);
}
pub(crate) fn black_hole_detected(&mut self, now: Instant) -> bool {
if !self.black_hole_detector.black_hole_detected() {
return false;
}
self.current_mtu = self.black_hole_detector.min_mtu;
if let Some(state) = &mut self.state {
state.on_black_hole_detected(now);
}
true
}
}
#[derive(Debug, Clone)]
struct EnabledMtuDiscovery {
phase: Phase,
peer_max_udp_payload_size: u16,
config: MtuDiscoveryConfig,
}
impl EnabledMtuDiscovery {
fn new(config: MtuDiscoveryConfig) -> Self {
Self {
phase: Phase::Initial,
peer_max_udp_payload_size: MAX_UDP_PAYLOAD,
config,
}
}
fn poll_transmit(&mut self, now: Instant, current_mtu: u16, next_pn: u64) -> Option<u16> {
if let Phase::Initial = &self.phase {
self.phase = Phase::Searching(SearchState::new(
current_mtu,
self.peer_max_udp_payload_size,
&self.config,
));
} else if let Phase::Complete(next_mtud_activation) = &self.phase {
if now < *next_mtud_activation {
return None;
}
self.phase = Phase::Searching(SearchState::new(
current_mtu,
self.peer_max_udp_payload_size,
&self.config,
));
}
if let Phase::Searching(state) = &mut self.phase {
if state.in_flight_probe.is_some() {
return None;
}
if 0 < state.lost_probe_count && state.lost_probe_count < MAX_PROBE_RETRANSMITS {
state.in_flight_probe = Some(next_pn);
return Some(state.last_probed_mtu);
}
let last_probe_succeeded = state.lost_probe_count == 0;
if !last_probe_succeeded {
state.lost_probe_count = 0;
state.in_flight_probe = None;
}
if let Some(probe_udp_payload_size) = state.next_mtu_to_probe(last_probe_succeeded) {
state.in_flight_probe = Some(next_pn);
state.last_probed_mtu = probe_udp_payload_size;
return Some(probe_udp_payload_size);
} else {
let next_mtud_activation = now + self.config.interval;
self.phase = Phase::Complete(next_mtud_activation);
return None;
}
}
None
}
fn on_probe_acked(&mut self, pn: u64) -> Option<u16> {
match &mut self.phase {
Phase::Searching(state) if state.in_flight_probe == Some(pn) => {
state.in_flight_probe = None;
state.lost_probe_count = 0;
Some(state.last_probed_mtu)
}
_ => None,
}
}
fn on_probe_lost(&mut self) {
if let Phase::Searching(state) = &mut self.phase {
state.in_flight_probe = None;
state.lost_probe_count += 1;
}
}
fn on_black_hole_detected(&mut self, now: Instant) {
let next_mtud_activation = now + self.config.black_hole_cooldown;
self.phase = Phase::Complete(next_mtud_activation);
}
}
#[derive(Debug, Clone, Copy)]
enum Phase {
Initial,
Searching(SearchState),
Complete(Instant),
}
#[derive(Debug, Clone, Copy)]
struct SearchState {
lower_bound: u16,
upper_bound: u16,
minimum_change: u16,
last_probed_mtu: u16,
in_flight_probe: Option<u64>,
lost_probe_count: usize,
}
impl SearchState {
fn new(
mut lower_bound: u16,
peer_max_udp_payload_size: u16,
config: &MtuDiscoveryConfig,
) -> Self {
lower_bound = lower_bound.min(peer_max_udp_payload_size);
let upper_bound = config
.upper_bound
.clamp(lower_bound, peer_max_udp_payload_size);
Self {
in_flight_probe: None,
lost_probe_count: 0,
lower_bound,
upper_bound,
minimum_change: config.minimum_change,
last_probed_mtu: lower_bound,
}
}
fn next_mtu_to_probe(&mut self, last_probe_succeeded: bool) -> Option<u16> {
debug_assert_eq!(self.in_flight_probe, None);
if last_probe_succeeded {
self.lower_bound = self.last_probed_mtu;
} else {
self.upper_bound = self.last_probed_mtu - 1;
}
let next_mtu = (self.lower_bound as i32 + self.upper_bound as i32) / 2;
if ((next_mtu - self.last_probed_mtu as i32).unsigned_abs() as u16) < self.minimum_change {
if self.upper_bound.saturating_sub(self.last_probed_mtu) >= self.minimum_change {
return Some(self.upper_bound);
}
return None;
}
Some(next_mtu as u16)
}
}
#[derive(Clone, Debug)]
struct BlackHoleDetector {
suspicious_loss_bursts: Vec<LossBurst>,
current_loss_burst: Option<CurrentLossBurst>,
largest_post_loss_packet: u64,
acked_mtu: u16,
min_mtu: u16,
}
impl BlackHoleDetector {
fn new(min_mtu: u16) -> Self {
Self {
suspicious_loss_bursts: Vec::with_capacity(BLACK_HOLE_THRESHOLD + 1),
current_loss_burst: None,
largest_post_loss_packet: 0,
acked_mtu: min_mtu,
min_mtu,
}
}
fn on_probe_acked(&mut self, pn: u64, len: u16) {
self.suspicious_loss_bursts.clear();
self.acked_mtu = len;
self.largest_post_loss_packet = pn;
}
fn on_non_probe_acked(&mut self, pn: u64, len: u16) {
if len <= self.acked_mtu {
return;
}
self.acked_mtu = len;
self.largest_post_loss_packet = pn;
self.suspicious_loss_bursts
.retain(|burst| burst.smallest_packet_size > len);
}
fn on_non_probe_lost(&mut self, pn: u64, len: u16) {
let end_last_burst = self
.current_loss_burst
.as_ref()
.is_some_and(|current| pn - current.latest_non_probe != 1);
if end_last_burst {
self.finish_loss_burst();
}
self.current_loss_burst = Some(CurrentLossBurst {
latest_non_probe: pn,
smallest_packet_size: self
.current_loss_burst
.map_or(len, |prev| cmp::min(prev.smallest_packet_size, len)),
});
}
fn black_hole_detected(&mut self) -> bool {
self.finish_loss_burst();
if self.suspicious_loss_bursts.len() <= BLACK_HOLE_THRESHOLD {
return false;
}
self.suspicious_loss_bursts.clear();
true
}
fn finish_loss_burst(&mut self) {
let Some(burst) = self.current_loss_burst.take() else {
return;
};
if burst.smallest_packet_size <= self.min_mtu
|| (burst.latest_non_probe < self.largest_post_loss_packet
&& burst.smallest_packet_size <= self.acked_mtu)
{
return;
}
if burst.latest_non_probe > self.largest_post_loss_packet {
self.acked_mtu = self.min_mtu;
}
let burst = LossBurst {
smallest_packet_size: burst.smallest_packet_size,
};
if self.suspicious_loss_bursts.len() <= BLACK_HOLE_THRESHOLD {
self.suspicious_loss_bursts.push(burst);
return;
}
let smallest = self
.suspicious_loss_bursts
.iter_mut()
.min_by_key(|prev| prev.smallest_packet_size)
.filter(|prev| prev.smallest_packet_size < burst.smallest_packet_size);
if let Some(smallest) = smallest {
*smallest = burst;
}
}
#[cfg(test)]
fn suspicious_loss_burst_count(&self) -> usize {
self.suspicious_loss_bursts.len()
}
#[cfg(test)]
fn largest_non_probe_lost(&self) -> Option<u64> {
self.current_loss_burst.as_ref().map(|x| x.latest_non_probe)
}
}
#[derive(Copy, Clone, Debug)]
struct LossBurst {
smallest_packet_size: u16,
}
#[derive(Copy, Clone, Debug)]
struct CurrentLossBurst {
smallest_packet_size: u16,
latest_non_probe: u64,
}
const MAX_PROBE_RETRANSMITS: usize = 3;
const BLACK_HOLE_THRESHOLD: usize = 3;
#[cfg(test)]
mod tests {
use super::*;
use crate::Duration;
use crate::MAX_UDP_PAYLOAD;
use assert_matches::assert_matches;
fn default_mtud() -> MtuDiscovery {
let config = MtuDiscoveryConfig::default();
MtuDiscovery::new(1_200, 1_200, None, config)
}
fn completed(mtud: &MtuDiscovery) -> bool {
matches!(mtud.state.as_ref().unwrap().phase, Phase::Complete(_))
}
fn drive_to_completion(
mtud: &mut MtuDiscovery,
now: Instant,
link_payload_size_limit: u16,
) -> Vec<u16> {
let mut probed_sizes = Vec::new();
for probe_pn in 1..100 {
let result = mtud.poll_transmit(now, probe_pn);
if completed(mtud) {
break;
}
assert!(result.is_some());
let probe_size = result.unwrap();
probed_sizes.push(probe_size);
if probe_size <= link_payload_size_limit {
mtud.on_acked(SpaceKind::Data, probe_pn, probe_size);
} else {
mtud.on_probe_lost();
}
}
probed_sizes
}
#[test]
fn black_hole_detector_ignores_burst_containing_non_suspicious_packet() {
let mut mtud = default_mtud();
mtud.on_non_probe_lost(2, 1300);
mtud.on_non_probe_lost(3, 1300);
assert_eq!(mtud.black_hole_detector.largest_non_probe_lost(), Some(3));
assert_eq!(mtud.black_hole_detector.suspicious_loss_burst_count(), 0);
mtud.on_non_probe_lost(4, 800);
assert!(!mtud.black_hole_detected(Instant::now()));
assert_eq!(mtud.black_hole_detector.largest_non_probe_lost(), None);
assert_eq!(mtud.black_hole_detector.suspicious_loss_burst_count(), 0);
}
#[test]
fn black_hole_detector_counts_burst_containing_only_suspicious_packets() {
let mut mtud = default_mtud();
mtud.on_non_probe_lost(2, 1300);
mtud.on_non_probe_lost(3, 1300);
assert_eq!(mtud.black_hole_detector.largest_non_probe_lost(), Some(3));
assert_eq!(mtud.black_hole_detector.suspicious_loss_burst_count(), 0);
assert!(!mtud.black_hole_detected(Instant::now()));
assert_eq!(mtud.black_hole_detector.largest_non_probe_lost(), None);
assert_eq!(mtud.black_hole_detector.suspicious_loss_burst_count(), 1);
}
#[test]
fn black_hole_detector_ignores_empty_burst() {
let mut mtud = default_mtud();
assert!(!mtud.black_hole_detected(Instant::now()));
assert_eq!(mtud.black_hole_detector.suspicious_loss_burst_count(), 0);
}
#[test]
fn mtu_discovery_disabled_does_nothing() {
let mut mtud = MtuDiscovery::disabled(1_200, 1_200);
let probe_size = mtud.poll_transmit(Instant::now(), 0);
assert_eq!(probe_size, None);
}
#[test]
fn mtu_discovery_disabled_lost_four_packet_bursts_triggers_black_hole_detection() {
let mut mtud = MtuDiscovery::disabled(1_400, 1_250);
let now = Instant::now();
for i in 0..4 {
mtud.on_non_probe_lost(i * 2, 1300);
}
assert!(mtud.black_hole_detected(now));
assert_eq!(mtud.current_mtu, 1250);
assert_matches!(mtud.state, None);
}
#[test]
fn mtu_discovery_lost_two_packet_bursts_does_not_trigger_black_hole_detection() {
let mut mtud = default_mtud();
let now = Instant::now();
for i in 0..2 {
mtud.on_non_probe_lost(i, 1300);
assert!(!mtud.black_hole_detected(now));
}
}
#[test]
fn mtu_discovery_lost_four_packet_bursts_triggers_black_hole_detection_and_resets_timer() {
let mut mtud = default_mtud();
let now = Instant::now();
for i in 0..4 {
mtud.on_non_probe_lost(i * 2, 1300);
}
assert!(mtud.black_hole_detected(now));
assert_eq!(mtud.current_mtu, 1200);
if let Phase::Complete(next_mtud_activation) = mtud.state.unwrap().phase {
assert_eq!(next_mtud_activation, now + Duration::from_secs(60));
} else {
panic!("Unexpected MTUD phase!");
}
}
#[test]
fn mtu_discovery_after_complete_reactivates_when_interval_elapsed() {
let mut config = MtuDiscoveryConfig::default();
config.upper_bound(9_000);
let mut mtud = MtuDiscovery::new(1_200, 1_200, None, config);
let now = Instant::now();
drive_to_completion(&mut mtud, now, 1_500);
assert_eq!(mtud.poll_transmit(now, 42), None);
assert!(completed(&mtud));
assert_eq!(mtud.current_mtu, 1_471);
assert_eq!(
mtud.poll_transmit(now + Duration::from_secs(600), 43),
Some(5235)
);
match mtud.state.unwrap().phase {
Phase::Searching(state) => {
assert_eq!(state.lower_bound, 1_471);
assert_eq!(state.upper_bound, 9_000);
}
_ => {
panic!("Unexpected MTUD phase!")
}
}
}
#[test]
fn mtu_discovery_lost_three_probes_lowers_probe_size() {
let mut mtud = default_mtud();
let mut probe_sizes = (0..4).map(|i| {
let probe_size = mtud.poll_transmit(Instant::now(), i);
assert!(probe_size.is_some(), "no probe returned for packet {i}");
mtud.on_probe_lost();
probe_size.unwrap()
});
let first_probe_size = probe_sizes.next().unwrap();
for _ in 0..2 {
assert_eq!(probe_sizes.next().unwrap(), first_probe_size)
}
let fourth_probe_size = probe_sizes.next().unwrap();
assert!(fourth_probe_size < first_probe_size);
assert_eq!(
fourth_probe_size,
first_probe_size - (first_probe_size - 1_200) / 2 - 1
);
}
#[test]
fn mtu_discovery_with_peer_max_udp_payload_size_clamps_upper_bound() {
let mut mtud = default_mtud();
mtud.on_peer_max_udp_payload_size_received(1300);
let probed_sizes = drive_to_completion(&mut mtud, Instant::now(), 1500);
assert_eq!(mtud.state.as_ref().unwrap().peer_max_udp_payload_size, 1300);
assert_eq!(mtud.current_mtu, 1300);
let expected_probed_sizes = &[1250, 1275, 1300];
assert_eq!(probed_sizes, expected_probed_sizes);
assert!(completed(&mtud));
}
#[test]
fn mtu_discovery_with_previous_peer_max_udp_payload_size_clamps_upper_bound() {
let mut mtud = MtuDiscovery::new(1500, 1_200, Some(1400), MtuDiscoveryConfig::default());
assert_eq!(mtud.current_mtu, 1400);
assert_eq!(mtud.state.as_ref().unwrap().peer_max_udp_payload_size, 1400);
let probed_sizes = drive_to_completion(&mut mtud, Instant::now(), 1500);
assert_eq!(mtud.current_mtu, 1400);
assert!(probed_sizes.is_empty());
assert!(completed(&mtud));
}
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "Transport parameters received after MTU probing started")]
fn mtu_discovery_with_peer_max_udp_payload_size_during_search_panics() {
let mut mtud = default_mtud();
assert!(mtud.poll_transmit(Instant::now(), 0).is_some());
assert!(matches!(
mtud.state.as_ref().unwrap().phase,
Phase::Searching(_)
));
mtud.on_peer_max_udp_payload_size_received(1300);
}
#[test]
fn mtu_discovery_with_1500_limit() {
let mut mtud = default_mtud();
let probed_sizes = drive_to_completion(&mut mtud, Instant::now(), 1500);
let expected_probed_sizes = &[1326, 1389, 1420, 1452];
assert_eq!(probed_sizes, expected_probed_sizes);
assert_eq!(mtud.current_mtu, 1452);
assert!(completed(&mtud));
}
#[test]
fn mtu_discovery_with_1500_limit_and_10000_upper_bound() {
let mut config = MtuDiscoveryConfig::default();
config.upper_bound(10_000);
let mut mtud = MtuDiscovery::new(1_200, 1_200, None, config);
let probed_sizes = drive_to_completion(&mut mtud, Instant::now(), 1500);
let expected_probed_sizes = &[
5600, 5600, 5600, 3399, 3399, 3399, 2299, 2299, 2299, 1749, 1749, 1749, 1474, 1611,
1611, 1611, 1542, 1542, 1542, 1507, 1507, 1507,
];
assert_eq!(probed_sizes, expected_probed_sizes);
assert_eq!(mtud.current_mtu, 1474);
assert!(completed(&mtud));
}
#[test]
fn mtu_discovery_no_lost_probes_finds_maximum_udp_payload() {
let mut config = MtuDiscoveryConfig::default();
config.upper_bound(MAX_UDP_PAYLOAD);
let mut mtud = MtuDiscovery::new(1200, 1200, None, config);
drive_to_completion(&mut mtud, Instant::now(), u16::MAX);
assert_eq!(mtud.current_mtu, 65527);
assert!(completed(&mtud));
}
#[test]
fn mtu_discovery_lost_half_of_probes_finds_maximum_udp_payload() {
let mut config = MtuDiscoveryConfig::default();
config.upper_bound(MAX_UDP_PAYLOAD);
let mut mtud = MtuDiscovery::new(1200, 1200, None, config);
let now = Instant::now();
let mut iterations = 0;
for i in 1..100 {
iterations += 1;
let probe_pn = i * 2 - 1;
let other_pn = i * 2;
let result = mtud.poll_transmit(Instant::now(), probe_pn);
if completed(&mtud) {
break;
}
assert!(result.is_some());
assert!(mtud.in_flight_mtu_probe().is_some());
assert_matches!(mtud.poll_transmit(now, other_pn), None);
if i % 2 == 0 {
let previous_max_size = mtud.current_mtu;
mtud.on_acked(SpaceKind::Data, probe_pn, result.unwrap());
println!(
"ACK packet {}. Previous MTU = {previous_max_size}. New MTU = {}",
result.unwrap(),
mtud.current_mtu
);
} else {
mtud.on_probe_lost();
}
}
assert_eq!(iterations, 25);
assert_eq!(mtud.current_mtu, 65527);
assert!(completed(&mtud));
}
#[test]
fn search_state_lower_bound_higher_than_upper_bound_clamps_upper_bound() {
let mut config = MtuDiscoveryConfig::default();
config.upper_bound(1400);
let state = SearchState::new(1500, u16::MAX, &config);
assert_eq!(state.lower_bound, 1500);
assert_eq!(state.upper_bound, 1500);
}
#[test]
fn search_state_lower_bound_higher_than_peer_max_udp_payload_size_clamps_lower_bound() {
let mut config = MtuDiscoveryConfig::default();
config.upper_bound(9000);
let state = SearchState::new(1500, 1300, &config);
assert_eq!(state.lower_bound, 1300);
assert_eq!(state.upper_bound, 1300);
}
#[test]
fn search_state_upper_bound_higher_than_peer_max_udp_payload_size_clamps_upper_bound() {
let mut config = MtuDiscoveryConfig::default();
config.upper_bound(9000);
let state = SearchState::new(1200, 1450, &config);
assert_eq!(state.lower_bound, 1200);
assert_eq!(state.upper_bound, 1450);
}
#[test]
fn simple_black_hole_detection() {
let mut bhd = BlackHoleDetector::new(1200);
bhd.on_non_probe_acked((BLACK_HOLE_THRESHOLD + 1) as u64 * 2, 1300);
for i in 0..BLACK_HOLE_THRESHOLD {
bhd.on_non_probe_lost(i as u64 * 2, 1400);
}
assert!(!bhd.black_hole_detected());
bhd.on_non_probe_lost(BLACK_HOLE_THRESHOLD as u64 * 2, 1400);
assert!(bhd.black_hole_detected());
}
#[test]
fn non_suspicious_bursts() {
let mut bhd = BlackHoleDetector::new(1200);
bhd.on_non_probe_acked((BLACK_HOLE_THRESHOLD + 1) as u64 * 2, 1500);
for i in 0..(BLACK_HOLE_THRESHOLD + 1) {
bhd.on_non_probe_lost(i as u64 * 2, 1400);
}
assert!(!bhd.black_hole_detected());
}
#[test]
fn dynamic_mtu_reduction() {
let mut bhd = BlackHoleDetector::new(1200);
bhd.on_non_probe_acked(0, 1500);
for i in 0..(BLACK_HOLE_THRESHOLD + 1) {
bhd.on_non_probe_lost(i as u64 * 2, 1400);
}
assert!(bhd.black_hole_detected());
}
#[test]
fn mixed_non_suspicious_bursts() {
let mut bhd = BlackHoleDetector::new(1200);
bhd.on_non_probe_acked((BLACK_HOLE_THRESHOLD + 1) as u64 * 3, 1400);
for i in 0..(BLACK_HOLE_THRESHOLD + 1) {
bhd.on_non_probe_lost(i as u64 * 3, 1500);
bhd.on_non_probe_lost(i as u64 * 3 + 1, 1300);
}
assert!(!bhd.black_hole_detected());
}
#[test]
fn bursts_count_once() {
let mut bhd = BlackHoleDetector::new(1200);
bhd.on_non_probe_acked((BLACK_HOLE_THRESHOLD + 1) as u64 * 3, 1400);
for i in 0..(BLACK_HOLE_THRESHOLD) {
bhd.on_non_probe_lost(i as u64 * 3, 1500);
bhd.on_non_probe_lost(i as u64 * 3 + 1, 1500);
}
assert!(!bhd.black_hole_detected());
bhd.on_non_probe_lost(BLACK_HOLE_THRESHOLD as u64 * 3, 1500);
assert!(bhd.black_hole_detected());
}
#[test]
fn interleaved_bursts() {
let mut bhd = BlackHoleDetector::new(1200);
bhd.on_non_probe_acked((BLACK_HOLE_THRESHOLD + 1) as u64 * 4, 1400);
for i in 0..(BLACK_HOLE_THRESHOLD + 1) {
bhd.on_non_probe_lost(i as u64 * 4, 1500);
bhd.on_non_probe_lost(i as u64 * 4 + 2, 1300);
}
assert!(bhd.black_hole_detected());
}
#[test]
fn suspicious_after_acked() {
let mut bhd = BlackHoleDetector::new(1200);
bhd.on_non_probe_acked((BLACK_HOLE_THRESHOLD + 1) as u64 * 2, 1400);
for i in 0..(BLACK_HOLE_THRESHOLD + 1) {
bhd.on_non_probe_lost(i as u64 * 2, 1300);
}
assert!(
!bhd.black_hole_detected(),
"1300 byte losses preceding a 1400 byte delivery are not suspicious"
);
for i in 0..(BLACK_HOLE_THRESHOLD + 1) {
bhd.on_non_probe_lost((BLACK_HOLE_THRESHOLD as u64 + 1 + i as u64) * 2, 1300);
}
assert!(
bhd.black_hole_detected(),
"1300 byte losses following a 1400 byte delivery are suspicious"
);
}
#[test]
fn retroactively_non_suspicious() {
let mut bhd = BlackHoleDetector::new(1200);
for i in 0..BLACK_HOLE_THRESHOLD {
bhd.on_non_probe_lost(i as u64 * 2, 1400);
}
bhd.on_non_probe_acked(BLACK_HOLE_THRESHOLD as u64 * 2, 1400);
bhd.on_non_probe_lost(BLACK_HOLE_THRESHOLD as u64 * 2 + 1, 1400);
assert!(!bhd.black_hole_detected());
}
}