-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Description
Hello,
Description
Currently, the rustls backend in pingora-core (specifically in listeners/tls/rustls/mod.rs) is architecturally limited to serving certificates via static file paths (cert_path and key_path).
While the openssl backend allows for dynamic SNI certificate selection through callbacks (e.g., set_servername_callback), the rustls implementation forces a static ServerConfig creation at build time using with_single_cert. This prevents users from implementing dynamic certificate resolution (e.g., loading from a database, memory cache, or custom resolver) for multi-tenant proxy services.
Proposed Solution
I propose exposing an API in TlsSettings that allows injecting a pre-built Arc<ServerConfig> or providing a mechanism to inject a custom ResolvesServerCert trait implementation.
By allowing the injection of a custom ServerConfig (which already supports the with_cert_resolver method in rustls), Pingora users could easily implement dynamic SNI, bringing rustls support to parity with the openssl backend capabilities.
Example of the desired API:
let cert_resolver = Arc::new(MyCustomSniResolver::new());
let server_config = ServerConfig::builder()
.with_no_client_auth()
.with_cert_resolver(cert_resolver);
let mut tls_settings = TlsSettings::with_server_config(Arc::new(server_config));
tls_settings.enable_h2();
proxy_service.add_tls_with_settings(&address, Some(tcp_options), tls_settings);Impact
This change would:
- Unblock multi-tenant architectures using rustls that require SNI.
- Improve flexibility for users who manage certificates in memory (e.g., via HashMaps or databases) rather than on the filesystem.
- Parity with openssl backend features: Aligning the capabilities of both TLS backends, allowing users to choose rustls for its memory safety without sacrificing core proxy functionality.
Context
I am currently working on a proxy solution that requires serving hundreds of certificates dynamically. The current limitation forces a choice between using the openssl backend (which is less ideal for our pure-Rust stack) or manually patching the pingora-core source code to allow custom ServerConfig injection.
Suggestion
use std::sync::Arc;
use crate::listeners::TlsAcceptCallbacks;
use crate::protocols::tls::{TlsStream, server::handshake, server::handshake_with_callback};
use log::debug;
use pingora_error::ErrorType::InternalError;
use pingora_error::{Error, OrErr, Result};
use pingora_rustls::ClientCertVerifier;
use pingora_rustls::ServerConfig;
use pingora_rustls::load_certs_and_key_files;
use pingora_rustls::{TlsAcceptor as RusTlsAcceptor, version};
use crate::protocols::{ALPN, IO};
/// The TLS settings of a listening endpoint
pub struct TlsSettings {
alpn_protocols: Option<Vec<Vec<u8>>>,
// Optional paths to allow custom_config usage
cert_path: Option<String>,
key_path: Option<String>,
client_cert_verifier: Option<Arc<dyn ClientCertVerifier>>,
// ADDITION: Allows injecting a pre-built ServerConfig (e.g., for dynamic SNI)
custom_config: Option<Arc<ServerConfig>>,
}
pub struct Acceptor {
pub acceptor: RusTlsAcceptor,
callbacks: Option<TlsAcceptCallbacks>,
}
impl TlsSettings {
/// ADDITION: Creates TlsSettings from an existing rustls ServerConfig
pub fn with_server_config(config: Arc<ServerConfig>) -> Self {
TlsSettings {
alpn_protocols: None,
cert_path: None,
key_path: None,
client_cert_verifier: None,
custom_config: Some(config),
}
}
/// Create a Rustls acceptor based on the current setting for certificates,
/// keys, and protocols.
pub fn build(self) -> Acceptor {
// ADDITION: Use custom configuration if provided
let config = if let Some(custom) = self.custom_config {
let mut config_clone = (*custom).clone();
// Apply ALPN configured via the enable_h2() method
if let Some(alpn_protocols) = self.alpn_protocols {
config_clone.alpn_protocols = alpn_protocols;
}
Arc::new(config_clone)
} else {
// ORIGINAL PINGORA 0.8 BEHAVIOR
let cert_path = self.cert_path.as_ref().expect("cert_path missing");
let key_path = self.key_path.as_ref().expect("key_path missing");
let Ok(Some((certs, key))) = load_certs_and_key_files(cert_path, key_path) else {
panic!(
"Failed to load provided certificates \"{}\" or key \"{}\".",
cert_path, key_path
)
};
let builder =
ServerConfig::builder_with_protocol_versions(&[&version::TLS12, &version::TLS13]);
let builder = if let Some(verifier) = self.client_cert_verifier {
builder.with_client_cert_verifier(verifier)
} else {
builder.with_no_client_auth()
};
let mut config = builder
.with_single_cert(certs, key)
.explain_err(InternalError, |e| {
format!("Failed to create server listener config: {e}")
})
.unwrap();
if let Some(alpn_protocols) = self.alpn_protocols {
config.alpn_protocols = alpn_protocols;
}
Arc::new(config)
};
Acceptor {
acceptor: RusTlsAcceptor::from(config),
callbacks: None,
}
}
/// Enable HTTP/2 support for this endpoint, which is default off.
/// This effectively sets the ALPN to prefer HTTP/2 with HTTP/1.1 allowed
pub fn enable_h2(&mut self) {
self.set_alpn(ALPN::H2H1);
}
pub fn set_alpn(&mut self, alpn: ALPN) {
self.alpn_protocols = Some(alpn.to_wire_protocols());
}
/// Configure mTLS by providing a rustls client certificate verifier.
pub fn set_client_cert_verifier(&mut self, verifier: Arc<dyn ClientCertVerifier>) {
self.client_cert_verifier = Some(verifier);
}
pub fn intermediate(cert_path: &str, key_path: &str) -> Result<Self>
where
Self: Sized,
{
Ok(TlsSettings {
alpn_protocols: None,
cert_path: Some(cert_path.to_string()),
key_path: Some(key_path.to_string()),
client_cert_verifier: None,
custom_config: None, // Initialize our new field
})
}
pub fn with_callbacks() -> Result<Self>
where
Self: Sized,
{
// TODO: verify if/how callback in handshake can be done using Rustls
Error::e_explain(
InternalError,
"Certificate callbacks are not supported with feature \"rustls\".",
)
}
}
impl Acceptor {
pub async fn tls_handshake<S: IO>(&self, stream: S) -> Result<TlsStream<S>> {
debug!("new tls session");
// TODO: be able to offload this handshake in a thread pool
if let Some(cb) = self.callbacks.as_ref() {
handshake_with_callback(self, stream, cb).await
} else {
handshake(self, stream).await
}
}
}Thanks..