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