From b2939cfddd1720152c5aafd2b587f7b3235e431c Mon Sep 17 00:00:00 2001 From: iadev09 <166385752+iadev09@users.noreply.github.com> Date: Wed, 12 Nov 2025 17:37:52 +0300 Subject: [PATCH 1/3] feat: expose 0-RTT detection at stream level --- h3-quinn/src/lib.rs | 32 ++++++++++++++++++++++++++++++++ h3/src/frame.rs | 7 +++++++ h3/src/server/mod.rs | 2 +- h3/src/server/stream.rs | 28 ++++++++++++++++++++++++++++ h3/src/stream.rs | 7 +++++++ 5 files changed, 75 insertions(+), 1 deletion(-) diff --git a/h3-quinn/src/lib.rs b/h3-quinn/src/lib.rs index 3e6e03e3..2d7d9075 100644 --- a/h3-quinn/src/lib.rs +++ b/h3-quinn/src/lib.rs @@ -270,6 +270,18 @@ where recv: RecvStream, } +impl BidiStream +where + B: Buf, +{ + /// Check if this stream was opened during 0-RTT. + /// + /// See [RFC 8470 Section 5.2](https://www.rfc-editor.org/rfc/rfc8470.html#section-5.2). + pub fn is_0rtt(&self) -> bool { + self.recv.is_0rtt() + } +} + impl quic::BidiStream for BidiStream where B: Buf, @@ -338,6 +350,15 @@ where } } +impl h3::server::Is0rtt for BidiStream +where + B: Buf, +{ + fn is_0rtt(&self) -> bool { + BidiStream::is_0rtt(self) + } +} + /// Quinn-backed receive stream /// /// Implements a [`quic::RecvStream`] backed by a [`quinn::RecvStream`]. @@ -362,6 +383,17 @@ impl RecvStream { read_chunk_fut: ReusableBoxFuture::new(async { unreachable!() }), } } + + /// Check if this stream has been opened during 0-RTT. + /// + /// In which case any non-idempotent request should be considered dangerous at the application + /// level. Because read data is subject to replay attacks. + pub fn is_0rtt(&self) -> bool { + self.stream + .as_ref() + .map(|s| s.is_0rtt()) + .unwrap_or(false) + } } impl quic::RecvStream for RecvStream { diff --git a/h3/src/frame.rs b/h3/src/frame.rs index 2bb07dfa..cd9f8a5b 100644 --- a/h3/src/frame.rs +++ b/h3/src/frame.rs @@ -41,6 +41,13 @@ impl FrameStream { pub fn into_inner(self) -> BufRecvStream { self.stream } + + pub(crate) fn is_0rtt(&self) -> bool + where + S: crate::server::Is0rtt, + { + self.stream.is_0rtt() + } } impl FrameStream diff --git a/h3/src/server/mod.rs b/h3/src/server/mod.rs index bdc570d1..8c8c235a 100644 --- a/h3/src/server/mod.rs +++ b/h3/src/server/mod.rs @@ -56,4 +56,4 @@ pub use builder::builder; pub use builder::Builder; pub use connection::Connection; pub use request::RequestResolver; -pub use stream::RequestStream; +pub use stream::{Is0rtt, RequestStream}; diff --git a/h3/src/server/stream.rs b/h3/src/server/stream.rs index fa1aecf2..d99fb12a 100644 --- a/h3/src/server/stream.rs +++ b/h3/src/server/stream.rs @@ -105,6 +105,34 @@ where pub fn id(&self) -> StreamId { self.inner.stream.id() } + + /// Check if this stream was opened during 0-RTT. + /// + /// See [RFC 8470 Section 5.2](https://www.rfc-editor.org/rfc/rfc8470.html#section-5.2). + /// + /// # Example + /// + /// ```no_run + /// # use h3::server::RequestStream; + /// # async fn example(mut stream: RequestStream, bytes::Bytes>) { + /// if stream.is_0rtt() { + /// // Reject non-idempotent methods (e.g., POST, PUT, DELETE) + /// // to prevent replay attacks + /// } + /// # } + /// ``` + pub fn is_0rtt(&self) -> bool + where + S: Is0rtt, + { + self.inner.stream.is_0rtt() + } +} + +/// Trait for QUIC streams that support 0-RTT detection. +pub trait Is0rtt { + /// Check if this stream was opened during 0-RTT. + fn is_0rtt(&self) -> bool; } impl RequestStream diff --git a/h3/src/stream.rs b/h3/src/stream.rs index ff6fdd52..9ed7840f 100644 --- a/h3/src/stream.rs +++ b/h3/src/stream.rs @@ -437,6 +437,13 @@ impl BufRecvStream { _marker: PhantomData, } } + + pub(crate) fn is_0rtt(&self) -> bool + where + S: crate::server::Is0rtt, + { + self.stream.is_0rtt() + } } impl BufRecvStream { From 1040a703392f5bbda44e734cd00e867a00f0811d Mon Sep 17 00:00:00 2001 From: iadev09 <166385752+iadev09@users.noreply.github.com> Date: Thu, 13 Nov 2025 00:28:14 +0300 Subject: [PATCH 2/3] 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 --- h3-quinn/src/lib.rs | 30 ++++++++++++------------------ h3/src/frame.rs | 2 +- h3/src/quic.rs | 16 ++++++++++++++++ h3/src/server/mod.rs | 2 +- h3/src/server/stream.rs | 8 +------- h3/src/stream.rs | 2 +- 6 files changed, 32 insertions(+), 28 deletions(-) diff --git a/h3-quinn/src/lib.rs b/h3-quinn/src/lib.rs index 2d7d9075..1d004a98 100644 --- a/h3-quinn/src/lib.rs +++ b/h3-quinn/src/lib.rs @@ -270,18 +270,6 @@ where recv: RecvStream, } -impl BidiStream -where - B: Buf, -{ - /// Check if this stream was opened during 0-RTT. - /// - /// See [RFC 8470 Section 5.2](https://www.rfc-editor.org/rfc/rfc8470.html#section-5.2). - pub fn is_0rtt(&self) -> bool { - self.recv.is_0rtt() - } -} - impl quic::BidiStream for BidiStream where B: Buf, @@ -350,12 +338,12 @@ where } } -impl h3::server::Is0rtt for BidiStream +impl quic::Is0rtt for BidiStream where B: Buf, { fn is_0rtt(&self) -> bool { - BidiStream::is_0rtt(self) + self.recv.is_0rtt() } } @@ -365,6 +353,7 @@ where pub struct RecvStream { stream: Option, read_chunk_fut: ReadChunkFuture, + is_0rtt: bool, } type ReadChunkFuture = ReusableBoxFuture< @@ -377,10 +366,12 @@ type ReadChunkFuture = ReusableBoxFuture< impl RecvStream { fn new(stream: quinn::RecvStream) -> Self { + let is_0rtt = stream.is_0rtt(); Self { stream: Some(stream), // Should only allocate once the first time it's used read_chunk_fut: ReusableBoxFuture::new(async { unreachable!() }), + is_0rtt, } } @@ -389,10 +380,7 @@ impl RecvStream { /// In which case any non-idempotent request should be considered dangerous at the application /// level. Because read data is subject to replay attacks. pub fn is_0rtt(&self) -> bool { - self.stream - .as_ref() - .map(|s| s.is_0rtt()) - .unwrap_or(false) + self.is_0rtt } } @@ -435,6 +423,12 @@ impl quic::RecvStream for RecvStream { } } +impl quic::Is0rtt for RecvStream { + fn is_0rtt(&self) -> bool { + self.is_0rtt + } +} + fn convert_read_error_to_stream_error(error: ReadError) -> StreamErrorIncoming { match error { ReadError::Reset(var_int) => StreamErrorIncoming::StreamTerminated { diff --git a/h3/src/frame.rs b/h3/src/frame.rs index cd9f8a5b..420ac6b8 100644 --- a/h3/src/frame.rs +++ b/h3/src/frame.rs @@ -44,7 +44,7 @@ impl FrameStream { pub(crate) fn is_0rtt(&self) -> bool where - S: crate::server::Is0rtt, + S: crate::quic::Is0rtt, { self.stream.is_0rtt() } diff --git a/h3/src/quic.rs b/h3/src/quic.rs index f8e57035..923b46f8 100644 --- a/h3/src/quic.rs +++ b/h3/src/quic.rs @@ -230,3 +230,19 @@ pub trait BidiStream: SendStream + RecvStream { /// Split this stream into two halves. fn split(self) -> (Self::SendStream, Self::RecvStream); } + +/// Trait for QUIC streams that support 0-RTT detection. +/// +/// This allows detection of streams opened during the 0-RTT phase of a QUIC connection. +/// 0-RTT data is vulnerable to replay attacks, so applications should be cautious when +/// processing non-idempotent requests on such streams. +/// +/// See [RFC 8470 Section 5.2](https://www.rfc-editor.org/rfc/rfc8470.html#section-5.2) +/// for guidance on handling 0-RTT data in HTTP/3. +pub trait Is0rtt { + /// Check if this stream was opened during 0-RTT. + /// + /// Returns `true` if the stream was opened during the 0-RTT phase, + /// `false` otherwise. + fn is_0rtt(&self) -> bool; +} diff --git a/h3/src/server/mod.rs b/h3/src/server/mod.rs index 8c8c235a..bdc570d1 100644 --- a/h3/src/server/mod.rs +++ b/h3/src/server/mod.rs @@ -56,4 +56,4 @@ pub use builder::builder; pub use builder::Builder; pub use connection::Connection; pub use request::RequestResolver; -pub use stream::{Is0rtt, RequestStream}; +pub use stream::RequestStream; diff --git a/h3/src/server/stream.rs b/h3/src/server/stream.rs index d99fb12a..76dc31bf 100644 --- a/h3/src/server/stream.rs +++ b/h3/src/server/stream.rs @@ -123,18 +123,12 @@ where /// ``` pub fn is_0rtt(&self) -> bool where - S: Is0rtt, + S: quic::Is0rtt, { self.inner.stream.is_0rtt() } } -/// Trait for QUIC streams that support 0-RTT detection. -pub trait Is0rtt { - /// Check if this stream was opened during 0-RTT. - fn is_0rtt(&self) -> bool; -} - impl RequestStream where S: quic::SendStream, diff --git a/h3/src/stream.rs b/h3/src/stream.rs index 9ed7840f..764eccb8 100644 --- a/h3/src/stream.rs +++ b/h3/src/stream.rs @@ -440,7 +440,7 @@ impl BufRecvStream { pub(crate) fn is_0rtt(&self) -> bool where - S: crate::server::Is0rtt, + S: crate::quic::Is0rtt, { self.stream.is_0rtt() } From ded5a0fa3caffd587e987257c9c81d650a776137 Mon Sep 17 00:00:00 2001 From: iadev09 <166385752+iadev09@users.noreply.github.com> Date: Sat, 15 Nov 2025 20:10:16 +0300 Subject: [PATCH 3/3] fix: replace tabs with spaces in documentation comments --- h3-quinn/src/lib.rs | 8 ++++---- h3/src/server/stream.rs | 30 +++++++++++++++--------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/h3-quinn/src/lib.rs b/h3-quinn/src/lib.rs index 1d004a98..cc7e2690 100644 --- a/h3-quinn/src/lib.rs +++ b/h3-quinn/src/lib.rs @@ -375,10 +375,10 @@ impl RecvStream { } } - /// Check if this stream has been opened during 0-RTT. - /// - /// In which case any non-idempotent request should be considered dangerous at the application - /// level. Because read data is subject to replay attacks. + /// Check if this stream has been opened during 0-RTT. + /// + /// In which case any non-idempotent request should be considered dangerous at the application + /// level. Because read data is subject to replay attacks. pub fn is_0rtt(&self) -> bool { self.is_0rtt } diff --git a/h3/src/server/stream.rs b/h3/src/server/stream.rs index 76dc31bf..e6c94bed 100644 --- a/h3/src/server/stream.rs +++ b/h3/src/server/stream.rs @@ -106,21 +106,21 @@ where self.inner.stream.id() } - /// Check if this stream was opened during 0-RTT. - /// - /// See [RFC 8470 Section 5.2](https://www.rfc-editor.org/rfc/rfc8470.html#section-5.2). - /// - /// # Example - /// - /// ```no_run - /// # use h3::server::RequestStream; - /// # async fn example(mut stream: RequestStream, bytes::Bytes>) { - /// if stream.is_0rtt() { - /// // Reject non-idempotent methods (e.g., POST, PUT, DELETE) - /// // to prevent replay attacks - /// } - /// # } - /// ``` + /// Check if this stream was opened during 0-RTT. + /// + /// See [RFC 8470 Section 5.2](https://www.rfc-editor.org/rfc/rfc8470.html#section-5.2). + /// + /// # Example + /// + /// ```no_run + /// # use h3::server::RequestStream; + /// # async fn example(mut stream: RequestStream, bytes::Bytes>) { + /// if stream.is_0rtt() { + /// // Reject non-idempotent methods (e.g., POST, PUT, DELETE) + /// // to prevent replay attacks + /// } + /// # } + /// ``` pub fn is_0rtt(&self) -> bool where S: quic::Is0rtt,