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
11// Only the transports that finish their handshake in a spawned future need `.boxed()`;
12// the stream listeners hand back an already-built `Request`.
13#[cfg(any(
14	feature = "noq",
15	feature = "quinn",
16	feature = "quiche",
17	feature = "iroh",
18	feature = "websocket"
19))]
20use futures::FutureExt;
21use futures::future::BoxFuture;
22use futures::stream::FuturesUnordered;
23use futures::stream::StreamExt;
24
25/// Configuration for the MoQ server.
26#[derive(clap::Args, Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
27#[serde(deny_unknown_fields, default)]
28#[non_exhaustive]
29pub struct ServerConfig {
30	/// Listen for QUIC (UDP) on the given address. Defaults to `[::]:443`.
31	///
32	/// Accepts standard socket address syntax (e.g. `[::]:443`) or a DNS
33	/// `host:port` pair (e.g. `fly-global-services:443`), resolved at bind time
34	/// (first address only; Quinn cannot bind multiple). Leave unset while a
35	/// `tcp`/`unix` listener is configured to run a stream-only server with no
36	/// QUIC.
37	#[serde(alias = "listen")]
38	#[arg(id = "server-bind", long = "server-bind", alias = "listen", env = "MOQ_SERVER_BIND")]
39	pub bind: Option<String>,
40
41	/// Plaintext qmux TCP listener (`--server-tcp-bind`, no TLS). Requires the
42	/// `tcp` feature.
43	#[cfg(feature = "tcp")]
44	#[command(flatten)]
45	#[serde(default)]
46	pub tcp: crate::tcp::Config,
47
48	/// Plaintext qmux Unix-socket listener (`--server-unix-bind`) with an optional
49	/// peer-credential allowlist. Requires the `uds` feature; unix-only.
50	#[cfg(all(feature = "uds", unix))]
51	#[command(flatten)]
52	#[serde(default)]
53	pub unix: crate::unix::Config,
54
55	/// The QUIC backend to use.
56	/// Auto-detected from compiled features if not specified.
57	#[arg(id = "server-backend", long = "server-backend", env = "MOQ_SERVER_BACKEND")]
58	pub backend: Option<QuicBackend>,
59
60	/// QUIC transport tuning (`--server-quic-*`): stream limits, GSO, timeouts,
61	/// plus the accept-side knobs (preferred address, QUIC-LB connection IDs).
62	#[command(flatten)]
63	#[serde(default)]
64	pub quic: crate::quic::Server,
65
66	/// Restrict the server to specific MoQ protocol version(s).
67	///
68	/// By default, the server accepts all supported versions.
69	/// Use this to restrict to specific versions, e.g. `--server-version moq-lite-02`.
70	/// Can be specified multiple times to accept a subset of versions.
71	#[serde(default, skip_serializing_if = "Vec::is_empty")]
72	#[arg(
73		id = "server-version",
74		long = "server-version",
75		env = "MOQ_SERVER_VERSION",
76		value_parser = crate::version_parser(),
77	)]
78	pub version: Vec<moq_net::Version>,
79
80	/// The certificates to serve and the roots that authenticate mTLS clients
81	/// (`--server-tls-*`).
82	#[command(flatten)]
83	#[serde(default)]
84	pub tls: crate::tls::Server,
85}
86
87impl ServerConfig {
88	/// Build the [`Server`] this config describes, binding its listeners.
89	pub fn init(self) -> crate::Result<Server> {
90		Server::new(self)
91	}
92
93	/// Returns the configured versions, defaulting to all if none specified.
94	pub fn versions(&self) -> moq_net::Versions {
95		if self.version.is_empty() {
96			moq_net::Versions::all()
97		} else {
98			moq_net::Versions::from(self.version.clone())
99		}
100	}
101
102	/// Whether a `tcp`/`unix` stream listener is configured.
103	///
104	/// When true and [`bind`](Self::bind) is unset, the server runs stream-only
105	/// (no default QUIC listener).
106	#[allow(unused_mut)]
107	fn has_stream_listener(&self) -> bool {
108		let mut has = false;
109		#[cfg(feature = "tcp")]
110		{
111			has |= self.tcp.bind.is_some();
112		}
113		#[cfg(all(feature = "uds", unix))]
114		{
115			has |= self.unix.bind.is_some();
116		}
117		has
118	}
119}
120
121/// Default bind address used when [`ServerConfig::bind`] is not set.
122#[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
123pub(crate) const DEFAULT_BIND: &str = "[::]:443";
124
125/// Server for accepting MoQ connections.
126///
127/// Accepts QUIC (and optionally WebSocket), plus plaintext qmux over TCP
128/// (`--server-tcp-bind`) and Unix sockets (`--server-unix-bind`). Create via
129/// [`ServerConfig::init`] or [`Server::new`].
130pub struct Server {
131	moq: moq_net::Server,
132	versions: moq_net::Versions,
133	accept: FuturesUnordered<BoxFuture<'static, crate::Result<Request>>>,
134	#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
135	streams: StreamListeners,
136	#[cfg(feature = "iroh")]
137	iroh: Option<iroh::Endpoint>,
138	#[cfg(feature = "noq")]
139	noq: Option<crate::noq::NoqServer>,
140	#[cfg(feature = "quinn")]
141	quinn: Option<crate::quinn::QuinnServer>,
142	#[cfg(feature = "quiche")]
143	quiche: Option<crate::quiche::QuicheServer>,
144	#[cfg(feature = "websocket")]
145	websocket: Option<crate::websocket::Listener>,
146}
147
148impl Server {
149	/// Build a server from its config, binding the QUIC socket up front.
150	///
151	/// The stream (`tcp`/`unix`) listeners bind lazily on the first
152	/// [`accept`](Self::accept), since they need a runtime.
153	pub fn new(config: ServerConfig) -> crate::Result<Self> {
154		// `default_quic_backend` panics when no backend is compiled, so a WebSocket- or
155		// stream-only build must not ask it.
156		#[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
157		let backend = config.backend.clone().unwrap_or_else(crate::default_quic_backend);
158
159		let versions = config.versions();
160
161		// Build a QUIC backend when `--server-bind` is set, or when nothing else
162		// is (the default). A stream-only server (`--server-unix-bind` with no
163		// `--server-bind`) doesn't also open UDP/443.
164		config.quic.validate()?;
165
166		let build_quic = config.bind.is_some() || !config.has_stream_listener();
167		#[cfg(not(any(feature = "noq", feature = "quinn", feature = "quiche")))]
168		if config.bind.is_some() {
169			return Err(Error::NoBackend(
170				"--server-bind requires a noq, quinn, or quiche backend feature",
171			));
172		}
173
174		if build_quic && !config.tls.root.is_empty() {
175			// Only a QUIC backend validates client certificates; the qmux listeners
176			// (tcp/unix/websocket) carry no TLS of their own.
177			#[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
178			let mtls_supported = match backend {
179				#[cfg(feature = "quinn")]
180				QuicBackend::Quinn => true,
181				#[cfg(feature = "noq")]
182				QuicBackend::Noq => true,
183				#[cfg(feature = "quiche")]
184				QuicBackend::Quiche => true,
185				#[allow(unreachable_patterns)]
186				_ => false,
187			};
188			#[cfg(not(any(feature = "noq", feature = "quinn", feature = "quiche")))]
189			let mtls_supported = false;
190
191			if !mtls_supported {
192				return Err(Error::MtlsUnsupported);
193			}
194		}
195
196		#[cfg(feature = "noq")]
197		#[allow(unreachable_patterns)]
198		let noq = match backend {
199			QuicBackend::Noq if build_quic => Some(crate::noq::NoqServer::new(config.clone())?),
200			_ => None,
201		};
202
203		#[cfg(feature = "quinn")]
204		#[allow(unreachable_patterns)]
205		let quinn = match backend {
206			QuicBackend::Quinn if build_quic => Some(crate::quinn::QuinnServer::new(config.clone())?),
207			_ => None,
208		};
209
210		#[cfg(feature = "quiche")]
211		let quiche = match backend {
212			QuicBackend::Quiche if build_quic => Some(crate::quiche::QuicheServer::new(config.clone())?),
213			_ => None,
214		};
215
216		// Collect the configured stream listeners (at most one TCP, one Unix).
217		#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
218		let mut stream_binds = Vec::new();
219		#[cfg(feature = "tcp")]
220		if let Some(addr) = config.tcp.bind {
221			stream_binds.push(StreamBind::Tcp(addr));
222		}
223		#[cfg(all(feature = "uds", unix))]
224		if let Some(path) = config.unix.bind.clone() {
225			stream_binds.push(StreamBind::Unix(path));
226		}
227		// `None` (or an all-empty allowlist) means the listener enforces nothing.
228		#[cfg(all(feature = "uds", unix))]
229		let unix_allow = config.unix.allow.clone().filter(|allow| !allow.is_empty());
230		#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
231		let streams = StreamListeners::new(
232			stream_binds,
233			stream_versions(&versions),
234			#[cfg(all(feature = "uds", unix))]
235			unix_allow,
236		);
237
238		Ok(Server {
239			accept: Default::default(),
240			moq: moq_net::Server::new().with_versions(versions.clone()),
241			versions,
242			#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
243			streams,
244			#[cfg(feature = "iroh")]
245			iroh: None,
246			#[cfg(feature = "noq")]
247			noq,
248			#[cfg(feature = "quinn")]
249			quinn,
250			#[cfg(feature = "quiche")]
251			quiche,
252			#[cfg(feature = "websocket")]
253			websocket: None,
254		})
255	}
256
257	/// Add a standalone WebSocket listener on a separate TCP port.
258	///
259	/// This is useful for simple applications that want WebSocket on a dedicated port.
260	/// For applications that need WebSocket on the same HTTP port (e.g. moq-relay),
261	/// use `qmux::Session::accept()` with your own HTTP framework instead.
262	#[cfg(feature = "websocket")]
263	pub fn with_websocket(mut self, websocket: crate::websocket::Listener) -> Self {
264		self.websocket = Some(websocket);
265		self
266	}
267
268	/// Also accept sessions over the given Iroh endpoint.
269	#[cfg(feature = "iroh")]
270	pub fn with_iroh(mut self, iroh: iroh::Endpoint) -> Self {
271		self.iroh = Some(iroh);
272		self
273	}
274
275	/// Publish the given origin to every session this server accepts.
276	pub fn with_publisher(mut self, publish: impl moq_net::Consume<moq_net::origin::Consumer>) -> Self {
277		self.moq = self.moq.with_publisher(publish);
278		self
279	}
280
281	/// Subscribe to every session's broadcasts, ingesting them into the given origin.
282	pub fn with_subscriber(mut self, subscribe: moq_net::origin::Producer) -> Self {
283		self.moq = self.moq.with_subscriber(subscribe);
284		self
285	}
286
287	/// Attach a per-connection [`moq_net::stats::Session`] context to all sessions
288	/// accepted by this server.
289	pub fn with_stats(mut self, stats: moq_net::stats::Session) -> Self {
290		self.moq = self.moq.with_stats(stats);
291		self
292	}
293
294	/// Accept sessions until the listener stops, serving `origin` to each subscriber.
295	///
296	/// Spawns a task per session and logs (rather than propagates) per-session
297	/// errors, so one bad peer never tears down the listener. Returns when
298	/// interrupted (Ctrl-C) or on a fatal bind failure. For per-session auth or
299	/// routing, drive [`accept`](Self::accept) yourself instead.
300	pub async fn serve_publish(self, origin: moq_net::origin::Consumer) -> crate::Result<()> {
301		self.with_publisher(origin).serve().await
302	}
303
304	/// Accept sessions until the listener stops, ingesting each publisher into `origin`.
305	///
306	/// The mirror of [`serve_publish`](Self::serve_publish) for the consume direction.
307	pub async fn serve_consume(self, origin: moq_net::origin::Producer) -> crate::Result<()> {
308		self.with_subscriber(origin).serve().await
309	}
310
311	/// Accept sessions until the listener stops, serving `publish` to each subscriber
312	/// and ingesting each publisher into `subscribe`.
313	///
314	/// The both-directions counterpart of [`serve_publish`](Self::serve_publish) and
315	/// [`serve_consume`](Self::serve_consume), so an inbound session can subscribe to
316	/// the origin and publish into it over one connection.
317	pub async fn serve_both(
318		self,
319		publish: moq_net::origin::Consumer,
320		subscribe: moq_net::origin::Producer,
321	) -> crate::Result<()> {
322		self.with_publisher(publish).with_subscriber(subscribe).serve().await
323	}
324
325	/// Shared accept loop for the `serve_*` entry points; the origin is already
326	/// attached. Private so a server can't be served with no direction at all, which
327	/// would accept and handshake sessions that carry nothing.
328	async fn serve(mut self) -> crate::Result<()> {
329		if let Ok(addr) = self.local_addr() {
330			tracing::info!(%addr, "listening");
331		}
332		while let Some(request) = self.accept().await {
333			tokio::spawn(async move {
334				if let Err(err) = serve_session(request).await {
335					tracing::warn!(%err, "session ended with error");
336				}
337			});
338		}
339		Ok(())
340	}
341
342	/// A live handle to the certificates this server is serving.
343	///
344	/// Use it to publish the SHA-256 fingerprints of a generated certificate at
345	/// `/certificate.sha256`, which an `http://` client pins to reach a
346	/// self-signed server. The handle tracks cert hot reloads, so hold it rather
347	/// than the values it returns.
348	///
349	/// Empty when no TLS-bearing backend is configured (e.g. a stream-only server).
350	pub fn certificates(&self) -> crate::tls::Certificates {
351		#[cfg(feature = "noq")]
352		if let Some(noq) = self.noq.as_ref() {
353			return noq.certificates();
354		}
355		#[cfg(feature = "quinn")]
356		if let Some(quinn) = self.quinn.as_ref() {
357			return quinn.certificates();
358		}
359		#[cfg(feature = "quiche")]
360		if let Some(quiche) = self.quiche.as_ref() {
361			return quiche.certificates();
362		}
363		// No QUIC backend (e.g. a stream-only `--server-bind`): no certificates.
364		crate::tls::Certificates::empty()
365	}
366
367	#[cfg(not(any(
368		feature = "noq",
369		feature = "quinn",
370		feature = "quiche",
371		feature = "iroh",
372		feature = "websocket",
373		feature = "tcp",
374		all(feature = "uds", unix)
375	)))]
376	/// Returns the next partially established session.
377	///
378	/// Panics: no transport feature is compiled in, so nothing can be accepted.
379	pub async fn accept(&mut self) -> Option<Request> {
380		unreachable!("no transport compiled; enable a QUIC backend, websocket, tcp, or uds feature");
381	}
382
383	/// The accept-loop health of every listener this server owns that performs a real
384	/// `accept(2)`: the `tcp`/`unix` stream listeners and, if one was set,
385	/// [`with_websocket`](Self::with_websocket).
386	///
387	/// Empty on a QUIC-only server, which is the honest answer rather than a
388	/// convenient one: a QUIC backend multiplexes every session over one UDP socket,
389	/// so it never calls `accept` and has nothing that could fail this way. Publishing
390	/// a zero for it would read as a watch that is passing when it can never fire.
391	///
392	/// Available before [`listen`](Self::listen), so an owner can register these with
393	/// a metrics endpoint at startup even though the sockets bind later.
394	pub fn accept_health(&self) -> Vec<crate::accept::Health> {
395		#[allow(unused_mut)]
396		let mut health = Vec::new();
397		#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
398		health.extend(self.streams.health.iter().cloned());
399		#[cfg(feature = "websocket")]
400		health.extend(self.websocket.as_ref().map(|ws| ws.accept_health()));
401		health
402	}
403
404	/// Bind the listeners that bind lazily, so a bind failure surfaces here.
405	///
406	/// The QUIC socket is bound by [`ServerConfig::init`], but the stream
407	/// (`tcp`/`unix`) listeners need a runtime, so they wait for the first
408	/// [`accept`](Self::accept) instead. That makes a bind failure arrive as a `None`
409	/// from `accept`, which a caller cannot tell apart from an ordinary shutdown.
410	/// Call this first and the two are distinct: the error is yours to handle, and a
411	/// later `None` means the server stopped.
412	///
413	/// Idempotent: a call that fails binds nothing at all (any listener it did bind
414	/// is torn down again), so a retry starts from the same place. Optional, too:
415	/// `accept` still binds them itself, logging the failure, for a caller that
416	/// doesn't call this.
417	///
418	/// Call it after [`with_publisher`](Self::with_publisher) and friends: the stream
419	/// listeners serve what is configured at the moment they bind.
420	pub async fn listen(&mut self) -> crate::Result<()> {
421		#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
422		self.streams.ensure_started(self.moq.clone()).await?;
423		Ok(())
424	}
425
426	/// Returns the next partially established session, across every configured
427	/// transport (QUIC, WebSocket, and plaintext qmux over TCP/Unix).
428	///
429	/// This returns a [Request] instead of a session so the connection can be
430	/// rejected early on an invalid path or missing auth. Call [Request::ok] or
431	/// [Request::close] to complete the handshake.
432	///
433	/// `None` means the server stopped: it was interrupted (Ctrl-C), or a lazy
434	/// listener failed to bind. Call [`listen`](Self::listen) up front to tell those
435	/// two apart.
436	#[cfg(any(
437		feature = "noq",
438		feature = "quinn",
439		feature = "quiche",
440		feature = "iroh",
441		feature = "websocket",
442		feature = "tcp",
443		all(feature = "uds", unix)
444	))]
445	pub async fn accept(&mut self) -> Option<Request> {
446		// Bind the stream (tcp/unix) listeners on first poll; a bind failure is
447		// fatal, mirroring how a QUIC bind failure aborts startup. They handshake
448		// with the same configured server as the QUIC arms below.
449		#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
450		if let Err(err) = self.streams.ensure_started(self.moq.clone()).await {
451			tracing::error!(%err, "failed to bind stream listener");
452			return None;
453		}
454
455		loop {
456			// tokio::select! does not support cfg directives on arms, so we need to create the futures here.
457			#[cfg(feature = "noq")]
458			let noq_accept = async {
459				#[cfg(feature = "noq")]
460				if let Some(noq) = self.noq.as_mut() {
461					return noq.accept().await;
462				}
463				None
464			};
465			#[cfg(not(feature = "noq"))]
466			let noq_accept = async { None::<()> };
467
468			#[cfg(feature = "iroh")]
469			let iroh_accept = async {
470				#[cfg(feature = "iroh")]
471				if let Some(endpoint) = self.iroh.as_mut() {
472					return endpoint.accept().await;
473				}
474				None
475			};
476			#[cfg(not(feature = "iroh"))]
477			let iroh_accept = async { None::<()> };
478
479			#[cfg(feature = "quinn")]
480			let quinn_accept = async {
481				#[cfg(feature = "quinn")]
482				if let Some(quinn) = self.quinn.as_mut() {
483					return quinn.accept().await;
484				}
485				None
486			};
487			#[cfg(not(feature = "quinn"))]
488			let quinn_accept = async { None::<()> };
489
490			#[cfg(feature = "quiche")]
491			let quiche_accept = async {
492				#[cfg(feature = "quiche")]
493				if let Some(quiche) = self.quiche.as_mut() {
494					return quiche.accept().await;
495				}
496				None
497			};
498			#[cfg(not(feature = "quiche"))]
499			let quiche_accept = async { None::<()> };
500
501			#[cfg(feature = "websocket")]
502			let ws_ref = self.websocket.as_ref();
503			#[cfg(feature = "websocket")]
504			let ws_accept = async {
505				match ws_ref {
506					Some(ws) => ws.accept_with_url().await,
507					None => std::future::pending().await,
508				}
509			};
510			#[cfg(not(feature = "websocket"))]
511			let ws_accept = std::future::pending::<Option<crate::Result<()>>>();
512
513			#[allow(unused_variables)]
514			let server = self.moq.clone();
515			#[allow(unused_variables)]
516			let versions = self.versions.clone();
517
518			// No streams configured: never resolves, so it doesn't disturb select!.
519			#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
520			let stream_accept = self.streams.recv();
521			#[cfg(not(any(feature = "tcp", all(feature = "uds", unix))))]
522			let stream_accept = std::future::pending::<Option<Request>>();
523
524			tokio::select! {
525				Some(request) = stream_accept => {
526					return Some(request);
527				}
528				Some(_conn) = noq_accept => {
529					#[cfg(feature = "noq")]
530					{
531						let alpns = versions.alpns();
532						self.accept.push(async move {
533							// Accept the transport (capturing url + mTLS identity) and exchange the
534							// MoQ SETUP up front, so path/role are known before the caller authorizes
535							// (like the stream bindings).
536							let (session, url, identity) = super::noq::accept(_conn, alpns).await?;
537							let request = server.accept_request(session).await?;
538							Ok(Request { transport: Transport::Quic, url, identity, kind: RequestKind::Noq(Box::new(request)) })
539						}.boxed());
540					}
541				}
542				Some(_conn) = quinn_accept => {
543					#[cfg(feature = "quinn")]
544					{
545						let alpns = versions.alpns();
546						self.accept.push(async move {
547							let (session, url, identity) = super::quinn::accept(_conn, alpns).await?;
548							let request = server.accept_request(session).await?;
549							Ok(Request { transport: Transport::Quic, url, identity, kind: RequestKind::Quinn(Box::new(request)) })
550						}.boxed());
551					}
552				}
553				Some(_conn) = quiche_accept => {
554					#[cfg(feature = "quiche")]
555					{
556						let alpns = versions.alpns();
557						self.accept.push(async move {
558							let (session, url, identity) = super::quiche::accept(_conn, alpns).await?;
559							let request = server.accept_request(session).await?;
560							Ok(Request { transport: Transport::Quic, url, identity, kind: RequestKind::Quiche(Box::new(request)) })
561						}.boxed());
562					}
563				}
564				Some(_conn) = iroh_accept => {
565					#[cfg(feature = "iroh")]
566					self.accept.push(async move {
567						let (session, url, identity) = super::iroh::accept(_conn).await?;
568						let request = server.accept_request(session).await?;
569						Ok(Request { transport: Transport::Iroh, url, identity, kind: RequestKind::Iroh(Box::new(request)) })
570					}.boxed());
571				}
572				Some(_res) = ws_accept => {
573					#[cfg(feature = "websocket")]
574					match _res {
575						Ok((session, url)) => {
576							// Read the SETUP off the qmux session before handing it over, so a
577							// slow peer doesn't stall the accept loop (spawned like the others).
578							self.accept.push(async move {
579								let request = server.accept_request(session).await?;
580								Ok(Request { transport: Transport::WebSocket, url: Some(url), identity: None, kind: RequestKind::Qmux(Box::new(request)) })
581							}.boxed());
582						}
583						// One connection's upgrade, not the listener's: a failed
584						// `accept(2)` never reaches here, having been classified,
585						// counted, and warned about by the listener itself.
586						Err(err) => tracing::debug!(%err, "WebSocket upgrade failed"),
587					}
588				}
589				Some(res) = self.accept.next() => {
590					match res {
591						Ok(session) => return Some(session),
592						Err(err) => tracing::debug!(%err, "failed to accept session"),
593					}
594				}
595				_ = tokio::signal::ctrl_c() => {
596					self.close().await;
597					return None;
598				}
599			}
600		}
601	}
602
603	/// The Iroh endpoint from [`with_iroh`](Self::with_iroh), if one was set.
604	#[cfg(feature = "iroh")]
605	pub fn iroh_endpoint(&self) -> Option<&iroh::Endpoint> {
606		self.iroh.as_ref()
607	}
608
609	/// The address the QUIC listener bound to, useful when the config asked for
610	/// port 0.
611	///
612	/// Errors with [`Error::NoBackend`] on a stream-only server, which has no
613	/// QUIC listener.
614	pub fn local_addr(&self) -> crate::Result<net::SocketAddr> {
615		#[cfg(feature = "noq")]
616		if let Some(noq) = self.noq.as_ref() {
617			return Ok(noq.local_addr()?);
618		}
619		#[cfg(feature = "quinn")]
620		if let Some(quinn) = self.quinn.as_ref() {
621			return Ok(quinn.local_addr()?);
622		}
623		#[cfg(feature = "quiche")]
624		if let Some(quiche) = self.quiche.as_ref() {
625			return Ok(quiche.local_addr()?);
626		}
627		// No QUIC backend (e.g. a stream-only `--server-bind`).
628		Err(Error::NoBackend("no QUIC listener configured"))
629	}
630
631	/// The address the WebSocket listener from
632	/// [`with_websocket`](Self::with_websocket) bound to, if one was set.
633	#[cfg(feature = "websocket")]
634	pub fn websocket_local_addr(&self) -> Option<net::SocketAddr> {
635		self.websocket.as_ref().and_then(|ws| ws.local_addr().ok())
636	}
637
638	/// Close every listener, giving in-flight connections a moment to see the
639	/// shutdown.
640	///
641	/// [`accept`](Self::accept) calls this for you on Ctrl-C.
642	pub async fn close(&mut self) {
643		#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
644		self.streams.close().await;
645		#[cfg(feature = "noq")]
646		if let Some(noq) = self.noq.as_mut() {
647			noq.close();
648			tokio::time::sleep(std::time::Duration::from_millis(100)).await;
649		}
650		#[cfg(feature = "quinn")]
651		if let Some(quinn) = self.quinn.as_mut() {
652			quinn.close();
653			tokio::time::sleep(std::time::Duration::from_millis(100)).await;
654		}
655		#[cfg(feature = "quiche")]
656		if let Some(quiche) = self.quiche.as_mut() {
657			quiche.close();
658			tokio::time::sleep(std::time::Duration::from_millis(100)).await;
659		}
660		#[cfg(feature = "iroh")]
661		if let Some(iroh) = self.iroh.take() {
662			iroh.close().await;
663		}
664		#[cfg(feature = "websocket")]
665		{
666			let _ = self.websocket.take();
667		}
668	}
669}
670
671/// Complete one accepted [`Request`] and wait for the session to close.
672async fn serve_session(request: Request) -> crate::Result<()> {
673	let session = request.ok().await?;
674	Err(session.closed().await.into())
675}
676
677/// The version set offered on stream (`tcp://`/`unix://`) listeners.
678///
679/// A URL-less transport carries the request path in the moq-lite-05 SETUP, so
680/// lite-05 is offered on top of the configured versions even when a custom set
681/// omits it. Older versions still work for clients that need no path.
682#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
683fn stream_versions(base: &moq_net::Versions) -> moq_net::Versions {
684	let mut versions: Vec<moq_net::Version> = base.iter().copied().collect();
685	if let Ok(lite05) = "moq-lite-05".parse::<moq_net::Version>()
686		&& !versions.contains(&lite05)
687	{
688		versions.push(lite05);
689	}
690	moq_net::Versions::from(versions)
691}
692
693/// A configured stream listener (`--server-tcp-bind` / `--server-unix-bind`).
694#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
695#[derive(Clone)]
696enum StreamBind {
697	#[cfg(feature = "tcp")]
698	Tcp(net::SocketAddr),
699	#[cfg(all(feature = "uds", unix))]
700	Unix(PathBuf),
701}
702
703#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
704impl StreamBind {
705	/// The name this listener reports its accept health under.
706	fn name(&self) -> &'static str {
707		match self {
708			#[cfg(feature = "tcp")]
709			Self::Tcp(_) => "tcp",
710			#[cfg(all(feature = "uds", unix))]
711			Self::Unix(_) => "unix",
712		}
713	}
714}
715
716/// The stream (`tcp`/`unix`) listeners owned by a [`Server`].
717///
718/// Bound lazily on the first [`Server::accept`] (they need a runtime), after
719/// which each runs an accept loop in its own task and feeds completed [`Request`]s
720/// back over a channel. The tasks own their listeners and are stopped when the
721/// server closes or drops, so bound sockets don't linger.
722#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
723struct StreamListeners {
724	binds: Vec<StreamBind>,
725	/// One per entry in `binds`, in the same order, and created up front rather than
726	/// with the listener: an owner registering these with a metrics endpoint does so
727	/// at startup, long before the first `accept` binds anything.
728	health: Vec<crate::accept::Health>,
729	versions: moq_net::Versions,
730	#[cfg(all(feature = "uds", unix))]
731	unix_allow: Option<crate::unix::Allow>,
732	rx: Option<tokio::sync::mpsc::Receiver<Request>>,
733	tasks: Vec<tokio::task::JoinHandle<()>>,
734}
735
736#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
737impl StreamListeners {
738	fn new(
739		binds: Vec<StreamBind>,
740		versions: moq_net::Versions,
741		#[cfg(all(feature = "uds", unix))] unix_allow: Option<crate::unix::Allow>,
742	) -> Self {
743		let health = binds
744			.iter()
745			.map(|bind| crate::accept::Health::new(bind.name()))
746			.collect();
747		Self {
748			binds,
749			health,
750			versions,
751			#[cfg(all(feature = "uds", unix))]
752			unix_allow,
753			rx: None,
754			tasks: Vec::new(),
755		}
756	}
757
758	/// Bind the configured listeners and spawn their accept loops, once.
759	///
760	/// `server` is the [`Server`]'s configured [`moq_net::Server`], so what
761	/// [`Server::with_publisher`] and friends set applies to stream sessions too.
762	async fn ensure_started(&mut self, server: moq_net::Server) -> crate::Result<()> {
763		if self.rx.is_some() || self.binds.is_empty() {
764			return Ok(());
765		}
766
767		// Stream listeners widen the version set (see `stream_versions`), so the
768		// handshake has to offer that set rather than the server's own.
769		let server = server.with_versions(self.versions.clone());
770
771		let (tx, rx) = tokio::sync::mpsc::channel(16);
772		if let Err(err) = self.start(&server, &tx).await {
773			// All or nothing. A half-bound set would leave the loops we did spawn
774			// feeding the channel this call is about to drop, so a retry would find
775			// listeners that can never deliver a request while `binds` looked done.
776			// Abort them instead and leave the binds untouched, so a retry starts over
777			// and a second `listen` cannot report success over a dead listener.
778			for task in self.tasks.drain(..) {
779				task.abort();
780			}
781			return Err(err);
782		}
783
784		self.rx = Some(rx);
785		Ok(())
786	}
787
788	/// Bind and spawn every configured listener, or return the first failure.
789	async fn start(&mut self, server: &moq_net::Server, tx: &tokio::sync::mpsc::Sender<Request>) -> crate::Result<()> {
790		// Cloned so the loop can push into `self.tasks` while iterating; there are at
791		// most two entries, each an address or a path.
792		let binds = self.binds.clone();
793		let health = self.health.clone();
794		for (bind, health) in binds.into_iter().zip(health) {
795			let alpns = self.versions.alpns();
796			match bind {
797				#[cfg(feature = "tcp")]
798				StreamBind::Tcp(addr) => {
799					if !addr.ip().is_loopback() {
800						tracing::warn!(%addr, "tcp listener bound to a non-loopback address; qmux is UNENCRYPTED, ensure the network is trusted");
801					}
802					let listener = crate::tcp::Listener::bind(addr)
803						.await?
804						.with_protocols(alpns)
805						.with_accept_health(health);
806					tracing::info!(%addr, "listening (tcp)");
807					self.tasks.push(spawn_tcp_loop(listener, server.clone(), tx.clone()));
808				}
809				#[cfg(all(feature = "uds", unix))]
810				StreamBind::Unix(path) => {
811					let listener = crate::unix::Listener::bind(&path)
812						.await?
813						.with_protocols(alpns)
814						.with_accept_health(health);
815					// Loose file perms: the uid/gid/pid allow list is the real gate,
816					// and the worker usually runs as a different user than the server.
817					listener.set_mode(0o666)?;
818					tracing::info!(path = %path.display(), allow = ?self.unix_allow, "listening (unix)");
819					self.tasks.push(spawn_unix_loop(
820						listener,
821						server.clone(),
822						self.unix_allow.clone(),
823						tx.clone(),
824					));
825				}
826			}
827		}
828
829		Ok(())
830	}
831
832	/// Yield the next stream [`Request`], or pend forever if none are running.
833	async fn recv(&mut self) -> Option<Request> {
834		match self.rx.as_mut() {
835			Some(rx) => rx.recv().await,
836			None => std::future::pending().await,
837		}
838	}
839
840	/// Stop every accept loop and wait until its listener has released the socket.
841	async fn close(&mut self) {
842		self.binds.clear();
843		self.rx = None;
844		let tasks = std::mem::take(&mut self.tasks);
845		for task in tasks {
846			task.abort();
847			let _ = task.await;
848		}
849	}
850}
851
852#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
853impl Drop for StreamListeners {
854	fn drop(&mut self) {
855		// Stop the accept loops so their listeners (and bound sockets) are freed.
856		for task in &self.tasks {
857			task.abort();
858		}
859	}
860}
861
862#[cfg(feature = "tcp")]
863fn spawn_tcp_loop(
864	listener: crate::tcp::Listener,
865	server: moq_net::Server,
866	tx: tokio::sync::mpsc::Sender<Request>,
867) -> tokio::task::JoinHandle<()> {
868	tokio::spawn(async move {
869		loop {
870			match listener.accept().await {
871				Some(Ok(session)) => spawn_stream_request(session, Transport::Tcp, server.clone(), tx.clone()),
872				// Per-connection: a failed `accept(2)` is the listener's own to
873				// classify and pace, and never surfaces here.
874				Some(Err(err)) => tracing::warn!(%err, "tcp qmux handshake failed"),
875				None => break,
876			}
877		}
878	})
879}
880
881#[cfg(all(feature = "uds", unix))]
882fn spawn_unix_loop(
883	listener: crate::unix::Listener,
884	server: moq_net::Server,
885	allow: Option<crate::unix::Allow>,
886	tx: tokio::sync::mpsc::Sender<Request>,
887) -> tokio::task::JoinHandle<()> {
888	tokio::spawn(async move {
889		loop {
890			match listener.accept().await {
891				Some(Ok((session, cred))) => {
892					// Enforce the allowlist (if any) before reading SETUP bytes from the peer.
893					if let Some(allow) = &allow
894						&& !allow.permits(&cred)
895					{
896						tracing::warn!(uid = cred.uid, gid = cred.gid, pid = ?cred.pid, "unix connection rejected by allow list");
897						continue;
898					}
899					spawn_stream_request(session, Transport::Unix, server.clone(), tx.clone());
900				}
901				// Per-connection, as in `spawn_tcp_loop`.
902				Some(Err(err)) => tracing::warn!(%err, "unix qmux handshake failed"),
903				None => break,
904			}
905		}
906	})
907}
908
909/// Read the SETUP from an accepted stream session (concurrently, so one slow or
910/// malicious peer doesn't stall the listener) and forward the resulting request.
911#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
912fn spawn_stream_request(
913	session: qmux::Session,
914	transport: Transport,
915	server: moq_net::Server,
916	tx: tokio::sync::mpsc::Sender<Request>,
917) {
918	tokio::spawn(async move {
919		match server.accept_request(session).await {
920			Ok(request) => {
921				let request = Request {
922					transport,
923					url: None,
924					identity: None,
925					kind: RequestKind::Qmux(Box::new(request)),
926				};
927				let _ = tx.send(request).await;
928			}
929			Err(err) => tracing::debug!(%err, "stream SETUP handshake failed"),
930		}
931	});
932}
933
934/// An accepted connection whose MoQ SETUP has already been exchanged.
935///
936/// Every backend drives the transport connect *and* the MoQ handshake up front, so the
937/// [`path`](Request::path)/[`role`](Request::role) a client advertised are available on
938/// every transport before the caller authorizes. The variant only distinguishes the
939/// underlying session type; all of them delegate identically.
940pub(crate) enum RequestKind {
941	#[cfg(feature = "noq")]
942	Noq(Box<moq_net::Request<web_transport_noq::Session>>),
943	#[cfg(feature = "quinn")]
944	Quinn(Box<moq_net::Request<web_transport_quinn::Session>>),
945	#[cfg(feature = "quiche")]
946	Quiche(Box<moq_net::Request<web_transport_quiche::Connection>>),
947	#[cfg(feature = "iroh")]
948	Iroh(Box<moq_net::Request<web_transport_iroh::Session>>),
949	#[cfg(any(feature = "tcp", all(feature = "uds", unix), feature = "websocket"))]
950	Qmux(Box<moq_net::Request<qmux::Session>>),
951}
952
953/// The network transport carrying an incoming MoQ session.
954#[non_exhaustive]
955#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
956pub enum Transport {
957	/// QUIC, either directly or through WebTransport over HTTP/3.
958	Quic,
959	/// An Iroh QUIC connection.
960	Iroh,
961	/// A WebSocket connection using qmux framing.
962	WebSocket,
963	/// A plaintext TCP connection using qmux framing.
964	Tcp,
965	/// A Unix domain socket using qmux framing.
966	Unix,
967}
968
969impl Transport {
970	/// Returns the stable lowercase name used in logs and external metadata.
971	pub const fn as_str(self) -> &'static str {
972		match self {
973			Self::Quic => "quic",
974			Self::Iroh => "iroh",
975			Self::WebSocket => "websocket",
976			Self::Tcp => "tcp",
977			Self::Unix => "unix",
978		}
979	}
980}
981
982impl std::fmt::Display for Transport {
983	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
984		f.write_str(self.as_str())
985	}
986}
987
988/// An incoming MoQ session that can be accepted or rejected.
989///
990/// The transport connection and the MoQ SETUP are already complete, so [`path`](Self::path),
991/// [`role`](Self::role), [`url`](Self::url), and [`peer_identity`](Self::peer_identity) are
992/// all populated consistently regardless of transport. [Self::with_publisher] and
993/// [Self::with_subscriber] configure what is published and subscribed to on the session;
994/// otherwise the Server's configuration is used by default. Call [Self::ok] to start the
995/// session, or [Self::close] to reject it (which closes the just-established session).
996pub struct Request {
997	transport: Transport,
998	/// The request URL, for transports that carry one (QUIC/WebTransport/WebSocket). `None` for the
999	/// URL-less stream bindings, whose request path rides the SETUP instead.
1000	url: Option<Url>,
1001	/// The peer's validated mTLS identity, captured at the transport handshake (before
1002	/// the MoQ SETUP), when the backend supports it.
1003	identity: Option<crate::tls::PeerIdentity>,
1004	kind: RequestKind,
1005}
1006
1007/// Delegate a read-only call to the inner [`moq_net::Request`], whatever the transport.
1008macro_rules! request_ref {
1009	($self:expr, $r:ident => $body:expr) => {
1010		match &$self.kind {
1011			#[cfg(feature = "noq")]
1012			RequestKind::Noq($r) => $body,
1013			#[cfg(feature = "quinn")]
1014			RequestKind::Quinn($r) => $body,
1015			#[cfg(feature = "quiche")]
1016			RequestKind::Quiche($r) => $body,
1017			#[cfg(feature = "iroh")]
1018			RequestKind::Iroh($r) => $body,
1019			#[cfg(any(feature = "tcp", all(feature = "uds", unix), feature = "websocket"))]
1020			RequestKind::Qmux($r) => $body,
1021		}
1022	};
1023}
1024
1025/// Delegate a consuming call whose arms all yield the same type (e.g. `ok`, `close`).
1026macro_rules! request_into {
1027	($kind:expr, $r:ident => $body:expr) => {
1028		match $kind {
1029			#[cfg(feature = "noq")]
1030			RequestKind::Noq($r) => $body,
1031			#[cfg(feature = "quinn")]
1032			RequestKind::Quinn($r) => $body,
1033			#[cfg(feature = "quiche")]
1034			RequestKind::Quiche($r) => $body,
1035			#[cfg(feature = "iroh")]
1036			RequestKind::Iroh($r) => $body,
1037			#[cfg(any(feature = "tcp", all(feature = "uds", unix), feature = "websocket"))]
1038			RequestKind::Qmux($r) => $body,
1039		}
1040	};
1041}
1042
1043/// Delegate a consuming builder call, rebuilding the same variant from the returned request.
1044macro_rules! request_map {
1045	($kind:expr, $r:ident => $body:expr) => {
1046		match $kind {
1047			#[cfg(feature = "noq")]
1048			RequestKind::Noq($r) => RequestKind::Noq(Box::new($body)),
1049			#[cfg(feature = "quinn")]
1050			RequestKind::Quinn($r) => RequestKind::Quinn(Box::new($body)),
1051			#[cfg(feature = "quiche")]
1052			RequestKind::Quiche($r) => RequestKind::Quiche(Box::new($body)),
1053			#[cfg(feature = "iroh")]
1054			RequestKind::Iroh($r) => RequestKind::Iroh(Box::new($body)),
1055			#[cfg(any(feature = "tcp", all(feature = "uds", unix), feature = "websocket"))]
1056			RequestKind::Qmux($r) => RequestKind::Qmux(Box::new($body)),
1057		}
1058	};
1059}
1060
1061impl Request {
1062	/// Reject the session. The transport is already accepted, so this closes the
1063	/// just-established MoQ session rather than answering the transport handshake:
1064	/// the `code` (an HTTP-style status the caller passes) maps to a MoQ close reason.
1065	pub async fn close(self, code: u16) -> crate::Result<()> {
1066		let err = match code {
1067			401 | 403 => moq_net::Error::Unauthorized,
1068			other => moq_net::Error::App(other),
1069		};
1070		request_into!(self.kind, request => request.close(err));
1071		Ok(())
1072	}
1073
1074	/// Publish the given origin to the session.
1075	pub fn with_publisher(self, publish: impl moq_net::Consume<moq_net::origin::Consumer>) -> Self {
1076		let Request {
1077			transport,
1078			url,
1079			identity,
1080			kind,
1081		} = self;
1082		let kind = request_map!(kind, request => request.with_publisher(publish));
1083		Request {
1084			transport,
1085			url,
1086			identity,
1087			kind,
1088		}
1089	}
1090
1091	/// Subscribe to the given origin from the session.
1092	pub fn with_subscriber(self, subscribe: moq_net::origin::Producer) -> Self {
1093		let Request {
1094			transport,
1095			url,
1096			identity,
1097			kind,
1098		} = self;
1099		let kind = request_map!(kind, request => request.with_subscriber(subscribe));
1100		Request {
1101			transport,
1102			url,
1103			identity,
1104			kind,
1105		}
1106	}
1107
1108	/// Attach a per-connection [`moq_net::stats::Session`] context to this session.
1109	pub fn with_stats(self, stats: moq_net::stats::Session) -> Self {
1110		let Request {
1111			transport,
1112			url,
1113			identity,
1114			kind,
1115		} = self;
1116		let kind = request_map!(kind, request => request.with_stats(stats));
1117		Request {
1118			transport,
1119			url,
1120			identity,
1121			kind,
1122		}
1123	}
1124
1125	/// Accept the session, starting the MoQ session loops.
1126	pub async fn ok(self) -> crate::Result<Session> {
1127		let pair = request_into!(self.kind, request => request.ok().await?);
1128		Ok(crate::spawn_session(pair))
1129	}
1130
1131	/// Returns the network transport carrying this session.
1132	pub fn transport(&self) -> Transport {
1133		self.transport
1134	}
1135
1136	/// Returns the request URL for transports that carry one (QUIC/WebTransport/WebSocket).
1137	///
1138	/// `None` for the URL-less stream bindings (`tcp`/`unix`); use [`Self::path`] for their
1139	/// in-band request path.
1140	pub fn url(&self) -> Option<&Url> {
1141		self.url.as_ref()
1142	}
1143
1144	/// The request path the client advertised, uniform across transports.
1145	///
1146	/// Taken from the SETUP for the URL-less stream bindings (and moq-transport, which
1147	/// carries it in-band), or the request [`url`](Self::url) for
1148	/// WebTransport/QUIC/WebSocket.
1149	/// The missing or root path is returned as an empty string.
1150	pub fn path(&self) -> &str {
1151		// An empty SETUP path means the client advertised none, so fall back to the
1152		// request URL. URL-carrying bindings are the ones that must not send a path at
1153		// all, so this never discards a path the client meant us to use.
1154		let setup = request_ref!(self, r => r.path());
1155		let path = if setup.is_empty() {
1156			self.url.as_ref().map(Url::path).unwrap_or("")
1157		} else {
1158			setup.split_once('?').map_or(setup, |(path, _)| path)
1159		};
1160		if path == "/" { "" } else { path }
1161	}
1162
1163	/// The encoded request query without the leading `?`, if one was advertised.
1164	///
1165	/// Query values can contain credentials. Avoid logging this value.
1166	pub fn query(&self) -> Option<&str> {
1167		let setup = request_ref!(self, r => r.path());
1168		if setup.is_empty() {
1169			self.url.as_ref().and_then(Url::query)
1170		} else {
1171			setup.split_once('?').map(|(_, query)| query)
1172		}
1173	}
1174
1175	/// The single direction the client advertised in its SETUP, or `None` for a
1176	/// bidirectional session (it omitted the role, or the version carries none).
1177	/// Available on every transport. Use it to reject a token that lacks the scope for
1178	/// the client's intended direction.
1179	pub fn role(&self) -> Option<moq_net::Role> {
1180		request_ref!(self, r => r.role())
1181	}
1182
1183	/// The origin identity the peer declared in its SETUP (moq-lite-05+).
1184	///
1185	/// A peer declares this when it attaches a publish or subscribe origin.
1186	/// Older versions and peers without one return `None`.
1187	///
1188	/// Self-declared, so treat it as a correlation hint rather than an
1189	/// authenticated identity: authorize on the token or client certificate.
1190	pub fn peer_origin(&self) -> Option<moq_net::Origin> {
1191		request_ref!(self, r => r.peer_origin())
1192	}
1193
1194	/// The client certificate chain the peer presented, if any, validated
1195	/// against a configured [`crate::tls::Server::root`] during the handshake.
1196	///
1197	/// Captured at the transport handshake (before the SETUP). Only the Quinn and noq
1198	/// backends support mTLS; other transports always return `None`. Use it to grant
1199	/// elevated access or to close the session once the certificate expires (see
1200	/// [`crate::tls::PeerIdentity::expiry`]).
1201	pub fn peer_identity(&self) -> Option<crate::tls::PeerIdentity> {
1202		self.identity.clone()
1203	}
1204
1205	#[doc(hidden)]
1206	#[deprecated(note = "use `peer_identity` instead")]
1207	pub fn has_peer_certificate(&self) -> bool {
1208		self.peer_identity().is_some()
1209	}
1210}
1211
1212#[cfg(test)]
1213mod tests {
1214	use super::*;
1215
1216	#[test]
1217	fn version_help_lists_every_parseable_name() {
1218		let help = <ServerConfig as clap::Args>::augment_args(clap::Command::new("test"))
1219			.render_long_help()
1220			.to_string();
1221		for name in moq_net::Version::names() {
1222			assert!(help.contains(name), "missing {name} from --server-version help");
1223		}
1224	}
1225
1226	/// The handles have to exist before anything binds, and cover the stream
1227	/// listeners rather than just the ones an owner happens to construct itself.
1228	///
1229	/// `tcp`/`unix` bind lazily on the first `accept`, so a naive implementation
1230	/// hands out nothing at startup, which is exactly when a metrics endpoint is
1231	/// assembled. A stream-only node would then publish no accept counters for the
1232	/// only sockets on it that can fail.
1233	#[cfg(feature = "tcp")]
1234	#[test]
1235	fn accept_health_covers_stream_listeners_before_they_bind() {
1236		let mut config = ServerConfig::default();
1237		config.tcp.bind = Some("127.0.0.1:0".parse().unwrap());
1238		let server = Server::new(config).expect("stream-only server");
1239
1240		let names: Vec<_> = server.accept_health().iter().map(|h| h.listener()).collect();
1241		assert_eq!(names, vec!["tcp"], "the tcp listener must report before it binds");
1242	}
1243
1244	/// A failed `listen` must leave nothing bound, so a retry starts over.
1245	///
1246	/// The trap is `binds.drain(..)`: consume the list up front and a partial failure
1247	/// leaves it empty, so the *second* `listen` sees nothing left to do and reports
1248	/// success while no stream listener exists and `accept` parks forever.
1249	#[cfg(all(feature = "tcp", feature = "uds", unix))]
1250	#[tokio::test]
1251	async fn a_failed_listen_binds_nothing_and_can_be_retried() {
1252		// A path that cannot be a socket, so the unix bind fails after the tcp one
1253		// has already succeeded.
1254		let dir = tempfile::TempDir::new().unwrap();
1255		let occupied = dir.path().join("not-a-socket");
1256		std::fs::write(&occupied, b"in the way").unwrap();
1257
1258		let mut config = ServerConfig::default();
1259		config.tcp.bind = Some("127.0.0.1:0".parse().unwrap());
1260		config.unix.bind = Some(occupied);
1261		let mut server = Server::new(config).expect("stream-only server");
1262
1263		assert!(server.listen().await.is_err(), "the unix bind must fail");
1264		// Same error the second time, rather than a success over a listener that the
1265		// first call already tore down.
1266		assert!(server.listen().await.is_err(), "a retry must not report success");
1267	}
1268
1269	/// Closing a retained server must release its TCP socket before returning and
1270	/// must not let a later `listen` restart the terminal listener.
1271	#[cfg(feature = "tcp")]
1272	#[tokio::test]
1273	async fn close_releases_stream_listener_socket() {
1274		let probe = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1275		let addr = probe.local_addr().unwrap();
1276		drop(probe);
1277
1278		let mut config = ServerConfig::default();
1279		config.tcp.bind = Some(addr);
1280		let mut server = Server::new(config).expect("stream-only server");
1281		server.listen().await.expect("listen");
1282		assert!(tokio::net::TcpListener::bind(addr).await.is_err(), "listener is bound");
1283
1284		server.close().await;
1285		server.listen().await.expect("closed listener stays terminal");
1286		let _rebound = tokio::net::TcpListener::bind(addr)
1287			.await
1288			.expect("close must release the listener socket");
1289	}
1290
1291	/// An explicit QUIC bind cannot be honored without a QUIC backend.
1292	#[cfg(not(any(feature = "noq", feature = "quinn", feature = "quiche")))]
1293	#[test]
1294	fn quic_bind_without_a_quic_backend_is_rejected() {
1295		let config = ServerConfig {
1296			bind: Some("127.0.0.1:0".to_string()),
1297			..Default::default()
1298		};
1299
1300		assert!(matches!(Server::new(config), Err(Error::NoBackend(_))));
1301	}
1302
1303	/// A QUIC-only server reports nothing. It multiplexes over one UDP socket and
1304	/// never calls `accept`, so a zero counter would be a watch that cannot fire.
1305	#[cfg(all(feature = "quinn", not(feature = "tcp")))]
1306	#[test]
1307	fn accept_health_is_empty_without_a_stream_listener() {
1308		let server = ServerConfig::default().init().expect("quic server");
1309		assert!(server.accept_health().is_empty());
1310	}
1311
1312	#[test]
1313	fn transport_names_are_stable() {
1314		assert_eq!(Transport::Quic.as_str(), "quic");
1315		assert_eq!(Transport::Iroh.as_str(), "iroh");
1316		assert_eq!(Transport::WebSocket.as_str(), "websocket");
1317		assert_eq!(Transport::Tcp.as_str(), "tcp");
1318		assert_eq!(Transport::Unix.as_str(), "unix");
1319	}
1320
1321	/// Building the endpoint needs a runtime, and `certificates()` must stay
1322	/// readable without one (no guard escapes to the caller).
1323	#[cfg(feature = "quinn")]
1324	#[tokio::test]
1325	async fn certificates_expose_generated_fingerprints() {
1326		let mut config = ServerConfig {
1327			bind: Some("[::]:0".to_string()),
1328			..Default::default()
1329		};
1330		config.tls.generate = vec!["localhost".into()];
1331
1332		let certs = config.init().expect("server init").certificates();
1333		let fingerprints = certs.fingerprints();
1334		assert_eq!(fingerprints.len(), 1, "one generated certificate");
1335		// Hex-encoded SHA-256.
1336		assert_eq!(fingerprints[0].len(), 64);
1337		assert!(fingerprints[0].chars().all(|c| c.is_ascii_hexdigit()));
1338	}
1339
1340	/// The stream listeners must hand accepted sessions to the *configured*
1341	/// [`moq_net::Server`]. [`Server::serve_publish`] sets the publisher there
1342	/// rather than on the request, so a session that handshakes against any other
1343	/// server accepts and then serves nothing.
1344	#[cfg(all(feature = "uds", unix))]
1345	#[tokio::test]
1346	async fn unix_listener_serves_the_configured_publisher() {
1347		use rand::RngExt;
1348
1349		// macOS caps AF_UNIX paths near 104 bytes and the system temp dir is long,
1350		// so bind under /tmp with a name unique to this process.
1351		let path = PathBuf::from(format!("/tmp/moq-native-publish-{}.sock", std::process::id()));
1352		let _ = std::fs::remove_file(&path);
1353
1354		let origin = moq_net::Origin::random().produce();
1355		let mut broadcast = origin
1356			.create_broadcast("test", moq_net::broadcast::Route::new().with_announce(true))
1357			.expect("create broadcast");
1358		let mut track = broadcast.create_track("video", None).expect("create track");
1359		let mut group = track.append_group().expect("append group");
1360		group
1361			.write_frame(moq_net::Timestamp::ZERO, b"hello".as_ref())
1362			.expect("write frame");
1363		group.finish().expect("finish group");
1364
1365		let mut config = ServerConfig::default();
1366		config.unix.bind = Some(path.clone());
1367		let server = config.init().expect("server init");
1368
1369		// The publisher lives on the server, never on the accepted request.
1370		let serve = tokio::spawn(server.serve_publish(origin.consume()));
1371
1372		// The listener binds on the first accept, so wait for the socket. Keep the
1373		// last error: a bind failure is logged and swallowed, so it's the only clue
1374		// to why the socket never showed up.
1375		const MAX_DELAY: std::time::Duration = std::time::Duration::from_millis(100);
1376		let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
1377		let mut delay = std::time::Duration::from_millis(1);
1378		while let Err(err) = tokio::net::UnixStream::connect(&path).await {
1379			assert!(
1380				tokio::time::Instant::now() < deadline,
1381				"unix listener never bound: {err}"
1382			);
1383			tokio::time::sleep(delay.mul_f64(0.5 + rand::rng().random::<f64>() / 2.0)).await;
1384			delay = (delay * 2).min(MAX_DELAY);
1385		}
1386
1387		const TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
1388
1389		let url: Url = format!("unix://{}", path.display()).parse().expect("parse url");
1390		let subscriber = moq_net::Origin::random().produce();
1391		let mut announced = subscriber.consume().announced();
1392		let client = crate::ClientConfig::default()
1393			.init()
1394			.expect("client init")
1395			.with_subscriber(subscriber);
1396		let session = tokio::time::timeout(TIMEOUT, client.connect(url))
1397			.await
1398			.expect("connect timeout")
1399			.expect("connect");
1400
1401		// Without the server's publisher the session announces nothing, so this is
1402		// where the regression shows up.
1403		let update = tokio::time::timeout(TIMEOUT, announced.next())
1404			.await
1405			.expect("announce timeout")
1406			.expect("origin closed");
1407		assert_eq!(update.path.as_str(), "test");
1408		let broadcast = update.broadcast.expect("expected an announce");
1409
1410		let mut track = broadcast
1411			.track("video")
1412			.expect("track name")
1413			.subscribe(None)
1414			.await
1415			.expect("subscribe");
1416		let mut group = tokio::time::timeout(TIMEOUT, track.recv_group())
1417			.await
1418			.expect("recv group timeout")
1419			.expect("recv group")
1420			.expect("track closed early");
1421		let frame = tokio::time::timeout(TIMEOUT, group.read_frame())
1422			.await
1423			.expect("read frame timeout")
1424			.expect("read frame")
1425			.expect("group closed early");
1426		assert_eq!(&frame.payload[..], b"hello");
1427
1428		drop(session);
1429		serve.abort();
1430		let _ = std::fs::remove_file(&path);
1431	}
1432
1433	/// A stream-only server has no TLS backend, so there's nothing to pin. This
1434	/// must report empty rather than panic.
1435	#[cfg(all(feature = "uds", unix))]
1436	#[tokio::test]
1437	async fn certificates_are_empty_without_a_tls_backend() {
1438		let mut config = ServerConfig::default();
1439		config.unix.bind = Some(PathBuf::from("/tmp/moq-native-certificates-test.sock"));
1440
1441		let server = config.init().expect("server init");
1442		assert!(server.certificates().fingerprints().is_empty());
1443	}
1444
1445	#[test]
1446	fn test_tls_string_or_array() {
1447		// Single string should deserialize into a Vec with one entry.
1448		let single = r#"
1449			cert = "cert.pem"
1450			key = "key.pem"
1451		"#;
1452		let config: crate::tls::Server = toml::from_str(single).unwrap();
1453		assert_eq!(config.cert, vec![PathBuf::from("cert.pem")]);
1454		assert_eq!(config.key, vec![PathBuf::from("key.pem")]);
1455
1456		// TOML arrays should still work.
1457		let array = r#"
1458			cert = ["a.pem", "b.pem"]
1459			key = ["a.key", "b.key"]
1460			generate = ["localhost"]
1461			root = ["ca.pem"]
1462		"#;
1463		let config: crate::tls::Server = toml::from_str(array).unwrap();
1464		assert_eq!(config.cert, vec![PathBuf::from("a.pem"), PathBuf::from("b.pem")]);
1465		assert_eq!(config.key, vec![PathBuf::from("a.key"), PathBuf::from("b.key")]);
1466		assert_eq!(config.generate, vec!["localhost".to_string()]);
1467		assert_eq!(config.root, vec![PathBuf::from("ca.pem")]);
1468	}
1469
1470	#[test]
1471	fn bind_string_or_listen_alias() {
1472		// The QUIC bind is a plain address; the `listen` alias still works.
1473		let bind: ServerConfig = toml::from_str(r#"bind = "[::]:443""#).unwrap();
1474		assert_eq!(bind.bind.as_deref(), Some("[::]:443"));
1475
1476		let alias: ServerConfig = toml::from_str(r#"listen = "0.0.0.0:4443""#).unwrap();
1477		assert_eq!(alias.bind.as_deref(), Some("0.0.0.0:4443"));
1478	}
1479
1480	#[cfg(all(feature = "uds", unix))]
1481	#[test]
1482	fn stream_listener_config_parses() {
1483		let config: ServerConfig = toml::from_str(
1484			r#"
1485bind = "[::]:443"
1486
1487[unix]
1488bind = "/run/moq.sock"
1489
1490[unix.allow]
1491uid = [1001, 1002]
1492"#,
1493		)
1494		.unwrap();
1495		assert_eq!(config.bind.as_deref(), Some("[::]:443"));
1496		assert_eq!(config.unix.bind.as_deref(), Some(std::path::Path::new("/run/moq.sock")));
1497		assert_eq!(config.unix.allow.as_ref().expect("allow").uid, vec![1001, 1002]);
1498		assert!(config.has_stream_listener());
1499	}
1500
1501	#[cfg(all(feature = "uds", unix))]
1502	#[test]
1503	fn stream_only_config_has_no_quic() {
1504		// A unix listener with no `--server-bind` is stream-only.
1505		let mut config = ServerConfig::default();
1506		config.unix.bind = Some(PathBuf::from("/run/moq.sock"));
1507		assert!(config.has_stream_listener());
1508		assert!(config.bind.is_none());
1509
1510		// The default (nothing configured) still runs QUIC.
1511		assert!(!ServerConfig::default().has_stream_listener());
1512	}
1513}