feat(tls): add shared TLS acceptor with SNI resolver and session resumption; prefer shared acceptor and fall back to per-connection when routes specify custom TLS versions
This commit is contained in:
@@ -1,17 +1,99 @@
|
||||
use std::collections::HashMap;
|
||||
use std::io::BufReader;
|
||||
use std::sync::Arc;
|
||||
|
||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
|
||||
use rustls::server::ResolvesServerCert;
|
||||
use rustls::sign::CertifiedKey;
|
||||
use rustls::ServerConfig;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_rustls::{TlsAcceptor, TlsConnector, server::TlsStream as ServerTlsStream};
|
||||
use tracing::debug;
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::tcp_listener::TlsCertConfig;
|
||||
|
||||
/// Ensure the default crypto provider is installed.
|
||||
fn ensure_crypto_provider() {
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
}
|
||||
|
||||
/// SNI-based certificate resolver with pre-parsed CertifiedKeys.
|
||||
/// Enables shared ServerConfig across connections — avoids per-connection PEM parsing
|
||||
/// and enables TLS session resumption.
|
||||
#[derive(Debug)]
|
||||
pub struct CertResolver {
|
||||
certs: HashMap<String, Arc<CertifiedKey>>,
|
||||
fallback: Option<Arc<CertifiedKey>>,
|
||||
}
|
||||
|
||||
impl CertResolver {
|
||||
/// Build a resolver from PEM-encoded cert/key configs.
|
||||
/// Parses all PEM data upfront so connections only do a cheap HashMap lookup.
|
||||
pub fn new(configs: &HashMap<String, TlsCertConfig>) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
|
||||
ensure_crypto_provider();
|
||||
let provider = rustls::crypto::ring::default_provider();
|
||||
let mut certs = HashMap::new();
|
||||
let mut fallback = None;
|
||||
|
||||
for (domain, cfg) in configs {
|
||||
let cert_chain = load_certs(&cfg.cert_pem)?;
|
||||
let key = load_private_key(&cfg.key_pem)?;
|
||||
let ck = Arc::new(CertifiedKey::from_der(cert_chain, key, &provider)
|
||||
.map_err(|e| format!("CertifiedKey for {}: {}", domain, e))?);
|
||||
if domain == "*" {
|
||||
fallback = Some(Arc::clone(&ck));
|
||||
}
|
||||
certs.insert(domain.clone(), ck);
|
||||
}
|
||||
|
||||
// If no explicit "*" fallback, use the first available cert
|
||||
if fallback.is_none() {
|
||||
fallback = certs.values().next().map(Arc::clone);
|
||||
}
|
||||
|
||||
Ok(Self { certs, fallback })
|
||||
}
|
||||
}
|
||||
|
||||
impl ResolvesServerCert for CertResolver {
|
||||
fn resolve(&self, client_hello: rustls::server::ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
|
||||
let domain = match client_hello.server_name() {
|
||||
Some(name) => name,
|
||||
None => return self.fallback.clone(),
|
||||
};
|
||||
// Exact match
|
||||
if let Some(ck) = self.certs.get(domain) {
|
||||
return Some(Arc::clone(ck));
|
||||
}
|
||||
// Wildcard: sub.example.com → *.example.com
|
||||
if let Some(dot) = domain.find('.') {
|
||||
let wc = format!("*.{}", &domain[dot + 1..]);
|
||||
if let Some(ck) = self.certs.get(&wc) {
|
||||
return Some(Arc::clone(ck));
|
||||
}
|
||||
}
|
||||
self.fallback.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a shared TLS acceptor with SNI resolution, session cache, and session tickets.
|
||||
/// The returned acceptor can be reused across all connections (cheap Arc clone).
|
||||
pub fn build_shared_tls_acceptor(resolver: CertResolver) -> Result<TlsAcceptor, Box<dyn std::error::Error + Send + Sync>> {
|
||||
ensure_crypto_provider();
|
||||
let mut config = ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_cert_resolver(Arc::new(resolver));
|
||||
|
||||
// Shared session cache — enables session ID resumption across connections
|
||||
config.session_storage = rustls::server::ServerSessionMemoryCache::new(4096);
|
||||
// Session ticket resumption (12-hour lifetime, Chacha20Poly1305 encrypted)
|
||||
config.ticketer = rustls::crypto::ring::Ticketer::new()
|
||||
.map_err(|e| format!("Ticketer: {}", e))?;
|
||||
|
||||
info!("Built shared TLS config with session cache (4096) and ticket support");
|
||||
Ok(TlsAcceptor::from(Arc::new(config)))
|
||||
}
|
||||
|
||||
/// Build a TLS acceptor from PEM-encoded cert and key data.
|
||||
pub fn build_tls_acceptor(cert_pem: &str, key_pem: &str) -> Result<TlsAcceptor, Box<dyn std::error::Error + Send + Sync>> {
|
||||
build_tls_acceptor_with_config(cert_pem, key_pem, None)
|
||||
|
||||
Reference in New Issue
Block a user