Skip to content

Conversation

@iadev09
Copy link

@iadev09 iadev09 commented Nov 12, 2025

Summary

  • Adds is_0rtt() method to RequestStream to detect streams opened during 0-RTT
  • Implements Is0rtt trait for QUIC stream types
  • Exposes 0-RTT detection through the h3-quinn adapter layer

Motivation

Applications need to detect 0-RTT streams to properly handle replay-attack risks (RFC 8470 § 5.2).
For example, non-idempotent operations such as POST, PUT, or DELETE should be rejected or handled with extra care on 0-RTT streams.

@iadev09 iadev09 force-pushed the feature/stream-level-0rtt branch from 95fc57a to f97258d Compare November 12, 2025 19:22
@iadev09 iadev09 force-pushed the feature/stream-level-0rtt branch from f077a14 to b2939cf Compare November 12, 2025 19:33
Copy link
Contributor

@Ruben2424 Ruben2424 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR. I left some comments below.

h3/src/frame.rs Outdated

pub(crate) fn is_0rtt(&self) -> bool
where
S: crate::server::Is0rtt,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason you did not make an extra impl block? Like for S: RecvStream and T: SendStream<B>

h3/src/stream.rs Outdated

pub(crate) fn is_0rtt(&self) -> bool
where
S: crate::server::Is0rtt,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question, why not extra impl block?


/// Trait for QUIC streams that support 0-RTT detection.
pub trait Is0rtt {
/// Check if this stream was opened during 0-RTT.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All traits implemented by quic transport layer usually are in this file https://github.com/hyperium/h3/blob/master/h3/src/quic.rs

self.stream
.as_ref()
.map(|s| s.is_0rtt())
.unwrap_or(false)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when self.stream is None this will always return false.
IIRC this can only happen if the user uses the poll API of the streams wrong. If so this function would return false even for 0rtt request.

We could just change this to expect.
The better way IMO would be to call is_0rtt in the new function and store the result in a new field for this function to return.

Also you could implement Is0rtt trait also for RecvStream

///
/// 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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not put self.recv.is_0rtt() in the trait impl of h3::server::Is0rtt? Then this function could be removed.

@iadev09
Copy link
Author

iadev09 commented Nov 12, 2025

Thanks a lot for the detailed review!

I initially introduced the Is0rtt trait in server/stream.rs just to make the 0-RTT capability explicit and clearly separated from data I/O. The idea was to show it as a simple additive feature rather than a core stream operation.

That said, your points about consistency make sense — I’ll move the trait into quic.rs, simplify the impls, and clean up the unwrap_or(false) logic accordingly. Let’s update this version and keep it aligned with the existing conventions.

@seanmonstar
Copy link
Member

I had a thought while looking at this. Does it have to be something known by h3? Like, do we need to make it a trait about it? I see in HTTP/3 RFC mention to take a precaution, like you said, but it doesn't otherwise interact with HTTP things.

What if you could peek at specific &S type in a RequestStream, and thus if you knew it was quinn, you could use the quinn method to look at it.

@Ruben2424
Copy link
Contributor

What if you could peek at specific &S type in a RequestStream, and thus if you knew it was quinn, you could use the quinn method to look at it.

Could be useful generally. But in this case having a trait could allow libraries also generic over quic transport to use this information for example to automatically return 425 status code. Maybe something for hyper?

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 hyperium#323
@iadev09
Copy link
Author

iadev09 commented Nov 12, 2025

For reference, this topic was also discussed in quinn-rs/quinn#2440
.
In that context we concluded that 0-RTT state is best observed at the stream level.

@iadev09 iadev09 requested a review from Ruben2424 November 12, 2025 23:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants