1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
/// Contains the logic to implement PMTUD. Given a maximum supported MTU,
/// finds the PMTU between the given max and the QUIC version specific minimum.
#[derive(Default)]
pub struct Pmtud {
/// The minimum supported MTU (QUIC version dependent).
minimum_supported_mtu: usize,
/// The PMTU after the completion of PMTUD.
/// Will be [`None`] if the PMTU is less than the minimum supported MTU.
pmtu: Option<usize>,
/// The current PMTUD probe size. Set to maximum_supported_mtu at
/// initialization.
probe_size: usize,
/// The maximum supported MTU.
maximum_supported_mtu: usize,
/// The size of the smallest failed probe.
smallest_failed_probe_size: Option<usize>,
/// The size of the largest successful probe.
largest_successful_probe_size: Option<usize>,
/// Indicates if a PMTUD probe is in flight. Used to limit probes to 1/RTT.
in_flight: bool,
}
impl Pmtud {
/// Creates new PMTUD instance.
pub fn new(
minimum_supported_mtu: usize, maximum_supported_mtu: usize,
) -> Self {
Self {
minimum_supported_mtu,
maximum_supported_mtu,
probe_size: maximum_supported_mtu,
..Default::default()
}
}
/// Indicates whether probing should continue on the connection.
///
/// Checks there are no probes in flight, that a PMTU has not been
/// found, and that the minimum supported MTU has not been reached.
pub fn should_probe(&self) -> bool {
!self.in_flight &&
self.pmtu.is_none() &&
self.smallest_failed_probe_size != Some(self.minimum_supported_mtu)
}
/// Sets the PMTUD probe size.
fn set_probe_size(&mut self, probe_size: usize) {
self.probe_size = std::cmp::min(probe_size, self.maximum_supported_mtu);
}
/// Returns the PMTUD probe size.
pub fn get_probe_size(&self) -> usize {
self.probe_size
}
/// Returns the largest successful PMTUD probe size if one exists, otherwise
/// returns the minimum supported MTU.
pub fn get_current_mtu(&self) -> usize {
self.largest_successful_probe_size
.unwrap_or(self.minimum_supported_mtu)
}
/// Returns the PMTU.
pub fn get_pmtu(&self) -> Option<usize> {
self.pmtu
}
/// Selects PMTU probe size based on the binary search algorithm.
///
/// Based on the Optimistic Binary algorithm defined in:
/// Ref: <https://www.hb.fh-muenster.de/opus4/frontdoor/deliver/index/docId/14965/file/dplpmtudQuicPaper.pdf>
fn update_probe_size(&mut self) {
match (
self.smallest_failed_probe_size,
self.largest_successful_probe_size,
) {
// Binary search between successful and failed probes
(Some(failed_probe_size), Some(successful_probe_size)) => {
// Something has changed along the path that invalidates
// previous PMTUD probes. Restart PMTUD
if failed_probe_size <= successful_probe_size {
warn!(
"Inconsistent PMTUD probing results. Restarting PMTUD. \
failed_probe_size: {failed_probe_size}, \
successful_probe_size: {successful_probe_size}",
);
return self.restart_pmtud();
}
// Found the PMTU
if failed_probe_size - successful_probe_size <= 1 {
trace!("Found PMTU: {successful_probe_size}");
self.pmtu = Some(successful_probe_size);
self.probe_size = successful_probe_size
} else {
self.probe_size =
(successful_probe_size + failed_probe_size) / 2
}
},
// With only failed probes, binary search between the smallest failed
// probe and the minimum supported MTU
(Some(failed_probe_size), None) =>
self.probe_size =
(self.minimum_supported_mtu + failed_probe_size) / 2,
// As the algorithm is optimistic in that the initial probe size
// is the maximum supported MTU, then having only a successful probe
// means the maximum supported MTU is <= PMTU
(None, Some(successful_probe_size)) => {
self.pmtu = Some(successful_probe_size);
self.probe_size = successful_probe_size
},
// Use the initial probe size if no record of success/failures
(None, None) => self.probe_size = self.maximum_supported_mtu,
}
}
/// Sets whether a probe is currently in flight for this connection.
pub fn set_in_flight(&mut self, in_flight: bool) {
self.in_flight = in_flight;
}
/// Records a successful probe and returns the largest successful probe size
pub fn successful_probe(&mut self, probe_size: usize) -> Option<usize> {
self.largest_successful_probe_size = std::cmp::max(
// make sure we don't exceed the maximum supported MTU
Some(probe_size.min(self.maximum_supported_mtu)),
self.largest_successful_probe_size,
);
self.update_probe_size();
self.in_flight = false;
self.largest_successful_probe_size
}
/// Records a failed probe
pub fn failed_probe(&mut self, probe_size: usize) {
// Treat errant probes as if they failed at the minimum supported MTU
let probe_size = std::cmp::max(probe_size, self.minimum_supported_mtu);
// Check if we have one instance of a failed probe so that a min
// comparison can be made otherwise if this is the first failed
// probe just record it
self.smallest_failed_probe_size = self
.smallest_failed_probe_size
.map_or(Some(probe_size), |existing_size| {
Some(std::cmp::min(probe_size, existing_size))
});
self.update_probe_size();
self.in_flight = false;
}
// Resets PMTUD internals such that PMTUD will be recalculated
// on the next opportunity
fn restart_pmtud(&mut self) {
self.set_probe_size(self.maximum_supported_mtu);
self.smallest_failed_probe_size = None;
self.largest_successful_probe_size = None;
self.pmtu = None;
}
// Checks that a probe of PMTU size can be ack'd by enabling
// a probe on the next opportunity. If this probe is dropped
// PMTUD will restart from a fresh state
pub fn revalidate_pmtu(&mut self) {
if let Some(pmtu) = self.pmtu {
self.set_probe_size(pmtu);
self.pmtu = None;
};
}
}
impl std::fmt::Debug for Pmtud {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "pmtu={:?} ", self.pmtu)?;
write!(f, "probe_size={:?} ", self.probe_size)?;
write!(f, "should_probe={:?} ", self.should_probe())?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::MIN_CLIENT_INITIAL_LEN;
use super::*;
#[test]
fn pmtud_initial_state() {
let pmtud = Pmtud::new(MIN_CLIENT_INITIAL_LEN, 1350);
assert_eq!(pmtud.get_current_mtu(), 1200);
assert_eq!(pmtud.get_probe_size(), 1350);
assert!(pmtud.should_probe());
}
#[test]
fn pmtud_binary_search_algorithm() {
let mut pmtud = Pmtud::new(MIN_CLIENT_INITIAL_LEN, 1500);
// Set initial probe size to 1500
assert_eq!(pmtud.get_probe_size(), 1500);
// Simulate probe loss - should update to midpoint
pmtud.failed_probe(1500);
// Expected: 1200 + ((1500 - 1200) / 2) = 1200 + 150 = 1350
assert_eq!(pmtud.get_probe_size(), 1350);
// Another probe loss
pmtud.failed_probe(1350);
// Expected: 1200 + ((1350 - 1200) / 2) = 1200 + 75 = 1275
assert_eq!(pmtud.get_probe_size(), 1275);
pmtud.failed_probe(1275);
// Expected: 1200 + ((1275 - 1200) / 2) = 1200 + 37 = 1237
assert_eq!(pmtud.get_probe_size(), 1237);
pmtud.failed_probe(1237);
// Expected: 1200 + ((1237 - 1200) / 2) = 1200 + 18 = 1218
assert_eq!(pmtud.get_probe_size(), 1218);
pmtud.failed_probe(1218);
// Expected: 1200 + ((1218 - 1200) / 2) = 1200 + 9 = 1209
assert_eq!(pmtud.get_probe_size(), 1209);
pmtud.failed_probe(1209);
// Expected: 1200 + ((1209 - 1200) / 2) = 1200 + 4 = 1204
assert_eq!(pmtud.get_probe_size(), 1204);
pmtud.failed_probe(1204);
// Expected: 1200 + ((1204 - 1200) / 2) = 1200 + 2 = 1202
assert_eq!(pmtud.get_probe_size(), 1202);
pmtud.failed_probe(1202);
// Expected: 1200 + ((1202 - 1200) / 2) = 1200 + 1 = 1201
assert_eq!(pmtud.get_probe_size(), 1201);
pmtud.failed_probe(1201);
// Expected: 1200 + ((1201 - 1200) / 2) = 1200 + 0 = 1200
assert_eq!(pmtud.get_probe_size(), 1200);
}
#[test]
fn pmtud_probe_lost_behavior() {
let mut pmtud = Pmtud::new(MIN_CLIENT_INITIAL_LEN, 1500);
// Simulate probe loss
pmtud.failed_probe(1500);
// Should re-enable probing and adjust size
assert!(pmtud.should_probe());
assert_eq!(pmtud.get_probe_size(), 1350); // binary search result
assert_eq!(pmtud.get_current_mtu(), 1200); // MTU does not
// change
}
#[test]
fn pmtud_successful_probe() {
let mut pmtud = Pmtud::new(MIN_CLIENT_INITIAL_LEN, 1400);
// Simulate successful probe
pmtud.successful_probe(1400);
assert_eq!(pmtud.get_current_mtu(), 1400);
}
#[test]
fn pmtud_binary_search_convergence() {
let mut pmtud = Pmtud::new(MIN_CLIENT_INITIAL_LEN, 2000);
// Simulate repeated probe losses to test convergence
pmtud_test_runner(&mut pmtud, 1200);
// Should converge to the minimum allowed packet size
assert_eq!(pmtud.get_probe_size(), 1200);
}
/// Test case for resetting the PMTUD state.
///
/// This test initializes the PMTUD instance, performs a successful probe,
/// recalculates the PMTU, and then uses the `pmtud_test_runner` function
/// to verify the PMTU discovery process.
#[test]
fn test_pmtud_reset() {
let mut pmtud = Pmtud::new(MIN_CLIENT_INITIAL_LEN, 1350);
pmtud.successful_probe(1350);
assert_eq!(pmtud.pmtu, Some(1350));
assert!(!pmtud.should_probe());
// Restart PMTUD and expect the state to reset
pmtud.restart_pmtud();
// Run the PMTUD test runner with the reset state
pmtud_test_runner(&mut pmtud, 1237);
}
/// Test case for receiving a probe outside the defined supported MTU range.
#[test]
fn test_pmtud_errant_probe() {
let mut pmtud = Pmtud::new(MIN_CLIENT_INITIAL_LEN, 1350);
pmtud.successful_probe(1500);
// Even though we've received a probe larger than supported
// maximum MTU, the PMTU should still respect the configured maximum
assert_eq!(pmtud.pmtu, Some(1350));
assert!(!pmtud.should_probe());
pmtud.restart_pmtud();
// A failed probe of a value less than the minimum supported MTU
// should stop probing
pmtud.failed_probe(1100);
assert_eq!(pmtud.pmtu, None);
assert_eq!(pmtud.get_probe_size(), 1200);
assert!(!pmtud.should_probe());
}
/// Test case for PMTU equal to the minimum supported MTU.
///
/// This test verifies that the PMTU discovery process correctly identifies
/// when the PMTU is equal to the minimum supported MTU.
#[test]
fn test_pmtu_equal_to_min_supported_mtu() {
let mut pmtud = Pmtud::new(MIN_CLIENT_INITIAL_LEN, 1350);
pmtud_test_runner(&mut pmtud, 1200);
}
/// Test case for PMTU greater than the minimum supported MTU.
///
/// This test verifies that the PMTU discovery process correctly identifies
/// when the PMTU is greater than the minimum supported MTU.
#[test]
fn test_pmtu_greater_than_min_supported_mtu() {
let mut pmtud = Pmtud::new(MIN_CLIENT_INITIAL_LEN, 1350);
pmtud_test_runner(&mut pmtud, 1500);
}
/// Test case for PMTU less than the minimum supported MTU.
///
/// This test verifies that the PMTU discovery process correctly handles
/// the case when the PMTU is less than the minimum supported MTU.
#[test]
fn test_pmtu_less_than_min_supported_mtu() {
let mut pmtud = Pmtud::new(MIN_CLIENT_INITIAL_LEN, 1350);
pmtud_test_runner(&mut pmtud, 1100);
}
/// Test case for PMTU revalidation.
///
/// This test verifies that the PMTU recalculation logic correctly resets
/// the PMTUD state and identifies the correct PMTU after a failed
/// validation probe.
#[test]
fn test_pmtu_revalidation() {
let mut pmtud = Pmtud::new(MIN_CLIENT_INITIAL_LEN, 1350);
pmtud.set_probe_size(1350);
pmtud.successful_probe(1350);
// Simulate a case where an a probe of an established PMTU is dropped
pmtud.revalidate_pmtu();
pmtud.failed_probe(1350);
// Run the PMTUD test runner with the reset state
pmtud_test_runner(&mut pmtud, 1250);
}
/// Runs a test for the PMTUD algorithm, given a target PMTU `target_mtu`.
///
/// The test iteratively sends probes until the PMTU is found or the minimum
/// supported MTU is reached. Verifies that the PMTU is equal to the target
/// PMTU.
fn pmtud_test_runner(pmtud: &mut Pmtud, test_pmtu: usize) {
// Loop until the PMTU is found or the minimum supported MTU is reached
while pmtud.get_probe_size() >= pmtud.minimum_supported_mtu {
// Send a probe with the current probe size
let probe_size = pmtud.get_probe_size();
if probe_size <= test_pmtu {
pmtud.successful_probe(probe_size);
} else {
pmtud.failed_probe(probe_size);
}
// Update the probe size based on the result
pmtud.update_probe_size();
// If the probe size hasn't changed and is equal to the minimum
// supported MTU, break the loop
if pmtud.get_probe_size() == probe_size &&
probe_size == pmtud.minimum_supported_mtu
{
break;
}
// If the PMTU is found, break the loop
if pmtud.get_pmtu().is_some() {
break;
}
}
// Verify that the PMTU is correct
if test_pmtu < pmtud.minimum_supported_mtu {
assert_eq!(pmtud.get_pmtu(), None);
} else if test_pmtu > pmtud.maximum_supported_mtu {
assert_eq!(pmtud.get_pmtu(), Some(pmtud.maximum_supported_mtu));
} else {
assert_eq!(pmtud.get_pmtu(), Some(test_pmtu));
}
}
}