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