From ff4c844f2fff618cfcc6875b1f587736aa5f3b6b Mon Sep 17 00:00:00 2001 From: Joe Dye Date: Thu, 11 Jun 2026 16:40:18 +0100 Subject: [PATCH 1/3] tests: datagrams should only be sent if both local and peer have sent SETTINGS_H3_DATAGRAM --- h3-datagram/src/datagram_handler.rs | 64 +++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/h3-datagram/src/datagram_handler.rs b/h3-datagram/src/datagram_handler.rs index c1ef9b6f..2aef5d6f 100644 --- a/h3-datagram/src/datagram_handler.rs +++ b/h3-datagram/src/datagram_handler.rs @@ -137,3 +137,67 @@ impl Display for SendDatagramError { } impl Error for SendDatagramError {} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::datagram::EncodedDatagram; + use h3::proto::frame; + + use bytes::Bytes; + + mod sender { + use super::*; + + pub fn create(shared: Arc) -> DatagramSender { + DatagramSender { + handler: CountingSender::default(), + _marker: PhantomData, + shared_state: shared, + stream_id: StreamId::try_from(0u64).unwrap(), + } + } + + #[derive(Default)] + pub struct CountingSender { + pub calls: usize, + } + + impl SendDatagram for CountingSender { + fn send_datagram>>( + &mut self, + _data: T, + ) -> Result<(), SendDatagramErrorIncoming> { + self.calls += 1; + Ok(()) + } + } + } + + #[test] + fn refuses_send_when_datagrams_not_negotiated() { + let mut s = sender::create(Arc::new(SharedState::default())); + let res = s.send_datagram(Bytes::from_static(b"hi")); + + assert!(res.is_err()); + assert!(matches!(res.unwrap_err(), SendDatagramError::NotAvailable)); + assert_eq!(s.handler.calls, 0); + } + + #[test] + fn sends_when_datagrams_negotiated() { + let mut s = { + let mut settings = frame::Settings::default(); + settings.insert(frame::SettingId::H3_DATAGRAM, 1).unwrap(); + + let shared_settings = Arc::new(SharedState::default()); + shared_settings.set_settings((&settings).into()); + + sender::create(shared_settings) + }; + + s.send_datagram(Bytes::from_static(b"hi")).unwrap(); + assert_eq!(s.handler.calls, 1, "should delegate to the QUIC backend"); + } +} From 778b6f42432a0599f715a9cacdbbda03d4a0e035 Mon Sep 17 00:00:00 2001 From: Joe Dye Date: Thu, 11 Jun 2026 18:10:10 +0100 Subject: [PATCH 2/3] fixes: `SharedState` contains the combined gate of our datagram enabled setting and the peers datagram enabled setting --- h3-datagram/src/datagram_handler.rs | 10 ++++++++++ h3/src/connection.rs | 15 ++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/h3-datagram/src/datagram_handler.rs b/h3-datagram/src/datagram_handler.rs index 2aef5d6f..3c929a7a 100644 --- a/h3-datagram/src/datagram_handler.rs +++ b/h3-datagram/src/datagram_handler.rs @@ -40,6 +40,16 @@ where { /// Sends a datagram pub fn send_datagram(&mut self, data: B) -> Result<(), SendDatagramError> { + //= https://www.rfc-editor.org/rfc/rfc9297#section-2.1.1 + //# QUIC DATAGRAM frames MUST NOT be sent until the SETTINGS_H3_DATAGRAM + //# setting has been both sent and received with a value of 1. + // + // `settings()` holds the negotiated value: the peer's advertisement + // ANDed with our own (see the SETTINGS handling in `h3::connection`). + if !self.settings().enable_datagram() { + return Err(SendDatagramError::NotAvailable); + } + let encoded_datagram = Datagram::new(self.stream_id, data); match self.handler.send_datagram(encoded_datagram.encode()) { Ok(()) => Ok(()), diff --git a/h3/src/connection.rs b/h3/src/connection.rs index 25ab6b03..adf6703c 100644 --- a/h3/src/connection.rs +++ b/h3/src/connection.rs @@ -14,7 +14,7 @@ use stream::WriteBuf; use tracing::{instrument, warn}; use crate::{ - config::Config, + config::{Config, Settings}, error::{ connection_error_creators::{ CloseRawQuicConnection, CloseStream, HandleFrameStreamErrorOnRequestStream, @@ -579,14 +579,19 @@ where ), ))); } - Ok(Some(Frame::Settings(settings))) => { + Ok(Some(Frame::Settings(peer_settings))) => { if !self.got_peer_settings { // Received settings frame - self.got_peer_settings = true; - self.set_settings((&settings).into()); - Frame::Settings(settings) + //= https://www.rfc-editor.org/rfc/rfc9297#section-2.1.1 + //# QUIC DATAGRAM frames MUST NOT be sent until the SETTINGS_H3_DATAGRAM + //# setting has been both sent and received with a value of 1. + let mut negotiated = Settings::from(&peer_settings); + negotiated.enable_datagram &= self.config.settings.enable_datagram; + self.set_settings(negotiated); + + Frame::Settings(peer_settings) } else { //= https://www.rfc-editor.org/rfc/rfc9114#section-7.2.4 //# If an endpoint receives a second SETTINGS From ca01c4474aeba1ec9c1df3cd763631b530c632eb Mon Sep 17 00:00:00 2001 From: Joe Dye Date: Thu, 11 Jun 2026 18:08:00 +0100 Subject: [PATCH 3/3] enable duvet for h3-datagrams --- .duvet/config.toml | 6 + .duvet/snapshot.txt | 165 ++++++++++++++++++++++++++++ h3-datagram/src/datagram.rs | 21 ++-- h3-datagram/src/datagram_handler.rs | 8 ++ 4 files changed, 193 insertions(+), 7 deletions(-) diff --git a/.duvet/config.toml b/.duvet/config.toml index 8482911b..5a8c7a01 100644 --- a/.duvet/config.toml +++ b/.duvet/config.toml @@ -6,6 +6,12 @@ pattern = "h3/**/*.rs" [[specification]] source = "https://www.rfc-editor.org/rfc/rfc9114" +[[source]] +pattern = "h3-datagram/**/*.rs" + +[[specification]] +source = "https://www.rfc-editor.org/rfc/rfc9297" + [report.html] enabled = true issue-link = "https://github.com/hyperium/h3/issues" diff --git a/.duvet/snapshot.txt b/.duvet/snapshot.txt index a11d810f..a71441a8 100644 --- a/.duvet/snapshot.txt +++ b/.duvet/snapshot.txt @@ -865,3 +865,168 @@ SPECIFICATION: https://www.rfc-editor.org/rfc/rfc9114 TEXT[!MUST,exception]: values of N (that is, 0x21, 0x40, ..., through 0x3ffffffffffffffe) TEXT[!MUST,exception]: MUST NOT be assigned by IANA and MUST NOT appear in the listing of TEXT[!MUST,exception]: assigned values. + +SPECIFICATION: https://www.rfc-editor.org/rfc/rfc9297 + SECTION: [HTTP Datagrams](#section-2) + TEXT[!MUST]: HTTP Datagrams MUST only be sent with an association to an HTTP + TEXT[!MUST]: request that explicitly supports them. + TEXT[!MUST]: If an HTTP Datagram is received and it is associated with a request + TEXT[!MUST]: that has no known semantics for HTTP Datagrams, the receiver MUST + TEXT[!MUST]: terminate the request. + TEXT[!MUST]: If HTTP/3 is in use, the request stream MUST + TEXT[!MUST]: be aborted with H3_DATAGRAM_ERROR (0x33). + TEXT[!MAY]: HTTP extensions MAY + TEXT[!MAY]: override these requirements by defining a negotiation mechanism and + TEXT[!MAY]: semantics for HTTP Datagrams. + + SECTION: [HTTP/3 Datagrams](#section-2.1) + TEXT[implementation]: Quarter Stream ID: A variable-length integer that contains the value + TEXT[implementation]: of the client-initiated bidirectional stream that this datagram is + TEXT[implementation]: associated with divided by four (the division by four stems from + TEXT[implementation]: the fact that HTTP requests are sent on client-initiated + TEXT[implementation]: bidirectional streams, which have stream IDs that are divisible by + TEXT[implementation]: four). The largest legal QUIC stream ID value is 2^62-1, so the + TEXT[implementation]: largest legal value of the Quarter Stream ID field is 2^60-1. + TEXT[!MUST,implementation]: Receipt of an HTTP/3 Datagram that includes a larger value MUST be + TEXT[!MUST,implementation]: treated as an HTTP/3 connection error of type H3_DATAGRAM_ERROR + TEXT[!MUST,implementation]: (0x33). + TEXT[!MUST,implementation]: Receipt of a QUIC DATAGRAM frame whose payload is too short to allow + TEXT[!MUST,implementation]: parsing the Quarter Stream ID field MUST be treated as an HTTP/3 + TEXT[!MUST,implementation]: connection error of type H3_DATAGRAM_ERROR (0x33). + TEXT[!MUST]: HTTP/3 Datagrams MUST NOT be sent unless the corresponding stream's + TEXT[!MUST]: send side is open. + TEXT[!MUST]: If a datagram is received after the corresponding + TEXT[!MUST]: stream's receive side is closed, the received datagrams MUST be + TEXT[!MUST]: silently dropped. + TEXT[!MUST]: If an HTTP/3 Datagram is received and its Quarter Stream ID field + TEXT[!MUST]: maps to a stream that has not yet been created, the receiver SHALL + TEXT[!MUST]: either drop that datagram silently or buffer it temporarily (on the + TEXT[!MUST]: order of a round trip) while awaiting the creation of the + TEXT[!MUST]: corresponding stream. + TEXT[!SHOULD]: If an HTTP/3 Datagram is received and its Quarter Stream ID field + TEXT[!SHOULD]: maps to a stream that cannot be created due to client-initiated + TEXT[!SHOULD]: bidirectional stream limits, it SHOULD be treated as an HTTP/3 + TEXT[!SHOULD]: connection error of type H3_ID_ERROR. + TEXT[!MAY]: Future extensions MAY define how to prioritize datagrams and MAY + TEXT[!MAY]: define signaling to allow communicating prioritization preferences. + + SECTION: [The SETTINGS_H3_DATAGRAM HTTP/3 Setting](#section-2.1.1) + TEXT[!MUST]: The value of the SETTINGS_H3_DATAGRAM setting MUST be either 0 or 1. + TEXT[!MUST]: If the SETTINGS_H3_DATAGRAM setting is + TEXT[!MUST]: received with a value that is neither 0 nor 1, the receiver MUST + TEXT[!MUST]: terminate the connection with error H3_SETTINGS_ERROR. + TEXT[!MUST,implementation,test]: QUIC DATAGRAM frames MUST NOT be sent until the SETTINGS_H3_DATAGRAM + TEXT[!MUST,implementation,test]: setting has been both sent and received with a value of 1. + TEXT[!MAY]: When clients use 0-RTT, they MAY store the value of the server's + TEXT[!MAY]: SETTINGS_H3_DATAGRAM setting. + TEXT[!MUST]: When servers decide to accept + TEXT[!MUST]: 0-RTT data, they MUST send a SETTINGS_H3_DATAGRAM setting greater + TEXT[!MUST]: than or equal to the value they sent to the client in the connection + TEXT[!MUST]: where they sent them the NewSessionTicket message. + TEXT[!MUST]: If a client + TEXT[!MUST]: stores the value of the SETTINGS_H3_DATAGRAM setting with their 0-RTT + TEXT[!MUST]: state, they MUST validate that the new value of the + TEXT[!MUST]: SETTINGS_H3_DATAGRAM setting sent by the server in the handshake is + TEXT[!MUST]: greater than or equal to the stored value; if not, the client MUST + TEXT[!MUST]: terminate the connection with error H3_SETTINGS_ERROR. + TEXT[!SHOULD]: It is RECOMMENDED that implementations that support receiving HTTP/3 + TEXT[!SHOULD]: Datagrams always send the SETTINGS_H3_DATAGRAM setting with a value + TEXT[!SHOULD]: of 1, even if the application does not intend to use HTTP/3 + TEXT[!SHOULD]: Datagrams. + + SECTION: [The Capsule Protocol](#section-3.2) + TEXT[!SHOULD]: Because new protocols or extensions might define new Capsule Types, + TEXT[!SHOULD]: intermediaries that wish to allow for future extensibility SHOULD + TEXT[!SHOULD]: forward Capsules without modification unless the definition of the + TEXT[!SHOULD]: Capsule Type in use specifies additional intermediary processing. + TEXT[!SHOULD]: In + TEXT[!SHOULD]: particular, intermediaries SHOULD forward Capsules with an unknown + TEXT[!SHOULD]: Capsule Type without modification. + TEXT[!MUST]: Endpoints that receive a Capsule with an unknown Capsule Type MUST + TEXT[!MUST]: silently drop that Capsule and skip over it to parse the next + TEXT[!MUST]: Capsule. + TEXT[!MAY]: A future extension MAY + TEXT[!MAY]: define a new Capsule Type to carry HTTP content. + TEXT[!MUST]: The Capsule Protocol MUST NOT be used with messages that contain + TEXT[!MUST]: Content-Length, Content-Type, or Transfer-Encoding header fields. + TEXT[!MUST]: Additionally, HTTP status codes 204 (No Content), 205 (Reset + TEXT[!MUST]: Content), and 206 (Partial Content) MUST NOT be sent on responses + TEXT[!MUST]: that use the Capsule Protocol. + TEXT[!MUST]: A receiver that observes a violation + TEXT[!MUST]: of these requirements MUST treat the HTTP message as malformed. + TEXT[!SHOULD]: This approach SHOULD be avoided because it can consume + TEXT[!SHOULD]: flow control in underlying layers, and that might lead to deadlocks + TEXT[!SHOULD]: if the Capsule data exhausts the flow control window. + + SECTION: [Error Handling](#section-3.3) + TEXT[!MUST]: When a receiver encounters an error processing the Capsule Protocol, + TEXT[!MUST]: the receiver MUST treat it as if it had received a malformed or + TEXT[!MUST]: incomplete HTTP message. + TEXT[!MUST]: Each Capsule's payload MUST contain exactly the fields identified in + TEXT[!MUST]: its description. + TEXT[!MUST]: A Capsule payload that contains additional bytes + TEXT[!MUST]: after the identified fields or a Capsule payload that terminates + TEXT[!MUST]: before the end of the identified fields MUST be treated as it if were + TEXT[!MUST]: a malformed or incomplete message. + TEXT[!MUST]: In particular, redundant length + TEXT[!MUST]: encodings MUST be verified to be self-consistent. + TEXT[!MUST]: If the receive side of a stream carrying Capsules is terminated + TEXT[!MUST]: cleanly (for example, in HTTP/3 this is defined as receiving a QUIC + TEXT[!MUST]: STREAM frame with the FIN bit set) and the last Capsule on the stream + TEXT[!MUST]: was truncated, this MUST be treated as if it were a malformed or + TEXT[!MUST]: incomplete message. + + SECTION: [The Capsule-Protocol Header Field](#section-3.4) + TEXT[!MUST]: Its value MUST be a Boolean; any + TEXT[!MUST]: other value type MUST be handled as if the field were not present by + TEXT[!MUST]: recipients (for example, if this field is included multiple times, + TEXT[!MUST]: its type will become a List and the field will be ignored). + TEXT[!MUST]: Receivers MUST ignore unknown parameters. + TEXT[!MAY]: Intermediaries MAY use this header field to allow processing of HTTP + TEXT[!MAY]: Datagrams for unknown HTTP upgrade tokens. + TEXT[!MUST]: The Capsule-Protocol header field MUST NOT be used on HTTP responses + TEXT[!MUST]: with a status code that is both different from 101 (Switching + TEXT[!MUST]: Protocols) and outside the 2xx (Successful) range. + TEXT[!SHOULD]: When using the Capsule Protocol, HTTP endpoints SHOULD send the + TEXT[!SHOULD]: Capsule-Protocol header field to simplify intermediary processing. + TEXT[!MAY]: Definitions of new HTTP upgrade tokens that use the Capsule Protocol + TEXT[!MAY]: MAY alter this recommendation. + + SECTION: [The DATAGRAM Capsule](#section-3.5) + TEXT[!MAY]: In + TEXT[!MAY]: other words, an intermediary MAY send a DATAGRAM Capsule to forward + TEXT[!MAY]: an HTTP Datagram that was received in a QUIC DATAGRAM frame and vice + TEXT[!MAY]: versa. + TEXT[!MUST]: Intermediaries MUST NOT perform this re-encoding unless they + TEXT[!MUST]: have identified the use of the Capsule Protocol on the corresponding + TEXT[!MUST]: request stream; see Section 3.2. + TEXT[!SHOULD]: If an intermediary receives an HTTP Datagram in a QUIC DATAGRAM frame + TEXT[!SHOULD]: and is forwarding it on a connection that supports QUIC DATAGRAM + TEXT[!SHOULD]: frames, the intermediary SHOULD NOT convert that HTTP Datagram to a + TEXT[!SHOULD]: DATAGRAM Capsule. + TEXT[!SHOULD]: If the HTTP Datagram is too large to fit in a + TEXT[!SHOULD]: DATAGRAM frame (for example, because the Path MTU (PMTU) of that QUIC + TEXT[!SHOULD]: connection is too low or if the maximum UDP payload size advertised + TEXT[!SHOULD]: on that connection is too low), the intermediary SHOULD drop the HTTP + TEXT[!SHOULD]: Datagram instead of converting it to a DATAGRAM Capsule. + TEXT[!SHOULD]: Implementations SHOULD take those limits into account when parsing + TEXT[!SHOULD]: DATAGRAM Capsules. + TEXT[!SHOULD]: If an incoming DATAGRAM Capsule has a length that + TEXT[!SHOULD]: is known to be so large as to not be usable, the implementation + TEXT[!SHOULD]: SHOULD discard the Capsule without buffering its contents into + TEXT[!SHOULD]: memory. + TEXT[!SHOULD]: This SHOULD be avoided, because it can + TEXT[!SHOULD]: cause flow control problems; see Section 3.2. + TEXT[!SHOULD]: However, new HTTP extensions that + TEXT[!SHOULD]: wish to use HTTP Datagrams SHOULD use the Capsule Protocol, as + TEXT[!SHOULD]: failing to do so will make it harder for the HTTP extension to + TEXT[!SHOULD]: support versions of HTTP other than HTTP/3 and will prevent + TEXT[!SHOULD]: interoperability with intermediaries that only support the Capsule + TEXT[!SHOULD]: Protocol. + + SECTION: [Capsule Types](#section-5.4) + TEXT[!MUST]: In addition to those common fields, all + TEXT[!MUST]: registrations in this registry MUST include a "Capsule Type" field + TEXT[!MUST]: that contains a short name or label for the Capsule Type. + TEXT[!MUST]: These values MUST NOT be assigned by IANA + TEXT[!MUST]: and MUST NOT appear in the listing of assigned values. diff --git a/h3-datagram/src/datagram.rs b/h3-datagram/src/datagram.rs index 6516df58..67ac118e 100644 --- a/h3-datagram/src/datagram.rs +++ b/h3-datagram/src/datagram.rs @@ -33,18 +33,25 @@ where /// Decodes a datagram frame from the QUIC datagram pub fn decode(mut buf: B) -> Result { + //= https://www.rfc-editor.org/rfc/rfc9297#section-2.1 + //# Receipt of a QUIC DATAGRAM frame whose payload is too short to allow + //# parsing the Quarter Stream ID field MUST be treated as an HTTP/3 + //# connection error of type H3_DATAGRAM_ERROR (0x33). let q_stream_id = VarInt::decode(&mut buf).map_err(|_| { InternalConnectionError::new(Code::H3_DATAGRAM_ERROR, "invalid stream id".to_string()) })?; //= https://www.rfc-editor.org/rfc/rfc9297#section-2.1 - // Quarter Stream ID: A variable-length integer that contains the value of the client-initiated bidirectional - // stream that this datagram is associated with divided by four (the division by four stems - // from the fact that HTTP requests are sent on client-initiated bidirectional streams, - // which have stream IDs that are divisible by four). The largest legal QUIC stream ID - // value is 262-1, so the largest legal value of the Quarter Stream ID field is 260-1. - // Receipt of an HTTP/3 Datagram that includes a larger value MUST be treated as an HTTP/3 - // connection error of type H3_DATAGRAM_ERROR (0x33). + //# Quarter Stream ID: A variable-length integer that contains the value + //# of the client-initiated bidirectional stream that this datagram is + //# associated with divided by four (the division by four stems from + //# the fact that HTTP requests are sent on client-initiated + //# bidirectional streams, which have stream IDs that are divisible by + //# four). The largest legal QUIC stream ID value is 2^62-1, so the + //# largest legal value of the Quarter Stream ID field is 2^60-1. + //# Receipt of an HTTP/3 Datagram that includes a larger value MUST be + //# treated as an HTTP/3 connection error of type H3_DATAGRAM_ERROR + //# (0x33). let stream_id = StreamId::try_from(u64::from(q_stream_id) * 4).map_err(|_| { InternalConnectionError::new(Code::H3_DATAGRAM_ERROR, "invalid stream id".to_string()) })?; diff --git a/h3-datagram/src/datagram_handler.rs b/h3-datagram/src/datagram_handler.rs index 3c929a7a..58d7d8be 100644 --- a/h3-datagram/src/datagram_handler.rs +++ b/h3-datagram/src/datagram_handler.rs @@ -185,6 +185,10 @@ mod tests { } } + //= https://www.rfc-editor.org/rfc/rfc9297#section-2.1.1 + //= type=test + //# QUIC DATAGRAM frames MUST NOT be sent until the SETTINGS_H3_DATAGRAM + //# setting has been both sent and received with a value of 1. #[test] fn refuses_send_when_datagrams_not_negotiated() { let mut s = sender::create(Arc::new(SharedState::default())); @@ -195,6 +199,10 @@ mod tests { assert_eq!(s.handler.calls, 0); } + //= https://www.rfc-editor.org/rfc/rfc9297#section-2.1.1 + //= type=test + //# QUIC DATAGRAM frames MUST NOT be sent until the SETTINGS_H3_DATAGRAM + //# setting has been both sent and received with a value of 1. #[test] fn sends_when_datagrams_negotiated() { let mut s = {