Skip to main content

moq_net/
client.rs

1use crate::origin;
2#[cfg(test)]
3use crate::runtime::Timers;
4use crate::time::{Clock, Instant};
5use crate::{
6	ALPN_14, ALPN_15, ALPN_16, ALPN_17, ALPN_18, ALPN_19, ALPN_20, ALPN_21, ALPN_22, ALPN_LITE, ALPN_LITE_03,
7	ALPN_LITE_04, ALPN_LITE_05, ALPN_LITE_06, Consume, Error, NEGOTIATED, Session, Version, Versions,
8	coding::{self, Decode, Encode, Stream},
9	ietf, lite, setup, stats,
10};
11
12/// A MoQ client session builder.
13#[derive(Default, Clone)]
14pub struct Client {
15	publish: Option<origin::Consumer>,
16	subscribe: Option<origin::Producer>,
17	stats: stats::Session,
18	versions: Versions,
19	setup_path: Option<String>,
20	cost: Option<u64>,
21	peer_hop: Option<crate::Hop>,
22}
23
24impl Client {
25	/// A client that neither publishes nor subscribes until configured.
26	pub fn new() -> Self {
27		Default::default()
28	}
29
30	/// Publish local broadcasts to the remote: the session reads from the given
31	/// origin (pass an [`origin::Producer`] or [`origin::Consumer`] by reference) and
32	/// forwards its announcements. Omit to publish nothing.
33	pub fn with_publisher(mut self, publish: impl Consume<origin::Consumer>) -> Self {
34		self.publish = Some(publish.consume());
35		self
36	}
37
38	/// Subscribe to remote broadcasts: the session writes the broadcasts the
39	/// remote announces into this [`origin::Producer`]. Omit to subscribe to nothing.
40	pub fn with_subscriber(mut self, subscribe: origin::Producer) -> Self {
41		self.subscribe = Some(subscribe);
42		self
43	}
44
45	/// Attach a per-connection [`stats::Session`] context. The session's publish
46	/// (egress) and subscribe (ingress) origin handles are tagged with it, so all
47	/// traffic counters are attributed through the model for this session's lifetime.
48	/// Pass [`stats::Session::default`] (a no-op context) to opt out.
49	pub fn with_stats(mut self, stats: stats::Session) -> Self {
50		self.stats = stats;
51		self
52	}
53
54	/// Set both publish and subscribe from one shared [`origin::Producer`].
55	///
56	/// Equivalent to [`with_publisher`](Self::with_publisher) and
57	/// [`with_subscriber`](Self::with_subscriber) with the same origin.
58	pub fn with_origin(self, origin: origin::Producer) -> Self {
59		self.with_publisher(&origin).with_subscriber(origin)
60	}
61
62	/// Restrict which protocol versions to offer, in preference order.
63	/// Defaults to every version this crate supports.
64	pub fn with_versions(mut self, versions: Versions) -> Self {
65		self.versions = versions;
66		self
67	}
68
69	/// Set the request path to advertise in SETUP (moq-lite-05 and newer, and
70	/// every moq-transport draft we speak).
71	///
72	/// Only for transports that carry no request URI of their own (native QUIC, qmux
73	/// over TCP/TLS, unix sockets), so the server learns which path the client wants.
74	/// Append `?` and the URI query when there is one: that is how a credential in the
75	/// query (`?jwt=`) reaches the server.
76	/// Bindings that already carry a URI (WebTransport, qmux over WebSocket) convey
77	/// the path there and MUST NOT send this; a server is entitled to treat it as a
78	/// protocol violation. An empty path is equivalent to omitting it. Ignored by
79	/// versions with no in-band request path (lite 01-04).
80	pub fn with_path(mut self, path: impl Into<String>) -> Self {
81		self.setup_path = Some(path.into());
82		self
83	}
84
85	/// Price this link, in the units the rest of the mesh uses (moq-lite-06+, and
86	/// `moqt-17`+ via the MoQ Cluster extension).
87	///
88	/// The dialer is the side that knows what a link costs, because it chose the peer:
89	/// use `0` for a sibling in the same datacenter and something large for another
90	/// region across a metered backbone. So this prices both directions. We add it to
91	/// the route cost of every announcement the peer sends us, and declare it in our
92	/// SETUP so the peer adds it to every announcement we send, which is what a server
93	/// accepting an anonymous connection needs: it cannot tell a sibling from a
94	/// stranger, so it has no price of its own to apply.
95	///
96	/// A price the peer declares applies only where we set none. An unpriced link costs
97	/// `1`, which makes the cost track the hop count and so reproduces plain
98	/// shortest-path routing.
99	pub fn with_cost(mut self, cost: u64) -> Self {
100		self.cost = Some(cost);
101		self
102	}
103
104	/// Assign an origin (hop) id to the peer, used whenever the peer doesn't declare
105	/// one itself.
106	///
107	/// Some relays never declare their identity: moq-lite peers without the hops
108	/// extension, and moq-transport peers that don't negotiate the MoQ Cluster
109	/// extension (or predate it, on `moqt-16` and earlier).
110	/// Broadcasts received from such a peer are normally attributed to the reserved
111	/// Hop ID 0 ("unknown"), which identifies nothing: it never proves continuity,
112	/// so their advertisements neither splice nor survive a restart in place. This
113	/// knob pins a real identity instead, exactly as if the peer had declared it:
114	///
115	/// - broadcasts received from the peer carry `origin` in their hop chains, so
116	///   every session dialing the same relay (with the same id) resolves to one
117	///   route and loop checks can recognize it;
118	/// - broadcasts whose hop chain already contains `origin` are neither announced
119	///   nor served back to the peer, preventing an echo through a relay that does
120	///   no loop detection of its own.
121	///
122	/// An identity the peer does declare wins over this one.
123	pub fn with_peer_hop(mut self, hop: crate::Hop) -> Self {
124		self.peer_hop = Some(hop);
125		self
126	}
127
128	/// The origin pair a session attaches, tagged and filtered.
129	///
130	/// Reads through the publish (egress) consumer and writes through the
131	/// subscribe (ingress) producer are attributed by the model through the
132	/// stats context; one shared context, so presence and viewer counts are
133	/// never double-attributed across the two halves. An assigned peer identity
134	/// means subscriptions from the peer resolve to a source whose hop chain
135	/// excludes it, the same split-horizon rule applied when a peer declares
136	/// its own id; announce filtering is per-protocol and handled inside each
137	/// publisher.
138	fn origins(&self) -> (Option<origin::Consumer>, Option<origin::Producer>) {
139		if self.publish.is_none() && self.subscribe.is_none() {
140			tracing::warn!("not publishing or consuming anything");
141		}
142		let publish = self.publish.clone().map(|origin| origin.with_stats(self.stats.clone()));
143		let subscribe = self
144			.subscribe
145			.clone()
146			.map(|origin| origin.with_stats(self.stats.clone()));
147		let publish = publish.map(|origin| origin.excluding(self.peer_hop.unwrap_or(crate::Hop::UNKNOWN)));
148		(publish, subscribe)
149	}
150
151	/// Start a lite session on an already-negotiated version: build our SETUP,
152	/// wire the origins, and return the session and its driver.
153	fn start_lite<S>(
154		&self,
155		runtime: Clock,
156		session: S,
157		version: lite::Version,
158	) -> Result<(Session, crate::Driver<S>), Error>
159	where
160		S: crate::transport::poll::Session,
161	{
162		let (publish, subscribe) = self.origins();
163
164		// Advertise our capabilities (we report what the transport measures; we
165		// don't pad) plus the request path on URI-less transports, and the
166		// direction we intend to use so the server can reject a token that lacks
167		// the matching scope during the handshake instead of silently carrying
168		// no media. Versions without a Setup Stream have nothing to advertise.
169		let our_setup = if version.has_setup_stream() {
170			lite::Setup {
171				probe: lite::ProbeLevel::detect(&session),
172				path: self.setup_path.clone(),
173				role: lite::Role::from_origins(self.publish.is_some(), self.subscribe.is_some()),
174				cost: self.cost,
175				// Filled by `lite::start` from the attached origin handles.
176				hop: None,
177			}
178		} else {
179			lite::Setup::default()
180		};
181
182		let start = lite::start(lite::Config {
183			runtime: runtime.clone(),
184			session: session.clone(),
185			setup_stream: None,
186			publish,
187			subscribe,
188			peer_hop: self.peer_hop,
189			version,
190			our_setup,
191			peer_setup: None,
192		})?;
193
194		Ok(Session::new(
195			runtime,
196			session,
197			version.into(),
198			start.recv_bandwidth,
199			crate::driver::Protocol::Lite(Box::new(start.driver)),
200			start.goaway,
201		))
202	}
203
204	/// Perform the MoQ handshake for moq-lite only, over any transport.
205	///
206	/// Unlike [`connect`](Self::connect) this puts no thread-affinity bound on
207	/// the transport, so a pinned `!Send` transport works and yields a `!Send`
208	/// machine that stays on its thread. The trade is protocol scope: only a
209	/// moq-lite ALPN is accepted, since the moq-transport driver still needs a
210	/// [`Boxable`](crate::transport::poll::Boxable) transport. An ietf ALPN, an
211	/// unknown one, or the legacy no-ALPN SETUP negotiation is refused with
212	/// [`Error::Version`].
213	pub async fn connect_lite<S>(&self, now: Instant, session: S) -> Result<(Session, crate::Driver<S>), Error>
214	where
215		S: crate::transport::poll::Session,
216	{
217		let runtime = Clock::new(now);
218		let version = match session.protocol() {
219			Some(ALPN_LITE_06) => lite::Version::Lite06,
220			Some(ALPN_LITE_05) => lite::Version::Lite05,
221			Some(ALPN_LITE_04) => lite::Version::Lite04,
222			Some(ALPN_LITE_03) => lite::Version::Lite03,
223			_ => return Err(Error::Version),
224		};
225		self.versions.select(Version::Lite(version)).ok_or(Error::Version)?;
226		self.start_lite(runtime, session, version)
227	}
228
229	/// Perform the MoQ handshake, returning the [`Session`] and its [`Driver`](crate::Driver).
230	///
231	/// Poll the returned driver with nondecreasing time, starting at `now`.
232	pub async fn connect<S>(&self, now: Instant, mut session: S) -> Result<(Session, crate::Driver<S>), Error>
233	where
234		S: crate::transport::poll::Boxable,
235	{
236		let runtime = Clock::new(now);
237		let (publish, subscribe) = self.origins();
238
239		// If ALPN was used to negotiate the version, use the appropriate encoding.
240		// Default to IETF 14 if no ALPN was used and we'll negotiate the version later.
241		let (encoding, supported) = match session.protocol() {
242			Some(alpn @ (ALPN_22 | ALPN_21 | ALPN_20 | ALPN_19 | ALPN_18 | ALPN_17)) => {
243				let draft = match alpn {
244					ALPN_22 => ietf::Version::Draft22,
245					ALPN_21 => ietf::Version::Draft21,
246					ALPN_20 => ietf::Version::Draft20,
247					ALPN_19 => ietf::Version::Draft19,
248					ALPN_18 => ietf::Version::Draft18,
249					_ => ietf::Version::Draft17,
250				};
251
252				let v = self.versions.select(Version::Ietf(draft)).ok_or(Error::Version)?;
253
254				// Draft-17+: SETUP is exchanged by the connection driver.
255				// We advertise the request path in our SETUP for URL-less transports.
256				let (protocol, goaway) = ietf::start(ietf::Config {
257					runtime: runtime.clone(),
258					session: session.clone(),
259					setup: None,
260					request_id_max: None,
261					client: true,
262					publish: publish.clone(),
263					subscribe: subscribe.clone(),
264					peer_hop: self.peer_hop,
265					cost: self.cost,
266					version: draft,
267					path: self.setup_path.clone(),
268					peer_setup_stream: None,
269					peer_declared: None,
270				})?;
271
272				tracing::debug!(version = ?v, "connected");
273				return Ok(Session::new(
274					runtime,
275					session,
276					v,
277					None,
278					crate::driver::Protocol::Ietf(protocol),
279					goaway,
280				));
281			}
282			Some(ALPN_16) => {
283				let v = self
284					.versions
285					.select(Version::Ietf(ietf::Version::Draft16))
286					.ok_or(Error::Version)?;
287				(v, v.into())
288			}
289			Some(ALPN_15) => {
290				let v = self
291					.versions
292					.select(Version::Ietf(ietf::Version::Draft15))
293					.ok_or(Error::Version)?;
294				(v, v.into())
295			}
296			Some(ALPN_14) => {
297				let v = self
298					.versions
299					.select(Version::Ietf(ietf::Version::Draft14))
300					.ok_or(Error::Version)?;
301				(v, v.into())
302			}
303			Some(alpn @ (ALPN_LITE_05 | ALPN_LITE_06)) => {
304				let version = match alpn {
305					ALPN_LITE_06 => lite::Version::Lite06,
306					_ => lite::Version::Lite05,
307				};
308				self.versions.select(Version::Lite(version)).ok_or(Error::Version)?;
309				return self.start_lite(runtime, session, version);
310			}
311			Some(ALPN_LITE_04) => {
312				self.versions
313					.select(Version::Lite(lite::Version::Lite04))
314					.ok_or(Error::Version)?;
315				return self.start_lite(runtime, session, lite::Version::Lite04);
316			}
317			Some(ALPN_LITE_03) => {
318				self.versions
319					.select(Version::Lite(lite::Version::Lite03))
320					.ok_or(Error::Version)?;
321				return self.start_lite(runtime, session, lite::Version::Lite03);
322			}
323			Some(ALPN_LITE) | None => {
324				let supported = self.versions.filter(&NEGOTIATED.into()).ok_or(Error::Version)?;
325				(Version::Ietf(ietf::Version::Draft14), supported)
326			}
327			Some(p) => return Err(Error::UnknownAlpn(p.to_string())),
328		};
329
330		let mut stream = Stream::open(&mut session, encoding).await?;
331
332		// The encoding is always an IETF version for SETUP negotiation.
333		let ietf_encoding = ietf::Version::try_from(encoding).map_err(|_| Error::Version)?;
334
335		let mut parameters = ietf::Parameters::default();
336		parameters.set_varint(ietf::ParameterVarInt::MaxRequestId, u32::MAX as u64);
337		parameters.set_bytes(ietf::ParameterBytes::Implementation, b"moq-lite-rs".to_vec());
338		// Advertise the request path in-band (draft 14-16), same as the lite-05 SETUP.
339		if let Some(path) = &self.setup_path {
340			parameters.set_bytes(ietf::ParameterBytes::Path, path.clone().into_bytes());
341		}
342		ietf::solicit::into_setup(&mut parameters, ietf_encoding);
343		let parameters = parameters.encode_bytes(ietf_encoding)?;
344
345		let client = setup::Client {
346			versions: supported.clone().into(),
347			parameters,
348		};
349
350		stream.writer.encode(&client).await?;
351
352		let mut server: setup::Server = stream.reader.decode().await?;
353
354		let version = supported
355			.iter()
356			.find(|v| coding::Version::from(**v) == server.version)
357			.copied()
358			.ok_or(Error::Version)?;
359
360		let (recv_bw, protocol, goaway) = match version {
361			Version::Lite(v) => {
362				let stream = stream.with_version(v);
363				let start = lite::start(lite::Config {
364					runtime: runtime.clone(),
365					session: session.clone(),
366					setup_stream: Some(stream),
367					publish: publish.clone(),
368					subscribe: subscribe.clone(),
369					peer_hop: self.peer_hop,
370					version: v,
371					// This path only handles versions negotiated via the bidi SETUP exchange
372					// (pre-lite-05), which have no Setup Stream.
373					our_setup: lite::Setup::default(),
374					peer_setup: None,
375				})?;
376
377				(
378					start.recv_bandwidth,
379					crate::driver::Protocol::Lite(Box::new(start.driver)),
380					start.goaway,
381				)
382			}
383			Version::Ietf(v) => {
384				// Decode the parameters to get the initial request ID and what the server
385				// requires of us.
386				let parameters = ietf::Parameters::decode(&mut server.parameters, v)?;
387				let request_id_max = parameters
388					.get_varint(ietf::ParameterVarInt::MaxRequestId)
389					.map(ietf::RequestId);
390				let peer_declared = ietf::peer::Peer {
391					solicit: ietf::solicit::from_setup(&parameters, v)?,
392					..Default::default()
393				};
394
395				let stream = stream.with_version(v);
396				// Draft 14-16: the path rode in the bidi SETUP above, not the uni one.
397				let (protocol, goaway) = ietf::start(ietf::Config {
398					runtime: runtime.clone(),
399					session: session.clone(),
400					setup: Some(stream),
401					request_id_max,
402					client: true,
403					publish: publish.clone(),
404					subscribe: subscribe.clone(),
405					peer_hop: self.peer_hop,
406					cost: self.cost,
407					version: v,
408					path: None,
409					peer_setup_stream: None,
410					peer_declared: Some(peer_declared),
411				})?;
412				(None, crate::driver::Protocol::Ietf(protocol), goaway)
413			}
414		};
415
416		Ok(Session::new(runtime, session, version, recv_bw, protocol, goaway))
417	}
418}
419
420#[cfg(test)]
421mod tests {
422	use super::*;
423	use crate::model::ProduceTest;
424	use std::{
425		collections::VecDeque,
426		sync::{Arc, Mutex},
427	};
428
429	use std::task::{Context, Poll};
430
431	use crate::SessionError;
432	use crate::coding::{Decode, Encode};
433	use bytes::{BufMut, Bytes};
434
435	#[derive(Debug, Clone, Default)]
436	struct FakeError;
437
438	impl std::fmt::Display for FakeError {
439		fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
440			write!(f, "fake transport error")
441		}
442	}
443
444	impl std::error::Error for FakeError {}
445
446	impl web_transport_trait::Error for FakeError {
447		fn session_error(&self) -> Option<(u32, String)> {
448			Some((0, "closed".to_string()))
449		}
450	}
451
452	#[derive(Clone, Default)]
453	struct FakeSession {
454		state: Arc<FakeSessionState>,
455		// Per-clone, so each pending poll_closed keeps its own registration live.
456		park: kio::Park,
457	}
458
459	#[derive(Default)]
460	struct FakeSessionState {
461		protocol: Option<&'static str>,
462		control_stream: Mutex<Option<(FakeSendStream, FakeRecvStream)>>,
463		close_events: Mutex<Vec<(u32, String)>>,
464		closed: kio::Fan,
465		control_writes: Arc<Mutex<Vec<u8>>>,
466		send_rate: Mutex<Option<u64>>,
467		bytes_sent: Mutex<Option<u64>>,
468	}
469
470	impl FakeSession {
471		fn new(protocol: Option<&'static str>, server_control_bytes: Vec<u8>) -> Self {
472			let writes = Arc::new(Mutex::new(Vec::new()));
473			let send = FakeSendStream { writes: writes.clone() };
474			let recv = FakeRecvStream {
475				data: VecDeque::from(server_control_bytes),
476			};
477			let state = FakeSessionState {
478				protocol,
479				control_stream: Mutex::new(Some((send, recv))),
480				close_events: Mutex::new(Vec::new()),
481				closed: kio::Fan::default(),
482				control_writes: writes,
483				send_rate: Mutex::new(None),
484				bytes_sent: Mutex::new(None),
485			};
486			Self {
487				state: Arc::new(state),
488				park: kio::Park::default(),
489			}
490		}
491
492		fn set_send_rate(&self, rate: Option<u64>) {
493			*self.state.send_rate.lock().unwrap() = rate;
494		}
495
496		fn set_bytes_sent(&self, bytes: Option<u64>) {
497			*self.state.bytes_sent.lock().unwrap() = bytes;
498		}
499
500		fn control_writes(&self) -> Vec<u8> {
501			self.state.control_writes.lock().unwrap().clone()
502		}
503
504		async fn wait_for_first_close(&self) -> (u32, String) {
505			kio::wait(|waiter| {
506				self.state.closed.register(waiter);
507				match self.state.close_events.lock().unwrap().first().cloned() {
508					Some(close) => std::task::Poll::Ready(close),
509					None => std::task::Poll::Pending,
510				}
511			})
512			.await
513		}
514	}
515
516	impl web_transport_trait::poll::Session for FakeSession {
517		type SendStream = FakeSendStream;
518		type RecvStream = FakeRecvStream;
519		type Error = FakeError;
520
521		fn poll_accept_uni(&mut self, _cx: &mut Context<'_>) -> Poll<Result<Self::RecvStream, Self::Error>> {
522			Poll::Pending
523		}
524
525		fn poll_accept_bi(
526			&mut self,
527			_cx: &mut Context<'_>,
528		) -> Poll<Result<(Self::SendStream, Self::RecvStream), Self::Error>> {
529			Poll::Pending
530		}
531
532		fn poll_open_bi(
533			&mut self,
534			_cx: &mut Context<'_>,
535		) -> Poll<Result<(Self::SendStream, Self::RecvStream), Self::Error>> {
536			Poll::Ready(self.state.control_stream.lock().unwrap().take().ok_or(FakeError))
537		}
538
539		fn poll_open_uni(&mut self, _cx: &mut Context<'_>) -> Poll<Result<Self::SendStream, Self::Error>> {
540			Poll::Pending
541		}
542
543		fn poll_send_datagram(&mut self, _cx: &mut Context<'_>, _payload: &[u8]) -> Poll<Result<(), Self::Error>> {
544			Poll::Ready(Ok(()))
545		}
546
547		fn poll_recv_datagram(&mut self, _cx: &mut Context<'_>) -> Poll<Result<Bytes, Self::Error>> {
548			Poll::Pending
549		}
550
551		fn max_datagram_size(&self) -> usize {
552			1200
553		}
554
555		fn protocol(&self) -> Option<&str> {
556			self.state.protocol
557		}
558
559		fn close(&mut self, code: u32, reason: &str) {
560			self.state.close_events.lock().unwrap().push((code, reason.to_string()));
561			self.state.closed.wake();
562		}
563
564		fn poll_closed(&mut self, cx: &mut Context<'_>) -> Poll<Self::Error> {
565			// Register before checking so a close racing this poll still wakes it.
566			self.state.closed.register(self.park.hold(cx));
567			match self.state.close_events.lock().unwrap().is_empty() {
568				false => Poll::Ready(FakeError),
569				true => Poll::Pending,
570			}
571		}
572
573		fn stats(&self) -> impl web_transport_trait::Stats {
574			FakeStats {
575				send_rate: *self.state.send_rate.lock().unwrap(),
576				bytes_sent: *self.state.bytes_sent.lock().unwrap(),
577			}
578		}
579	}
580
581	struct FakeStats {
582		send_rate: Option<u64>,
583		bytes_sent: Option<u64>,
584	}
585
586	impl web_transport_trait::Stats for FakeStats {
587		fn estimated_send_rate(&self) -> Option<u64> {
588			self.send_rate
589		}
590
591		fn bytes_sent(&self) -> Option<u64> {
592			self.bytes_sent
593		}
594	}
595
596	#[derive(Clone, Default)]
597	struct FakeSendStream {
598		writes: Arc<Mutex<Vec<u8>>>,
599	}
600
601	impl web_transport_trait::poll::SendStream for FakeSendStream {
602		type Error = FakeError;
603
604		fn poll_write(&mut self, _cx: &mut Context<'_>, buf: &[u8]) -> Poll<Result<usize, Self::Error>> {
605			self.writes.lock().unwrap().put_slice(buf);
606			Poll::Ready(Ok(buf.len()))
607		}
608
609		fn set_priority(&mut self, _order: u8) {}
610
611		fn finish(&mut self) -> Result<(), Self::Error> {
612			Ok(())
613		}
614
615		fn reset(&mut self, _code: u32) {}
616
617		fn poll_closed(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
618			Poll::Ready(Ok(()))
619		}
620	}
621
622	struct FakeRecvStream {
623		data: VecDeque<u8>,
624	}
625
626	impl web_transport_trait::poll::RecvStream for FakeRecvStream {
627		type Error = FakeError;
628
629		fn poll_read(&mut self, _cx: &mut Context<'_>, dst: &mut [u8]) -> Poll<Result<Option<usize>, Self::Error>> {
630			if self.data.is_empty() {
631				return Poll::Ready(Ok(None));
632			}
633
634			let size = dst.len().min(self.data.len());
635			for slot in dst.iter_mut().take(size) {
636				*slot = self.data.pop_front().unwrap();
637			}
638			Poll::Ready(Ok(Some(size)))
639		}
640
641		fn stop(&mut self, _code: u32) {}
642
643		fn poll_closed(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
644			Poll::Ready(Ok(()))
645		}
646	}
647
648	fn mock_server_setup(negotiated: Version) -> Vec<u8> {
649		let mut encoded = Vec::new();
650		let server = setup::Server {
651			version: negotiated.into(),
652			parameters: Bytes::new(),
653		};
654		server
655			.encode(&mut encoded, Version::Ietf(ietf::Version::Draft14))
656			.unwrap();
657
658		// Add a setup-stream SessionInfo frame using the negotiated Lite version.
659		let info = lite::SessionInfo { bitrate: Some(1) };
660		let lite_v = lite::Version::try_from(negotiated).unwrap();
661		info.encode(&mut encoded, lite_v).unwrap();
662
663		encoded
664	}
665
666	async fn run_alpn_lite_fallback_case(protocol: Option<&'static str>) {
667		let fake = FakeSession::new(protocol, mock_server_setup(Version::Lite(lite::Version::Lite01)));
668		let client = Client::new().with_versions(
669			[
670				Version::Lite(lite::Version::Lite03),
671				Version::Lite(lite::Version::Lite02),
672				Version::Lite(lite::Version::Lite01),
673				Version::Ietf(ietf::Version::Draft14),
674			]
675			.into(),
676		);
677
678		// Start the returned driver after the handshake completes.
679		let (_session, driver) = client
680			.connect(tokio::time::Instant::now().into_std(), fake.clone())
681			.await
682			.unwrap();
683		tokio::spawn(crate::time::run(driver));
684
685		// Verify the client setup was encoded using Draft14 framing (ALPN_LITE fallback path).
686		let mut setup_bytes = Bytes::from(fake.control_writes());
687		let setup = setup::Client::decode(&mut setup_bytes, Version::Ietf(ietf::Version::Draft14)).unwrap();
688		let advertised: Vec<Version> = setup.versions.iter().map(|v| Version::try_from(*v).unwrap()).collect();
689		assert_eq!(
690			advertised,
691			vec![
692				Version::Lite(lite::Version::Lite02),
693				Version::Lite(lite::Version::Lite01),
694				Version::Ietf(ietf::Version::Draft14),
695			]
696		);
697
698		// The first close comes from the lite connection driver.
699		// Any non-Version error here means SessionInfo decoded successfully
700		// after set_version(). This test cares about the SETUP framing
701		// fallback, not the specific close code. Cancel is what we'd see
702		// with no origin; a protocol violation (or similar) is what an
703		// auto-created origin's first interaction with a Lite01 peer trips.
704		let (code, _) = fake.wait_for_first_close().await;
705		// Session closes encode through the session registry, so compare against that one.
706		assert_ne!(code, SessionError::Version.to_code(), "SessionInfo failed to decode");
707	}
708
709	/// `connect` must not depend on the peer answering. A peer that opens the announce
710	/// stream and then says nothing (or promises a count it never delivers) used to hold
711	/// `connect` for the life of the session, since it waited for the initial announce
712	/// set. Resolving a path you need is `routed`'s job, which waits for
713	/// that path rather than for the peer to finish talking.
714	#[tokio::test(start_paused = true)]
715	async fn connect_does_not_wait_for_the_peer_to_announce() {
716		// Serves bidi streams, so the announce stream opens, and never answers on them.
717		let gate = kio::Producer::new(true);
718		let transport = crate::lite::test_transport::SinkSession::gated_bi(gate.consume())
719			.with_protocol(crate::version::ALPN_LITE_05);
720
721		// A subscribe origin is what makes the client open an announce stream at all.
722		let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce();
723		let client = Client::new()
724			.with_versions([Version::Lite(lite::Version::Lite05)].into())
725			.with_subscriber(origin);
726
727		// Paused time auto-advances while every task is idle, so a `connect` that waits
728		// on the silent peer trips this rather than hanging the suite.
729		let (_session, _driver) = tokio::time::timeout(
730			std::time::Duration::from_secs(30),
731			client.connect(tokio::time::Instant::now().into_std(), transport),
732		)
733		.await
734		.expect("connect waited on a peer that never announced")
735		.expect("connect failed");
736	}
737
738	#[tokio::test(start_paused = true)]
739	async fn alpn_lite_falls_back_to_draft14_and_switches_version_post_setup() {
740		run_alpn_lite_fallback_case(Some(ALPN_LITE)).await;
741	}
742
743	#[tokio::test(start_paused = true)]
744	async fn no_alpn_falls_back_to_draft14_and_switches_version_post_setup() {
745		run_alpn_lite_fallback_case(None).await;
746	}
747
748	// No executor is running: only explicitly polling the driver may process
749	// a session close, and the driver must not retain a session handle.
750	#[test]
751	fn driver_is_caller_polled_and_holds_no_session() {
752		let fake = FakeSession::new(Some(ALPN_LITE_04), Vec::new());
753		let client = Client::new().with_versions(Version::Lite(lite::Version::Lite04).into());
754
755		let runtime = crate::runtime::Test::new();
756		let (session, mut driver) = futures::executor::block_on(client.connect(runtime.now(), fake.clone())).unwrap();
757		assert_eq!(session.version(), Version::Lite(lite::Version::Lite04));
758
759		// Construction leaves the driver idle until the caller polls it.
760		assert!(driver.poll(runtime.now(), &kio::Waiter::noop()).is_ok());
761
762		// The caller drops their only session clone; the machine observes the
763		// last handle going away and closes the transport.
764		drop(session);
765		assert!(fake.state.close_events.lock().unwrap().is_empty());
766		let _ = driver.poll(runtime.now(), &kio::Waiter::noop());
767		assert_eq!(
768			fake.state.close_events.lock().unwrap()[0].0,
769			SessionError::Cancel.to_code()
770		);
771	}
772
773	// Clones share the connection: the transport closes on the LAST drop, and
774	// abort() closes it explicitly (first close wins). Both are relayed through
775	// the machine, so each takes a tick to land.
776	#[test]
777	fn session_clones_share_the_close() {
778		let fake = FakeSession::new(Some(ALPN_LITE_04), Vec::new());
779		let client = Client::new().with_versions(Version::Lite(lite::Version::Lite04).into());
780
781		let runtime = crate::runtime::Test::new();
782		let (session, mut driver) = futures::executor::block_on(client.connect(runtime.now(), fake.clone())).unwrap();
783		let clone = session.clone();
784
785		// One clone dropping does nothing while another is alive.
786		drop(session);
787		assert!(fake.state.close_events.lock().unwrap().is_empty());
788		let _ = driver.poll(runtime.now(), &kio::Waiter::noop());
789		assert!(fake.state.close_events.lock().unwrap().is_empty());
790
791		clone.abort(Error::Cancel);
792		let _ = driver.poll(runtime.now(), &kio::Waiter::noop());
793		assert_eq!(
794			fake.state.close_events.lock().unwrap()[0].0,
795			SessionError::Cancel.to_code()
796		);
797
798		// And the machine publishes the transport's terminal error, which is
799		// what `closed()` reports.
800		let _ = driver.poll(runtime.now(), &kio::Waiter::noop());
801		futures::executor::block_on(clone.closed());
802
803		// The final drop requests no second close: the handle-side close is once.
804		let closes = fake.state.close_events.lock().unwrap().len();
805		drop(clone);
806		let _ = driver.poll(runtime.now(), &kio::Waiter::noop());
807		assert_eq!(fake.state.close_events.lock().unwrap().len(), closes);
808	}
809
810	// Dropping the driver instead of running it tears the session
811	// down: the machine was the only transport holder, and `closed()` resolves
812	// rather than parking forever on a machine nobody polls.
813	#[test]
814	fn dropped_driver_resolves_closed() {
815		let fake = FakeSession::new(Some(ALPN_LITE_04), Vec::new());
816		let client = Client::new().with_versions(Version::Lite(lite::Version::Lite04).into());
817
818		let runtime = crate::runtime::Test::new();
819		let (session, driver) = futures::executor::block_on(client.connect(runtime.now(), fake.clone())).unwrap();
820
821		drop(driver);
822		assert!(matches!(futures::executor::block_on(session.closed()), Error::Cancel));
823	}
824
825	/// A transport made deliberately `!Send` by an `Rc` marker on the session and
826	/// both stream types: compiling at all is the point, proving the lite path
827	/// never demands thread mobility of any transport piece.
828	#[derive(Clone)]
829	struct LocalSession {
830		inner: FakeSession,
831		_local: std::rc::Rc<()>,
832	}
833
834	struct LocalSend {
835		inner: FakeSendStream,
836		_local: std::rc::Rc<()>,
837	}
838
839	struct LocalRecv {
840		inner: FakeRecvStream,
841		_local: std::rc::Rc<()>,
842	}
843
844	impl web_transport_trait::poll::Session for LocalSession {
845		type SendStream = LocalSend;
846		type RecvStream = LocalRecv;
847		type Error = FakeError;
848
849		fn poll_accept_uni(&mut self, cx: &mut Context<'_>) -> Poll<Result<Self::RecvStream, Self::Error>> {
850			self.inner.poll_accept_uni(cx).map_ok(|stream| LocalRecv {
851				inner: stream,
852				_local: self._local.clone(),
853			})
854		}
855
856		fn poll_accept_bi(
857			&mut self,
858			cx: &mut Context<'_>,
859		) -> Poll<Result<(Self::SendStream, Self::RecvStream), Self::Error>> {
860			self.inner.poll_accept_bi(cx).map_ok(|(send, recv)| {
861				(
862					LocalSend {
863						inner: send,
864						_local: self._local.clone(),
865					},
866					LocalRecv {
867						inner: recv,
868						_local: self._local.clone(),
869					},
870				)
871			})
872		}
873
874		fn poll_open_bi(
875			&mut self,
876			cx: &mut Context<'_>,
877		) -> Poll<Result<(Self::SendStream, Self::RecvStream), Self::Error>> {
878			self.inner.poll_open_bi(cx).map_ok(|(send, recv)| {
879				(
880					LocalSend {
881						inner: send,
882						_local: self._local.clone(),
883					},
884					LocalRecv {
885						inner: recv,
886						_local: self._local.clone(),
887					},
888				)
889			})
890		}
891
892		fn poll_open_uni(&mut self, cx: &mut Context<'_>) -> Poll<Result<Self::SendStream, Self::Error>> {
893			self.inner.poll_open_uni(cx).map_ok(|stream| LocalSend {
894				inner: stream,
895				_local: self._local.clone(),
896			})
897		}
898
899		fn poll_send_datagram(&mut self, cx: &mut Context<'_>, payload: &[u8]) -> Poll<Result<(), Self::Error>> {
900			self.inner.poll_send_datagram(cx, payload)
901		}
902
903		fn poll_recv_datagram(&mut self, cx: &mut Context<'_>) -> Poll<Result<Bytes, Self::Error>> {
904			self.inner.poll_recv_datagram(cx)
905		}
906
907		fn max_datagram_size(&self) -> usize {
908			self.inner.max_datagram_size()
909		}
910
911		fn protocol(&self) -> Option<&str> {
912			self.inner.protocol()
913		}
914
915		fn close(&mut self, code: u32, reason: &str) {
916			self.inner.close(code, reason);
917		}
918
919		fn poll_closed(&mut self, cx: &mut Context<'_>) -> Poll<Self::Error> {
920			self.inner.poll_closed(cx)
921		}
922
923		fn stats(&self) -> impl web_transport_trait::Stats {
924			self.inner.stats()
925		}
926	}
927
928	impl web_transport_trait::poll::SendStream for LocalSend {
929		type Error = FakeError;
930
931		fn poll_write(&mut self, cx: &mut Context<'_>, buf: &[u8]) -> Poll<Result<usize, Self::Error>> {
932			self.inner.poll_write(cx, buf)
933		}
934
935		fn set_priority(&mut self, order: u8) {
936			self.inner.set_priority(order);
937		}
938
939		fn finish(&mut self) -> Result<(), Self::Error> {
940			self.inner.finish()
941		}
942
943		fn reset(&mut self, code: u32) {
944			self.inner.reset(code);
945		}
946
947		fn poll_closed(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
948			web_transport_trait::poll::SendStream::poll_closed(&mut self.inner, cx)
949		}
950	}
951
952	impl web_transport_trait::poll::RecvStream for LocalRecv {
953		type Error = FakeError;
954
955		fn poll_read(&mut self, cx: &mut Context<'_>, dst: &mut [u8]) -> Poll<Result<Option<usize>, Self::Error>> {
956			self.inner.poll_read(cx, dst)
957		}
958
959		fn stop(&mut self, code: u32) {
960			self.inner.stop(code);
961		}
962
963		fn poll_closed(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
964			web_transport_trait::poll::RecvStream::poll_closed(&mut self.inner, cx)
965		}
966	}
967
968	// The point of the lite-only entry: a `!Send` transport yields a `!Send`
969	// driver polled by its caller, while the severed Session handle stays
970	// Send + Sync. Compiling is most of the assertion; the rest checks the
971	// machine still relays the close.
972	#[test]
973	fn connect_lite_over_a_send_less_transport() {
974		let fake = FakeSession::new(Some(ALPN_LITE_04), Vec::new());
975		let local = LocalSession {
976			inner: fake.clone(),
977			_local: std::rc::Rc::new(()),
978		};
979		let client = Client::new().with_versions(Version::Lite(lite::Version::Lite04).into());
980
981		let runtime = crate::runtime::Test::new();
982		let (session, mut driver) = futures::executor::block_on(client.connect_lite(runtime.now(), local)).unwrap();
983		assert!(driver.poll(runtime.now(), &kio::Waiter::noop()).is_ok());
984
985		fn assert_send_sync<T: Send + Sync>(_: &T) {}
986		assert_send_sync(&session);
987
988		session.abort(Error::Cancel);
989		let _ = driver.poll(runtime.now(), &kio::Waiter::noop());
990		assert_eq!(
991			fake.state.close_events.lock().unwrap()[0].0,
992			SessionError::Cancel.to_code()
993		);
994	}
995
996	// The server-side twin: a `!Send` transport accepts a lite session whose
997	// driver the caller polls directly.
998	#[test]
999	fn accept_lite_over_a_send_less_transport() {
1000		let fake = FakeSession::new(Some(ALPN_LITE_04), Vec::new());
1001		let local = LocalSession {
1002			inner: fake.clone(),
1003			_local: std::rc::Rc::new(()),
1004		};
1005		let server = crate::Server::new().with_versions(Version::Lite(lite::Version::Lite04).into());
1006
1007		let runtime = crate::runtime::Test::new();
1008		let (session, mut driver) = futures::executor::block_on(server.accept_lite(runtime.now(), local)).unwrap();
1009		assert_eq!(session.version(), Version::Lite(lite::Version::Lite04));
1010		assert!(driver.poll(runtime.now(), &kio::Waiter::noop()).is_ok());
1011
1012		drop(session);
1013		assert!(fake.state.close_events.lock().unwrap().is_empty());
1014		let _ = driver.poll(runtime.now(), &kio::Waiter::noop());
1015		assert_eq!(
1016			fake.state.close_events.lock().unwrap()[0].0,
1017			SessionError::Cancel.to_code()
1018		);
1019	}
1020
1021	// The lite-only entry refuses everything that still needs the boxed ietf
1022	// driver, instead of silently negotiating it.
1023	#[test]
1024	fn connect_lite_refuses_ietf_alpns() {
1025		let fake = FakeSession::new(Some(ALPN_19), Vec::new());
1026		let local = LocalSession {
1027			inner: fake,
1028			_local: std::rc::Rc::new(()),
1029		};
1030		let client = Client::new();
1031		let runtime = crate::runtime::Test::new();
1032		let result = futures::executor::block_on(client.connect_lite(runtime.now(), local));
1033		assert!(matches!(result, Err(Error::Version)));
1034	}
1035
1036	// `stats()` reads the machine's latest sample and primes the sampler, so a
1037	// periodic poller observes fresh counters without consuming the bandwidth
1038	// channel.
1039	#[tokio::test(start_paused = true)]
1040	async fn stats_reads_prime_the_sampler() {
1041		let fake = FakeSession::new(Some(ALPN_LITE_04), Vec::new());
1042		fake.set_send_rate(Some(1_000_000));
1043
1044		let client = Client::new().with_versions(Version::Lite(lite::Version::Lite04).into());
1045		let (session, driver) = client
1046			.connect(tokio::time::Instant::now().into_std(), fake.clone())
1047			.await
1048			.unwrap();
1049		tokio::spawn(crate::time::run(driver));
1050
1051		// The construction-time snapshot, before the machine sampled anything.
1052		assert_eq!(
1053			session.stats().estimated_send_rate,
1054			Some(crate::bandwidth::Rate::from_bps(1_000_000))
1055		);
1056
1057		// That read was demand: the machine keeps sampling while stats are read,
1058		// so the new rate shows up within an interval (paused time auto-advances).
1059		fake.set_send_rate(Some(2_000_000));
1060		while session.stats().estimated_send_rate != Some(crate::bandwidth::Rate::from_bps(2_000_000)) {
1061			tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1062		}
1063	}
1064
1065	// Sampling stops when the supervisor ends, but `stats()` keeps serving its
1066	// cell, so the last thing the supervisor does is take a final snapshot.
1067	// Without one, "what did that session move?" asked at teardown answers with
1068	// the construction-time snapshot: this backend reports no send rate, so
1069	// there is no bandwidth consumer keeping the sampler ticking, and the test
1070	// never reads stats while the session is live.
1071	#[tokio::test(start_paused = true)]
1072	async fn stats_capture_the_final_counters() {
1073		let fake = FakeSession::new(Some(ALPN_LITE_04), Vec::new());
1074		fake.set_send_rate(None);
1075		fake.set_bytes_sent(Some(0));
1076
1077		let client = Client::new().with_versions(Version::Lite(lite::Version::Lite04).into());
1078		let (session, driver) = client
1079			.connect(tokio::time::Instant::now().into_std(), fake.clone())
1080			.await
1081			.unwrap();
1082		tokio::spawn(crate::time::run(driver));
1083		assert!(
1084			session.send_bandwidth().is_none(),
1085			"no send-rate estimate, so nothing samples on its own"
1086		);
1087
1088		fake.set_bytes_sent(Some(4242));
1089
1090		session.abort(Error::Cancel);
1091		session.closed().await;
1092
1093		assert_eq!(
1094			session.stats().bytes_sent,
1095			Some(4242),
1096			"the closing snapshot must carry the session's final counters"
1097		);
1098	}
1099
1100	// The send-bandwidth sampler lives inside the driver: it samples as soon as a
1101	// consumer exists and keeps sampling on its interval. Paused tokio time makes
1102	// the interval fire deterministically.
1103	#[tokio::test(start_paused = true)]
1104	async fn send_bandwidth_samples_while_the_driver_runs() {
1105		let fake = FakeSession::new(Some(ALPN_LITE_04), Vec::new());
1106		fake.set_send_rate(Some(1_000_000));
1107
1108		let client = Client::new().with_versions(Version::Lite(lite::Version::Lite04).into());
1109		let (session, driver) = client
1110			.connect(tokio::time::Instant::now().into_std(), fake.clone())
1111			.await
1112			.unwrap();
1113		tokio::spawn(crate::time::run(driver));
1114
1115		let mut bandwidth = session.send_bandwidth().expect("backend reports an estimate");
1116		assert_eq!(
1117			bandwidth.changed().await.unwrap(),
1118			Some(crate::bandwidth::Rate::from_bps(1_000_000))
1119		);
1120
1121		// A later change is picked up by the next interval tick.
1122		fake.set_send_rate(Some(2_000_000));
1123		assert_eq!(
1124			bandwidth.changed().await.unwrap(),
1125			Some(crate::bandwidth::Rate::from_bps(2_000_000))
1126		);
1127	}
1128}