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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/devcert/** -linguist-detectable
10 changes: 5 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ members = [
]

[workspace.package]
version = "0.1.0"
version = "0.2.0"
edition = "2024"
authors = ["shellrow <shellrow@foctal.com>"]

[workspace.dependencies]
webtrans-proto = { path = "webtrans-proto", version = "0.1.0" }
webtrans-trait = { path = "webtrans-trait", version = "0.1.0" }
webtrans-quinn = { path = "webtrans-quinn", version = "0.1.0" }
webtrans-wasm = { path = "webtrans-wasm", version = "0.1.0" }
webtrans-proto = { path = "webtrans-proto", version = "0.2.0" }
webtrans-trait = { path = "webtrans-trait", version = "0.2.0" }
webtrans-quinn = { path = "webtrans-quinn", version = "0.2.0" }
webtrans-wasm = { path = "webtrans-wasm", version = "0.2.0" }
bytes = "1"
thiserror = "2"
http = "1"
Expand Down
20 changes: 7 additions & 13 deletions webtrans-proto/src/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,14 @@ impl Frame {

Ok((typ, limit))
}
}

macro_rules! frames {
{$($name:ident = $val:expr,)*} => {
impl Frame {
$(pub const $name: Frame = Frame(VarInt::from_u32($val));)*
}
pub const fn from_u32(value: u32) -> Self {
Self(VarInt::from_u32(value))
}
}

// Frames sent at the start of a bidirectional stream.
frames! {
DATA = 0x00,
HEADERS = 0x01,
SETTINGS = 0x04,
WEBTRANSPORT = 0x41,
// Frames sent at the start of a bidirectional stream.
pub const DATA: Frame = Frame::from_u32(0x00);
pub const HEADERS: Frame = Frame::from_u32(0x01);
pub const SETTINGS: Frame = Frame::from_u32(0x04);
pub const WEBTRANSPORT: Frame = Frame::from_u32(0x41);
}
28 changes: 12 additions & 16 deletions webtrans-proto/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,31 +64,27 @@ impl Debug for Setting {
}
}

macro_rules! settings {
{$($name:ident = $val:expr,)*} => {
impl Setting {
$(pub const $name: Setting = Setting(VarInt::from_u32($val));)*
}
impl Setting {
pub const fn from_u32(value: u32) -> Self {
Self(VarInt::from_u32(value))
}
}

settings! {
// HTTP/3 settings that WebTransport ignores.
QPACK_MAX_TABLE_CAPACITY = 0x1, // Default is 0, which disables the dynamic table.
MAX_FIELD_SECTION_SIZE = 0x6,
QPACK_BLOCKED_STREAMS = 0x7,
pub const QPACK_MAX_TABLE_CAPACITY: Setting = Setting::from_u32(0x1); // Default is 0, which disables the dynamic table.
pub const MAX_FIELD_SECTION_SIZE: Setting = Setting::from_u32(0x6);
pub const QPACK_BLOCKED_STREAMS: Setting = Setting::from_u32(0x7);

// Both values are required for WebTransport.
ENABLE_CONNECT_PROTOCOL = 0x8,
ENABLE_DATAGRAM = 0x33,
ENABLE_DATAGRAM_DEPRECATED = 0xFFD277, // Still used by some Chrome versions.
pub const ENABLE_CONNECT_PROTOCOL: Setting = Setting::from_u32(0x8);
pub const ENABLE_DATAGRAM: Setting = Setting::from_u32(0x33);
pub const ENABLE_DATAGRAM_DEPRECATED: Setting = Setting::from_u32(0xFFD277); // Still used by some Chrome versions.

// Removed in draft-06.
WEBTRANSPORT_ENABLE_DEPRECATED = 0x2b603742,
WEBTRANSPORT_MAX_SESSIONS_DEPRECATED = 0x2b603743,
pub const WEBTRANSPORT_ENABLE_DEPRECATED: Setting = Setting::from_u32(0x2b603742);
pub const WEBTRANSPORT_MAX_SESSIONS_DEPRECATED: Setting = Setting::from_u32(0x2b603743);

// Current way to enable WebTransport.
WEBTRANSPORT_MAX_SESSIONS = 0xc671706a,
pub const WEBTRANSPORT_MAX_SESSIONS: Setting = Setting::from_u32(0xc671706a);
}

#[derive(Error, Debug, Clone)]
Expand Down
20 changes: 7 additions & 13 deletions webtrans-proto/src/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,14 @@ impl UniStream {
(val - 0x21) % 0x1f == 0
}
}
}

macro_rules! streams_uni {
{$($name:ident = $val:expr,)*} => {
impl UniStream {
$(pub const $name: UniStream = UniStream(VarInt::from_u32($val));)*
}
pub const fn from_u32(value: u32) -> Self {
Self(VarInt::from_u32(value))
}
}

streams_uni! {
CONTROL = 0x00,
PUSH = 0x01,
QPACK_ENCODER = 0x02,
QPACK_DECODER = 0x03,
WEBTRANSPORT = 0x54,
pub const CONTROL: UniStream = UniStream::from_u32(0x00);
pub const PUSH: UniStream = UniStream::from_u32(0x01);
pub const QPACK_ENCODER: UniStream = UniStream::from_u32(0x02);
pub const QPACK_DECODER: UniStream = UniStream::from_u32(0x03);
pub const WEBTRANSPORT: UniStream = UniStream::from_u32(0x54);
}
4 changes: 2 additions & 2 deletions webtrans-quinn/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Native WebTransport implementation built on top of QUIC using Quinn.
//!
//! This crate provides a low-level, QUIC WebTransport API for native environments.
//!
//! This crate provides a low-level, QUIC WebTransport API for native environments.
//!
//! The implementation is powered by [`quinn`], and most transport-level
//! behavior (congestion control, flow control, crypto, etc.) is delegated
//! directly to Quinn.
Expand Down
80 changes: 65 additions & 15 deletions webtrans-quinn/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,28 @@ use crate::{

use webtrans_proto::{Frame, UniStream, VarInt};

fn is_graceful_close(e: &webtrans_proto::CapsuleError) -> bool {
use std::io::ErrorKind;

match e {
webtrans_proto::CapsuleError::Io(ioe) => {
matches!(
ioe.kind(),
ErrorKind::UnexpectedEof
| ErrorKind::BrokenPipe
| ErrorKind::ConnectionReset
| ErrorKind::ConnectionAborted
| ErrorKind::NotConnected
) || ioe
.to_string()
.to_ascii_lowercase()
.contains("connection lost")
}
webtrans_proto::CapsuleError::UnexpectedEnd => true,
_ => false,
}
}

/// An established WebTransport session, acting like a full QUIC connection. See [`quinn::Connection`].
///
/// Remember that WebTransport is layered on top of QUIC:
Expand Down Expand Up @@ -84,35 +106,62 @@ impl Session {
};

// Run a background task to detect CONNECT stream closure.
let mut this2 = this.clone();
let this2 = this.clone();
tokio::spawn(async move {
let (code, reason) = this2.run_closed(connect).await;
tracing::debug!(
"closing QUIC connection after WebTransport close: code={code} reason={reason}"
);
if this2.conn.close_reason().is_none() {
this2.conn.close(0u32.into(), reason.as_bytes());
match this2.run_closed(connect).await {
Ok(Some((code, reason))) => {
tracing::debug!("WebTransport close received: code={code} reason={reason}");
if this2.conn.close_reason().is_none() {
this2.close(code, reason.as_bytes());
}
}
Ok(None) => {
if let Some(reason) = this2.conn.close_reason() {
let se: crate::SessionError = reason.into();
tracing::debug!("CONNECT stream ended: {se}");
} else {
tracing::debug!("CONNECT stream ended without CloseWebTransportSession");
}
}
Err(e) if is_graceful_close(&e) => {
if let Some(reason) = this2.conn.close_reason() {
let se: crate::SessionError = reason.into();
tracing::debug!(
"CONNECT stream closed after QUIC close: {se} (capsule={e})"
);
} else {
tracing::debug!("CONNECT stream closed: {e}");
}
}
Err(e) => {
tracing::debug!("CONNECT stream error: {e}");
if this2.conn.close_reason().is_none() {
this2.close(1, b"capsule error");
}
}
}
});

this
}

// Keep reading from the control stream until it closes.
async fn run_closed(&mut self, connect: Connect) -> (u32, String) {
async fn run_closed(
&self,
connect: Connect,
) -> Result<Option<(u32, String)>, webtrans_proto::CapsuleError> {
let (_send, mut recv) = connect.into_inner();

loop {
match webtrans_proto::Capsule::read(&mut recv).await {
Ok(webtrans_proto::Capsule::CloseWebTransportSession { code, reason }) => {
return (code, reason);
return Ok(Some((code, reason)));
}
Ok(webtrans_proto::Capsule::Unknown { typ, payload }) => {
tracing::warn!("unknown capsule: type={typ} size={}", payload.len());
}
Err(_) => {
return (1, "capsule error".to_string());
}
Err(e) if is_graceful_close(&e) => return Ok(None),
Err(e) => return Err(e),
}
}
}
Expand Down Expand Up @@ -250,10 +299,11 @@ impl Session {

/// Immediately close the connection with an error code and reason. See [`quinn::Connection::close`].
pub fn close(&self, code: u32, reason: &[u8]) {
let code = if self.session_id.is_some() {
webtrans_proto::error_to_http3(code).try_into().unwrap()
let code: quinn::VarInt = if self.session_id.is_some() {
let mapped = webtrans_proto::error_to_http3(code);
quinn::VarInt::from_u64(mapped).unwrap_or(quinn::VarInt::from_u32(1))
} else {
code.into()
quinn::VarInt::from_u32(code)
};

self.conn.close(code, reason)
Expand Down
4 changes: 2 additions & 2 deletions webtrans/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ license = "MIT"
webtrans-proto = { workspace = true }

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
webtrans-quinn = { version = "0.1.0", path = "../webtrans-quinn" }
webtrans-quinn = { workspace = true }

[target.'cfg(target_arch = "wasm32")'.dependencies]
webtrans-wasm = { version = "0.1.0", path = "../webtrans-wasm" }
webtrans-wasm = { workspace = true }

[dev-dependencies]
anyhow = "1"
Expand Down
4 changes: 4 additions & 0 deletions webtrans/examples/echo-client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ async fn main() -> anyhow::Result<()> {
session.close(42069, b"bye");
session.closed().await;

tracing::info!("session closed");
tracing::info!("waiting a moment to ensure close");
tokio::time::sleep(std::time::Duration::from_millis(100)).await;

Ok(())
}

Expand Down