Skip to content

Commit

Permalink
Rate-limit stateless resets
Browse files Browse the repository at this point in the history
  • Loading branch information
Ralith committed Mar 29, 2024
1 parent 56f03f1 commit 4822fd4
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 2 deletions.
16 changes: 16 additions & 0 deletions quinn-proto/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,8 @@ pub struct EndpointConfig {
Arc<dyn Fn() -> Box<dyn ConnectionIdGenerator> + Send + Sync>,
pub(crate) supported_versions: Vec<u32>,
pub(crate) grease_quic_bit: bool,
/// Minimum interval between outgoing stateless reset packets
pub(crate) min_reset_interval: Duration,
}

impl EndpointConfig {
Expand All @@ -625,6 +627,7 @@ impl EndpointConfig {
connection_id_generator_factory: Arc::new(cid_factory),
supported_versions: DEFAULT_SUPPORTED_VERSIONS.to_vec(),
grease_quic_bit: true,
min_reset_interval: Duration::from_millis(20),
}
}

Expand Down Expand Up @@ -698,6 +701,19 @@ impl EndpointConfig {
self.grease_quic_bit = value;
self
}

/// Minimum interval between outgoing stateless reset packets
///
/// Defaults to 20ms. Limits the impact of attacks which flood an endpoint with garbage packets,
/// e.g. [ISAKMP/IKE amplification]. Larger values provide a stronger defense, but may delay
/// detection of some error conditions by clients.
///
/// [ISAKMP/IKE
/// amplification]: https://bughunters.google.com/blog/5960150648750080/preventing-cross-service-udp-loops-in-quic#isakmp-ike-amplification-vs-quic
pub fn min_reset_interval(&mut self, value: Duration) -> &mut Self {
self.min_reset_interval = value;
self
}
}

impl fmt::Debug for EndpointConfig {
Expand Down
17 changes: 15 additions & 2 deletions quinn-proto/src/endpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ pub struct Endpoint {
server_config: Option<Arc<ServerConfig>>,
/// Whether the underlying UDP socket promises not to fragment packets
allow_mtud: bool,
/// Time at which a stateless reset was most recently sent
last_stateless_reset: Option<Instant>,
}

impl Endpoint {
Expand All @@ -67,6 +69,7 @@ impl Endpoint {
config,
server_config,
allow_mtud,
last_stateless_reset: None,
}
}

Expand Down Expand Up @@ -205,7 +208,7 @@ impl Endpoint {
None => {
debug!("packet for unrecognized connection {}", dst_cid);
return self
.stateless_reset(datagram_len, addresses, dst_cid, buf)
.stateless_reset(now, datagram_len, addresses, dst_cid, buf)
.map(DatagramEvent::Response);
}
};
Expand Down Expand Up @@ -267,7 +270,7 @@ impl Endpoint {
//
if !dst_cid.is_empty() {
return self
.stateless_reset(datagram_len, addresses, dst_cid, buf)
.stateless_reset(now, datagram_len, addresses, dst_cid, buf)
.map(DatagramEvent::Response);
}

Expand All @@ -277,11 +280,20 @@ impl Endpoint {

fn stateless_reset(
&mut self,
now: Instant,
inciting_dgram_len: usize,
addresses: FourTuple,
dst_cid: &ConnectionId,
buf: &mut BytesMut,
) -> Option<Transmit> {
if self
.last_stateless_reset
.map_or(false, |last| last + self.config.min_reset_interval > now)
{
debug!("ignoring unexpected packet within minimum stateless reset interval");
return None;
}

/// Minimum amount of padding for the stateless reset to look like a short-header packet
const MIN_PADDING_LEN: usize = 5;

Expand All @@ -299,6 +311,7 @@ impl Endpoint {
"sending stateless reset for {} to {}",
dst_cid, addresses.remote
);
self.last_stateless_reset = Some(now);
// Resets with at least this much padding can't possibly be distinguished from real packets
const IDEAL_MIN_PADDING_LEN: usize = MIN_PADDING_LEN + MAX_CID_SIZE;
let padding_len = if max_padding_len <= IDEAL_MIN_PADDING_LEN {
Expand Down
38 changes: 38 additions & 0 deletions quinn-proto/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,44 @@ fn client_stateless_reset() {
);
}

/// Verify that stateless resets are rate-limited
#[test]
fn stateless_reset_limit() {
let _guard = subscribe();
let remote = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 42);
let endpoint_config = Arc::new(EndpointConfig::default());
let mut endpoint = Endpoint::new(
endpoint_config.clone(),
Some(Arc::new(server_config())),
true,
None,
);
let time = Instant::now();
let mut buf = BytesMut::new();
let event = endpoint.handle(time, remote, None, None, [0u8; 1024][..].into(), &mut buf);
assert!(matches!(event, Some(DatagramEvent::Response(_))));
let event = endpoint.handle(time, remote, None, None, [0u8; 1024][..].into(), &mut buf);
assert!(event.is_none());
let event = endpoint.handle(
time + endpoint_config.min_reset_interval - Duration::from_nanos(1),
remote,
None,
None,
[0u8; 1024][..].into(),
&mut buf,
);
assert!(event.is_none());
let event = endpoint.handle(
time + endpoint_config.min_reset_interval,
remote,
None,
None,
[0u8; 1024][..].into(),
&mut buf,
);
assert!(matches!(event, Some(DatagramEvent::Response(_))));
}

#[test]
fn export_keying_material() {
let _guard = subscribe();
Expand Down

0 comments on commit 4822fd4

Please sign in to comment.