// SPDX-License-Identifier: AGPL-3.0-only //! QUIC endpoint construction for client and server. //! //! Builds the `quinn` endpoints and configures ALPN and TLS 1.3. Both endpoints negotiate the `synvael` application protocol; a peer advertising any other ALPN identifier is rejected during the TLS handshake. use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; use quinn::crypto::rustls::{QuicClientConfig, QuicServerConfig}; use quinn::{ClientConfig, Endpoint, IdleTimeout, ServerConfig, TransportConfig, VarInt}; use rustls::DigitallySignedStruct; use rustls::SignatureScheme; use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer, ServerName, UnixTime}; use tracing::debug; 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() .install_default() .is_err() { debug!("rustls crypto provider already installed; reusing existing default"); } } /// Builds a QUIC server endpoint bound to `bind`, using a freshly generated self-signed certificate and the `synvael` ALPN. /// /// # Errors /// /// Returns [`NetError::Rcgen`] if certificate generation fails, [`NetError::Rustls`] if the TLS configuration cannot be built, [`NetError::NoInitialCipherSuite`] if the configuration lacks a TLS 1.3 cipher suite, or [`NetError::Io`] if the UDP socket cannot be bound. pub fn server_endpoint(bind: SocketAddr) -> Result { ensure_crypto_provider(); // Generate a self-signed certificate for the "localhost" subject. The subject is not validated by the current client verifier and exists only to satisfy certificate structure. let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()])?; let cert_der = cert.cert.der().clone(); let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(cert.signing_key.serialize_der())); let mut tls_config = rustls::ServerConfig::builder() .with_no_client_auth() .with_single_cert(vec![cert_der], key_der)?; tls_config.alpn_protocols = vec![ALPN.to_vec()]; let quic_config = QuicServerConfig::try_from(tls_config)?; let mut server_config = ServerConfig::with_crypto(Arc::new(quic_config)); server_config.transport_config(transport_config()); Ok(Endpoint::server(server_config, bind)?) } /// Builds a QUIC client endpoint bound to an ephemeral local address, configured with the `synvael` ALPN and a permissive certificate verifier. /// /// # Errors /// /// Returns [`NetError::NoInitialCipherSuite`] if the TLS configuration lacks a TLS 1.3 cipher suite, or [`NetError::Io`] if the local UDP socket cannot be bound. pub fn client_endpoint() -> Result { ensure_crypto_provider(); let mut tls_config = rustls::ClientConfig::builder() .dangerous() .with_custom_certificate_verifier(Arc::new(AcceptAnyServerCert)) .with_no_client_auth(); tls_config.alpn_protocols = vec![ALPN.to_vec()]; let quic_config = QuicClientConfig::try_from(tls_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); Ok(endpoint) } /// A certificate verifier that unconditionally accepts any server certificate, disabling server authentication. // TODO: replace with trust-on-first-use + certificate pinning. #[derive(Debug)] struct AcceptAnyServerCert; impl ServerCertVerifier for AcceptAnyServerCert { fn verify_server_cert( &self, _end_entity: &CertificateDer<'_>, _intermediates: &[CertificateDer<'_>], _server_name: &ServerName<'_>, _ocsp_response: &[u8], _now: UnixTime, ) -> Result { Ok(ServerCertVerified::assertion()) } fn verify_tls12_signature( &self, _message: &[u8], _cert: &CertificateDer<'_>, _dss: &DigitallySignedStruct, ) -> Result { Ok(HandshakeSignatureValid::assertion()) } fn verify_tls13_signature( &self, _message: &[u8], _cert: &CertificateDer<'_>, _dss: &DigitallySignedStruct, ) -> Result { Ok(HandshakeSignatureValid::assertion()) } fn supported_verify_schemes(&self) -> Vec { // Advertise the schemes the installed provider can verify, so the shim does not artificially restrict handshake negotiation. rustls::crypto::ring::default_provider() .signature_verification_algorithms .supported_schemes() } } #[cfg(test)] #[path = "tests/endpoint.rs"] mod tests;