feat(net): add QUIC endpoints with synvael ALPN and self-signed TLS

This commit is contained in:
Serkyo 2026-07-13 14:23:30 +02:00
parent f83a0f1232
commit aa9dc18efe
2 changed files with 144 additions and 1 deletions

View file

@ -33,6 +33,15 @@ pub enum NetError {
/// Reading an exact number of bytes from a quinn recv stream failed. /// Reading an exact number of bytes from a quinn recv stream failed.
#[error("quinn read error: {0}")] #[error("quinn read error: {0}")]
Read(#[from] quinn::ReadExactError), Read(#[from] quinn::ReadExactError),
/// Generation of the self-signed server certificate failed.
#[error("certificate generation error: {0}")]
Rcgen(#[from] rcgen::Error),
/// Construction of the `rustls` TLS configuration failed.
#[error("rustls configuration error: {0}")]
Rustls(#[from] rustls::Error),
/// The `rustls` configuration lacked a TLS 1.3 cipher suite, which QUIC requires.
#[error("no initial cipher suite for quic: {0}")]
NoInitialCipherSuite(#[from] quinn::crypto::rustls::NoInitialCipherSuite),
} }
/// The maximum payload length, in bytes, accepted on the control stream (64 KiB), matching the mod-payload cap. Higher-bandwidth tiers such as chunk streaming define their own caps. /// The maximum payload length, in bytes, accepted on the control stream (64 KiB), matching the mod-payload cap. Higher-bandwidth tiers such as chunk streaming define their own caps.

View file

@ -2,4 +2,138 @@
//! QUIC endpoint construction for client and server. //! QUIC endpoint construction for client and server.
//! //!
//! Builds the `quinn` endpoints and configures ALPN and TLS 1.3 (self-signed server certificate, permissive client verifier for the initial milestone). Populated in a later concept; currently a placeholder. //! 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 quinn::crypto::rustls::{QuicClientConfig, QuicServerConfig};
use quinn::{ClientConfig, Endpoint, ServerConfig};
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::codec::NetError;
/// The Application-Layer Protocol Negotiation identifier for the Synvael protocol.
pub const ALPN: &[u8] = b"synvael";
/// 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.
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 server_config = ServerConfig::with_crypto(Arc::new(quic_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.
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 client_config = ClientConfig::new(Arc::new(quic_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)]
mod tests {
use super::*;
#[test]
fn alpn_identifier_is_synvael() {
assert_eq!(ALPN, b"synvael");
}
// A tokio runtime is required because `quinn::Endpoint` spawns its driver task on construction.
#[tokio::test]
async fn server_endpoint_constructs_and_binds() -> Result<(), NetError> {
let bind = "127.0.0.1:0".parse().map_err(std::io::Error::other)?;
let endpoint = server_endpoint(bind)?;
// A concrete port is assigned once the UDP socket is bound.
assert_ne!(endpoint.local_addr()?.port(), 0, "socket must bind a port");
Ok(())
}
#[tokio::test]
async fn client_endpoint_constructs_and_binds() -> Result<(), NetError> {
client_endpoint()?;
Ok(())
}
}