Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rate-limit stateless resets #1794

Merged
merged 1 commit into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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