fix(net): enable QUIC keep-alive to survive backpressure stalls

This commit is contained in:
Serkyo 2026-07-22 05:27:01 +02:00
parent f465cfeeaf
commit 20d36624c7

View file

@ -6,9 +6,10 @@
use std::net::SocketAddr; use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use quinn::crypto::rustls::{QuicClientConfig, QuicServerConfig}; use quinn::crypto::rustls::{QuicClientConfig, QuicServerConfig};
use quinn::{ClientConfig, Endpoint, ServerConfig}; use quinn::{ClientConfig, Endpoint, IdleTimeout, ServerConfig, TransportConfig, VarInt};
use rustls::DigitallySignedStruct; use rustls::DigitallySignedStruct;
use rustls::SignatureScheme; use rustls::SignatureScheme;
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
@ -20,6 +21,29 @@ use crate::error::NetError;
/// The Application-Layer Protocol Negotiation identifier for the Synvael protocol. /// The Application-Layer Protocol Negotiation identifier for the Synvael protocol.
pub const ALPN: &[u8] = b"synvael"; pub const ALPN: &[u8] = b"synvael";
/// Interval between QUIC keep-alive probes, in milliseconds.
///
/// Kept well below [`MAX_IDLE_TIMEOUT_MS`] so several probes elapse before the idle timeout could fire. Keep-alives are required because chunk delivery deliberately stalls the stream when the client cannot mesh fast enough: during such a flow-control stall no application data flows in either direction, and without a probe the connection would be indistinguishable from a dead peer and closed on the idle timeout.
const KEEP_ALIVE_INTERVAL_MS: u32 = 5_000;
/// Maximum time with no received packets before a connection is considered lost, in milliseconds.
const MAX_IDLE_TIMEOUT_MS: u32 = 30_000;
/// Builds the QUIC transport configuration shared by both endpoints.
///
/// Enables keep-alive probes and sets an explicit idle timeout; see [`KEEP_ALIVE_INTERVAL_MS`] for why probes are mandatory given the chunk stream's backpressure behaviour. All other transport parameters retain their `quinn` defaults.
fn transport_config() -> Arc<TransportConfig> {
let mut transport = TransportConfig::default();
transport.keep_alive_interval(Some(Duration::from_millis(u64::from(
KEEP_ALIVE_INTERVAL_MS,
))));
// `VarInt::from_u32` is infallible, so no fallible `IdleTimeout::try_from(Duration)` conversion is needed.
transport.max_idle_timeout(Some(IdleTimeout::from(VarInt::from_u32(
MAX_IDLE_TIMEOUT_MS,
))));
Arc::new(transport)
}
/// Installs the process-wide default `rustls` `CryptoProvider` if one is not already installed. /// Installs the process-wide default `rustls` `CryptoProvider` if one is not already installed.
fn ensure_crypto_provider() { fn ensure_crypto_provider() {
if rustls::crypto::ring::default_provider() if rustls::crypto::ring::default_provider()
@ -49,7 +73,8 @@ pub fn server_endpoint(bind: SocketAddr) -> Result<Endpoint, NetError> {
tls_config.alpn_protocols = vec![ALPN.to_vec()]; tls_config.alpn_protocols = vec![ALPN.to_vec()];
let quic_config = QuicServerConfig::try_from(tls_config)?; let quic_config = QuicServerConfig::try_from(tls_config)?;
let server_config = ServerConfig::with_crypto(Arc::new(quic_config)); let mut server_config = ServerConfig::with_crypto(Arc::new(quic_config));
server_config.transport_config(transport_config());
Ok(Endpoint::server(server_config, bind)?) Ok(Endpoint::server(server_config, bind)?)
} }
@ -69,7 +94,8 @@ pub fn client_endpoint() -> Result<Endpoint, NetError> {
tls_config.alpn_protocols = vec![ALPN.to_vec()]; tls_config.alpn_protocols = vec![ALPN.to_vec()];
let quic_config = QuicClientConfig::try_from(tls_config)?; let quic_config = QuicClientConfig::try_from(tls_config)?;
let client_config = ClientConfig::new(Arc::new(quic_config)); let mut client_config = ClientConfig::new(Arc::new(quic_config));
client_config.transport_config(transport_config());
let mut endpoint = Endpoint::client("0.0.0.0:0".parse().map_err(std::io::Error::other)?)?; let mut endpoint = Endpoint::client("0.0.0.0:0".parse().map_err(std::io::Error::other)?)?;
endpoint.set_default_client_config(client_config); endpoint.set_default_client_config(client_config);