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

Filter by extension

Filter by extension


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

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

2 changes: 1 addition & 1 deletion crates/tuic-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "tuic-core"
version.workspace = true
repository.workspace = true
edition.workspace = true
description = "Backend-agnostic TUIC protocol codecs and state machine shared by wind-tuic and wind-tuiche"
description = "Backend-agnostic TUIC protocol codecs and state machine shared by wind-tuic's quinn and quiche backends"
license = "MIT OR Apache-2.0"

[features]
Expand Down
7 changes: 4 additions & 3 deletions crates/tuic-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//! Backend-agnostic TUIC protocol primitives shared by the quinn-based
//! [`wind-tuic`] crate and the tokio-quiche-based [`wind-tuiche`] crate.
//! Backend-agnostic TUIC protocol primitives shared by the `wind-tuic` crate's
//! quinn and quiche (tokio-quiche) backends.
//!
//! This crate deliberately has **no QUIC backend dependency**. It contains:
//!
Expand All @@ -10,7 +10,8 @@
//! ([`FragmentReassemblyBuffer`](udp::FragmentReassemblyBuffer)).
//!
//! The backend-specific glue (opening QUIC streams, sending datagrams, the
//! connection lifecycle) lives in the `wind-tuic` / `wind-tuiche` crates.
//! connection lifecycle) lives in the `wind-tuic` crate, generic over the
//! `wind-quic` QUIC abstraction.

pub mod proto;
pub mod udp;
Expand Down
10 changes: 5 additions & 5 deletions crates/tuic-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ ring = ["rustls/ring", "rcgen/ring", "quinn/rustls-ring"]
aws-lc-rs = ["dep:aws-lc-rs", "rustls/aws-lc-rs", "rcgen/aws_lc_rs", "quinn/rustls-aws-lc-rs"]
jemallocator = ["tikv-jemallocator"]
quinn = ["wind-tuic/quinn"]
# The tokio-quiche (`wind-tuiche`) backend. 64-bit only — tokio-quiche does not
# compile on 32-bit (its GSO path transmutes `u128` -> `Instant`). Enabled per
# target via `.github/target.toml`; do NOT enable on 32-bit targets.
quiche = ["dep:wind-tuiche"]
# The tokio-quiche backend, now hosted inside wind-tuic behind its `quiche`
# feature. 64-bit only — tokio-quiche does not compile on 32-bit (its GSO path
# transmutes `u128` -> `Instant`). Enabled per target via `.github/target.toml`;
# do NOT enable on 32-bit targets.
quiche = ["wind-tuic/quiche"]

[dependencies]

Expand All @@ -30,7 +31,6 @@ wind-base = { path = "../wind-base" }
wind-dns = { path = "../wind-dns" }
wind-socks = { path = "../wind-socks" }
wind-tuic = { path = "../wind-tuic", features = ["server"] }
wind-tuiche = { path = "../wind-tuiche", default-features = false, features = ["server"], optional = true }
wind-acme = { path = "../wind-acme" }

toml = "1.0"
Expand Down
1 change: 0 additions & 1 deletion crates/tuic-server/src/log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ pub fn init(config: &Config) -> Result<LogGuards> {
("wind_socks", LevelFilter::from(config.log_level)),
("wind_acme", LevelFilter::from(config.log_level)),
("wind_base", LevelFilter::from(config.log_level)),
("wind_tuiche", LevelFilter::from(config.log_level)),
("wind_dns", LevelFilter::from(config.log_level)),
])
.with_default(max(LogLevel::Info, config.log_level));
Expand Down
22 changes: 11 additions & 11 deletions crates/tuic-server/src/wind_adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ use wind_core::{
utils::{StackPrefer, is_private_ip},
};
use wind_socks::action::{Socks5Action, Socks5ActionOpts};
use wind_tuic::quinn::inbound::{TuicInbound, TuicInboundOpts};
// The quiche backend lives behind the `quiche` cargo feature (enabled per target
// via `.github/target.toml`).
#[cfg(feature = "quiche")]
use wind_tuiche::{TuicheInbound, TuicheInboundBuilder};
use wind_tuic::quiche::{TuicheInbound, TuicheInboundBuilder};
use wind_tuic::quinn::inbound::{TuicInbound, TuicInboundOpts};

