Skip to main content

moq_net/lite/
setup.rs

1//! The lite-05 SETUP message: each endpoint advertises its capabilities once, as
2//! the sole message on a unidirectional Setup Stream, then closes it.
3
4use crate::coding::*;
5
6use super::{Message, Parameters, Version};
7
8/// Setup Parameter id for the Probe capability level.
9const PARAM_PROBE: u64 = 0x1;
10/// Setup Parameter id for the request Path (client-only, URI-less transports).
11const PARAM_PATH: u64 = 0x2;
12/// Setup Parameter id for the client's intended [`Role`] (client-only).
13const PARAM_ROLE: u64 = 0x3;
14/// Setup Parameter id for the link cost the dialer assigns to this connection.
15const PARAM_COST: u64 = 0x4;
16/// Setup Parameter id for the endpoint's origin (hop) id.
17const PARAM_ORIGIN: u64 = 0x5;
18
19/// The cost of crossing a link that neither end priced.
20///
21/// One, so a mesh that configures no costs accumulates a route cost equal to the
22/// hop count and ranks routes exactly as pre-lite-06 shortest-path routing did. Pricing
23/// a link at 0 makes it free (a sibling in the same datacenter); pricing it higher
24/// makes it a last resort (a metered backbone).
25pub const DEFAULT_COST: u64 = 1;
26
27/// The probe capability an endpoint advertises in SETUP.
28///
29/// Monotonic: a higher level implies every lower one. An unknown (future) value
30/// decodes as the highest level we understand, so a peer that gains a new level is
31/// treated as at least [`Increase`](Self::Increase).
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
33pub enum ProbeLevel {
34	/// No probing. Equivalent to omitting the parameter.
35	#[default]
36	None,
37	/// The publisher can measure and periodically report its estimated bitrate.
38	Report,
39	/// The publisher can additionally pad the connection (or send redundant data).
40	Increase,
41}
42
43impl ProbeLevel {
44	/// Map the wire value to a level, saturating unknown values to [`Increase`](Self::Increase).
45	fn from_code(code: u64) -> Self {
46		match code {
47			0 => Self::None,
48			1 => Self::Report,
49			_ => Self::Increase,
50		}
51	}
52
53	/// The wire value for this level.
54	fn to_code(self) -> u64 {
55		match self {
56			Self::None => 0,
57			Self::Report => 1,
58			Self::Increase => 2,
59		}
60	}
61}
62
63/// The single direction a client intends to use the session for.
64///
65/// A client advertises this in its SETUP so the server can reject a token that lacks
66/// the matching scope during the handshake, instead of accepting a connection that
67/// then silently carries no media (a subscribe-only token used to publish, or vice
68/// versa). It only ever narrows what the server grants, so it is not a security
69/// boundary: the server still enforces the token's scope regardless.
70///
71/// A session is bidirectional by default, which the wire says by omitting the
72/// parameter. `Option<Role>` mirrors that: `None` is the default, and it's also what
73/// a client that predates the parameter decodes to.
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75#[non_exhaustive]
76pub enum Role {
77	/// The client will publish tracks (ingest); the server must consume.
78	Publisher,
79	/// The client will subscribe to tracks (egress); the server must publish.
80	Subscriber,
81}
82
83impl Role {
84	/// Map the wire value to a role. `0` and any unrecognized future value are `None`
85	/// (bidirectional): the draft requires a receiver that does not recognize the value
86	/// to treat it as both directions, so a newer client can't break an older server (it
87	/// just loses the early reject and defers fully to the token's scope).
88	fn from_code(code: u64) -> Option<Self> {
89		match code {
90			1 => Some(Role::Publisher),
91			2 => Some(Role::Subscriber),
92			_ => None,
93		}
94	}
95
96	/// The wire value for this role.
97	fn to_code(self) -> u64 {
98		match self {
99			Role::Publisher => 1,
100			Role::Subscriber => 2,
101		}
102	}
103
104	/// Derive the advertised role from which origins a client wired up: publish-only is
105	/// a [`Publisher`](Role::Publisher), consume-only a [`Subscriber`](Role::Subscriber),
106	/// and both (or neither) advertises nothing. This keeps the advertised role from
107	/// drifting away from what the session actually does.
108	pub(crate) fn from_origins(publishes: bool, consumes: bool) -> Option<Self> {
109		match (publishes, consumes) {
110			(true, false) => Some(Role::Publisher),
111			(false, true) => Some(Role::Subscriber),
112			_ => None,
113		}
114	}
115
116	/// Lowercase label for this role (`"publisher"` / `"subscriber"`).
117	pub fn as_str(self) -> &'static str {
118		match self {
119			Role::Publisher => "publisher",
120			Role::Subscriber => "subscriber",
121		}
122	}
123}
124
125impl std::fmt::Display for Role {
126	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127		f.write_str(self.as_str())
128	}
129}
130
131/// The SETUP message, sent once per endpoint on the unidirectional Setup Stream.
132///
133/// lite-05+ only. The two endpoints' SETUP messages are independent: neither side
134/// blocks on the peer's before opening other streams, but a stream whose encoding
135/// depends on a negotiated capability (e.g. PROBE) must wait for it.
136#[derive(Debug, Clone, Default, PartialEq, Eq)]
137pub struct Setup {
138	/// The probe capability this endpoint supports. [`ProbeLevel::None`] when absent.
139	pub probe: ProbeLevel,
140	/// The request path, for transports that carry no request URI (native QUIC,
141	/// qmux over TCP/TLS, unix sockets), with `?` and the URI query appended when
142	/// there is one. Sent only by the client; a server never sends one and a relay
143	/// never forwards it. `None` on URI-carrying bindings, where it would be a
144	/// protocol violation. An empty path means the same thing as `None`; both are
145	/// on the wire so a client need not special-case the root.
146	pub path: Option<String>,
147	/// The single direction the client intends to use, or `None` for a bidirectional
148	/// session. `None` is sent as the absence of the parameter, which is also how a
149	/// client that predates the parameter decodes.
150	pub role: Option<Role>,
151	/// What crossing this link costs (lite-06+), added to the route cost of every
152	/// announcement forwarded over it. Sent only by the dialing side, since the link
153	/// cost lives in its connect config; the accepting side reads it here so both
154	/// ends price the same link identically. `None` means the default cost of 1.
155	pub cost: Option<u64>,
156	/// This endpoint's origin (hop) id, the identity it stamps onto forwarded
157	/// announcements. The peer uses it to serve this endpoint's subscriptions from
158	/// a route that does not flow through it (the same split horizon the announce
159	/// filter applies). `None` when the endpoint has no meaningful identity (a
160	/// leaf that never forwards); a wire value of 0 decodes as `None`.
161	pub origin: Option<crate::Origin>,
162}
163
164impl Message for Setup {
165	fn decode_msg<R: bytes::Buf>(r: &mut R, version: Version) -> Result<Self, DecodeError> {
166		if !version.has_setup_stream() {
167			return Err(DecodeError::Version);
168		}
169
170		let params = Parameters::decode(r, version)?;
171		let probe = params
172			.get_varint(PARAM_PROBE)?
173			.map(ProbeLevel::from_code)
174			.unwrap_or_default();
175		let path = match params.get_bytes(PARAM_PATH) {
176			Some(bytes) => Some(
177				std::str::from_utf8(bytes)
178					.map_err(|_| DecodeError::InvalidValue)?
179					.to_string(),
180			),
181			None => None,
182		};
183		let role = params.get_varint(PARAM_ROLE)?.and_then(Role::from_code);
184		let cost = params.get_varint(PARAM_COST)?;
185		// 0 is legal on the wire but carries no identity (it can't be excluded),
186		// so it decodes as "not declared" rather than an error.
187		let origin = params
188			.get_varint(PARAM_ORIGIN)?
189			.and_then(|id| crate::Origin::new(id).ok());
190
191		Ok(Self {
192			probe,
193			path,
194			role,
195			cost,
196			origin,
197		})
198	}
199
200	fn encode_msg<W: bytes::BufMut>(&self, w: &mut W, version: Version) -> Result<(), EncodeError> {
201		if !version.has_setup_stream() {
202			return Err(EncodeError::Version);
203		}
204
205		let mut params = Parameters::default();
206		// None is the wire default, so omit it to keep the message empty when nothing is set.
207		if self.probe != ProbeLevel::None {
208			params.set_varint(PARAM_PROBE, self.probe.to_code());
209		}
210		if let Some(path) = &self.path {
211			params.set_bytes(PARAM_PATH, path.as_bytes().to_vec());
212		}
213		// Bidirectional is the wire default (absence of the parameter), so only a
214		// directional role is encoded.
215		if let Some(role) = self.role {
216			params.set_varint(PARAM_ROLE, role.to_code());
217		}
218		if let Some(cost) = self.cost {
219			params.set_varint(PARAM_COST, cost);
220		}
221		if let Some(origin) = self.origin {
222			params.set_varint(PARAM_ORIGIN, origin.id());
223		}
224
225		params.encode(w, version)
226	}
227}
228
229/// Shared slot for the peer's SETUP, written once when its Setup stream is read.
230///
231/// Streams whose encoding depends on a negotiated capability (e.g. the PROBE
232/// stream) wait on this before deciding what to do. Cheap to clone: every handle
233/// shares the same slot.
234#[derive(Clone, Default)]
235pub(crate) struct PeerSetup(kio::Shared<Option<Setup>>);
236
237impl PeerSetup {
238	/// Record the peer's SETUP.
239	pub fn set(&self, setup: Setup) {
240		*self.0.lock() = Some(setup);
241	}
242
243	/// Await the peer's advertised probe level, blocking until its SETUP arrives.
244	pub async fn probe_level(&self) -> ProbeLevel {
245		self.wait(|setup| setup.probe).await
246	}
247
248	/// Await the link cost the peer (the dialing side) declared in its SETUP.
249	/// `None` when it declared none, meaning the default cost of 1.
250	pub async fn cost(&self) -> Option<u64> {
251		self.wait(|setup| setup.cost).await
252	}
253
254	/// Await the origin (hop) id the peer declared in its SETUP. `None` when it
255	/// declared none: a leaf with no identity worth excluding.
256	pub async fn origin(&self) -> Option<crate::Origin> {
257		self.wait(|setup| setup.origin).await
258	}
259
260	/// Await the peer's SETUP and read a field out of it.
261	///
262	/// The peer MUST send exactly one SETUP, so this resolves once that stream is read.
263	/// Waits forever if it never does; the caller is a session task, cancelled when the
264	/// driver drops.
265	async fn wait<T>(&self, f: impl FnOnce(&Setup) -> T) -> T {
266		let slot = self
267			.0
268			.wait(|setup| {
269				if setup.is_some() {
270					std::task::Poll::Ready(())
271				} else {
272					std::task::Poll::Pending
273				}
274			})
275			.await;
276		f(slot.as_ref().expect("waited for Some"))
277	}
278}
279
280#[cfg(test)]
281mod tests {
282	use super::*;
283
284	fn round_trip(msg: &Setup) -> Setup {
285		let mut buf = bytes::BytesMut::new();
286		msg.encode(&mut buf, Version::Lite05).unwrap();
287		let mut slice = &buf[..];
288		let got = Setup::decode(&mut slice, Version::Lite05).unwrap();
289		assert!(bytes::Buf::remaining(&slice) == 0, "trailing bytes after decode");
290		got
291	}
292
293	#[test]
294	fn empty_round_trip() {
295		let msg = Setup::default();
296		assert_eq!(round_trip(&msg), msg);
297	}
298
299	#[test]
300	fn probe_levels_round_trip() {
301		for probe in [ProbeLevel::None, ProbeLevel::Report, ProbeLevel::Increase] {
302			let msg = Setup {
303				probe,
304				..Default::default()
305			};
306			assert_eq!(round_trip(&msg), msg);
307		}
308	}
309
310	#[test]
311	fn cost_round_trip() {
312		// Zero is a meaningful price (a free same-datacenter link), so it must survive
313		// the round trip as `Some(0)` rather than collapsing into "unpriced".
314		for cost in [None, Some(0), Some(1), Some(7)] {
315			let msg = Setup {
316				cost,
317				..Default::default()
318			};
319			assert_eq!(round_trip(&msg), msg);
320		}
321	}
322
323	#[test]
324	fn path_round_trip() {
325		let msg = Setup {
326			probe: ProbeLevel::Report,
327			path: Some("/room/123".to_string()),
328			..Default::default()
329		};
330		assert_eq!(round_trip(&msg), msg);
331	}
332
333	#[test]
334	fn origin_round_trip() {
335		let msg = Setup {
336			origin: Some(crate::Origin::new(42).unwrap()),
337			..Default::default()
338		};
339		assert_eq!(round_trip(&msg), msg);
340	}
341
342	// A declared id of 0 carries no identity (it cannot be excluded), so it
343	// decodes as absent rather than erroring.
344	#[test]
345	fn origin_zero_decodes_as_none() {
346		use crate::coding::Encode;
347
348		let version = Version::Lite05;
349		let mut params = Parameters::default();
350		params.set_varint(super::PARAM_ORIGIN, 0);
351		let mut body = bytes::BytesMut::new();
352		params.encode(&mut body, version).unwrap();
353		// Frame the body with the Message Length prefix `Setup::decode` expects.
354		let mut buf = bytes::BytesMut::new();
355		(body.len() as u64).encode(&mut buf, version).unwrap();
356		buf.extend_from_slice(&body);
357		let mut slice = &buf[..];
358		let got = Setup::decode(&mut slice, version).unwrap();
359		assert_eq!(got.origin, None);
360	}
361
362	#[test]
363	fn empty_path_round_trips() {
364		// An empty path is valid and distinct from absent only on the wire; both mean
365		// the root, so a client doesn't have to special-case it.
366		let msg = Setup {
367			path: Some(String::new()),
368			..Default::default()
369		};
370		assert_eq!(round_trip(&msg), msg);
371	}
372
373	#[test]
374	fn roles_round_trip() {
375		for role in [Some(Role::Publisher), Some(Role::Subscriber), None] {
376			let msg = Setup {
377				path: Some("/room/123".to_string()),
378				role,
379				..Default::default()
380			};
381			assert_eq!(round_trip(&msg), msg);
382		}
383	}
384
385	#[test]
386	fn unknown_probe_level_saturates_to_increase() {
387		// Frame a SETUP message carrying an unknown probe level (99) by hand: the
388		// parameters body, prefixed with its length (the lite Message size prefix).
389		let mut params = Parameters::default();
390		params.set_varint(PARAM_PROBE, 99);
391		let mut body = Vec::new();
392		params.encode(&mut body, Version::Lite05).unwrap();
393
394		let mut buf = bytes::BytesMut::new();
395		body.len().encode(&mut buf, Version::Lite05).unwrap();
396		buf.extend_from_slice(&body);
397
398		let mut slice = &buf[..];
399		let got = Setup::decode(&mut slice, Version::Lite05).unwrap();
400		assert_eq!(got.probe, ProbeLevel::Increase);
401	}
402
403	#[test]
404	fn role_wire_codes() {
405		// The draft pins Publisher=1 / Subscriber=2. A swap here would still round-trip
406		// against our own decoder, but break every other implementation.
407		for (role, code) in [(Role::Publisher, 1u64), (Role::Subscriber, 2)] {
408			assert_eq!(role.to_code(), code);
409			assert_eq!(Role::from_code(code), Some(role));
410		}
411	}
412
413	#[test]
414	fn unknown_role_decodes_as_bidirectional() {
415		// A role value the receiver doesn't recognize (a future extension, or an explicit
416		// 0) decodes to `None` rather than failing, so a newer client can't break an older
417		// server. The draft mandates this fallback.
418		for code in [0u64, 9, 250] {
419			let mut params = Parameters::default();
420			params.set_varint(PARAM_ROLE, code);
421			let mut body = Vec::new();
422			params.encode(&mut body, Version::Lite05).unwrap();
423
424			let mut buf = bytes::BytesMut::new();
425			body.len().encode(&mut buf, Version::Lite05).unwrap();
426			buf.extend_from_slice(&body);
427
428			let mut slice = &buf[..];
429			let got = Setup::decode(&mut slice, Version::Lite05).unwrap();
430			assert_eq!(got.role, None, "role code {code} should decode as bidirectional");
431		}
432	}
433
434	#[test]
435	fn rejects_before_lite05() {
436		let msg = Setup::default();
437		let mut buf = bytes::BytesMut::new();
438		assert!(matches!(
439			msg.encode(&mut buf, Version::Lite04),
440			Err(EncodeError::Version)
441		));
442	}
443
444	#[test]
445	fn ignores_unknown_parameters() {
446		// Frame a SETUP carrying an unknown parameter ID alongside the path.
447		let mut params = Parameters::default();
448		params.set_bytes(PARAM_PATH, b"/foo".to_vec());
449		params.set_bytes(0xbeef, b"whatever".to_vec());
450
451		let mut body = Vec::new();
452		params.encode(&mut body, Version::Lite05).unwrap();
453
454		// Wrap with the message size prefix the Message impl expects.
455		let mut buf = bytes::BytesMut::new();
456		body.len().encode(&mut buf, Version::Lite05).unwrap();
457		buf.extend_from_slice(&body);
458
459		let mut slice = &buf[..];
460		let got = Setup::decode(&mut slice, Version::Lite05).unwrap();
461		assert_eq!(got.path.as_deref(), Some("/foo"));
462	}
463}