From 7f57eb9dcf94d8b506db8e72c2a9500c75be1fa6 Mon Sep 17 00:00:00 2001 From: iHsin Date: Thu, 25 Jun 2026 07:43:44 +0800 Subject: [PATCH] feat(quic): per-algorithm congestion-control tuning Thread a backend-neutral `CongestionTuning` through `TransportConfig` so each QUIC backend can apply per-algorithm knobs, instead of only selecting an algorithm name. - tokio-quiche patch: add `enable_cubic_idle_restart_fix` and a `BbrParamsField` (`Option`) to `QuicSettings`, applied in `make_quiche_config`. The latter wraps the quiche type in a newtype because the `#[settings]` macro requires every field to impl `Settings` (which `quiche::BbrParams` does not). Guard the `set_custom_bbr_params` call behind `quiche_internal`. - wind-quic: add a serde-able, backend-neutral `Bbr2gcConfig` (the full experimental quiche `BbrParams` surface), `BbrBwLoReductionStrategy`, and a `CongestionTuning` aggregate (initial cwnd packets, pacing, max pacing rate, HyStart++, CUBIC idle-restart-fix, BBR params) on `TransportConfig`. Map them onto `QuicSettings` in the quiche backend (converting the neutral BBR config to `quiche::BbrParams`). Enable the tokio-quiche `quiche_internal` feature. - wind-tuic: carry `CongestionTuning` on the quiche `ConnectionOpts` (passed through `to_transport`) and add `newreno_loss_reduction_factor` to the quinn inbound opts; re-export the neutral config types. These are consumed by the breeze tuic-server's new `[backend..cc]` config sections. No behavior change for existing callers: every new knob is optional and defaults to leaving the backend default in place. Co-Authored-By: Claude Opus 4.8 --- Cargo.lock | 1 + crates/tuic-server/src/wind_adapter.rs | 3 + crates/wind-quic/Cargo.toml | 9 +- crates/wind-quic/src/config.rs | 116 ++++++++++++++++++++ crates/wind-quic/src/lib.rs | 4 +- crates/wind-quic/src/quiche/mod.rs | 64 ++++++++++- crates/wind-tuic/src/lib.rs | 6 + crates/wind-tuic/src/quiche/utils.rs | 7 +- crates/wind-tuic/src/quinn/inbound.rs | 8 ++ patches/tokio-quiche/src/settings/config.rs | 11 ++ patches/tokio-quiche/src/settings/quic.rs | 50 +++++++++ 11 files changed, 274 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8947950..ce3c49e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6150,6 +6150,7 @@ dependencies = [ "rustls", "rustls-pemfile", "rustls-platform-verifier", + "serde", "tempfile", "test-log", "thiserror 2.0.18", diff --git a/crates/tuic-server/src/wind_adapter.rs b/crates/tuic-server/src/wind_adapter.rs index 690af96..d6be4ee 100644 --- a/crates/tuic-server/src/wind_adapter.rs +++ b/crates/tuic-server/src/wind_adapter.rs @@ -318,6 +318,9 @@ async fn create_quiche_inbound(ctx: &Arc) -> eyre::Result, + /// BBR startup pacing gain. + pub startup_pacing_gain: Option, + /// BBR full-bandwidth threshold. + pub full_bw_threshold: Option, + /// Rounds to stay in STARTUP before exiting on a bandwidth plateau. + pub startup_full_bw_rounds: Option, + /// Loss count needed to exit STARTUP. + pub startup_full_loss_count: Option, + /// BBR drain cwnd gain. + pub drain_cwnd_gain: Option, + /// BBR drain pacing gain. + pub drain_pacing_gain: Option, + /// Respect Reno coexistence. + pub enable_reno_coexistence: Option, + /// Avoid overestimating bandwidth on ack compression. + pub enable_overestimate_avoidance: Option, + /// Enable the `a0` point fix in the bandwidth sampler. + pub choose_a0_point_fix: Option, + /// PROBE_BW up-phase pacing gain. + pub probe_bw_probe_up_pacing_gain: Option, + /// PROBE_BW down-phase pacing gain. + pub probe_bw_probe_down_pacing_gain: Option, + /// PROBE_BW DOWN/CRUISE/REFILL cwnd gain. + pub probe_bw_cwnd_gain: Option, + /// PROBE_BW UP cwnd gain. + pub probe_bw_up_cwnd_gain: Option, + /// PROBE_RTT pacing gain. + pub probe_rtt_pacing_gain: Option, + /// PROBE_RTT cwnd gain. + pub probe_rtt_cwnd_gain: Option, + /// Rounds to stay in PROBE_BW up if bytes-in-flight doesn't drop below + /// target. + pub max_probe_up_queue_rounds: Option, + /// BBR loss threshold. + pub loss_threshold: Option, + /// Use bytes-delivered as the estimate for `inflight_hi`. + pub use_bytes_delivered_for_inflight_hi: Option, + /// Decrease startup pacing at round end. + pub decrease_startup_pacing_at_end_of_round: Option, + /// Bandwidth-`lo` reduction strategy. + pub bw_lo_reduction_strategy: Option, + /// Count app-limited rounds with no bandwidth growth toward the + /// exit-startup rounds threshold. + pub ignore_app_limited_for_no_bandwidth_growth: Option, + /// Initial pacing rate (bytes/sec) before an RTT estimate is available. + pub initial_pacing_rate_bytes_per_second: Option, + /// Scale the pacing rate when the MSS changes during PMTUD. + pub scale_pacing_rate_by_mss: Option, + /// Disable the `has_stayed_long_enough_in_probe_down` early exit. + pub disable_probe_down_early_exit: Option, + /// Set the expected packet send time to `now` instead of the computed + /// next-release time. + pub time_sent_set_to_now: Option, +} + +/// Backend-neutral congestion-control tuning carried by [`TransportConfig`], +/// alongside the algorithm selector ([`TransportConfig::congestion`]). +/// +/// Each backend applies the subset it understands. Today only the quiche +/// backend consumes these; the quinn backend reads +/// [`TransportConfig::initial_window`] and ignores the rest. +#[derive(Clone, Debug, Default)] +pub struct CongestionTuning { + /// Initial congestion window, in packets (quiche). `None` = backend + /// default. + pub initial_cwnd_packets: Option, + /// Enable pacing of outgoing packets (quiche). + pub pacing: Option, + /// Maximum pacing rate, in bytes/sec (quiche). `None` = unlimited. + pub max_pacing_rate: Option, + /// Enable HyStart++ — only with cubic/reno (quiche). + pub hystart: Option, + /// Enable the CUBIC idle-restart fix — only with cubic (quiche). + pub cubic_idle_restart_fix: Option, + /// Custom BBR parameters — only with the BBR controller (quiche). + pub bbr: Option, +} + /// QUIC transport tuning, shared by both backends. #[derive(Clone, Debug)] pub struct TransportConfig { @@ -31,7 +141,12 @@ pub struct TransportConfig { /// Congestion-control algorithm. pub congestion: QuicCongestionControl, /// Initial congestion window in bytes. `None` uses the backend default. + /// (quinn; the quiche backend uses + /// [`CongestionTuning::initial_cwnd_packets`]). pub initial_window: Option, + /// Per-algorithm congestion-control tuning. Each backend applies the subset + /// it supports. + pub cc: CongestionTuning, /// Advertise QUIC DATAGRAM (RFC 9221) support. pub enable_datagram: bool, /// Allow 0-RTT early data (resumption). @@ -53,6 +168,7 @@ impl Default for TransportConfig { gso: false, congestion: QuicCongestionControl::default(), initial_window: None, + cc: CongestionTuning::default(), enable_datagram: true, enable_0rtt: false, alpn: vec![b"h3".to_vec()], diff --git a/crates/wind-quic/src/lib.rs b/crates/wind-quic/src/lib.rs index e701232..d9c173c 100644 --- a/crates/wind-quic/src/lib.rs +++ b/crates/wind-quic/src/lib.rs @@ -36,7 +36,9 @@ pub mod error; pub mod prefixed; pub mod traits; -pub use config::{CertSource, ClientTlsConfig, ServerTlsConfig, TransportConfig}; +pub use config::{ + Bbr2gcConfig, BbrBwLoReductionStrategy, CertSource, ClientTlsConfig, CongestionTuning, ServerTlsConfig, TransportConfig, +}; pub use error::{QuicError, Result}; pub use prefixed::PrefixedRecv; pub use traits::{QuicConnection, QuicRecvStream, QuicSendStream}; diff --git a/crates/wind-quic/src/quiche/mod.rs b/crates/wind-quic/src/quiche/mod.rs index cab55f5..5124263 100644 --- a/crates/wind-quic/src/quiche/mod.rs +++ b/crates/wind-quic/src/quiche/mod.rs @@ -24,7 +24,7 @@ use tokio_quiche::{ ConnectionParams, metrics::DefaultMetrics, quic::connect_with_config, - settings::{CertificateKind, Hooks, QuicSettings, TlsCertificatePaths}, + settings::{BbrParamsField, CertificateKind, Hooks, QuicSettings, TlsCertificatePaths}, socket::Socket, }; use tracing::warn; @@ -46,6 +46,47 @@ fn cc_name(cc: crate::config::QuicCongestionControl) -> &'static str { } } +/// Convert the backend-neutral [`Bbr2gcConfig`](crate::config::Bbr2gcConfig) +/// onto quiche's native experimental `BbrParams`. +fn quiche_bbr_params(c: &crate::config::Bbr2gcConfig) -> tokio_quiche::quiche::BbrParams { + use tokio_quiche::quiche::BbrBwLoReductionStrategy as QuicheStrategy; + + use crate::config::BbrBwLoReductionStrategy as Strategy; + tokio_quiche::quiche::BbrParams { + startup_cwnd_gain: c.startup_cwnd_gain, + startup_pacing_gain: c.startup_pacing_gain, + full_bw_threshold: c.full_bw_threshold, + startup_full_bw_rounds: c.startup_full_bw_rounds, + startup_full_loss_count: c.startup_full_loss_count, + drain_cwnd_gain: c.drain_cwnd_gain, + drain_pacing_gain: c.drain_pacing_gain, + enable_reno_coexistence: c.enable_reno_coexistence, + enable_overestimate_avoidance: c.enable_overestimate_avoidance, + choose_a0_point_fix: c.choose_a0_point_fix, + probe_bw_probe_up_pacing_gain: c.probe_bw_probe_up_pacing_gain, + probe_bw_probe_down_pacing_gain: c.probe_bw_probe_down_pacing_gain, + probe_bw_cwnd_gain: c.probe_bw_cwnd_gain, + probe_bw_up_cwnd_gain: c.probe_bw_up_cwnd_gain, + probe_rtt_pacing_gain: c.probe_rtt_pacing_gain, + probe_rtt_cwnd_gain: c.probe_rtt_cwnd_gain, + max_probe_up_queue_rounds: c.max_probe_up_queue_rounds, + loss_threshold: c.loss_threshold, + use_bytes_delivered_for_inflight_hi: c.use_bytes_delivered_for_inflight_hi, + decrease_startup_pacing_at_end_of_round: c.decrease_startup_pacing_at_end_of_round, + bw_lo_reduction_strategy: c.bw_lo_reduction_strategy.map(|s| match s { + Strategy::Default => QuicheStrategy::Default, + Strategy::MinRtt => QuicheStrategy::MinRttReduction, + Strategy::Inflight => QuicheStrategy::InflightReduction, + Strategy::Cwnd => QuicheStrategy::CwndReduction, + }), + ignore_app_limited_for_no_bandwidth_growth: c.ignore_app_limited_for_no_bandwidth_growth, + initial_pacing_rate_bytes_per_second: c.initial_pacing_rate_bytes_per_second, + scale_pacing_rate_by_mss: c.scale_pacing_rate_by_mss, + disable_probe_down_early_exit: c.disable_probe_down_early_exit, + time_sent_set_to_now: c.time_sent_set_to_now, + } +} + /// Translate the backend-neutral [`TransportConfig`] into a [`QuicSettings`]. fn quic_settings(t: &TransportConfig) -> QuicSettings { let mut s = QuicSettings::default(); @@ -62,6 +103,27 @@ fn quic_settings(t: &TransportConfig) -> QuicSettings { s.enable_dgram = t.enable_datagram; s.enable_early_data = t.enable_0rtt; s.alpn = t.alpn.clone(); + + // Per-algorithm congestion-control tuning. `None` leaves quiche's default. + let cc = &t.cc; + if let Some(packets) = cc.initial_cwnd_packets { + s.initial_congestion_window_packets = packets; + } + if let Some(pacing) = cc.pacing { + s.enable_pacing = pacing; + } + if let Some(rate) = cc.max_pacing_rate { + s.max_pacing_rate = Some(rate); + } + if let Some(hystart) = cc.hystart { + s.enable_hystart = hystart; + } + if let Some(fix) = cc.cubic_idle_restart_fix { + s.enable_cubic_idle_restart_fix = fix; + } + if let Some(bbr) = &cc.bbr { + s.custom_bbr_params = BbrParamsField(Some(quiche_bbr_params(bbr))); + } s } diff --git a/crates/wind-tuic/src/lib.rs b/crates/wind-tuic/src/lib.rs index dbc8e84..45b9d4c 100644 --- a/crates/wind-tuic/src/lib.rs +++ b/crates/wind-tuic/src/lib.rs @@ -8,6 +8,12 @@ pub mod proto; +// Backend-neutral congestion-control config types (defined in `wind-quic`), +// re-exported so config front-ends (e.g. the TUIC server) can build a +// [`CongestionTuning`] and per-algorithm tuning without depending on +// `wind-quic` directly. Available regardless of which backend feature is on. +pub use wind_quic::{Bbr2gcConfig, BbrBwLoReductionStrategy, CongestionTuning}; + #[cfg(feature = "server")] pub mod active; #[cfg(feature = "server")] diff --git a/crates/wind-tuic/src/quiche/utils.rs b/crates/wind-tuic/src/quiche/utils.rs index 09fb627..8392e03 100644 --- a/crates/wind-tuic/src/quiche/utils.rs +++ b/crates/wind-tuic/src/quiche/utils.rs @@ -6,7 +6,7 @@ use std::{ time::Duration, }; -use wind_quic::QuicCongestionControl; +use wind_quic::{CongestionTuning, QuicCongestionControl}; /// Congestion control algorithm. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] @@ -79,6 +79,9 @@ pub struct ConnectionOpts { pub receive_window: u64, /// Congestion control algorithm. pub congestion_control: CongestionControl, + /// Per-algorithm congestion-control tuning (initial window, pacing, + /// HyStart++, CUBIC idle-restart-fix, custom BBR params). + pub cc: CongestionTuning, /// UDP relay mode. pub udp_relay_mode: UdpRelayMode, /// Enable 0-RTT. @@ -94,6 +97,7 @@ impl Default for ConnectionOpts { send_window: 8 * 1024 * 1024, // 8 MB receive_window: 8 * 1024 * 1024, // 8 MB congestion_control: CongestionControl::default(), + cc: CongestionTuning::default(), udp_relay_mode: UdpRelayMode::default(), enable_0rtt: true, } @@ -114,6 +118,7 @@ impl ConnectionOpts { enable_datagram: matches!(self.udp_relay_mode, UdpRelayMode::Datagram), enable_0rtt: self.enable_0rtt, alpn: vec![b"h3".to_vec()], + cc: self.cc.clone(), ..Default::default() } } diff --git a/crates/wind-tuic/src/quinn/inbound.rs b/crates/wind-tuic/src/quinn/inbound.rs index 15e9109..8a1b0bd 100644 --- a/crates/wind-tuic/src/quinn/inbound.rs +++ b/crates/wind-tuic/src/quinn/inbound.rs @@ -70,6 +70,10 @@ pub struct TuicInboundOpts { /// of slow-start faster instead of trickling the first few round trips. pub initial_window: u64, + /// NewReno loss-reduction factor (β). Only applies to the `newreno` + /// controller; `None` keeps quinn's default. Other controllers ignore it. + pub newreno_loss_reduction_factor: Option, + /// HTTP/3 masquerade. When `Some`, connections that aren't TUIC (their /// first stream byte isn't `0x05`) are served as a reverse-proxy HTTP/3 /// web server instead of being dropped. @@ -106,6 +110,7 @@ impl Default for TuicInboundOpts { gso: true, congestion_control: CongestionControl::Bbr, initial_window: 1024 * 1024, + newreno_loss_reduction_factor: None, masquerade: None, hooks: InboundHooks::default(), active: None, @@ -200,6 +205,9 @@ impl TuicInbound { CongestionControl::NewReno => { let mut cfg = quinn::congestion::NewRenoConfig::default(); cfg.initial_window(iw); + if let Some(factor) = self.opts.newreno_loss_reduction_factor { + cfg.loss_reduction_factor(factor); + } Arc::new(cfg) } } diff --git a/patches/tokio-quiche/src/settings/config.rs b/patches/tokio-quiche/src/settings/config.rs index a3a6e14..984e3f4 100644 --- a/patches/tokio-quiche/src/settings/config.rs +++ b/patches/tokio-quiche/src/settings/config.rs @@ -178,6 +178,17 @@ fn make_quiche_config( config.discover_pmtu(quic_settings.discover_path_mtu); config.set_pmtud_max_probes(quic_settings.pmtud_max_probes); config.enable_hystart(quic_settings.enable_hystart); + config.set_enable_cubic_idle_restart_fix( + quic_settings.enable_cubic_idle_restart_fix, + ); + + // Custom BBR (gcongestion) tuning. `set_custom_bbr_params` is gated behind + // quiche's `internal` feature (our `quiche_internal`); when that feature is + // off the params are simply ignored so the crate still builds. + #[cfg(feature = "quiche_internal")] + if let Some(bbr_params) = quic_settings.custom_bbr_params.0 { + config.set_custom_bbr_params(bbr_params); + } config.enable_pacing(quic_settings.enable_pacing); if let Some(max_pacing_rate) = quic_settings.max_pacing_rate { diff --git a/patches/tokio-quiche/src/settings/quic.rs b/patches/tokio-quiche/src/settings/quic.rs index c163f9d..924a3bc 100644 --- a/patches/tokio-quiche/src/settings/quic.rs +++ b/patches/tokio-quiche/src/settings/quic.rs @@ -31,6 +31,35 @@ use std::time::Duration; pub use qlog::writer::QlogCompression; +/// Programmatically-set holder for quiche's experimental [`quiche::BbrParams`]. +/// +/// [`QuicSettings`] is built by the `#[settings]` macro, which requires every +/// field type to implement [`foundations::settings::Settings`] (and therefore +/// `Serialize`/`Deserialize`). `quiche::BbrParams` implements neither, so it +/// cannot be embedded directly. This newtype wraps it and supplies trivial, +/// never-exercised serde impls (the [`QuicSettings::custom_bbr_params`] field is +/// `#[serde(skip)]`) plus an empty `Settings` impl, so the BBR params can ride +/// inside `QuicSettings` while still being set purely in code. +#[derive(Clone, Debug, Default)] +pub struct BbrParamsField(pub Option); + +impl serde::Serialize for BbrParamsField { + fn serialize(&self, serializer: S) -> Result { + // Set programmatically; never serialized (the field is `serde(skip)`). + serializer.serialize_none() + } +} + +impl<'de> serde::Deserialize<'de> for BbrParamsField { + fn deserialize>(deserializer: D) -> Result { + // Ignore any input; BBR params are injected programmatically. + serde::de::IgnoredAny::deserialize(deserializer)?; + Ok(Self(None)) + } +} + +impl foundations::settings::Settings for BbrParamsField {} + /// QUIC configuration parameters. #[serde_as] #[settings] @@ -203,6 +232,22 @@ pub struct QuicSettings { #[serde(default = "QuicSettings::default_enable_hystart")] pub enable_hystart: bool, + /// Whether to enable the CUBIC idle-restart fix (only with `cubic` CC). + /// + /// Mirrors `quiche::Config::set_enable_cubic_idle_restart_fix`. Defaults to + /// `true` (quiche's own default). + #[serde(default = "QuicSettings::default_enable_cubic_idle_restart_fix")] + pub enable_cubic_idle_restart_fix: bool, + + /// Custom BBR (`bbr2_gcongestion`) parameters applied via + /// `quiche::Config::set_custom_bbr_params`. Only takes effect when + /// `cc_algorithm` resolves to a BBR variant *and* the crate is built with + /// the `quiche_internal` feature. Set programmatically (never from config + /// files), hence `#[serde(skip)]`; defaults to `None` (quiche's built-in + /// BBR defaults). See [`BbrParamsField`]. + #[serde(skip)] + pub custom_bbr_params: BbrParamsField, + /// Optionally enables pacing for outgoing packets. /// /// Note: this also requires pacing-compatible @@ -423,6 +468,11 @@ impl QuicSettings { true } + #[inline] + fn default_enable_cubic_idle_restart_fix() -> bool { + true + } + #[inline] fn default_listen_backlog() -> usize { // Given a worst-case 1 minute handshake timeout and up to 4096 concurrent