Skip to content

Commit

Permalink
Make max_guaranteed_udp_payload_size user-configurable
Browse files Browse the repository at this point in the history
Sponsored by Stormshield
  • Loading branch information
aochagavia committed Apr 27, 2023
1 parent 699a914 commit dee8226
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 39 deletions.
37 changes: 29 additions & 8 deletions quinn-proto/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub struct TransportConfig {
pub(crate) time_threshold: f32,
pub(crate) initial_rtt: Duration,
pub(crate) initial_max_udp_payload_size: u16,
pub(crate) max_guaranteed_udp_payload_size: u16,
pub(crate) mtu_discovery_config: Option<MtuDiscoveryConfig>,

pub(crate) persistent_congestion_threshold: u32,
Expand Down Expand Up @@ -160,19 +161,38 @@ impl TransportConfig {
/// (see [`TransportConfig::mtu_discovery_config`]).
///
/// Must be at least 1200, which is the default, and known to be safe for typical internet
/// applications. Larger values are more efficient, but increase the risk of unpredictable
/// catastrophic packet loss due to exceeding the network path's IP MTU. If the provided value
/// is higher than what the network path actually supports, packet loss will eventually trigger
/// black hole detection and bring it down to 1200.
///
/// Real-world MTUs can vary according to ISP, VPN, and properties of intermediate network links
/// outside of either endpoint's control. Caution should be used when raising this value for
/// connections outside of private networks where these factors are fully controlled.
/// applications. Larger values are more efficient, but increase the risk of packet loss due to
/// exceeding the network path's IP MTU. If the provided value is higher than what the network
/// path actually supports, packet loss will eventually trigger black hole detection and bring
/// it down to [`TransportConfig::max_guaranteed_udp_payload_size`].
pub fn initial_max_udp_payload_size(&mut self, value: u16) -> &mut Self {
self.initial_max_udp_payload_size = value.max(INITIAL_MAX_UDP_PAYLOAD_SIZE);
self
}

pub(crate) fn get_initial_max_udp_payload_size(&self) -> u16 {
self.initial_max_udp_payload_size
.max(self.max_guaranteed_udp_payload_size)
}

/// The maximum UDP payload size guaranteed to be supported by the network.
///
/// Must be at least 1200, which is the default, and lower than or equal to
/// [`TransportConfig::initial_max_udp_payload_size`].
///
/// Real-world MTUs can vary according to ISP, VPN, and properties of intermediate network links
/// outside of either endpoint's control. Extreme care should be used when raising this value
/// outside of private networks where these factors are fully controlled. If the provided value
/// is higher than what the network path actually supports, the result will be unpredictable and
/// catastrophic packet loss, without a possibility of repair. Prefer
/// [`TransportConfig::initial_max_udp_payload_size`] together with
/// [`TransportConfig::mtu_discovery_config`] to set a maximum UDP payload size that robustly
/// adapts to the network.
pub fn max_guaranteed_udp_payload_size(&mut self, value: u16) -> &mut Self {
self.max_guaranteed_udp_payload_size = value.max(INITIAL_MAX_UDP_PAYLOAD_SIZE);
self
}

/// Specifies the MTU discovery config (see [`MtuDiscoveryConfig`] for details).
///
/// Defaults to `None`, which disables MTU discovery altogether.
Expand Down Expand Up @@ -296,6 +316,7 @@ impl Default for TransportConfig {
time_threshold: 9.0 / 8.0,
initial_rtt: Duration::from_millis(333), // per spec, intentionally distinct from EXPECTED_RTT
initial_max_udp_payload_size: INITIAL_MAX_UDP_PAYLOAD_SIZE,
max_guaranteed_udp_payload_size: INITIAL_MAX_UDP_PAYLOAD_SIZE,
mtu_discovery_config: None,

persistent_congestion_threshold: 3,
Expand Down
10 changes: 6 additions & 4 deletions quinn-proto/src/connection/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,8 +265,9 @@ impl Connection {
config.initial_rtt,
config
.congestion_controller_factory
.build(now, config.initial_max_udp_payload_size),
config.initial_max_udp_payload_size,
.build(now, config.get_initial_max_udp_payload_size()),
config.get_initial_max_udp_payload_size(),
config.max_guaranteed_udp_payload_size,
None,
config.mtu_discovery_config.clone(),
now,
Expand Down Expand Up @@ -2737,8 +2738,9 @@ impl Connection {
self.config.initial_rtt,
self.config
.congestion_controller_factory
.build(now, self.config.initial_max_udp_payload_size),
self.config.initial_max_udp_payload_size,
.build(now, self.config.get_initial_max_udp_payload_size()),
self.config.get_initial_max_udp_payload_size(),
self.config.max_guaranteed_udp_payload_size,
Some(peer_max_udp_payload_size),
self.config.mtu_discovery_config.clone(),
now,
Expand Down
59 changes: 35 additions & 24 deletions quinn-proto/src/connection/mtud.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,20 @@ pub(crate) struct MtuDiscovery {
impl MtuDiscovery {
pub(crate) fn new(
initial_plpmtu: u16,
max_guaranteed_udp_payload_size: u16,
peer_max_udp_payload_size: Option<u16>,
config: MtuDiscoveryConfig,
) -> Self {
debug_assert!(
initial_plpmtu >= BASE_UDP_PAYLOAD_SIZE,
"initial_max_udp_payload_size must be at least {BASE_UDP_PAYLOAD_SIZE}"
initial_plpmtu >= max_guaranteed_udp_payload_size,
"initial_max_udp_payload_size must be at least {max_guaranteed_udp_payload_size}"
);

let mut mtud = Self::with_state(initial_plpmtu, Some(EnabledMtuDiscovery::new(config)));
let mut mtud = Self::with_state(
initial_plpmtu,
max_guaranteed_udp_payload_size,
Some(EnabledMtuDiscovery::new(config)),
);

// We might be migrating an existing connection to a new path, in which case the transport
// parameters have already been transmitted, and we already know the value of
Expand All @@ -39,15 +44,19 @@ impl MtuDiscovery {
}

/// MTU discovery will be disabled and the current MTU will be fixed to the provided value
pub(crate) fn disabled(plpmtu: u16) -> Self {
Self::with_state(plpmtu, None)
pub(crate) fn disabled(plpmtu: u16, max_guaranteed_udp_payload_size: u16) -> Self {
Self::with_state(plpmtu, max_guaranteed_udp_payload_size, None)
}

fn with_state(current_mtu: u16, state: Option<EnabledMtuDiscovery>) -> Self {
fn with_state(
current_mtu: u16,
max_guaranteed_udp_payload_size: u16,
state: Option<EnabledMtuDiscovery>,
) -> Self {
Self {
current_mtu,
state,
black_hole_detector: BlackHoleDetector::new(),
black_hole_detector: BlackHoleDetector::new(max_guaranteed_udp_payload_size),
}
}

Expand Down Expand Up @@ -142,13 +151,13 @@ impl MtuDiscovery {
/// Returns true if a black hole was detected
///
/// Calling this function will close the previous loss burst. If a black hole is detected, the
/// current MTU will be reset to `BASE_UDP_PAYLOAD_SIZE`.
/// current MTU will be reset to `max_guaranteed_udp_payload_size`.
pub(crate) fn black_hole_detected(&mut self, now: Instant) -> bool {
if !self.black_hole_detector.black_hole_detected() {
return false;
}

self.current_mtu = BASE_UDP_PAYLOAD_SIZE;
self.current_mtu = self.black_hole_detector.max_guaranteed_udp_payload_size;

if let Some(state) = &mut self.state {
state.on_black_hole_detected(now);
Expand Down Expand Up @@ -362,26 +371,29 @@ struct BlackHoleDetector {
suspicious_loss_bursts: u8,
/// Indicates whether the current loss burst has any non-suspicious packets
///
/// Non-suspicious packets are non-probe packets of size <= `BASE_UDP_PAYLOAD_SIZE`
/// Non-suspicious packets are non-probe packets of size <= `max_guaranteed_udp_payload_size`
loss_burst_has_non_suspicious_packets: bool,
/// The largest suspicious packet that was lost in the current burst
///
/// Suspicious packets are non-probe packets of size > `BASE_UDP_PAYLOAD_SIZE`
/// Suspicious packets are non-probe packets of size > `max_guaranteed_udp_payload_size`
largest_suspicious_packet_lost: Option<u64>,
/// The largest non-probe packet that was lost (used to keep track of loss bursts)
largest_non_probe_lost: Option<u64>,
/// The largest acked packet of size `current_mtu`
largest_acked_mtu_sized_packet: Option<u64>,
/// The UDP payload size guaranteed to be supported by the network
max_guaranteed_udp_payload_size: u16,
}

impl BlackHoleDetector {
fn new() -> Self {
fn new(max_guaranteed_udp_payload_size: u16) -> Self {
Self {
suspicious_loss_bursts: 0,
largest_non_probe_lost: None,
loss_burst_has_non_suspicious_packets: false,
largest_suspicious_packet_lost: None,
largest_acked_mtu_sized_packet: None,
max_guaranteed_udp_payload_size,
}
}

Expand Down Expand Up @@ -414,7 +426,7 @@ impl BlackHoleDetector {
self.finish_loss_burst();
}

if packet_bytes <= BASE_UDP_PAYLOAD_SIZE {
if packet_bytes <= self.max_guaranteed_udp_payload_size {
self.loss_burst_has_non_suspicious_packets = true;
} else {
self.largest_suspicious_packet_lost = Some(packet_number);
Expand Down Expand Up @@ -472,7 +484,6 @@ impl BlackHoleDetector {
// Corresponds to the RFC's `MAX_PROBES` constant (see
// https://www.rfc-editor.org/rfc/rfc8899#section-5.1.2)
const MAX_PROBE_RETRANSMITS: usize = 3;
const BASE_UDP_PAYLOAD_SIZE: u16 = 1_200;
const BLACK_HOLE_THRESHOLD: u8 = 3;
const BINARY_SEARCH_MINIMUM_CHANGE: u16 = 20;

Expand All @@ -486,7 +497,7 @@ mod tests {

fn default_mtud() -> MtuDiscovery {
let config = MtuDiscoveryConfig::default();
MtuDiscovery::new(1_200, None, config)
MtuDiscovery::new(1_200, 1_200, None, config)
}

fn completed(mtud: &MtuDiscovery) -> bool {
Expand Down Expand Up @@ -569,14 +580,14 @@ mod tests {

#[test]
fn mtu_discovery_disabled_does_nothing() {
let mut mtud = MtuDiscovery::disabled(1_200);
let mut mtud = MtuDiscovery::disabled(1_200, 1_200);
let probe_size = mtud.poll_transmit(Instant::now(), 0);
assert_eq!(probe_size, None);
}

#[test]
fn mtu_discovery_disabled_lost_four_packet_bursts_triggers_black_hole_detection() {
let mut mtud = MtuDiscovery::disabled(1_400);
let mut mtud = MtuDiscovery::disabled(1_400, 1_250);
let now = Instant::now();

for i in 0..4 {
Expand All @@ -585,7 +596,7 @@ mod tests {
}

assert!(mtud.black_hole_detected(now));
assert_eq!(mtud.current_mtu, 1200);
assert_eq!(mtud.current_mtu, 1250);
assert_matches!(mtud.state, None);
}

Expand Down Expand Up @@ -623,7 +634,7 @@ mod tests {
fn mtu_discovery_after_complete_reactivates_when_interval_elapsed() {
let mut config = MtuDiscoveryConfig::default();
config.upper_bound(9_000);
let mut mtud = MtuDiscovery::new(1_200, None, config);
let mut mtud = MtuDiscovery::new(1_200, 1_200, None, config);
let now = Instant::now();
drive_to_completion(&mut mtud, now, 1_500);

Expand Down Expand Up @@ -672,7 +683,7 @@ mod tests {
assert!(fourth_probe_size < first_probe_size);
assert_eq!(
fourth_probe_size,
first_probe_size - (first_probe_size - BASE_UDP_PAYLOAD_SIZE) / 2 - 1
first_probe_size - (first_probe_size - 1_200) / 2 - 1
);
}

Expand All @@ -692,7 +703,7 @@ mod tests {

#[test]
fn mtu_discovery_with_previous_peer_max_udp_payload_size_clamps_upper_bound() {
let mut mtud = MtuDiscovery::new(1500, Some(1400), MtuDiscoveryConfig::default());
let mut mtud = MtuDiscovery::new(1500, 1_200, Some(1400), MtuDiscoveryConfig::default());

assert_eq!(mtud.current_mtu, 1400);
assert_eq!(mtud.state.as_ref().unwrap().peer_max_udp_payload_size, 1400);
Expand Down Expand Up @@ -728,7 +739,7 @@ mod tests {
fn mtu_discovery_with_1500_limit_and_10000_upper_bound() {
let mut config = MtuDiscoveryConfig::default();
config.upper_bound(10_000);
let mut mtud = MtuDiscovery::new(1_200, None, config);
let mut mtud = MtuDiscovery::new(1_200, 1_200, None, config);

let probed_sizes = drive_to_completion(&mut mtud, Instant::now(), 1500);

Expand All @@ -745,7 +756,7 @@ mod tests {
fn mtu_discovery_no_lost_probes_finds_maximum_udp_payload() {
let mut config = MtuDiscoveryConfig::default();
config.upper_bound(MAX_UDP_PAYLOAD);
let mut mtud = MtuDiscovery::new(1200, None, config);
let mut mtud = MtuDiscovery::new(1200, 1200, None, config);

drive_to_completion(&mut mtud, Instant::now(), u16::MAX);

Expand All @@ -757,7 +768,7 @@ mod tests {
fn mtu_discovery_lost_half_of_probes_finds_maximum_udp_payload() {
let mut config = MtuDiscoveryConfig::default();
config.upper_bound(MAX_UDP_PAYLOAD);
let mut mtud = MtuDiscovery::new(1200, None, config);
let mut mtud = MtuDiscovery::new(1200, 1200, None, config);

let now = Instant::now();
let mut iterations = 0;
Expand Down
7 changes: 6 additions & 1 deletion quinn-proto/src/connection/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ impl PathData {
initial_rtt: Duration,
congestion: Box<dyn congestion::Controller>,
initial_max_udp_payload_size: u16,
max_guaranteed_udp_payload_size: u16,
peer_max_udp_payload_size: Option<u16>,
mtud_config: Option<MtuDiscoveryConfig>,
now: Instant,
Expand All @@ -60,10 +61,14 @@ impl PathData {
total_sent: 0,
total_recvd: 0,
mtud: mtud_config.map_or(
MtuDiscovery::disabled(initial_max_udp_payload_size),
MtuDiscovery::disabled(
initial_max_udp_payload_size,
max_guaranteed_udp_payload_size,
),
|config| {
MtuDiscovery::new(
initial_max_udp_payload_size,
max_guaranteed_udp_payload_size,
peer_max_udp_payload_size,
config,
)
Expand Down
2 changes: 0 additions & 2 deletions quinn-proto/src/endpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,6 @@ pub struct Endpoint {

impl Endpoint {
/// Create a new endpoint
///
/// Returns `Err` if the configuration is invalid.
pub fn new(config: Arc<EndpointConfig>, server_config: Option<Arc<ServerConfig>>) -> Self {
Self {
rng: StdRng::from_entropy(),
Expand Down

0 comments on commit dee8226

Please sign in to comment.