From d202b31c57ffc5ae646e9e35b0522052cb55f6ab Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 26 May 2026 12:32:57 -0700 Subject: [PATCH 1/2] moq-net: map MoQ versions to required qmux versions The MoQ WG decided qmux's draft version is tied to the moq-transport version. moq-transport-18 requires qmux-01; moq-transport-14..17 stay on qmux-00. moq-lite is unconstrained: existing Lite01..Lite04 advertise both for back-compat, and future moq-lite versions should pin to a single qmux version like moq-transport does. Add a local QmuxVersion enum, Version::qmux_versions (the spec mapping), Version::accepts_qmux (server-side validation), and Versions::qmux_alpns (ordered, dedup'd (qmux_version, app_alpn) pairs for building the Sec-WebSocket-Protocol list). Lite arms are listed by variant rather than wildcard so adding a new Lite variant fails to compile until someone makes a deliberate choice. Wiring through moq-native (and the qmux 0.0.8 bump) follows in a separate change once upstream PR moq-dev/web-transport#226 lands and exposes an API for explicit (version, protocol) pairs instead of the default cross-product. Co-Authored-By: Claude Opus 4.7 (1M context) --- rs/moq-net/src/version.rs | 173 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/rs/moq-net/src/version.rs b/rs/moq-net/src/version.rs index 7c4872c1f..d37c9aa41 100644 --- a/rs/moq-net/src/version.rs +++ b/rs/moq-net/src/version.rs @@ -36,6 +36,37 @@ pub(crate) const ALPN_16: &str = "moqt-16"; pub(crate) const ALPN_17: &str = "moqt-17"; pub(crate) const ALPN_18: &str = "moqt-18"; +/// The qmux draft version used to carry a MoQ ALPN over WebSocket / TLS. +/// +/// The MoQ WG decided that qmux's version is tied to the moq-transport draft +/// (moq-transport-18 requires qmux-01; moq-transport-14..17 use qmux-00). +/// moq-lite is unconstrained and may ride on either. +/// +/// Mirrors `qmux::Version` but kept local so `moq-net` stays independent of +/// the `qmux` crate; the `moq-native` layer converts at the boundary. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum QmuxVersion { + QMux00, + QMux01, +} + +impl QmuxVersion { + /// The bare ALPN string for this qmux version. + pub fn alpn(&self) -> &'static str { + match self { + Self::QMux00 => "qmux-00", + Self::QMux01 => "qmux-01", + } + } +} + +impl fmt::Display for QmuxVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.alpn()) + } +} + /// A MoQ protocol version. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] @@ -117,6 +148,31 @@ impl Version { ) } + /// The qmux versions this MoQ version may ride on, in preference order. + /// + /// moq-transport-18 requires qmux-01; moq-transport-14..17 require qmux-00. + /// Existing moq-lite versions (Lite01..Lite04) advertise both for back-compat. + /// Future moq-lite versions should pin to a single qmux version, like moq-transport. + pub fn qmux_versions(&self) -> &'static [QmuxVersion] { + use ietf::Version as I; + use lite::Version as L; + match self { + Self::Ietf(I::Draft18) => &[QmuxVersion::QMux01], + Self::Ietf(I::Draft14 | I::Draft15 | I::Draft16 | I::Draft17) => &[QmuxVersion::QMux00], + Self::Lite(L::Lite01 | L::Lite02 | L::Lite03 | L::Lite04) => { + &[QmuxVersion::QMux01, QmuxVersion::QMux00] + } + } + } + + /// Whether this MoQ version is permitted to ride on the given qmux version. + /// + /// Use server-side after the qmux/app pair has been negotiated to reject + /// pairings the moq-transport spec forbids (e.g. `qmux-00.moqt-18`). + pub fn accepts_qmux(&self, qv: QmuxVersion) -> bool { + self.qmux_versions().contains(&qv) + } + /// Whether this is a lite protocol version. pub fn is_lite(&self) -> bool { match self { @@ -242,6 +298,26 @@ impl Versions { alpns } + /// Compute the `(qmux_version, app_alpn)` pairs to advertise over WebSocket / TLS, + /// in preference order, dedup'd. + /// + /// Each MoQ version is paired only with the qmux versions it's permitted to ride on + /// (see [`Version::qmux_versions`]). Use this to build the `Sec-WebSocket-Protocol` + /// list (or TLS ALPN list) when fronting a qmux session. + pub fn qmux_alpns(&self) -> Vec<(QmuxVersion, &'static str)> { + let mut pairs = Vec::new(); + for v in &self.0 { + let alpn = v.alpn(); + for &qv in v.qmux_versions() { + let pair = (qv, alpn); + if !pairs.contains(&pair) { + pairs.push(pair); + } + } + } + pairs + } + /// Return only versions present in both self and other, or `None` if the intersection is empty. pub fn filter(&self, other: &Versions) -> Option { let filtered: Vec = self.0.iter().filter(|v| other.0.contains(v)).copied().collect(); @@ -296,3 +372,100 @@ impl From for coding::Versions { coding::Versions::from(inner) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn qmux_versions_for_each_moq_version() { + assert_eq!( + Version::Ietf(ietf::Version::Draft18).qmux_versions(), + &[QmuxVersion::QMux01] + ); + for v in [ + ietf::Version::Draft14, + ietf::Version::Draft15, + ietf::Version::Draft16, + ietf::Version::Draft17, + ] { + assert_eq!(Version::Ietf(v).qmux_versions(), &[QmuxVersion::QMux00], "{v}"); + } + for v in [ + lite::Version::Lite01, + lite::Version::Lite02, + lite::Version::Lite03, + lite::Version::Lite04, + ] { + assert_eq!( + Version::Lite(v).qmux_versions(), + &[QmuxVersion::QMux01, QmuxVersion::QMux00], + "{v}" + ); + } + } + + #[test] + fn accepts_qmux_is_consistent() { + assert!(Version::Ietf(ietf::Version::Draft18).accepts_qmux(QmuxVersion::QMux01)); + assert!(!Version::Ietf(ietf::Version::Draft18).accepts_qmux(QmuxVersion::QMux00)); + assert!(Version::Ietf(ietf::Version::Draft17).accepts_qmux(QmuxVersion::QMux00)); + assert!(!Version::Ietf(ietf::Version::Draft17).accepts_qmux(QmuxVersion::QMux01)); + assert!(Version::Lite(lite::Version::Lite04).accepts_qmux(QmuxVersion::QMux01)); + assert!(Version::Lite(lite::Version::Lite04).accepts_qmux(QmuxVersion::QMux00)); + } + + #[test] + fn qmux_alpns_all_matches_table() { + let pairs = Versions::all().qmux_alpns(); + assert_eq!( + pairs, + vec![ + (QmuxVersion::QMux01, "moq-lite-04"), + (QmuxVersion::QMux00, "moq-lite-04"), + (QmuxVersion::QMux01, "moq-lite-03"), + (QmuxVersion::QMux00, "moq-lite-03"), + (QmuxVersion::QMux01, "moql"), + (QmuxVersion::QMux00, "moql"), + (QmuxVersion::QMux01, "moqt-18"), + (QmuxVersion::QMux00, "moqt-17"), + (QmuxVersion::QMux00, "moqt-16"), + (QmuxVersion::QMux00, "moqt-15"), + (QmuxVersion::QMux00, "moq-00"), + ] + ); + } + + #[test] + fn qmux_alpns_singleton_moqt_18() { + assert_eq!( + Versions::from(Version::Ietf(ietf::Version::Draft18)).qmux_alpns(), + vec![(QmuxVersion::QMux01, "moqt-18")] + ); + } + + #[test] + fn qmux_alpns_singleton_moqt_17() { + assert_eq!( + Versions::from(Version::Ietf(ietf::Version::Draft17)).qmux_alpns(), + vec![(QmuxVersion::QMux00, "moqt-17")] + ); + } + + #[test] + fn qmux_alpns_singleton_lite_offers_both() { + assert_eq!( + Versions::from(Version::Lite(lite::Version::Lite04)).qmux_alpns(), + vec![ + (QmuxVersion::QMux01, "moq-lite-04"), + (QmuxVersion::QMux00, "moq-lite-04"), + ] + ); + } + + #[test] + fn qmux_version_alpn_strings() { + assert_eq!(QmuxVersion::QMux00.alpn(), "qmux-00"); + assert_eq!(QmuxVersion::QMux01.alpn(), "qmux-01"); + } +} From 8999f9646192ae34c4b55e900fe4d1b1c493ad13 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 26 May 2026 12:42:24 -0700 Subject: [PATCH 2/2] moq-net: fix rustfmt for Lite qmux match arm CI's rustfmt collapses single-expr match arms; local fmt didn't flag it. Co-Authored-By: Claude Opus 4.7 (1M context) --- rs/moq-net/src/version.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/rs/moq-net/src/version.rs b/rs/moq-net/src/version.rs index d37c9aa41..9fa7f6cff 100644 --- a/rs/moq-net/src/version.rs +++ b/rs/moq-net/src/version.rs @@ -159,9 +159,7 @@ impl Version { match self { Self::Ietf(I::Draft18) => &[QmuxVersion::QMux01], Self::Ietf(I::Draft14 | I::Draft15 | I::Draft16 | I::Draft17) => &[QmuxVersion::QMux00], - Self::Lite(L::Lite01 | L::Lite02 | L::Lite03 | L::Lite04) => { - &[QmuxVersion::QMux01, QmuxVersion::QMux00] - } + Self::Lite(L::Lite01 | L::Lite02 | L::Lite03 | L::Lite04) => &[QmuxVersion::QMux01, QmuxVersion::QMux00], } }