diff --git a/crates/net/src/endpoint.rs b/crates/net/src/endpoint.rs index 2ef792b..479156d 100644 --- a/crates/net/src/endpoint.rs +++ b/crates/net/src/endpoint.rs @@ -6,9 +6,10 @@ use std::net::SocketAddr; use std::sync::Arc; +use std::time::Duration; use quinn::crypto::rustls::{QuicClientConfig, QuicServerConfig}; -use quinn::{ClientConfig, Endpoint, ServerConfig}; +use quinn::{ClientConfig, Endpoint, IdleTimeout, ServerConfig, TransportConfig, VarInt}; use rustls::DigitallySignedStruct; use rustls::SignatureScheme; 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. 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 { + 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. fn ensure_crypto_provider() { if rustls::crypto::ring::default_provider() @@ -49,7 +73,8 @@ pub fn server_endpoint(bind: SocketAddr) -> Result { tls_config.alpn_protocols = vec![ALPN.to_vec()]; 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)?) } @@ -69,7 +94,8 @@ pub fn client_endpoint() -> Result { tls_config.alpn_protocols = vec![ALPN.to_vec()]; 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)?)?; endpoint.set_default_client_config(client_config);