s2n_quic_core/path/mtu.rs
1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::{
5 counter::{Counter, Saturating},
6 event::{self, builder::MtuUpdatedCause, IntoEvent},
7 frame, inet,
8 packet::number::PacketNumber,
9 path,
10 path::mtu,
11 recovery::{congestion_controller, CongestionController},
12 time::{timer, Timer, Timestamp},
13 transmission,
14};
15use core::{
16 fmt,
17 fmt::{Display, Formatter},
18 num::NonZeroU16,
19 time::Duration,
20};
21use s2n_codec::EncoderValue;
22
23#[cfg(test)]
24mod tests;
25
26#[cfg(any(test, feature = "testing"))]
27pub mod testing {
28 use super::*;
29 use crate::inet::{IpV4Address, SocketAddressV4};
30
31 /// Creates a new mtu::Controller with an IPv4 address and the given `max_mtu`
32 pub fn new_controller(max_mtu: u16) -> Controller {
33 let ip = IpV4Address::new([127, 0, 0, 1]);
34 let addr = inet::SocketAddress::IpV4(SocketAddressV4::new(ip, 443));
35 Controller::new(
36 Config {
37 max_mtu: max_mtu.try_into().unwrap(),
38 ..Default::default()
39 },
40 &addr,
41 )
42 }
43
44 /// Creates a new mtu::Controller with the given mtu and probed size
45 pub fn test_controller(mtu: u16, probed_size: u16) -> Controller {
46 let mut controller = new_controller(u16::MAX);
47 controller.plpmtu = mtu;
48 controller.probed_size = probed_size;
49 controller
50 }
51}
52
53#[derive(Clone, Debug, PartialEq, Eq)]
54enum State {
55 /// EARLY_SEARCH_REQUESTED indicates the initial MTU was configured higher
56 /// than the base MTU, to allow for quick confirmation or rejection of the
57 /// initial MTU
58 EarlySearchRequested,
59 //= https://www.rfc-editor.org/rfc/rfc8899#section-5.2
60 //# The DISABLED state is the initial state before probing has started.
61 Disabled,
62 /// SEARCH_REQUESTED is used to indicate a probe packet has been requested
63 /// to be transmitted, but has not been transmitted yet.
64 SearchRequested,
65 //= https://www.rfc-editor.org/rfc/rfc8899#section-5.2
66 //# The SEARCHING state is the main probing state.
67 Searching(PacketNumber, Timestamp),
68 //= https://www.rfc-editor.org/rfc/rfc8899#section-5.2
69 //# The SEARCH_COMPLETE state indicates that a search has completed.
70 SearchComplete,
71}
72
73impl State {
74 /// Returns true if the MTU controller is in the early search requested state
75 fn is_early_search_requested(&self) -> bool {
76 matches!(self, State::EarlySearchRequested)
77 }
78
79 /// Returns true if the MTU controller is in the disabled state
80 fn is_disabled(&self) -> bool {
81 matches!(self, State::Disabled)
82 }
83
84 /// Returns true if the MTU controller is in the search complete state
85 fn is_search_complete(&self) -> bool {
86 matches!(self, State::SearchComplete)
87 }
88}
89
90//= https://www.rfc-editor.org/rfc/rfc8899#section-5.1.2
91//# The MAX_PROBES is the maximum value of the PROBE_COUNT
92//# counter (see Section 5.1.3). MAX_PROBES represents the limit for
93//# the number of consecutive probe attempts of any size. Search
94//# algorithms benefit from a MAX_PROBES value greater than 1 because
95//# this can provide robustness to isolated packet loss. The default
96//# value of MAX_PROBES is 3.
97const MAX_PROBES: u8 = 3;
98
99/// The minimum length of the data field of a packet sent over an
100/// Ethernet is 1500 octets, thus the maximum length of an IP datagram
101/// sent over an Ethernet is 1500 octets.
102/// See https://www.rfc-editor.org/rfc/rfc894.txt
103const ETHERNET_MTU: u16 = 1500;
104
105/// If the next value to probe is within the PROBE_THRESHOLD bytes of
106/// the current Path MTU, probing will be considered complete.
107const PROBE_THRESHOLD: u16 = 20;
108
109/// When the black_hole_counter exceeds this threshold, on_black_hole_detected will be
110/// called to reduce the MTU to the BASE_PLPMTU. The black_hole_counter is incremented when
111/// a burst of consecutive packets is lost that starts with a packet that is:
112/// 1) not an MTU probe
113/// 2) larger than the BASE_PLPMTU
114/// 3) sent after the largest MTU-sized acknowledged packet number
115/// This is a possible indication that the path cannot support the MTU that was previously confirmed.
116const BLACK_HOLE_THRESHOLD: u8 = 3;
117
118/// After a black hole has been detected, the mtu::Controller will wait this duration
119/// before probing for a larger MTU again.
120const BLACK_HOLE_COOL_OFF_DURATION: Duration = Duration::from_secs(60);
121
122//= https://www.rfc-editor.org/rfc/rfc8899#section-5.1.1
123//# The PMTU_RAISE_TIMER is configured to the period a
124//# sender will continue to use the current PLPMTU, after which it
125//# reenters the Search Phase. This timer has a period of 600
126//# seconds, as recommended by PLPMTUD [RFC4821].
127const PMTU_RAISE_TIMER_DURATION: Duration = Duration::from_secs(600);
128
129//= https://www.rfc-editor.org/rfc/rfc9000#section-14
130//# QUIC MUST NOT be used if the network path cannot support a
131//# maximum datagram size of at least 1200 bytes.
132
133//= https://www.rfc-editor.org/rfc/rfc8899#section-5.1.2
134//# When using IPv4, there is no currently equivalent size specified,
135//# and a default BASE_PLPMTU of 1200 bytes is RECOMMENDED.
136pub const MINIMUM_MAX_DATAGRAM_SIZE: u16 = 1200;
137
138// Length is the length in octets of this user datagram including this
139// header and the data. (This means the minimum value of the length is
140// eight.)
141// See https://www.rfc-editor.org/rfc/rfc768.txt
142const UDP_HEADER_LEN: u16 = 8;
143
144// IPv4 header ranges from 20-60 bytes, depending on Options
145const IPV4_MIN_HEADER_LEN: u16 = 20;
146// IPv6 header is always 40 bytes, plus extensions
147const IPV6_MIN_HEADER_LEN: u16 = 40;
148
149// The minimum allowed Max MTU is the minimum UDP datagram size of 1200 bytes plus
150// the UDP header length and minimal IP header length
151const fn const_min(a: u16, b: u16) -> u16 {
152 if a < b {
153 a
154 } else {
155 b
156 }
157}
158
159const MINIMUM_MTU: u16 = MINIMUM_MAX_DATAGRAM_SIZE
160 + UDP_HEADER_LEN
161 + const_min(IPV4_MIN_HEADER_LEN, IPV6_MIN_HEADER_LEN);
162
163macro_rules! impl_mtu {
164 ($name:ident, $default:expr) => {
165 #[derive(Clone, Copy, Debug, PartialEq)]
166 pub struct $name(NonZeroU16);
167
168 impl $name {
169 /// The minimum value required for path MTU
170 pub const MIN: Self = Self(unsafe { NonZeroU16::new_unchecked(MINIMUM_MTU) });
171
172 /// The largest size of a QUIC datagram that can be sent on a path that supports this
173 /// MTU. This does not include the size of UDP and IP headers.
174 #[inline]
175 pub fn max_datagram_size(&self, peer_socket_address: &inet::SocketAddress) -> u16 {
176 let min_ip_header_len = match peer_socket_address {
177 inet::SocketAddress::IpV4(_) => IPV4_MIN_HEADER_LEN,
178 inet::SocketAddress::IpV6(_) => IPV6_MIN_HEADER_LEN,
179 };
180 (u16::from(*self) - UDP_HEADER_LEN - min_ip_header_len)
181 .max(MINIMUM_MAX_DATAGRAM_SIZE)
182 }
183 }
184
185 impl Default for $name {
186 #[inline]
187 fn default() -> Self {
188 $default
189 }
190 }
191
192 impl TryFrom<u16> for $name {
193 type Error = MtuError;
194
195 fn try_from(value: u16) -> Result<Self, Self::Error> {
196 if value < MINIMUM_MTU {
197 return Err(MtuError);
198 }
199
200 Ok($name(value.try_into().expect(
201 "Value must be greater than zero according to the check above",
202 )))
203 }
204 }
205
206 impl From<$name> for usize {
207 #[inline]
208 fn from(value: $name) -> Self {
209 value.0.get() as usize
210 }
211 }
212
213 impl From<$name> for u16 {
214 #[inline]
215 fn from(value: $name) -> Self {
216 value.0.get()
217 }
218 }
219 };
220}
221
222// Safety: 1500 and MINIMUM_MTU are greater than zero
223const DEFAULT_MAX_MTU: MaxMtu = MaxMtu(NonZeroU16::new(1500).unwrap());
224const DEFAULT_BASE_MTU: BaseMtu = BaseMtu(NonZeroU16::new(MINIMUM_MTU).unwrap());
225const DEFAULT_INITIAL_MTU: InitialMtu = InitialMtu(NonZeroU16::new(MINIMUM_MTU).unwrap());
226
227impl_mtu!(MaxMtu, DEFAULT_MAX_MTU);
228impl_mtu!(InitialMtu, DEFAULT_INITIAL_MTU);
229impl_mtu!(BaseMtu, DEFAULT_BASE_MTU);
230
231#[derive(Debug, Eq, PartialEq)]
232pub struct MtuError;
233
234impl Display for MtuError {
235 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
236 write!(
237 f,
238 "MTU must have {} <= base_mtu (default: {}) <= initial_mtu (default: {}) <= max_mtu (default: {})",
239 MINIMUM_MTU, DEFAULT_BASE_MTU.0, DEFAULT_INITIAL_MTU.0, DEFAULT_MAX_MTU.0
240 )
241 }
242}
243
244impl core::error::Error for MtuError {}
245
246/// Information about the path that may be used when generating MTU configuration.
247#[non_exhaustive]
248pub struct PathInfo<'a> {
249 pub remote_address: event::api::SocketAddress<'a>,
250}
251
252impl<'a> PathInfo<'a> {
253 #[inline]
254 #[doc(hidden)]
255 pub fn new(remote_address: &'a inet::SocketAddress) -> Self {
256 PathInfo {
257 remote_address: remote_address.into_event(),
258 }
259 }
260}
261
262/// MTU configuration manager.
263#[derive(Debug)]
264pub struct Manager<E: mtu::Endpoint> {
265 provider: E,
266 endpoint_mtu_config: Config,
267}
268
269impl<E: mtu::Endpoint> Manager<E> {
270 pub fn new(provider: E) -> Self {
271 Manager {
272 provider,
273 // Instantiate the Manager with default values since the endpoint is
274 // created before the IO provider (IO provider sets the actual config `set_mtu_config()`).
275 endpoint_mtu_config: Default::default(),
276 }
277 }
278
279 pub fn config(&mut self, remote_address: &inet::SocketAddress) -> Result<Config, MtuError> {
280 let info = mtu::PathInfo::new(remote_address);
281 if let Some(conn_config) = self.provider.on_path(&info, self.endpoint_mtu_config) {
282 ensure!(conn_config.is_valid(), Err(MtuError));
283 ensure!(
284 u16::from(conn_config.max_mtu) <= u16::from(self.endpoint_mtu_config.max_mtu()),
285 Err(MtuError)
286 );
287
288 Ok(conn_config)
289 } else {
290 Ok(self.endpoint_mtu_config)
291 }
292 }
293
294 pub fn set_endpoint_config(&mut self, config: Config) {
295 self.endpoint_mtu_config = config;
296 }
297
298 pub fn endpoint_config(&self) -> &Config {
299 &self.endpoint_mtu_config
300 }
301}
302
303/// Specify MTU configuration for the given path.
304pub trait Endpoint: 'static + Send {
305 /// Provide path specific MTU config.
306 ///
307 /// The MTU provider is invoked for each new path established during a
308 /// connection. Returning `None` means that the path should inherit
309 /// the endpoint configured values.
310 ///
311 /// Application must ensure that `max_mtu <= endpoint_mtu_config.max_mtu()`.
312 fn on_path(&mut self, info: &mtu::PathInfo, endpoint_mtu_config: Config)
313 -> Option<mtu::Config>;
314}
315
316/// Inherit the endpoint configured values.
317#[derive(Debug, Default)]
318pub struct Inherit {}
319
320impl Endpoint for Inherit {
321 fn on_path(
322 &mut self,
323 _info: &mtu::PathInfo,
324 _endpoint_mtu_config: Config,
325 ) -> Option<mtu::Config> {
326 None
327 }
328}
329
330/// MTU configuration.
331#[derive(Copy, Clone, Debug, Default)]
332pub struct Config {
333 initial_mtu: InitialMtu,
334 base_mtu: BaseMtu,
335 max_mtu: MaxMtu,
336}
337
338impl Endpoint for Config {
339 fn on_path(
340 &mut self,
341 _info: &mtu::PathInfo,
342 _endpoint_mtu_config: Config,
343 ) -> Option<mtu::Config> {
344 Some(*self)
345 }
346}
347
348impl Config {
349 pub const MIN: Self = Self {
350 initial_mtu: InitialMtu::MIN,
351 base_mtu: BaseMtu::MIN,
352 max_mtu: MaxMtu::MIN,
353 };
354
355 pub fn builder() -> Builder {
356 Builder::default()
357 }
358
359 /// The maximum transmission unit (MTU) to use when initiating a connection.
360 pub fn initial_mtu(&self) -> InitialMtu {
361 self.initial_mtu
362 }
363
364 /// The smallest maximum transmission unit (MTU) to use when transmitting.
365 pub fn base_mtu(&self) -> BaseMtu {
366 self.base_mtu
367 }
368
369 /// The largest maximum transmission unit (MTU) that can be sent on a path.
370 pub fn max_mtu(&self) -> MaxMtu {
371 self.max_mtu
372 }
373
374 /// Returns true if the MTU configuration is valid
375 ///
376 /// A valid MTU configuration must have base_mtu <= initial_mtu <= max_mtu
377 #[inline]
378 pub fn is_valid(&self) -> bool {
379 self.base_mtu.0 <= self.initial_mtu.0 && self.initial_mtu.0 <= self.max_mtu.0
380 }
381}
382
383#[derive(Debug, Default)]
384pub struct Builder {
385 initial_mtu: Option<InitialMtu>,
386 base_mtu: Option<BaseMtu>,
387 max_mtu: Option<MaxMtu>,
388}
389
390impl Builder {
391 /// Sets the maximum transmission unit (MTU) to use when initiating a connection (default: 1228)
392 ///
393 /// For a detailed description see the [with_initial_mtu] documentation in the IO provider.
394 ///
395 /// [with_initial_mtu]: https://docs.rs/s2n-quic/latest/s2n_quic/provider/io/tokio/struct.Builder.html#method.with_initial_mtu
396 pub fn with_initial_mtu(mut self, initial_mtu: u16) -> Result<Self, MtuError> {
397 if let Some(base_mtu) = self.base_mtu {
398 ensure!(initial_mtu >= base_mtu.0.get(), Err(MtuError));
399 }
400
401 if let Some(max_mtu) = self.max_mtu {
402 ensure!(initial_mtu <= max_mtu.0.get(), Err(MtuError));
403 }
404
405 self.initial_mtu = Some(initial_mtu.try_into()?);
406 Ok(self)
407 }
408
409 /// Sets the largest maximum transmission unit (MTU) that can be sent on a path (default: 1500)
410 ///
411 /// For a detailed description see the [with_base_mtu] documentation in the IO provider.
412 ///
413 /// [with_base_mtu]: https://docs.rs/s2n-quic/latest/s2n_quic/provider/io/tokio/struct.Builder.html#method.with_base_mtu
414 pub fn with_base_mtu(mut self, base_mtu: u16) -> Result<Self, MtuError> {
415 if let Some(initial_mtu) = self.initial_mtu {
416 ensure!(initial_mtu.0.get() >= base_mtu, Err(MtuError));
417 }
418
419 if let Some(max_mtu) = self.max_mtu {
420 ensure!(base_mtu <= max_mtu.0.get(), Err(MtuError));
421 }
422
423 self.base_mtu = Some(base_mtu.try_into()?);
424 Ok(self)
425 }
426
427 /// Sets the largest maximum transmission unit (MTU) that can be sent on a path (default: 1500)
428 ///
429 /// Application must ensure that max_mtu <= endpoint_mtu_config.max_mtu(). For a detailed
430 /// description see the [with_max_mtu] documentation in the IO provider.
431 ///
432 /// [with_max_mtu]: https://docs.rs/s2n-quic/latest/s2n_quic/provider/io/tokio/struct.Builder.html#method.with_max_mtu
433 pub fn with_max_mtu(mut self, max_mtu: u16) -> Result<Self, MtuError> {
434 if let Some(initial_mtu) = self.initial_mtu {
435 ensure!(initial_mtu.0.get() <= max_mtu, Err(MtuError));
436 }
437
438 if let Some(base_mtu) = self.base_mtu {
439 ensure!(base_mtu.0.get() <= max_mtu, Err(MtuError));
440 }
441
442 self.max_mtu = Some(max_mtu.try_into()?);
443 Ok(self)
444 }
445
446 pub fn build(self) -> Result<Config, MtuError> {
447 let base_mtu = self.base_mtu.unwrap_or_default();
448 let max_mtu = self.max_mtu.unwrap_or_default();
449 let mut initial_mtu = self.initial_mtu.unwrap_or_default();
450
451 if self.initial_mtu.is_none() {
452 // The initial_mtu was not configured, so adjust the value from the default
453 // based on the default or configured base and max MTUs
454 initial_mtu = initial_mtu
455 .0
456 .max(base_mtu.0)
457 .min(max_mtu.0)
458 .get()
459 .try_into()?
460 };
461
462 let config = Config {
463 initial_mtu,
464 max_mtu,
465 base_mtu,
466 };
467
468 ensure!(config.is_valid(), Err(MtuError));
469 Ok(config)
470 }
471}
472
473#[derive(Eq, PartialEq, Debug)]
474pub enum MtuResult {
475 NoChange,
476 MtuUpdated(u16),
477}
478
479#[derive(Clone, Debug)]
480pub struct Controller {
481 state: State,
482 //= https://www.rfc-editor.org/rfc/rfc8899#section-5.1.2
483 //# The BASE_PLPMTU is a configured size expected to work for most paths.
484 //# The size is equal to or larger than the MIN_PLPMTU and smaller than
485 //# the MAX_PLPMTU.
486 base_plpmtu: u16,
487 //= https://www.rfc-editor.org/rfc/rfc8899#section-2
488 //# The Packetization Layer PMTU is an estimate of the largest size
489 //# of PL datagram that can be sent by a path, controlled by PLPMTUD
490 plpmtu: u16,
491 /// The maximum size the UDP payload can reach for any probe packet.
492 max_udp_payload: u16,
493 //= https://www.rfc-editor.org/rfc/rfc8899#section-5.1.3
494 //# The PROBED_SIZE is the size of the current probe packet
495 //# as determined at the PL. This is a tentative value for the
496 //# PLPMTU, which is awaiting confirmation by an acknowledgment.
497 probed_size: u16,
498 /// The maximum size datagram to probe for. In contrast to the max_udp_payload,
499 /// this value will decrease if probes are not acknowledged.
500 max_probe_size: u16,
501 //= https://www.rfc-editor.org/rfc/rfc8899#section-5.1.3
502 //# The PROBE_COUNT is a count of the number of successive
503 //# unsuccessful probe packets that have been sent.
504 probe_count: u8,
505 /// A count of the number of packets with a size > base_plpmtu lost since
506 /// the last time a packet with size equal to the current MTU was acknowledged.
507 black_hole_counter: Counter<u8, Saturating>,
508 /// The largest acknowledged packet with size >= the plpmtu. Used when tracking
509 /// packets that have been lost for the purpose of detecting a black hole.
510 largest_acked_mtu_sized_packet: Option<PacketNumber>,
511 //= https://www.rfc-editor.org/rfc/rfc8899#section-5.1.1
512 //# The PMTU_RAISE_TIMER is configured to the period a
513 //# sender will continue to use the current PLPMTU, after which it
514 //# reenters the Search Phase.
515 pmtu_raise_timer: Timer,
516}
517
518impl Controller {
519 /// Construct a new mtu::Controller with the given `max_mtu` and `peer_socket_address`
520 ///
521 /// The UDP header length and IP header length will be subtracted from `max_mtu` to
522 /// determine the max_udp_payload used for limiting the payload length of probe packets.
523 /// max_mtu is the maximum allowed mtu, e.g. for jumbo frames this value is expected to
524 /// be over 9000.
525 #[inline]
526 pub fn new(config: Config, peer_socket_address: &inet::SocketAddress) -> Self {
527 debug_assert!(config.is_valid(), "Invalid MTU configuration {config:?}");
528
529 //= https://www.rfc-editor.org/rfc/rfc9000#section-14.3
530 //# Endpoints SHOULD set the initial value of BASE_PLPMTU (Section 5.1 of
531 //# [DPLPMTUD]) to be consistent with QUIC's smallest allowed maximum
532 //# datagram size.
533 let base_plpmtu = config.base_mtu.max_datagram_size(peer_socket_address);
534 let max_udp_payload = config.max_mtu.max_datagram_size(peer_socket_address);
535
536 //= https://www.rfc-editor.org/rfc/rfc9000#section-14.1
537 //# Datagrams containing Initial packets MAY exceed 1200 bytes if the sender
538 //# believes that the network path and peer both support the size that it chooses.
539 let plpmtu = config.initial_mtu.max_datagram_size(peer_socket_address);
540
541 let initial_probed_size = if u16::from(config.initial_mtu) > ETHERNET_MTU - PROBE_THRESHOLD
542 {
543 // An initial MTU was provided within the probe threshold of the Ethernet MTU, so we can
544 // instead try probing for an MTU larger than the Ethernet MTU
545 Self::next_probe_size(plpmtu, max_udp_payload)
546 } else {
547 // The UDP payload size for the most likely MTU is based on standard Ethernet MTU minus
548 // the minimum length IP headers (without IPv4 options or IPv6 extensions) and UPD header
549 let min_ip_header_len = match peer_socket_address {
550 inet::SocketAddress::IpV4(_) => IPV4_MIN_HEADER_LEN,
551 inet::SocketAddress::IpV6(_) => IPV6_MIN_HEADER_LEN,
552 };
553 ETHERNET_MTU - UDP_HEADER_LEN - min_ip_header_len
554 }
555 .min(max_udp_payload);
556
557 let state = if plpmtu > base_plpmtu {
558 // The initial MTU has been configured higher than the base MTU
559 State::EarlySearchRequested
560 } else if initial_probed_size - base_plpmtu < PROBE_THRESHOLD {
561 // The next probe size is within the probe threshold of the
562 // base MTU, so no probing will occur and the search is complete
563 State::SearchComplete
564 } else {
565 // Otherwise wait for regular MTU probing to be enabled
566 State::Disabled
567 };
568
569 Self {
570 state,
571 base_plpmtu,
572 plpmtu,
573 probed_size: initial_probed_size,
574 max_udp_payload,
575 max_probe_size: max_udp_payload,
576 probe_count: 0,
577 black_hole_counter: Default::default(),
578 largest_acked_mtu_sized_packet: None,
579 pmtu_raise_timer: Timer::default(),
580 }
581 }
582
583 /// Enable path MTU probing
584 #[inline]
585 pub fn enable(&mut self) {
586 // ensure we haven't already enabled the controller
587 ensure!(self.state.is_disabled() || self.state.is_early_search_requested());
588
589 // TODO: Look up current MTU in a cache. If there is a cache hit
590 // move directly to SearchComplete and arm the PMTU raise timer.
591 // Otherwise, start searching for a larger PMTU immediately
592 self.request_new_search(None);
593 }
594
595 /// Called when the connection timer expires
596 #[inline]
597 pub fn on_timeout(&mut self, now: Timestamp) {
598 ensure!(self.pmtu_raise_timer.poll_expiration(now).is_ready());
599 self.request_new_search(None);
600 }
601
602 //= https://www.rfc-editor.org/rfc/rfc8899#section-4.2
603 //# When
604 //# supported, this mechanism MAY also be used by DPLPMTUD to acknowledge
605 //# reception of a probe packet.
606 /// This method gets called when a packet delivery got acknowledged
607 #[inline]
608 pub fn on_packet_ack<CC: CongestionController, Pub: event::ConnectionPublisher>(
609 &mut self,
610 packet_number: PacketNumber,
611 sent_bytes: u16,
612 congestion_controller: &mut CC,
613 path_id: path::Id,
614 publisher: &mut Pub,
615 ) -> MtuResult {
616 if self.state.is_early_search_requested() && sent_bytes > self.base_plpmtu {
617 if self.is_next_probe_size_above_threshold() {
618 // Early probing has succeeded, but the max MTU is higher still so
619 // wait for regular MTU probing to be enabled to attempt higher MTUs
620 self.state = State::Disabled;
621 } else {
622 self.state = State::SearchComplete;
623 }
624
625 // Publish an `on_mtu_updated` event since the cause
626 // and possibly search_complete status have changed
627 publisher.on_mtu_updated(event::builder::MtuUpdated {
628 path_id: path_id.into_event(),
629 mtu: self.plpmtu,
630 cause: MtuUpdatedCause::InitialMtuPacketAcknowledged,
631 search_complete: self.state.is_search_complete(),
632 });
633 }
634
635 // no need to process anything in the disabled state
636 ensure!(self.state != State::Disabled, MtuResult::NoChange);
637
638 // MTU probes are only sent in application data space
639 ensure!(
640 packet_number.space().is_application_data(),
641 MtuResult::NoChange
642 );
643
644 if sent_bytes >= self.plpmtu
645 && self
646 .largest_acked_mtu_sized_packet
647 .is_none_or(|pn| packet_number > pn)
648 {
649 // Reset the black hole counter since a packet the size of the current MTU or larger
650 // has been acknowledged, indicating the path can still support the current MTU
651 self.black_hole_counter = Default::default();
652 self.largest_acked_mtu_sized_packet = Some(packet_number);
653 }
654
655 if let State::Searching(probe_packet_number, transmit_time) = self.state {
656 if packet_number == probe_packet_number {
657 self.plpmtu = self.probed_size;
658 // A new MTU has been confirmed, notify the congestion controller
659 congestion_controller.on_mtu_update(
660 self.plpmtu,
661 &mut congestion_controller::PathPublisher::new(publisher, path_id),
662 );
663
664 self.update_probed_size();
665
666 //= https://www.rfc-editor.org/rfc/rfc8899#section-8
667 //# To avoid excessive load, the interval between individual probe
668 //# packets MUST be at least one RTT, and the interval between rounds of
669 //# probing is determined by the PMTU_RAISE_TIMER.
670
671 // Subsequent probe packets are sent based on the round trip transmission and
672 // acknowledgement/loss of a packet, so the interval will be at least 1 RTT.
673 self.request_new_search(Some(transmit_time));
674
675 publisher.on_mtu_updated(event::builder::MtuUpdated {
676 path_id: path_id.into_event(),
677 mtu: self.plpmtu,
678 cause: MtuUpdatedCause::ProbeAcknowledged,
679 search_complete: self.state.is_search_complete(),
680 });
681
682 return MtuResult::MtuUpdated(self.plpmtu);
683 }
684 }
685
686 MtuResult::NoChange
687 }
688
689 //= https://www.rfc-editor.org/rfc/rfc8899#section-3
690 //# The PL is REQUIRED to be
691 //# robust in the case where probe packets are lost due to other
692 //# reasons (including link transmission error, congestion).
693 /// This method gets called when a packet loss is reported
694 #[inline]
695 pub fn on_packet_loss<CC: CongestionController, Pub: event::ConnectionPublisher>(
696 &mut self,
697 packet_number: PacketNumber,
698 lost_bytes: u16,
699 new_loss_burst: bool,
700 now: Timestamp,
701 congestion_controller: &mut CC,
702 path_id: path::Id,
703 publisher: &mut Pub,
704 ) -> MtuResult {
705 // MTU probes are only sent in the application data space, but since early packet
706 // spaces will use the `InitialMtu` prior to MTU probing being enabled, we need
707 // to check for potentially MTU-related packet loss if an early search has been requested
708 ensure!(
709 self.state.is_early_search_requested() || packet_number.space().is_application_data(),
710 MtuResult::NoChange
711 );
712
713 match &self.state {
714 State::Disabled => {}
715 State::EarlySearchRequested => {
716 // MTU probing hasn't been enabled yet, but since the initial MTU was configured
717 // higher than the base PLPMTU and this setting resulted in a lost packet
718 // we drop back down to the base PLPMTU.
719 self.plpmtu = self.base_plpmtu;
720
721 congestion_controller.on_mtu_update(
722 self.plpmtu,
723 &mut congestion_controller::PathPublisher::new(publisher, path_id),
724 );
725
726 if self.is_next_probe_size_above_threshold() {
727 // Resume regular probing when the MTU controller is enabled
728 self.state = State::Disabled;
729 } else {
730 // The next probe is within the threshold, so move directly
731 // to the SearchComplete state
732 self.state = State::SearchComplete;
733 }
734
735 publisher.on_mtu_updated(event::builder::MtuUpdated {
736 path_id: path_id.into_event(),
737 mtu: self.plpmtu,
738 cause: MtuUpdatedCause::InitialMtuPacketLost,
739 search_complete: self.state.is_search_complete(),
740 });
741
742 return MtuResult::MtuUpdated(self.plpmtu);
743 }
744 State::Searching(probe_pn, _) if *probe_pn == packet_number => {
745 // The MTU probe was lost
746 if self.probe_count == MAX_PROBES {
747 // We've sent MAX_PROBES without acknowledgement, so
748 // attempt a smaller probe size
749 self.max_probe_size = self.probed_size;
750 self.update_probed_size();
751 self.request_new_search(None);
752
753 if self.is_search_completed() {
754 // Emit an on_mtu_updated event as the search has now completed
755 publisher.on_mtu_updated(event::builder::MtuUpdated {
756 path_id: path_id.into_event(),
757 mtu: self.plpmtu,
758 cause: MtuUpdatedCause::LargerProbesLost,
759 search_complete: true,
760 })
761 }
762 } else {
763 // Try the same probe size again
764 self.state = State::SearchRequested
765 }
766 }
767 State::Searching(_, _) | State::SearchComplete | State::SearchRequested => {
768 if (self.base_plpmtu + 1..=self.plpmtu).contains(&lost_bytes)
769 && self
770 .largest_acked_mtu_sized_packet
771 .is_none_or(|pn| packet_number > pn)
772 && new_loss_burst
773 {
774 // A non-probe packet larger than the BASE_PLPMTU that was sent after the last
775 // acknowledged MTU-sized packet has been lost
776 self.black_hole_counter += 1;
777 }
778
779 if self.black_hole_counter > BLACK_HOLE_THRESHOLD {
780 return self.on_black_hole_detected(
781 now,
782 congestion_controller,
783 path_id,
784 publisher,
785 );
786 }
787 }
788 }
789
790 MtuResult::NoChange
791 }
792
793 /// Gets the currently validated maximum QUIC datagram size
794 ///
795 /// This does not include the size of UDP and IP headers.
796 #[inline]
797 pub fn max_datagram_size(&self) -> usize {
798 self.plpmtu as usize
799 }
800
801 /// Gets the max datagram size currently being probed for
802 #[inline]
803 pub fn probed_sized(&self) -> usize {
804 self.probed_size as usize
805 }
806
807 /// Returns true if probing for the MTU has completed
808 pub fn is_search_completed(&self) -> bool {
809 self.state.is_search_complete()
810 }
811
812 /// Sets `probed_size` to the next MTU size to probe for based on a binary search
813 #[inline]
814 fn update_probed_size(&mut self) {
815 //= https://www.rfc-editor.org/rfc/rfc8899#section-5.3.2
816 //# Implementations SHOULD select the set of probe packet sizes to
817 //# maximize the gain in PLPMTU from each search step.
818 self.probed_size = Self::next_probe_size(self.plpmtu, self.max_probe_size);
819 }
820
821 /// Calculates the next probe size as halfway from the current to the max size
822 #[inline]
823 fn next_probe_size(current: u16, max: u16) -> u16 {
824 current + ((max - current) / 2)
825 }
826
827 #[inline]
828 fn is_next_probe_size_above_threshold(&self) -> bool {
829 self.probed_size - self.plpmtu >= PROBE_THRESHOLD
830 }
831
832 /// Requests a new search to be initiated
833 ///
834 /// If `last_probe_time` is supplied, the PMTU Raise Timer will be armed as
835 /// necessary if the probed_size is already within the PROBE_THRESHOLD
836 /// of the current PLPMTU
837 #[inline]
838 fn request_new_search(&mut self, last_probe_time: Option<Timestamp>) {
839 if self.is_next_probe_size_above_threshold() {
840 self.probe_count = 0;
841 self.state = State::SearchRequested;
842 } else {
843 // The next probe size is within the threshold of the current MTU
844 // so its not worth additional probing.
845 self.state = State::SearchComplete;
846
847 if let Some(last_probe_time) = last_probe_time {
848 self.arm_pmtu_raise_timer(last_probe_time + PMTU_RAISE_TIMER_DURATION);
849 }
850 }
851 }
852
853 /// Called when an excessive number of packets larger than the BASE_PLPMTU have been lost
854 #[inline]
855 fn on_black_hole_detected<CC: CongestionController, Pub: event::ConnectionPublisher>(
856 &mut self,
857 now: Timestamp,
858 congestion_controller: &mut CC,
859 path_id: path::Id,
860 publisher: &mut Pub,
861 ) -> MtuResult {
862 self.black_hole_counter = Default::default();
863 self.largest_acked_mtu_sized_packet = None;
864 // Reset the plpmtu back to the base_plpmtu and notify the congestion controller
865 self.plpmtu = self.base_plpmtu;
866 congestion_controller.on_mtu_update(
867 self.plpmtu,
868 &mut congestion_controller::PathPublisher::new(publisher, path_id),
869 );
870 // Cancel any current probes
871 self.state = State::SearchComplete;
872 // Arm the PMTU raise timer to try a larger MTU again after a cooling off period
873 self.arm_pmtu_raise_timer(now + BLACK_HOLE_COOL_OFF_DURATION);
874
875 publisher.on_mtu_updated(event::builder::MtuUpdated {
876 path_id: path_id.into_event(),
877 mtu: self.plpmtu,
878 cause: MtuUpdatedCause::Blackhole,
879 search_complete: self.state.is_search_complete(),
880 });
881
882 MtuResult::MtuUpdated(self.plpmtu)
883 }
884
885 /// Arm the PMTU Raise Timer if there is still room to increase the
886 /// MTU before hitting the max plpmtu
887 #[inline]
888 fn arm_pmtu_raise_timer(&mut self, timestamp: Timestamp) {
889 // Reset the max_probe_size to the max_udp_payload to allow for larger probe sizes
890 self.max_probe_size = self.max_udp_payload;
891 self.update_probed_size();
892
893 if self.is_next_probe_size_above_threshold() {
894 // There is still some room to try a larger MTU again,
895 // so arm the pmtu raise timer
896 self.pmtu_raise_timer.set(timestamp);
897 }
898 }
899}
900
901impl timer::Provider for Controller {
902 #[inline]
903 fn timers<Q: timer::Query>(&self, query: &mut Q) -> timer::Result {
904 self.pmtu_raise_timer.timers(query)?;
905
906 Ok(())
907 }
908}
909
910impl transmission::Provider for Controller {
911 /// Queries the component for any outgoing frames that need to get sent
912 ///
913 /// This method assumes that no other data (other than the packet header) has been written
914 /// to the supplied `WriteContext`. This necessitates the caller ensuring the probe packet
915 /// written by this method to be in its own connection transmission.
916 #[inline]
917 fn on_transmit<W: transmission::Writer>(&mut self, context: &mut W) {
918 //= https://www.rfc-editor.org/rfc/rfc8899#section-5.2
919 //# When used with an acknowledged PL (e.g., SCTP), DPLPMTUD SHOULD NOT continue to
920 //# generate PLPMTU probes in this state.
921 ensure!(self.state == State::SearchRequested);
922
923 ensure!(context.transmission_mode().is_mtu_probing());
924
925 // Each packet contains overhead in the form of a packet header and an authentication tag.
926 // This overhead contributes to the overall size of the packet, so the payload we write
927 // to the packet will account for this overhead to reach the target probed size.
928 let probe_payload_size =
929 self.probed_size as usize - context.header_len() - context.tag_len();
930
931 if context.remaining_capacity() < probe_payload_size {
932 // There isn't enough capacity in the buffer to write the datagram we
933 // want to probe, so we've reached the maximum pmtu and the search is complete.
934 self.state = State::SearchComplete;
935 return;
936 }
937
938 //= https://www.rfc-editor.org/rfc/rfc9000#section-14.4
939 //# Endpoints could limit the content of PMTU probes to PING and PADDING
940 //# frames, since packets that are larger than the current maximum
941 //# datagram size are more likely to be dropped by the network.
942
943 //= https://www.rfc-editor.org/rfc/rfc8899#section-3
944 //# Probe loss recovery: It is RECOMMENDED to use probe packets that
945 //# do not carry any user data that would require retransmission if
946 //# lost.
947
948 //= https://www.rfc-editor.org/rfc/rfc8899#section-4.1
949 //# DPLPMTUD MAY choose to use only one of these methods to simplify the
950 //# implementation.
951
952 context.write_frame(&frame::Ping);
953 let padding_size = probe_payload_size - frame::Ping.encoding_size();
954 if let Some(packet_number) = context.write_frame(&frame::Padding {
955 length: padding_size,
956 }) {
957 self.probe_count += 1;
958 self.state = State::Searching(packet_number, context.current_time());
959 }
960 }
961}
962
963impl transmission::interest::Provider for Controller {
964 #[inline]
965 fn transmission_interest<Q: transmission::interest::Query>(
966 &self,
967 query: &mut Q,
968 ) -> transmission::interest::Result {
969 match self.state {
970 State::SearchRequested => query.on_new_data(),
971 _ => Ok(()),
972 }
973 }
974}