Skip to content
Open
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
29 changes: 15 additions & 14 deletions rs/web-transport-noq/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ pub struct Session {
// Uses OnceLock for set-once, first-writer-wins semantics with lock-free reads.
error: Arc<OnceLock<SessionError>>,

// The request sent by the client.
request: ConnectRequest,
// The request sent by the client, or None for a raw QUIC session.
request: Option<ConnectRequest>,

// The response sent by the server.
response: ConnectResponse,
Expand Down Expand Up @@ -93,7 +93,7 @@ impl Session {
settings: Some(Arc::new(settings)),
connect_send: Arc::new(Mutex::new(Some(connect.send))),
error: error.clone(),
request: connect.request.clone(),
request: Some(connect.request.clone()),
response: connect.response.clone(),
};

Expand Down Expand Up @@ -479,15 +479,14 @@ impl Session {
}
}

/// Create a new session from a raw QUIC connection and a URL.
/// Create a new session from a raw QUIC connection.
///
/// This is used to pretend like a QUIC connection is a WebTransport session.
/// It's a hack, but it makes it much easier to support WebTransport and raw QUIC simultaneously.
pub fn raw(
conn: noq::Connection,
request: impl Into<ConnectRequest>,
response: impl Into<ConnectResponse>,
) -> Self {
/// This is used to pretend like a QUIC connection is a WebTransport session,
/// making it easier to support WebTransport and raw QUIC simultaneously.
///
/// There is no CONNECT request, so [`Self::request`] returns `None`. The response
/// is supplied by the caller to carry the negotiated ALPN via [`Self::protocol`].
pub fn raw(conn: noq::Connection, response: impl Into<ConnectResponse>) -> Self {
Self {
conn,
session_id: None,
Expand All @@ -498,13 +497,15 @@ impl Session {
settings: None,
connect_send: Arc::new(Mutex::new(None)),
error: Arc::new(OnceLock::new()),
request: request.into(),
request: None,
response: response.into(),
}
}

pub fn request(&self) -> &ConnectRequest {
&self.request
/// Returns the [`ConnectRequest`] if this session was established over HTTP/3,
/// or `None` for a raw QUIC session.
pub fn request(&self) -> Option<&ConnectRequest> {
self.request.as_ref()
}

pub fn response(&self) -> &ConnectResponse {
Expand Down
32 changes: 17 additions & 15 deletions rs/web-transport-quiche/src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ pub struct Connection {
settings: Option<Arc<h3::Settings>>,

// The request and response that were sent and received.
request: ConnectRequest,
// The request is None for a raw QUIC session.
request: Option<ConnectRequest>,
response: ConnectResponse,
}

Expand Down Expand Up @@ -99,16 +100,16 @@ impl Connection {
header_uni,
header_bi,
header_datagram,
request: connect.request.clone(),
request: Some(connect.request.clone()),
response: connect.response.clone(),
settings: Some(Arc::new(settings)),
};

tracing::debug!(url = %connect.request.url, "WebTransport connection established");

// Run a background task to check if the connect stream is closed.
tokio::spawn(this.clone().run_closed(connect));

tracing::debug!(url = %this.request().url, "WebTransport connection established");

this
}

Expand Down Expand Up @@ -304,15 +305,14 @@ impl Connection {
self.conn.closed().await.into()
}

/// Create a new session from a raw QUIC connection and a URL.
/// Create a new session from a raw QUIC connection.
///
/// This is used to pretend like a QUIC connection is a WebTransport session.
/// It's a hack, but it makes it much easier to support WebTransport and raw QUIC simultaneously.
pub fn raw(
conn: ez::Connection,
request: impl Into<ConnectRequest>,
response: impl Into<ConnectResponse>,
) -> Self {
/// This is used to pretend like a QUIC connection is a WebTransport session,
/// making it easier to support WebTransport and raw QUIC simultaneously.
///
/// There is no CONNECT request, so [`Self::request`] returns `None`. The response
/// is supplied by the caller to carry the negotiated ALPN via [`Self::protocol`].
pub fn raw(conn: ez::Connection, response: impl Into<ConnectResponse>) -> Self {
let drop = Arc::new(ConnectionDrop { conn: conn.clone() });
Self {
conn,
Expand All @@ -323,13 +323,15 @@ impl Connection {
header_datagram: Default::default(),
accept: None,
settings: None,
request: request.into(),
request: None,
response: response.into(),
}
}

pub fn request(&self) -> &ConnectRequest {
&self.request
/// Returns the [`ConnectRequest`] if this session was established over HTTP/3,
/// or `None` for a raw QUIC session.
pub fn request(&self) -> Option<&ConnectRequest> {
self.request.as_ref()
}

pub fn response(&self) -> &ConnectResponse {
Expand Down
1 change: 1 addition & 0 deletions rs/web-transport-quinn/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ web-transport-trait = { workspace = true }
[dev-dependencies]
anyhow = "1"
clap = { version = "4", features = ["derive"] }
rcgen = "0.14"
rustls-pemfile = "2"
tokio = { version = "1", features = ["full"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
29 changes: 15 additions & 14 deletions rs/web-transport-quinn/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ pub struct Session {
// Uses OnceLock for set-once, first-writer-wins semantics with lock-free reads.
error: Arc<OnceLock<SessionError>>,

// The request sent by the client.
request: ConnectRequest,
// The request sent by the client, or None for a raw QUIC session.
request: Option<ConnectRequest>,

// The response sent by the server.
response: ConnectResponse,
Expand Down Expand Up @@ -93,7 +93,7 @@ impl Session {
settings: Some(Arc::new(settings)),
connect_send: Arc::new(Mutex::new(Some(connect.send))),
error: error.clone(),
request: connect.request.clone(),
request: Some(connect.request.clone()),
response: connect.response.clone(),
};

Expand Down Expand Up @@ -488,15 +488,14 @@ impl Session {
}
}

/// Create a new session from a raw QUIC connection and a URL.
/// Create a new session from a raw QUIC connection.
///
/// This is used to pretend like a QUIC connection is a WebTransport session.
/// It's a hack, but it makes it much easier to support WebTransport and raw QUIC simultaneously.
pub fn raw(
conn: quinn::Connection,
request: impl Into<ConnectRequest>,
response: impl Into<ConnectResponse>,
) -> Self {
/// This is used to pretend like a QUIC connection is a WebTransport session,
/// making it easier to support WebTransport and raw QUIC simultaneously.
///
/// There is no CONNECT request, so [`Self::request`] returns `None`. The response
/// is supplied by the caller to carry the negotiated ALPN via [`Self::protocol`].
pub fn raw(conn: quinn::Connection, response: impl Into<ConnectResponse>) -> Self {
Self {
conn,
session_id: None,
Expand All @@ -507,13 +506,15 @@ impl Session {
settings: None,
connect_send: Arc::new(Mutex::new(None)),
error: Arc::new(OnceLock::new()),
request: request.into(),
request: None,
response: response.into(),
}
}

pub fn request(&self) -> &ConnectRequest {
&self.request
/// Returns the [`ConnectRequest`] if this session was established over HTTP/3,
/// or `None` for a raw QUIC session.
pub fn request(&self) -> Option<&ConnectRequest> {
self.request.as_ref()
}

pub fn response(&self) -> &ConnectResponse {
Expand Down
155 changes: 155 additions & 0 deletions rs/web-transport-quinn/tests/raw.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
//! Raw QUIC sessions: a QUIC connection using a non-HTTP/3 ALPN, wrapped in a
//! [`Session`] so callers can treat WebTransport and raw QUIC uniformly.
//!
//! There is no CONNECT request on this path, so there is no request URL. The
//! negotiated ALPN is carried by the response instead.

// A crypto provider is needed to establish the connection, matching the gate on
// the crate's own builders.
#![cfg(any(feature = "aws-lc-rs", feature = "ring"))]

use std::{
net::{Ipv4Addr, SocketAddr},
sync::Arc,
};

use anyhow::{Context, Result};
use rcgen::{CertifiedKey, KeyPair};
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
// The trait provides `protocol()`; imported anonymously to avoid clashing with
// the concrete `Session`.
use web_transport_quinn::generic::Session as _;
use web_transport_quinn::{proto::ConnectResponse, Session};

/// A raw QUIC ALPN, i.e. anything other than the `h3` used by WebTransport.
const RAW_ALPN: &str = "moq-00";

fn self_signed() -> Result<(CertificateDer<'static>, PrivateKeyDer<'static>)> {
let CertifiedKey { cert, signing_key } =
rcgen::generate_simple_self_signed(vec!["localhost".into()]).context("rcgen")?;

let cert_der = CertificateDer::from(cert.der().to_vec());
let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(KeyPair::serialize_der(
&signing_key,
)));

Ok((cert_der, key_der))
}

/// The provider is passed explicitly rather than relying on the process-level
/// default, which rustls cannot determine when both backend features are enabled
/// (as `--all-features` does).
fn crypto_provider() -> Arc<rustls::crypto::CryptoProvider> {
#[cfg(feature = "aws-lc-rs")]
{
Arc::new(rustls::crypto::aws_lc_rs::default_provider())
}
#[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))]
{
Arc::new(rustls::crypto::ring::default_provider())
}
}

/// Bind a QUIC server and connect a client to it, both offering only `RAW_ALPN`.
async fn connect_raw() -> Result<(quinn::Connection, quinn::Connection)> {
let (cert, key) = self_signed()?;
let provider = crypto_provider();

let mut server_crypto = rustls::ServerConfig::builder_with_provider(provider.clone())
.with_protocol_versions(&[&rustls::version::TLS13])?
.with_no_client_auth()
.with_single_cert(vec![cert.clone()], key)?;
server_crypto.alpn_protocols = vec![RAW_ALPN.as_bytes().to_vec()];

let server_config = quinn::ServerConfig::with_crypto(Arc::new(
quinn::crypto::rustls::QuicServerConfig::try_from(server_crypto)?,
));

let bind: SocketAddr = (Ipv4Addr::LOCALHOST, 0).into();
let server = quinn::Endpoint::server(server_config, bind)?;
let server_addr = server.local_addr()?;

let mut roots = rustls::RootCertStore::empty();
roots.add(cert)?;

let mut client_crypto = rustls::ClientConfig::builder_with_provider(provider)
.with_protocol_versions(&[&rustls::version::TLS13])?
.with_root_certificates(roots)
.with_no_client_auth();
client_crypto.alpn_protocols = vec![RAW_ALPN.as_bytes().to_vec()];

let client_config = quinn::ClientConfig::new(Arc::new(
quinn::crypto::rustls::QuicClientConfig::try_from(client_crypto)?,
));

let mut client = quinn::Endpoint::client((Ipv4Addr::LOCALHOST, 0).into())?;
client.set_default_client_config(client_config);

let accept = tokio::spawn(async move {
let conn = server.accept().await.context("no connection")?.await?;
anyhow::Ok(conn)
});

let client_conn = client.connect(server_addr, "localhost")?.await?;
let server_conn = accept.await??;

Ok((client_conn, server_conn))
}

/// The ALPN the peers negotiated, as an accept path would read it.
fn negotiated_alpn(conn: &quinn::Connection) -> Result<String> {
let data = conn
.handshake_data()
.context("no handshake data")?
.downcast::<quinn::crypto::rustls::HandshakeData>()
.ok()
.context("unexpected handshake data")?;

Ok(String::from_utf8(data.protocol.context("no ALPN")?)?)
}

/// A raw session has no CONNECT request, so it has no URL. Before this was an
/// `Option`, callers had to synthesize a fake request to construct one.
#[tokio::test]
async fn raw_session_has_no_request() -> Result<()> {
let (client_conn, _server_conn) = connect_raw().await?;

let session = Session::raw(client_conn, ConnectResponse::OK);
assert!(session.request().is_none());

Ok(())
}

/// The response is what carries the negotiated ALPN on the raw path.
#[tokio::test]
async fn raw_session_reports_alpn_as_protocol() -> Result<()> {
let (client_conn, _server_conn) = connect_raw().await?;

let alpn = negotiated_alpn(&client_conn)?;
assert_eq!(alpn, RAW_ALPN);

let session = Session::raw(client_conn, ConnectResponse::OK.with_protocol(&alpn));
assert_eq!(session.protocol(), Some(RAW_ALPN));

Ok(())
}

/// A raw session must not write the WebTransport stream header, since the peer
/// is reading plain QUIC.
#[tokio::test]
async fn raw_session_streams_omit_webtransport_header() -> Result<()> {
let (client_conn, server_conn) = connect_raw().await?;

let client = Session::raw(client_conn, ConnectResponse::OK);
let server = Session::raw(server_conn, ConnectResponse::OK);

let mut send = client.open_uni().await?;
send.write_all(b"hello").await?;
send.finish()?;

let mut recv = server.accept_uni().await?;
let data = recv.read_to_end(1024).await?;
assert_eq!(&data, b"hello");

Ok(())
}
7 changes: 4 additions & 3 deletions rs/web-transport/src/quinn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,10 @@ impl Session {
self.inner.closed().await.into()
}

/// Return the URL used to create the session.
pub fn url(&self) -> &Url {
&self.inner.request().url
/// Return the URL used to create the session, or `None` for a raw QUIC
/// session established without an HTTP/3 CONNECT request.
pub fn url(&self) -> Option<&Url> {
self.inner.request().map(|request| &request.url)
}

/// Return the application protocol used to create the session.
Expand Down
7 changes: 5 additions & 2 deletions rs/web-transport/src/wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,11 @@ impl Session {
}

/// Return the URL used to create the session.
pub fn url(&self) -> &Url {
self.0.url()
///
/// Always `Some` on this platform; the browser API has no raw QUIC session.
/// Matches the native signature so callers compile on both targets.
pub fn url(&self) -> Option<&Url> {
Some(self.0.url())
}

/// Return the application protocol used to create the session.
Expand Down