Skip to main content

moq_native/
server.rs

1use std::net;
2#[cfg(any(test, all(feature = "uds", unix)))]
3use std::path::PathBuf;
4
5#[cfg(feature = "iroh")]
6use crate::iroh;
7use crate::{Error, QuicBackend};
8use moq_net::Session;
9use url::Url;
10
11use futures::FutureExt;
12use futures::future::BoxFuture;
13use futures::stream::FuturesUnordered;
14use futures::stream::StreamExt;
15
16/// Configuration for the MoQ server.
17#[derive(clap::Args, Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
18#[serde(deny_unknown_fields, default)]
19#[non_exhaustive]
20pub struct ServerConfig {
21	/// Listen for QUIC (UDP) on the given address. Defaults to `[::]:443`.
22	///
23	/// Accepts standard socket address syntax (e.g. `[::]:443`) or a DNS
24	/// `host:port` pair (e.g. `fly-global-services:443`), resolved at bind time
25	/// (first address only; Quinn cannot bind multiple). Leave unset while a
26	/// `tcp`/`unix` listener is configured to run a stream-only server with no
27	/// QUIC.
28	#[serde(alias = "listen")]
29	#[arg(id = "server-bind", long = "server-bind", alias = "listen", env = "MOQ_SERVER_BIND")]
30	pub bind: Option<String>,
31
32	/// Plaintext qmux TCP listener (`--server-tcp-bind`, no TLS). Requires the
33	/// `tcp` feature.
34	#[cfg(feature = "tcp")]
35	#[command(flatten)]
36	#[serde(default)]
37	pub tcp: crate::tcp::Config,
38
39	/// Plaintext qmux Unix-socket listener (`--server-unix-bind`) with an optional
40	/// peer-credential allowlist. Requires the `uds` feature; unix-only.
41	#[cfg(all(feature = "uds", unix))]
42	#[command(flatten)]
43	#[serde(default)]
44	pub unix: crate::unix::Config,
45
46	/// The QUIC backend to use.
47	/// Auto-detected from compiled features if not specified.
48	#[arg(id = "server-backend", long = "server-backend", env = "MOQ_SERVER_BACKEND")]
49	pub backend: Option<QuicBackend>,
50
51	/// QUIC transport tuning (`--server-quic-*`): stream limits, GSO, timeouts,
52	/// plus the accept-side knobs (preferred address, QUIC-LB connection IDs).
53	#[command(flatten)]
54	#[serde(default)]
55	pub quic: crate::quic::Server,
56
57	/// Restrict the server to specific MoQ protocol version(s).
58	///
59	/// By default, the server accepts all supported versions.
60	/// Use this to restrict to specific versions, e.g. `--server-version moq-lite-02`.
61	/// Can be specified multiple times to accept a subset of versions.
62	///
63	/// Valid values: moq-lite-01, moq-lite-02, moq-lite-03, moq-transport-14, moq-transport-15, moq-transport-16
64	#[serde(default, skip_serializing_if = "Vec::is_empty")]
65	#[arg(id = "server-version", long = "server-version", env = "MOQ_SERVER_VERSION")]
66	pub version: Vec<moq_net::Version>,
67
68	/// The certificates to serve and the roots that authenticate mTLS clients
69	/// (`--server-tls-*`).
70	#[command(flatten)]
71	#[serde(default)]
72	pub tls: crate::tls::Server,
73}
74
75impl ServerConfig {
76	/// Build the [`Server`] this config describes, binding its listeners.
77	pub fn init(self) -> crate::Result<Server> {
78		Server::new(self)
79	}
80
81	/// Returns the configured versions, defaulting to all if none specified.
82	pub fn versions(&self) -> moq_net::Versions {
83		if self.version.is_empty() {
84			moq_net::Versions::all()
85		} else {
86			moq_net::Versions::from(self.version.clone())
87		}
88	}
89
90	/// Whether a `tcp`/`unix` stream listener is configured.
91	///
92	/// When true and [`bind`](Self::bind) is unset, the server runs stream-only
93	/// (no default QUIC listener).
94	#[allow(unused_mut)]
95	fn has_stream_listener(&self) -> bool {
96		let mut has = false;
97		#[cfg(feature = "tcp")]
98		{
99			has |= self.tcp.bind.is_some();
100		}
101		#[cfg(all(feature = "uds", unix))]
102		{
103			has |= self.unix.bind.is_some();
104		}
105		has
106	}
107}
108
109/// Default bind address used when [`ServerConfig::bind`] is not set.
110pub(crate) const DEFAULT_BIND: &str = "[::]:443";
111
112/// Server for accepting MoQ connections.
113///
114/// Accepts QUIC (and optionally WebSocket), plus plaintext qmux over TCP
115/// (`--server-tcp-bind`) and Unix sockets (`--server-unix-bind`). Create via
116/// [`ServerConfig::init`] or [`Server::new`].
117pub struct Server {
118	moq: moq_net::Server,
119	versions: moq_net::Versions,
120	accept: FuturesUnordered<BoxFuture<'static, crate::Result<Request>>>,
121	#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
122	streams: StreamListeners,
123	#[cfg(feature = "iroh")]
124	iroh: Option<iroh::Endpoint>,
125	#[cfg(feature = "noq")]
126	noq: Option<crate::noq::NoqServer>,
127	#[cfg(feature = "quinn")]
128	quinn: Option<crate::quinn::QuinnServer>,
129	#[cfg(feature = "quiche")]
130	quiche: Option<crate::quiche::QuicheServer>,
131	#[cfg(feature = "websocket")]
132	websocket: Option<crate::websocket::Listener>,
133}
134
135impl Server {
136	/// Build a server from its config, binding the QUIC socket up front.
137	///
138	/// The stream (`tcp`/`unix`) listeners bind lazily on the first
139	/// [`accept`](Self::accept), since they need a runtime.
140	pub fn new(config: ServerConfig) -> crate::Result<Self> {
141		let backend = config.backend.clone().unwrap_or_else(crate::default_quic_backend);
142
143		let versions = config.versions();
144
145		// Build a QUIC backend when `--server-bind` is set, or when nothing else
146		// is (the default). A stream-only server (`--server-unix-bind` with no
147		// `--server-bind`) doesn't also open UDP/443.
148		config.quic.validate()?;
149
150		let build_quic = config.bind.is_some() || !config.has_stream_listener();
151
152		if build_quic && !config.tls.root.is_empty() {
153			let mtls_supported = match backend {
154				#[cfg(feature = "quinn")]
155				QuicBackend::Quinn => true,
156				#[cfg(feature = "noq")]
157				QuicBackend::Noq => true,
158				#[allow(unreachable_patterns)]
159				_ => false,
160			};
161			if !mtls_supported {
162				return Err(Error::MtlsUnsupported);
163			}
164		}
165
166		#[cfg(feature = "noq")]
167		#[allow(unreachable_patterns)]
168		let noq = match backend {
169			QuicBackend::Noq if build_quic => Some(crate::noq::NoqServer::new(config.clone())?),
170			_ => None,
171		};
172
173		#[cfg(feature = "quinn")]
174		#[allow(unreachable_patterns)]
175		let quinn = match backend {
176			QuicBackend::Quinn if build_quic => Some(crate::quinn::QuinnServer::new(config.clone())?),
177			_ => None,
178		};
179
180		#[cfg(feature = "quiche")]
181		let quiche = match backend {
182			QuicBackend::Quiche if build_quic => Some(crate::quiche::QuicheServer::new(config.clone())?),
183			_ => None,
184		};
185
186		// Collect the configured stream listeners (at most one TCP, one Unix).
187		#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
188		let mut stream_binds = Vec::new();
189		#[cfg(feature = "tcp")]
190		if let Some(addr) = config.tcp.bind {
191			stream_binds.push(StreamBind::Tcp(addr));
192		}
193		#[cfg(all(feature = "uds", unix))]
194		if let Some(path) = config.unix.bind.clone() {
195			stream_binds.push(StreamBind::Unix(path));
196		}
197		// `None` (or an all-empty allowlist) means the listener enforces nothing.
198		#[cfg(all(feature = "uds", unix))]
199		let unix_allow = config.unix.allow.clone().filter(|allow| !allow.is_empty());
200		#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
201		let streams = StreamListeners::new(
202			stream_binds,
203			stream_versions(&versions),
204			#[cfg(all(feature = "uds", unix))]
205			unix_allow,
206		);
207
208		Ok(Server {
209			accept: Default::default(),
210			moq: moq_net::Server::new().with_versions(versions.clone()),
211			versions,
212			#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
213			streams,
214			#[cfg(feature = "iroh")]
215			iroh: None,
216			#[cfg(feature = "noq")]
217			noq,
218			#[cfg(feature = "quinn")]
219			quinn,
220			#[cfg(feature = "quiche")]
221			quiche,
222			#[cfg(feature = "websocket")]
223			websocket: None,
224		})
225	}
226
227	/// Add a standalone WebSocket listener on a separate TCP port.
228	///
229	/// This is useful for simple applications that want WebSocket on a dedicated port.
230	/// For applications that need WebSocket on the same HTTP port (e.g. moq-relay),
231	/// use `qmux::Session::accept()` with your own HTTP framework instead.
232	#[cfg(feature = "websocket")]
233	pub fn with_websocket(mut self, websocket: crate::websocket::Listener) -> Self {
234		self.websocket = Some(websocket);
235		self
236	}
237
238	/// Also accept sessions over the given Iroh endpoint.
239	#[cfg(feature = "iroh")]
240	pub fn with_iroh(mut self, iroh: iroh::Endpoint) -> Self {
241		self.iroh = Some(iroh);
242		self
243	}
244
245	/// Publish the given origin to every session this server accepts.
246	pub fn with_publisher(mut self, publish: impl moq_net::Consume<moq_net::origin::Consumer>) -> Self {
247		self.moq = self.moq.with_publisher(publish);
248		self
249	}
250
251	/// Subscribe to every session's broadcasts, ingesting them into the given origin.
252	pub fn with_subscriber(mut self, subscribe: moq_net::origin::Producer) -> Self {
253		self.moq = self.moq.with_subscriber(subscribe);
254		self
255	}
256
257	/// Attach a per-connection [`moq_net::stats::Session`] context to all sessions
258	/// accepted by this server.
259	pub fn with_stats(mut self, stats: moq_net::stats::Session) -> Self {
260		self.moq = self.moq.with_stats(stats);
261		self
262	}
263
264	/// Accept sessions until the listener stops, serving `origin` to each subscriber.
265	///
266	/// Spawns a task per session and logs (rather than propagates) per-session
267	/// errors, so one bad peer never tears down the listener. Returns when
268	/// interrupted (Ctrl-C) or on a fatal bind failure. For per-session auth or
269	/// routing, drive [`accept`](Self::accept) yourself instead.
270	pub async fn serve_publish(self, origin: moq_net::origin::Consumer) -> crate::Result<()> {
271		self.with_publisher(origin).serve().await
272	}
273
274	/// Accept sessions until the listener stops, ingesting each publisher into `origin`.
275	///
276	/// The mirror of [`serve_publish`](Self::serve_publish) for the consume direction.
277	pub async fn serve_consume(self, origin: moq_net::origin::Producer) -> crate::Result<()> {
278		self.with_subscriber(origin).serve().await
279	}
280
281	/// Shared accept loop for [`serve_publish`](Self::serve_publish) /
282	/// [`serve_consume`](Self::serve_consume); the origin is already attached.
283	async fn serve(mut self) -> crate::Result<()> {
284		if let Ok(addr) = self.local_addr() {
285			tracing::info!(%addr, "listening");
286		}
287		while let Some(request) = self.accept().await {
288			tokio::spawn(async move {
289				if let Err(err) = serve_session(request).await {
290					tracing::warn!(%err, "session ended with error");
291				}
292			});
293		}
294		Ok(())
295	}
296
297	/// A live handle to the certificates this server is serving.
298	///
299	/// Use it to publish the SHA-256 fingerprints of a generated certificate at
300	/// `/certificate.sha256`, which an `http://` client pins to reach a
301	/// self-signed server. The handle tracks cert hot reloads, so hold it rather
302	/// than the values it returns.
303	///
304	/// Empty when no TLS-bearing backend is configured (e.g. a stream-only server).
305	pub fn certificates(&self) -> crate::tls::Certificates {
306		#[cfg(feature = "noq")]
307		if let Some(noq) = self.noq.as_ref() {
308			return noq.certificates();
309		}
310		#[cfg(feature = "quinn")]
311		if let Some(quinn) = self.quinn.as_ref() {
312			return quinn.certificates();
313		}
314		#[cfg(feature = "quiche")]
315		if let Some(quiche) = self.quiche.as_ref() {
316			return quiche.certificates();
317		}
318		// No QUIC backend (e.g. a stream-only `--server-bind`): no certificates.
319		crate::tls::Certificates::empty()
320	}
321
322	#[cfg(not(any(
323		feature = "noq",
324		feature = "quinn",
325		feature = "quiche",
326		feature = "iroh",
327		feature = "tcp",
328		all(feature = "uds", unix)
329	)))]
330	/// Returns the next partially established session.
331	///
332	/// Panics: no transport feature is compiled in, so nothing can be accepted.
333	pub async fn accept(&mut self) -> Option<Request> {
334		unreachable!("no transport compiled; enable a QUIC backend, tcp, or uds feature");
335	}
336
337	/// Returns the next partially established session, across every configured
338	/// transport (QUIC, WebSocket, and plaintext qmux over TCP/Unix).
339	///
340	/// This returns a [Request] instead of a session so the connection can be
341	/// rejected early on an invalid path or missing auth. Call [Request::ok] or
342	/// [Request::close] to complete the handshake.
343	#[cfg(any(
344		feature = "noq",
345		feature = "quinn",
346		feature = "quiche",
347		feature = "iroh",
348		feature = "tcp",
349		all(feature = "uds", unix)
350	))]
351	pub async fn accept(&mut self) -> Option<Request> {
352		// Bind the stream (tcp/unix) listeners on first poll; a bind failure is
353		// fatal, mirroring how a QUIC bind failure aborts startup.
354		#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
355		if let Err(err) = self.streams.ensure_started().await {
356			tracing::error!(%err, "failed to bind stream listener");
357			return None;
358		}
359
360		loop {
361			// tokio::select! does not support cfg directives on arms, so we need to create the futures here.
362			#[cfg(feature = "noq")]
363			let noq_accept = async {
364				#[cfg(feature = "noq")]
365				if let Some(noq) = self.noq.as_mut() {
366					return noq.accept().await;
367				}
368				None
369			};
370			#[cfg(not(feature = "noq"))]
371			let noq_accept = async { None::<()> };
372
373			#[cfg(feature = "iroh")]
374			let iroh_accept = async {
375				#[cfg(feature = "iroh")]
376				if let Some(endpoint) = self.iroh.as_mut() {
377					return endpoint.accept().await;
378				}
379				None
380			};
381			#[cfg(not(feature = "iroh"))]
382			let iroh_accept = async { None::<()> };
383
384			#[cfg(feature = "quinn")]
385			let quinn_accept = async {
386				#[cfg(feature = "quinn")]
387				if let Some(quinn) = self.quinn.as_mut() {
388					return quinn.accept().await;
389				}
390				None
391			};
392			#[cfg(not(feature = "quinn"))]
393			let quinn_accept = async { None::<()> };
394
395			#[cfg(feature = "quiche")]
396			let quiche_accept = async {
397				#[cfg(feature = "quiche")]
398				if let Some(quiche) = self.quiche.as_mut() {
399					return quiche.accept().await;
400				}
401				None
402			};
403			#[cfg(not(feature = "quiche"))]
404			let quiche_accept = async { None::<()> };
405
406			#[cfg(feature = "websocket")]
407			let ws_ref = self.websocket.as_ref();
408			#[cfg(feature = "websocket")]
409			let ws_accept = async {
410				match ws_ref {
411					Some(ws) => ws.accept().await,
412					None => std::future::pending().await,
413				}
414			};
415			#[cfg(not(feature = "websocket"))]
416			let ws_accept = std::future::pending::<Option<crate::Result<()>>>();
417
418			#[allow(unused_variables)]
419			let server = self.moq.clone();
420			#[allow(unused_variables)]
421			let versions = self.versions.clone();
422
423			// No streams configured: never resolves, so it doesn't disturb select!.
424			#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
425			let stream_accept = self.streams.recv();
426			#[cfg(not(any(feature = "tcp", all(feature = "uds", unix))))]
427			let stream_accept = std::future::pending::<Option<Request>>();
428
429			tokio::select! {
430				Some(request) = stream_accept => {
431					return Some(request);
432				}
433				Some(_conn) = noq_accept => {
434					#[cfg(feature = "noq")]
435					{
436						let alpns = versions.alpns();
437						self.accept.push(async move {
438							// Accept the transport (capturing url + mTLS identity) and exchange the
439							// MoQ SETUP up front, so path/role are known before the caller authorizes
440							// (like the stream bindings).
441							let (session, url, identity) = super::noq::accept(_conn, alpns).await?;
442							let request = server.accept_request(session).await?;
443							Ok(Request { transport: Transport::Quic, url, identity, kind: RequestKind::Noq(Box::new(request)) })
444						}.boxed());
445					}
446				}
447				Some(_conn) = quinn_accept => {
448					#[cfg(feature = "quinn")]
449					{
450						let alpns = versions.alpns();
451						self.accept.push(async move {
452							let (session, url, identity) = super::quinn::accept(_conn, alpns).await?;
453							let request = server.accept_request(session).await?;
454							Ok(Request { transport: Transport::Quic, url, identity, kind: RequestKind::Quinn(Box::new(request)) })
455						}.boxed());
456					}
457				}
458				Some(_conn) = quiche_accept => {
459					#[cfg(feature = "quiche")]
460					{
461						let alpns = versions.alpns();
462						self.accept.push(async move {
463							let (session, url, identity) = super::quiche::accept(_conn, alpns).await?;
464							let request = server.accept_request(session).await?;
465							Ok(Request { transport: Transport::Quic, url, identity, kind: RequestKind::Quiche(Box::new(request)) })
466						}.boxed());
467					}
468				}
469				Some(_conn) = iroh_accept => {
470					#[cfg(feature = "iroh")]
471					self.accept.push(async move {
472						let (session, url, identity) = super::iroh::accept(_conn).await?;
473						let request = server.accept_request(session).await?;
474						Ok(Request { transport: Transport::Iroh, url, identity, kind: RequestKind::Iroh(Box::new(request)) })
475					}.boxed());
476				}
477				Some(_res) = ws_accept => {
478					#[cfg(feature = "websocket")]
479					match _res {
480						Ok(session) => {
481							// Read the SETUP off the qmux session before handing it over, so a
482							// slow peer doesn't stall the accept loop (spawned like the others).
483							self.accept.push(async move {
484								let request = server.accept_request(session).await?;
485								Ok(Request { transport: Transport::WebSocket, url: None, identity: None, kind: RequestKind::Qmux(Box::new(request)) })
486							}.boxed());
487						}
488						Err(err) => tracing::debug!(%err, "failed to accept WebSocket session"),
489					}
490				}
491				Some(res) = self.accept.next() => {
492					match res {
493						Ok(session) => return Some(session),
494						Err(err) => tracing::debug!(%err, "failed to accept session"),
495					}
496				}
497				_ = tokio::signal::ctrl_c() => {
498					self.close().await;
499					return None;
500				}
501			}
502		}
503	}
504
505	/// The Iroh endpoint from [`with_iroh`](Self::with_iroh), if one was set.
506	#[cfg(feature = "iroh")]
507	pub fn iroh_endpoint(&self) -> Option<&iroh::Endpoint> {
508		self.iroh.as_ref()
509	}
510
511	/// The address the QUIC listener bound to, useful when the config asked for
512	/// port 0.
513	///
514	/// Errors with [`Error::NoBackend`] on a stream-only server, which has no
515	/// QUIC listener.
516	pub fn local_addr(&self) -> crate::Result<net::SocketAddr> {
517		#[cfg(feature = "noq")]
518		if let Some(noq) = self.noq.as_ref() {
519			return Ok(noq.local_addr()?);
520		}
521		#[cfg(feature = "quinn")]
522		if let Some(quinn) = self.quinn.as_ref() {
523			return Ok(quinn.local_addr()?);
524		}
525		#[cfg(feature = "quiche")]
526		if let Some(quiche) = self.quiche.as_ref() {
527			return Ok(quiche.local_addr()?);
528		}
529		// No QUIC backend (e.g. a stream-only `--server-bind`).
530		Err(Error::NoBackend("no QUIC listener configured"))
531	}
532
533	/// The address the WebSocket listener from
534	/// [`with_websocket`](Self::with_websocket) bound to, if one was set.
535	#[cfg(feature = "websocket")]
536	pub fn websocket_local_addr(&self) -> Option<net::SocketAddr> {
537		self.websocket.as_ref().and_then(|ws| ws.local_addr().ok())
538	}
539
540	/// Close every listener, giving in-flight connections a moment to see the
541	/// shutdown.
542	///
543	/// [`accept`](Self::accept) calls this for you on Ctrl-C.
544	pub async fn close(&mut self) {
545		#[cfg(feature = "noq")]
546		if let Some(noq) = self.noq.as_mut() {
547			noq.close();
548			tokio::time::sleep(std::time::Duration::from_millis(100)).await;
549		}
550		#[cfg(feature = "quinn")]
551		if let Some(quinn) = self.quinn.as_mut() {
552			quinn.close();
553			tokio::time::sleep(std::time::Duration::from_millis(100)).await;
554		}
555		#[cfg(feature = "quiche")]
556		if let Some(quiche) = self.quiche.as_mut() {
557			quiche.close();
558			tokio::time::sleep(std::time::Duration::from_millis(100)).await;
559		}
560		#[cfg(feature = "iroh")]
561		if let Some(iroh) = self.iroh.take() {
562			iroh.close().await;
563		}
564		#[cfg(feature = "websocket")]
565		{
566			let _ = self.websocket.take();
567		}
568		#[cfg(not(any(feature = "noq", feature = "quinn", feature = "quiche", feature = "iroh")))]
569		unreachable!("no QUIC backend compiled");
570	}
571}
572
573/// Complete one accepted [`Request`] and wait for the session to close.
574async fn serve_session(request: Request) -> crate::Result<()> {
575	let session = request.ok().await?;
576	Err(session.closed().await.into())
577}
578
579/// The version set offered on stream (`tcp://`/`unix://`) listeners.
580///
581/// A URL-less transport carries the request path in the moq-lite-05 SETUP, so
582/// lite-05 is offered on top of the configured versions even when a custom set
583/// omits it. Older versions still work for clients that need no path.
584#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
585fn stream_versions(base: &moq_net::Versions) -> moq_net::Versions {
586	let mut versions: Vec<moq_net::Version> = base.iter().copied().collect();
587	if let Ok(lite05) = "moq-lite-05".parse::<moq_net::Version>()
588		&& !versions.contains(&lite05)
589	{
590		versions.push(lite05);
591	}
592	moq_net::Versions::from(versions)
593}
594
595/// A configured stream listener (`--server-tcp-bind` / `--server-unix-bind`).
596#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
597enum StreamBind {
598	#[cfg(feature = "tcp")]
599	Tcp(net::SocketAddr),
600	#[cfg(all(feature = "uds", unix))]
601	Unix(PathBuf),
602}
603
604/// The stream (`tcp`/`unix`) listeners owned by a [`Server`].
605///
606/// Bound lazily on the first [`Server::accept`] (they need a runtime), after
607/// which each runs an accept loop in its own task and feeds completed [`Request`]s
608/// back over a channel. The tasks own their listeners and are aborted when the
609/// `Server` (and thus this) is dropped, so bound sockets don't linger.
610#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
611struct StreamListeners {
612	binds: Vec<StreamBind>,
613	versions: moq_net::Versions,
614	#[cfg(all(feature = "uds", unix))]
615	unix_allow: Option<crate::unix::Allow>,
616	rx: Option<tokio::sync::mpsc::Receiver<Request>>,
617	tasks: Vec<tokio::task::JoinHandle<()>>,
618}
619
620#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
621impl StreamListeners {
622	fn new(
623		binds: Vec<StreamBind>,
624		versions: moq_net::Versions,
625		#[cfg(all(feature = "uds", unix))] unix_allow: Option<crate::unix::Allow>,
626	) -> Self {
627		Self {
628			binds,
629			versions,
630			#[cfg(all(feature = "uds", unix))]
631			unix_allow,
632			rx: None,
633			tasks: Vec::new(),
634		}
635	}
636
637	/// Bind the configured listeners and spawn their accept loops, once.
638	async fn ensure_started(&mut self) -> crate::Result<()> {
639		if self.rx.is_some() || self.binds.is_empty() {
640			return Ok(());
641		}
642
643		let (tx, rx) = tokio::sync::mpsc::channel(16);
644		for bind in self.binds.drain(..) {
645			let versions = self.versions.clone();
646			match bind {
647				#[cfg(feature = "tcp")]
648				StreamBind::Tcp(addr) => {
649					if !addr.ip().is_loopback() {
650						tracing::warn!(%addr, "tcp listener bound to a non-loopback address; qmux is UNENCRYPTED, ensure the network is trusted");
651					}
652					let listener = crate::tcp::Listener::bind(addr).await?.with_protocols(versions.alpns());
653					tracing::info!(%addr, "listening (tcp)");
654					self.tasks.push(spawn_tcp_loop(listener, versions, tx.clone()));
655				}
656				#[cfg(all(feature = "uds", unix))]
657				StreamBind::Unix(path) => {
658					let listener = crate::unix::Listener::bind(&path)
659						.await?
660						.with_protocols(versions.alpns());
661					// Loose file perms: the uid/gid/pid allow list is the real gate,
662					// and the worker usually runs as a different user than the server.
663					listener.set_mode(0o666)?;
664					tracing::info!(path = %path.display(), allow = ?self.unix_allow, "listening (unix)");
665					self.tasks
666						.push(spawn_unix_loop(listener, versions, self.unix_allow.clone(), tx.clone()));
667				}
668			}
669		}
670
671		self.rx = Some(rx);
672		Ok(())
673	}
674
675	/// Yield the next stream [`Request`], or pend forever if none are running.
676	async fn recv(&mut self) -> Option<Request> {
677		match self.rx.as_mut() {
678			Some(rx) => rx.recv().await,
679			None => std::future::pending().await,
680		}
681	}
682}
683
684#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
685impl Drop for StreamListeners {
686	fn drop(&mut self) {
687		// Stop the accept loops so their listeners (and bound sockets) are freed.
688		for task in &self.tasks {
689			task.abort();
690		}
691	}
692}
693
694#[cfg(feature = "tcp")]
695fn spawn_tcp_loop(
696	listener: crate::tcp::Listener,
697	versions: moq_net::Versions,
698	tx: tokio::sync::mpsc::Sender<Request>,
699) -> tokio::task::JoinHandle<()> {
700	tokio::spawn(async move {
701		loop {
702			match listener.accept().await {
703				Some(Ok(session)) => spawn_stream_request(session, Transport::Tcp, versions.clone(), tx.clone()),
704				Some(Err(err)) => tracing::warn!(%err, "tcp listener accept failed"),
705				None => break,
706			}
707		}
708	})
709}
710
711#[cfg(all(feature = "uds", unix))]
712fn spawn_unix_loop(
713	listener: crate::unix::Listener,
714	versions: moq_net::Versions,
715	allow: Option<crate::unix::Allow>,
716	tx: tokio::sync::mpsc::Sender<Request>,
717) -> tokio::task::JoinHandle<()> {
718	tokio::spawn(async move {
719		loop {
720			match listener.accept().await {
721				Some(Ok((session, cred))) => {
722					// Enforce the allowlist (if any) before reading SETUP bytes from the peer.
723					if let Some(allow) = &allow
724						&& !allow.permits(&cred)
725					{
726						tracing::warn!(uid = cred.uid, gid = cred.gid, pid = ?cred.pid, "unix connection rejected by allow list");
727						continue;
728					}
729					spawn_stream_request(session, Transport::Unix, versions.clone(), tx.clone());
730				}
731				Some(Err(err)) => tracing::warn!(%err, "unix listener accept failed"),
732				None => break,
733			}
734		}
735	})
736}
737
738/// Read the SETUP from an accepted stream session (concurrently, so one slow or
739/// malicious peer doesn't stall the listener) and forward the resulting request.
740#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
741fn spawn_stream_request(
742	session: qmux::Session,
743	transport: Transport,
744	versions: moq_net::Versions,
745	tx: tokio::sync::mpsc::Sender<Request>,
746) {
747	tokio::spawn(async move {
748		let server = moq_net::Server::new().with_versions(versions);
749		match server.accept_request(session).await {
750			Ok(request) => {
751				let request = Request {
752					transport,
753					url: None,
754					identity: None,
755					kind: RequestKind::Qmux(Box::new(request)),
756				};
757				let _ = tx.send(request).await;
758			}
759			Err(err) => tracing::debug!(%err, "stream SETUP handshake failed"),
760		}
761	});
762}
763
764/// An accepted connection whose MoQ SETUP has already been exchanged.
765///
766/// Every backend drives the transport connect *and* the MoQ handshake up front, so the
767/// [`path`](Request::path)/[`role`](Request::role) a client advertised are available on
768/// every transport before the caller authorizes. The variant only distinguishes the
769/// underlying session type; all of them delegate identically.
770pub(crate) enum RequestKind {
771	#[cfg(feature = "noq")]
772	Noq(Box<moq_net::Request<web_transport_noq::Session>>),
773	#[cfg(feature = "quinn")]
774	Quinn(Box<moq_net::Request<web_transport_quinn::Session>>),
775	#[cfg(feature = "quiche")]
776	Quiche(Box<moq_net::Request<web_transport_quiche::Connection>>),
777	#[cfg(feature = "iroh")]
778	Iroh(Box<moq_net::Request<web_transport_iroh::Session>>),
779	#[cfg(any(feature = "tcp", all(feature = "uds", unix), feature = "websocket"))]
780	Qmux(Box<moq_net::Request<qmux::Session>>),
781}
782
783/// The network transport carrying an incoming MoQ session.
784#[non_exhaustive]
785#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
786pub enum Transport {
787	/// QUIC, either directly or through WebTransport over HTTP/3.
788	Quic,
789	/// An Iroh QUIC connection.
790	Iroh,
791	/// A WebSocket connection using qmux framing.
792	WebSocket,
793	/// A plaintext TCP connection using qmux framing.
794	Tcp,
795	/// A Unix domain socket using qmux framing.
796	Unix,
797}
798
799impl Transport {
800	/// Returns the stable lowercase name used in logs and external metadata.
801	pub const fn as_str(self) -> &'static str {
802		match self {
803			Self::Quic => "quic",
804			Self::Iroh => "iroh",
805			Self::WebSocket => "websocket",
806			Self::Tcp => "tcp",
807			Self::Unix => "unix",
808		}
809	}
810}
811
812impl std::fmt::Display for Transport {
813	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
814		f.write_str(self.as_str())
815	}
816}
817
818/// An incoming MoQ session that can be accepted or rejected.
819///
820/// The transport connection and the MoQ SETUP are already complete, so [`path`](Self::path),
821/// [`role`](Self::role), [`url`](Self::url), and [`peer_identity`](Self::peer_identity) are
822/// all populated consistently regardless of transport. [Self::with_publisher] and
823/// [Self::with_subscriber] configure what is published and subscribed to on the session;
824/// otherwise the Server's configuration is used by default. Call [Self::ok] to start the
825/// session, or [Self::close] to reject it (which closes the just-established session).
826pub struct Request {
827	transport: Transport,
828	/// The dial URL, for transports that carry one (QUIC/WebTransport). `None` for the
829	/// URL-less stream bindings, whose request path rides the SETUP instead.
830	url: Option<Url>,
831	/// The peer's validated mTLS identity, captured at the transport handshake (before
832	/// the MoQ SETUP), when the backend supports it.
833	identity: Option<crate::tls::PeerIdentity>,
834	kind: RequestKind,
835}
836
837/// Delegate a read-only call to the inner [`moq_net::Request`], whatever the transport.
838macro_rules! request_ref {
839	($self:expr, $r:ident => $body:expr) => {
840		match &$self.kind {
841			#[cfg(feature = "noq")]
842			RequestKind::Noq($r) => $body,
843			#[cfg(feature = "quinn")]
844			RequestKind::Quinn($r) => $body,
845			#[cfg(feature = "quiche")]
846			RequestKind::Quiche($r) => $body,
847			#[cfg(feature = "iroh")]
848			RequestKind::Iroh($r) => $body,
849			#[cfg(any(feature = "tcp", all(feature = "uds", unix), feature = "websocket"))]
850			RequestKind::Qmux($r) => $body,
851		}
852	};
853}
854
855/// Delegate a consuming call whose arms all yield the same type (e.g. `ok`, `close`).
856macro_rules! request_into {
857	($kind:expr, $r:ident => $body:expr) => {
858		match $kind {
859			#[cfg(feature = "noq")]
860			RequestKind::Noq($r) => $body,
861			#[cfg(feature = "quinn")]
862			RequestKind::Quinn($r) => $body,
863			#[cfg(feature = "quiche")]
864			RequestKind::Quiche($r) => $body,
865			#[cfg(feature = "iroh")]
866			RequestKind::Iroh($r) => $body,
867			#[cfg(any(feature = "tcp", all(feature = "uds", unix), feature = "websocket"))]
868			RequestKind::Qmux($r) => $body,
869		}
870	};
871}
872
873/// Delegate a consuming builder call, rebuilding the same variant from the returned request.
874macro_rules! request_map {
875	($kind:expr, $r:ident => $body:expr) => {
876		match $kind {
877			#[cfg(feature = "noq")]
878			RequestKind::Noq($r) => RequestKind::Noq(Box::new($body)),
879			#[cfg(feature = "quinn")]
880			RequestKind::Quinn($r) => RequestKind::Quinn(Box::new($body)),
881			#[cfg(feature = "quiche")]
882			RequestKind::Quiche($r) => RequestKind::Quiche(Box::new($body)),
883			#[cfg(feature = "iroh")]
884			RequestKind::Iroh($r) => RequestKind::Iroh(Box::new($body)),
885			#[cfg(any(feature = "tcp", all(feature = "uds", unix), feature = "websocket"))]
886			RequestKind::Qmux($r) => RequestKind::Qmux(Box::new($body)),
887		}
888	};
889}
890
891impl Request {
892	/// Reject the session. The transport is already accepted, so this closes the
893	/// just-established MoQ session rather than answering the transport handshake:
894	/// the `code` (an HTTP-style status the caller passes) maps to a MoQ close reason.
895	pub async fn close(self, code: u16) -> crate::Result<()> {
896		let err = match code {
897			401 | 403 => moq_net::Error::Unauthorized,
898			other => moq_net::Error::App(other),
899		};
900		request_into!(self.kind, request => request.close(err));
901		Ok(())
902	}
903
904	/// Publish the given origin to the session.
905	pub fn with_publisher(self, publish: impl moq_net::Consume<moq_net::origin::Consumer>) -> Self {
906		let Request {
907			transport,
908			url,
909			identity,
910			kind,
911		} = self;
912		let kind = request_map!(kind, request => request.with_publisher(publish));
913		Request {
914			transport,
915			url,
916			identity,
917			kind,
918		}
919	}
920
921	/// Subscribe to the given origin from the session.
922	pub fn with_subscriber(self, subscribe: moq_net::origin::Producer) -> Self {
923		let Request {
924			transport,
925			url,
926			identity,
927			kind,
928		} = self;
929		let kind = request_map!(kind, request => request.with_subscriber(subscribe));
930		Request {
931			transport,
932			url,
933			identity,
934			kind,
935		}
936	}
937
938	/// Attach a per-connection [`moq_net::stats::Session`] context to this session.
939	pub fn with_stats(self, stats: moq_net::stats::Session) -> Self {
940		let Request {
941			transport,
942			url,
943			identity,
944			kind,
945		} = self;
946		let kind = request_map!(kind, request => request.with_stats(stats));
947		Request {
948			transport,
949			url,
950			identity,
951			kind,
952		}
953	}
954
955	/// Accept the session, starting the MoQ session loops.
956	pub async fn ok(self) -> crate::Result<Session> {
957		let pair = request_into!(self.kind, request => request.ok().await?);
958		Ok(crate::spawn_session(pair))
959	}
960
961	/// Returns the network transport carrying this session.
962	pub fn transport(&self) -> Transport {
963		self.transport
964	}
965
966	/// Returns the URL the client dialed, for transports that carry one (QUIC/WebTransport).
967	///
968	/// `None` for the URL-less stream bindings (`tcp`/`unix`); use [`Self::path`] for their
969	/// in-band request path.
970	pub fn url(&self) -> Option<&Url> {
971		self.url.as_ref()
972	}
973
974	/// The request path the client advertised, uniform across transports.
975	///
976	/// Taken from the SETUP for the URL-less stream bindings (and moq-transport, which
977	/// carries it in-band), and from the dial [`url`](Self::url) for WebTransport/QUIC.
978	/// Empty only when neither carries one.
979	pub fn path(&self) -> &str {
980		// An empty SETUP path means the client advertised none, so fall back to the
981		// dial URL. URI-carrying bindings are the ones that must not send a path at
982		// all, so this never discards a path the client meant us to use.
983		let setup = request_ref!(self, r => r.path());
984		if setup.is_empty() {
985			self.url.as_ref().map(Url::path).unwrap_or("")
986		} else {
987			setup
988		}
989	}
990
991	/// The single direction the client advertised in its SETUP, or `None` for a
992	/// bidirectional session (it omitted the role, or the version carries none).
993	/// Available on every transport. Use it to reject a token that lacks the scope for
994	/// the client's intended direction.
995	pub fn role(&self) -> Option<moq_net::Role> {
996		request_ref!(self, r => r.role())
997	}
998
999	/// The client certificate chain the peer presented, if any, validated
1000	/// against a configured [`crate::tls::Server::root`] during the handshake.
1001	///
1002	/// Captured at the transport handshake (before the SETUP). Only the Quinn and noq
1003	/// backends support mTLS; other transports always return `None`. Use it to grant
1004	/// elevated access or to close the session once the certificate expires (see
1005	/// [`crate::tls::PeerIdentity::expiry`]).
1006	pub fn peer_identity(&self) -> Option<crate::tls::PeerIdentity> {
1007		self.identity.clone()
1008	}
1009
1010	#[doc(hidden)]
1011	#[deprecated(note = "use `peer_identity` instead")]
1012	pub fn has_peer_certificate(&self) -> bool {
1013		self.peer_identity().is_some()
1014	}
1015}
1016
1017#[cfg(test)]
1018mod tests {
1019	use super::*;
1020
1021	#[test]
1022	fn transport_names_are_stable() {
1023		assert_eq!(Transport::Quic.as_str(), "quic");
1024		assert_eq!(Transport::Iroh.as_str(), "iroh");
1025		assert_eq!(Transport::WebSocket.as_str(), "websocket");
1026		assert_eq!(Transport::Tcp.as_str(), "tcp");
1027		assert_eq!(Transport::Unix.as_str(), "unix");
1028	}
1029
1030	/// Building the endpoint needs a runtime, and `certificates()` must stay
1031	/// readable without one (no guard escapes to the caller).
1032	#[cfg(feature = "quinn")]
1033	#[tokio::test]
1034	async fn certificates_expose_generated_fingerprints() {
1035		let mut config = ServerConfig {
1036			bind: Some("[::]:0".to_string()),
1037			..Default::default()
1038		};
1039		config.tls.generate = vec!["localhost".into()];
1040
1041		let certs = config.init().expect("server init").certificates();
1042		let fingerprints = certs.fingerprints();
1043		assert_eq!(fingerprints.len(), 1, "one generated certificate");
1044		// Hex-encoded SHA-256.
1045		assert_eq!(fingerprints[0].len(), 64);
1046		assert!(fingerprints[0].chars().all(|c| c.is_ascii_hexdigit()));
1047	}
1048
1049	/// A stream-only server has no TLS backend, so there's nothing to pin. This
1050	/// must report empty rather than panic.
1051	#[cfg(all(feature = "uds", unix))]
1052	#[tokio::test]
1053	async fn certificates_are_empty_without_a_tls_backend() {
1054		let mut config = ServerConfig::default();
1055		config.unix.bind = Some(PathBuf::from("/tmp/moq-native-certificates-test.sock"));
1056
1057		let server = config.init().expect("server init");
1058		assert!(server.certificates().fingerprints().is_empty());
1059	}
1060
1061	#[test]
1062	fn test_tls_string_or_array() {
1063		// Single string should deserialize into a Vec with one entry.
1064		let single = r#"
1065			cert = "cert.pem"
1066			key = "key.pem"
1067		"#;
1068		let config: crate::tls::Server = toml::from_str(single).unwrap();
1069		assert_eq!(config.cert, vec![PathBuf::from("cert.pem")]);
1070		assert_eq!(config.key, vec![PathBuf::from("key.pem")]);
1071
1072		// TOML arrays should still work.
1073		let array = r#"
1074			cert = ["a.pem", "b.pem"]
1075			key = ["a.key", "b.key"]
1076			generate = ["localhost"]
1077			root = ["ca.pem"]
1078		"#;
1079		let config: crate::tls::Server = toml::from_str(array).unwrap();
1080		assert_eq!(config.cert, vec![PathBuf::from("a.pem"), PathBuf::from("b.pem")]);
1081		assert_eq!(config.key, vec![PathBuf::from("a.key"), PathBuf::from("b.key")]);
1082		assert_eq!(config.generate, vec!["localhost".to_string()]);
1083		assert_eq!(config.root, vec![PathBuf::from("ca.pem")]);
1084	}
1085
1086	#[test]
1087	fn bind_string_or_listen_alias() {
1088		// The QUIC bind is a plain address; the `listen` alias still works.
1089		let bind: ServerConfig = toml::from_str(r#"bind = "[::]:443""#).unwrap();
1090		assert_eq!(bind.bind.as_deref(), Some("[::]:443"));
1091
1092		let alias: ServerConfig = toml::from_str(r#"listen = "0.0.0.0:4443""#).unwrap();
1093		assert_eq!(alias.bind.as_deref(), Some("0.0.0.0:4443"));
1094	}
1095
1096	#[cfg(all(feature = "uds", unix))]
1097	#[test]
1098	fn stream_listener_config_parses() {
1099		let config: ServerConfig = toml::from_str(
1100			r#"
1101bind = "[::]:443"
1102
1103[unix]
1104bind = "/run/moq.sock"
1105
1106[unix.allow]
1107uid = [1001, 1002]
1108"#,
1109		)
1110		.unwrap();
1111		assert_eq!(config.bind.as_deref(), Some("[::]:443"));
1112		assert_eq!(config.unix.bind.as_deref(), Some(std::path::Path::new("/run/moq.sock")));
1113		assert_eq!(config.unix.allow.as_ref().expect("allow").uid, vec![1001, 1002]);
1114		assert!(config.has_stream_listener());
1115	}
1116
1117	#[cfg(all(feature = "uds", unix))]
1118	#[test]
1119	fn stream_only_config_has_no_quic() {
1120		// A unix listener with no `--server-bind` is stream-only.
1121		let mut config = ServerConfig::default();
1122		config.unix.bind = Some(PathBuf::from("/run/moq.sock"));
1123		assert!(config.has_stream_listener());
1124		assert!(config.bind.is_none());
1125
1126		// The default (nothing configured) still runs QUIC.
1127		assert!(!ServerConfig::default().has_stream_listener());
1128	}
1129}