Skip to main content

moq_native/
server.rs

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