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