Skip to main content

tokio_quiche/http3/driver/
datagram.rs

1// Copyright (C) 2025, Cloudflare, Inc.
2// All rights reserved.
3//
4// Redistribution and use in source and binary forms, with or without
5// modification, are permitted provided that the following conditions are
6// met:
7//
8//     * Redistributions of source code must retain the above copyright notice,
9//       this list of conditions and the following disclaimer.
10//
11//     * Redistributions in binary form must reproduce the above copyright
12//       notice, this list of conditions and the following disclaimer in the
13//       documentation and/or other materials provided with the distribution.
14//
15// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
16// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
17// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
18// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
19// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
23// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
24// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
27use datagram_socket::DgramBuffer;
28use quiche::h3::NameValue;
29use quiche::h3::{
30    self,
31};
32
33use super::InboundFrame;
34use crate::buf_factory::BufFactory;
35use crate::quic::QuicheConnection;
36
37/// Extracts the DATAGRAM flow ID or quarter stream id proxied over the given
38/// `stream_id`, or `None` if this is not a proxy request.
39pub(crate) fn extract_quarter_stream_id(
40    stream_id: u64, headers: &[h3::Header],
41) -> Option<u64> {
42    let mut method = None;
43    let mut datagram_flow_id: Option<u64> = None;
44    let mut protocol = None;
45
46    for header in headers {
47        match header.name() {
48            b":method" => method = Some(header.value()),
49            b":protocol" => protocol = Some(header.value()),
50            b"datagram-flow-id" =>
51                datagram_flow_id = std::str::from_utf8(header.value())
52                    .ok()
53                    .and_then(|v| v.parse().ok()),
54            _ => {},
55        };
56
57        // We have all of the information needed to get a flow_id or
58        // quarter_stream_id
59        if method.is_some() && (datagram_flow_id.is_some() || protocol.is_some())
60        {
61            break;
62        }
63    }
64
65    // draft-ietf-masque-connect-udp-03 CONNECT-UDP
66    if method == Some(b"CONNECT-UDP") && datagram_flow_id.is_some() {
67        datagram_flow_id
68    // RFC 9298 CONNECT-UDP
69    } else if method == Some(b"CONNECT") && protocol.is_some() {
70        // we use the quarter_stream_id for RFC 9297
71        // https://www.rfc-editor.org/rfc/rfc9297.html#name-http-3-datagrams
72        Some(stream_id / 4)
73    } else {
74        None
75    }
76}
77
78/// Sends an HTTP/3 datagram over the QUIC connection with the given
79/// `quarter_stream_id`.
80#[inline]
81pub(crate) fn send_h3_dgram(
82    conn: &mut QuicheConnection, quarter_stream_id: u64, dgram: DgramBuffer,
83) -> quiche::Result<()> {
84    conn.dgram_send_buf(h3_dgram_add_quarter_stream_id(quarter_stream_id, dgram)?)
85}
86
87/// Prepend the `quarter_stream_id` to `dgram`
88fn h3_dgram_add_quarter_stream_id(
89    quarter_stream_id: u64, mut dgram: DgramBuffer,
90) -> quiche::Result<DgramBuffer> {
91    let mut prefix_buf = [0u8; 8];
92    let mut enc = octets::OctetsMut::with_slice(&mut prefix_buf);
93    let prefix = enc.put_varint(quarter_stream_id)?;
94
95    if dgram.try_add_prefix(prefix).is_err() {
96        // There wasn't enough room. Let's add more headroom and add the
97        // prefix again. DGRAM_HEADROOM is large enough, so
98        // try_add_prefix cannot fail after splice_headroom.
99        const {
100            // Note, since this is const, it asserts at compile time.
101            assert!(BufFactory::DGRAM_HEADROOM >= /* max varint len */ 8);
102        }
103        dgram.splice_headroom(BufFactory::DGRAM_HEADROOM);
104        dgram.try_add_prefix(prefix).unwrap();
105    }
106    Ok(dgram)
107}
108
109/// Strips the varint-encoded quarter stream ID from the front of `dgram` and
110/// returns `(quarter_stream_id, dgram)` with the cursor advanced past the
111/// prefix.
112fn h3_dgram_remove_quarter_stream_id(
113    mut dgram: DgramBuffer,
114) -> quiche::Result<(u64, DgramBuffer)> {
115    let mut b = octets::Octets::with_slice(dgram.as_slice());
116    let quarter_stream_id = b.get_varint()?;
117    // Advance the cursor past the varint prefix — zero copy.
118    let advance = b.off();
119    dgram.advance(advance);
120    Ok((quarter_stream_id, dgram))
121}
122
123/// Reads the next HTTP/3 datagram from the QUIC connection.
124///
125/// [`quiche::Error::Done`] is returned if there is no datagram to read.
126#[inline]
127pub(crate) fn receive_h3_dgram(
128    conn: &mut QuicheConnection,
129) -> quiche::Result<(u64, InboundFrame)> {
130    let dgram = conn.dgram_recv_buf()?;
131    let (quarter_stream_id, dgram) = h3_dgram_remove_quarter_stream_id(dgram)?;
132    Ok((quarter_stream_id, InboundFrame::Datagram(dgram)))
133}
134
135#[cfg(test)]
136mod tests {
137    use bytes::BufMut;
138    use datagram_socket::DgramBuffer;
139
140    use super::*;
141
142    #[test]
143    fn h3_dgram_add_quarter_stream_id_enough_headroom() {
144        let mut dgram = DgramBuffer::with_capacity_and_headroom(16, 8);
145        dgram.put_slice(&[0xaa, 0xbb, 0xcc]);
146
147        // 67 requires two bytes to encode
148        let result = h3_dgram_add_quarter_stream_id(67, dgram).unwrap();
149        assert_eq!(result.as_slice(), &[64, 67, 0xaa, 0xbb, 0xcc]);
150    }
151
152    /// When there is no headroom, splice_headroom is invoked automatically.
153    #[test]
154    fn h3_dgram_add_quarter_stream_id_need_more_headroom() {
155        let dgram = DgramBuffer::from_slice(&[1, 2]);
156
157        // 42 requires a single byte for encoding
158        let result = h3_dgram_add_quarter_stream_id(42, dgram).unwrap();
159        assert_eq!(result.as_slice(), &[42, 1, 2]);
160    }
161
162    #[test]
163    fn h3_dgram_remove_quarter_stream_id_tests() {
164        let dgram = DgramBuffer::from_slice(&[1, 2, 3, 4]);
165        let dgram = h3_dgram_add_quarter_stream_id(67, dgram).unwrap();
166        let (quarter_stream_id, rest) =
167            h3_dgram_remove_quarter_stream_id(dgram).unwrap();
168
169        assert_eq!(quarter_stream_id, 67);
170        assert_eq!(rest.as_slice(), &[1, 2, 3, 4]);
171    }
172
173    #[test]
174    fn h3_dgram_remove_quarter_stream_id_non_minimal_varint() {
175        // Quarter Stream ID = 0 encoded NON-minimally as a 2-byte varint (0x40
176        // 0x00), followed by application payload [1, 2, 3, 4].
177        // RFC 9000 §16 permits non-minimal encodings; quiche's get_varint()
178        // accepts them.
179        let dgram = DgramBuffer::from_slice(&[0x40, 0x00, 1, 2, 3, 4]);
180        let (qsid, rest) = h3_dgram_remove_quarter_stream_id(dgram).unwrap();
181        assert_eq!(qsid, 0);
182
183        assert_eq!(
184            rest.as_slice(),
185            &[1, 2, 3, 4],
186            "leftover non-minimal varint byte leaked into payload prefix"
187        );
188    }
189
190    /// remove_quarter_stream_id on an empty buffer returns an error (buffer too
191    /// short).
192    #[test]
193    fn remove_quarter_stream_id_empty_buffer_errors() {
194        let dgram = DgramBuffer::new();
195        assert!(h3_dgram_remove_quarter_stream_id(dgram).is_err());
196    }
197}