// `CongestionController` is only referenced by the quiche backend wiring.
#[cfg(feature = "quiche")]
Expand Down Expand Up @@ -302,19 +302,19 @@ async fn create_quiche_inbound(ctx: &Arc<TuicAppContext>) -> eyre::Result<Tuiche
let (cert, key, acme_rx) = resolve_quiche_cert_files(ctx).await?;

let congestion_control = match quiche.congestion_control.controller {
CongestionController::Cubic => wind_tuiche::CongestionControl::Cubic,
CongestionController::Bbr | CongestionController::Bbr3 => wind_tuiche::CongestionControl::Bbr,
CongestionController::NewReno => wind_tuiche::CongestionControl::Reno,
CongestionController::Cubic => wind_tuic::quiche::CongestionControl::Cubic,
CongestionController::Bbr | CongestionController::Bbr3 => wind_tuic::quiche::CongestionControl::Bbr,
CongestionController::NewReno => wind_tuic::quiche::CongestionControl::Reno,
};

let opts = wind_tuiche::ConnectionOpts {
let opts = wind_tuic::quiche::ConnectionOpts {
max_idle_timeout: quiche.max_idle_time,
max_concurrent_bi_streams: quiche.max_concurrent_bi_streams,
max_concurrent_uni_streams: quiche.max_concurrent_uni_streams,
send_window: quiche.send_window,
receive_window: quiche.receive_window,
congestion_control,
udp_relay_mode: wind_tuiche::UdpRelayMode::Datagram,
udp_relay_mode: wind_tuic::quiche::UdpRelayMode::Datagram,
enable_0rtt: quiche.zero_rtt,
};

Expand All @@ -340,13 +340,13 @@ async fn create_quiche_inbound(ctx: &Arc<TuicAppContext>) -> eyre::Result<Tuiche
}

