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}