Skip to content

Commit e75b509

Browse files
iadev09Ruben2424
andauthored
feat: expose 0-RTT detection at stream level (#323)
* feat: expose 0-RTT detection at stream level * feat: move Is0rtt trait to quic layer and improve implementation Address review feedback by refactoring 0-RTT detection: - Move Is0rtt trait from h3::server to h3::quic module All QUIC transport traits belong in the quic abstraction layer, allowing libraries like hyper to use 0-RTT detection generically across different QUIC implementations. - Fix RecvStream::is_0rtt() to cache value at construction time Previously used unwrap_or(false) which could incorrectly return false for 0-RTT streams if poll API was misused. Now stores the 0-RTT flag in a dedicated field initialized in new(). - Implement Is0rtt trait for both BidiStream and RecvStream Ensures consistent 0-RTT detection across all stream types. - Simplify BidiStream implementation Remove redundant public is_0rtt() method, keep only trait impl that delegates to recv.is_0rtt(). - Keep RequestStream::is_0rtt() convenience method for ergonomic stream-level access in server applications. This maintains backward compatibility for users while fixing the potential security issue where 0-RTT status could be lost. Refs: PR #323 * fix: replace tabs with spaces in documentation comments * fix doctest * cleanup --------- Co-authored-by: ruben <[email protected]>
1 parent 0a19b03 commit e75b509

File tree

5 files changed

+80
-0
lines changed

5 files changed

+80
-0
lines changed

h3-quinn/src/lib.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,12 +338,22 @@ where
338338
}
339339
}
340340

341+
impl<B> quic::Is0rtt for BidiStream<B>
342+
where
343+
B: Buf,
344+
{
345+
fn is_0rtt(&self) -> bool {
346+
self.recv.is_0rtt()
347+
}
348+
}
349+
341350
/// Quinn-backed receive stream
342351
///
343352
/// Implements a [`quic::RecvStream`] backed by a [`quinn::RecvStream`].
344353
pub struct RecvStream {
345354
stream: Option<quinn::RecvStream>,
346355
read_chunk_fut: ReadChunkFuture,
356+
is_0rtt: bool,
347357
}
348358

349359
type ReadChunkFuture = ReusableBoxFuture<
@@ -356,10 +366,12 @@ type ReadChunkFuture = ReusableBoxFuture<
356366

357367
impl RecvStream {
358368
fn new(stream: quinn::RecvStream) -> Self {
369+
let is_0rtt = stream.is_0rtt();
359370
Self {
360371
stream: Some(stream),
361372
// Should only allocate once the first time it's used
362373
read_chunk_fut: ReusableBoxFuture::new(async { unreachable!() }),
374+
is_0rtt,
363375
}
364376
}
365377
}
@@ -403,6 +415,16 @@ impl quic::RecvStream for RecvStream {
403415
}
404416
}
405417

418+
impl quic::Is0rtt for RecvStream {
419+
/// Check if this stream has been opened during 0-RTT.
420+
///
421+
/// In which case any non-idempotent request should be considered dangerous at the application
422+
/// level. Because read data is subject to replay attacks.
423+
fn is_0rtt(&self) -> bool {
424+
self.is_0rtt
425+
}
426+
}
427+
406428
fn convert_read_error_to_stream_error(error: ReadError) -> StreamErrorIncoming {
407429
match error {
408430
ReadError::Reset(var_int) => StreamErrorIncoming::StreamTerminated {

h3/src/frame.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ impl<S, B> FrameStream<S, B> {
4343
}
4444
}
4545

46+
impl<S, B> FrameStream<S, B>
47+
where
48+
S: crate::quic::Is0rtt,
49+
{
50+
/// Checks if the stream was opened in 0-RTT mode
51+
pub(crate) fn is_0rtt(&self) -> bool {
52+
self.stream.is_0rtt()
53+
}
54+
}
55+
4656
impl<S, B> FrameStream<S, B>
4757
where
4858
S: RecvStream,

h3/src/quic.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,19 @@ pub trait BidiStream<B: Buf>: SendStream<B> + RecvStream {
230230
/// Split this stream into two halves.
231231
fn split(self) -> (Self::SendStream, Self::RecvStream);
232232
}
233+
234+
/// Trait for QUIC streams that support 0-RTT detection.
235+
///
236+
/// This allows detection of streams opened during the 0-RTT phase of a QUIC connection.
237+
/// 0-RTT data is vulnerable to replay attacks, so applications should be cautious when
238+
/// processing non-idempotent requests on such streams.
239+
///
240+
/// See [RFC 8470 Section 5.2](https://www.rfc-editor.org/rfc/rfc8470.html#section-5.2)
241+
/// for guidance on handling 0-RTT data in HTTP/3.
242+
pub trait Is0rtt {
243+
/// Check if this stream was opened during 0-RTT.
244+
///
245+
/// Returns `true` if the stream was opened during the 0-RTT phase,
246+
/// `false` otherwise.
247+
fn is_0rtt(&self) -> bool;
248+
}

h3/src/server/stream.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,28 @@ where
105105
pub fn id(&self) -> StreamId {
106106
self.inner.stream.id()
107107
}
108+
109+
/// Check if this stream was opened during 0-RTT.
110+
///
111+
/// See [RFC 8470 Section 5.2](https://www.rfc-editor.org/rfc/rfc8470.html#section-5.2).
112+
///
113+
/// # Example
114+
///
115+
/// ```no_run
116+
/// # use h3::server::RequestStream;
117+
/// # async fn example(mut stream: RequestStream<impl h3::quic::BidiStream<bytes::Bytes> + h3::quic::Is0rtt, bytes::Bytes>) {
118+
/// if stream.is_0rtt() {
119+
/// // Reject non-idempotent methods (e.g., POST, PUT, DELETE)
120+
/// // to prevent replay attacks
121+
/// }
122+
/// # }
123+
/// ```
124+
pub fn is_0rtt(&self) -> bool
125+
where
126+
S: quic::Is0rtt,
127+
{
128+
self.inner.stream.is_0rtt()
129+
}
108130
}
109131

110132
impl<S, B> RequestStream<S, B>

h3/src/stream.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,16 @@ impl<S, B> BufRecvStream<S, B> {
439439
}
440440
}
441441

442+
impl<S, B> BufRecvStream<S, B>
443+
where
444+
S: crate::quic::Is0rtt,
445+
{
446+
/// Checks if the stream was opened in 0-RTT mode
447+
pub(crate) fn is_0rtt(&self) -> bool {
448+
self.stream.is_0rtt()
449+
}
450+
}
451+
442452
impl<B, S: RecvStream> BufRecvStream<S, B> {
443453
/// Reads more data into the buffer, returning the number of bytes read.
444454
///

0 commit comments

Comments
 (0)