Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ url = "2"
usage = { package = "usage-rs", version = "6.11.1" }
uuid = { version = "1.26", features = ["v7"] }
web-async = { version = "0.1.5", features = ["tracing"] }
web-transport-iroh = "0.8"
web-transport-iroh = "0.8.1"
# default-features off so the QUIC crypto provider is chosen by moq-tokio's
# aws-lc-rs / ring features.
# The WebTransport adapter released with moq-noq, from the same repository.
Expand Down
1 change: 0 additions & 1 deletion quest/m1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ QUIC studies there on that rule.
- [Merge queue](/quest/m1/merge-queue.md) - the required checks run on `merge_group`, so a stale green check can no longer break main
- [Binding audio delay](/quest/m1/binding-surface.md) - moq-ffi and every wrapper configure and observe audio playout delay
- [FFI shape](/quest/m1/ffi-shape/README.md) - the bindings mirror Rust's layers: net at the root, then media, json, flate, audio, and video namespaces built from the handle below
- [Raw stream codes](/quest/m1/raw-stream-codes.md) - raw QUIC stream resets and stops carry the application's code, not an HTTP/3-mapped one
- [moq-net owns its transport seam](/quest/m1/transport-seam.md) - moq-net names only its own transport traits, so a web-transport-trait or qmux major bump is a patch for it
- [IETF drain before close](/quest/m1/ietf-drain-before-close.md) - moq-transport sessions deliver finished tracks before a graceful close, as moq-lite does
- [Close waits for the tail](/quest/m1/close-tail.md) - on lite-07, `close()` returns `Ok` only after each subscriber FINs its Subscribe Stream, having read the track to its end
Expand Down
39 changes: 0 additions & 39 deletions quest/m1/raw-stream-codes.md

This file was deleted.

1 change: 0 additions & 1 deletion quest/m1/transport-seam.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,3 @@ Wire: none.
## Related

- [Sans-IO moq-net](/quest/m1/rs2ts/sans-io/README.md) - builds on the same seam
- [Raw stream codes](/quest/m1/raw-stream-codes.md) - the trait bump that exposed this
107 changes: 107 additions & 0 deletions rs/moq-tokio/tests/stream_code.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//! Over raw QUIC, a stream code reaches the peer as the protocol's own value, not
//! mapped into the HTTP/3 WebTransport code space, so other raw QUIC MoQ stacks
//! read it.
//!
//! The peer here is a plain QUIC client. A moq-lite server refuses a uni stream opened
//! ahead of the SETUP with STOP_SENDING, and the client reads that code off the wire.
//! moq-transport holds such a stream instead, but its codes take the same raw session.

#![cfg(feature = "noq")]

use std::{sync::Arc, time::Duration};

use web_transport_moq::noq;

/// moq-lite sends INTERNAL_ERROR (0x0) for a refused stream.
const REFUSED: u32 = 0x0;

struct Server {
port: u16,
cert: rustls::pki_types::CertificateDer<'static>,
_dir: tempfile::TempDir,
}

async fn serve() -> Server {
use std::io::Write;

let dir = tempfile::tempdir().expect("tempdir");
let key = rcgen::KeyPair::generate().expect("key");
let cert = rcgen::CertificateParams::new(vec!["localhost".to_string()])
.expect("params")
.self_signed(&key)
.expect("cert");
let write = |name: &str, contents: String| {
let path = dir.path().join(name);
std::fs::File::create(&path)
.and_then(|mut file| file.write_all(contents.as_bytes()))
.expect("write pem file");
path
};

let mut config = moq_tokio::server::Config::default();
config.listen.bind = Some("127.0.0.1:0".parse().unwrap());
config.listen.tls.cert = vec![write("server.pem", cert.pem())];
config.listen.tls.key = vec![write("server.key", key.serialize_pem())];
let mut listener = config
.init()
.expect("failed to init server")
.listen()
.await
.expect("failed to listen");
let port = listener.local_addr().expect("no quic addr").port();

tokio::spawn(async move {
while let Some(request) = listener.accept().await {
tokio::spawn(async move {
let _session = request.ok().await;
std::future::pending::<()>().await;
});
}
});

Server {
port,
cert: cert.der().clone(),
_dir: dir,
}
}

