Skip to main content

moq_net/
server.rs

1//! Accepting a MoQ session, including the paused handshake that inspects the
2//! peer's SETUP before granting origins.
3
4use web_transport_trait::{MaybeSend, MaybeSync};
5
6use crate::origin;
7use crate::time::{Clock, Instant};
8use crate::{
9	ALPN_14, ALPN_15, ALPN_16, ALPN_17, ALPN_18, ALPN_19, ALPN_20, ALPN_21, ALPN_22, ALPN_LITE, ALPN_LITE_03,
10	ALPN_LITE_04, ALPN_LITE_05, ALPN_LITE_06, ALPN_LITE_07_WIP, Consume, Error, NEGOTIATED, Role, Session,
11	SessionError, Version, Versions,
12	coding::{Decode, Encode, Stream},
13	ietf, lite, setup, stats,
14};
15
16/// A MoQ server session builder.
17#[derive(Default, Clone)]
18pub struct Server {
19	publish: Option<origin::Consumer>,
20	subscribe: Option<origin::Producer>,
21	stats: stats::Session,
22	versions: Versions,
23}
24
25impl Server {
26	/// A server that neither publishes nor subscribes until configured.
27	pub fn new() -> Self {
28		Default::default()
29	}
30
31	/// Publish to the connected client: the session reads from the given origin
32	/// (pass an [`origin::Producer`] or [`origin::Consumer`] by reference) and forwards
33	/// its announcements. Omit to publish nothing. Pre-scoped via
34	/// [`origin::Producer::scope`] for token-gated relays.
35	pub fn with_publisher(mut self, publish: impl Consume<origin::Consumer>) -> Self {
36		self.publish = Some(publish.consume());
37		self
38	}
39
40	/// Subscribe to the connected client: the session writes the broadcasts the
41	/// client announces into this [`origin::Producer`]. Omit to subscribe to nothing.
42	pub fn with_subscriber(mut self, subscribe: origin::Producer) -> Self {
43		self.subscribe = Some(subscribe);
44		self
45	}
46
47	/// Attach a per-connection [`stats::Session`] context. The session's publish
48	/// (egress) and subscribe (ingress) origin handles are tagged with it, so all
49	/// traffic counters are attributed through the model for this session's lifetime.
50	/// Pass [`stats::Session::default`] (a no-op context) to opt out.
51	pub fn with_stats(mut self, stats: stats::Session) -> Self {
52		self.stats = stats;
53		self
54	}
55
56	/// Set both publish and subscribe from one shared [`origin::Producer`].
57	pub fn with_origin(self, origin: origin::Producer) -> Self {
58		self.with_publisher(&origin).with_subscriber(origin)
59	}
60
61	/// Restrict which protocol versions to accept, in preference order.
62	/// Defaults to every version this crate supports.
63	pub fn with_versions(mut self, versions: Versions) -> Self {
64		self.versions = versions;
65		self
66	}
67
68	/// The configured origin pair, each tagged with the stats context so the
69	/// model attributes reads (egress) and writes (ingress) for this session.
70	/// One shared context across both halves keeps presence and viewer counts
71	/// from double-attributing.
72	fn stat_tagged_origins(&self) -> (Option<origin::Consumer>, Option<origin::Producer>) {
73		let publish = self.publish.clone().map(|origin| origin.with_stats(self.stats.clone()));
74		let subscribe = self
75			.subscribe
76			.clone()
77			.map(|origin| origin.with_stats(self.stats.clone()));
78		(publish, subscribe)
79	}
80
81	/// Start a lite session on an accepted transport: wire the origins, answer
82	/// with our SETUP, and return the session and its driver.
83	fn start_lite<S>(
84		&self,
85		runtime: Clock,
86		session: S,
87		version: lite::Version,
88		client_setup: Option<lite::Setup>,
89		peer_hop: Option<crate::Hop>,
90	) -> Result<(Session, crate::Driver<S>), Error>
91	where
92		S: crate::transport::poll::Session,
93	{
94		let (publish, subscribe) = self.stat_tagged_origins();
95
96		// We report what the transport actually measures; a server never
97		// advertises a request Path or Role, and only the dialing side prices a
98		// link. Versions without a Setup Stream have nothing to advertise.
99		let our_setup = if version.has_setup_stream() {
100			lite::Setup {
101				probe: lite::ProbeLevel::detect(&session),
102				path: None,
103				role: None,
104				cost: None,
105				// Filled by `lite::start` from the attached origin handles.
106				hop: None,
107			}
108		} else {
109			lite::Setup::default()
110		};
111
112		let start = lite::start(lite::Config {
113			runtime: runtime.clone(),
114			session: session.clone(),
115			setup_stream: None,
116			publish,
117			subscribe,
118			peer_hop,
119			version,
120			our_setup,
121			peer_setup: client_setup,
122		})?;
123
124		Ok(Session::new(
125			runtime,
126			session,
127			version.into(),
128			start.recv_bandwidth,
129			crate::driver::Protocol::Lite(Box::new(start.driver)),
130			start.goaway,
131		))
132	}
133
134	/// Perform the MoQ handshake for moq-lite only, over any transport.
135	///
136	/// Same trade as [`Client::connect_lite`](crate::Client::connect_lite): no
137	/// thread-affinity bound on the transport, so a pinned `!Send` transport
138	/// works, and only a moq-lite ALPN is accepted (anything else is refused
139	/// with [`Error::Version`]). Completes the handshake immediately; a caller
140	/// gating on the advertised path uses
141	/// [`accept_request_lite`](Self::accept_request_lite) instead.
142	pub async fn accept_lite<S>(&self, now: Instant, session: S) -> Result<(Session, crate::Driver<S>), Error>
143	where
144		S: crate::transport::poll::Session,
145	{
146		self.accept_request_lite(now, session).await?.ok().await
147	}
148
149	/// Begin the moq-lite handshake, pausing like
150	/// [`accept_request`](Self::accept_request) but for moq-lite ALPNs only,
151	/// which is what drops the thread-affinity bounds: a pinned `!Send`
152	/// transport can gate on the advertised path too. Anything but a moq-lite
153	/// ALPN is refused with [`Error::Version`].
154	pub async fn accept_request_lite<S>(&self, now: Instant, session: S) -> Result<Handshake<S>, Error>
155	where
156		S: crate::transport::poll::Session,
157	{
158		let mut refused = session.clone();
159		self.handshake_lite(now, session)
160			.await
161			.inspect_err(|err| close(&mut refused, err))
162	}
163
164	async fn handshake_lite<S>(&self, now: Instant, mut session: S) -> Result<Handshake<S>, Error>
165	where
166		S: crate::transport::poll::Session,
167	{
168		let runtime = Clock::new(now);
169		let (path, role, origin, handshake) = match session.protocol() {
170			Some(alpn @ (ALPN_LITE_05 | ALPN_LITE_06 | ALPN_LITE_07_WIP)) => {
171				let version = match alpn {
172					ALPN_LITE_07_WIP => lite::Version::Lite07,
173					ALPN_LITE_06 => lite::Version::Lite06,
174					_ => lite::Version::Lite05,
175				};
176				self.versions.select(Version::Lite(version)).ok_or(Error::Version)?;
177				// Gate on the client's SETUP: read it before serving so the
178				// caller can scope by the advertised path. Seeded back into
179				// `start` on `ok()` so PROBE gating resolves without
180				// re-reading the (consumed) Setup Stream.
181				let client_setup = lite::accept_setup(&mut session, version).await?;
182				(
183					client_setup.path.clone(),
184					client_setup.role,
185					client_setup.hop,
186					PausedHandshake::LiteSetup {
187						session,
188						version,
189						client_setup,
190					},
191				)
192			}
193			Some(ALPN_LITE_04) => {
194				self.versions
195					.select(Version::Lite(lite::Version::Lite04))
196					.ok_or(Error::Version)?;
197				(
198					None,
199					None,
200					None,
201					PausedHandshake::LiteBare {
202						session,
203						version: lite::Version::Lite04,
204					},
205				)
206			}
207			Some(ALPN_LITE_03) => {
208				self.versions
209					.select(Version::Lite(lite::Version::Lite03))
210					.ok_or(Error::Version)?;
211				(
212					None,
213					None,
214					None,
215					PausedHandshake::LiteBare {
216						session,
217						version: lite::Version::Lite03,
218					},
219				)
220			}
221			_ => return Err(Error::Version),
222		};
223
224		Ok(Handshake {
225			path,
226			role,
227			origin,
228			// moq-lite carries no SETUP token.
229			token: None,
230			assigned_hop: crate::Hop::random(),
231			inner: Some(RequestInner {
232				server: self.clone(),
233				runtime,
234				handshake,
235			}),
236		})
237	}
238
239	/// Perform the MoQ handshake as a server, returning the [`Session`] and its [`Driver`](crate::Driver).
240	///
241	/// Poll the returned driver with nondecreasing time, starting at `now`.
242	///
243	/// Convenience wrapper over [`accept_request`](Self::accept_request) that
244	/// completes the handshake immediately. Use `accept_request` when you need to
245	/// inspect the client's advertised path before deciding what to serve.
246	pub async fn accept<S>(&self, now: Instant, session: S) -> Result<(Session, crate::Driver<S>), Error>
247	where
248		S: crate::transport::poll::Boxable,
249		S::SendStream: MaybeSync,
250		S::RecvStream: MaybeSync,
251	{
252		self.accept_request(now, session).await?.ok().await
253	}
254
255	/// Begin the MoQ handshake, pausing once the client's request path is known so
256	/// the caller can authorize/scope before serving.
257	///
258	/// Reads the client's SETUP (the in-band path lives there on URL-less transports),
259	/// then returns a [`Handshake`]: inspect [`path`](Handshake::path), set the origins to
260	/// serve, and call [`ok`](Handshake::ok) or [`close`](Handshake::close). Session start
261	/// is deferred to `ok()`, so origins set on the handshake always take effect.
262	///
263	/// The path is surfaced for moq-lite-05 and newer, and every moq-transport
264	/// draft we speak; it's empty on versions with no in-band request path (lite 01-04).
265	///
266	/// A SETUP that fails to parse or negotiate closes the session with the matching code,
267	/// so the peer learns why instead of seeing a bare disconnect.
268	pub async fn accept_request<S>(&self, now: Instant, session: S) -> Result<Handshake<S>, Error>
269	where
270		S: crate::transport::poll::Boxable,
271		S::SendStream: MaybeSync,
272		S::RecvStream: MaybeSync,
273	{
274		let mut refused = session.clone();
275		self.handshake(now, session)
276			.await
277			.inspect_err(|err| close(&mut refused, err))
278	}
279
280	async fn handshake<S>(&self, now: Instant, mut session: S) -> Result<Handshake<S>, Error>
281	where
282		S: crate::transport::poll::Boxable,
283		S::SendStream: MaybeSync,
284		S::RecvStream: MaybeSync,
285	{
286		let runtime = Clock::new(now);
287		let (encoding, supported) = match session.protocol() {
288			Some(alpn @ (ALPN_22 | ALPN_21 | ALPN_20 | ALPN_19 | ALPN_18 | ALPN_17)) => {
289				let draft = match alpn {
290					ALPN_22 => ietf::Version::Draft22,
291					ALPN_21 => ietf::Version::Draft21,
292					ALPN_20 => ietf::Version::Draft20,
293					ALPN_19 => ietf::Version::Draft19,
294					ALPN_18 => ietf::Version::Draft18,
295					_ => ietf::Version::Draft17,
296				};
297
298				self.versions.select(Version::Ietf(draft)).ok_or(Error::Version)?;
299				return self.accept_ietf_modern(runtime, session, draft).await;
300			}
301			Some(ALPN_16) => {
302				let v = self
303					.versions
304					.select(Version::Ietf(ietf::Version::Draft16))
305					.ok_or(Error::Version)?;
306				(v, v.into())
307			}
308			Some(ALPN_15) => {
309				let v = self
310					.versions
311					.select(Version::Ietf(ietf::Version::Draft15))
312					.ok_or(Error::Version)?;
313				(v, v.into())
314			}
315			Some(ALPN_14) => {
316				let v = self
317					.versions
318					.select(Version::Ietf(ietf::Version::Draft14))
319					.ok_or(Error::Version)?;
320				(v, v.into())
321			}
322			// Every lite ALPN goes through the same entry point, which is also
323			// what a `!Send` transport calls directly.
324			Some(ALPN_LITE_07_WIP | ALPN_LITE_06 | ALPN_LITE_05 | ALPN_LITE_04 | ALPN_LITE_03) => {
325				return self.handshake_lite(now, session).await;
326			}
327			Some(ALPN_LITE) | None => {
328				let supported = self.versions.filter(&NEGOTIATED.into()).ok_or(Error::Version)?;
329				(Version::Ietf(ietf::Version::Draft14), supported)
330			}
331			Some(p) => return Err(Error::UnknownAlpn(p.to_string())),
332		};
333
334		// Legacy bidi SETUP exchange (IETF 14-16, lite 01/02). Read the client's
335		// SETUP to choose the version; `ok()` sends the server SETUP and starts.
336		let mut stream = Stream::accept(&mut session, encoding).await?;
337		let mut client: setup::Client = stream.reader.decode().await?;
338
339		let version = client
340			.versions
341			.iter()
342			.flat_map(|v| Version::try_from(*v).ok())
343			.find(|v| supported.contains(v))
344			.ok_or(Error::Version)?;
345
346		// Pull the request path and max request ID out now (IETF only) so `ok()`
347		// doesn't re-decode the consumed parameters. moq-transport carries the path
348		// in its SETUP just like lite-05.
349		let (path, token, request_id_max, peer_declared) = match version {
350			Version::Ietf(v) => {
351				let params = ietf::Parameters::decode(&mut client.parameters, v)?;
352				let path = match params.get_bytes(ietf::ParameterBytes::Path) {
353					Some(bytes) => Some(
354						std::str::from_utf8(bytes)
355							.map_err(|_| Error::Decode(crate::DecodeError::InvalidValue))?
356							.to_owned(),
357					),
358					None => None,
359				};
360				let token = ietf::token::from_setup(&params, v)?;
361				let request_id_max = params
362					.get_varint(ietf::ParameterVarInt::MaxRequestId)
363					.map(ietf::RequestId);
364				let peer_declared = ietf::peer::Peer {
365					solicit: ietf::solicit::from_setup(&params, v)?,
366					hidden: ietf::hidden::from_setup(&params, v),
367					..Default::default()
368				};
369				(path, token, request_id_max, peer_declared)
370			}
371			Version::Lite(_) => (None, None, None, ietf::peer::Peer::default()),
372		};
373
374		Ok(Handshake {
375			path,
376			role: None,
377			origin: None,
378			token,
379			assigned_hop: crate::Hop::random(),
380			inner: Some(RequestInner {
381				server: self.clone(),
382				runtime,
383				handshake: PausedHandshake::Boxed(Box::new(PausedLegacy {
384					session,
385					stream,
386					version,
387					request_id_max,
388					peer_declared,
389				})),
390			}),
391		})
392	}
393
394	/// Read a draft-17/18 client's SETUP (with its request path) off its uni stream,
395	/// then pause. `ok()` starts the session and hands the stream back for GOAWAY.
396	async fn accept_ietf_modern<S>(
397		&self,
398		runtime: Clock,
399		mut session: S,
400		version: ietf::Version,
401	) -> Result<Handshake<S>, Error>
402	where
403		S: crate::transport::poll::Boxable,
404		S::SendStream: MaybeSync,
405		S::RecvStream: MaybeSync,
406	{
407		let peer_setup = ietf::accept_setup(&mut session, version).await?;
408		Ok(Handshake {
409			path: peer_setup.path.clone(),
410			role: None,
411			// A moq-transport peer only has an identity if it negotiated the MoQ
412			// Cluster extension and declared a non-zero Hop ID.
413			origin: peer_setup.declared.cluster.hop.filter(|h| *h != crate::Hop::UNKNOWN),
414			token: peer_setup.token.clone(),
415			assigned_hop: crate::Hop::random(),
416			inner: Some(RequestInner {
417				server: self.clone(),
418				runtime,
419				handshake: PausedHandshake::Boxed(Box::new(PausedIetfModern {
420					session,
421					version,
422					peer_setup,
423				})),
424			}),
425		})
426	}
427}
428
429/// A paused server-side handshake.
430///
431/// Returned by [`Server::accept_request`] once the peer's advertised
432/// [`path`](Self::path) is known but before the session is granted anything. Set
433/// the origins to serve, then call [`ok`](Self::ok) to complete the handshake, or
434/// [`close`](Self::close) to reject it. Modeled on the WebTransport `Request` in
435/// moq-tokio.
436pub struct Handshake<S: crate::transport::poll::Session> {
437	path: Option<String>,
438	role: Option<Role>,
439	origin: Option<crate::Hop>,
440	token: Option<setup::Token>,
441	/// The identity this session's routes are stamped with when the peer declares none
442	/// on the wire. Fresh per request unless the caller overrides it
443	/// ([`Handshake::with_peer_hop`]).
444	assigned_hop: crate::Hop,
445	// Taken by `ok`/`close`; `Drop` rejects the handshake if neither ran.
446	inner: Option<RequestInner<S>>,
447}
448
449/// The parts of a [`Handshake`] consumed by [`Handshake::ok`] / [`Handshake::close`].
450struct RequestInner<S: crate::transport::poll::Session> {
451	server: Server,
452	/// Supplies the clock and timers for the accepted session.
453	runtime: Clock,
454	handshake: PausedHandshake<S>,
455}
456
457/// The handshake state captured at the pause point. Every variant defers its
458/// session start to [`Handshake::ok`] so origins set on the handshake still apply.
459enum PausedHandshake<S: crate::transport::poll::Session> {
460	/// moq-lite 03/04: no Setup Stream.
461	LiteBare { session: S, version: lite::Version },
462	/// moq-lite 05+: the client's Setup Stream has been read. `ok()` starts the
463	/// session, seeding the SETUP back so PROBE gating resolves.
464	LiteSetup {
465		session: S,
466		version: lite::Version,
467		client_setup: lite::Setup,
468	},
469	/// An IETF (or legacy bidi-SETUP) handshake, boxed where its
470	/// thread-affinity bounds held. The boxing is what keeps [`Handshake`] and
471	/// its lite path free of those bounds: the ietf machinery erases its
472	/// futures, which forces a per-target `Send` choice a pinned `!Send`
473	/// transport cannot satisfy, so the choice is made here, at construction,
474	/// where the caller proved the bounds.
475	Boxed(Box<dyn Paused<S>>),
476}
477
478type Accept<S> = crate::util::MaybeSendBox<'static, Result<(Session, crate::Driver<S>), Error>>;
479
480/// A paused non-lite handshake. See [`PausedHandshake::Boxed`] for why this is a
481/// trait object.
482///
483/// `MaybeSync` is not decoration: a caller holding a [`Handshake`] across an
484/// await behind `&self` (moq-relay authenticates that way) needs
485/// `&Handshake: Send`, which is `Handshake: Sync`, which is this.
486trait Paused<S: crate::transport::poll::Session>: MaybeSend + MaybeSync {
487	/// Complete the handshake with the final server config.
488	fn ok(self: Box<Self>, server: Server, runtime: Clock, peer_hop: Option<crate::Hop>) -> Accept<S>;
489
490	/// Reject the handshake, closing the transport with `err`'s wire code.
491	fn close(self: Box<Self>, err: Error);
492}
493
494/// Modern IETF (17/18): the client's SETUP (with its request path) has been
495/// read off its uni stream; `ok` starts the session, handing that stream back
496/// for GOAWAY monitoring.
497struct PausedIetfModern<S: crate::transport::poll::Session> {
498	session: S,
499	version: ietf::Version,
500	peer_setup: ietf::PeerSetup<S>,
501}
502
503impl<S> Paused<S> for PausedIetfModern<S>
504where
505	S: crate::transport::poll::Boxable,
506	S::SendStream: MaybeSync,
507	S::RecvStream: MaybeSync,
508{
509	fn ok(self: Box<Self>, server: Server, runtime: Clock, peer_hop: Option<crate::Hop>) -> Accept<S> {
510		use crate::util::MaybeBoxedExt as _;
511		async move {
512			let Self {
513				session,
514				version,
515				peer_setup,
516			} = *self;
517			let (publish, subscribe) = server.stat_tagged_origins();
518
519			// The client's SETUP was read at the pause; hand the stream back
520			// for GOAWAY. A server never advertises a path, hence `None`.
521			let (protocol, goaway) = ietf::start(ietf::Config {
522				runtime: runtime.clone(),
523				session: session.clone(),
524				setup: None,
525				request_id_max: None,
526				client: false,
527				publish,
528				subscribe,
529				peer_hop,
530				// Only the dialing side prices a link.
531				cost: None,
532				version,
533				path: None,
534				authority: None,
535				peer_setup_stream: Some(peer_setup.stream),
536				peer_declared: Some(peer_setup.declared),
537				early_unis: peer_setup.early,
538			})?;
539			tracing::debug!(?version, "connected");
540			Ok(Session::new(
541				runtime,
542				session,
543				version.into(),
544				None,
545				crate::driver::Protocol::Ietf(protocol),
546				goaway,
547			))
548		}
549		.maybe_boxed()
550	}
551
552	fn close(mut self: Box<Self>, err: Error) {
553		close(&mut self.session, &err);
554	}
555}
556
557/// Legacy IETF (draft 14-16) and lite 01/02: the client SETUP has been read
558/// off the bidi stream (including its request path) but the server SETUP
559/// hasn't been sent; `ok` finishes it.
560struct PausedLegacy<S: crate::transport::poll::Session> {
561	session: S,
562	stream: Stream<S, Version>,
563	version: Version,
564	request_id_max: Option<ietf::RequestId>,
565	/// What the client's SETUP declared, for the options `ok` acts on.
566	peer_declared: ietf::peer::Peer,
567}
568
569impl<S> Paused<S> for PausedLegacy<S>
570where
571	S: crate::transport::poll::Boxable,
572	S::SendStream: MaybeSync,
573	S::RecvStream: MaybeSync,
574{
575	fn ok(self: Box<Self>, server: Server, runtime: Clock, peer_hop: Option<crate::Hop>) -> Accept<S> {
576		use crate::util::MaybeBoxedExt as _;
577		async move {
578			let Self {
579				session,
580				mut stream,
581				version,
582				request_id_max,
583				peer_declared,
584			} = *self;
585			let (publish, subscribe) = server.stat_tagged_origins();
586
587			// Encode parameters using the version-appropriate type.
588			let parameters = match version {
589				Version::Ietf(v) => {
590					let mut parameters = ietf::Parameters::default();
591					parameters.set_varint(ietf::ParameterVarInt::MaxRequestId, u32::MAX as u64);
592					parameters.set_bytes(ietf::ParameterBytes::Implementation, b"moq-lite-rs".to_vec());
593					ietf::solicit::into_setup(&mut parameters, v);
594					ietf::hidden::into_setup(&mut parameters, v);
595					parameters.encode_bytes(v)?
596				}
597				Version::Lite(v) => lite::Parameters::default().encode_bytes(v)?,
598			};
599
600			let server_setup = setup::Server {
601				version: version.into(),
602				parameters,
603			};
604			stream.writer.encode(&server_setup).await?;
605
606			let (recv_bw, protocol, goaway) = match version {
607				Version::Lite(v) => {
608					let stream = stream.with_version(v);
609					// Pre-lite-05: no Setup Stream, so nothing to advertise or seed.
610					let start = lite::start(lite::Config {
611						runtime: runtime.clone(),
612						session: session.clone(),
613						setup_stream: Some(stream),
614						publish,
615						subscribe,
616						peer_hop,
617						version: v,
618						our_setup: lite::Setup::default(),
619						peer_setup: None,
620					})?;
621					(
622						start.recv_bandwidth,
623						crate::driver::Protocol::Lite(Box::new(start.driver)),
624						start.goaway,
625					)
626				}
627				Version::Ietf(v) => {
628					let stream = stream.with_version(v);
629					// Draft 14-16: path came in the bidi SETUP, no uni SETUP to hand back.
630					let (protocol, goaway) = ietf::start(ietf::Config {
631						runtime: runtime.clone(),
632						session: session.clone(),
633						setup: Some(stream),
634						request_id_max,
635						client: false,
636						publish,
637						subscribe,
638						peer_hop,
639						cost: None,
640						version: v,
641						path: None,
642						authority: None,
643						peer_setup_stream: None,
644						peer_declared: Some(peer_declared),
645						early_unis: Vec::new(),
646					})?;
647					(None, crate::driver::Protocol::Ietf(protocol), goaway)
648				}
649			};
650
651			Ok(Session::new(runtime, session, version, recv_bw, protocol, goaway))
652		}
653		.maybe_boxed()
654	}
655
656	fn close(mut self: Box<Self>, err: Error) {
657		close(&mut self.session, &err);
658	}
659}
660
661impl<S> Handshake<S>
662where
663	S: crate::transport::poll::Session,
664{
665	/// The request path the client advertised in its SETUP.
666	///
667	/// Empty when the client advertised none: either it sent an empty path, or the
668	/// version carries none in-band (lite 01-04). Those mean the same thing, so the
669	/// wire distinction isn't surfaced. Populated for moq-lite-05 and newer,
670	/// and every moq-transport draft we speak. See the note on [`Server::accept_request`].
671	pub fn path(&self) -> &str {
672		self.path.as_deref().unwrap_or("")
673	}
674
675	/// The single [`Role`] the client advertised in its SETUP, or `None` for a
676	/// bidirectional session.
677	///
678	/// Only moq-lite-05 and newer carry a role, so `None` covers three cases
679	/// that the wire doesn't distinguish: an older version, a client that omitted the parameter, and a
680	/// client that explicitly advertised both directions. All three mean the same thing
681	/// (the client may publish and subscribe), so authorize on what the token grants.
682	/// See the note on [`Server::accept_request`].
683	pub fn role(&self) -> Option<Role> {
684		self.role
685	}
686
687	/// The Hop ID declared by the peer, when the negotiated protocol carries one.
688	///
689	/// A moq-lite-05+ endpoint declares this when it attaches a publish or subscribe
690	/// origin; a `moqt-17`+ endpoint declares it via the MoQ Cluster extension. Older
691	/// versions and endpoints without one return `None`.
692	///
693	/// Self-declared, so treat it as a correlation hint rather than an
694	/// authenticated identity: authorize on the token or client certificate.
695	pub fn peer_hop(&self) -> Option<crate::Hop> {
696		self.origin
697	}
698
699	/// The credential the client presented in its SETUP's `AUTHORIZATION TOKEN` option.
700	///
701	/// Only moq-transport carries one, so moq-lite sessions return `None`. The transport
702	/// has not verified it: authorize on it the way you would a URL token.
703	pub fn token(&self) -> Option<&setup::Token> {
704		self.token.as_ref()
705	}
706
707	/// Publish to the connected client. Overrides any value from the [`Server`]
708	/// builder; typically set after inspecting [`path`](Self::path).
709	pub fn with_publisher(mut self, publish: impl Consume<origin::Consumer>) -> Self {
710		self.inner_mut().server.publish = Some(publish.consume());
711		self
712	}
713
714	/// Subscribe to the connected client. Overrides any value from the [`Server`] builder.
715	pub fn with_subscriber(mut self, subscribe: origin::Producer) -> Self {
716		self.inner_mut().server.subscribe = Some(subscribe);
717		self
718	}
719
720	/// Assign the identity this peer's routes are attributed to, overriding the fresh
721	/// per-session default.
722	///
723	/// Only for a peer whose identity the server has actually established, such as one
724	/// authenticated by mTLS or a token ([`crate::Client::with_peer_hop`] is the
725	/// dialing-side equivalent). An identity the peer declares on the wire still wins.
726	///
727	/// Two sessions given the same origin are treated as one endpoint: routes learned
728	/// from either are kept off both, and content arriving on either is interchangeable
729	/// with the other's. That is the point when they really are one peer reconnecting or
730	/// running redundant links, and a bug otherwise. Derive it from the authenticated
731	/// identity, never from something coarser like the remote address.
732	pub fn with_peer_hop(mut self, hop: crate::Hop) -> Self {
733		self.assigned_hop = hop;
734		self
735	}
736
737	/// Set the per-connection [`stats::Session`] context. Overrides any value from the
738	/// [`Server`] builder.
739	pub fn with_stats(mut self, stats: stats::Session) -> Self {
740		self.inner_mut().server.stats = stats;
741		self
742	}
743
744	fn inner_mut(&mut self) -> &mut RequestInner<S> {
745		self.inner.as_mut().expect("request already responded")
746	}
747
748	/// Accept the session, returning the [`Session`] and its [`Driver`](crate::Driver).
749	///
750	/// Poll or spawn the returned driver to run the session.
751	pub async fn ok(mut self) -> Result<(Session, crate::Driver<S>), Error> {
752		let peer_hop = Some(self.assigned_hop);
753		let RequestInner {
754			server,
755			runtime,
756			handshake,
757		} = self.inner.take().expect("request already responded");
758
759		match handshake {
760			PausedHandshake::LiteBare { session, version } => {
761				server.start_lite(runtime, session, version, None, peer_hop)
762			}
763			PausedHandshake::LiteSetup {
764				session,
765				version,
766				client_setup,
767			} => server.start_lite(runtime, session, version, Some(client_setup), peer_hop),
768			PausedHandshake::Boxed(paused) => paused.ok(server, runtime, peer_hop).await,
769		}
770	}
771
772	/// Reject the session, closing the transport with `err`'s wire code.
773	pub fn close(mut self, err: Error) {
774		let inner = self.inner.take().expect("request already responded");
775		inner.close(err);
776	}
777}
778
779impl<S: crate::transport::poll::Session> RequestInner<S> {
780	fn close(self, err: Error) {
781		let mut session = match self.handshake {
782			PausedHandshake::LiteBare { session, .. } => session,
783			PausedHandshake::LiteSetup { session, .. } => session,
784			PausedHandshake::Boxed(paused) => return paused.close(err),
785		};
786		close(&mut session, &err);
787	}
788}
789
790/// Close `session` with `err`'s wire code.
791fn close<S: crate::transport::poll::Session>(session: &mut S, err: &Error) {
792	session.close(SessionError::from(err).to_code(), &err.to_string());
793}
794
795impl<S: crate::transport::poll::Session> Drop for Handshake<S> {
796	// A dropped request would otherwise leave the client hanging until its idle
797	// timeout: it already sent SETUP and is waiting on a response. Reject loudly.
798	fn drop(&mut self) {
799		if let Some(inner) = self.inner.take() {
800			tracing::warn!("Handshake dropped without ok() or close(); rejecting the session");
801			inner.close(Error::Cancel);
802		}
803	}
804}
805
806#[cfg(test)]
807mod tests {
808	use super::*;
809	use crate::Hop;
810	use crate::model::ProduceTest;
811	use std::{
812		collections::VecDeque,
813		sync::{Arc, Mutex},
814	};
815
816	use crate::ALPN_LITE_05;
817	use bytes::Bytes;
818
819	fn occurrences(log: &crate::lite::test_transport::Log, needle: &[u8]) -> usize {
820		let writes = log.writes.lock().unwrap();
821		writes.windows(needle.len()).filter(|window| *window == needle).count()
822	}
823
824	#[derive(Debug, Clone, Default)]
825	struct FakeError;
826	impl std::fmt::Display for FakeError {
827		fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
828			write!(f, "fake transport error")
829		}
830	}
831	impl std::error::Error for FakeError {}
832	impl web_transport_trait::Error for FakeError {
833		fn session_error(&self) -> Option<(u32, String)> {
834			Some((0, "closed".to_string()))
835		}
836	}
837
838	/// A session that replays a queue of streams (each a `Vec<u8>`) in order from
839	/// `accept_uni` and `accept_bi`, and records the code it was closed with; everything
840	/// else is inert.
841	#[derive(Clone)]
842	struct FakeSession {
843		protocol: Option<&'static str>,
844		uni: Arc<Mutex<VecDeque<Vec<u8>>>>,
845		bi: Arc<Mutex<VecDeque<Vec<u8>>>>,
846		closed: Arc<Mutex<Option<u32>>>,
847	}
848
849	impl FakeSession {
850		fn new(protocol: &'static str, uni: impl IntoIterator<Item = Vec<u8>>) -> Self {
851			Self {
852				protocol: Some(protocol),
853				uni: Arc::new(Mutex::new(uni.into_iter().collect())),
854				bi: Default::default(),
855				closed: Default::default(),
856			}
857		}
858
859		fn with_bi(self, bi: Vec<u8>) -> Self {
860			self.bi.lock().unwrap().push_back(bi);
861			self
862		}
863
864		fn closed(&self) -> Option<u32> {
865			*self.closed.lock().unwrap()
866		}
867	}
868
869	impl web_transport_trait::poll::Session for FakeSession {
870		type SendStream = FakeSend;
871		type RecvStream = FakeRecv;
872		type Error = FakeError;
873
874		fn poll_accept_uni(
875			&mut self,
876			_cx: &mut std::task::Context<'_>,
877		) -> std::task::Poll<Result<Self::RecvStream, Self::Error>> {
878			match self.uni.lock().unwrap().pop_front() {
879				Some(data) => std::task::Poll::Ready(Ok(FakeRecv { data: data.into() })),
880				None => std::task::Poll::Pending,
881			}
882		}
883		fn poll_accept_bi(
884			&mut self,
885			_cx: &mut std::task::Context<'_>,
886		) -> std::task::Poll<Result<(Self::SendStream, Self::RecvStream), Self::Error>> {
887			match self.bi.lock().unwrap().pop_front() {
888				Some(data) => std::task::Poll::Ready(Ok((FakeSend, FakeRecv { data: data.into() }))),
889				None => std::task::Poll::Pending,
890			}
891		}
892		fn poll_open_bi(
893			&mut self,
894			_cx: &mut std::task::Context<'_>,
895		) -> std::task::Poll<Result<(Self::SendStream, Self::RecvStream), Self::Error>> {
896			std::task::Poll::Pending
897		}
898		fn poll_open_uni(
899			&mut self,
900			_cx: &mut std::task::Context<'_>,
901		) -> std::task::Poll<Result<Self::SendStream, Self::Error>> {
902			std::task::Poll::Pending
903		}
904		fn poll_send_datagram(
905			&mut self,
906			_cx: &mut std::task::Context<'_>,
907			_payload: &[u8],
908		) -> std::task::Poll<Result<(), Self::Error>> {
909			std::task::Poll::Ready(Ok(()))
910		}
911		fn poll_recv_datagram(
912			&mut self,
913			_cx: &mut std::task::Context<'_>,
914		) -> std::task::Poll<Result<Bytes, Self::Error>> {
915			std::task::Poll::Pending
916		}
917		fn max_datagram_size(&self) -> usize {
918			1200
919		}
920		fn protocol(&self) -> Option<&str> {
921			self.protocol
922		}
923		fn close(&mut self, code: u32, _reason: &str) {
924			self.closed.lock().unwrap().get_or_insert(code);
925		}
926		fn poll_closed(&mut self, _cx: &mut std::task::Context<'_>) -> std::task::Poll<Self::Error> {
927			std::task::Poll::Pending
928		}
929		fn stats(&self) -> impl web_transport_trait::Stats {
930			web_transport_trait::StatsUnavailable
931		}
932	}
933
934	#[derive(Clone, Default)]
935	struct FakeSend;
936	impl web_transport_trait::poll::SendStream for FakeSend {
937		type Error = FakeError;
938		fn poll_write(
939			&mut self,
940			_cx: &mut std::task::Context<'_>,
941			buf: &[u8],
942		) -> std::task::Poll<Result<usize, Self::Error>> {
943			std::task::Poll::Ready(Ok(buf.len()))
944		}
945		fn set_priority(&mut self, _order: u8) {}
946		fn finish(&mut self) -> Result<(), Self::Error> {
947			Ok(())
948		}
949		fn reset(&mut self, _code: u32) {}
950		fn poll_closed(&mut self, _cx: &mut std::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
951			std::task::Poll::Ready(Ok(()))
952		}
953	}
954
955	struct FakeRecv {
956		data: VecDeque<u8>,
957	}
958	impl web_transport_trait::poll::RecvStream for FakeRecv {
959		type Error = FakeError;
960		fn poll_read(
961			&mut self,
962			_cx: &mut std::task::Context<'_>,
963			dst: &mut [u8],
964		) -> std::task::Poll<Result<Option<usize>, Self::Error>> {
965			if self.data.is_empty() {
966				return std::task::Poll::Ready(Ok(None));
967			}
968			let size = dst.len().min(self.data.len());
969			for slot in dst.iter_mut().take(size) {
970				*slot = self.data.pop_front().unwrap();
971			}
972			std::task::Poll::Ready(Ok(Some(size)))
973		}
974		fn stop(&mut self, _code: u32) {}
975		fn poll_closed(&mut self, _cx: &mut std::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
976			std::task::Poll::Ready(Ok(()))
977		}
978	}
979
980	/// Encode a lite-05 Setup Stream: the `DataType::Setup` tag then the SETUP message.
981	fn lite05_setup(path: Option<&str>, role: Option<Role>, hop: Option<Hop>) -> Vec<u8> {
982		let v = lite::Version::Lite05;
983		let mut buf = Vec::new();
984		lite::DataType::Setup.encode(&mut buf, v).unwrap();
985		lite::Setup {
986			probe: lite::ProbeLevel::None,
987			path: path.map(str::to_string),
988			role,
989			cost: None,
990			hop,
991		}
992		.encode(&mut buf, v)
993		.unwrap();
994		buf
995	}
996
997	/// Encode a draft-17+ Setup Stream: the unified SETUP message, whose parameters
998	/// carry the request path the same way lite-05's does.
999	fn ietf_setup(version: ietf::Version, path: Option<&str>) -> Vec<u8> {
1000		let mut params = ietf::Parameters::default();
1001		if let Some(path) = path {
1002			params.set_bytes(ietf::ParameterBytes::Path, path.as_bytes().to_vec());
1003		}
1004		ietf_setup_with(version, params)
1005	}
1006
1007	fn ietf_setup_with(version: ietf::Version, params: ietf::Parameters) -> Vec<u8> {
1008		let parameters = params.encode_bytes(version).unwrap();
1009
1010		let mut buf = Vec::new();
1011		setup::Setup { parameters }
1012			.encode(&mut buf, crate::Version::Ietf(version))
1013			.unwrap();
1014		buf
1015	}
1016
1017	/// Encode a draft 14-16 CLIENT_SETUP, sent on the control bidi stream.
1018	fn legacy_setup(version: ietf::Version, params: ietf::Parameters) -> Vec<u8> {
1019		let mut buf = Vec::new();
1020		setup::Client {
1021			versions: crate::coding::Versions::from([crate::Version::Ietf(version).into()]),
1022			parameters: params.encode_bytes(version).unwrap(),
1023		}
1024		.encode(&mut buf, crate::Version::Ietf(version))
1025		.unwrap();
1026		buf
1027	}
1028
1029	fn setup_token() -> setup::Token {
1030		setup::Token {
1031			kind: setup::Token::OUT_OF_BAND,
1032			value: vec![0x00, 0xff, b'j', b'w', b't'],
1033		}
1034	}
1035
1036	fn token_params(version: ietf::Version) -> ietf::Parameters {
1037		let mut params = ietf::Parameters::default();
1038		ietf::token::into_setup(&mut params, &setup_token(), version).unwrap();
1039		params
1040	}
1041
1042	#[tokio::test(start_paused = true)]
1043	async fn accept_request_exposes_the_setup_token() {
1044		let modern = FakeSession::new(
1045			ALPN_19,
1046			[ietf_setup_with(
1047				ietf::Version::Draft19,
1048				token_params(ietf::Version::Draft19),
1049			)],
1050		);
1051		let legacy = FakeSession::new(ALPN_16, []).with_bi(legacy_setup(
1052			ietf::Version::Draft16,
1053			token_params(ietf::Version::Draft16),
1054		));
1055		for (name, session) in [("draft-19", modern), ("draft-16", legacy)] {
1056			let request = Server::new()
1057				.accept_request(tokio::time::Instant::now().into_std(), session)
1058				.await
1059				.unwrap();
1060			assert_eq!(request.token(), Some(&setup_token()), "{name}");
1061		}
1062	}
1063
1064	#[tokio::test(start_paused = true)]
1065	async fn accept_request_without_a_token_reports_none() {
1066		let ietf = FakeSession::new(ALPN_19, [ietf_setup(ietf::Version::Draft19, None)]);
1067		let lite = FakeSession::new(ALPN_LITE_05, [lite05_setup(None, None, None)]);
1068		for (name, session) in [("draft-19", ietf), ("lite-05", lite)] {
1069			let request = Server::new()
1070				.accept_request(tokio::time::Instant::now().into_std(), session)
1071				.await
1072				.unwrap();
1073			assert_eq!(request.token(), None, "{name}");
1074		}
1075	}
1076
1077	/// A SETUP the server refuses closes the session with the code naming why, on both
1078	/// the draft-17+ uni stream and the draft 14-16 bidi stream.
1079	#[tokio::test(start_paused = true)]
1080	async fn a_refused_setup_token_closes_with_its_code() {
1081		let delete = [0x0, 0x7]; // DELETE alias 7
1082		let truncated = [0x3]; // USE_VALUE with no Token Type
1083		for (raw, code) in [
1084			(&delete[..], SessionError::ProtocolViolation),
1085			(&truncated[..], SessionError::KeyValueFormatting),
1086		] {
1087			let mut params = ietf::Parameters::default();
1088			params.set_bytes(ietf::ParameterBytes::AuthorizationToken, raw.to_vec());
1089
1090			let modern = FakeSession::new(ALPN_19, [ietf_setup_with(ietf::Version::Draft19, params.clone())]);
1091			let legacy = FakeSession::new(ALPN_16, []).with_bi(legacy_setup(ietf::Version::Draft16, params));
1092			for (name, session) in [("draft-19", modern), ("draft-16", legacy)] {
1093				let result = Server::new()
1094					.accept_request(tokio::time::Instant::now().into_std(), session.clone())
1095					.await;
1096				assert!(result.is_err(), "{name}");
1097				assert_eq!(session.closed(), Some(code.to_code()), "{name} {code}");
1098			}
1099		}
1100	}
1101
1102	#[tokio::test(start_paused = true)]
1103	async fn accept_request_reads_ietf_path() {
1104		// Every draft-17+ version gates on the SETUP stream before starting, so the
1105		// path is known at authorization time just like lite-05.
1106		for (alpn, version) in [
1107			(ALPN_17, ietf::Version::Draft17),
1108			(ALPN_18, ietf::Version::Draft18),
1109			(ALPN_19, ietf::Version::Draft19),
1110		] {
1111			let session = FakeSession::new(alpn, [ietf_setup(version, Some("/team/room"))]);
1112			let request = Server::new()
1113				.accept_request(tokio::time::Instant::now().into_std(), session)
1114				.await
1115				.unwrap();
1116			assert_eq!(request.path(), "/team/room", "{alpn}");
1117		}
1118	}
1119
1120	#[tokio::test(start_paused = true)]
1121	async fn accept_request_ietf_without_path_is_empty() {
1122		let session = FakeSession::new(ALPN_19, [ietf_setup(ietf::Version::Draft19, None)]);
1123		let request = Server::new()
1124			.accept_request(tokio::time::Instant::now().into_std(), session)
1125			.await
1126			.unwrap();
1127		assert_eq!(request.path(), "");
1128	}
1129
1130	#[tokio::test(start_paused = true)]
1131	async fn accept_request_ietf_empty_path_is_accepted() {
1132		let session = FakeSession::new(ALPN_19, [ietf_setup(ietf::Version::Draft19, Some(""))]);
1133		let request = Server::new()
1134			.accept_request(tokio::time::Instant::now().into_std(), session)
1135			.await
1136			.unwrap();
1137		assert_eq!(request.path(), "");
1138	}
1139
1140	/// Encode a lite-05 GROUP uni stream header (just the `DataType::Group` tag).
1141	fn lite05_group() -> Vec<u8> {
1142		let mut buf = Vec::new();
1143		lite::DataType::Group.encode(&mut buf, lite::Version::Lite05).unwrap();
1144		buf
1145	}
1146
1147	#[tokio::test(start_paused = true)]
1148	async fn accept_request_reads_lite05_path() {
1149		let session = FakeSession::new(ALPN_LITE_05, [lite05_setup(Some("/team/room"), None, None)]);
1150		let request = Server::new()
1151			.accept_request(tokio::time::Instant::now().into_std(), session)
1152			.await
1153			.unwrap();
1154		assert_eq!(request.path(), "/team/room");
1155		assert_eq!(request.role(), None, "a client that omits the role is bidirectional");
1156	}
1157
1158	#[tokio::test(start_paused = true)]
1159	async fn accept_request_lite05_without_path_is_empty() {
1160		let session = FakeSession::new(ALPN_LITE_05, [lite05_setup(None, None, None)]);
1161		let request = Server::new()
1162			.accept_request(tokio::time::Instant::now().into_std(), session)
1163			.await
1164			.unwrap();
1165		assert_eq!(request.path(), "");
1166	}
1167
1168	#[tokio::test(start_paused = true)]
1169	async fn accept_request_lite05_empty_path_is_accepted() {
1170		// An empty path is valid on the wire and means the same as omitting it, so a
1171		// client that wants the root doesn't have to special-case the parameter.
1172		let session = FakeSession::new(ALPN_LITE_05, [lite05_setup(Some(""), None, None)]);
1173		let request = Server::new()
1174			.accept_request(tokio::time::Instant::now().into_std(), session)
1175			.await
1176			.unwrap();
1177		assert_eq!(request.path(), "");
1178	}
1179
1180	#[tokio::test(start_paused = true)]
1181	async fn accept_request_reads_lite05_role() {
1182		let session = FakeSession::new(
1183			ALPN_LITE_05,
1184			[lite05_setup(Some("/team/room"), Some(Role::Publisher), None)],
1185		);
1186		let request = Server::new()
1187			.accept_request(tokio::time::Instant::now().into_std(), session)
1188			.await
1189			.unwrap();
1190		assert_eq!(request.role(), Some(Role::Publisher));
1191	}
1192
1193	#[tokio::test(start_paused = true)]
1194	async fn accept_request_skips_uni_stream_before_setup() {
1195		// A GROUP racing ahead of the SETUP is STOP_SENDING-ed and skipped; the gate
1196		// keeps reading until it finds the SETUP.
1197		let session = FakeSession::new(
1198			ALPN_LITE_05,
1199			[lite05_group(), lite05_setup(Some("/team/room"), None, None)],
1200		);
1201		let request = Server::new()
1202			.accept_request(tokio::time::Instant::now().into_std(), session)
1203			.await
1204			.unwrap();
1205		assert_eq!(request.path(), "/team/room");
1206	}
1207
1208	#[tokio::test(start_paused = true)]
1209	async fn accept_request_reads_lite05_peer_hop() {
1210		let hop = Hop::new(42).unwrap();
1211		let session = FakeSession::new(ALPN_LITE_05, [lite05_setup(None, None, Some(hop))]);
1212		let request = Server::new()
1213			.accept_request(tokio::time::Instant::now().into_std(), session)
1214			.await
1215			.unwrap();
1216		assert_eq!(request.peer_hop(), Some(hop));
1217	}
1218
1219	#[tokio::test(start_paused = true)]
1220	async fn anonymous_peer_hop_filters_routes_from_server_session() {
1221		let other = Hop::new(778).unwrap();
1222		let origin = crate::origin::Config::new(Hop::new(1).unwrap()).produce();
1223
1224		let gate = kio::Producer::new(true);
1225		let transport = crate::lite::test_transport::SinkSession::gated_bi(gate.consume());
1226		let log = transport.log.clone();
1227		let version = ietf::Version::Draft18;
1228		let request = Handshake {
1229			path: None,
1230			role: None,
1231			origin: None,
1232			token: None,
1233			assigned_hop: Hop::random(),
1234			inner: Some(RequestInner {
1235				server: Server::new().with_publisher(&origin),
1236				runtime: Clock::new(tokio::time::Instant::now().into_std()),
1237				handshake: PausedHandshake::Boxed(Box::new(PausedIetfModern {
1238					session: transport,
1239					version,
1240					peer_setup: ietf::PeerSetup {
1241						stream: crate::coding::Reader::new(
1242							crate::lite::test_transport::PendingRecv,
1243							Version::Ietf(version),
1244						),
1245						path: None,
1246						token: None,
1247						declared: ietf::peer::Peer::default(),
1248						early: Vec::new(),
1249					},
1250				})),
1251			}),
1252		};
1253		let assigned = request.assigned_hop;
1254
1255		let mut echoed_hops = crate::Hops::new();
1256		echoed_hops.push(crate::Hop::UNKNOWN).unwrap();
1257		let _echoed = origin
1258			.announce(
1259				"echoed-route",
1260				crate::origin::Route::default()
1261					.with_hops(echoed_hops)
1262					.with_via(assigned),
1263			)
1264			.unwrap();
1265
1266		let mut local_hops = crate::Hops::new();
1267		local_hops.push(other).unwrap();
1268		let _local = origin
1269			.announce("local-route", crate::origin::Route::default().with_hops(local_hops))
1270			.unwrap();
1271
1272		let (session, driver) = request.ok().await.unwrap();
1273		tokio::spawn(crate::time::run(driver));
1274
1275		for _ in 0..100 {
1276			if occurrences(&log, b"local-route") > 0 {
1277				break;
1278			}
1279			tokio::time::sleep(std::time::Duration::from_millis(1)).await;
1280		}
1281
1282		assert_eq!(occurrences(&log, b"echoed-route"), 0);
1283		assert_eq!(occurrences(&log, b"local-route"), 1);
1284		drop(session);
1285	}
1286}