Skip to main content

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}