use std::time::{Duration, Instant};
use bitfold_core::{config::Config, shared::SharedBytes};
use bitfold_protocol::command::ProtocolCommand;
use rand::RngCore;
#[derive(Debug)]
pub struct PmtuDiscovery {
config: Config,
fragment_size: u16,
low: u16,
high: u16,
last_probe: Instant,
outstanding: Option<(u16, u32, Instant)>,
}
impl PmtuDiscovery {
pub fn new(config: &Config, time: Instant) -> Self {
Self {
config: config.clone(),
fragment_size: config.fragment_size,
low: config.pmtu_min,
high: config.pmtu_max,
last_probe: time,
outstanding: None,
}
}
pub fn current_fragment_size(&self) -> u16 {
self.fragment_size
}
pub fn set_fragment_size(&mut self, size: u16) {
self.fragment_size = size;
}
pub fn low_bound(&self) -> u16 {
self.low
}
pub fn high_bound(&self) -> u16 {
self.high
}
pub fn has_outstanding_probe(&self) -> bool {
self.outstanding.is_some()
}
#[cfg(test)]
pub fn outstanding_probe(&self) -> Option<(u16, u32, Instant)> {
self.outstanding
}
pub fn handle_pmtu(&mut self, time: Instant, rto: Duration) -> Option<ProtocolCommand> {
if !self.config.use_pmtu_discovery {
return None;
}
if let Some((size, _token, sent)) = self.outstanding {
let timeout = rto.max(Duration::from_millis(200));
if time.duration_since(sent) > timeout {
if size > 0 {
self.high = self.high.min(size - 1);
}
self.outstanding = None;
self.last_probe = time;
}
return None;
}
let datagram_cap = self.config.receive_buffer_max_size.min(u16::MAX as usize) as u16;
if self.high > datagram_cap {
self.high = datagram_cap;
}
if self.high.saturating_sub(self.low) <= self.config.pmtu_converge_threshold {
self.fragment_size = self.low;
return None;
}
let interval = Duration::from_millis(self.config.pmtu_interval_ms as u64);
if time.duration_since(self.last_probe) < interval {
return None;
}
let mid = ((self.low as u32 + self.high as u32) / 2) as u16;
let target = mid.min(datagram_cap);
let compression_overhead = match self.config.compression {
bitfold_core::config::CompressionAlgorithm::Lz4 => 5, _ => 1, } as u16;
let checksum_overhead = if self.config.use_checksums { 4 } else { 0 } as u16;
let static_overhead = 1 + compression_overhead + checksum_overhead;
let per_command_overhead = 2 + (1 + 2 + 4 + 2 );
let total_overhead = static_overhead + per_command_overhead;
let payload_len =
if target > total_overhead { (target - total_overhead).max(1) } else { 1 } as usize;
let token: u32 = rand::random();
let mut payload_vec = vec![0u8; payload_len];
rand::rng().fill_bytes(&mut payload_vec);
let payload = SharedBytes::from_vec(payload_vec);
let command = ProtocolCommand::PMTUProbe { size: target, token, payload };
self.outstanding = Some((mid, token, time));
self.last_probe = time;
Some(command)
}
pub fn process_reply(&mut self, size: u16, token: u32, time: Instant) -> bool {
if let Some((_pending_size, pending_token, _sent)) = self.outstanding {
if pending_token == token {
self.low = self.low.max(size);
self.fragment_size = self.low;
self.outstanding = None;
self.last_probe = time;
tracing::debug!("PMTU success: token={}, size={}", token, size);
return true;
}
}
false
}
pub fn create_reply(size: u16, token: u32) -> ProtocolCommand {
ProtocolCommand::PMTUReply { size, token }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pmtu_discovery_probe_reply() {
let mut config = Config::default();
config.use_pmtu_discovery = true;
config.pmtu_min = 576;
config.pmtu_max = 1400;
config.pmtu_interval_ms = 100;
let start_time = Instant::now();
let mut pmtu = PmtuDiscovery::new(&config, start_time);
assert_eq!(pmtu.current_fragment_size(), config.fragment_size);
let time = start_time + Duration::from_millis(150);
let rto = Duration::from_millis(200);
let probe_cmd = pmtu.handle_pmtu(time, rto);
assert!(probe_cmd.is_some());
assert!(pmtu.has_outstanding_probe());
if let Some(outstanding) = pmtu.outstanding {
let (size, token, _) = outstanding;
let success = pmtu.process_reply(size, token, time);
assert!(success);
assert_eq!(pmtu.current_fragment_size(), size);
assert!(!pmtu.has_outstanding_probe());
}
}
#[test]
fn test_pmtu_discovery_timeout() {
let mut config = Config::default();
config.use_pmtu_discovery = true;
config.pmtu_min = 576;
config.pmtu_max = 1400;
config.pmtu_interval_ms = 100;
let start_time = Instant::now();
let mut pmtu = PmtuDiscovery::new(&config, start_time);
let mut time = start_time + Duration::from_millis(150);
let rto = Duration::from_millis(200);
let probe_cmd = pmtu.handle_pmtu(time, rto);
assert!(probe_cmd.is_some());
let high_before = pmtu.high_bound();
time = time + Duration::from_secs(2);
let result = pmtu.handle_pmtu(time, rto);
assert!(!pmtu.has_outstanding_probe());
assert!(pmtu.high_bound() < high_before);
assert!(result.is_none()); }
#[test]
fn test_pmtu_discovery_enabled_by_default() {
let config = Config::default();
assert!(config.use_pmtu_discovery);
let creation_time = Instant::now();
let mut pmtu = PmtuDiscovery::new(&config, creation_time);
let probe_time =
creation_time + Duration::from_millis(config.pmtu_interval_ms as u64 + 100);
let rto = Duration::from_millis(200);
let probe = pmtu.handle_pmtu(probe_time, rto);
assert!(probe.is_some());
}
#[test]
fn test_pmtu_discovery_can_be_disabled() {
let mut config = Config::default();
config.use_pmtu_discovery = false;
assert!(!config.use_pmtu_discovery);
let mut pmtu = PmtuDiscovery::new(&config, Instant::now());
let time = Instant::now();
let rto = Duration::from_millis(200);
let probe = pmtu.handle_pmtu(time, rto);
assert!(probe.is_none());
}
#[test]
fn test_pmtu_discovery_convergence() {
let mut config = Config::default();
config.use_pmtu_discovery = true;
config.pmtu_min = 1200;
config.pmtu_max = 1232; config.pmtu_converge_threshold = 64;
let mut pmtu = PmtuDiscovery::new(&config, Instant::now());
let time = Instant::now();
let rto = Duration::from_millis(200);
let probe = pmtu.handle_pmtu(time, rto);
assert!(probe.is_none());
assert_eq!(pmtu.current_fragment_size(), config.pmtu_min);
}
#[test]
fn test_pmtu_invalid_reply_token() {
let mut config = Config::default();
config.use_pmtu_discovery = true;
let start_time = Instant::now();
let mut pmtu = PmtuDiscovery::new(&config, start_time);
let time = start_time + Duration::from_millis(config.pmtu_interval_ms as u64 + 100);
let rto = Duration::from_millis(200);
pmtu.handle_pmtu(time, rto);
let wrong_token = 99999;
let success = pmtu.process_reply(1000, wrong_token, time);
assert!(!success);
assert!(pmtu.has_outstanding_probe());
}
#[test]
fn test_pmtu_create_reply() {
let size = 1200;
let token = 12345;
let reply = PmtuDiscovery::create_reply(size, token);
match reply {
ProtocolCommand::PMTUReply { size: s, token: t } => {
assert_eq!(s, size);
assert_eq!(t, token);
}
_ => panic!("Expected PMTUReply command"),
}
}
}