synvael/crates/net/src/endpoint.rs

152 lines
6.5 KiB
Rust

// 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<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.
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<Endpoint, NetError> {
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<Endpoint, NetError> {
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<ServerCertVerified, rustls::Error> {
Ok(ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
// 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;