moq_uring/quic/mod.rs
1//! QUIC connections over the worker's UDP path, as a MoQ transport.
2//!
3//! [`client::connect`] and [`server::accept`] wrap a sans-IO QUIC stack
4//! around a [`udp::Socket`](crate::udp::Socket): a spawned driver task
5//! shuttles packets between the socket and the connection (GSO trains out,
6//! GRO coalesces in), arms the worker's userspace timers from the
7//! connection's timeout, and wakes stream waiters. The returned
8//! [`Connection`] implements [`web_transport_trait::poll`], so
9//! `moq_net::Client::connect_lite` / `Server::accept_lite` run real moq-lite
10//! sessions on the worker; everything is `Rc`-shared and `!Send` by design.
11//!
12//! An [`Endpoint`] serves many connections on one socket, demuxed by
13//! connection id; [`client::connect`] and [`server::accept`] are its
14//! single-connection shorthands. Native peers speak raw QUIC (the ALPN
15//! carries the application protocol); browsers negotiate `h3` and get the
16//! [`web`] layer's HTTP/3 CONNECT handshake on top of the same adapter, with
17//! [`web::Session`] as the one transport type covering both.
18//!
19//! Noq is the sans-IO QUIC stack underneath the worker.
20
21pub mod client;
22pub mod endpoint;
23#[cfg(feature = "qlog")]
24pub mod qlog;
25pub mod server;
26pub mod web;
27
28mod noq;
29
30pub use endpoint::Endpoint;
31pub use noq::{Connection, RecvStream, SendStream};
32
33/// The QUIC payload size every full datagram in a GSO train uses, and the
34/// stride GRO coalesces with.
35pub(crate) const SEGMENT: usize = 1350;
36
37/// A TLS certificate chain and the private key that signs for it, as PEM.
38/// One value, so neither half can be configured alone.
39///
40/// The bytes are read once and held, not re-read per connection: a worker
41/// group builds one of these before it spawns, so replacing the files on disk
42/// afterwards cannot leave two workers serving different identities.
43#[derive(Clone)]
44pub struct Identity {
45 cert: Vec<u8>,
46 key: Vec<u8>,
47}
48
49impl Identity {
50 /// Read the PEM chain at `cert` and the PEM key at `key`.
51 pub fn open(cert: impl AsRef<std::path::Path>, key: impl AsRef<std::path::Path>) -> Result<Self, Error> {
52 let read = |path: &std::path::Path| {
53 std::fs::read(path).map_err(|err| Error::Tls(format!("{}: {err}", path.display())))
54 };
55 Ok(Self {
56 cert: read(cert.as_ref())?,
57 key: read(key.as_ref())?,
58 })
59 }
60
61 /// The same, from PEM already in hand.
62 pub fn from_pem(cert: impl Into<Vec<u8>>, key: impl Into<Vec<u8>>) -> Self {
63 Self {
64 cert: cert.into(),
65 key: key.into(),
66 }
67 }
68
69 /// The PEM certificate chain being presented, for a caller that publishes
70 /// its fingerprint. The key is deliberately not readable back out.
71 pub fn cert(&self) -> &[u8] {
72 &self.cert
73 }
74
75 /// The PEM private key, for the backend loading it into its TLS stack.
76 pub(crate) fn key(&self) -> &[u8] {
77 &self.key
78 }
79}
80
81impl std::fmt::Debug for Identity {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 // Whatever else gets logged, the private key does not.
84 f.debug_struct("Identity")
85 .field("cert", &format_args!("{} PEM bytes", self.cert.len()))
86 .finish_non_exhaustive()
87 }
88}
89
90/// The per-connection transport knobs, the same for either role.
91///
92/// Separate from the role configs so a caller that already has these
93/// settings (a relay applying its `--quic-*` section, say) sets them once and
94/// hands the same value to a dial and a listener.
95#[derive(Clone, Debug)]
96#[non_exhaustive]
97pub struct Transport {
98 /// Close the connection after this long without activity.
99 pub idle_timeout: std::time::Duration,
100 /// The most streams of each kind (bidirectional and unidirectional) a peer
101 /// may have open at once. MoQ opens a stream per group, so busy endpoints
102 /// want this high.
103 pub max_streams: u64,
104 /// Which congestion controller to run.
105 pub congestion: Congestion,
106 /// How often to send an ack-eliciting packet on an otherwise idle
107 /// connection, or `None` (the default) to send none and let the idle
108 /// timeout decide.
109 pub keep_alive: Option<std::time::Duration>,
110 /// Where to write qlog traces, or `None` (the default) to write none.
111 ///
112 /// Noq takes one sink per configuration and writes one file per connection,
113 /// named from that connection's Initial destination connection ID.
114 ///
115 /// Only compiled with the `qlog` feature, so a build without it cannot ask
116 /// for traces the backends would not produce.
117 #[cfg(feature = "qlog")]
118 pub qlog: Option<qlog::Sink>,
119}
120
121impl Default for Transport {
122 fn default() -> Self {
123 Self {
124 idle_timeout: std::time::Duration::from_secs(10),
125 max_streams: 1024,
126 congestion: Congestion::default(),
127 keep_alive: None,
128 #[cfg(feature = "qlog")]
129 qlog: None,
130 }
131 }
132}
133
134/// The congestion control family a connection runs.
135///
136/// Noq uses CUBIC for loss-based control and BBRv3 for delay-based control.
137///
138/// The default is [`Loss`](Self::Loss), which is what noq runs
139/// unasked. An application carrying live media wants [`Delay`](Self::Delay)
140/// and should say so; the relay does.
141#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
142#[non_exhaustive]
143pub enum Congestion {
144 /// Loss-based: grows until it drops packets, so the send rate sawtooths.
145 #[default]
146 Loss,
147 /// Delay-based: tracks the measured delivery rate and RTT instead of
148 /// waiting for loss, which keeps queues short and the send rate steady
149 /// enough for an encoder to track.
150 Delay,
151}
152
153/// Why a connection could not be set up or has ended.
154///
155/// One error type for the whole module: it is also what every
156/// [`web_transport_trait::poll`] operation on a [`Connection`] reports, which
157/// is how the close reason reaches `moq_net::Error::from_transport`.
158#[derive(Clone, Debug, thiserror::Error)]
159#[non_exhaustive]
160pub enum Error {
161 /// The application (ours or the peer's) closed the connection.
162 #[error("application closed: code={code} reason={reason:?}")]
163 App {
164 /// The application close code (a MoQ session code here).
165 code: u64,
166 /// The UTF-8 lossy close reason.
167 reason: String,
168 },
169 /// QUIC closed the connection with a transport-level code.
170 #[error("transport closed: code={code} reason={reason:?}")]
171 Transport {
172 /// The QUIC transport error code.
173 code: u64,
174 /// The UTF-8 lossy close reason.
175 reason: String,
176 },
177 /// The connection idled out or the handshake never completed.
178 #[error("connection timed out")]
179 TimedOut,
180 /// The peer reset the stream with this code.
181 #[error("stream reset: {0}")]
182 Reset(u64),
183 /// The peer told us to stop sending with this code.
184 #[error("stream stopped: {0}")]
185 Stop(u64),
186 /// The TLS material a connection needs could not be loaded.
187 #[error("tls error: {0}")]
188 Tls(String),
189 /// The socket died underneath the connection.
190 #[error("socket error: {0}")]
191 Io(String),
192 /// The QUIC stack refused an operation. The backend's own message, since
193 /// the two describe the same failures differently.
194 #[error("quic error: {0}")]
195 Quic(String),
196 /// Accepting needs the server configuration the endpoint was built
197 /// without.
198 #[error("endpoint has no server configuration")]
199 NotServer,
200 /// The WebTransport (HTTP/3) layer failed: a broken handshake, or a
201 /// stream that could not be framed.
202 #[error("webtransport error: {0}")]
203 Web(String),
204 /// HTTP/3 failed with a code of its own, one that names no WebTransport
205 /// error (`H3_NO_ERROR`, say). Neither trait accessor reports it, because
206 /// it is not a code the peer's application chose.
207 #[error("http/3 closed: code={code} reason={reason:?}")]
208 Http3 {
209 /// The HTTP/3 error code.
210 code: u64,
211 /// The UTF-8 lossy close reason, empty for a stream-level code.
212 reason: String,
213 },
214 /// qlog traces were asked for and cannot be captured: the directory is
215 /// missing or unwritable, or the writer thread could not be started.
216 #[error("qlog error: {0}")]
217 Qlog(String),
218}
219
220impl web_transport_trait::Error for Error {
221 fn session_error(&self) -> Option<(u32, String)> {
222 match self {
223 Self::App { code, reason } => Some((u32::try_from(*code).unwrap_or(u32::MAX), reason.clone())),
224 _ => None,
225 }
226 }
227
228 fn stream_error(&self) -> Option<u32> {
229 match self {
230 Self::Reset(code) | Self::Stop(code) => Some(u32::try_from(*code).unwrap_or(u32::MAX)),
231 _ => None,
232 }
233 }
234}