/// Background task: on every ACME (re)issuance, hot-reload the certificate into
/// the running quiche listener via its [`CertStore`](wind_tuiche::CertStore)
/// and refresh the on-disk PEM files (so a restart also picks up the latest
/// cert).
/// the running quiche listener via its
/// [`CertStore`](wind_tuic::quiche::CertStore) and refresh the on-disk PEM
/// files (so a restart also picks up the latest cert).
#[cfg(feature = "quiche")]
fn spawn_quiche_cert_reload(
ctx: &Arc<TuicAppContext>,
store: wind_tuiche::CertStore,
store: wind_tuic::quiche::CertStore,
mut rx: tokio::sync::watch::Receiver<Option<wind_acme::CertPem>>,
cert_path: String,
key_path: String,
Expand Down
7 changes: 4 additions & 3 deletions crates/tuic-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,12 @@ tokio-util = { version = "0.7", features = ["codec"] }

# The quiche e2e tests only *run* on 64-bit (cross-emulated 32-bit test execution
# is unreliable for real sockets). On 64-bit we enable tuic-server's `quiche`
# feature (so the tests exercise it) and pull wind-tuiche + a raw quinn client +
# rcgen for the cert-reload test. On 32-bit none of this is built and the quiche
# feature (so the tests exercise it), add wind-tuic's `quiche` feature (for the
# cert-reload test's direct `wind_tuic::quiche::TuicheInboundBuilder` use), and
# pull a raw quinn client + rcgen. On 32-bit none of this is built and the quiche
# test files compile to nothing. (The quiche backend itself builds on 32-bit too.)
[target.'cfg(target_pointer_width = "64")'.dependencies]
tuic-server = { path = "../tuic-server", default-features = false, features = ["quiche"] }
wind-tuiche = { path = "../wind-tuiche", default-features = false, features = ["server"] }
wind-tuic = { path = "../wind-tuic", default-features = false, features = ["quiche"] }
quinn = { workspace = true, default-features = false, features = ["runtime-tokio", "rustls-aws-lc-rs"] }
rcgen = { version = "0.14", default-features = false, features = ["crypto", "pem", "aws_lc_rs"] }
2 changes: 1 addition & 1 deletion crates/tuic-tests/tests/quiche_cert_reload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use rustls::{
pki_types::{CertificateDer, ServerName, UnixTime},
};
use wind_core::{AbstractInbound, InboundCallback, tcp::AbstractTcpStream, types::TargetAddr, udp::UdpStream};
use wind_tuiche::TuicheInboundBuilder;
use wind_tuic::quiche::TuicheInboundBuilder;

// ---- a no-op inbound callback (TLS handshake is all we need) --------------

Expand Down
11 changes: 9 additions & 2 deletions crates/wind-core/src/tcp.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
use tokio::io::{AsyncRead, AsyncWrite};

pub trait AbstractTcpStream: AsyncRead + AsyncWrite + Send + Sync + Unpin {}
/// A duplex byte stream relayed by the proxy.
///
/// `Sync` is intentionally **not** required: relay streams are always owned and
/// moved into a spawned task (which needs `Send`, not `Sync`). Requiring `Sync`
/// would exclude perfectly valid streams whose halves are joined from channel
/// senders/receivers — e.g. the quiche QUIC backend's `tokio_util::PollSender`,
/// which is `Send` but not `Sync`.
pub trait AbstractTcpStream: AsyncRead + AsyncWrite + Send + Unpin {}

impl<T> AbstractTcpStream for T where T: AsyncRead + AsyncWrite + Send + Sync + Unpin {}
impl<T> AbstractTcpStream for T where T: AsyncRead + AsyncWrite + Send + Unpin {}
2 changes: 2 additions & 0 deletions crates/wind-quic/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ quiche = [
"dep:boring",
"dep:boring-sys",
"dep:foreign-types-shared",
"dep:arc-swap",
]

[dependencies]
Expand Down Expand Up @@ -62,6 +63,7 @@ tokio-quiche = { version = "0.19", optional = true }
boring = { version = "4", default-features = false, optional = true }
boring-sys = { version = "4", default-features = false, optional = true }
foreign-types-shared = { version = "0.3", optional = true }
arc-swap = { version = "1", optional = true }

[dev-dependencies]
tokio = { version = "1", features = ["full", "macros", "rt-multi-thread"] }
Expand Down
24 changes: 21 additions & 3 deletions crates/wind-quic/src/quiche/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@
pub mod conn;
pub mod driver;
pub mod stream;
pub mod tls;

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

pub use conn::QuicheConnection;
use futures_util::StreamExt as _;
pub use stream::{QuicheRecv, QuicheSend};
pub use tls::CertStore;
use tokio::{net::UdpSocket, sync::mpsc};
use tokio_quiche::{
ConnectionParams,
Expand All @@ -27,7 +32,7 @@ use tracing::warn;
use crate::{
config::{CertSource, ClientTlsConfig, ServerTlsConfig, TransportConfig},
error::QuicError,
quiche::driver::BridgeDriver,
quiche::{driver::BridgeDriver, tls::CertReloadHook},
};

/// Map the shared congestion-control selector onto a quiche cc-algorithm name.
Expand Down Expand Up @@ -86,10 +91,19 @@ impl QuicheAcceptor {
///
/// The quiche backend loads TLS credentials from file paths, so `tls_cfg` must
/// use [`CertSource::PemPaths`].
///
/// When `cert_store` is `Some`, a per-handshake [`ConnectionHook`] serves the
/// store's *current* certificate, enabling live rotation (e.g. ACME renewal)
/// without restarting the listener. The certificate files in `tls_cfg` are
/// still required by tokio-quiche's API, but the hook supersedes them for the
/// actual TLS context.
///
/// [`ConnectionHook`]: tokio_quiche::quic::ConnectionHook
pub async fn bind_server(
addr: SocketAddr,
tls_cfg: &ServerTlsConfig,
transport: &TransportConfig,
cert_store: Option<&CertStore>,
) -> Result<QuicheAcceptor, QuicError> {
let (cert, key) = match &tls_cfg.cert {
CertSource::PemPaths { cert, key } => (cert.clone(), key.clone()),
Expand All @@ -107,14 +121,18 @@ pub async fn bind_server(
.local_addr()
.map_err(|e| QuicError::Endpoint(format!("local_addr: {e}")))?;

let hooks = Hooks {
connection_hook: cert_store.map(|s| Arc::new(CertReloadHook::new(s.clone())) as _),
};

let params = ConnectionParams::new_server(
quic_settings(transport),
TlsCertificatePaths {
cert: &cert,
private_key: &key,
kind: CertificateKind::X509,
},
Hooks { connection_hook: None },
hooks,
);

let mut listeners = tokio_quiche::listen([socket], params, DefaultMetrics)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
//! Hot-reloadable TLS for the tokio-quiche backend.
//! Hot-reloadable TLS for the quiche backend.
//!
//! tokio-quiche loads the certificate into the BoringSSL `SSL_CTX` **once** at
//! `listen()` time (see `tokio_quiche::settings::config`) and reuses that
//! context for every connection, so a renewed certificate on disk is never
//! picked up without a restart.
//! `listen()` time and reuses that context for every connection, so a renewed
//! certificate on disk is never picked up without a restart.
//!
//! We work around this through the only TLS customization seam tokio-quiche
//! exposes — [`ConnectionHook::create_custom_ssl_context_builder`]. The hook
Expand All @@ -13,9 +12,11 @@
//! handshakes with no listener restart; in-flight connections keep the
//! certificate they negotiated.
//!
//! Note: the cert is installed on the per-connection `SslRef` (not by swapping
//! the whole `SSL_CTX`), so quiche's own QUIC TLS method on the context stays
//! intact.
//! This is pure QUIC-TLS infrastructure (boring-based) — independent of any
//! application protocol — so it lives in `wind-quic` and is shared by every
//! quiche-backed server. `wind-quic` exposes it so quiche servers can opt into
//! live certificate rotation (e.g. ACME renewal); quinn/rustls has no
//! equivalent seam.

use std::sync::Arc;

Expand Down Expand Up @@ -82,13 +83,13 @@ impl CertStore {

/// A [`ConnectionHook`] that serves the current certificate from a
/// [`CertStore`] via a per-handshake `select_certificate` callback, enabling
/// live cert rotation on the tokio-quiche backend.
pub struct CertReloadHook {
/// live cert rotation on the quiche backend.
pub(crate) struct CertReloadHook {
store: CertStore,
}

impl CertReloadHook {
pub fn new(store: CertStore) -> Self {
pub(crate) fn new(store: CertStore) -> Self {
Self { store }
}
}
Expand Down
4 changes: 3 additions & 1 deletion crates/wind-quic/tests/loopback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,9 @@ async fn quiche_loopback() {
let (server_tls, client_tls, transport) = configs(&cert, &key);

let addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
let mut acceptor = quiche::bind_server(addr, &server_tls, &transport).await.expect("bind_server");
let mut acceptor = quiche::bind_server(addr, &server_tls, &transport, None)
.await
.expect("bind_server");
let local = acceptor.local_addr();

let server_fut = async move { acceptor.accept().await.expect("server conn") };
Expand Down
23 changes: 20 additions & 3 deletions crates/wind-tuic/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,20 @@ license = "MIT OR Apache-2.0"
default = ["server", "client", "quinn", "aws-lc-rs"]
decode = ["tuic-core/decode"]
encode = ["tuic-core/encode"]
server = ["decode"]
# The server decodes client requests AND encodes UDP response datagrams, so it
# needs both codec directions (matching the former wind-tuiche `server` feature).
server = ["decode", "encode"]
client = ["encode"]

# Quinn Backend
quinn = ["dep:quinn", "dep:quinn-congestions", "dep:tokio-rustls", "dep:rustls", "dep:rustls-platform-verifier"]
quinn = [
"wind-quic/quinn",
"dep:quinn",
"dep:quinn-congestions",
"dep:tokio-rustls",
"dep:rustls",
"dep:rustls-platform-verifier",
]
aws-lc-rs = [
"rustls/aws-lc-rs",
"quinn/rustls-aws-lc-rs",
Expand All @@ -26,12 +35,20 @@ ring = [
"quinn/rustls-ring"
]

# Quiche (tokio-quiche) backend — the TUIC protocol runs through the same
# backend-agnostic core over `wind_quic::quiche::QuicheConnection`.
quiche = ["wind-quic/quiche"]

[dependencies]
wind-core = { version = "0.1.1", path = "../wind-core", features = ["quic"] }
tuic-core = { version = "0.1.1", path = "../tuic-core", default-features = false }
# Backend-agnostic QUIC abstraction. The TUIC protocol core is generic over
# `wind_quic::QuicConnection`; the `quinn`/`quiche` features enable the matching
# wind-quic backend.
wind-quic = { version = "0.1.1", path = "../wind-quic", default-features = false }

# Async
tokio = { version = "1", default-features = false, features = ["net", "time", "rt", "sync"] }
tokio = { version = "1", default-features = false, features = ["net", "time", "rt", "sync", "io-util", "macros"] }
tokio-util = { version = "0.7", features = ["codec"] }
tokio-stream = "0.1"
futures-util = { version = "0.3", default-features = false, features = ["sink"] }
Expand Down
Loading
Loading