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