// Values <= 1500 based on: A. Custura, G. Fairhurst and I. Learmonth, "Exploring Usable Path MTU in // the Internet," 2018 Network Traffic Measurement and Analysis Conference (TMA), Vienna, Austria, // 2018, pp. 1-8, doi: 10.23919/TMA.2018.8506538. keywords: // {Servers;Probes;Tools;Clamps;Middleboxes;Standards}, const MTU_SIZES_V4: &[usize] = &[ 1280, 1380, 1420, 1472, 1500, 2047, 4095, 8191, 16383, 32767, 65535,
]; const MTU_SIZES_V6: &[usize] = &[ 1280, 1380, 1420, // 1420 is not in the paper for v6, but adding it makes the arrays the same length 1470, 1500, 2047, 4095, 8191, 16383, 32767, 65535,
];
const_assert!(MTU_SIZES_V4.len() == MTU_SIZES_V6.len()); const SEARCH_TABLE_LEN: usize = MTU_SIZES_V4.len();
#[derive(Debug)] pubstruct Pmtud {
search_table: &'static [usize],
header_size: usize,
mtu: usize,
iface_mtu: usize, /// The peer's [`max_udp_payload_size`](https://www.rfc-editor.org/rfc/rfc9000#section-18.2) /// transport parameter, i.e., the maximum UDP payload (not including IP and UDP headers) /// the peer is willing to receive.
peer_max_udp_payload: Option<usize>,
probe_index: usize,
probe_count: usize,
probe_state: Probe,
raise_timer: Option<Instant>,
qlog: Qlog,
}
impl Pmtud { /// Returns the MTU search table for the given remote IP address family. constfn search_table(remote_ip: IpAddr) -> &'static [usize] { match remote_ip {
IpAddr::V4(_) => MTU_SIZES_V4,
IpAddr::V6(_) => MTU_SIZES_V6,
}
}
/// Size of the IPv4/IPv6 and UDP headers, in bytes. #[must_use] pubconstfn header_size(remote_ip: IpAddr) -> usize { match remote_ip {
IpAddr::V4(_) => 20 + 8,
IpAddr::V6(_) => 40 + 8,
}
}
fn set_mtu(&mutself, idx: usize, stats: &mut Stats, now: Instant) { let old_mtu = self.plpmtu(); self.mtu = self.search_table[idx];
stats.pmtud_pmtu = self.mtu; let new_mtu = self.plpmtu(); if old_mtu != new_mtu { let done = !self.needs_probe();
qlog::mtu_updated(&mutself.qlog, old_mtu, new_mtu, done, now);
}
}
/// Set the peer's `max_udp_payload_size` transport parameter as an upper bound for probing. pubconstfn set_peer_max_udp_payload(&mutself, peer_max_udp_payload: usize) { self.peer_max_udp_payload = Some(peer_max_udp_payload);
}
/// Returns the peer's `max_udp_payload_size`, if known. #[must_use] pubconstfn peer_max_udp_payload(&self) -> Option<usize> { self.peer_max_udp_payload
}
/// Checks whether the PMTUD raise timer should be fired, and does so if needed. pubfn maybe_fire_raise_timer(&mutself, now: Instant, stats: &n style='color:red'>mut Stats) { ifself.probe_state == Probe::NotNeeded && self.raise_timer.is_some_and(|t| now >= t) {
qdebug!("PMTUD raise timer fired"); self.raise_timer = None; self.next(now, stats);
}
}
/// Returns the current Packetization Layer Path MTU, i.e., the maximum UDP payload that can be /// sent. During probing, this may be larger than the actual path MTU. #[must_use] pubconstfn plpmtu(&self) -> usize { self.mtu - self.header_size
}
/// Returns true if a PMTUD probe should be sent. #[must_use] pubfn needs_probe(&self) -> bool { self.probe_state == Probe::Needed
}
/// Returns the size of the current PMTUD probe. #[must_use] pubconstfn probe_size(&self) -> usize { self.search_table[self.probe_index] - self.header_size
}
/// Sends a PMTUD probe. pubfn send_probe<B: Buffer>(
&mutself,
builder: &mut packet::Builder<B>,
tokens: &mut recovery::Tokens,
stats: &mut Stats,
) { // The packet may include ACK-eliciting data already, but rather than check for that, it // seems OK to burn one byte here to simply include a PING.
builder.encode_frame(FrameType::Ping, |_| {});
tokens.push(recovery::Token::PmtudProbe);
stats.frame_tx.ping += 1;
stats.pmtud_tx += 1; self.probe_count += 1; self.probe_state = Probe::Sent;
qdebug!( "Sending PMTUD probe of size {}, count {}", self.search_table[self.probe_index], self.probe_count
);
}
/// Returns the maximum Packetization Layer Path MTU for the configured /// address family. Note that this ignores the interface MTU. #[expect(clippy::missing_panics_doc, reason = "search table is never empty")] #[must_use] pubconstfn address_family_max_mtu(&self) -> usize {
*self.search_table.last().expect("search table is empty")
}
/// Count the PMTUD probes included in `pkts`. fn count_probes(pkts: &[sent::Packet]) -> usize {
pkts.iter().filter(|p| p.is_pmtud_probe()).count()
}
/// Checks whether a PMTUD probe has been acknowledged, and if so, updates the PMTUD state. /// May also initiate a new probe process for a larger MTU. pubfn on_packets_acked(
&mutself,
acked_pkts: &[sent::Packet],
now: Instant,
stats: &mut Stats,
) { let acked = Self::count_probes(acked_pkts); if acked == 0 { return;
}
// A probe was ACKed, confirm the new MTU and try to probe upwards further.
stats.pmtud_ack += acked; let confirmed_idx = self.probe_index;
qdebug!( "PMTUD probe of size {} succeeded", self.search_table[confirmed_idx]
); self.next(now, stats); self.set_mtu(confirmed_idx, stats, now);
}
/// Stops the PMTUD process, setting the MTU to the largest successful probe size. fn stop(&mutself, idx: usize, now: Instant, stats: &mut Stats) { self.probe_state = Probe::NotNeeded; // We don't need to send any more probes self.probe_index = idx; // Index of the last successful probe self.set_mtu(idx, stats, now); // Leading to this MTU self.probe_count = 0; // Reset the count self.raise_timer = Some(now + PMTU_RAISE_TIMER);
qinfo!( "PMTUD stopped, PLPMTU is now {}, raise timer {:?}", self.mtu, self.raise_timer
);
}
/// Checks whether a PMTUD probe has been lost. If it has been lost more than `MAX_PROBES` /// times, the PMTUD process is stopped at the current MTU. pubfn on_packets_lost(
&mutself,
lost_packets: &[sent::Packet],
stats: &mut Stats,
now: Instant,
) { let lost = Self::count_probes(lost_packets); if lost == 0 { return;
}
stats.pmtud_lost += lost;
ifself.probe_count >= MAX_PROBES { // We've sent MAX_PROBES probes and they were all lost. Stop probing at the // previous successful MTU. let ok_idx = self.probe_index.saturating_sub(1);
qdebug!( "PMTUD probe of size {} failed after {MAX_PROBES} attempts", self.search_table[self.probe_index]
); self.stop(ok_idx, now, stats);
} else { // Probe was lost but we haven't exhausted retries yet. self.probe_state = Probe::Needed;
}
}
/// Starts PMTUD from the minimum MTU, probing upward. pubfn start(&mutself, now: Instant, stats: &mut Stats) { self.probe_index = 0; self.raise_timer = None; self.next(now, stats); self.set_mtu(0, stats, now);
qdebug!("PMTUD started, PLPMTU is now {}", self.mtu);
}
/// Starts the next upward PMTUD probe. pubfn next(&mutself, now: Instant, stats: &mut Stats) { ifself.probe_index == SEARCH_TABLE_LEN - 1 {
qdebug!( "PMTUD reached end of search table, i.e. {}, stopping upwards search", self.mtu,
); self.stop(self.probe_index, now, stats); return;
}
self.probe_state = Probe::Needed; // We need to send a probe self.probe_count = 0; // For the first time self.probe_index += 1; // At this size
qdebug!( "PMTUD started with probe size {}", self.search_table[self.probe_index],
);
}
/// Returns the default PLPMTU for the given remote IP address. #[must_use] pubconstfn default_plpmtu(remote_ip: IpAddr) -> usize { let search_table = Self::search_table(remote_ip);
search_table[0] - Self::header_size(remote_ip)
}
}
#[cfg(all(not(feature = "disable-encryption"), test))] mod tests { use std::{
cmp::min,
net::{IpAddr, Ipv4Addr, Ipv6Addr},
time::Instant,
};
use neqo_common::{Encoder, qdebug, qinfo}; use test_fixture::{fixture_init, now};
let final_mtu = iface_mtu.map_or(mtu, |iface_mtu| min(mtu, iface_mtu));
assert_mtu(&pmtud, final_mtu);
(pmtud, stats, prot, now)
}
/// Tests that when the path MTU decreases, PMTUD does not automatically reprobe downward. /// The raise timer only triggers probing for *larger* MTUs. MTU reductions are not /// automatically detected by PMTUD; the connection will continue using the old MTU /// and packets will be lost until the raise timer fires and probing completes at /// the same or a higher MTU (depending on path conditions). fn find_pmtu_no_reduction_detection(addr: IpAddr, mtu: usize) { let (mut pmtud, mut stats, _prot, now) = find_pmtu(addr, mtu, None);
// The current MTU is set. let current_mtu = pmtud.mtu;
assert_eq!(Probe::NotNeeded, pmtud.probe_state);
// Fire the raise timer - this only triggers probing for *higher* MTUs.
qdebug!("Firing raise timer after reaching MTU {current_mtu}"); let now = now + PMTU_RAISE_TIMER;
pmtud.maybe_fire_raise_timer(now, &mut stats);
// If we're not at the max MTU, the timer should trigger a probe for a higher MTU. // If we're at the max MTU (or interface limit), no probe is needed. if pmtud.probe_index < SEARCH_TABLE_LEN - 1
&& pmtud.search_table[pmtud.probe_index + 1] <= pmtud.iface_mtu
{ // Timer should have started probing for a larger MTU.
assert_eq!(Probe::Needed, pmtud.probe_state);
} else { // At max MTU, timer doesn't change state.
assert_eq!(Probe::NotNeeded, pmtud.probe_state);
}
// Regardless, the current MTU should be unchanged.
assert_eq!(current_mtu, pmtud.mtu);
}
qdebug!("Increasing MTU to {larger_mtu}"); let now = now + PMTU_RAISE_TIMER;
pmtud.maybe_fire_raise_timer(now, &mut stats); while pmtud.needs_probe() {
pmtud_step(&mut pmtud, &mut stats, &style='color:red'>mut prot, addr, larger_mtu, now);
}
assert_mtu(&pmtud, larger_mtu);
}
#[test] fn pmtud() { for &addr in &[V4, V6] { for path_mtu in path_mtus() { for &iface_mtu in IFACE_MTUS {
qinfo!("PMTUD for {addr}, path MTU {path_mtu}, iface MTU {iface_mtu:?}");
find_pmtu(addr, path_mtu, iface_mtu);
}
}
}
}
/// Tests that the raise timer only probes upward, not downward. #[test] fn raise_timer_probes_upward_only() { for &addr in &[V4, V6] { for path_mtu in path_mtus() {
qinfo!("Testing raise timer behavior for {addr}, path MTU {path_mtu}");
find_pmtu_no_reduction_detection(addr, path_mtu);
}
}
}
#[test] fn pmtud_with_increase() { for &addr in &[V4, V6] { for path_mtu in path_mtus() { let path_mtus = path_mtus(); let larger_mtus = path_mtus.iter().filter(|&mtu| *mtu > path_mtu); for &larger_mtu in larger_mtus {
qinfo!("PMTUD for {addr}, path MTU {path_mtu}, larger path MTU {larger_mtu}");
find_pmtu_with_increase(addr, path_mtu, larger_mtu);
}
}
}
}
/// Tests that losing non-probe packets does not affect PMTUD state. #[test] fn non_probe_loss_ignored() { const MTU: usize = 1500; let now = now(); letmut pmtud = Pmtud::new(V4, Some(MTU)); letmut stats = Stats::default();
// Complete PMTUD at MTU 1500.
pmtud.stop(
pmtud
.search_table
.iter()
.position(|&mtu| mtu == MTU)
.unwrap(),
now,
&mut stats,
);
assert_mtu(&pmtud, MTU); let initial_lost = stats.pmtud_lost;
// Lose various non-probe packets - should not change PMTUD state.
pmtud.on_packets_lost(&[], &mut stats, now);
assert_eq!(Probe::NotNeeded, pmtud.probe_state);
// The effective upper limit is the minimum of: // - the actual path MTU // - the interface MTU (if set) // - the peer's max_udp_payload_size + header_size let peer_limit = peer_max_udp_payload + Pmtud::header_size(addr); let effective = mtu.min(iface_mtu.unwrap_or(usize::MAX)).min(peer_limit);
assert_mtu(&pmtud, effective);
/// `probe_count` is 0 before sending and 1 after the first probe. #[test] fn send_probe_increments_count() {
fixture_init(); let now = now(); letmut pmtud = Pmtud::new(V4, None); letmut stats = Stats::default();
pmtud.next(now, &mut stats);
assert!(pmtud.needs_probe());
assert_eq!(pmtud.probe_count, 0);
let limit = pmtud.probe_size() - CryptoDxState::test_default_write().expansion(); letmut builder = packet::Builder::short(Encoder::default(), false, None::<&[u8]>, limit);
pmtud.send_probe(&mut builder, &mut Vec::new(), &mut stats);
assert_eq!(
pmtud.probe_count, 1, "probe_count must be 1 after first probe"
);
}
/// PMTUD gives up after exactly `MAX_PROBES` consecutive probe failures, not fewer. #[test] fn max_probes_required_before_giving_up() { // Use a path MTU smaller than the first probe so every probe fails. const PATH_MTU: usize = 1200; // Below all probes in the search table
fixture_init(); let now = now(); letmut pmtud = Pmtud::new(V4, None); letmut stats = Stats::default(); letmut prot = CryptoDxState::test_default_write();
pmtud.next(now, &mut stats);
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.