Skip to main content

moq_uring/quic/noq/
mod.rs

1//! The rustls-backed noq-proto stack on the worker's UDP path.
2//!
3//! TLS goes through rustls, which is what the rest of this workspace speaks,
4//! so a build selecting this backend links one TLS stack instead of two.
5//!
6//! [`moq_noq_proto::Endpoint`] holds
7//! the connection-id routing table, mints and retires ids, answers unsupported
8//! versions, and buffers half-open handshakes. So [`Endpoint`] here is mostly
9//! the socket plumbing around it, and the parts that are ours (the accept
10//! backlog, shard steering, and the driver task per connection.
11
12mod connection;
13mod endpoint;
14mod stream;
15
16pub use connection::Connection;
17pub use endpoint::Endpoint;
18pub use stream::{RecvStream, SendStream};
19
20pub(crate) use connection::{End, Shared};
21
22use std::sync::Arc;
23
24use moq_noq_proto::crypto::rustls::{QuicClientConfig, QuicServerConfig};
25use rustls::pki_types::pem::PemObject;
26use rustls::pki_types::{CertificateDer, PrivateKeyDer};
27
28#[cfg(feature = "qlog")]
29use super::qlog;
30use super::{Congestion, Error, Identity, SEGMENT, Transport, client, endpoint::CID_LEN, server};
31use crate::udp;
32
33/// Per-stream flow control credit.
34const STREAM_WINDOW: u32 = 4 * 1024 * 1024;
35/// Per-connection flow control credit.
36const CONNECTION_WINDOW: u32 = 16 * 1024 * 1024;
37/// How many datagrams to buffer in each direction.
38const DATAGRAM_WINDOW: usize = 64 * SEGMENT;
39
40fn ecn_to_noq(ecn: udp::Ecn) -> moq_noq_proto::EcnCodepoint {
41	match ecn {
42		udp::Ecn::Ect0 => moq_noq_proto::EcnCodepoint::Ect0,
43		udp::Ecn::Ect1 => moq_noq_proto::EcnCodepoint::Ect1,
44		udp::Ecn::Ce => moq_noq_proto::EcnCodepoint::Ce,
45	}
46}
47
48fn ecn_from_noq(ecn: moq_noq_proto::EcnCodepoint) -> udp::Ecn {
49	match ecn {
50		moq_noq_proto::EcnCodepoint::Ect0 => udp::Ecn::Ect0,
51		moq_noq_proto::EcnCodepoint::Ect1 => udp::Ecn::Ect1,
52		moq_noq_proto::EcnCodepoint::Ce => udp::Ecn::Ce,
53	}
54}
55
56impl From<moq_noq_proto::ConnectionError> for Error {
57	fn from(err: moq_noq_proto::ConnectionError) -> Self {
58		use moq_noq_proto::ConnectionError;
59		match err {
60			ConnectionError::ApplicationClosed(close) => Self::App {
61				code: close.error_code.into_inner(),
62				reason: String::from_utf8_lossy(&close.reason).into_owned(),
63			},
64			ConnectionError::ConnectionClosed(close) => Self::Transport {
65				code: close.error_code.into(),
66				reason: String::from_utf8_lossy(&close.reason).into_owned(),
67			},
68			ConnectionError::TransportError(err) => Self::Transport {
69				code: err.code.into(),
70				reason: err.reason.clone(),
71			},
72			ConnectionError::TimedOut => Self::TimedOut,
73			// The rest (a stateless reset, an unsupported version, exhausted
74			// ids) have no code to report, and a local close is published
75			// where it happens rather than waited for as an event.
76			err => Self::Quic(err.to_string()),
77		}
78	}
79}
80
81/// The crypto provider every config here is built from.
82///
83/// Built explicitly rather than read from the process-wide default, so a
84/// consumer that never installed one still gets working TLS.
85fn provider() -> Arc<rustls::crypto::CryptoProvider> {
86	static PROVIDER: std::sync::OnceLock<Arc<rustls::crypto::CryptoProvider>> = std::sync::OnceLock::new();
87	PROVIDER
88		.get_or_init(|| Arc::new(rustls::crypto::aws_lc_rs::default_provider()))
89		.clone()
90}
91
92/// The endpoint-wide configuration: how connection ids are minted, and the
93/// largest datagram we tell peers we can receive.
94pub(crate) fn endpoint_config(
95	shard: Option<moq_sock::shard::Shard>,
96) -> Result<Arc<moq_noq_proto::EndpointConfig>, Error> {
97	let mut config = moq_noq_proto::EndpointConfig::default();
98	config.cid_generator(Arc::new(move || Box::new(Cids { shard })));
99	config
100		.max_udp_payload_size(SEGMENT as u16)
101		.map_err(|err| Error::Quic(err.to_string()))?;
102	Ok(Arc::new(config))
103}
104
105/// Mints the endpoint's connection ids, steering prefix included.
106///
107/// noq-proto asks its generator for every id it issues, dials and rotations
108/// alike, so this is the one place the reuseport group's byte has to be
109/// stamped.
110#[derive(Debug)]
111struct Cids {
112	shard: Option<moq_sock::shard::Shard>,
113}
114
115impl moq_noq_proto::ConnectionIdGenerator for Cids {
116	fn generate_cid(&mut self) -> moq_noq_proto::ConnectionId {
117		moq_noq_proto::ConnectionId::new(&super::endpoint::cid(self.shard))
118	}
119
120	fn cid_len(&self) -> usize {
121		CID_LEN
122	}
123
124	fn cid_lifetime(&self) -> Option<std::time::Duration> {
125		None
126	}
127}
128
129/// Dial as `config` says.
130pub(crate) fn client_config(config: &client::Config) -> Result<moq_noq_proto::ClientConfig, Error> {
131	let provider = provider();
132	let builder = rustls::ClientConfig::builder_with_provider(provider.clone())
133		.with_protocol_versions(&[&rustls::version::TLS13])
134		.map_err(|err| Error::Tls(err.to_string()))?;
135
136	// Nothing is checked with verification off, so nothing is loaded: a root
137	// path that does not exist must not fail a connection that was never
138	// going to look at it.
139	let verifier: Arc<dyn rustls::client::danger::ServerCertVerifier> = match (config.verify, config.system_roots) {
140		(false, _) => Arc::new(NoVerify(provider.clone())),
141		// The platform verifier reads the OS trust store the way the OS means
142		// it, so extra roots go through it rather than around it.
143		(true, true) if config.roots.is_empty() => Arc::new(
144			rustls_platform_verifier::Verifier::new(provider.clone()).map_err(|err| Error::Tls(err.to_string()))?,
145		),
146		(true, true) => Arc::new(
147			rustls_platform_verifier::Verifier::new_with_extra_roots(read_roots(&config.roots)?, provider.clone())
148				.map_err(|err| Error::Tls(err.to_string()))?,
149		),
150		// Trusting only the configured roots means a store built from
151		// scratch, never the platform's with ours added on top.
152		(true, false) => rustls::client::WebPkiServerVerifier::builder_with_provider(
153			Arc::new(root_store(&config.roots)?),
154			provider.clone(),
155		)
156		.build()
157		.map_err(|err| Error::Tls(err.to_string()))?,
158	};
159	let builder = builder.dangerous().with_custom_certificate_verifier(verifier);
160
161	let mut tls = match &config.identity {
162		Some(identity) => {
163			let (chain, key) = keypair(identity)?;
164			builder
165				.with_client_auth_cert(chain, key)
166				.map_err(|err| Error::Tls(err.to_string()))?
167		}
168		None => builder.with_no_client_auth(),
169	};
170	tls.alpn_protocols = alpn(&config.alpn);
171
172	let crypto = QuicClientConfig::try_from(tls).map_err(|err| Error::Tls(err.to_string()))?;
173	let mut client = moq_noq_proto::ClientConfig::new(Arc::new(crypto));
174	let transport = transport_config(&config.transport)?;
175	#[cfg(feature = "qlog")]
176	let transport = with_qlog(transport, &config.transport);
177	client.transport_config(Arc::new(transport));
178	Ok(client)
179}
180
181/// Serve as `config` says.
182pub(crate) fn server_config(config: &server::Config) -> Result<moq_noq_proto::ServerConfig, Error> {
183	config.check()?;
184	let provider = provider();
185	let builder = rustls::ServerConfig::builder_with_provider(provider.clone())
186		.with_protocol_versions(&[&rustls::version::TLS13])
187		.map_err(|err| Error::Tls(err.to_string()))?;
188
189	let verifier = match config.client_auth.roots() {
190		None => rustls::server::WebPkiClientVerifier::no_client_auth(),
191		Some((roots, required)) => {
192			// Client certificates chain to the roots configured here, never to
193			// the platform store for public sites.
194			let builder =
195				rustls::server::WebPkiClientVerifier::builder_with_provider(Arc::new(root_store(roots)?), provider);
196			let builder = match required {
197				true => builder,
198				false => builder.allow_unauthenticated(),
199			};
200			builder.build().map_err(|err| Error::Tls(err.to_string()))?
201		}
202	};
203
204	let (chain, key) = keypair(&config.identity)?;
205	let mut tls = builder
206		.with_client_cert_verifier(verifier)
207		.with_single_cert(chain, key)
208		.map_err(|err| Error::Tls(err.to_string()))?;
209	tls.alpn_protocols = alpn(&config.alpn);
210
211	let crypto = QuicServerConfig::try_from(tls).map_err(|err| Error::Tls(err.to_string()))?;
212	let mut server = moq_noq_proto::ServerConfig::with_crypto(Arc::new(crypto));
213	let transport = transport_config(&config.transport)?;
214	#[cfg(feature = "qlog")]
215	let transport = with_qlog(transport, &config.transport);
216	server.transport_config(Arc::new(transport));
217	Ok(server)
218}
219
220/// Attach the configured qlog sink, if any.
221///
222/// Noq asks a factory per connection, so each gets a file of its own.
223#[cfg(feature = "qlog")]
224fn with_qlog(mut transport: moq_noq_proto::TransportConfig, config: &Transport) -> moq_noq_proto::TransportConfig {
225	let Some(sink) = config.qlog.clone() else {
226		return transport;
227	};
228
229	transport.qlog_factory(Arc::new(Traces { sink }));
230
231	transport
232}
233
234/// Opens one trace per connection, which is what noq-proto's factory hook is
235/// for.
236#[cfg(feature = "qlog")]
237#[derive(Debug)]
238struct Traces {
239	sink: qlog::Sink,
240}
241
242#[cfg(feature = "qlog")]
243impl moq_noq_proto::QlogFactory for Traces {
244	fn for_connection(
245		&self,
246		side: moq_noq_proto::Side,
247		_remote: std::net::SocketAddr,
248		initial_dst_cid: moq_noq_proto::ConnectionId,
249		_now: std::time::Instant,
250	) -> Option<moq_noq_proto::QlogConfig> {
251		let side = match side {
252			moq_noq_proto::Side::Client => qlog::Side::Client,
253			moq_noq_proto::Side::Server => qlog::Side::Server,
254		};
255		Some(moq_noq_proto::QlogConfig::new(self.sink.trace(&initial_dst_cid, side)))
256	}
257}
258
259/// The per-connection knobs both roles share.
260fn transport_config(config: &Transport) -> Result<moq_noq_proto::TransportConfig, Error> {
261	use moq_noq_proto::VarInt;
262
263	let idle = moq_noq_proto::IdleTimeout::try_from(config.idle_timeout)
264		.map_err(|_| Error::Quic(format!("idle timeout out of range: {:?}", config.idle_timeout)))?;
265	let streams = VarInt::from_u64(config.max_streams)
266		.map_err(|_| Error::Quic(format!("stream limit out of range: {}", config.max_streams)))?;
267
268	let mut transport = moq_noq_proto::TransportConfig::default();
269	transport.max_idle_timeout(Some(idle));
270	transport.keep_alive_interval(config.keep_alive);
271	transport.max_concurrent_bidi_streams(streams);
272	transport.max_concurrent_uni_streams(streams);
273	transport.stream_receive_window(STREAM_WINDOW.into());
274	transport.receive_window(CONNECTION_WINDOW.into());
275	transport.send_window(CONNECTION_WINDOW.into());
276	transport.datagram_receive_buffer_size(Some(DATAGRAM_WINDOW));
277	transport.datagram_send_buffer_size(DATAGRAM_WINDOW);
278	// Every datagram in a GSO train is one SEGMENT, so the packet size is not
279	// noq's to discover: pin it and turn the probing off.
280	transport.initial_mtu(SEGMENT as u16);
281	transport.min_mtu(SEGMENT as u16);
282	transport.mtu_discovery_config(None);
283	transport.congestion_controller_factory(match config.congestion {
284		Congestion::Loss => Arc::new(moq_noq_proto::congestion::CubicConfig::default())
285			as Arc<dyn moq_noq_proto::congestion::ControllerFactory + Send + Sync>,
286		Congestion::Delay => Arc::new(moq_noq_proto::congestion::Bbr3Config::default()),
287	});
288	Ok(transport)
289}
290
291/// ALPN protocols on the wire, which is a length-prefixed list of byte
292/// strings rather than the `String`s a caller configures.
293fn alpn(protocols: &[String]) -> Vec<Vec<u8>> {
294	protocols.iter().map(|proto| proto.as_bytes().to_vec()).collect()
295}
296
297/// Split an [`Identity`] into the chain and key rustls wants.
298fn keypair(identity: &Identity) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>), Error> {
299	let chain = CertificateDer::pem_slice_iter(identity.cert())
300		.collect::<Result<Vec<_>, _>>()
301		.map_err(|err| Error::Tls(format!("certificate: {err}")))?;
302	if chain.is_empty() {
303		return Err(Error::Tls("certificate chain holds no certificates".to_string()));
304	}
305	let key = PrivateKeyDer::from_pem_slice(identity.key()).map_err(|err| Error::Tls(format!("key: {err}")))?;
306	Ok((chain, key))
307}
308
309/// Read every PEM certificate in each root file, naming the one that fails.
310///
311/// A root is routinely a bundle of several CAs, and taking only the first
312/// would reject a peer chaining to any of the others while looking configured.
313/// A file holding none is an error rather than a store that trusts nothing.
314fn read_roots(paths: &[std::path::PathBuf]) -> Result<Vec<CertificateDer<'static>>, Error> {
315	let mut roots = Vec::new();
316	for path in paths {
317		let pem = std::fs::read(path).map_err(|err| Error::Tls(format!("{}: {err}", path.display())))?;
318		let certs = CertificateDer::pem_slice_iter(&pem)
319			.collect::<Result<Vec<_>, _>>()
320			.map_err(|err| Error::Tls(format!("{}: {err}", path.display())))?;
321		if certs.is_empty() {
322			return Err(Error::Tls(format!("{}: no certificates", path.display())));
323		}
324		roots.extend(certs);
325	}
326	Ok(roots)
327}
328
329/// A store holding exactly the roots `paths` names, and nothing else.
330fn root_store(paths: &[std::path::PathBuf]) -> Result<rustls::RootCertStore, Error> {
331	let mut store = rustls::RootCertStore::empty();
332	for root in read_roots(paths)? {
333		store.add(root).map_err(|err| Error::Tls(err.to_string()))?;
334	}
335	Ok(store)
336}
337
338/// Accepts any server certificate, for
339/// [`verify`](client::Config::verify) turned off.
340#[derive(Debug)]
341struct NoVerify(Arc<rustls::crypto::CryptoProvider>);
342
343impl rustls::client::danger::ServerCertVerifier for NoVerify {
344	fn verify_server_cert(
345		&self,
346		_end_entity: &CertificateDer<'_>,
347		_intermediates: &[CertificateDer<'_>],
348		_server_name: &rustls::pki_types::ServerName<'_>,
349		_ocsp: &[u8],
350		_now: rustls::pki_types::UnixTime,
351	) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
352		Ok(rustls::client::danger::ServerCertVerified::assertion())
353	}
354
355	fn verify_tls12_signature(
356		&self,
357		message: &[u8],
358		cert: &CertificateDer<'_>,
359		dss: &rustls::DigitallySignedStruct,
360	) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
361		rustls::crypto::verify_tls12_signature(message, cert, dss, &self.0.signature_verification_algorithms)
362	}
363
364	fn verify_tls13_signature(
365		&self,
366		message: &[u8],
367		cert: &CertificateDer<'_>,
368		dss: &rustls::DigitallySignedStruct,
369	) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
370		rustls::crypto::verify_tls13_signature(message, cert, dss, &self.0.signature_verification_algorithms)
371	}
372
373	fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
374		self.0.signature_verification_algorithms.supported_schemes()
375	}
376}