1use std::time::{Duration, Instant};
40use tracing::{debug, info, warn};
41
42pub const VCL_HEADER_OVERHEAD: usize = 149;
45
46pub const ETHERNET_MTU: usize = 1500;
48
49pub const IPV4_HEADER: usize = 20;
51
52pub const IPV6_HEADER: usize = 40;
54
55pub const UDP_HEADER: usize = 8;
57
58pub const MIN_MTU: usize = 576;
60
61pub const MAX_MTU: usize = 9000; #[derive(Debug, Clone)]
66pub struct MtuConfig {
67 pub start_mtu: usize,
69 pub min_mtu: usize,
71 pub max_mtu: usize,
73 pub step: usize,
75 pub probe_timeout: Duration,
77 pub ipv6: bool,
79 pub extra_overhead: usize,
81}
82
83impl Default for MtuConfig {
84 fn default() -> Self {
85 MtuConfig {
86 start_mtu: ETHERNET_MTU,
87 min_mtu: MIN_MTU,
88 max_mtu: ETHERNET_MTU,
89 step: 8,
90 probe_timeout: Duration::from_secs(2),
91 ipv6: false,
92 extra_overhead: 0,
93 }
94 }
95}
96
97impl MtuConfig {
98 pub fn ipv4_udp() -> Self {
100 MtuConfig::default()
101 }
102
103 pub fn ipv6_udp() -> Self {
105 MtuConfig {
106 ipv6: true,
107 ..Default::default()
108 }
109 }
110
111 pub fn inside_wireguard() -> Self {
113 MtuConfig {
114 max_mtu: 1420,
115 start_mtu: 1420,
116 extra_overhead: 60,
117 ..Default::default()
118 }
119 }
120}
121
122#[derive(Debug, Clone)]
124pub struct PathMtu {
125 pub mtu: usize,
127 pub fragment_size: usize,
129 pub measured_at: Instant,
131 pub is_probed: bool,
133}
134
135impl PathMtu {
136 pub fn new(mtu: usize, fragment_size: usize, is_probed: bool) -> Self {
138 PathMtu {
139 mtu,
140 fragment_size,
141 measured_at: Instant::now(),
142 is_probed,
143 }
144 }
145
146 pub fn is_stale(&self, max_age: Duration) -> bool {
148 self.measured_at.elapsed() > max_age
149 }
150}
151
152#[derive(Debug, Clone, PartialEq)]
154pub enum MtuState {
155 Initial,
157 Probing {
159 low: usize,
160 high: usize,
161 current: usize,
162 },
163 Confirmed(usize),
165 FallbackToMin,
167}
168
169pub struct MtuNegotiator {
174 config: MtuConfig,
175 state: MtuState,
176 current_mtu: usize,
178 pending_probe: Option<(usize, Instant)>,
180 probe_history: Vec<(usize, bool)>,
182 total_probes: u64,
184 successful_probes: u64,
186}
187
188impl MtuNegotiator {
189 pub fn new(config: MtuConfig) -> Self {
191 let start = config.start_mtu;
192 let min = config.min_mtu;
193 let max = config.max_mtu;
194 MtuNegotiator {
195 state: MtuState::Initial,
196 current_mtu: start.min(max),
197 pending_probe: None,
198 probe_history: Vec::new(),
199 total_probes: 0,
200 successful_probes: 0,
201 config: MtuConfig { start_mtu: start, min_mtu: min, max_mtu: max, ..config },
202 }
203 }
204
205 pub fn start_discovery(&mut self) -> usize {
210 let low = self.config.min_mtu;
211 let high = self.config.max_mtu;
212 let current = (low + high) / 2;
213 self.state = MtuState::Probing { low, high, current };
214 self.pending_probe = Some((current, Instant::now()));
215 self.total_probes += 1;
216 info!(low, high, probe_size = current, "MTU discovery started");
217 current
218 }
219
220 pub fn record_probe(&mut self, size: usize, success: bool) -> Option<usize> {
227 self.probe_history.push((size, success));
228 self.pending_probe = None;
229
230 if success {
231 self.successful_probes += 1;
232 debug!(size, "MTU probe succeeded");
233 } else {
234 warn!(size, "MTU probe failed (packet dropped)");
235 }
236
237 match self.state.clone() {
238 MtuState::Probing { low, high, current } => {
239 let (new_low, new_high) = if success {
240 self.current_mtu = current;
241 (current, high)
242 } else {
243 (low, current - self.config.step)
244 };
245
246 if new_high <= new_low || new_high - new_low <= self.config.step {
248 let final_mtu = if success { current } else { self.current_mtu };
250 let final_mtu = final_mtu.max(self.config.min_mtu);
251 self.current_mtu = final_mtu;
252 self.state = MtuState::Confirmed(final_mtu);
253 info!(mtu = final_mtu, "MTU discovery complete");
254 return None;
255 }
256
257 let next = (new_low + new_high) / 2;
258 self.state = MtuState::Probing {
259 low: new_low,
260 high: new_high,
261 current: next,
262 };
263 self.pending_probe = Some((next, Instant::now()));
264 self.total_probes += 1;
265 debug!(next_probe = next, low = new_low, high = new_high, "Next MTU probe");
266 Some(next)
267 }
268 _ => {
269 if success && size > self.current_mtu {
271 self.current_mtu = size;
272 }
273 None
274 }
275 }
276 }
277
278 pub fn check_probe_timeout(&self) -> Option<usize> {
283 if let Some((size, sent_at)) = self.pending_probe {
284 if sent_at.elapsed() > self.config.probe_timeout {
285 return Some(size);
286 }
287 }
288 None
289 }
290
291 pub fn current_mtu(&self) -> usize {
293 self.current_mtu
294 }
295
296 pub fn state(&self) -> &MtuState {
298 &self.state
299 }
300
301 pub fn is_complete(&self) -> bool {
303 matches!(self.state, MtuState::Confirmed(_) | MtuState::FallbackToMin)
304 }
305
306 pub fn recommended_fragment_size(&self) -> usize {
311 let ip_header = if self.config.ipv6 { IPV6_HEADER } else { IPV4_HEADER };
312 let overhead = ip_header
313 + UDP_HEADER
314 + VCL_HEADER_OVERHEAD
315 + self.config.extra_overhead;
316
317 if self.current_mtu <= overhead {
318 warn!(
319 mtu = self.current_mtu,
320 overhead,
321 "MTU smaller than overhead — using minimum fragment size"
322 );
323 return 64; }
325
326 let fragment_size = self.current_mtu - overhead;
327 (fragment_size / 8) * 8
329 }
330
331 pub fn set_mtu(&mut self, mtu: usize) {
333 let clamped = mtu.clamp(self.config.min_mtu, MAX_MTU);
334 info!(mtu = clamped, "MTU manually set");
335 self.current_mtu = clamped;
336 self.state = MtuState::Confirmed(clamped);
337 }
338
339 pub fn fallback_to_min(&mut self) {
341 warn!(min = self.config.min_mtu, "MTU falling back to minimum");
342 self.current_mtu = self.config.min_mtu;
343 self.state = MtuState::FallbackToMin;
344 }
345
346 pub fn path_mtu(&self) -> PathMtu {
348 PathMtu::new(
349 self.current_mtu,
350 self.recommended_fragment_size(),
351 self.successful_probes > 0,
352 )
353 }
354
355 pub fn total_probes(&self) -> u64 {
357 self.total_probes
358 }
359
360 pub fn successful_probes(&self) -> u64 {
362 self.successful_probes
363 }
364
365 pub fn probe_history(&self) -> &[(usize, bool)] {
367 &self.probe_history
368 }
369}
370
371pub fn fragment_size_for_mtu(mtu: usize, ipv6: bool, extra_overhead: usize) -> usize {
382 let ip_header = if ipv6 { IPV6_HEADER } else { IPV4_HEADER };
383 let overhead = ip_header + UDP_HEADER + VCL_HEADER_OVERHEAD + extra_overhead;
384 if mtu <= overhead {
385 return 64;
386 }
387 ((mtu - overhead) / 8) * 8
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393
394 #[test]
395 fn test_default_config() {
396 let c = MtuConfig::default();
397 assert_eq!(c.start_mtu, 1500);
398 assert_eq!(c.min_mtu, 576);
399 assert!(!c.ipv6);
400 }
401
402 #[test]
403 fn test_ipv6_config() {
404 let c = MtuConfig::ipv6_udp();
405 assert!(c.ipv6);
406 }
407
408 #[test]
409 fn test_wireguard_config() {
410 let c = MtuConfig::inside_wireguard();
411 assert_eq!(c.max_mtu, 1420);
412 assert_eq!(c.extra_overhead, 60);
413 }
414
415 #[test]
416 fn test_negotiator_new() {
417 let n = MtuNegotiator::new(MtuConfig::default());
418 assert_eq!(n.state(), &MtuState::Initial);
419 assert_eq!(n.current_mtu(), 1500);
420 assert!(!n.is_complete());
421 }
422
423 #[test]
424 fn test_start_discovery() {
425 let mut n = MtuNegotiator::new(MtuConfig::default());
426 let probe = n.start_discovery();
427 assert!(probe > 576 && probe < 1500);
428 assert!(matches!(n.state(), MtuState::Probing { .. }));
429 assert_eq!(n.total_probes(), 1);
430 }
431
432 #[test]
433 fn test_record_probe_success() {
434 let mut n = MtuNegotiator::new(MtuConfig::default());
435 n.start_discovery();
436 let next = n.record_probe(1038, true);
437 assert!(n.current_mtu() >= 1038);
439 }
440
441 #[test]
442 fn test_record_probe_failure() {
443 let mut n = MtuNegotiator::new(MtuConfig::default());
444 n.start_discovery();
445 let _ = n.record_probe(1038, false);
446 assert!(n.current_mtu() <= 1500);
448 }
449
450 #[test]
451 fn test_full_discovery_converges() {
452 let mut n = MtuNegotiator::new(MtuConfig::default());
453 let mut probe = n.start_discovery();
454
455 for _ in 0..20 {
457 let success = probe <= 1400;
458 match n.record_probe(probe, success) {
459 Some(next) => probe = next,
460 None => break,
461 }
462 }
463
464 assert!(n.is_complete());
465 assert!(n.current_mtu() <= 1400);
466 assert!(n.current_mtu() >= 576);
467 }
468
469 #[test]
470 fn test_recommended_fragment_size() {
471 let mut n = MtuNegotiator::new(MtuConfig::default());
472 n.set_mtu(1500);
473 let fs = n.recommended_fragment_size();
474 assert!(fs > 0);
475 assert!(fs < 1500);
476 assert_eq!(fs % 8, 0);
478 }
479
480 #[test]
481 fn test_fragment_size_for_mtu_fn() {
482 let fs = fragment_size_for_mtu(1500, false, 0);
483 assert!(fs > 0 && fs < 1500);
484 assert_eq!(fs % 8, 0);
485
486 let fs_v6 = fragment_size_for_mtu(1500, true, 0);
487 assert!(fs_v6 < fs); }
489
490 #[test]
491 fn test_set_mtu() {
492 let mut n = MtuNegotiator::new(MtuConfig::default());
493 n.set_mtu(1280);
494 assert_eq!(n.current_mtu(), 1280);
495 assert!(n.is_complete());
496 assert!(matches!(n.state(), MtuState::Confirmed(1280)));
497 }
498
499 #[test]
500 fn test_set_mtu_clamped() {
501 let mut n = MtuNegotiator::new(MtuConfig::default());
502 n.set_mtu(100); assert_eq!(n.current_mtu(), 576);
504 }
505
506 #[test]
507 fn test_fallback_to_min() {
508 let mut n = MtuNegotiator::new(MtuConfig::default());
509 n.fallback_to_min();
510 assert_eq!(n.current_mtu(), 576);
511 assert_eq!(n.state(), &MtuState::FallbackToMin);
512 assert!(n.is_complete());
513 }
514
515 #[test]
516 fn test_path_mtu() {
517 let mut n = MtuNegotiator::new(MtuConfig::default());
518 n.set_mtu(1400);
519 let pm = n.path_mtu();
520 assert_eq!(pm.mtu, 1400);
521 assert!(pm.fragment_size < 1400);
522 assert!(!pm.is_probed); }
524
525 #[test]
526 fn test_probe_history() {
527 let mut n = MtuNegotiator::new(MtuConfig::default());
528 n.start_discovery();
529 n.record_probe(1038, true);
530 assert_eq!(n.probe_history().len(), 1);
531 assert_eq!(n.probe_history()[0], (1038, true));
532 }
533
534 #[test]
535 fn test_check_probe_timeout_no_pending() {
536 let n = MtuNegotiator::new(MtuConfig::default());
537 assert!(n.check_probe_timeout().is_none());
538 }
539
540 #[test]
541 fn test_check_probe_timeout_not_yet() {
542 let mut n = MtuNegotiator::new(MtuConfig::default());
543 n.start_discovery();
544 assert!(n.check_probe_timeout().is_none());
546 }
547
548 #[test]
549 fn test_mtu_smaller_than_overhead() {
550 let config = MtuConfig {
551 start_mtu: 100,
552 min_mtu: 64,
553 max_mtu: 100,
554 ..Default::default()
555 };
556 let mut n = MtuNegotiator::new(config);
557 n.set_mtu(100);
558 assert_eq!(n.recommended_fragment_size(), 64);
560 }
561
562 #[test]
563 fn test_extra_overhead() {
564 let fs1 = fragment_size_for_mtu(1500, false, 0);
565 let fs2 = fragment_size_for_mtu(1500, false, 60); assert!(fs2 < fs1);
567 }
568
569 #[test]
570 fn test_total_probes_counted() {
571 let mut n = MtuNegotiator::new(MtuConfig::default());
572 n.start_discovery();
573 assert_eq!(n.total_probes(), 1);
574 n.record_probe(1038, true);
575 assert!(n.total_probes() >= 1);
576 }
577}