/// Dial the server over raw QUIC with `alpn`, open a uni stream that is not a SETUP,
/// and return the code the server stopped it with.
async fn refused_code(server: &Server, alpn: &str) -> Option<noq::VarInt> {
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();

let mut roots = rustls::RootCertStore::empty();
roots.add(server.cert.clone()).expect("root");
let mut tls = rustls::ClientConfig::builder()
.with_root_certificates(roots)
.with_no_client_auth();
tls.alpn_protocols = vec![alpn.as_bytes().to_vec()];
let quic = noq::crypto::rustls::QuicClientConfig::try_from(tls).expect("quic config");

let endpoint = noq::Endpoint::client("127.0.0.1:0".parse().unwrap()).expect("endpoint");
let conn = endpoint
.connect_with(
noq::ClientConfig::new(Arc::new(quic)),
([127, 0, 0, 1], server.port).into(),
"localhost",
)
.expect("connect")
.await
.expect("handshake");

// A group stream, which moq-lite refuses before the SETUP.
let mut send = conn.open_uni().await.expect("open");
send.write_all(&[0x00]).await.expect("write");

tokio::time::timeout(Duration::from_secs(10), send.stopped())
.await
.expect("the stream was not stopped")
.expect("stopped")
}

#[tokio::test]
async fn a_refused_stream_code_is_not_mapped() {
let server = serve().await;
assert_eq!(refused_code(&server, "moq-lite-06").await, Some(REFUSED.into()));
}
47 changes: 44 additions & 3 deletions rs/moq-uring/src/quic/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
//! WebTransport layering disabled, so native peers and browsers run the same
//! machinery. Stream and session error codes map through the HTTP/3 error
//! space ([`web_transport_proto::error_to_http3`]) in web mode and pass
//! through untouched in raw mode.
//! through untouched in raw mode, except that a received stream code in the
//! HTTP/3 range is unmapped, since older raw peers send that form.

use std::cell::RefCell;
use std::collections::VecDeque;
Expand Down Expand Up @@ -811,7 +812,7 @@ impl SendStream {
fn map(&self, err: Error) -> Error {
match self.web {
true => unmap_err(err),
false => err,
false => unmap_legacy(err),
}
}
}
Expand Down Expand Up @@ -953,7 +954,7 @@ impl RecvStream {
fn map(&self, err: Error) -> Error {
match self.web {
true => unmap_err(err),
false => err,
false => unmap_legacy(err),
}
}
}
Expand Down Expand Up @@ -998,6 +999,21 @@ impl std::fmt::Debug for RecvStream {
}
}

/// Unmap a raw-QUIC stream code that arrived in the HTTP/3 code space.
///
/// Older `web-transport-moq` raw sessions map RESET_STREAM and STOP_SENDING
/// codes as if they were WebTransport. No `u32` application code reaches that
/// range, so a code in it can only be the mapped form. Anything else is the
/// peer's own code and passes through.
fn unmap_legacy(err: Error) -> Error {
let unmap = |code: u64| proto::error_from_http3(code).map_or(code, u64::from);
match err {
Error::Reset(code) => Error::Reset(unmap(code)),
Error::Stop(code) => Error::Stop(unmap(code)),
err => err,
}
}

/// Rewrite HTTP/3-mapped error codes back into WebTransport code space.
///
/// Only a sparse range of HTTP/3 codes names a WebTransport error. Anything
Expand Down Expand Up @@ -1475,6 +1491,31 @@ mod tests {
));
}

/// A raw session reads a plain stream code as is, and an older raw peer's
/// HTTP/3-mapped one as the code it stands for. Session closes are left alone.
#[test]
fn a_raw_session_unmaps_legacy_stream_codes() {
assert!(matches!(unmap_legacy(Error::Reset(5)), Error::Reset(5)));
assert!(matches!(unmap_legacy(Error::Stop(5)), Error::Stop(5)));
assert!(matches!(
unmap_legacy(Error::Reset(proto::error_to_http3(5))),
Error::Reset(5)
));
assert!(matches!(
unmap_legacy(Error::Stop(proto::error_to_http3(5))),
Error::Stop(5)
));

let code = proto::error_to_http3(5);
assert!(matches!(
unmap_legacy(Error::App {
code,
reason: String::new()
}),
Error::App { code: c, .. } if c == code
));
}

/// Wrap `payload` in an HTTP/3 frame of type `typ`.
fn frame(typ: u64, payload: &[u8]) -> Vec<u8> {
let mut buf = Vec::new();
Expand Down
Loading