feat: extract tuic-core, add tokio-quiche backend, runtime backend switch#15
Merged
Conversation
…itch
Split the TUIC implementation into a shared, backend-agnostic core and two
interchangeable QUIC backends, then wire runtime backend selection into
tuic-server.
tuic-core (new):
- Backend-agnostic TUIC protocol primitives shared by both backends:
wire codecs (header/command/address), the pure decode helpers used on the
hot path, and the UDP fragment-reassembly state machine.
- ProtoError decoupled from quinn.
wind-tuic:
- Removed the placeholder quiche backend; re-exports tuic_core::proto so
existing wind_tuic::proto::* paths keep working. Keeps the quinn-specific
ClientProtoExt and the quinn-coupled UdpStream send path.
wind-tuiche (new): tokio-quiche backend
- Real TUIC server over tokio-quiche's ApplicationOverQuic worker
(crates/wind-tuiche/src/driver.rs): auth, TCP CONNECT relay (bridged to
wind-core via a channel-backed duplex stream), native UDP relay over
RFC 9221 datagrams using the shared fragment buffer, heartbeat, dissociate.
- Pinned to tokio-quiche 0.18 to keep serde_with/quiche aligned with the
workspace. Known limitation: quiche does not expose the RFC 5705 keying
material exporter, so TUIC token verification is UUID-gated (documented).
tuic-server:
- New [backend] config section: backend.mode = "quinn" | "quiche" with
per-backend tuning under backend.quinn / backend.quiche.
- create_inbound dispatches on backend.mode (ServerInbound::{Tuic, Tuiche}).
- Legacy top-level [quic] section and flat keys migrate into backend.quinn,
so existing configs keep working.
tests:
- New crates/tuic-tests/tests/protocol_tests.rs (14 tests): wire-format
stability, decode-helper/codec agreement, malformed-input handling,
TargetAddr<->Address conversions, UDP reassembly via tuic-core, and the
[backend] config surface.
- Backend-config unit tests in tuic-server.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tokio-quiche (added with the wind-tuiche backend) newly pulls datagram-socket into the dependency tree. Re-enable the vendored patch that works around the libc::msghdr private-padding-field build failure on musl targets (libc >= 0.2.169), so cross-compiled musl builds keep working. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…al exporter quiche built with `boringssl-boring-crate` (which tokio-quiche enables) exposes the underlying SSL object via `impl AsMut<boring::ssl::SslRef> for Connection`, and tokio-quiche's `QuicheConnection` is that `quiche::Connection`. So the RFC 5705 exporter IS reachable from inside `ApplicationOverQuic::process_reads`. Replace the previous UUID-only auth gate with real token verification: recompute the token from the live session (label = UUID bytes, context = password) and compare in constant time, with the same dummy-password timing mitigation as the quinn backend. The BoringSSL FFI is called directly (rather than the safe `SslRef::export_keying_material`) because TUIC's label is the raw, non-UTF-8 UUID bytes and the safe wrapper requires a `&str`. Adds boring / boring-sys / foreign-types-shared (the same builds tokio-quiche already pulls). Updates the driver/README/config docs that claimed the exporter was unavailable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The quiche (wind-tuiche / tokio-quiche) backend reads TLS material from files,
so previously it only accepted explicit `tls.certificate` / `tls.private_key`
paths. Materialise certs to disk so `auto_ssl` (ACME) and `self_sign` work too.
wind-acme:
- Wrap `DirCache` in a `CapturingCache` that also publishes the issued/cached
cert PEM blob (PKCS#8 key + chain) to a `watch` channel.
- Add `start_acme_with_cert` returning `(resolver, watch::Receiver<Option<CertPem>>)`;
`start_acme` stays a thin wrapper for the quinn path.
tuic-server (quiche backend):
- `resolve_quiche_cert_files` materialises the cert/key to `<data_dir>/wind-tuiche.{cert,key}.pem`:
- ACME: wait for the first cert (cached or freshly issued), split the PEM into
cert/key files, and refresh them on renewal (restart applies — tokio-quiche
reads files once at listen time).
- self-sign: generate via rcgen and write PEM files.
- otherwise: use the configured file paths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause of the no-reload limitation: tokio-quiche loads the certificate into the BoringSSL SSL_CTX once at listen() time (settings/config.rs) and reuses that context for every connection, so renewed cert files were never re-read. Fix: use tokio-quiche's only TLS seam, `ConnectionHook::create_custom_ssl_context_builder`. The hook installs a per-handshake `select_certificate` callback that serves the current cert from a hot-swappable `CertStore` (arc-swap of parsed X509/chain/PKey). The cert is set on the per-connection `SslRef` (set_certificate/set_private_key/add_chain_cert) rather than by swapping the SSL_CTX, so quiche's QUIC TLS method on the context is preserved. - wind-tuiche: new `tls` module (`CertStore`, `CertReloadHook`); the inbound seeds the store from the cert/key files at build time, wires the hook into `Hooks`, and exposes `TuicheInbound::cert_store()` for live rotation. - tuic-server: the ACME renewal task now calls `cert_store.update(...)` to hot-reload the renewed certificate into the running quiche listener (and still refreshes the on-disk files for restart persistence) — no restart needed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Also wire 0-RTT into wind-tuiche (the inbound now maps ConnectionOpts.enable_0rtt to QuicSettings.enable_early_data and sizes the flow-control windows from the receive window). New tests (in tuic-tests; one client per file because tuic_client::run installs a process-global connection): - quiche_integration.rs: end-to-end TCP + UDP relay through a quiche-backed tuic-server + quinn tuic-client (self-signed). Exercises the handshake, the RFC 5705 exporter auth, the ApplicationOverQuic worker, and the relay. - quiche_zero_rtt.rs: the 0-RTT-enabled config path relays correctly. - quiche_cert_reload.rs: a raw quinn client reads the served leaf cert before and after CertStore::update; asserts it changes — proving live cert rotation with no listener restart. Shared helpers (config builders + start_quiche_pair + install_crypto_provider) live in tuic-tests' lib. All three files pass; the quiche relay test confirms the wind-tuiche driver works end-to-end. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The quiche integration test's UDP relay failed on CI Linux runners with "socks5 server UDP associate dual-stack socket setting error: Protocol not available (os error 92)": the test client used `local.dual_stack = Some(false)`, which makes the SOCKS5 UDP-associate socket call `set_only_v6(true)` on its IPv4 socket — rejected with ENOPROTOOPT on runners without usable IPv6. Use `dual_stack = None` in the test client config so the dual-stack setsockopt is skipped entirely. TCP relay (and the rest of the quiche path) was already passing on CI; this makes the UDP assertion pass there too. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… in CI Replace the ad-hoc `cfg(target_pointer_width="64")` gating with a proper `quiche` cargo feature on tuic-server, controlled per-target via `.github/target.toml`. tokio-quiche only compiles on 64-bit (its GSO path transmutes `u128` -> `Instant`), so the quiche backend is opt-in and enabled only for 64-bit targets. - wind-tuiche: move tokio-quiche/boring/boring-sys/foreign-types-shared into a `[target.'cfg(target_pointer_width = "64")'.dependencies]` section and gate the backend modules (inbound/driver/stream/tls/outbound) on `cfg(all(feature = ..., target_pointer_width = "64"))`. On 32-bit the crate compiles to just the backend-agnostic `proto`/`udp` re-exports, so the `cargo --all` member build succeeds everywhere. - tuic-server: `wind-tuiche` becomes an optional dep behind a new `quiche` feature; the quiche backend code in wind_adapter is gated on `cfg(feature = "quiche")` (with a clear runtime error if `backend.mode = "quiche"` is selected in a build without the feature). - .github/target.toml: 64-bit targets get `extra-args = "--features tuic-server/quiche"`; 32-bit targets (i686, armv7) don't. - tuic-tests: enable tuic-server/quiche + pull wind-tuiche/quinn/rcgen only on 64-bit; the quiche test files are already 64-bit-gated. Verified: i686 builds wind-tuiche empty (no tokio-quiche) and tuic-server quinn-only; 64-bit builds with the feature and all quiche tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tokio-quiche 0.19 requires serde_with ^3.20, which previously conflicted with the quinn fork's `qlog` feature (qlog 0.17 -> serde_with ~3.17) and forced the 0.18 pin. That `qlog` feature was enabled on the quinn deps but never used in code, so drop it from tuic-server and tuic-client. With qlog 0.17 gone the graph resolves cleanly: tokio-quiche 0.18 -> 0.19.0 quiche 0.28 -> 0.29.1 qlog 0.17 -> 0.18.0 (now only via tokio-quiche) serde_with 3.17 -> 3.21.0 No source changes were needed — the tokio-quiche API (listen / ConnectionParams / ApplicationOverQuic / AsMut<SslRef> / ConnectionHook) is unchanged across the bump, and the vendored datagram-socket patch (0.8.0) still matches. The quiche e2e / 0-RTT / cert-hot-reload tests and the quinn e2e all pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tokio-quiche only compiles on 64-bit because src/quic/io/gso.rs (the Linux GSO/SO_TXTIME path) transmutes `u128` -> `std::time::Instant` to get the monotonic-clock epoch, which is only valid where `Instant` is 16 bytes. Vendor tokio-quiche 0.19.0 under patches/tokio-quiche and replace that transmute with a portable `clock_gettime(CLOCK_MONOTONIC)`-correlated conversion, wired via [patch.crates-io]. Only gso.rs differs from upstream. - patches/tokio-quiche: vendored lib-only copy (examples/tests/docs removed), carries the gso.rs fix and a `disable_all_formatting` rustfmt.toml so the wind style never rewrites it. Excluded from the workspace (root `exclude`) so fmt/clippy/-D warnings don't apply to upstream code. - wind-tuiche: tokio-quiche/boring/etc. deps and the backend modules are no longer target-gated to 64-bit (the patch makes 32-bit build). - tuic-server: quiche backend stays behind the `quiche` cargo feature. - .github/target.toml: enable `--features tuic-server/quiche` on the 32-bit targets too (i686-linux, armv7-linux, armv7-linux-hf, i686-windows). The e2e tests still only *run* on 64-bit (cross-emulated 32-bit socket tests are unreliable); this change is validated by the 32-bit build/compile in CI. 64-bit build, clippy, fmt and all quiche tests pass locally. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The prepare job runs a typos spell-checker over the repo. The vendored tokio-quiche copy under patches/ carries upstream typos (readyness, clsoe, Inboud, ...); exclude patches/ so third-party code we only patch minimally isn't spell-checked. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
Splits the TUIC implementation into a shared, backend-agnostic core (
tuic-core) plus two interchangeable QUIC backends — the existing quinn-basedwind-tuicand a new tokio-quiche-basedwind-tuiche— and adds runtime backend selection totuic-servervia a[backend]config section.What changed
tuic-core(new crate)Backend-agnostic TUIC primitives shared by both backends: wire codecs (header / command / address), the pure
decode_*helpers, the UDP fragment-reassembly state machine (FragmentReassemblyBuffer), and a quinn-decoupledProtoError.wind-tuicRemoved the old placeholder
quichebackend;protonow re-exportstuic_core::proto::*so existing paths keep working. Keeps the quinn-specificClientProtoExtand the quinn-coupledUdpStreamsend path (reassembly delegates totuic-core).wind-tuiche(new crate) — tokio-quiche backendApplicationOverQuicworker (src/driver.rs): auth, TCPCONNECTrelay (bridged towind-corevia a channel-backed duplexQuicheStream), native UDP relay over RFC 9221 datagrams using the shared fragment buffer, heartbeat / dissociate.impl AsMut<boring::ssl::SslRef> for Connectionunderboringssl-boring-crate.src/tls.rs): aConnectionHookinstalls a per-handshakeselect_certificatecallback that serves the current cert from a hot-swappableCertStore, so certificate rotation needs no listener restart. The cert is set on the per-connectionSslRef, keeping quiche's QUIC TLS method on the context intact.serde_with/quichealigned with the workspace. The client (outbound) path is still a config-only placeholder.wind-acmeAdded a
CapturingCache(wrappingDirCache) that publishes the issued/cached cert PEM blob via awatchchannel, exposed through a newstart_acme_with_cert.start_acmestays a thin resolver-only wrapper for the quinn path.tuic-server— runtime backend switch[backend]section:backend.mode = "quinn" | "quiche", with per-backend tuning underbackend.quinn/backend.quiche.create_inbounddispatches onbackend.mode(ServerInbound::{Tuic, Tuiche}).auto_ssl(ACME) /self_sign/ explicit paths, and an ACME renewal task hot-reloads the renewed cert into the running listener viaCertStore::update.[quic]section and the older flat keys migrate intobackend.quinn.Build
Re-enabled the vendored
datagram-socketpatch (newly pulled in by tokio-quiche) to keep musl cross-compilation building.Tests
crates/tuic-tests/tests/protocol_tests.rs(14 tests): wire-format stability, decode-helper/codec agreement, malformed-input handling,TargetAddr ↔ Addressconversions, UDP reassembly viatuic-core, and the[backend]config surface.tuic-server.Reviewer notes
backend.mode = "quiche"supportsauto_ssl(ACME),self_sign, and explicit cert/key paths. ACME renewals hot-reload into the running listener with no restart (via the per-handshake cert callback); the on-disk<data_dir>/wind-tuiche.{cert,key}.pemfiles are also refreshed for restart persistence.backend.modedefaults toquinn.wind-tuichedriver + TLS hook build against the full BoringSSL stack and are unit-tested, but have not had an end-to-end run against a live TUIC client / live cert rotation in this change.cargo +nightly fmt,cargo clippy --workspace(clean), and the touched crates' tests all pass.🤖 Generated with Claude Code