Skip to content

Commit 32c9b11

Browse files
proto: Add option to pad application data UDP datagrams to MTU
1 parent 113fa61 commit 32c9b11

3 files changed

Lines changed: 100 additions & 2 deletions

File tree

quinn-proto/src/config/transport.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ pub struct TransportConfig {
2929
pub(crate) initial_mtu: u16,
3030
pub(crate) min_mtu: u16,
3131
pub(crate) mtu_discovery_config: Option<MtuDiscoveryConfig>,
32+
pub(crate) pad_to_mtu: bool,
3233
pub(crate) ack_frequency_config: Option<AckFrequencyConfig>,
3334

3435
pub(crate) persistent_congestion_threshold: u32,
@@ -203,6 +204,20 @@ impl TransportConfig {
203204
self
204205
}
205206

207+
/// Pad UDP datagrams carrying application data to current maximum UDP payload size
208+
///
209+
/// Disabled by default. UDP datagrams containing loss probes are exempt from padding.
210+
///
211+
/// Enabling this helps mitigate traffic analysis by network observers, but it increases
212+
/// bandwidth usage. Without this mitigation precise plain text size of application datagrams as
213+
/// well as the total size of stream write bursts can be inferred by observers under certain
214+
/// conditions. This analysis requires either an uncongested connection or application datagrams
215+
/// too large to be coalesced.
216+
pub fn pad_to_mtu(&mut self, value: bool) -> &mut Self {
217+
self.pad_to_mtu = value;
218+
self
219+
}
220+
206221
/// Specifies the ACK frequency config (see [`AckFrequencyConfig`] for details)
207222
///
208223
/// The provided configuration will be ignored if the peer does not support the acknowledgement
@@ -340,6 +355,7 @@ impl Default for TransportConfig {
340355
initial_mtu: INITIAL_MTU,
341356
min_mtu: INITIAL_MTU,
342357
mtu_discovery_config: Some(MtuDiscoveryConfig::default()),
358+
pad_to_mtu: false,
343359
ack_frequency_config: None,
344360

345361
persistent_congestion_threshold: 3,
@@ -374,6 +390,7 @@ impl fmt::Debug for TransportConfig {
374390
initial_mtu,
375391
min_mtu,
376392
mtu_discovery_config,
393+
pad_to_mtu,
377394
ack_frequency_config,
378395
persistent_congestion_threshold,
379396
keep_alive_interval,
@@ -400,6 +417,7 @@ impl fmt::Debug for TransportConfig {
400417
.field("initial_mtu", initial_mtu)
401418
.field("min_mtu", min_mtu)
402419
.field("mtu_discovery_config", mtu_discovery_config)
420+
.field("pad_to_mtu", pad_to_mtu)
403421
.field("ack_frequency_config", ack_frequency_config)
404422
.field(
405423
"persistent_congestion_threshold",

quinn-proto/src/connection/mod.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,7 @@ impl Connection {
509509
let mut builder_storage: Option<PacketBuilder> = None;
510510
let mut sent_frames = None;
511511
let mut pad_datagram = false;
512+
let mut pad_datagram_to_mtu = false;
512513
let mut congestion_blocked = false;
513514

514515
// Iterate over all spaces and find data to send
@@ -542,6 +543,8 @@ impl Connection {
542543
ack_eliciting |= self.can_send_1rtt(frame_space_1rtt);
543544
}
544545

546+
pad_datagram_to_mtu |= space_id == SpaceId::Data && self.config.pad_to_mtu;
547+
545548
// Can we append more data into the current buffer?
546549
// It is not safe to assume that `buf.len()` is the end of the data,
547550
// since the last packet might not have been finished.
@@ -628,7 +631,7 @@ impl Connection {
628631
builder.pad_to(MIN_INITIAL_SIZE);
629632
}
630633

631-
if num_datagrams > 1 {
634+
if num_datagrams > 1 || pad_datagram_to_mtu {
632635
// If too many padding bytes would be required to continue the GSO batch
633636
// after this packet, end the GSO batch here. Ensures that fixed-size frames
634637
// with heterogeneous sizes (e.g. application datagrams) won't inadvertently
@@ -645,7 +648,8 @@ impl Connection {
645648
let packet_len_unpadded = cmp::max(builder.min_size, buf.len())
646649
- datagram_start
647650
+ builder.tag_len;
648-
if packet_len_unpadded + MAX_PADDING < segment_size
651+
if (packet_len_unpadded + MAX_PADDING < segment_size
652+
&& !pad_datagram_to_mtu)
649653
|| datagram_start + segment_size > buf_capacity
650654
{
651655
trace!(
@@ -904,6 +908,16 @@ impl Connection {
904908
if pad_datagram {
905909
builder.pad_to(MIN_INITIAL_SIZE);
906910
}
911+
912+
// If this datagram is a loss probe and `segment_size` is larger than `INITIAL_MTU`,
913+
// then padding it to `segment_size` would risk failure to recover from a reduction in
914+
// path MTU.
915+
// Loss probes are the only packets for which we might grow `buf_capacity`
916+
// by less than `segment_size`.
917+
if pad_datagram_to_mtu && buf_capacity >= datagram_start + segment_size {
918+
builder.pad_to(segment_size as u16);
919+
}
920+
907921
let last_packet_number = builder.exact_number;
908922
builder.finish_and_track(now, self, sent_frames, buf);
909923
self.path

quinn-proto/src/tests/mod.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3217,6 +3217,72 @@ fn gso_truncation() {
32173217
}
32183218
}
32193219

3220+
/// Verify that UDP datagrams are padded to MTU if specified in the transport config.
3221+
#[test]
3222+
fn pad_to_mtu() {
3223+
let _guard = subscribe();
3224+
const MTU: u16 = 1333;
3225+
let client_config = {
3226+
let mut c_config = client_config();
3227+
let t_config = TransportConfig {
3228+
initial_mtu: MTU,
3229+
mtu_discovery_config: None,
3230+
pad_to_mtu: true,
3231+
..TransportConfig::default()
3232+
};
3233+
c_config.transport_config(t_config.into());
3234+
c_config
3235+
};
3236+
let mut pair = Pair::default();
3237+
let (client_ch, server_ch) = pair.connect_with(client_config);
3238+
3239+
let initial_ios = pair.client_conn_mut(client_ch).stats().udp_tx.ios;
3240+
pair.server.capture_inbound_packets = true;
3241+
3242+
info!("sending");
3243+
// Send two datagrams significantly smaller than MTU, but large enough to require two UDP datagrams.
3244+
const LEN_1: usize = 800;
3245+
const LEN_2: usize = 600;
3246+
pair.client_datagrams(client_ch)
3247+
.send(vec![0; LEN_1].into(), false)
3248+
.unwrap();
3249+
pair.client_datagrams(client_ch)
3250+
.send(vec![0; LEN_2].into(), false)
3251+
.unwrap();
3252+
pair.client.drive(pair.time, pair.server.addr);
3253+
3254+
// Check padding
3255+
assert_eq!(pair.client.outbound.len(), 2);
3256+
assert_eq!(pair.client.outbound[0].0.size, usize::from(MTU));
3257+
assert_eq!(pair.client.outbound[0].1.len(), usize::from(MTU));
3258+
assert_eq!(pair.client.outbound[1].0.size, usize::from(MTU));
3259+
assert_eq!(pair.client.outbound[1].1.len(), usize::from(MTU));
3260+
pair.drive_client();
3261+
assert_eq!(pair.server.inbound.len(), 2);
3262+
assert_eq!(pair.server.inbound[0].2.len(), usize::from(MTU));
3263+
assert_eq!(pair.server.inbound[1].2.len(), usize::from(MTU));
3264+
pair.drive();
3265+
3266+
// Check that both datagrams ended up in the same GSO batch
3267+
let final_ios = pair.client_conn_mut(client_ch).stats().udp_tx.ios;
3268+
assert_eq!(final_ios - initial_ios, 1);
3269+
3270+
assert_eq!(
3271+
pair.server_datagrams(server_ch)
3272+
.recv()
3273+
.expect("datagram lost")
3274+
.len(),
3275+
LEN_1
3276+
);
3277+
assert_eq!(
3278+
pair.server_datagrams(server_ch)
3279+
.recv()
3280+
.expect("datagram lost")
3281+
.len(),
3282+
LEN_2
3283+
);
3284+
}
3285+
32203286
/// Verify that a large application datagram is sent successfully when an ACK frame too large to fit
32213287
/// alongside it is also queued, in exactly 2 UDP datagrams.
32223288
#[test]

0 commit comments

Comments
 (0)