diff --git a/quinn-proto/src/config.rs b/quinn-proto/src/config.rs index 9194db374..5a603f6e5 100644 --- a/quinn-proto/src/config.rs +++ b/quinn-proto/src/config.rs @@ -612,6 +612,8 @@ pub struct EndpointConfig { Arc Box + Send + Sync>, pub(crate) supported_versions: Vec, pub(crate) grease_quic_bit: bool, + /// Minimum interval between outgoing stateless reset packets + pub(crate) min_reset_interval: Duration, } impl EndpointConfig { @@ -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), } } @@ -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 { diff --git a/quinn-proto/src/endpoint.rs b/quinn-proto/src/endpoint.rs index 19c1f61e7..ac48151ff 100644 --- a/quinn-proto/src/endpoint.rs +++ b/quinn-proto/src/endpoint.rs @@ -45,6 +45,8 @@ pub struct Endpoint { server_config: Option>, /// 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, } impl Endpoint { @@ -67,6 +69,7 @@ impl Endpoint { config, server_config, allow_mtud, + last_stateless_reset: None, } } @@ -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); } }; @@ -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); } @@ -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 { + 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; @@ -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 { diff --git a/quinn-proto/src/tests/mod.rs b/quinn-proto/src/tests/mod.rs index 52f17aece..d693e4e60 100644 --- a/quinn-proto/src/tests/mod.rs +++ b/quinn-proto/src/tests/mod.rs @@ -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();