Skip to main content

moq_native/
server.rs

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