From 13fb38f84c8f092570327a766552fcd5a7ba9ab7 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 30 Sep 2026 21:26:09 -0700 Subject: [PATCH] refactor(tokio)!: make Quinn the only native QUIC backend Co-Authored-By: GPT-6 --- Cargo.lock | 96 ++++++-- Cargo.toml | 11 +- doc/bin/cli.md | 2 +- doc/bin/relay/config.md | 9 +- doc/lib/rs/index.md | 2 +- doc/lib/rs/moq-net.md | 2 +- doc/setup/upgrade.md | 6 +- rs/hang/Cargo.toml | 2 +- rs/justfile | 7 +- rs/moq-bench/Cargo.toml | 3 +- rs/moq-boy/Cargo.toml | 3 +- rs/moq-c/Cargo.toml | 2 +- rs/moq-cli/Cargo.toml | 7 +- rs/moq-gst/src/lib.rs | 1 + rs/moq-hls/Cargo.toml | 2 +- rs/moq-relay/Cargo.toml | 7 +- rs/moq-relay/src/relay.rs | 51 +--- rs/moq-relay/tests/drills.rs | 2 +- rs/moq-relay/tests/embed.rs | 2 - rs/moq-relay/tests/runtime_workers.rs | 2 +- rs/moq-rtmp/Cargo.toml | 2 +- rs/moq-tokio/Cargo.toml | 51 ++-- rs/moq-tokio/README.md | 13 +- rs/moq-tokio/benches/latency.rs | 111 +++++++++ rs/moq-tokio/src/client.rs | 213 +++-------------- rs/moq-tokio/src/connect.rs | 3 +- rs/moq-tokio/src/error.rs | 26 +- rs/moq-tokio/src/iroh.rs | 4 +- rs/moq-tokio/src/lib.rs | 20 +- rs/moq-tokio/src/listen.rs | 9 +- rs/moq-tokio/src/log.rs | 1 + rs/moq-tokio/src/quic.rs | 15 +- rs/moq-tokio/src/{noq.rs => quinn.rs} | 329 ++++++++++++++------------ rs/moq-tokio/src/resolve.rs | 1 - rs/moq-tokio/src/server.rs | 167 +++---------- rs/moq-tokio/src/tls.rs | 76 ++---- rs/moq-tokio/src/transport.rs | 2 +- rs/moq-tokio/src/websocket.rs | 4 +- rs/moq-tokio/src/worker/group.rs | 8 +- rs/moq-tokio/src/worker/mod.rs | 2 - rs/moq-tokio/tests/backend.rs | 88 +++---- rs/moq-tokio/tests/broadcast.rs | 2 +- rs/moq-tokio/tests/close_code.rs | 2 +- rs/moq-tokio/tests/worker.rs | 2 +- rs/moq-transcode/Cargo.toml | 2 +- rs/moq-uring/Cargo.toml | 5 +- sh/rs/tokio-features.sh | 19 +- 47 files changed, 603 insertions(+), 793 deletions(-) create mode 100644 rs/moq-tokio/benches/latency.rs rename rs/moq-tokio/src/{noq.rs => quinn.rs} (76%) diff --git a/Cargo.lock b/Cargo.lock index 7bd01a7c9a..f1e1e7ed5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1718,7 +1718,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c54e03a951783e8b327515db3f2a2fd0e3bed362a96b066f341ce66ed49b4ead" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 3.0.6", ] [[package]] @@ -2186,7 +2186,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4532,7 +4532,7 @@ dependencies = [ "pin-project-lite", "rustc-hash 2.1.3", "rustls", - "socket2 0.5.10", + "socket2 0.6.5", "thiserror 2.0.21", "tokio", "tokio-stream", @@ -4558,7 +4558,6 @@ dependencies = [ "n0-qlog", "rand 0.10.3", "rand_pcg", - "ring", "rustc-hash 2.1.3", "rustls", "rustls-pki-types", @@ -4579,7 +4578,7 @@ checksum = "b8dc8cf6fa35ba82ba89a4db24661068a75b7432abfb1ff3d74993dedfc2c9f0" dependencies = [ "cfg_aliases", "libc", - "socket2 0.5.10", + "socket2 0.6.5", "tracing", "windows-sys 0.61.2", ] @@ -4785,6 +4784,7 @@ dependencies = [ "anyhow", "bytes", "chrono", + "criterion", "dns-lookup", "flume", "futures", @@ -4825,8 +4825,8 @@ dependencies = [ "url", "usage-rs", "web-transport-iroh", - "web-transport-moq", - "web-transport-proto", + "web-transport-proto 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "web-transport-quinn", "web-transport-trait", "webpki-roots", "x509-parser", @@ -4881,7 +4881,7 @@ dependencies = [ "tracing", "url", "web-transport-moq", - "web-transport-proto", + "web-transport-proto 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", "web-transport-trait", ] @@ -5411,7 +5411,7 @@ dependencies = [ "pin-project-lite", "rustc-hash 2.1.3", "rustls", - "socket2 0.5.10", + "socket2 0.6.5", "thiserror 2.0.21", "tokio", "tokio-stream", @@ -5454,7 +5454,7 @@ checksum = "4dc50afea61aff926e150a36c31f06094608848e114a3fe667d76ec233163b1c" dependencies = [ "cfg_aliases", "libc", - "socket2 0.5.10", + "socket2 0.6.5", "tracing", "windows-sys 0.61.2", ] @@ -6845,6 +6845,18 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "qlog" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f15b83c59e6b945f2261c95a1dd9faf239187f32ff0a96af1d1d28c4557f919" +dependencies = [ + "serde", + "serde_json", + "serde_with", + "smallvec", +] + [[package]] name = "qmux" version = "0.6.1" @@ -6860,7 +6872,7 @@ dependencies = [ "tokio-rustls", "tokio-tungstenite 0.30.0", "tracing", - "web-transport-proto", + "web-transport-proto 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", "web-transport-trait", ] @@ -6895,7 +6907,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.3", "rustls", - "socket2 0.5.10", + "socket2 0.6.5", "thiserror 2.0.21", "tokio", "tracing", @@ -6910,14 +6922,17 @@ checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc" dependencies = [ "aws-lc-rs", "bytes", + "fastbloom", "getrandom 0.4.3", "lru-slab", + "qlog", "rand 0.10.3", "rand_pcg", "ring", "rustc-hash 2.1.3", "rustls", "rustls-pki-types", + "rustls-platform-verifier", "slab", "thiserror 2.0.21", "tinyvec", @@ -6934,9 +6949,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.5", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -7378,7 +7393,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -7437,7 +7452,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -8046,6 +8061,9 @@ name = "smallvec" version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" +dependencies = [ + "serde", +] [[package]] name = "smawk" @@ -8590,10 +8608,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix 1.1.5", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -9861,7 +9879,7 @@ dependencies = [ "tokio", "tracing", "url", - "web-transport-proto", + "web-transport-proto 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", "web-transport-trait", ] @@ -9882,7 +9900,7 @@ dependencies = [ "tokio", "tracing", "url", - "web-transport-proto", + "web-transport-proto 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", "web-transport-trait", ] @@ -9900,11 +9918,43 @@ dependencies = [ "url", ] +[[package]] +name = "web-transport-proto" +version = "0.6.2" +source = "git+https://github.com/moq-dev/web-transport?rev=c979d8c237753fae08086acd8c357e6ae385739a#c979d8c237753fae08086acd8c357e6ae385739a" +dependencies = [ + "bytes", + "http", + "sfv", + "thiserror 2.0.21", + "tokio", + "url", +] + +[[package]] +name = "web-transport-quinn" +version = "0.13.0" +source = "git+https://github.com/moq-dev/web-transport?rev=c979d8c237753fae08086acd8c357e6ae385739a#c979d8c237753fae08086acd8c357e6ae385739a" +dependencies = [ + "bytes", + "futures", + "http", + "kio 0.5.9", + "quinn", + "rustls", + "rustls-native-certs", + "thiserror 2.0.21", + "tokio", + "tracing", + "url", + "web-transport-proto 0.6.2 (git+https://github.com/moq-dev/web-transport?rev=c979d8c237753fae08086acd8c357e6ae385739a)", + "web-transport-trait", +] + [[package]] name = "web-transport-trait" version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f1e801f997864ab0074afb1c7d3262df46d5d9ad9b78274c47694544db2dba7" +source = "git+https://github.com/moq-dev/web-transport?rev=c979d8c237753fae08086acd8c357e6ae385739a#c979d8c237753fae08086acd8c357e6ae385739a" dependencies = [ "bytes", ] @@ -10172,7 +10222,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 28369f6eee..9b64a30737 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -160,7 +160,7 @@ moq-rtmp = { version = "0.3.9", path = "rs/moq-rtmp" } moq-sock = { version = "0.2.0", path = "rs/moq-sock" } moq-srt = { version = "0.3.9", path = "rs/moq-srt" } moq-stats = { version = "0.2.9", path = "rs/moq-stats" } -moq-tokio = { version = "0.19.20", path = "rs/moq-tokio", default-features = false } +moq-tokio = { version = "0.19.20", path = "rs/moq-tokio", default-features = false, features = ["aws-lc-rs"] } # Default features off on moq-transcode and moq-video so each workspace consumer # chooses native codecs, OpenH264, and rendering explicitly. Both crates still # provide working native plus software defaults when depended on directly. @@ -229,17 +229,20 @@ 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" -# 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. +# The WebTransport adapter used by the experimental io_uring backend. web-transport-moq = { version = "2.0", default-features = false } web-transport-proto = "0.6" +web-transport-quinn = { version = "0.13", default-features = false } web-transport-trait = "0.5" web-transport-wasm = "0.7" x11rb = { version = "0.14.0", features = ["randr", "xfixes"] } zeroize = { version = "1", features = ["derive"] } [patch.crates-io] +# Preserve raw QUIC close codes until moq-dev/web-transport#409 is released. +web-transport-quinn = { git = "https://github.com/moq-dev/web-transport", rev = "c979d8c237753fae08086acd8c357e6ae385739a" } +# The adapter's workspace dependency must share the trait identity with registry consumers. +web-transport-trait = { git = "https://github.com/moq-dev/web-transport", rev = "c979d8c237753fae08086acd8c357e6ae385739a" } # web-transport-iroh depends on kio from crates.io. Without # this, the lock carries a second kio whose pin drifts behind the member, and the # next publish that reaches both fails to resolve a single version (#3551). diff --git a/doc/bin/cli.md b/doc/bin/cli.md index 3f22497a27..e8bfba67c2 100644 --- a/doc/bin/cli.md +++ b/doc/bin/cli.md @@ -162,7 +162,7 @@ behind the `play` feature, since it pulls in windowing and audio-device dependencies: ```bash -cargo install moq-cli --no-default-features --features "iroh,noq,websocket,play" +cargo install moq-cli --no-default-features --features "iroh,websocket,play" ``` ## Capture diff --git a/doc/bin/relay/config.md b/doc/bin/relay/config.md index 209b8032e3..add00f679b 100644 --- a/doc/bin/relay/config.md +++ b/doc/bin/relay/config.md @@ -52,8 +52,8 @@ send_window = 33554432 qlog = "/var/log/moq/qlog" # Existing directory. Needs the `qlog` build feature. ``` -The native QUIC stack uses BBRv3 for delay-based congestion control. Iroh also -uses noq and the same congestion controller. +The native QUIC stack always uses Quinn, with BBRv1 for delay-based +congestion control. Iroh and the experimental io\_uring runtime use noq and BBRv3. Raise the receive windows when a fat, long path idles below the link rate: a window under the bandwidth-delay product stalls the sender waiting for credit. @@ -79,10 +79,7 @@ io_uring = false # Drive them with io_uring instead of tokio Packets are steered by connection ID, so a client that migrates stays with its worker. The group shares one port, including an ephemeral (zero) port, which is resolved once and joined by every worker. Use an explicit port unless -something reads the bound address at startup. `workers` needs the `noq` -feature and real certificate files rather than `tls.generate`. A build without -QUIC rejects `workers` instead of -ignoring it. An embedding process leaves this group inside `Relay::run`; +something reads the bound address at startup. `workers` needs real certificate files rather than `tls.generate`. An embedding process leaves this group inside `Relay::run`; taking the sockets out and driving them yourself is how a later library update can drop QUIC while still compiling. `io_uring` additionally needs Linux 6.12+, the `io-uring` cargo feature, and exactly one certificate read at startup; it serves moq-lite only, diff --git a/doc/lib/rs/index.md b/doc/lib/rs/index.md index 48bf90f55d..4d55c0b741 100644 --- a/doc/lib/rs/index.md +++ b/doc/lib/rs/index.md @@ -14,7 +14,7 @@ The reference implementation. Every crate is on | --- | --- | | [moq-net](/lib/rs/moq-net) | The pub/sub layer: sessions, origins, broadcasts, tracks, groups, frames. Transport-agnostic. | | [moq-pattern](https://docs.rs/moq-pattern) | Exact path patterns: grammar, matching, and set algebra. Re-exported by moq-net and moq-auth. | -| [moq-tokio](https://docs.rs/moq-tokio) | Stands up QUIC with noq, TLS, WebSocket fallback, and iroh, from config or CLI flags. | +| [moq-tokio](https://docs.rs/moq-tokio) | Stands up QUIC with Quinn, TLS, WebSocket fallback, and iroh, from config or CLI flags. | | [moq-sock](https://docs.rs/moq-sock) | Dual-stack socket binding, `SO_REUSEPORT` groups steered by QUIC connection ID, and CPU pinning for thread-per-core listeners. | | [moq-uring](https://docs.rs/moq-uring) | Experimental Linux io\_uring worker: one pinned thread per ring serving moq-lite over its own QUIC stack. | | [hang](/lib/rs/hang) | The media layer: catalog, containers, ordered frame delivery. | diff --git a/doc/lib/rs/moq-net.md b/doc/lib/rs/moq-net.md index cb2975a206..44b90ef92b 100644 --- a/doc/lib/rs/moq-net.md +++ b/doc/lib/rs/moq-net.md @@ -25,7 +25,7 @@ above ([hang](/lib/rs/hang)); relays and CDNs implement only this. - **Routes** record the relay hops and a cost, which is what the relay [cluster](/bin/relay/cluster) routes on. A hop of 0 marks the chain anonymous: `Route::is_anonymous()` is true, and that route ranks below every fully identified one. `Route::source()` says where a delivered route entered: `Source::Local`, or `Source::Peer(hop)` when a handle marked `origin::Producer::peer()` announced it. `origin::Consumer::local()` sees only the local ones. - **Stats** counters per broadcast and session, drained by [`moq-stats`](https://docs.rs/moq-stats). -It runs over anything implementing `web_transport_trait::poll::Session`: noq, the +It runs over anything implementing `web_transport_trait::poll::Session`: Quinn, noq, the browser, iroh, or qmux over TCP, Unix sockets, and WebSockets. [`moq-tokio`](https://docs.rs/moq-tokio) wires those up. diff --git a/doc/setup/upgrade.md b/doc/setup/upgrade.md index 149cb99f86..f59cc11505 100644 --- a/doc/setup/upgrade.md +++ b/doc/setup/upgrade.md @@ -134,8 +134,10 @@ Other changes to a deployment: frame. `moq auth serve --key` takes a file, not an https or JWKS URL. `moq auth sign --root` is the token root and JS `verify --root` the dialed path. `/.cluster*` roots are reserved. `--auth-public a,b` splits on commas. -- **noq is the only QUIC stack** (#3811). The `quinn` and `quiche` cargo - features and the backend setting are gone. +- **Quinn is the native QUIC stack**. Remove `noq` and `quinn` from Cargo feature + lists; QUIC is always available. The backend setting and `quiche` feature + remain removed. Other moq-tokio features remain optional. Iroh and the + experimental io\_uring runtime still use noq. - **Stats counters** are `*_started` / `*_ended` (`sessions_started`, `announces_ended`, ...). This release still writes the old `announced` / `*_closed` names beside them, so move consumers now. diff --git a/rs/hang/Cargo.toml b/rs/hang/Cargo.toml index 940ce9dea4..a8e6c115b2 100644 --- a/rs/hang/Cargo.toml +++ b/rs/hang/Cargo.toml @@ -33,7 +33,7 @@ features = ["from", "display", "debug"] [dev-dependencies] anyhow = { workspace = true } moq-mux = { path = "../moq-mux" } -moq-tokio = { path = "../moq-tokio", default-features = false, features = ["noq", "aws-lc-rs", "websocket"] } +moq-tokio = { path = "../moq-tokio", default-features = false, features = ["aws-lc-rs", "websocket"] } tokio = { workspace = true, features = ["macros", "fs"] } tracing = { workspace = true } url = { workspace = true } diff --git a/rs/justfile b/rs/justfile index c583c4e739..dfea3f45eb 100644 --- a/rs/justfile +++ b/rs/justfile @@ -153,10 +153,6 @@ wasm-fix *args: audit: cargo deny check --show-stats -# Verify relay startup validation without a QUIC backend. -relay-minimal: - cargo nextest run --locked -p moq-relay --no-default-features --lib -E 'test(workers_require_a_quic_backend)' - # Outside nextest because the staticlib link alone outlasts its hang budget. # Compile and run moq-c's C fixtures, and the C doc samples, against libmoq.a. Must run on Unix. @@ -171,9 +167,8 @@ c-tests: # Compile the workspace at the feature extremes. features: cargo check --locked --workspace {{ no_fuzz }} --all-targets --no-default-features - cargo hack check --locked -p moq-tokio --each-feature --exclude-features _transport --features aws-lc-rs + cargo hack check --locked -p moq-tokio --each-feature --features aws-lc-rs cargo check --locked -p moq-relay --no-default-features - just rs relay-minimal cargo check --locked -p moq-cli --no-default-features cargo check --locked -p moq-bench --no-default-features cargo check --locked -p moq-boy --no-default-features diff --git a/rs/moq-bench/Cargo.toml b/rs/moq-bench/Cargo.toml index 0de559a2e6..d5fd6fbed4 100644 --- a/rs/moq-bench/Cargo.toml +++ b/rs/moq-bench/Cargo.toml @@ -35,9 +35,8 @@ required-features = ["uring"] doc = false [features] -default = ["noq", "websocket"] +default = ["websocket"] iroh = ["moq-tokio/iroh"] -noq = ["moq-tokio/noq"] uring = ["dep:moq-uring"] websocket = ["moq-tokio/websocket"] diff --git a/rs/moq-boy/Cargo.toml b/rs/moq-boy/Cargo.toml index 53102aa218..3ad8d907c7 100644 --- a/rs/moq-boy/Cargo.toml +++ b/rs/moq-boy/Cargo.toml @@ -12,9 +12,8 @@ edition = "2024" rust-version.workspace = true [features] -default = ["noq", "websocket"] +default = ["websocket"] jemalloc = ["moq-tokio/jemalloc"] -noq = ["moq-tokio/noq"] websocket = ["moq-tokio/websocket"] [dependencies] diff --git a/rs/moq-c/Cargo.toml b/rs/moq-c/Cargo.toml index c7564df387..fd67711553 100644 --- a/rs/moq-c/Cargo.toml +++ b/rs/moq-c/Cargo.toml @@ -26,7 +26,7 @@ moq-audio = { workspace = true, features = ["aac"] } moq-json = { workspace = true } moq-mux = { workspace = true } moq-net = { workspace = true } -moq-tokio = { workspace = true, features = ["noq", "aws-lc-rs", "websocket", "tcp", "uds"] } +moq-tokio = { workspace = true, features = ["aws-lc-rs", "websocket", "tcp", "uds"] } # Select the OpenH264 fallback plus NVIDIA and VAAPI hardware codecs explicitly # because workspace defaults are off. The hardware backends dlopen their driver; # `vaapi` also runs bindgen, so Linux builds need libclang. diff --git a/rs/moq-cli/Cargo.toml b/rs/moq-cli/Cargo.toml index 1d93822e90..c664b72121 100644 --- a/rs/moq-cli/Cargo.toml +++ b/rs/moq-cli/Cargo.toml @@ -26,12 +26,11 @@ path = "src/main.rs" doc = false [features] -default = ["iroh", "cluster-lan", "noq", "websocket", "nvidia"] +default = ["iroh", "cluster-lan", "websocket", "nvidia"] iroh = ["moq-tokio/iroh"] # LAN discovery and meshing (`--cluster-lan`). cluster-lan = ["moq-relay/cluster-lan"] jemalloc = ["moq-tokio/jemalloc"] -noq = ["moq-tokio/noq"] # qlog trace capture (`--client-quic-qlog `). Off by default: it compiles the # qlog machinery into the enabled backends and is only wanted for debugging or # congestion-control testing. @@ -57,7 +56,7 @@ play = ["dep:moq-video", "moq-video/render", "dep:moq-audio", "moq-audio/playbac # the matching moq-video feature, so it only bites when something else pulls in # moq-video: `capture`, `transcode`, or `play`. For a build with no CUDA # dependencies, drop it: -# cargo build -p moq-cli --no-default-features --features "iroh,noq,websocket,capture" +# cargo build -p moq-cli --no-default-features --features "iroh,websocket,capture" nvidia = ["moq-video?/nvidia", "moq-transcode?/nvidia"] # Compatibility aliases for the pre-consolidation names, kept so a manifest that # already asks for the old split still resolves. Undocumented on purpose: the @@ -99,7 +98,7 @@ moq-auth = { workspace = true, features = ["serve"] } moq-hls = { workspace = true, features = ["server"] } moq-mux = { workspace = true } # Cluster, LAN mesh, and inbound `/.cluster` auth. default-features off so the -# CLI's own feature set (iroh, noq, cluster-lan) is what selects them. +# CLI's own feature set (iroh, cluster-lan) is what selects them. moq-relay = { workspace = true } moq-rtc = { workspace = true } moq-rtmp = { workspace = true } diff --git a/rs/moq-gst/src/lib.rs b/rs/moq-gst/src/lib.rs index 57c518542e..d6989f3276 100644 --- a/rs/moq-gst/src/lib.rs +++ b/rs/moq-gst/src/lib.rs @@ -22,6 +22,7 @@ pub fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { .with_default_directive(LevelFilter::INFO.into()) .from_env_lossy() // Allow overriding with RUST_LOG .add_directive("h2=warn".parse().unwrap()) + .add_directive("quinn=info".parse().unwrap()) .add_directive("noq=info".parse().unwrap()) .add_directive("tracing::span=off".parse().unwrap()) .add_directive("tracing::span::active=off".parse().unwrap()); diff --git a/rs/moq-hls/Cargo.toml b/rs/moq-hls/Cargo.toml index 122ad928b4..f63b6f4274 100644 --- a/rs/moq-hls/Cargo.toml +++ b/rs/moq-hls/Cargo.toml @@ -36,6 +36,6 @@ tracing = { workspace = true } url = { workspace = true } [dev-dependencies] -moq-tokio = { path = "../moq-tokio", default-features = false, features = ["tcp", "noq", "aws-lc-rs"] } +moq-tokio = { path = "../moq-tokio", default-features = false, features = ["tcp", "aws-lc-rs"] } tokio = { workspace = true, features = ["test-util"] } tower = { version = "0.5", features = ["util"] } diff --git a/rs/moq-relay/Cargo.toml b/rs/moq-relay/Cargo.toml index 2f77c34c96..cacecbd82c 100644 --- a/rs/moq-relay/Cargo.toml +++ b/rs/moq-relay/Cargo.toml @@ -25,16 +25,11 @@ doc = false [features] # A programmatic relay fixture with generated TLS and ephemeral ports. test-support = [] -default = ["iroh", "cluster-lan", "noq", "websocket", "uds"] +default = ["iroh", "cluster-lan", "websocket", "uds"] iroh = ["moq-tokio/iroh"] # Discover cluster peers on the LAN with mDNS (`--cluster-lan`). cluster-lan = ["moq-tokio/mdns"] jemalloc = ["moq-tokio/jemalloc"] -noq = ["_quic", "moq-tokio/noq"] -# The tokio QUIC listener, and the thread-per-core worker group behind -# `runtime.workers`. Not one to enable on its own: it names no backend, so -# moq-tokio would have nothing to bind a socket with. -_quic = [] # qlog trace capture (`--quic-qlog `). Off by default: it compiles the # qlog machinery into the enabled backends and is only wanted for debugging or # congestion-control testing. Covers the io_uring workers too, so one feature diff --git a/rs/moq-relay/src/relay.rs b/rs/moq-relay/src/relay.rs index cdf037dac9..625419b441 100644 --- a/rs/moq-relay/src/relay.rs +++ b/rs/moq-relay/src/relay.rs @@ -93,7 +93,6 @@ pub struct Relay { /// The thread-per-core QUIC workers, already bound and waiting to be split /// and run. `None` unless `runtime.workers` is configured, in which case /// `server` carries no QUIC listener of its own. - #[cfg(feature = "_quic")] workers: Option, /// The io_uring QUIC workers, already bound and waiting for /// [`serve`](crate::uring::Workers::serve). `None` unless both @@ -158,15 +157,6 @@ impl Relay { "runtime.io_uring needs moq-relay built with the `io-uring` feature" ); - // Refused rather than ignored: a group is the whole point of the setting, and - // silently serving from the shared runtime instead would look like it worked. - #[cfg(not(feature = "_quic"))] - anyhow::ensure!( - io_uring || config.runtime.workers().is_none(), - "runtime.workers needs moq-relay built with the `noq` feature" - ); - - #[cfg(feature = "_quic")] let workers = match config.runtime.workers() { Some(worker) if !io_uring => { let mut server = moq_tokio::server::Config::default(); @@ -177,16 +167,10 @@ impl Relay { _ => None, }; - // What the rest of setup reads off the group, so it has one shape whether or - // not a QUIC backend gave us one to read. - #[cfg(feature = "_quic")] + // Read the worker endpoint before moving the group into the serve loop. let workers_addr = workers.as_ref().map(moq_tokio::worker::Workers::local_addr); - #[cfg(not(feature = "_quic"))] - let workers_addr: Option = None; - #[cfg(feature = "_quic")] + let workers_certificates = workers.as_ref().map(moq_tokio::worker::Workers::certificates); - #[cfg(not(feature = "_quic"))] - let workers_certificates: Option = None; #[cfg(all(target_os = "linux", feature = "_uring"))] let uring = match config.runtime.workers() { @@ -326,7 +310,6 @@ impl Relay { web_routes: None, internal_routes: None, sessions, - #[cfg(feature = "_quic")] workers, #[cfg(all(target_os = "linux", feature = "_uring"))] uring, @@ -482,7 +465,6 @@ impl Relay { web_routes, internal_routes, sessions, - #[cfg(feature = "_quic")] workers, #[cfg(all(target_os = "linux", feature = "_uring"))] uring, @@ -548,13 +530,11 @@ impl Relay { // reports the outcome. The group owns those threads and ends serving // when the first member finishes, so it has to outlive the loop below // and is torn down after it. - #[cfg(feature = "_quic")] let mut workers = workers.map(|workers| workers.split()); // Pends forever with no workers, so it composes into the `select!` either // way. A worker only stops on error or on shutdown, so the first one to // finish ends the relay the same way the shared accept loop does. - #[cfg(feature = "_quic")] let quic_workers = { let mut running = futures::stream::FuturesUnordered::new(); if let Some(workers) = workers.as_mut() { @@ -582,15 +562,7 @@ impl Relay { } } }; - // No group to run, so this leg never resolves and the shared accept loop - // below decides the `select!` on its own. - #[cfg(not(feature = "_quic"))] - let quic_workers = std::future::pending::>(); - - #[cfg(feature = "_quic")] let has_workers = workers.is_some(); - #[cfg(not(feature = "_quic"))] - let has_workers = false; // With the QUIC listener owned by a worker group, the shared server has // only the `tcp`/`unix` stream listeners left, and a config with none @@ -636,7 +608,6 @@ impl Relay { // Explicitly, so the joins land on the blocking pool rather than on the // executor thread this future happens to be running on. - #[cfg(feature = "_quic")] if let Some(workers) = workers { workers.shutdown().await; } @@ -704,7 +675,6 @@ async fn shutdown_signal() -> anyhow::Result<()> { /// /// The accept loop for a single [`moq_tokio::Server`]. [`Relay::run`] owns /// worker selection and shutdown for embedders. -#[cfg(feature = "_quic")] async fn serve( server: moq_tokio::Server, cluster: cluster::Cluster, @@ -740,20 +710,3 @@ async fn serve_listening( anyhow::bail!("stopped accepting connections") } - -#[cfg(all(test, not(feature = "_quic")))] -mod tests { - #[tokio::test] - async fn workers_require_a_quic_backend() { - let mut config = crate::Config::default(); - config.runtime.workers = Some(1); - let error = match super::Relay::load(config).await { - Ok(_) => panic!("workers accepted without a QUIC backend"), - Err(error) => error, - }; - assert_eq!( - error.to_string(), - "runtime.workers needs moq-relay built with the `noq` feature" - ); - } -} diff --git a/rs/moq-relay/tests/drills.rs b/rs/moq-relay/tests/drills.rs index 6ba7488364..11cc709cf5 100644 --- a/rs/moq-relay/tests/drills.rs +++ b/rs/moq-relay/tests/drills.rs @@ -21,7 +21,7 @@ //! See `test/drill/README.md` for the recipe, the sensitivity proof that each //! drill fails when its recovery behavior is removed, and the loom/fuzz cases //! covering the primitives underneath. -#![cfg(all(feature = "noq", feature = "websocket"))] +#![cfg(feature = "websocket")] use std::time::Duration; diff --git a/rs/moq-relay/tests/embed.rs b/rs/moq-relay/tests/embed.rs index cc7055aae9..52407e006e 100644 --- a/rs/moq-relay/tests/embed.rs +++ b/rs/moq-relay/tests/embed.rs @@ -7,8 +7,6 @@ //! live QUIC subscriber, then stops the owner through its shutdown trigger //! and proves `run` returned with its sockets closed. -#![cfg(feature = "_quic")] - use std::net::{SocketAddr, TcpListener}; use std::time::Duration; diff --git a/rs/moq-relay/tests/runtime_workers.rs b/rs/moq-relay/tests/runtime_workers.rs index 97ce387de3..c9988dff7a 100644 --- a/rs/moq-relay/tests/runtime_workers.rs +++ b/rs/moq-relay/tests/runtime_workers.rs @@ -4,7 +4,7 @@ //! `SO_REUSEPORT`, and no other platform load-balances a unicast UDP port //! across the group. There is also nothing to serve without a QUIC backend, so //! a build without one refuses `runtime.workers` at load. -#![cfg(all(target_os = "linux", feature = "_quic"))] +#![cfg(target_os = "linux")] use std::time::Duration; diff --git a/rs/moq-rtmp/Cargo.toml b/rs/moq-rtmp/Cargo.toml index a94613dbe3..d34340412e 100644 --- a/rs/moq-rtmp/Cargo.toml +++ b/rs/moq-rtmp/Cargo.toml @@ -56,6 +56,6 @@ tracing = { workspace = true } # The RTMPS test borrows moq-tokio's self-signed cert generation to build a # `rustls::ServerConfig`. No transport feature: a QUIC backend here would be # unified into every other crate a `--workspace` build selects, which is exactly -# what hid moq-tokio's backend-less build from the nightly feature matrix. +# what would hide a missing provider from the isolated feature matrix. moq-tokio = { path = "../moq-tokio", default-features = false, features = ["aws-lc-rs"] } tokio = { workspace = true, features = ["test-util"] } diff --git a/rs/moq-tokio/Cargo.toml b/rs/moq-tokio/Cargo.toml index 4e1042c518..1bcc5ac68b 100644 --- a/rs/moq-tokio/Cargo.toml +++ b/rs/moq-tokio/Cargo.toml @@ -13,45 +13,25 @@ keywords = ["quic", "http3", "webtransport", "media", "live"] categories = ["multimedia", "network-programming", "web-programming"] [features] -default = ["noq", "aws-lc-rs", "websocket", "tcp", "uds"] -noq = ["_transport", "_certs", "dep:web-transport-moq", "dep:reqwest", "watch", "dep:dns-lookup"] -# Server-side certificate handling: chains loaded from disk, self-signed generation, -# in-memory identities, and per-SNI selection. Not one to enable on its own, since -# nothing turns it into a listener: every QUIC backend needs it for its own TLS, and -# so does the plain-TLS `tls::Listen::server_config` an RTMPS or HTTPS listener builds, -# which is why the crypto-provider features below turn it on too. -_certs = ["dep:rcgen", "dep:rustls-webpki"] -# Private "any transport is compiled" marker. Not one to enable on its own: every -# backend that can move bytes turns it on, and `uds` reaches it through `tcp`. -_transport = [] -# Filesystem watcher for hot-reloading on-disk TLS certs, keys, and root CAs. +default = ["aws-lc-rs", "watch", "websocket", "tcp", "uds"] +# Filesystem watcher for hot-reloading TLS certificates, keys, and root CAs. watch = ["dep:notify"] -# Capture qlog traces (`--*-quic-qlog `). Off by default so production -# builds do not pull in the qlog crate and its serde_json encoder. -qlog = ["web-transport-moq?/qlog"] -# The QUIC backend pulls in its crypto provider from these features (default-features -# are off on the dependency), so a backend without aws-lc-rs/ring has no initial cipher. -# A provider is also all it takes to sign and serve certificates, so each turns on -# `_certs` even where there is no QUIC backend to serve them for. -aws-lc-rs = ["_certs", "rustls/aws-lc-rs", "rcgen/aws_lc_rs", "web-transport-moq?/aws-lc-rs"] -iroh = ["_transport", "dep:web-transport-iroh", "dep:web-transport-proto", "dep:noq-proto"] -# LAN peer discovery over mDNS (the `mdns` module). +qlog = ["web-transport-quinn/qlog"] +aws-lc-rs = ["rustls/aws-lc-rs", "rcgen/aws_lc_rs", "web-transport-quinn/aws-lc-rs"] +ring = ["rustls/ring", "rcgen/ring", "web-transport-quinn/ring"] +iroh = ["dep:web-transport-iroh", "dep:web-transport-proto", "dep:noq-proto"] mdns = ["dep:mdns-sd", "dep:hmac", "dep:sha2", "dep:subtle"] jemalloc = ["dep:tikv-jemallocator", "dep:tikv-jemalloc-ctl"] -websocket = ["_transport", "dep:qmux", "watch", "dep:dns-lookup"] -# Plain-TCP qmux transport (`tcp://`), no TLS. Server-side and client-side. -tcp = ["_transport", "dep:qmux", "dep:dns-lookup"] -# Unix-domain-socket qmux transport (`unix://`), unix-only. Adds peer-credential -# inspection on accept. Pulls in `tcp` (shares qmux's stream transport). +websocket = ["dep:qmux", "watch"] +tcp = ["dep:qmux"] uds = ["tcp", "qmux/uds"] -ring = ["_certs", "rustls/ring", "rcgen/ring", "web-transport-moq?/ring"] android-logcat = ["dep:tracing-android"] [dependencies] bytes = "1" # Family-restricted `getaddrinfo`, so the AAAA and A queries can be issued separately # (RFC 8305 section 3) while still going through the system resolver. -dns-lookup = { version = "4", optional = true } +dns-lookup = { version = "4" } futures = "0.3" hex = { workspace = true } hmac = { workspace = true, optional = true } @@ -67,12 +47,12 @@ noq-proto = { workspace = true, optional = true } notify = { version = "8", optional = true } qmux = { workspace = true, features = ["wss", "tls", "tcp"], optional = true } rand = "0.10.1" -rcgen = { version = "0.14", default-features = false, optional = true } -reqwest = { version = "0.13", default-features = false, optional = true } +rcgen = { version = "0.14", default-features = false } +reqwest = { version = "0.13", default-features = false } rustls = "0.23" # OS-native server-cert verifier for the rustls backend. rustls-platform-verifier = "0.7" -rustls-webpki = { version = "0.103", features = ["aws-lc-rs"], optional = true } +rustls-webpki = { version = "0.103", features = ["aws-lc-rs"] } serde = { version = "1", features = ["derive"] } serde_with = { version = "3", features = ["hex"] } sha2 = { workspace = true, optional = true } @@ -88,8 +68,8 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } url = { version = "2", features = ["serde"] } usage = { workspace = true, features = ["config"] } web-transport-iroh = { workspace = true, optional = true } -web-transport-moq = { workspace = true, optional = true } web-transport-proto = { workspace = true, optional = true } +web-transport-quinn = { workspace = true } web-transport-trait = { workspace = true } x509-parser = "0.18" @@ -110,6 +90,7 @@ libc = { workspace = true } anyhow = "1" bytes = "1" chrono = "0.4" +criterion = { workspace = true } # Use the channel type behind `mdns_sd::Receiver` so the mDNS readiness gate can # be driven with synthetic daemon events. flume = "0.12" @@ -125,3 +106,7 @@ tracing-test = { workspace = true, features = ["no-env-filter"] } # group closed its own without rebinding a port another test could take. [target.'cfg(target_os = "linux")'.dev-dependencies] socket2 = { workspace = true, features = ["all"] } + +[[bench]] +name = "latency" +harness = false diff --git a/rs/moq-tokio/README.md b/rs/moq-tokio/README.md index 4802a6760b..1f8d6b57fe 100644 --- a/rs/moq-tokio/README.md +++ b/rs/moq-tokio/README.md @@ -12,7 +12,7 @@ Tokio-based connection helpers for native [Media over QUIC](https://moq.dev) app Establishes MoQ connections over a few different transports, selectable via cargo features: -- **WebTransport** (HTTP/3) via [noq](https://crates.io/crates/noq) +- **WebTransport** (HTTP/3) via [Quinn](https://crates.io/crates/quinn) - **Raw QUIC** with ALPN negotiation - **WebSocket** as a fallback when QUIC isn't available - **Iroh** P2P (`iroh` feature) @@ -41,3 +41,14 @@ including in one-shot mode. With mixed `Addrs`, the target that establishes the session determines its redirect policy; unused pinned fallbacks do not restrict it. To refresh DNS, resolve and apply your address policy again before creating a new connection. Address filtering belongs to the caller. + +## Latency benchmark + +Measure loopback group delivery over raw QUIC and WebTransport across payload sizes: + +```bash +cargo bench -p moq-tokio --bench latency +``` + +Quinn is always enabled. Iroh and the experimental io\_uring runtime still use noq. +Other transports and diagnostics retain their optional Cargo features. diff --git a/rs/moq-tokio/benches/latency.rs b/rs/moq-tokio/benches/latency.rs new file mode 100644 index 0000000000..98aa8185eb --- /dev/null +++ b/rs/moq-tokio/benches/latency.rs @@ -0,0 +1,111 @@ +//! Loopback group delivery latency through the selected native QUIC backend. + +use criterion::{BenchmarkId, Throughput}; +use criterion::{Criterion, criterion_group, criterion_main}; +use moq_tokio::{moq_net, origin}; + +fn latency(c: &mut Criterion) { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("runtime"); + let mut bench = c.benchmark_group("quic_group_delivery"); + + for scheme in ["moqt", "https"] { + for size in [1200, 64 * 1024, 1024 * 1024] { + bench.throughput(Throughput::Bytes(size as u64)); + bench.bench_with_input(BenchmarkId::new(scheme, size), &size, |b, &size| { + let (track, mut received, client, connection, serving) = runtime.block_on(async { + let publisher = origin::spawn(); + let broadcast = publisher.create_broadcast("latency").expect("broadcast"); + broadcast.announce(Default::default()).expect("announce"); + let track = broadcast.create_track("data", None).expect("track"); + + let mut listen = moq_tokio::listen::Config::default(); + listen.bind = Some("127.0.0.1:0".parse().unwrap()); + listen.tls.generate = vec!["localhost".into()]; + let mut server = listen + .init(Default::default()) + .expect("server") + .listen() + .await + .expect("listen"); + let addr = server.local_addr().expect("address"); + let serving = tokio::spawn(async move { + let session = server + .accept() + .await + .expect("accept") + .with_publisher(&publisher) + .ok() + .await + .expect("handshake"); + let _broadcast = broadcast; + let _ = session.closed().await; + }); + + let subscriber = origin::spawn(); + let consumer = subscriber.consume(); + let mut announcements = consumer.announced(); + let mut connect = moq_tokio::connect::Config::default(); + connect.bind = Some("127.0.0.1:0".parse().unwrap()); + connect.tls.insecure = Some(true); + let client = connect + .init(Default::default()) + .expect("client") + .with_subscriber(subscriber); + let url = url::Url::parse(&format!("{scheme}://localhost:{}", addr.port())).unwrap(); + let connection = client + .clone() + .with_reconnect(false) + .connect(url) + .established() + .await + .expect("connect"); + loop { + match announcements.next().await.expect("announcement") { + moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => { + assert_eq!(route.prefix.as_str(), "latency"); + break; + } + moq_net::announce::Event::Live => continue, + moq_net::announce::Event::End(_) => panic!("broadcast ended"), + } + } + let remote = consumer.request_broadcast("latency").await.expect("remote broadcast"); + let received = remote.track("data").unwrap().subscribe(None).await.expect("subscribe"); + (track, received, client, connection, serving) + }); + let payload = vec![0x5a; size]; + let mut sequence = 0; + + b.iter_custom(|iterations| { + runtime.block_on(async { + let start = std::time::Instant::now(); + for _ in 0..iterations { + let mut group = track.append_group().expect("group"); + // Advance media time so the cache can evict previous groups. + let timestamp = moq_net::Timestamp::from_secs(sequence).expect("timestamp"); + sequence += 1; + group.write_frame(timestamp, &payload).expect("write"); + group.finish().expect("finish"); + let mut group = received.recv_group().await.expect("receive").expect("group"); + let frame = group.read_frame().await.expect("read").expect("frame"); + assert_eq!(frame.payload.len(), size); + } + start.elapsed() + }) + }); + runtime.block_on(async { + drop(connection); + serving.await.expect("server task"); + drop(client); + }); + }); + } + } + bench.finish(); +} + +criterion_group!(benches, latency); +criterion_main!(benches); diff --git a/rs/moq-tokio/src/client.rs b/rs/moq-tokio/src/client.rs index 686fd6c6df..1477b116dc 100644 --- a/rs/moq-tokio/src/client.rs +++ b/rs/moq-tokio/src/client.rs @@ -6,7 +6,7 @@ use crate::connection::Goaway; use crate::{Addrs, Backoff, Connection, Error}; -#[cfg(all(feature = "websocket", feature = "noq"))] +#[cfg(feature = "websocket")] use std::future::Future; use url::Url; @@ -46,18 +46,10 @@ pub struct Client { /// every transport (passed into the QUIC backend's `connect` and used directly for /// raw TCP/UDS qmux and WebSocket). Resolved once in [`Client::new`] so the ALPN list /// can't diverge between transports. - #[cfg(any( - feature = "noq", - feature = "iroh", - feature = "websocket", - feature = "tcp", - feature = "uds" - ))] versions: moq_net::Versions, /// The URL from [`connect.url`](crate::connect::Config::url), dialed by [`Client::publish`] / [`Client::consume`]. connect: Option, /// Deadline for one [`Client::connect`], from [`crate::connect::Config::timeout`]. Zero waits forever. - #[cfg(feature = "_transport")] timeout: std::time::Duration, pub(crate) reconnect: bool, pub(crate) backoff: Backoff, @@ -74,10 +66,8 @@ pub struct Client { #[cfg(feature = "websocket")] tls_host_name: Option, /// Only the TLS-based dials read this; the plaintext qmux transports have none. - #[cfg(any(feature = "noq", feature = "websocket"))] tls: rustls::ClientConfig, - #[cfg(feature = "noq")] - noq: Option, + quic: crate::quinn::Client, #[cfg(feature = "iroh")] iroh: Option, #[cfg(feature = "iroh")] @@ -85,18 +75,7 @@ pub struct Client { } impl Client { - /// Build a client from its config. - /// - /// Errors if no transport feature is compiled in. - #[cfg(not(feature = "_transport"))] - pub fn new(_config: Config) -> crate::Result { - Err(Error::NoBackend( - "no backend compiled; enable noq, iroh, websocket, tcp, or uds feature", - )) - } - /// Build a client from its config, binding the QUIC socket up front. - #[cfg(feature = "_transport")] pub fn new(config: Config) -> crate::Result { let Config { connect: config, quic, .. @@ -112,13 +91,10 @@ impl Client { quic.validate()?; - // Only the rustls-backed transports use this. Iroh and the plaintext - // qmux transports must not require a rustls crypto provider. - #[cfg(any(feature = "noq", feature = "websocket"))] + // Quinn always uses the configured rustls provider. let tls = config.tls.build()?; - #[cfg(feature = "noq")] - let noq = Some(crate::noq::NoqClient::new(&config, &quic)?); + let quic = crate::quinn::Client::new(&config, &quic)?; let versions = config.versions(); // Read before the struct literal below moves fields out of `config`. @@ -133,13 +109,6 @@ impl Client { Ok(Self { moq: moq_net::Client::new().with_versions(versions.clone()), - #[cfg(any( - feature = "noq", - feature = "iroh", - feature = "websocket", - feature = "tcp", - feature = "uds" - ))] versions, connect: config.url, timeout, @@ -154,10 +123,8 @@ impl Client { websocket: config.websocket, #[cfg(feature = "websocket")] tls_host_name, - #[cfg(any(feature = "noq", feature = "websocket"))] tls, - #[cfg(feature = "noq")] - noq, + quic, #[cfg(feature = "iroh")] iroh: None, #[cfg(feature = "iroh")] @@ -258,14 +225,11 @@ impl Client { /// This closes at once, discarding stream data the peer has not acknowledged yet. /// Call [`Connection::close`] on each connection first to deliver it. /// - /// Only the noq endpoint is closed. WebSocket, TCP, and UDS sessions end when their + /// Only the QUIC endpoint is closed. WebSocket, TCP, and UDS sessions end when their /// [`Connection`] is dropped (the kernel closes the socket on exit), and an iroh /// endpoint passed to `with_iroh` is closed by its owner. pub async fn close(self) { - #[cfg(feature = "noq")] - if let Some(noq) = self.noq { - noq.close().await; - } + self.quic.close().await; } /// Connect to the configured [`connect.url`](crate::connect::Config::url) URL, publishing @@ -291,35 +255,12 @@ impl Client { Some(self.with_subscriber(origin).connect(url)) } - /// Dial the given URL and complete the MoQ handshake. - /// - /// Errors if no transport feature is compiled in. - #[cfg(not(any( - feature = "noq", - feature = "iroh", - feature = "websocket", - feature = "tcp", - feature = "uds" - )))] - pub(crate) async fn dial(&self, _addr: crate::connect::Addr) -> crate::Result { - Err(Error::NoBackend( - "no backend compiled; enable noq, iroh, websocket, tcp, or uds feature", - )) - } - /// Dial the given URL and complete the MoQ handshake. /// /// The scheme picks the transport, and `https://` races QUIC against the /// WebSocket fallback so a blocked UDP path still connects. The session's /// protocol driver is spawned on the current tokio runtime; the session /// closes once the last returned handle drops. - #[cfg(any( - feature = "noq", - feature = "iroh", - feature = "websocket", - feature = "tcp", - feature = "uds" - ))] pub(crate) async fn dial(&self, addr: crate::connect::Addr) -> crate::Result { // Each compiled backend adds state to this dispatch future. Keep it off the // caller's stack so all-feature builds remain safe on standard 2 MiB threads. @@ -341,13 +282,6 @@ impl Client { } /// The moq client builder, advertising `path` in the SETUP when there is one. - #[cfg(any( - feature = "noq", - feature = "iroh", - feature = "websocket", - feature = "tcp", - feature = "uds" - ))] fn moq_with_path(&self, path: Option) -> moq_net::Client { match path { Some(path) => self.moq.clone().with_path(path), @@ -355,13 +289,6 @@ impl Client { } } - #[cfg(any( - feature = "noq", - feature = "iroh", - feature = "websocket", - feature = "tcp", - feature = "uds" - ))] async fn connect_inner(&self, addr: crate::connect::Addr) -> crate::Result { let url = addr.url().clone(); // Transports with no request URI of their own advertise the request target in the @@ -418,34 +345,24 @@ impl Client { return Ok(connect_session(&moq, crate::transport::Session::new(session)).await?); } - #[cfg(feature = "noq")] - if let Some(noq) = self.noq.as_ref() { - let tls = self.tls.clone(); - let quic_addr = addr.clone(); - let quic_handle = async { - noq.connect(&tls, quic_addr, &self.versions) - .await - .map(crate::transport::Session::new) - .map_err(Error::from) - }; - - #[cfg(feature = "websocket")] - { - return self.race_moq_connect(&moq, addr, quic_handle).await; - } - - #[cfg(not(feature = "websocket"))] - { - let session = quic_handle.await?; - return Ok(connect_session(&moq, session).await?); - } - } + let tls = self.tls.clone(); + let quic_addr = addr.clone(); + let quic_handle = async { + self.quic + .connect(&tls, quic_addr, &self.versions) + .await + .map(crate::transport::Session::new) + .map_err(Error::from) + }; #[cfg(feature = "websocket")] - return self.connect_websocket(addr).await; + return self.race_moq_connect(&moq, addr, quic_handle).await; #[cfg(not(feature = "websocket"))] - return Err(Error::NoBackend("no QUIC backend matched; this should not happen")); + { + let session = quic_handle.await?; + Ok(connect_session(&moq, session).await?) + } } /// Connect over WebSocket alone. qmux over WebSocket carries the path in its request @@ -463,10 +380,7 @@ impl Client { /// `moq` is the QUIC-side builder, which carries the SETUP path for a raw QUIC dial. /// The WebSocket fallback uses the plain builder: qmux over WebSocket carries the /// path in its request URI, so repeating it in the SETUP is a protocol violation. - /// - /// Only compiled when there is a QUIC dial to race: a WebSocket-only build connects - /// over the fallback directly. - #[cfg(all(feature = "websocket", feature = "noq"))] + #[cfg(feature = "websocket")] async fn race_moq_connect( &self, moq: &moq_net::Client, @@ -502,13 +416,6 @@ impl Client { /// /// `None` when the result is empty, which means the same as omitting the parameter: the /// server's default path. A peer on published lite-05 rejects an empty value outright. -#[cfg(any( - feature = "noq", - feature = "iroh", - feature = "websocket", - feature = "tcp", - feature = "uds" -))] fn request_target(url: &Url) -> Option { // A trailing `?` parses as an empty query, which is not a query: appending it would // spell one target two ways, and `moqt://host?` would yield a bare "?" rather than @@ -528,13 +435,6 @@ fn request_target(url: &Url) -> Option { /// on top of it is a protocol violation. `iroh` is `None` here because its binding is /// picked by ALPN negotiation rather than by the scheme; that dial reads the negotiated /// [`crate::iroh::Binding`] and calls [`request_target`] itself. -#[cfg(any( - feature = "noq", - feature = "iroh", - feature = "websocket", - feature = "tcp", - feature = "uds" -))] fn setup_path(url: &Url) -> Option { match url.scheme() { // A Unix socket URL's path is the socket file, so the request target rides in @@ -557,13 +457,6 @@ fn setup_path(url: &Url) -> Option { /// A raw-QUIC `moqt://` client MUST send it (draft-ietf-moq-transport-21, 9.1.1). Built /// from the host and port so URL userinfo never goes on the wire. `None` for every other /// scheme, and when the URL has no host, since an empty authority is meaningless. -#[cfg(any( - feature = "noq", - feature = "iroh", - feature = "websocket", - feature = "tcp", - feature = "uds" -))] fn setup_authority(url: &Url) -> Option { if url.scheme() != "moqt" { return None; @@ -576,14 +469,14 @@ fn setup_authority(url: &Url) -> Option { }) } -#[cfg(all(feature = "websocket", feature = "noq"))] +#[cfg(feature = "websocket")] #[derive(Debug, PartialEq, Eq)] enum TransportRace { Quic(Q), WebSocket(W), } -#[cfg(all(feature = "websocket", feature = "noq"))] +#[cfg(feature = "websocket")] async fn race_transport_connect(quic: Q, websocket: W) -> crate::Result> where Q: Future>, @@ -647,13 +540,6 @@ where } } -#[cfg(any( - feature = "noq", - feature = "iroh", - feature = "websocket", - feature = "tcp", - all(feature = "uds", unix) -))] async fn connect_session( client: &moq_net::Client, transport: S, @@ -670,25 +556,21 @@ async fn connect_session( mod tests { use super::*; - #[cfg(feature = "noq")] #[tokio::test] async fn fixed_target_preserves_request_and_refuses_redirect() { check_fixed_redirect(false, true).await; } - #[cfg(feature = "noq")] #[tokio::test] async fn one_shot_fixed_target_refuses_redirect() { check_fixed_redirect(true, true).await; } - #[cfg(feature = "noq")] #[tokio::test] async fn unused_fixed_fallback_does_not_restrict_connected_target() { check_fixed_redirect(true, false).await; } - #[cfg(feature = "noq")] async fn check_fixed_redirect(once: bool, pinned: bool) { let mut listen = crate::listen::Config { bind: Some("127.0.0.1:0".parse().unwrap()), @@ -764,13 +646,6 @@ mod tests { } } - #[cfg(any( - feature = "noq", - feature = "iroh", - feature = "websocket", - feature = "tcp", - feature = "uds" - ))] #[test] fn setup_path_covers_the_uri_less_transports() { // An empty path and an absent one both mean the server's default, so we send @@ -813,13 +688,6 @@ mod tests { } } - #[cfg(any( - feature = "noq", - feature = "iroh", - feature = "websocket", - feature = "tcp", - feature = "uds" - ))] #[test] fn setup_authority_is_only_for_raw_quic_moqt() { let cases = [ @@ -852,13 +720,6 @@ mod tests { /// The iroh dial derives its target here rather than through [`setup_path`], since /// only the negotiated binding says whether to send one. - #[cfg(any( - feature = "noq", - feature = "iroh", - feature = "websocket", - feature = "tcp", - feature = "uds" - ))] #[test] fn request_target_joins_the_path_and_query() { const PEER: &str = "k5lnrlndqpqcgh4d5nhbnbnhcyrgvw6ttxwrsvsu4nlt6foorxaa"; @@ -1228,7 +1089,7 @@ mod tests { assert_eq!(config.versions().alpns().len(), moq_net::ALPNS.len()); } - #[cfg(all(feature = "websocket", feature = "noq"))] + #[cfg(feature = "websocket")] #[tokio::test] async fn race_transport_connect_keeps_websocket_after_quic_auth_error() { let quic = async { Err::(crate::ConnectError::Unauthorized.into()) }; @@ -1242,7 +1103,7 @@ mod tests { assert_eq!(value, super::TransportRace::WebSocket(1)); } - #[cfg(all(feature = "websocket", feature = "noq"))] + #[cfg(feature = "websocket")] #[tokio::test] async fn race_transport_connect_keeps_quic_after_websocket_forbidden() { let quic = async { @@ -1262,7 +1123,7 @@ mod tests { /// Inline rather than in `tests/` so each arm dials its own ephemeral port: the /// public connect sends the fallback to the QUIC port, which nothing reserves over /// TCP as well. - #[cfg(all(feature = "websocket", feature = "noq"))] + #[cfg(feature = "websocket")] #[tokio::test] async fn websocket_forbidden_does_not_end_a_quic_connect() { use tokio::io::{AsyncReadExt, AsyncWriteExt}; @@ -1304,13 +1165,13 @@ mod tests { // The same race `connect_inner` runs, except the fallback dials its own port, // as plain ws:// so the listener above can answer without TLS. - let noq = client.noq.as_ref().unwrap(); + let quic = &client.quic; let quic_addr: crate::connect::Addr = Url::parse(&format!("https://localhost:{quic_port}")).unwrap().into(); let ws_addr: crate::connect::Addr = Url::parse(&format!("http://localhost:{ws_port}")).unwrap().into(); // Hold QUIC until the fallback has been refused, so the 403 is always exercised. let quic = async { (&mut forbid).await.unwrap().expect("fallback listener failed"); - noq.connect(&client.tls, quic_addr, &client.versions) + quic.connect(&client.tls, quic_addr, &client.versions) .await .map(crate::transport::Session::new) .map_err(Error::from) @@ -1328,7 +1189,7 @@ mod tests { accepted.await.unwrap().expect("server handshake failed"); } - #[cfg(all(feature = "websocket", feature = "noq"))] + #[cfg(feature = "websocket")] #[tokio::test] async fn race_transport_connect_reports_auth_when_both_refuse() { let quic = async { Err::(crate::ConnectError::Unauthorized.into()) }; @@ -1338,7 +1199,7 @@ mod tests { assert!(err.is_auth(), "unexpected error: {err}"); } - #[cfg(all(feature = "websocket", feature = "noq"))] + #[cfg(feature = "websocket")] #[tokio::test] async fn race_transport_connect_reports_quic_error_when_websocket_forbidden() { let quic = async { @@ -1352,7 +1213,7 @@ mod tests { assert!(!err.is_auth(), "mixed auth/non-auth must stay retryable: {err}"); } - #[cfg(all(feature = "websocket", feature = "noq"))] + #[cfg(feature = "websocket")] #[tokio::test] async fn race_transport_connect_keeps_websocket_after_quic_non_auth_error() { let quic = async { Err::(Error::ConnectFailed) }; @@ -1362,7 +1223,7 @@ mod tests { assert_eq!(value, super::TransportRace::WebSocket(7)); } - #[cfg(all(feature = "websocket", feature = "noq"))] + #[cfg(feature = "websocket")] #[tokio::test] async fn race_transport_connect_returns_when_quic_transport_connects() { let quic = async { Ok("quic") }; @@ -1387,14 +1248,6 @@ mod tests { assert_eq!(config.resolve().race, std::time::Duration::from_millis(250)); } - /// Iroh carries no rustls state, so constructing its client must not require an - /// application-installed crypto provider. - #[cfg(all(feature = "iroh", not(any(feature = "noq", feature = "websocket"))))] - #[test] - fn iroh_only_client_does_not_require_a_tls_provider() { - crate::connect::Config::default().init().expect("iroh-only client"); - } - #[test] fn connect_timeout_defaults_to_thirty_seconds() { let config = Cli::config_from(["test"]); diff --git a/rs/moq-tokio/src/connect.rs b/rs/moq-tokio/src/connect.rs index 3dbbe6a889..20b095bfcc 100644 --- a/rs/moq-tokio/src/connect.rs +++ b/rs/moq-tokio/src/connect.rs @@ -341,7 +341,6 @@ pub enum ConnectError { impl ConnectError { /// Only the transports that carry an HTTP status (WebTransport, WebSocket) can /// classify one; qmux over tcp/unix has no such response. - #[cfg(any(feature = "noq", feature = "websocket"))] pub(crate) fn from_status_u16(status: u16) -> Option { match status { 401 => Some(Self::Unauthorized), @@ -357,7 +356,7 @@ impl ConnectError { } } -#[cfg(all(test, any(feature = "noq", feature = "websocket")))] +#[cfg(test)] mod tests { use super::*; diff --git a/rs/moq-tokio/src/error.rs b/rs/moq-tokio/src/error.rs index 209306d96a..325c634072 100644 --- a/rs/moq-tokio/src/error.rs +++ b/rs/moq-tokio/src/error.rs @@ -5,8 +5,6 @@ use std::sync::Arc; /// Dependency errors are stored as messages so their crates stay out of this crate's public /// API. Several of them (reqwest above all) keep the useful detail in `source()`, which a /// plain `to_string()` would drop. -// A build with no transport feature compiles no backend module, so nothing calls this. -#[allow(dead_code)] pub(crate) fn message(err: impl std::error::Error) -> String { use std::fmt::Write; @@ -78,7 +76,7 @@ pub enum Error { #[error("failed to initialize Android logcat layer")] Logcat(#[source] Arc), - /// No backend feature is compiled in that can serve this URL. The string names the features to enable. + /// The requested listener is not configured. #[error("{0}")] NoBackend(&'static str), @@ -200,11 +198,6 @@ pub enum Error { #[error(transparent)] Tls(Arc), - /// The noq backend failed. - #[cfg(feature = "noq")] - #[error(transparent)] - Noq(Arc), - /// The Iroh backend failed. #[cfg(feature = "iroh")] #[error(transparent)] @@ -224,6 +217,10 @@ pub enum Error { #[cfg(all(feature = "uds", unix))] #[error(transparent)] Unix(Arc), + + /// The Quinn backend failed. + #[error(transparent)] + Quinn(Arc), } impl Error { @@ -234,8 +231,7 @@ impl Error { Self::MoqNet( moq_net::Error::Unauthorized | moq_net::Error::Session(moq_net::SessionError::Unauthorized), ) => Some(crate::ConnectError::Unauthorized), - #[cfg(feature = "noq")] - Self::Noq(err) => err.connect_error(), + Self::Quinn(err) => err.connect_error(), #[cfg(feature = "websocket")] Self::TransportRace { quic, websocket } => quic.connect_error().or_else(|| websocket.connect_error()), #[cfg(feature = "websocket")] @@ -268,8 +264,7 @@ impl Error { _ => None, }, - #[cfg(feature = "noq")] - Self::Noq(err) => err.status(), + Self::Quinn(err) => err.status(), #[cfg(feature = "websocket")] Self::WebSocket(err) => err.status(), _ => None, @@ -298,14 +293,13 @@ impl From for Error { } } -#[cfg(feature = "noq")] -impl From for Error { - fn from(err: crate::noq::Error) -> Self { +impl From for Error { + fn from(err: crate::quinn::Error) -> Self { if let Some(err) = err.connect_error() { return Self::Connect(err); } - Self::Noq(Arc::new(err)) + Self::Quinn(Arc::new(err)) } } diff --git a/rs/moq-tokio/src/iroh.rs b/rs/moq-tokio/src/iroh.rs index 951260a137..e23bc04f65 100644 --- a/rs/moq-tokio/src/iroh.rs +++ b/rs/moq-tokio/src/iroh.rs @@ -112,7 +112,7 @@ pub enum Error { RecvRequest(String), /// GSO is always on for iroh, so `--quic-gso=false` cannot be honored. - #[error("the iroh backend cannot disable GSO; drop --quic-gso=false or use the noq backend")] + #[error("the iroh backend cannot disable GSO; drop --quic-gso=false or use the Quinn backend")] GsoUnsupported, } @@ -516,7 +516,7 @@ mod tests { /// Build a controller from each family's factory and downcast it to the /// concrete implementation it must map to. iroh runs on noq, so this is the - /// same pairing as the noq backend. + /// pairing selected above. #[test] fn congestion_factory_maps_each_family() { let now = std::time::Instant::now(); diff --git a/rs/moq-tokio/src/lib.rs b/rs/moq-tokio/src/lib.rs index f520b86565..5f573dff55 100644 --- a/rs/moq-tokio/src/lib.rs +++ b/rs/moq-tokio/src/lib.rs @@ -11,18 +11,14 @@ //! See [`Client`] for connecting to relays and [`Server`] for accepting //! connections. The `mdns` feature finds peers to connect to on the local network. //! -//! With `default-features = false`, the `noq` backend must be paired -//! with the `aws-lc-rs` or `ring` crypto-provider feature. Every other subset -//! compiles, including no transport at all: such a build cannot connect to -//! anything, though a crypto provider on its own is still enough to configure TLS -//! and to build a plain-TLS listener's `rustls::ServerConfig` from -//! [`tls::Listen::server_config`]. +//! Quinn handles native QUIC connections. A build with default features disabled +//! must select either the `aws-lc-rs` or `ring` crypto provider. #![warn(missing_docs)] // The protocol crates need a compiled provider for reset and retry-token keys. A rustls provider // installed at runtime cannot supply constructors removed by their compile-time feature gates. -#[cfg(all(feature = "noq", not(any(feature = "aws-lc-rs", feature = "ring"))))] +#[cfg(not(any(feature = "aws-lc-rs", feature = "ring")))] compile_error!("a rustls QUIC backend requires a crypto provider: enable either the `aws-lc-rs` or `ring` feature"); mod abort; @@ -34,19 +30,17 @@ pub mod connect; pub mod connection; pub mod crypto; mod error; -#[cfg(any(feature = "noq", feature = "tcp", feature = "websocket"))] pub mod failover; #[cfg(feature = "jemalloc")] pub mod jemalloc; pub mod listen; mod log; -#[cfg(feature = "noq")] -pub mod noq; pub mod origin; pub mod quic; -#[cfg(any(feature = "noq", feature = "tcp", feature = "websocket"))] +/// The Quinn QUIC backend for WebTransport and raw QUIC. +pub mod quinn; + mod resolve; -#[cfg(feature = "_transport")] pub mod server; #[doc(hidden)] pub mod settings; @@ -59,7 +53,6 @@ pub mod unix; pub mod worker; // Resolving a `host:port` bind string is a QUIC-listener concern; the stream // listeners take a `SocketAddr`/path straight from their config. -#[cfg(feature = "noq")] mod util; #[cfg(feature = "watch")] pub mod watch; @@ -73,7 +66,6 @@ pub use connect::{Addrs, ConnectError}; pub use connection::{Backoff, Connection, Redirect, Status}; pub use error::{Error, Result}; pub use log::{Log, RedactedUrl}; -#[cfg(feature = "_transport")] pub use server::{Listener, Server}; // Re-export these crates. diff --git a/rs/moq-tokio/src/listen.rs b/rs/moq-tokio/src/listen.rs index 2d2cea2e92..2fca050eb3 100644 --- a/rs/moq-tokio/src/listen.rs +++ b/rs/moq-tokio/src/listen.rs @@ -14,7 +14,6 @@ pub enum Bind { } impl Bind { - #[cfg(feature = "noq")] pub(crate) fn resolve(&self) -> std::io::Result { match self { Self::Addr(addr) => Ok(*addr), @@ -155,7 +154,7 @@ pub struct Config { /// shortly after the handshake completes. Typical use: handshake on an /// anycast IP, steady-state on this host's unicast IP. /// - /// Honored by noq. Accept-only, which is why it lives + /// Honored by Quinn. Accept-only, which is why it lives /// here rather than in the shared [`crate::quic::Config`]. #[usage( name = "listen-preferred-v4", @@ -222,13 +221,11 @@ pub struct Config { pub quic: Option, } -#[cfg(feature = "noq")] pub(crate) use moq_sock::shard::Shard; /// One server's socket in a complete `SO_REUSEPORT` group. /// /// Crate-private on purpose: [`crate::worker::Workers`] is the only thing that /// forms a group here, and callers do not need its raw serving handles. -#[cfg(feature = "_transport")] pub(crate) use moq_sock::shard::Socket; /// The `--server-*` flags from before the accept side was named `listen`. @@ -371,12 +368,10 @@ impl Config { found } - #[cfg(feature = "_transport")] pub(crate) fn validate(&self) -> crate::Result<()> { Ok(()) } - #[cfg(feature = "noq")] /// Return the effective QUIC-LB connection-ID encoding. pub fn load_balancer(&self) -> Option { self.lb_id @@ -500,7 +495,6 @@ mod tests { assert!("relay.example.com:443:8443".parse::().is_err()); } - #[cfg(feature = "noq")] #[test] fn cli_load_balancer_survives_the_merge_round_trip() { let config = config_from(["test", "--listen-quic-lb-id", "ab", "--listen-quic-lb-nonce", "9"]); @@ -516,7 +510,6 @@ mod tests { } /// Programmatic configuration keeps the QUIC-LB id and nonce paired. - #[cfg(feature = "noq")] #[test] fn load_balancer_is_a_single_typed_value() { assert_eq!(Config::default().load_balancer(), None); diff --git a/rs/moq-tokio/src/log.rs b/rs/moq-tokio/src/log.rs index 2e47016ade..b3de382b8a 100644 --- a/rs/moq-tokio/src/log.rs +++ b/rs/moq-tokio/src/log.rs @@ -54,6 +54,7 @@ impl Log { .with_default_directive(self.level().into()) // Default to our -q/-v args .from_env_lossy() // Allow overriding with RUST_LOG .add_directive("h2=warn".parse()?) + .add_directive("quinn=info".parse()?) .add_directive("noq=info".parse()?) .add_directive("tungstenite=info".parse()?) .add_directive("rustls=info".parse()?) diff --git a/rs/moq-tokio/src/quic.rs b/rs/moq-tokio/src/quic.rs index 8d61d6f2d6..7571d1da19 100644 --- a/rs/moq-tokio/src/quic.rs +++ b/rs/moq-tokio/src/quic.rs @@ -55,7 +55,7 @@ pub struct LoadBalancer { /// The congestion control family for a QUIC connection. /// -/// Noq uses CUBIC for loss-based control and BBRv3 for delay-based control. +/// Quinn and Iroh use CUBIC for loss-based control; Quinn uses BBRv1 and Iroh uses BBRv3 for delay-based control. #[derive(Clone, Copy, Debug, PartialEq, Eq, usage::ValueEnum, serde::Serialize, serde::Deserialize)] #[usage(ignore_case)] #[serde(rename_all = "kebab-case")] @@ -222,7 +222,7 @@ pub struct Config { /// Write qlog traces into this directory, which must already exist. /// - /// Noq writes one file per connection. + /// Quinn writes one file per endpoint. /// /// Requires the `qlog` feature; setting it errors at init otherwise. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -482,7 +482,6 @@ impl Config { } /// Reject knobs this build can't honor. Called when the endpoint is built. - #[cfg(feature = "_transport")] pub(crate) fn validate(&self) -> crate::Result<()> { // Erroring beats silently ignoring the flag: the operator asked for traces and // would otherwise go looking for files that were never going to appear. @@ -523,14 +522,12 @@ impl Config { /// QUIC carries `max_idle_timeout` as a varint of milliseconds, so anything past this /// can't go on the wire. -#[cfg(feature = "_transport")] const MAX_IDLE_TIMEOUT: Duration = Duration::from_millis((1 << 62) - 1); /// Reject an idle timeout no QUIC connection can express. /// /// Checked here rather than in each backend because the conversion the backends do is /// infallible-by-panic, and this config reaches them from a TOML file or a C caller. -#[cfg(feature = "_transport")] fn validate_idle_timeout(idle_timeout: Option) -> crate::Result<()> { match idle_timeout { Some(timeout) if timeout > MAX_IDLE_TIMEOUT => Err(crate::Error::IdleTimeoutRange), @@ -540,7 +537,6 @@ fn validate_idle_timeout(idle_timeout: Option) -> crate::Result<()> { /// The largest value a QUIC varint can carry, which bounds every flow-control /// limit that goes on the wire as a transport parameter. -#[cfg(feature = "_transport")] const MAX_VARINT: u64 = (1 << 62) - 1; /// Reject a flow-control window no connection can honor. @@ -549,7 +545,6 @@ const MAX_VARINT: u64 = (1 << 62) - 1; /// the connection rather than tighten it. The receive windows additionally have to /// fit a varint, since they are sent as transport parameters; the send window is /// local bookkeeping and only has to be non-zero. -#[cfg(feature = "_transport")] fn validate_windows(quic: &Config) -> crate::Result<()> { for (name, window, wire) in [ ("receive_window", quic.receive_window, true), @@ -612,7 +607,6 @@ impl Resolved { /// Delay-based unless the operator says otherwise: BBR keeps queues short and the /// send rate steady enough for a live encoder to track, which is what this stack /// carries. - #[cfg_attr(not(any(feature = "noq", feature = "iroh")), allow(dead_code))] pub(crate) fn congestion(&self) -> CongestionControl { self.congestion_control.unwrap_or(CongestionControl::Delay) } @@ -621,7 +615,6 @@ impl Resolved { /// /// Only meaningful once [`Config::validate`] has passed; a build without the /// `qlog` feature never gets here with a directory set. - #[cfg_attr(not(feature = "noq"), allow(dead_code))] pub(crate) fn qlog_dir(&self) -> Option<&std::path::Path> { self.qlog.as_deref() } @@ -794,7 +787,6 @@ mod tests { /// A build that can't capture must reject the flag rather than ignore it, so an /// operator isn't left waiting on trace files that will never appear. - #[cfg(feature = "_transport")] #[test] fn qlog_requires_the_feature() { let unset = Config::default().validate(); @@ -814,7 +806,6 @@ mod tests { /// The backends convert this duration with an infallible-by-panic `expect`, and the /// value arrives from a TOML file or a C caller, so validation has to catch it here. - #[cfg(feature = "_transport")] #[test] fn idle_timeout_beyond_the_varint_is_rejected() { let over = Config { @@ -859,7 +850,6 @@ mod tests { /// A zero window credits nothing, so it would wedge the connection rather than /// tighten it. Reject it here, where the message can name the knob. - #[cfg(feature = "_transport")] #[test] fn zero_windows_are_rejected() { for (name, config) in [ @@ -892,7 +882,6 @@ mod tests { /// The receive windows go on the wire as transport parameters, so they have to fit /// a varint. The send window is local bookkeeping and does not. - #[cfg(feature = "_transport")] #[test] fn receive_windows_must_fit_a_varint() { let over = Config { diff --git a/rs/moq-tokio/src/noq.rs b/rs/moq-tokio/src/quinn.rs similarity index 76% rename from rs/moq-tokio/src/noq.rs rename to rs/moq-tokio/src/quinn.rs index 15e3692d75..53d91b5754 100644 --- a/rs/moq-tokio/src/noq.rs +++ b/rs/moq-tokio/src/quinn.rs @@ -1,4 +1,4 @@ -//! The noq QUIC backend, used for both WebTransport (`https://`) and raw QUIC (`moqt://`, `moql://`). +//! The quinn QUIC backend, used for both WebTransport (`https://`) and raw QUIC (`moqt://`, `moql://`). use crate::RedactedUrl; use crate::connect; @@ -12,13 +12,15 @@ use std::pin::Pin; use std::sync::{Arc, Mutex, RwLock}; use std::task::{Context, Poll, Waker, ready}; use std::time::Duration; -use web_transport_moq::noq; +pub(crate) use web_transport_quinn::Session; +use web_transport_quinn::quinn; -/// Attach a qlog factory writing into the configured directory, if any. +/// Attach a qlog stream writing into `dir`, if one was configured. /// -/// noq's factory opens one file per connection, named after its initial destination -/// connection ID, so nothing needs to be unique per endpoint here. -fn apply_qlog(transport: &mut noq::TransportConfig, quic: &Resolved, role: &str) -> Result<()> { +/// quinn's [`quinn::QlogStream`] is a shared handle: every connection on the endpoint +/// writes into it, tagged with its own qlog `group_id`. So this is one file per +/// endpoint rather than per connection. +fn apply_qlog(transport: &mut quinn::TransportConfig, quic: &Resolved, role: &str) -> Result<()> { // `Client::validate` already rejected a directory this build can't honor, so the // block below is only reached where capture actually works. let Some(dir) = quic.qlog_dir() else { @@ -27,8 +29,24 @@ fn apply_qlog(transport: &mut noq::TransportConfig, quic: &Resolved, role: &str) #[cfg(feature = "qlog")] { - transport.qlog_from_path(dir, &format!("moq-{role}")); - tracing::info!(dir = %dir.display(), "writing qlog"); + // Workers create several endpoints in one process, so the pid alone + // cannot keep their traces separate. Refuse to overwrite any existing trace. + let path = dir.join(format!( + "moq-{role}-{}-{:032x}.sqlog", + std::process::id(), + rand::random::() + )); + let file = std::fs::File::create_new(&path).map_err(Error::CreateQlog)?; + + // Deliberately unbuffered: qlog's streamer only flushes on `finish_log`, which + // quinn never calls per event, so a BufWriter would hold every trace in memory + // until the endpoint drops and lose the lot if the process is killed. Killing a + // stuck process is exactly when these traces are worth having. + let mut config = quinn::QlogConfig::default(); + config.writer(Box::new(file)).title(Some(format!("moq-tokio {role}"))); + + transport.qlog_stream(config.into_stream()); + tracing::info!(path = %path.display(), "writing qlog"); } #[cfg(not(feature = "qlog"))] @@ -37,17 +55,17 @@ fn apply_qlog(transport: &mut noq::TransportConfig, quic: &Resolved, role: &str) Ok(()) } -/// Apply the resolved quic knobs to a noq transport config. -fn apply_transport(transport: &mut noq::TransportConfig, quic: &Resolved) { +/// Apply the resolved quic knobs to a quinn transport config. +fn apply_transport(transport: &mut quinn::TransportConfig, quic: &Resolved) { transport.max_idle_timeout(Some(quic.idle_timeout.try_into().expect("idle timeout out of range"))); transport.keep_alive_interval(quic.keep_alive); - // noq enables MTU discovery by default; disable it unless asked. + // quinn enables MTU discovery by default; disable it unless asked. if !quic.mtu_discovery { transport.mtu_discovery_config(None); } - let max_streams = noq::VarInt::from_u64(quic.max_streams).unwrap_or(noq::VarInt::MAX); + let max_streams = quinn::VarInt::from_u64(quic.max_streams).unwrap_or(quinn::VarInt::MAX); transport.max_concurrent_bidi_streams(max_streams); transport.max_concurrent_uni_streams(max_streams); @@ -61,29 +79,29 @@ fn apply_transport(transport: &mut noq::TransportConfig, quic: &Resolved) { } /// Apply the flow-control windows, leaving each at the backend default when unset. -fn apply_windows(transport: &mut noq::TransportConfig, quic: &Resolved) { +fn apply_windows(transport: &mut quinn::TransportConfig, quic: &Resolved) { // Saturating rather than erroring: `Config::validate` already rejects a window // past the varint, so this only bites a `Resolved` built without it. if let Some(window) = quic.receive_window { - transport.receive_window(noq::VarInt::from_u64(window).unwrap_or(noq::VarInt::MAX)); + transport.receive_window(quinn::VarInt::from_u64(window).unwrap_or(quinn::VarInt::MAX)); } if let Some(window) = quic.stream_receive_window { - transport.stream_receive_window(noq::VarInt::from_u64(window).unwrap_or(noq::VarInt::MAX)); + transport.stream_receive_window(quinn::VarInt::from_u64(window).unwrap_or(quinn::VarInt::MAX)); } if let Some(window) = quic.send_window { transport.send_window(window); } } -/// The noq controller factory for a congestion control family. noq's BBR is v3. -fn congestion_factory(family: CongestionControl) -> Arc { +/// The quinn controller factory for a congestion control family. Quinn's BBR is v1. +fn congestion_factory(family: CongestionControl) -> Arc { match family { - CongestionControl::Loss => Arc::new(noq::congestion::CubicConfig::default()), - CongestionControl::Delay => Arc::new(noq::congestion::Bbr3Config::default()), + CongestionControl::Loss => Arc::new(quinn::congestion::CubicConfig::default()), + CongestionControl::Delay => Arc::new(quinn::congestion::BbrConfig::default()), } } -/// Errors specific to the noq QUIC backend. +/// Errors specific to the quinn QUIC backend. #[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum Error { @@ -95,6 +113,10 @@ pub enum Error { #[error("failed to create QUIC endpoint")] CreateEndpoint(#[source] std::io::Error), + /// The qlog trace file could not be created. + #[error("failed to create qlog file")] + CreateQlog(#[source] std::io::Error), + /// No async runtime was found. Construct the client or server from within a tokio runtime. #[error("no async runtime")] NoRuntime, @@ -203,7 +225,7 @@ pub enum Error { message: String, /// The HTTP status the server answered the CONNECT with, when it answered with one. /// - /// Read at conversion time rather than kept as a `web-transport-moq` error, so the + /// Read at conversion time rather than kept as a `web-transport-quinn` error, so the /// classification survives without that crate appearing in this crate's public API. status: Option, }, @@ -251,20 +273,20 @@ impl crate::failover::Aggregate for Error { } crate::error::from_message! { - noq::ConnectError => Connect, - noq::ConnectionError => Connection, - web_transport_moq::ServerError => Server, + quinn::ConnectError => Connect, + quinn::ConnectionError => Connection, + web_transport_quinn::ServerError => Server, hex::FromHexError => InvalidFingerprint, } -impl From for Error { - fn from(_: noq::crypto::rustls::NoInitialCipherSuite) -> Self { +impl From for Error { + fn from(_: quinn::crypto::rustls::NoInitialCipherSuite) -> Self { Self::NoInitialCipherSuite } } -impl From for Error { - fn from(err: web_transport_moq::ClientError) -> Self { +impl From for Error { + fn from(err: web_transport_quinn::ClientError) -> Self { Self::Client { status: client_status(&err), message: crate::error::message(err), @@ -277,9 +299,9 @@ type Result = std::result::Result; // ── Client ────────────────────────────────────────────────────────── #[derive(Clone)] -pub(crate) struct NoqClient { - pub quic: noq::Endpoint, - pub transport: Arc, +pub(crate) struct Client { + pub quic: quinn::Endpoint, + pub transport: Arc, /// Whether an `http://` URL may bootstrap a pin (see [crate::tls::Connect::allows_http_bootstrap]). pub http_bootstrap: bool, /// Optional TLS SNI / verification hostname override (from config). @@ -295,24 +317,24 @@ pub(crate) struct NoqClient { dual_stack: bool, } -impl NoqClient { +impl Client { pub fn new(config: &connect::Config, quic: &crate::quic::Config) -> Result { let resolved = config.resolve(); let socket = crate::bind::udp(crate::bind::Udp::new(resolved.bind)).map_err(Error::BindSocket)?; let dual_stack = crate::bind::udp_is_dual_stack(&socket); - let mut transport = noq::TransportConfig::default(); + let mut transport = quinn::TransportConfig::default(); let quic = quic.resolve(); apply_transport(&mut transport, &quic); apply_qlog(&mut transport, &quic, "client")?; let transport = Arc::new(transport); // There's a bit more boilerplate to make a generic endpoint. - let runtime = noq::default_runtime().ok_or(Error::NoRuntime)?; - let endpoint_config = noq::EndpointConfig::default(); + let runtime = quinn::default_runtime().ok_or(Error::NoRuntime)?; + let endpoint_config = quinn::EndpointConfig::default(); // Create the generic QUIC endpoint. - let quic = noq::Endpoint::new(endpoint_config, None, socket, runtime).map_err(Error::CreateEndpoint)?; + let quic = quinn::Endpoint::new(endpoint_config, None, socket, runtime).map_err(Error::CreateEndpoint)?; Ok(Self { quic, @@ -327,10 +349,10 @@ impl NoqClient { /// Close every connection, then wait until each has sent its close to the peer. pub async fn close(self) { - self.quic.close(noq::VarInt::from_u32(0), b"client shutdown"); - // Not `wait_idle`, which also sits out each connection's 3 PTO closing - // period: that only repeats the close to a peer that already has it. - self.quic.wait_all_draining().await; + self.quic.close(quinn::VarInt::from_u32(0), b"client shutdown"); + // Quinn keeps driving the close through its closing period so a lost + // CONNECTION_CLOSE is retransmitted before the socket is released. + self.quic.wait_idle().await; } pub async fn connect( @@ -338,7 +360,7 @@ impl NoqClient { tls: &rustls::ClientConfig, addr: crate::connect::Addr, versions: &moq_net::Versions, - ) -> Result { + ) -> Result { let mut url = addr.url().clone(); let mut config = tls.clone(); @@ -397,7 +419,7 @@ impl NoqClient { } let alpns: Vec> = match url.scheme() { - "https" => vec![web_transport_moq::ALPN.as_bytes().to_vec()], + "https" => vec![web_transport_quinn::ALPN.as_bytes().to_vec()], "moqt" | "moql" => versions.alpns().iter().map(|alpn| alpn.as_bytes().to_vec()).collect(), _ => return Err(Error::InvalidScheme), }; @@ -405,8 +427,8 @@ impl NoqClient { config.alpn_protocols = alpns; config.key_log = Arc::new(rustls::KeyLogFile::new()); - let config: noq::crypto::rustls::QuicClientConfig = config.try_into()?; - let mut config = noq::ClientConfig::new(Arc::new(config)); + let config: quinn::crypto::rustls::QuicClientConfig = config.try_into()?; + let mut config = quinn::ClientConfig::new(Arc::new(config)); config.transport_config(self.transport.clone()); tracing::debug!(peer = %crate::connect::Endpoint(&url), "connecting"); @@ -428,15 +450,15 @@ impl NoqClient { let session = match url.scheme() { "https" => { - let mut request = web_transport_moq::proto::ConnectRequest::new(url.clone()); + let mut request = web_transport_quinn::proto::ConnectRequest::new(url.clone()); for alpn in versions.alpns() { request = request.with_protocol(alpn.to_string()); } - web_transport_moq::Session::connect(connection, request) + web_transport_quinn::Session::connect(connection, request) .await .map_err(map_client_error)? } - "moqt" | "moql" => web_transport_moq::Session::raw(connection), + "moqt" | "moql" => web_transport_quinn::Session::raw(connection), _ => return Err(Error::UnsupportedScheme(url.scheme().to_string())), }; @@ -482,7 +504,7 @@ impl Error { } } -fn map_client_error(err: web_transport_moq::ClientError) -> Error { +fn map_client_error(err: web_transport_quinn::ClientError) -> Error { match client_status(&err).and_then(crate::ConnectError::from_status_u16) { Some(rejected) => rejected.into(), None => err.into(), @@ -496,49 +518,50 @@ fn map_client_error(err: web_transport_moq::ClientError) -> Error { /// [`crate::ConnectError`], and [`Error::status`] hands it to the caller, whose backoff consults /// the status. A `404` or `405` is the server's settled answer, so retrying /// it just burns the reconnect budget on a URL that will never work. -fn client_status(err: &web_transport_moq::ClientError) -> Option { +fn client_status(err: &web_transport_quinn::ClientError) -> Option { match err { - web_transport_moq::ClientError::HttpError(err) => connect_status(err), + web_transport_quinn::ClientError::HttpError(err) => connect_status(err), _ => None, } } -fn connect_status(err: &web_transport_moq::ConnectError) -> Option { +fn connect_status(err: &web_transport_quinn::ConnectError) -> Option { match err { - web_transport_moq::ConnectError::ErrorStatus(status) => Some(status.as_u16()), - web_transport_moq::ConnectError::ProtoError(err) => proto_status(err), + web_transport_quinn::ConnectError::ErrorStatus(status) => Some(status.as_u16()), + web_transport_quinn::ConnectError::ProtoError(err) => proto_status(err), _ => None, } } -fn proto_status(err: &web_transport_moq::proto::ConnectError) -> Option { +fn proto_status(err: &web_transport_quinn::proto::ConnectError) -> Option { match err { - web_transport_moq::proto::ConnectError::ErrorStatus(status) - | web_transport_moq::proto::ConnectError::WrongStatus(Some(status)) => Some(status.as_u16()), + web_transport_quinn::proto::ConnectError::ErrorStatus(status) + | web_transport_quinn::proto::ConnectError::WrongStatus(Some(status)) => Some(status.as_u16()), _ => None, } } // ── Server ────────────────────────────────────────────────────────── -pub(crate) struct NoqServer { - pub quic: noq::Endpoint, +pub(crate) struct Server { + pub quic: quinn::Endpoint, pub certs: Arc, socket: Socket, + #[cfg(feature = "watch")] _reload: crate::tls::Reload, } -/// The server's UDP socket, which [`NoqServer::shutdown`] closes even while noq +/// The server's UDP socket, which [`Server::shutdown`] closes even while quinn /// still holds references to it. /// -/// noq gives each connection a sender sharing the socket, and a connection lives +/// quinn gives each connection a sender sharing the socket, and a connection lives /// as long as anything holds its handle, so dropping the endpoint leaves the port /// bound until every accepted session is gone too. Taking the socket out of this /// slot closes it regardless. #[derive(Clone, Debug)] struct Socket { io: Arc>>>, - state: Arc, + state: Arc, local: net::SocketAddr, /// Wakes every sender blocked on a full send buffer from the socket's single /// write-readiness registration. @@ -547,7 +570,7 @@ struct Socket { impl Socket { fn new(socket: std::net::UdpSocket) -> std::io::Result { - let state = noq::udp::UdpSocketState::new((&socket).into())?; + let state = quinn::udp::UdpSocketState::new((&socket).into())?; let local = socket.local_addr()?; let io = tokio::net::UdpSocket::from_std(socket)?; Ok(Self { @@ -569,16 +592,28 @@ impl Socket { } } -impl noq::AsyncUdpSocket for Socket { - fn create_sender(&self) -> Pin> { - Box::pin(self.clone()) +impl quinn::AsyncUdpSocket for Socket { + fn create_io_poller(self: Arc) -> Pin> { + Box::pin((*self).clone()) + } + + fn try_send(&self, transmit: &quinn::udp::Transmit<'_>) -> std::io::Result<()> { + let io = self.io.read().unwrap(); + let Some(io) = io.as_deref() else { + return Ok(()); + }; + io.try_io(tokio::io::Interest::WRITABLE, || self.state.send(io.into(), transmit)) + } + + fn max_transmit_segments(&self) -> usize { + self.state.max_gso_segments() } fn poll_recv( - &mut self, + &self, cx: &mut Context<'_>, bufs: &mut [std::io::IoSliceMut<'_>], - meta: &mut [noq::udp::RecvMeta], + meta: &mut [quinn::udp::RecvMeta], ) -> Poll> { let io = self.io.read().unwrap(); // Released: nothing arrives again, and the endpoint stops through its close. @@ -598,7 +633,7 @@ impl noq::AsyncUdpSocket for Socket { Ok(self.local) } - fn max_receive_segments(&self) -> std::num::NonZeroUsize { + fn max_receive_segments(&self) -> usize { self.state.gro_segments() } @@ -607,32 +642,15 @@ impl noq::AsyncUdpSocket for Socket { } } -impl noq::UdpSender for Socket { - fn poll_send( - self: Pin<&mut Self>, - transmit: &noq::udp::Transmit<'_>, - cx: &mut Context<'_>, - ) -> Poll> { +impl quinn::UdpPoller for Socket { + fn poll_writable(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let io = self.io.read().unwrap(); - // Released: the datagram is lost, which UDP always allows. let Some(io) = io.as_deref() else { return Poll::Ready(Ok(())); }; - loop { - match io.try_io(tokio::io::Interest::WRITABLE, || self.state.send(io.into(), transmit)) { - Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {} - res => return Poll::Ready(res), - } - // Every connection sends from its own task, but tokio keeps one waker per - // direction, so register a fan-out to all of them instead of this task's. - self.blocked.push(cx.waker()); - let waker = Waker::from(self.blocked.clone()); - ready!(io.poll_send_ready(&mut Context::from_waker(&waker)))?; - } - } - - fn max_transmit_segments(&self) -> std::num::NonZeroUsize { - self.state.max_gso_segments() + self.blocked.push(cx.waker()); + let waker = Waker::from(self.blocked.clone()); + io.poll_send_ready(&mut Context::from_waker(&waker)) } } @@ -661,9 +679,9 @@ impl std::task::Wake for Blocked { } } -impl NoqServer { +impl Server { pub fn new(config: listen::Config, quic: &crate::quic::Config, member: Option) -> Result { - let mut transport = noq::TransportConfig::default(); + let mut transport = quinn::TransportConfig::default(); let quic = quic.resolve(); apply_transport(&mut transport, &quic); apply_qlog(&mut transport, &quic, "server")?; @@ -693,18 +711,18 @@ impl NoqServer { .iter() .map(|alpn| alpn.as_bytes().to_vec()) .collect(); - alpns.push(web_transport_moq::ALPN.as_bytes().to_vec()); + alpns.push(web_transport_quinn::ALPN.as_bytes().to_vec()); tls.alpn_protocols = alpns; tls.key_log = Arc::new(rustls::KeyLogFile::new()); config.tls.disable_resumption(&mut tls); - let tls: noq::crypto::rustls::QuicServerConfig = tls.try_into()?; - let mut tls = noq::ServerConfig::with_crypto(Arc::new(tls)); + let tls: quinn::crypto::rustls::QuicServerConfig = tls.try_into()?; + let mut tls = quinn::ServerConfig::with_crypto(Arc::new(tls)); tls.transport_config(transport); // Advertise the preferred_address transport parameter (RFC 9000 §9.6). - // noq allocates a fresh CID + reset token for the address during the handshake. + // quinn allocates a fresh CID + reset token for the address during the handshake. if let Some(addr) = config.preferred_v4 { tls.preferred_address_v4(Some(addr)); } @@ -713,7 +731,7 @@ impl NoqServer { } // There's a bit more boilerplate to make a generic endpoint. - let runtime = noq::default_runtime().ok_or(Error::NoRuntime)?; + let runtime = quinn::default_runtime().ok_or(Error::NoRuntime)?; let listen = config .bind @@ -725,7 +743,7 @@ impl NoqServer { let load_balancer = config.load_balancer(); // Configure connection ID generator with server ID if provided - let mut endpoint_config = noq::EndpointConfig::default(); + let mut endpoint_config = quinn::EndpointConfig::default(); if let Some(shard) = member.as_ref().map(listen::Socket::shard) { if load_balancer.is_some() { return Err(Error::ShardWithQuicLb); @@ -735,7 +753,7 @@ impl NoqServer { count = shard.count(), "encoding the shard in connection IDs" ); - endpoint_config.cid_generator(Arc::new(move || Box::new(ShardIdGenerator::new(shard)))); + endpoint_config.cid_generator(move || Box::new(ShardIdGenerator::new(shard))); } else if let Some(load_balancer) = load_balancer { let server_id = load_balancer.id; let nonce_len = load_balancer.nonce; @@ -753,9 +771,7 @@ impl NoqServer { nonce_len, "using QUIC-LB compatible connection ID generation" ); - endpoint_config.cid_generator(Arc::new(move || { - Box::new(ServerIdGenerator::new(server_id.clone(), nonce_len)) - })); + endpoint_config.cid_generator(move || Box::new(ServerIdGenerator::new(server_id.clone(), nonce_len))); } // A group socket was released only after every member bound and the @@ -768,22 +784,24 @@ impl NoqServer { // Create the generic QUIC endpoint. let socket = Socket::new(socket).map_err(Error::CreateEndpoint)?; let quic = - noq::Endpoint::new_with_abstract_socket(endpoint_config, Some(tls), Box::new(socket.clone()), runtime) + quinn::Endpoint::new_with_abstract_socket(endpoint_config, Some(tls), Arc::new(socket.clone()), runtime) .map_err(Error::CreateEndpoint)?; // Spawn the cert reload watcher only after endpoint creation succeeds, // so we don't leave a dangling watcher on failure. + #[cfg(feature = "watch")] let _reload = crate::tls::Reload::spawn(certs.clone(), config.tls.clone()); Ok(Self { quic, certs, socket, + #[cfg(feature = "watch")] _reload, }) } - pub fn accept(&self) -> impl std::future::Future> + '_ { + pub fn accept(&self) -> impl std::future::Future> + '_ { self.quic.accept() } @@ -796,16 +814,16 @@ impl NoqServer { } pub fn close(&self) { - self.quic.close(noq::VarInt::from_u32(0), b"server shutdown"); + self.quic.close(quinn::VarInt::from_u32(0), b"server shutdown"); } /// Close every connection, wait until each has sent its close to the peer, /// then release the socket. pub async fn shutdown(self) { self.close(); - // Not `wait_idle`, which also sits out each connection's 3 PTO closing - // period: that only repeats the close to a peer that already has it. - self.quic.wait_all_draining().await; + // Quinn keeps driving the close through its closing period so a lost + // CONNECTION_CLOSE is retransmitted before the socket is released. + self.quic.wait_idle().await; self.socket.release(); } @@ -815,32 +833,31 @@ impl NoqServer { } } -// ── NoqRequest ────────────────────────────────────────────────────── +// ── QuinnRequest ────────────────────────────────────────────────────── -/// A raw QUIC connection request without WebTransport framing (noq backend). +/// A raw QUIC connection request without WebTransport framing (quinn backend). /// Accept a QUIC connection, negotiate WebTransport or raw moq, and complete the /// handshake (a `200 OK` for WebTransport). Returns the established session, the request /// URL and validated mTLS identity (both captured before the response consumes the /// request), and the dialed authority (the CONNECT authority on WebTransport, the TLS /// SNI on raw QUIC). Raw QUIC carries no request URL (the path rides the SETUP instead). pub(crate) async fn accept( - conn: noq::Incoming, + conn: quinn::Incoming, alpns: Vec<&'static str>, -) -> Result> { +) -> Result> { let mut conn = conn.accept()?; let handshake = conn .handshake_data() .await? - .downcast::() + .downcast::() .unwrap(); let alpn = handshake.protocol.ok_or(Error::MissingAlpn)?; let alpn = String::from_utf8(alpn)?; let host = handshake.server_name.unwrap_or_default(); - // The established Connection no longer exposes a single peer address (noq 1.0 - // supports multipath), so capture it from the Connecting before awaiting. + // Capture the peer address alongside its handshake metadata. let remote = conn.remote_address(); tracing::debug!(%host, ip = %remote, %alpn, "accepting"); @@ -859,10 +876,10 @@ pub(crate) async fn accept( }; match alpn.as_str() { - web_transport_moq::ALPN => { + web_transport_quinn::ALPN => { // Wait for the CONNECT request, then capture its URL and mTLS identity before // the response consumes it. - let request = web_transport_moq::Request::accept(conn) + let request = web_transport_quinn::Request::accept(conn) .await .map_err(|err| Error::RecvRequest(crate::error::message(err)))?; let url = Some(request.url.clone()); @@ -870,7 +887,7 @@ pub(crate) async fn accept( // The authority the client put in its CONNECT URL. let authority = request.url.host_str().filter(|h| !h.is_empty()).map(str::to_owned); - let mut response = web_transport_moq::proto::ConnectResponse::OK; + let mut response = web_transport_quinn::proto::ConnectResponse::OK; let mut link = link; if let Some(protocol) = request.protocols.iter().find(|p| alpns.contains(&p.as_str())) { response = response.with_protocol(protocol); @@ -897,7 +914,7 @@ pub(crate) async fn accept( // Raw QUIC carries no request URL; the path rides the SETUP. The TLS SNI is the // only authority the client can offer here, and it is optional. let authority = (!host.is_empty()).then_some(host); - let session = web_transport_moq::Session::raw(conn); + let session = web_transport_quinn::Session::raw(conn); Ok(crate::server::Accepted { session, url: None, @@ -930,13 +947,13 @@ impl ShardIdGenerator { } } -impl noq::ConnectionIdGenerator for ShardIdGenerator { - fn generate_cid(&mut self) -> noq::ConnectionId { +impl quinn::ConnectionIdGenerator for ShardIdGenerator { + fn generate_cid(&mut self) -> quinn::ConnectionId { use rand::RngExt; let mut cid = Vec::with_capacity(Self::LEN); cid.push(moq_sock::shard::cid_prefix(self.shard)); cid.extend(rand::rng().random_iter::().take(Self::LEN - 1)); - noq::ConnectionId::new(cid.as_slice()) + quinn::ConnectionId::new(cid.as_slice()) } fn cid_len(&self) -> usize { @@ -961,8 +978,8 @@ impl ServerIdGenerator { } } -impl noq::ConnectionIdGenerator for ServerIdGenerator { - fn generate_cid(&mut self) -> noq::ConnectionId { +impl quinn::ConnectionIdGenerator for ServerIdGenerator { + fn generate_cid(&mut self) -> quinn::ConnectionId { use rand::RngExt; let cid_len = self.cid_len(); let mut cid = Vec::with_capacity(cid_len); @@ -970,7 +987,7 @@ impl noq::ConnectionIdGenerator for ServerIdGenerator { cid.push((cid_len - 1) as u8); cid.extend(self.server_id.0.iter()); cid.extend(rand::rng().random_iter::().take(self.nonce_len)); - noq::ConnectionId::new(cid.as_slice()) + quinn::ConnectionId::new(cid.as_slice()) } fn cid_len(&self) -> usize { @@ -987,6 +1004,20 @@ mod tests { use super::*; use url::Url; + #[cfg(feature = "qlog")] + #[test] + fn qlog_paths_are_unique_per_endpoint() { + let dir = tempfile::tempdir().unwrap(); + let mut config = crate::quic::Config::default(); + config.qlog = Some(dir.path().into()); + let config = config.resolve(); + let mut first = quinn::TransportConfig::default(); + let mut second = quinn::TransportConfig::default(); + apply_qlog(&mut first, &config, "server").unwrap(); + apply_qlog(&mut second, &config, "server").unwrap(); + assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 2); + } + #[tokio::test] async fn pinned_ipv6_connection() { pinned_connection("[::1]:0", None).await; @@ -1004,7 +1035,7 @@ mod tests { async fn pinned_connection(bind: &str, host_name: Option<&str>) { let quic = crate::quic::Config::default(); - let server = NoqServer::new( + let server = Server::new( listen::Config { bind: Some(bind.parse().unwrap()), tls: crate::tls::Listen { @@ -1028,7 +1059,7 @@ mod tests { ..Default::default() }; let tls = config.tls.build().expect("tls config"); - let client = NoqClient::new(&config, &quic).expect("client init"); + let client = Client::new(&config, &quic).expect("client init"); let url: Url = format!("moqt://{addr}/.cluster/test").parse().unwrap(); let versions = moq_net::Versions::default(); let dial = client.connect(&tls, url.into(), &versions); @@ -1042,19 +1073,19 @@ mod tests { let (_client, _server) = result.expect("pinned connection failed"); } - /// noq exposes no getters for the flow-control windows, but its `Debug` prints + /// quinn exposes no getters for the flow-control windows, but its `Debug` prints /// them, which is enough to prove each one reached the transport config and that - /// an unset knob leaves noq's own default in place. + /// an unset knob leaves quinn's own default in place. #[test] fn apply_windows_writes_each_field() { - let defaults = format!("{:?}", noq::TransportConfig::default()); + let defaults = format!("{:?}", quinn::TransportConfig::default()); let mut quic = crate::quic::Config::default(); quic.receive_window = Some(64 << 20); quic.stream_receive_window = Some(8 << 20); quic.send_window = Some(32 << 20); - let mut transport = noq::TransportConfig::default(); + let mut transport = quinn::TransportConfig::default(); apply_windows(&mut transport, &quic.resolve()); let applied = format!("{:?}", transport); @@ -1066,27 +1097,27 @@ mod tests { assert!(applied.contains(&field), "{field} missing from {applied}"); } - let mut untouched = noq::TransportConfig::default(); + let mut untouched = quinn::TransportConfig::default(); apply_windows(&mut untouched, &crate::quic::Config::default().resolve()); assert_eq!(format!("{:?}", untouched), defaults); } /// Build a controller from each family's factory and downcast it to the - /// concrete noq implementation it must map to. + /// concrete quinn implementation it must map to. #[test] fn congestion_factory_maps_each_family() { let now = std::time::Instant::now(); let mtu = 1200; let loss = congestion_factory(CongestionControl::Loss).build(now, mtu); - assert!(loss.into_any().downcast::().is_ok()); + assert!(loss.into_any().downcast::().is_ok()); let delay = congestion_factory(CongestionControl::Delay).build(now, mtu); - assert!(delay.into_any().downcast::().is_ok()); + assert!(delay.into_any().downcast::().is_ok()); } - /// Loopback regression test: with the knob unset, live noq connections must run - /// BBRv3 on both ends. + /// Loopback regression test: with the knob unset, live quinn connections must run + /// BBRv1 on both ends. #[tokio::test] async fn default_reaches_the_live_connection() { let server_config = listen::Config { @@ -1101,13 +1132,13 @@ mod tests { // The knob left unset, the way a binary that never mentions it runs. let quic = crate::quic::Config::default(); - let server = NoqServer::new(server_config, &quic, None).expect("server init"); + let server = Server::new(server_config, &quic, None).expect("server init"); let addr = server.local_addr().expect("local addr"); let accepted = tokio::spawn(async move { let incoming = server.accept().await.expect("no incoming connection"); let conn = incoming.accept().expect("accept").await.expect("handshake"); - is_bbr3(&conn) + is_bbr(&conn) }); // tls::Connect has a private field, so it can't be built with a struct literal. @@ -1121,7 +1152,7 @@ mod tests { }; let tls = client_config.tls.build().expect("tls config"); - let client = NoqClient::new(&client_config, &quic).expect("client init"); + let client = Client::new(&client_config, &quic).expect("client init"); // Dial the loopback IP directly so the system resolver is never involved. let url: Url = format!("moqt://127.0.0.1:{}", addr.port()).parse().unwrap(); @@ -1133,26 +1164,22 @@ mod tests { .await .expect("connect failed"); - // web_transport_moq::Session derefs to the noq connection. - assert!(is_bbr3(&session), "client connection is not running BBRv3"); + // web_transport_quinn::Session derefs to the quinn connection. + assert!(is_bbr(&session), "client connection is not running BBRv1"); assert!( accepted.await.expect("server task panicked"), - "server connection is not running BBRv3" + "server connection is not running BBRv1" ); }) .await .expect("test timed out"); } - /// Whether a live connection's initial path is running BBRv3. - /// - /// noq is multipath, so the controller is per path rather than per connection; - /// `PathId::ZERO` is the path the handshake came up on. - fn is_bbr3(conn: &noq::Connection) -> bool { - conn.congestion_state(noq::PathId::ZERO) - .expect("no controller on the initial path") + /// Whether a live connection is running BBRv1. + fn is_bbr(conn: &quinn::Connection) -> bool { + conn.congestion_state() .into_any() - .downcast::() + .downcast::() .is_ok() } } diff --git a/rs/moq-tokio/src/resolve.rs b/rs/moq-tokio/src/resolve.rs index 7410cae79a..db5cf9a0ec 100644 --- a/rs/moq-tokio/src/resolve.rs +++ b/rs/moq-tokio/src/resolve.rs @@ -284,7 +284,6 @@ impl Candidates { /// are handed out anyway so the dial surfaces the OS error instead of a /// confusing "no DNS entries". See /// for the Windows failure this family matching originally fixed. - #[cfg(any(feature = "noq", test))] pub(crate) fn with_local(mut self, local: SocketAddr, dual_stack: bool) -> Self { self.local = Some((local, dual_stack)); self diff --git a/rs/moq-tokio/src/server.rs b/rs/moq-tokio/src/server.rs index d0937f9cae..8edf226390 100644 --- a/rs/moq-tokio/src/server.rs +++ b/rs/moq-tokio/src/server.rs @@ -16,7 +16,6 @@ use url::Url; // Only the transports that finish their handshake in a spawned future need `.boxed()`; // the stream listeners hand back an already-built `Request`. -#[cfg(any(feature = "noq", feature = "iroh", feature = "websocket"))] use futures::FutureExt; use futures::future::BoxFuture; use futures::stream::FuturesUnordered; @@ -86,7 +85,6 @@ impl crate::listen::Config { } /// Default bind address used when [`crate::listen::Config::bind`] is not set. -#[cfg(feature = "noq")] pub(crate) const DEFAULT_BIND: net::SocketAddr = net::SocketAddr::V6(net::SocketAddrV6::new(net::Ipv6Addr::UNSPECIFIED, 443, 0, 0)); @@ -109,7 +107,6 @@ pub(crate) enum Parts { /// The QUIC listener only, as one member of a `SO_REUSEPORT` group. Folding /// the member in here is what stops a group member and a stream-only server /// from being asked for at once. - #[cfg_attr(not(feature = "noq"), expect(dead_code, reason = "no QUIC backend is compiled in"))] Member(crate::listen::Socket), } @@ -129,8 +126,7 @@ impl Parts { } /// This server's socket in a complete group, when it is a member of one. - /// Consuming, because only one backend may own the serving handle. - #[cfg_attr(not(feature = "noq"), expect(dead_code, reason = "no QUIC backend is compiled in"))] + /// Consuming, because only one endpoint may own the serving handle. fn member(self) -> Option { match self { Self::Member(member) => Some(member), @@ -191,7 +187,6 @@ impl Config { } /// Copy the settings one QUIC worker owns. - #[cfg(feature = "noq")] pub(crate) fn worker(&self) -> Self { Self { listen: self.listen.clone(), @@ -220,8 +215,7 @@ pub struct Server { streams: StreamListeners, #[cfg(feature = "iroh")] iroh: Option, - #[cfg(feature = "noq")] - noq: Option, + quic: Option, #[cfg(feature = "websocket")] websocket: Option, } @@ -238,10 +232,8 @@ pub struct Server { /// only caller. #[derive(Clone, Debug)] #[allow(dead_code)] -#[cfg(feature = "noq")] pub(crate) struct SocketRetainer { - #[cfg(feature = "noq")] - noq: Option>, + quic: Option>, } impl Server { @@ -290,35 +282,12 @@ impl Server { // Read before the member is taken out below, which consumes `parts`. #[cfg(any(feature = "tcp", all(feature = "uds", unix)))] let build_streams = parts.streams(); - // `parts.quic()` and not `build_quic`: a caller that asked for streams only - // is not asking for a backend, while leaving everything else configured is - // still the error it always was. - #[cfg(not(feature = "noq"))] - if config.bind.is_some() && parts.quic() { - return Err(Error::NoBackend("--listen requires the noq feature")); - } - - if build_quic && !config.tls.root.is_empty() { - // Only the QUIC backend validates client certificates; the qmux listeners - // (tcp/unix/websocket) carry no TLS of their own. - #[cfg(feature = "noq")] - let mtls_supported = true; - #[cfg(not(feature = "noq"))] - let mtls_supported = false; - - if !mtls_supported { - return Err(Error::MtlsUnsupported); - } - } - // The member is a serving handle released by a complete reuseport group, // so the backend may take it only once. - #[cfg(feature = "noq")] let member = parts.member(); - #[cfg(feature = "noq")] - let noq = build_quic - .then(|| crate::noq::NoqServer::new(config.clone(), &quic, member)) + let quic = build_quic + .then(|| crate::quinn::Server::new(config.clone(), &quic, member)) .transpose()?; // Collect the configured stream listeners (at most one TCP, one Unix). @@ -366,8 +335,7 @@ impl Server { streams, #[cfg(feature = "iroh")] iroh, - #[cfg(feature = "noq")] - noq, + quic, #[cfg(feature = "websocket")] websocket, }) @@ -439,9 +407,8 @@ impl Server { /// /// Empty when no TLS-bearing backend is configured (e.g. a stream-only server). pub fn certificates(&self) -> crate::tls::Certificates { - #[cfg(feature = "noq")] - if let Some(noq) = self.noq.as_ref() { - return noq.certificates(); + if let Some(quic) = self.quic.as_ref() { + return quic.certificates(); } // No QUIC backend (e.g. a stream-only `--listen-tcp-bind`): no certificates. crate::tls::Certificates::empty() @@ -451,29 +418,13 @@ impl Server { /// group after this server is gone. /// /// Crate-private: only the worker group retains sockets this way. A worker - /// member serves the same noq backend as the tokio listener. - #[cfg(feature = "noq")] + /// member serves the same QUIC backend as the tokio listener. pub(crate) fn retain(&self) -> SocketRetainer { SocketRetainer { - #[cfg(feature = "noq")] - noq: self.noq.as_ref().and_then(|server| server.retain()), + quic: self.quic.as_ref().and_then(|server| server.retain()), } } - #[cfg(not(any( - feature = "noq", - feature = "iroh", - feature = "websocket", - feature = "tcp", - all(feature = "uds", unix) - )))] - /// Returns the next partially established session. - /// - /// Panics: no transport feature is compiled in, so nothing can be accepted. - async fn accept_next(&mut self) -> Option { - unreachable!("no transport compiled; enable a QUIC backend, websocket, tcp, or uds feature"); - } - /// The accept-loop health of every listener this server owns that performs a real /// `accept(2)`: the `tcp`/`unix` stream listeners and, if one was set, /// [`Config::websocket`]. @@ -552,13 +503,6 @@ impl Server { /// The body of [`Listener::accept`]. Stream accept loops start here if /// [`Server::bind`] left them stopped; [`Server::listen`] already started them. - #[cfg(any( - feature = "noq", - feature = "iroh", - feature = "websocket", - feature = "tcp", - all(feature = "uds", unix) - ))] async fn accept_next(&mut self) -> Option { // A `bind` (rather than `listen`) leaves the stream loops stopped so an // embedder can read the port before it is willing to take sessions. @@ -570,20 +514,15 @@ impl Server { } loop { // The QUIC endpoint address, reported as a QUIC session's local side. - #[cfg(feature = "noq")] let local = self.local_addr().ok(); // tokio::select! does not support cfg directives on arms, so we need to create the futures here. - #[cfg(feature = "noq")] - let noq_accept = async { - #[cfg(feature = "noq")] - if let Some(noq) = self.noq.as_mut() { - return noq.accept().await; + let quic_accept = async { + if let Some(quic) = self.quic.as_mut() { + return quic.accept().await; } None }; - #[cfg(not(feature = "noq"))] - let noq_accept = async { None::<()> }; #[cfg(feature = "iroh")] let iroh_accept = async { @@ -626,20 +565,17 @@ impl Server { Some(request) = stream_accept => { return Some(request); } - Some(_conn) = noq_accept => { - #[cfg(feature = "noq")] - { - let alpns = versions.alpns(); - self.accept.push(async move { - // Accept the transport (capturing url + mTLS identity) and exchange the - // MoQ SETUP up front, so path/role are known before the caller authorizes - // (like the stream bindings). - let Accepted { session, url, identity, authority, mut link } = super::noq::accept(_conn, alpns).await?; - link.local = local; - let request = server.accept_request(tokio::time::Instant::now().into_std(), crate::transport::Session::new(session)).await?; - Ok(Request { transport: Transport::Quic, url, identity, authority, link, kind: RequestKind::Noq(Box::new(request)) }) - }.boxed()); - } + Some(conn) = quic_accept => { + let alpns = versions.alpns(); + self.accept.push(async move { + // Accept the transport (capturing url + mTLS identity) and exchange the + // MoQ SETUP up front, so path/role are known before the caller authorizes + // (like the stream bindings). + let Accepted { session, url, identity, authority, mut link } = crate::quinn::accept(conn, alpns).await?; + link.local = local; + let request = server.accept_request(tokio::time::Instant::now().into_std(), crate::transport::Session::new(session)).await?; + Ok(Request { transport: Transport::Quic, url, identity, authority, link, kind: RequestKind::Quic(Box::new(request)) }) + }.boxed()); } Some(_conn) = iroh_accept => { #[cfg(feature = "iroh")] @@ -699,9 +635,8 @@ impl Server { /// Errors with [`Error::NoBackend`] on a stream-only server, which has no /// QUIC listener. pub fn local_addr(&self) -> crate::Result { - #[cfg(feature = "noq")] - if let Some(noq) = self.noq.as_ref() { - return Ok(noq.local_addr()?); + if let Some(quic) = self.quic.as_ref() { + return Ok(quic.local_addr()?); } // No QUIC backend (e.g. a stream-only `--listen-tcp-bind`). Err(Error::NoBackend("no QUIC listener configured")) @@ -718,9 +653,8 @@ impl Server { #[cfg(any(feature = "tcp", all(feature = "uds", unix)))] self.streams.shutdown().await; - #[cfg(feature = "noq")] - if let Some(noq) = self.noq.take() { - noq.shutdown().await; + if let Some(quic) = self.quic.take() { + quic.shutdown().await; } #[cfg(feature = "iroh")] if let Some(iroh) = self.iroh.take() { @@ -734,9 +668,8 @@ impl Server { /// Close the QUIC connections without waiting for the socket, as a drop must. fn close(&mut self) { - #[cfg(feature = "noq")] - if let Some(noq) = self.noq.as_ref() { - noq.close(); + if let Some(quic) = self.quic.as_ref() { + quic.close(); } } } @@ -1146,8 +1079,7 @@ async fn stream_request( type PendingRequest = moq_net::server::Handshake; pub(crate) enum RequestKind { - #[cfg(feature = "noq")] - Noq(Box>>), + Quic(Box>>), #[cfg(feature = "iroh")] Iroh(Box>>), #[cfg(any(feature = "tcp", all(feature = "uds", unix), feature = "websocket"))] @@ -1157,7 +1089,6 @@ pub(crate) enum RequestKind { /// The transport-level facts a backend captures while accepting a connection, before the /// MoQ SETUP. Grouped so the shared accept loop builds a [`Request`] from named fields /// rather than a wide tuple. -#[cfg(any(feature = "noq", feature = "iroh"))] pub(crate) struct Accepted { pub session: S, pub url: Option, @@ -1258,8 +1189,7 @@ pub enum Reject { macro_rules! request_ref { ($self:expr, $r:ident => $body:expr) => { match &$self.kind { - #[cfg(feature = "noq")] - RequestKind::Noq($r) => $body, + RequestKind::Quic($r) => $body, #[cfg(feature = "iroh")] RequestKind::Iroh($r) => $body, #[cfg(any(feature = "tcp", all(feature = "uds", unix), feature = "websocket"))] @@ -1272,8 +1202,7 @@ macro_rules! request_ref { macro_rules! request_into { ($kind:expr, $r:ident => $body:expr) => { match $kind { - #[cfg(feature = "noq")] - RequestKind::Noq($r) => $body, + RequestKind::Quic($r) => $body, #[cfg(feature = "iroh")] RequestKind::Iroh($r) => $body, #[cfg(any(feature = "tcp", all(feature = "uds", unix), feature = "websocket"))] @@ -1286,8 +1215,7 @@ macro_rules! request_into { macro_rules! request_map { ($kind:expr, $r:ident => $body:expr) => { match $kind { - #[cfg(feature = "noq")] - RequestKind::Noq($r) => RequestKind::Noq(Box::new($body)), + RequestKind::Quic($r) => RequestKind::Quic(Box::new($body)), #[cfg(feature = "iroh")] RequestKind::Iroh($r) => RequestKind::Iroh(Box::new($body)), #[cfg(any(feature = "tcp", all(feature = "uds", unix), feature = "websocket"))] @@ -1422,7 +1350,7 @@ impl Request { /// transport carries no host (iroh, stream bindings). /// /// Reported as offered: rustls sends no SNI for an IP-literal dial (RFC 6066), - /// so noq reports `None`. + /// so Quinn reports `None`. /// /// Not the moq-net IETF SETUP `Authority` parameter. Client-asserted and not authenticated, /// so authorize on the token or [`Self::peer_identity`] rather than on this value. @@ -1512,7 +1440,7 @@ impl Request { /// The client certificate chain the peer presented, if any, validated /// against a configured [`crate::tls::Listen::root`] during the handshake. /// - /// Captured at the transport handshake (before the SETUP). Noq supports mTLS; + /// Captured at the transport handshake (before the SETUP). Quinn supports mTLS; /// other transports always return `None`. Use it to grant /// elevated access or to close the session once the certificate expires (see /// [`crate::tls::PeerIdentity::expiry`]). @@ -1835,7 +1763,6 @@ mod tests { /// Closing a listener must release its UDP socket before returning, even while /// an accepted session still holds its QUIC connection, so a restart can rebind /// the port at once. - #[cfg(feature = "noq")] #[tokio::test] async fn close_releases_quic_socket() { let config = crate::listen::Config { @@ -1865,28 +1792,9 @@ mod tests { drop((session, connection)); } - /// An explicit QUIC bind cannot be honored without a QUIC backend. - #[cfg(not(feature = "noq"))] - #[test] - fn quic_bind_without_a_quic_backend_is_rejected() { - let config = crate::listen::Config { - bind: Some("127.0.0.1:0".parse().unwrap()), - ..Default::default() - }; - - assert!(matches!( - Config { - listen: config, - ..Default::default() - } - .init(), - Err(Error::NoBackend(_)) - )); - } - /// A QUIC-only server reports nothing. It multiplexes over one UDP socket and /// never calls `accept`, so a zero counter would be a watch that cannot fire. - #[cfg(all(feature = "noq", not(feature = "tcp")))] + #[cfg(not(feature = "tcp"))] #[test] fn accept_health_is_empty_without_a_stream_listener() { let server = crate::listen::Config::default() @@ -1906,7 +1814,6 @@ mod tests { /// Building the endpoint needs a runtime, and `certificates()` must stay /// readable without one (no guard escapes to the caller). - #[cfg(feature = "noq")] #[tokio::test] async fn certificates_expose_generated_fingerprints() { let mut config = crate::listen::Config { diff --git a/rs/moq-tokio/src/tls.rs b/rs/moq-tokio/src/tls.rs index e58b917c44..85ccea2582 100644 --- a/rs/moq-tokio/src/tls.rs +++ b/rs/moq-tokio/src/tls.rs @@ -8,12 +8,11 @@ //! Certificates, keys, and custom root CAs loaded from disk are hot reloaded for //! new handshakes. [`Certificates`] reads the current served set back out. -#[cfg(all(feature = "watch", feature = "_certs"))] +#[cfg(feature = "watch")] use crate::abort::AbortOnDrop; use crate::crypto; use rustls::pki_types::pem::PemObject; use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime}; -#[cfg(feature = "_certs")] use std::fmt; use std::path::{Path, PathBuf}; use std::sync::{Arc, RwLock}; @@ -24,7 +23,7 @@ use rustls::pki_types::PrivatePkcs8KeyDer; /// Errors loading or generating TLS certificates and keys. /// -/// Shared by the client TLS config and noq server. +/// Shared by the client TLS config and QUIC server. #[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum Error { @@ -132,7 +131,6 @@ pub enum Error { Rustls(#[from] rustls::Error), /// The mTLS client-certificate verifier couldn't be built from the configured roots. - #[cfg(feature = "_certs")] #[error("failed to build client certificate verifier")] ClientVerifier(#[source] rustls::server::VerifierBuilderError), @@ -141,7 +139,6 @@ pub enum Error { ServerVerifier(#[source] rustls::client::VerifierBuilderError), /// Generating a self-signed certificate failed. - #[cfg(feature = "_certs")] #[error("failed to generate a self-signed certificate: {0}")] Rcgen(String), @@ -155,7 +152,6 @@ pub enum Error { Deprecated(crate::cli::Deprecated), } -#[cfg(feature = "_certs")] crate::error::from_message! { rcgen::Error => Rcgen, } @@ -194,13 +190,11 @@ fn read_roots(paths: &[PathBuf]) -> Result>> { // ── Certified ─────────────────────────────────────────────────────── /// A certificate chain and its rustls signing key. -#[cfg(feature = "_certs")] pub(crate) struct Certified { /// The chain and its rustls signer, in leaf-first order. pub rustls: Arc, } -#[cfg(feature = "_certs")] impl Certified { /// Load `chain` and `key`, building the rustls signer and keeping the DER. pub(crate) fn new( @@ -222,7 +216,6 @@ impl Certified { } } -#[cfg(feature = "_certs")] impl fmt::Debug for Certified { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { // Never derive: the DER private key would print in full. @@ -362,7 +355,6 @@ impl Peers { } /// Whether a raw digest is allowed, for the verifier's hot path. - #[cfg(feature = "_certs")] fn contains_raw(&self, fingerprint: &[u8]) -> bool { match <[u8; 32]>::try_from(fingerprint) { Ok(fingerprint) => self.read().contains(&fingerprint), @@ -759,7 +751,7 @@ pub(crate) enum Verification { Fingerprints(Vec<[u8; 32]>), /// Standard CA verification. When `system` is set the platform/default trust - /// store is trusted too; noq uses the OS platform verifier. + /// store is trusted too; Quinn uses the OS platform verifier. /// `custom` are extra PEM roots trusted in addition. Roots { custom: CustomRoots, system: bool }, } @@ -923,7 +915,6 @@ impl Connect { /// plaintext fetch, and there is nothing to bootstrap when verification is /// disabled. With CA roots (the default), `http://` is the deliberate /// per-connection way to pin a self-signed relay, so it is allowed. - #[cfg(feature = "noq")] pub(crate) fn allows_http_bootstrap(&self) -> bool { self.fingerprint.is_empty() && !self.insecure.unwrap_or_default() } @@ -1120,7 +1111,7 @@ fn generate(provider: &crypto::Provider, hostnames: &[String]) -> Result Result { Err(Error::NoCryptoProvider) } @@ -1357,7 +1348,6 @@ impl Listen { /// Gated with the rest of the serving side rather than with `watch`: a pinned /// [`peers`](Self::peers) set changes while the process runs whether or not /// anything is reloading from disk. - #[cfg(feature = "_certs")] pub(crate) fn disable_resumption(&self, tls: &mut rustls::ServerConfig) { if !self.root.is_empty() || self.peers.is_some() { tls.session_storage = Arc::new(rustls::server::NoServerSessionStorage {}); @@ -1386,7 +1376,6 @@ impl Listen { /// The one place the TLS listeners agree on what mTLS means here, so a new /// mode lands once rather than in each of them: pinned [`peers`](Self::peers), /// else optional CA-rooted [`root`](Self::root), else nothing. - #[cfg(feature = "_certs")] pub(crate) fn client_auth( &self, provider: crypto::Provider, @@ -1407,7 +1396,6 @@ impl Listen { } /// Build the optional-client-auth verifier, reloading configured roots in place. - #[cfg(feature = "_certs")] pub(crate) fn client_verifier( &self, provider: crypto::Provider, @@ -1430,7 +1418,6 @@ impl Listen { Ok(initial) } - #[cfg(feature = "_certs")] fn build_client_verifier( paths: &[PathBuf], provider: &crypto::Provider, @@ -1453,7 +1440,6 @@ impl Listen { /// `alpn` sets the advertised ALPN protocols (e.g. /// `vec![b"h2".to_vec(), b"http/1.1".to_vec()]`); pass an empty list for a /// protocol like RTMPS that doesn't use ALPN. - #[cfg(feature = "_certs")] pub fn server_config(&self, alpn: Vec>) -> Result> { self.refuse_deprecated()?; server_config(self, alpn).map(|(config, _)| config) @@ -1461,7 +1447,7 @@ impl Listen { /// Build a plain-TLS server config whose file-backed certificates reload. /// Must be called inside a Tokio runtime that drives the file watcher. - #[cfg(all(feature = "watch", feature = "_certs"))] + #[cfg(feature = "watch")] pub fn server_config_reloading(&self, alpn: Vec>) -> Result { self.refuse_deprecated()?; let (config, certs) = server_config(self, alpn)?; @@ -1475,14 +1461,14 @@ impl Listen { /// A plain-TLS server config with a watcher for its file-backed certificates. /// Keep this handle alive for as long as the listener serves. -#[cfg(all(feature = "watch", feature = "_certs"))] +#[cfg(feature = "watch")] #[derive(Debug)] pub struct ReloadingServerConfig { config: Arc, _reload: Reload, } -#[cfg(all(feature = "watch", feature = "_certs"))] +#[cfg(feature = "watch")] impl ReloadingServerConfig { /// The rustls config used for new handshakes; clones share the live resolver. pub fn config(&self) -> Arc { @@ -1491,7 +1477,6 @@ impl ReloadingServerConfig { } /// Build a [`rustls::ServerConfig`] from a [`Listen`] for a plain-TLS listener. -#[cfg(feature = "_certs")] fn server_config(config: &Listen, alpn: Vec>) -> Result<(Arc, Arc)> { let provider = crypto::provider(); @@ -1530,7 +1515,6 @@ impl PeerIdentity { /// Wrap the type-erased identity from the QUIC connection. /// Returns `None` if the peer presented no certificate or the identity is /// not a certificate chain. - #[cfg(feature = "noq")] pub(crate) fn from_any(identity: Option>) -> Option { let chain = identity?.downcast::>>().ok()?; Some(Self { chain: *chain }) @@ -1615,7 +1599,6 @@ impl PeerIdentity { /// alone. #[derive(Debug, Default)] pub(crate) struct Info { - #[cfg(feature = "_certs")] pub(crate) certs: Vec>, pub(crate) fingerprints: Vec, } @@ -1636,7 +1619,6 @@ impl Certificates { } /// An empty set, used when no TLS-bearing backend is configured. - #[cfg(feature = "_transport")] pub(crate) fn empty() -> Self { Self { info: Arc::new(RwLock::new(Info::default())), @@ -1666,7 +1648,6 @@ impl Certificates { .map(|cert| hex::encode(crypto::sha256(&provider, cert.as_ref()))) .collect(); Ok(Self::new(Arc::new(RwLock::new(Info { - #[cfg(feature = "_certs")] certs: Vec::new(), fingerprints, })))) @@ -1748,13 +1729,13 @@ impl rustls::client::danger::ServerCertVerifier for ReloadingServerVerifier { } /// Delegates each client-certificate check to the latest valid root verifier. -#[cfg(all(feature = "watch", feature = "_certs"))] +#[cfg(feature = "watch")] #[derive(Debug)] struct ReloadingClientVerifier { inner: Reloading, } -#[cfg(all(feature = "watch", feature = "_certs"))] +#[cfg(feature = "watch")] impl ReloadingClientVerifier { fn new( paths: &[PathBuf], @@ -1767,7 +1748,7 @@ impl ReloadingClientVerifier { } } -#[cfg(all(feature = "watch", feature = "_certs"))] +#[cfg(feature = "watch")] impl rustls::server::danger::ClientCertVerifier for ReloadingClientVerifier { fn offer_client_auth(&self) -> bool { self.inner.current().offer_client_auth() @@ -1867,21 +1848,18 @@ impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification { /// The mirror of [`FingerprintVerifier`] on the accept side, with one difference /// that matters: the allowed set is read per handshake rather than fixed when the /// listener was built, because a mesh learns its members while it runs. -#[cfg(feature = "_certs")] #[derive(Debug)] pub(crate) struct PeerVerifier { provider: crypto::Provider, peers: Peers, } -#[cfg(feature = "_certs")] impl PeerVerifier { pub fn new(provider: crypto::Provider, peers: Peers) -> Self { Self { provider, peers } } } -#[cfg(feature = "_certs")] impl rustls::server::danger::ClientCertVerifier for PeerVerifier { fn root_hint_subjects(&self) -> &[rustls::DistinguishedName] { // Self-signed identities chain to nothing, so there is no issuer to hint at. @@ -1989,7 +1967,7 @@ impl rustls::client::danger::ServerCertVerifier for FingerprintVerifier { } #[cfg(test)] -#[cfg(all(feature = "noq", feature = "aws-lc-rs"))] +#[cfg(feature = "aws-lc-rs")] mod tests { /// Disabling verification cannot be combined with trust material that it would /// otherwise ignore. @@ -2051,7 +2029,6 @@ mod tests { params.self_signed(&key).unwrap().into() } - #[cfg(feature = "noq")] #[test] fn peer_identity_expiry_reads_not_after() { // notAfter at a whole second so the round-trip is exact. @@ -2062,7 +2039,7 @@ mod tests { params.not_after = not_after; let cert: CertificateDer<'static> = params.self_signed(&key).unwrap().into(); - // noq hand back the chain as a boxed Vec. + // Quinn hands back the chain as a boxed Vec. let identity: Box = Box::new(vec![cert]); let parsed = PeerIdentity::from_any(Some(identity)).expect("chain parsed"); let expiry = parsed.expiry().expect("expiry parsed"); @@ -2072,7 +2049,6 @@ mod tests { ); } - #[cfg(feature = "noq")] #[test] fn peer_identity_none_without_chain() { assert!(PeerIdentity::from_any(None).is_none()); @@ -2175,7 +2151,7 @@ mod tests { /// keeps its task, an OS directory watch, and the certificate keys alive for the /// rest of the process. The `Arc` is the observable half: while the task lives it /// holds one, so a `Weak` that still upgrades is a watcher that never stopped. - #[cfg(all(feature = "watch", feature = "noq"))] + #[cfg(feature = "watch")] #[tokio::test] async fn dropping_the_reload_guard_stops_the_watcher() { let key = rcgen::KeyPair::generate().unwrap(); @@ -2310,7 +2286,7 @@ mod tests { /// A resumed handshake skips certificate verification, so a listener with a /// live peer set must disable resumption for removals to take effect. - #[cfg(all(feature = "watch", feature = "noq"))] + #[cfg(feature = "watch")] #[test] fn removed_peers_cannot_resume() { let server_identity = Identity::generate(["localhost"]).unwrap(); @@ -2344,7 +2320,7 @@ mod tests { } /// A plain-TLS embedder receives a working config while retaining its watcher. - #[cfg(all(feature = "watch", feature = "noq"))] + #[cfg(feature = "watch")] #[tokio::test] async fn plain_tls_config_reloading_handshakes() { let server = Listen { @@ -2468,7 +2444,7 @@ mod tests { (file, path) } - #[cfg(all(feature = "watch", feature = "noq"))] + #[cfg(feature = "watch")] fn signed_certificates() -> ( String, CertificateDer<'static>, @@ -2505,7 +2481,7 @@ mod tests { ) } - #[cfg(all(feature = "watch", feature = "noq"))] + #[cfg(feature = "watch")] fn handshake_kinds( client: Arc, server: Arc, @@ -2537,7 +2513,7 @@ mod tests { panic!("TLS handshake did not settle"); } - #[cfg(all(feature = "watch", feature = "noq"))] + #[cfg(feature = "watch")] #[test] fn reloadable_roots_disable_session_resumption() { use std::io::Write; @@ -2588,7 +2564,7 @@ mod tests { ); } - #[cfg(all(feature = "watch", feature = "noq"))] + #[cfg(feature = "watch")] #[test] fn reloadable_client_roots_disable_server_resumption() { let (ca_a, server_cert, server_key, client_cert, client_key) = signed_certificates(); @@ -2664,7 +2640,7 @@ mod tests { assert!(handshake_kinds(reloadable_client, reloadable).is_err()); } - #[cfg(all(feature = "watch", feature = "noq"))] + #[cfg(feature = "watch")] #[test] fn custom_roots_reload_for_new_client_and_server_handshakes() { let (ca_a, server_a, _, client_a, _) = signed_certificates(); @@ -2767,7 +2743,6 @@ mod tests { // ── ServeCerts ────────────────────────────────────────────────────── -#[cfg(feature = "_certs")] #[derive(Debug)] pub(crate) struct ServeCerts { pub info: Arc>, @@ -2776,7 +2751,6 @@ pub(crate) struct ServeCerts { generated: RwLock>>, } -#[cfg(feature = "_certs")] impl ServeCerts { pub fn new(provider: crypto::Provider) -> Self { Self { @@ -2875,7 +2849,7 @@ impl ServeCerts { /// The certificate to serve for `server_name`, or the first configured one when /// nothing matches. `None` only when nothing is configured at all. /// - /// Noq selects through this + /// Quinn selects through this /// on which certificate a given SNI gets. pub(crate) fn select(&self, server_name: Option<&str>) -> Option> { let info = self.info.read().expect("info read lock poisoned"); @@ -2904,7 +2878,6 @@ impl ServeCerts { } } -#[cfg(feature = "_certs")] impl rustls::server::ResolvesServerCert for ServeCerts { fn resolve(&self, client_hello: rustls::server::ClientHello<'_>) -> Option> { Some(self.select(client_hello.server_name())?.rustls.clone()) @@ -2919,14 +2892,14 @@ impl rustls::server::ResolvesServerCert for ServeCerts { /// its own, so a listener that goes away without this leaves the task, the keys it /// holds, and an OS directory watch behind. An embedder that builds listeners /// repeatedly in one process would accumulate all three. -#[cfg(all(feature = "watch", feature = "_certs"))] +#[cfg(feature = "watch")] #[derive(Debug)] pub(crate) struct Reload { /// Named for the drop alone: nothing reads it, and losing it stops the watcher. _task: AbortOnDrop, } -#[cfg(all(feature = "watch", feature = "_certs"))] +#[cfg(feature = "watch")] impl Reload { /// A guard over a task with nothing to do, for a listener with nothing to watch. fn inert() -> Self { @@ -2978,7 +2951,7 @@ impl Reload { /// Reacting to the filesystem means cert-manager, Kubernetes secret mounts, and /// `mv`-into-place rotate certs with no external signal. [`Reload::spawn`] owns /// registering the watch and is the only caller. -#[cfg(all(feature = "watch", feature = "_certs"))] +#[cfg(feature = "watch")] async fn reload_certs(mut watcher: crate::watch::Files, certs: Arc, tls_config: Listen) { loop { watcher.changed().await; @@ -3065,7 +3038,6 @@ mod legacy_tests { /// The accept side, where a dropped `--server-tls-root` takes the mTLS client /// CAs with it and leaves the listener accepting unauthenticated peers. /// moq-cli's `export hls` flattens this type directly, so it is a live path. - #[cfg(feature = "noq")] #[test] fn the_server_builders_refuse_a_released_spelling() { let tls = parse(&["--server-tls-root", "/tmp/ca.pem"]).listen; diff --git a/rs/moq-tokio/src/transport.rs b/rs/moq-tokio/src/transport.rs index 690493084b..1e784eed81 100644 --- a/rs/moq-tokio/src/transport.rs +++ b/rs/moq-tokio/src/transport.rs @@ -1,7 +1,7 @@ //! Adapts the async transport interface to the poll one moq-net requires. //! //! moq-net only accepts the poll interface (`web_transport_trait::poll`). -//! A backend that offers only the async interface (qmux, iroh, noq) is wrapped +//! A backend that offers only the async interface (qmux, iroh, Quinn) is wrapped //! in [`Session`] here, at the edge where async already lives. The wrapping //! costs one allocation per operation and one copy per write, and its //! closed-watch emulation is weaker than a native implementation (see diff --git a/rs/moq-tokio/src/websocket.rs b/rs/moq-tokio/src/websocket.rs index 9ee7e63ddf..075fd56c45 100644 --- a/rs/moq-tokio/src/websocket.rs +++ b/rs/moq-tokio/src/websocket.rs @@ -236,9 +236,7 @@ pub struct Resolved { pub delay: time::Duration, } -/// The fallback arm of the QUIC-vs-WebSocket race, so only compiled when there is a -/// QUIC dial to race against. A WebSocket-only build calls [`connect`] directly. -#[cfg(feature = "noq")] +/// The fallback arm of the QUIC-vs-WebSocket race. pub(crate) async fn race_handle( config: &Config, tls: &rustls::ClientConfig, diff --git a/rs/moq-tokio/src/worker/group.rs b/rs/moq-tokio/src/worker/group.rs index 85b6ca6bd3..dd7f730da2 100644 --- a/rs/moq-tokio/src/worker/group.rs +++ b/rs/moq-tokio/src/worker/group.rs @@ -90,7 +90,7 @@ impl Workers { // hands out one member per slot in the order the kernel numbers them by. let mut forming = moq_sock::shard::Group::acquire(requested, config.count).map_err(|err| match err { moq_sock::shard::Error::Count { count, max } => Error::WorkerCount { count, max }, - moq_sock::shard::Error::Resolve(err) => crate::noq::Error::BindSocket(err).into(), + moq_sock::shard::Error::Resolve(err) => crate::quinn::Error::BindSocket(err).into(), // The port lock is the only other way to lose the address, and it is // held by exactly one thing: another group of this UID. _ => Error::WorkerOverlap { addr: requested }, @@ -98,16 +98,16 @@ impl Workers { let count = forming.count(); let mut claims = Vec::with_capacity(count as usize); while let Some(member) = forming.member() { - claims.push(member.bind().map_err(crate::noq::Error::BindSocket)?); + claims.push(member.bind().map_err(crate::quinn::Error::BindSocket)?); } - let mut group = forming.complete(claims).map_err(crate::noq::Error::BindSocket)?; + let mut group = forming.complete(claims).map_err(crate::quinn::Error::BindSocket)?; let shared = Arc::new(Shared::default()); let mut workers = Vec::with_capacity(count as usize); let mut certificates = None; - while let Some(member) = group.member().map_err(crate::noq::Error::BindSocket)? { + while let Some(member) = group.member().map_err(crate::quinn::Error::BindSocket)? { let index = member.shard().index(); // `max(1)` because an empty core list means pinning is off, not that // there are no workers. diff --git a/rs/moq-tokio/src/worker/mod.rs b/rs/moq-tokio/src/worker/mod.rs index 403835a4c0..a7e3fed2c7 100644 --- a/rs/moq-tokio/src/worker/mod.rs +++ b/rs/moq-tokio/src/worker/mod.rs @@ -20,10 +20,8 @@ // Everything but the knobs: a group binds and serves QUIC, so it needs a backend // to bind with. -#[cfg(feature = "noq")] mod group; -#[cfg(feature = "noq")] pub use group::{Group, Member, Spawner, Workers}; /// How many QUIC workers to run, and whether to pin them. diff --git a/rs/moq-tokio/tests/backend.rs b/rs/moq-tokio/tests/backend.rs index 7ef7e52dd6..15e90cf61a 100644 --- a/rs/moq-tokio/tests/backend.rs +++ b/rs/moq-tokio/tests/backend.rs @@ -1,11 +1,24 @@ -//! Integration tests for noq QUIC and the iroh transport. +//! Integration tests for QUIC and the iroh transport. use std::time::Duration; const TIMEOUT: Duration = Duration::from_secs(10); +/// A real socket failure comes from Quinn without a backend-selection feature. +#[tokio::test] +async fn quinn_is_always_available() { + let occupied = std::net::UdpSocket::bind("127.0.0.1:0").expect("occupy port"); + let mut listen = moq_tokio::listen::Config::default(); + listen.bind = Some(occupied.local_addr().unwrap().to_string().parse().unwrap()); + listen.tls.generate = vec!["localhost".into()]; + let err = listen.init(Default::default()).err().expect("occupied port must fail"); + assert!( + matches!(&err, moq_tokio::Error::Quinn(err) if matches!(err.as_ref(), moq_tokio::quinn::Error::BindSocket(_))), + "selected another QUIC backend: {err}" + ); +} + /// Inputs for [`connect_test`]. -#[cfg(feature = "noq")] struct ConnectTest<'a> { /// URL scheme to dial (`moqt` for raw QUIC, `https` for WebTransport). scheme: &'a str, @@ -34,7 +47,6 @@ struct ConnectTest<'a> { /// /// Dials `localhost`, so the client sends an SNI. Use [`no_sni_test`] to cover /// the SNI-less path. -#[cfg(feature = "noq")] async fn backend_test(scheme: &str) { connect_test(ConnectTest { scheme, @@ -55,7 +67,6 @@ async fn backend_test(scheme: &str) { /// Raw QUIC (`moqt`/`moql`) has no request URI, so the whole request target has to /// ride the SETUP; WebTransport carries it in the CONNECT URL instead. Either way the /// server reports the same route and query through [`moq_tokio::server::Request`]. -#[cfg(feature = "noq")] async fn path_test(scheme: &str) { connect_test(ConnectTest { scheme, @@ -75,7 +86,6 @@ async fn path_test(scheme: &str) { /// in the server name). Raw QUIC has no in-band request URL, so this exercises /// the accept path with an empty server name, which must still establish rather /// than reject. Binds the loopback IP directly to avoid dual-stack flakiness. -#[cfg(feature = "noq")] async fn no_sni_test(scheme: &str) { connect_test(ConnectTest { scheme, @@ -93,7 +103,6 @@ async fn no_sni_test(scheme: &str) { /// Publish a broadcast on the server bound to `bind`, subscribe on a client that /// dials `authority`, and verify the data arrives over the requested scheme. -#[cfg(feature = "noq")] async fn connect_test(config: ConnectTest<'_>) { let ConnectTest { scheme, @@ -218,7 +227,6 @@ async fn connect_test(config: ConnectTest<'_>) { /// Write a self-signed PEM cert + key for `name` to `dir`, prefixed by `stem`. /// /// Returns the two paths, in the order the `cert` and `key` lists want them. -#[cfg(feature = "noq")] fn write_self_signed(dir: &std::path::Path, stem: &str, name: &str) -> (std::path::PathBuf, std::path::PathBuf) { use std::io::Write; @@ -246,7 +254,6 @@ fn write_self_signed(dir: &std::path::Path, stem: &str, name: &str) -> (std::pat /// when the server picked the certificate the SNI asked for. Without SNI /// selection every client would get the first configured certificate, so the /// `alt.localhost` pin below would never match. -#[cfg(feature = "noq")] async fn sni_test() { let dir = tempfile::tempdir().expect("tempdir"); let (first_cert, first_key) = write_self_signed(dir.path(), "first", "localhost"); @@ -303,7 +310,6 @@ async fn sni_test() { } /// A generated certificate joins the file-backed ones rather than replacing them. -#[cfg(feature = "noq")] async fn cert_sources_test() { let dir = tempfile::tempdir().expect("tempdir"); let (cert, key) = write_self_signed(dir.path(), "server", "localhost"); @@ -328,7 +334,7 @@ async fn cert_sources_test() { /// Rotate the certificate files under a running listener and assert the served /// set follows, without the listener being rebuilt. -#[cfg(feature = "noq")] +#[cfg(feature = "watch")] async fn reload_test() { let dir = tempfile::tempdir().expect("tempdir"); let (cert, key) = write_self_signed(dir.path(), "server", "localhost"); @@ -385,7 +391,6 @@ async fn reload_test() { /// Generate a CA, a server cert + key, and a client cert + key (all PEM, the /// leaf certs signed by the CA) written to a tempdir. Returns the dir plus the /// five paths so the caller can wire them into the TLS configs. -#[cfg(feature = "noq")] fn generate_mtls_certs() -> (tempfile::TempDir, MtlsPaths) { use rcgen::{ BasicConstraints, CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa, Issuer, KeyPair, KeyUsagePurpose, @@ -438,7 +443,6 @@ fn generate_mtls_certs() -> (tempfile::TempDir, MtlsPaths) { } /// Filesystem paths to the PEM material produced by [`generate_mtls_certs`]. -#[cfg(feature = "noq")] struct MtlsPaths { ca: std::path::PathBuf, server_cert: std::path::PathBuf, @@ -449,7 +453,6 @@ struct MtlsPaths { /// Connect with a client certificate signed by a CA the server trusts, and /// assert the server observes the validated peer certificate via mTLS. -#[cfg(feature = "noq")] async fn mtls_test(scheme: &str, reject: bool) { let (_dir, paths) = generate_mtls_certs(); @@ -736,14 +739,13 @@ async fn iroh_connect_test(version: Option<&str>) { .expect("server task failed"); } -// ── Noq backend ───────────────────────────────────────────────────── +// ── QUIC backend ───────────────────────────────────────────────────── /// A client that closes before its runtime stops tells the server at once, instead of /// leaving it to the idle timeout, which is what a process exiting on a signal does. -#[cfg(feature = "noq")] #[tracing_test::traced_test] #[tokio::test] -async fn noq_client_close_reaches_server() { +async fn quic_client_close_reaches_server() { let quic = moq_tokio::quic::Config::default(); assert!( quic.idle_timeout > TIMEOUT, @@ -798,10 +800,9 @@ async fn noq_client_close_reaches_server() { /// A client that finishes its track and closes before its runtime stops still delivers /// the queued group and the track's finish, instead of the close discarding them. -#[cfg(feature = "noq")] #[tracing_test::traced_test] #[tokio::test] -async fn noq_client_close_drains_finished_track() { +async fn quic_client_close_drains_finished_track() { // Small, since the debug build logging every packet is slow and the drain has // one second. Queued right before the close, it is still unacknowledged then. let payload: Vec = (0..1024).map(|i| i as u8).collect(); @@ -910,10 +911,9 @@ async fn noq_client_close_drains_finished_track() { /// A GOAWAY leaves the old session serving its subscriptions after the replacement /// connects, and a close then drains that predecessor too rather than dropping it. -#[cfg(feature = "noq")] #[tracing_test::traced_test] #[tokio::test] -async fn noq_client_close_drains_migrated_predecessor() { +async fn quic_client_close_drains_migrated_predecessor() { // Several congestion windows, so it is still in flight when the close starts. let payload: Vec = (0..64 * 1024).map(|i| i as u8).collect(); @@ -1056,10 +1056,9 @@ async fn noq_client_close_drains_migrated_predecessor() { /// A close cuts a predecessor that cannot drain off at its handover deadline, /// rather than holding it for the full one second close window. -#[cfg(feature = "noq")] #[tracing_test::traced_test] #[tokio::test] -async fn noq_client_close_keeps_predecessor_handover() { +async fn quic_client_close_keeps_predecessor_handover() { let quic = moq_tokio::quic::Config::default(); let mut server_config = moq_tokio::listen::Config::default(); server_config.bind = Some("127.0.0.1:0".parse().unwrap()); @@ -1163,73 +1162,64 @@ async fn noq_client_close_keeps_predecessor_handover() { ); } -#[cfg(feature = "noq")] #[tracing_test::traced_test] #[tokio::test] -async fn noq_raw_quic() { +async fn quic_raw_quic() { backend_test("moqt").await; } -#[cfg(feature = "noq")] #[tracing_test::traced_test] #[tokio::test] -async fn noq_raw_quic_no_sni() { +async fn quic_raw_quic_no_sni() { no_sni_test("moqt").await; } -#[cfg(feature = "noq")] #[tracing_test::traced_test] #[tokio::test] -async fn noq_raw_quic_path() { +async fn quic_raw_quic_path() { path_test("moqt").await; } -#[cfg(feature = "noq")] #[tracing_test::traced_test] #[tokio::test] -async fn noq_raw_quic_moql_path() { +async fn quic_raw_quic_moql_path() { path_test("moql").await; } -#[cfg(feature = "noq")] #[tracing_test::traced_test] #[tokio::test] -async fn noq_webtransport_path() { +async fn quic_webtransport_path() { path_test("https").await; } -#[cfg(feature = "noq")] #[tracing_test::traced_test] #[tokio::test] -async fn noq_webtransport() { +async fn quic_webtransport() { backend_test("https").await; } -#[cfg(feature = "noq")] #[tracing_test::traced_test] #[tokio::test] -async fn noq_mtls() { +async fn quic_mtls() { mtls_test("https", false).await; } -#[cfg(feature = "noq")] #[tracing_test::traced_test] #[tokio::test] -async fn noq_sni_certificate() { +async fn quic_sni_certificate() { sni_test().await; } -#[cfg(feature = "noq")] #[tracing_test::traced_test] #[tokio::test] -async fn noq_cert_sources() { +async fn quic_cert_sources() { cert_sources_test().await; } -#[cfg(feature = "noq")] +#[cfg(feature = "watch")] #[tracing_test::traced_test] #[tokio::test] -async fn noq_cert_reload() { +async fn quic_cert_reload() { reload_test().await; } @@ -1239,8 +1229,8 @@ async fn noq_cert_reload() { /// files it left behind. /// /// Both ends write into one directory, so this covers the client and server paths -/// at once. Noq writes one file per connection. -#[cfg(all(feature = "qlog", feature = "noq"))] +/// at once. Quinn writes one file per endpoint. +#[cfg(feature = "qlog")] async fn qlog_test(scheme: &str) -> Vec { let dir = tempfile::tempdir().expect("failed to create tempdir"); @@ -1266,7 +1256,7 @@ async fn qlog_test(scheme: &str) -> Vec { .collect(); for trace in &traces { - // Noq writes JSON-SEQ (RFC 7464): each record is a 0x1e separator then + // Both backends write JSON-SEQ (RFC 7464): each record is a 0x1e separator then // JSON, the first being the qlog header. Checking the bytes rather than just the // length catches a writer that was buffered and never flushed. let raw = std::fs::read(trace).expect("failed to read trace"); @@ -1293,9 +1283,9 @@ async fn qlog_test(scheme: &str) -> Vec { traces } -#[cfg(all(feature = "qlog", feature = "noq"))] +#[cfg(feature = "qlog")] #[tokio::test] -async fn noq_qlog() { +async fn quic_qlog() { let traces = qlog_test("moqt").await; assert!(!traces.is_empty(), "expected at least one trace"); } @@ -1324,7 +1314,6 @@ async fn connect_once( /// The size is the point: a frame that fits inside one window would pass whether or /// not the setting ever reached the backend. /// -#[cfg(feature = "noq")] async fn window_test(scheme: &str) { let mut quic = moq_tokio::quic::Config::default(); quic.receive_window = Some(64 * 1024); @@ -1347,9 +1336,8 @@ async fn window_test(scheme: &str) { .await; } -#[cfg(feature = "noq")] #[tokio::test] -async fn noq_windows() { +async fn quic_windows() { window_test("moqt").await; } diff --git a/rs/moq-tokio/tests/broadcast.rs b/rs/moq-tokio/tests/broadcast.rs index 28148e9575..cc85ae2634 100644 --- a/rs/moq-tokio/tests/broadcast.rs +++ b/rs/moq-tokio/tests/broadcast.rs @@ -2539,7 +2539,7 @@ async fn quic_driver_task_inherits_connection_span() { .expect("group closed prematurely"); assert_eq!(&frame.payload[..], b"hello"); - // Other tasks (e.g. noq's own internal per-connection IO loop) legitimately + // Other tasks (e.g. Quinn's internal per-connection IO loop) legitimately // carry the "conn" span too, since they're spawned while it's entered. So it's // not enough for the marker to appear *somewhere* in the logs; it must be on // the exact line moq-net emits from inside the driver task the runtime diff --git a/rs/moq-tokio/tests/close_code.rs b/rs/moq-tokio/tests/close_code.rs index 9d5f287e97..5682262f41 100644 --- a/rs/moq-tokio/tests/close_code.rs +++ b/rs/moq-tokio/tests/close_code.rs @@ -3,7 +3,7 @@ //! Each case runs several fresh connections, since the losses this guards against were //! races between the peer's close and whatever the transport reported next. -#![cfg(all(feature = "noq", feature = "websocket"))] +#![cfg(feature = "websocket")] use std::time::Duration; diff --git a/rs/moq-tokio/tests/worker.rs b/rs/moq-tokio/tests/worker.rs index 811b060d58..e1a98068e1 100644 --- a/rs/moq-tokio/tests/worker.rs +++ b/rs/moq-tokio/tests/worker.rs @@ -3,7 +3,7 @@ //! Linux-only, because the mode is: every worker binds the listen address with //! `SO_REUSEPORT`, and no other platform load-balances a unicast UDP port across //! the group. -#![cfg(all(target_os = "linux", feature = "noq"))] +#![cfg(target_os = "linux")] use std::net::UdpSocket; diff --git a/rs/moq-transcode/Cargo.toml b/rs/moq-transcode/Cargo.toml index 8fcb88f393..1b99d5dd82 100644 --- a/rs/moq-transcode/Cargo.toml +++ b/rs/moq-transcode/Cargo.toml @@ -44,7 +44,7 @@ tracing = { workspace = true } [dev-dependencies] anyhow = { workspace = true } -moq-tokio = { path = "../moq-tokio", default-features = false, features = ["noq", "aws-lc-rs", "websocket"] } +moq-tokio = { path = "../moq-tokio", default-features = false, features = ["aws-lc-rs", "websocket"] } # test-util unlocks tokio::time::pause for the deterministic live-feed tests. tokio = { workspace = true, features = ["full", "test-util"] } url = { workspace = true } diff --git a/rs/moq-uring/Cargo.toml b/rs/moq-uring/Cargo.toml index 8c0eeaeb6c..ccd2d45c16 100644 --- a/rs/moq-uring/Cargo.toml +++ b/rs/moq-uring/Cargo.toml @@ -53,7 +53,7 @@ tokio = { workspace = true, features = ["macros", "net", "rt", "sync", "time"] } io-uring = { workspace = true } libc = { workspace = true } moq-noq-udp = { workspace = true } -moq-tokio = { path = "../moq-tokio", default-features = false, features = ["noq", "aws-lc-rs"] } +moq-tokio = { path = "../moq-tokio", default-features = false, features = ["aws-lc-rs"] } nix = { workspace = true } rcgen = { version = "0.14", default-features = false, features = ["pem", "aws_lc_rs"] } rustls = "0.23" @@ -62,7 +62,8 @@ rustls = "0.23" serde_json = { workspace = true } tempfile = { workspace = true } tracing = "0.1" -web-transport-moq = { workspace = true } +# These test peers configure their own TLS instead of borrowing a tokio backend feature. +web-transport-moq = { workspace = true, features = ["aws-lc-rs"] } [[bench]] name = "udp_tokio" diff --git a/sh/rs/tokio-features.sh b/sh/rs/tokio-features.sh index b80de4f8e8..a1bbdb2ecb 100755 --- a/sh/rs/tokio-features.sh +++ b/sh/rs/tokio-features.sh @@ -6,23 +6,24 @@ set -euo pipefail cd "$(git rev-parse --show-toplevel)" -cargo clippy --locked -p moq-tokio --no-default-features -- -D warnings cargo clippy --locked -p moq-tokio --all-features -- -D warnings -cargo clippy --locked -p moq-tokio --no-default-features --features noq,aws-lc-rs -- -D warnings -cargo clippy --locked -p moq-tokio --no-default-features --features noq,ring -- -D warnings - -for feature in aws-lc-rs ring tcp uds websocket; do - cargo clippy --locked -p moq-tokio --no-default-features --features "$feature" -- -D warnings +for provider in aws-lc-rs ring; do + cargo clippy --locked -p moq-tokio --no-default-features --features "$provider" -- -D warnings + cargo nextest run --locked -p moq-tokio --no-default-features --features "$provider" --test backend +done +cargo bench --locked --profile dev -p moq-tokio --bench latency -- --test +for feature in tcp uds websocket; do + cargo clippy --locked -p moq-tokio --no-default-features --features "aws-lc-rs,$feature" -- -D warnings done # shellcheck disable=SC2016 # the backticks are literal, as rustc prints them want='a rustls QUIC backend requires a crypto provider: enable either the `aws-lc-rs` or `ring` feature' -if output=$(cargo check --locked -p moq-tokio --no-default-features --features noq 2>&1); then - echo "rs: moq-tokio with only noq should reject its missing crypto provider" >&2 +if output=$(cargo check --locked -p moq-tokio --no-default-features 2>&1); then + echo "rs: moq-tokio without a crypto provider should fail" >&2 exit 1 fi if ! grep -qF "$want" <<<"$output"; then - echo "rs: moq-tokio with only noq should fail on: $want" >&2 + echo "rs: moq-tokio should fail on: $want" >&2 echo "$output" | grep -E '^error' >&2 || true exit 1 fi