Skip to main content

moq_native/
server.rs

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