use pingora_core::listeners::tls::TlsSettings;
use praxis_core::ProxyError;
use praxis_tls::ListenerTls;
use tokio::sync::watch;
#[expect(clippy::too_many_lines, reason = "hot-reload vs static TLS branching")]
pub(crate) fn build_tls_settings(
tls: &ListenerTls,
address: &str,
context_label: &str,
advertise_http_alpn: bool,
) -> Result<(TlsSettings, Option<watch::Sender<bool>>), ProxyError> {
macro_rules! tls_err {
($e:expr) => {{
let err = $e;
ProxyError::Config(format!("TLS for {address}: {err}"))
}};
}
#[cfg(feature = "config-reload")]
if tls.is_hot_reload() {
tracing::debug!(address, context_label, "building TLS ServerConfig with hot-reload");
let result = praxis_tls::setup::build_reloadable_server_config(tls, advertise_http_alpn)
.map_err(|e| ProxyError::Config(format!("TLS hot-reload for {address}: {e}")))?;
let pair =
tls.certificates.first().cloned().ok_or_else(|| {
ProxyError::Config(format!("TLS hot-reload for {address}: no certificate configured"))
})?;
let verifier_reload = result.verifier_handle.and_then(|handle| {
tls.client_ca
.as_ref()
.map(|ca_cfg| praxis_tls::watcher::ClientVerifierReload {
ca_path: ca_cfg.ca_path.clone(),
crl_paths: ca_cfg.crl_paths.clone(),
mode: tls.client_cert_mode,
trusted_spiffe_ids: tls.trusted_spiffe_ids.clone(),
swap_handle: handle,
})
});
let (shutdown_tx, shutdown_rx) = watch::channel(false);
praxis_tls::watcher::CertWatcher::spawn(result.cert_handle, pair, verifier_reload, shutdown_rx);
let settings = TlsSettings::with_server_config(result.config).map_err(|e| tls_err!(e))?;
return Ok((settings, Some(shutdown_tx)));
}
#[cfg(not(feature = "config-reload"))]
if tls.is_hot_reload() {
tracing::warn!(
address,
context_label,
"listener requests TLS hot_reload but this build lacks the `config-reload` feature; \
serving a static certificate"
);
}
tracing::debug!(address, context_label, "building TLS ServerConfig");
let server_config = praxis_tls::setup::build_server_config(tls, advertise_http_alpn).map_err(|e| tls_err!(e))?;
let settings = TlsSettings::with_server_config(server_config).map_err(|e| tls_err!(e))?;
Ok((settings, None))
}
#[cfg(test)]
#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::assertions_on_result_states,
clippy::significant_drop_tightening,
clippy::must_use_candidate,
clippy::semicolon_if_nothing_returned,
clippy::must_use_unit,
clippy::let_underscore_must_use,
clippy::too_many_lines,
reason = "tests"
)]
mod tests {
use praxis_tls::{CaConfig, CertKeyPair, ClientCertMode};
use super::*;
fn gen_test_certs() -> (tempfile::TempDir, String, String, String) {
use rcgen::{CertificateParams, DnType, IsCa, Issuer, KeyPair};
let temp_dir = tempfile::TempDir::new().expect("create temp dir");
let ca_key = KeyPair::generate().expect("generate CA key");
let mut ca_params = CertificateParams::new(Vec::<String>::new()).expect("CA params");
ca_params.is_ca = IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
ca_params.distinguished_name.push(DnType::CommonName, "Test CA");
let ca_cert = ca_params.self_signed(&ca_key).expect("self-sign CA");
let issuer = Issuer::from_params(&ca_params, &ca_key);
let server_key = KeyPair::generate().expect("generate server key");
let mut server_params = CertificateParams::new(vec!["localhost".to_owned()]).expect("server params");
server_params.distinguished_name.push(DnType::CommonName, "localhost");
let server_cert = server_params.signed_by(&server_key, &issuer).expect("sign server cert");
let ca_path = temp_dir.path().join("ca.pem");
let cert_path = temp_dir.path().join("server.pem");
let key_path = temp_dir.path().join("server-key.pem");
std::fs::write(&ca_path, ca_cert.pem()).expect("write CA");
std::fs::write(&cert_path, server_cert.pem()).expect("write cert");
std::fs::write(&key_path, server_key.serialize_pem()).expect("write key");
(
temp_dir,
ca_path.to_str().unwrap().to_owned(),
cert_path.to_str().unwrap().to_owned(),
key_path.to_str().unwrap().to_owned(),
)
}
fn ensure_crypto_provider() {
use rustls::crypto::aws_lc_rs;
drop(aws_lc_rs::default_provider().install_default());
}
#[test]
fn static_config_single_cert() {
ensure_crypto_provider();
let (_temp, _ca, cert, key) = gen_test_certs();
let tls = ListenerTls {
certificates: vec![CertKeyPair {
cert_path: cert.clone(),
key_path: key.clone(),
default: false,
server_names: vec![],
}],
cipher_suites: None,
client_ca: None,
client_cert_mode: ClientCertMode::None,
trusted_spiffe_ids: vec![],
hot_reload: Some(false), min_version: None,
};
let result = build_tls_settings(&tls, "127.0.0.1:8443", "TEST", false);
assert!(result.is_ok(), "should build static config");
let (settings, shutdown_tx) = result.unwrap();
assert!(shutdown_tx.is_none(), "static config should not return shutdown sender");
drop(settings);
}
#[test]
fn static_config_with_alpn() {
ensure_crypto_provider();
let (_temp, _ca, cert, key) = gen_test_certs();
let tls = ListenerTls {
certificates: vec![CertKeyPair {
cert_path: cert,
key_path: key,
default: false,
server_names: vec![],
}],
cipher_suites: None,
client_ca: None,
client_cert_mode: ClientCertMode::None,
trusted_spiffe_ids: vec![],
hot_reload: Some(false),
min_version: None,
};
let result = build_tls_settings(&tls, "127.0.0.1:8443", "HTTP", true);
assert!(result.is_ok(), "should build with HTTP ALPN: ");
let (_, shutdown_tx) = result.unwrap();
assert!(shutdown_tx.is_none());
}
#[test]
fn static_config_multi_cert_disables_hot_reload() {
ensure_crypto_provider();
let (_temp, _ca, cert, key) = gen_test_certs();
let tls = ListenerTls {
certificates: vec![
CertKeyPair {
cert_path: cert.clone(),
key_path: key.clone(),
default: false,
server_names: vec!["example.com".to_owned()],
},
CertKeyPair {
cert_path: cert.clone(),
key_path: key.clone(),
default: true,
server_names: vec![],
},
],
cipher_suites: None,
client_ca: None,
client_cert_mode: ClientCertMode::None,
trusted_spiffe_ids: vec![],
hot_reload: None, min_version: None,
};
assert!(!tls.is_hot_reload(), "multi-cert should disable hot-reload");
let result = build_tls_settings(&tls, "127.0.0.1:8443", "HTTP", true);
assert!(result.is_ok(), "should build multi-cert config: ");
let (_, shutdown_tx) = result.unwrap();
assert!(shutdown_tx.is_none(), "multi-cert should use static path");
}
#[test]
fn static_config_with_client_ca() {
ensure_crypto_provider();
let (_temp, ca, cert, key) = gen_test_certs();
let tls = ListenerTls {
certificates: vec![CertKeyPair {
cert_path: cert,
key_path: key,
default: false,
server_names: vec![],
}],
cipher_suites: None,
client_ca: Some(CaConfig {
ca_path: ca,
crl_paths: vec![],
}),
client_cert_mode: ClientCertMode::Request,
trusted_spiffe_ids: vec![],
hot_reload: Some(false),
min_version: None,
};
let result = build_tls_settings(&tls, "127.0.0.1:8443", "mTLS", false);
assert!(result.is_ok(), "should build with client CA: ");
let (_, shutdown_tx) = result.unwrap();
assert!(shutdown_tx.is_none());
}
#[test]
fn static_config_invalid_cert_path() {
ensure_crypto_provider();
let tls = ListenerTls {
certificates: vec![CertKeyPair {
cert_path: "/nonexistent/cert.pem".to_owned(),
key_path: "/nonexistent/key.pem".to_owned(),
default: false,
server_names: vec![],
}],
cipher_suites: None,
client_ca: None,
client_cert_mode: ClientCertMode::None,
trusted_spiffe_ids: vec![],
hot_reload: Some(false),
min_version: None,
};
let result = build_tls_settings(&tls, "127.0.0.1:8443", "TEST", false);
assert!(result.is_err(), "should fail with invalid cert path");
if let Err(err) = result {
let err_msg = err.to_string();
assert!(err_msg.contains("127.0.0.1:8443"), "error should include address");
}
}
#[test]
#[cfg(not(feature = "config-reload"))]
fn hot_reload_without_feature_warns_and_uses_static() {
ensure_crypto_provider();
let (_temp, _ca, cert, key) = gen_test_certs();
let tls = ListenerTls {
certificates: vec![CertKeyPair {
cert_path: cert,
key_path: key,
default: false,
server_names: vec![],
}],
cipher_suites: None,
client_ca: None,
client_cert_mode: ClientCertMode::None,
trusted_spiffe_ids: vec![],
hot_reload: None, min_version: None,
};
assert!(
tls.is_hot_reload(),
"single cert with hot_reload=None should be hot-reload"
);
let result = build_tls_settings(&tls, "127.0.0.1:8443", "HTTP", true);
assert!(result.is_ok(), "should succeed with static fallback: ");
let (_, shutdown_tx) = result.unwrap();
assert!(
shutdown_tx.is_none(),
"without config-reload feature, should not return watcher"
);
}
#[test]
#[cfg(feature = "config-reload")]
fn hot_reload_with_feature_spawns_watcher() {
ensure_crypto_provider();
let (_temp, _ca, cert, key) = gen_test_certs();
let tls = ListenerTls {
certificates: vec![CertKeyPair {
cert_path: cert,
key_path: key,
default: false,
server_names: vec![],
}],
cipher_suites: None,
client_ca: None,
client_cert_mode: ClientCertMode::None,
trusted_spiffe_ids: vec![],
hot_reload: None, min_version: None,
};
assert!(tls.is_hot_reload(), "single cert should enable hot-reload by default");
let result = build_tls_settings(&tls, "127.0.0.1:8443", "HTTP", true);
assert!(result.is_ok(), "should build hot-reload config: ");
let (_, shutdown_tx) = result.unwrap();
assert!(
shutdown_tx.is_some(),
"with config-reload feature, should return shutdown sender"
);
if let Some(tx) = shutdown_tx {
let _ = tx.send(true);
}
}
#[test]
#[cfg(feature = "config-reload")]
fn hot_reload_with_client_ca() {
ensure_crypto_provider();
let (_temp, ca, cert, key) = gen_test_certs();
let tls = ListenerTls {
certificates: vec![CertKeyPair {
cert_path: cert,
key_path: key,
default: false,
server_names: vec![],
}],
cipher_suites: None,
client_ca: Some(CaConfig {
ca_path: ca,
crl_paths: vec![],
}),
client_cert_mode: ClientCertMode::Require,
trusted_spiffe_ids: vec![],
hot_reload: None,
min_version: None,
};
let result = build_tls_settings(&tls, "127.0.0.1:8443", "mTLS", false);
assert!(result.is_ok(), "should build hot-reload with client CA: ");
let (_, shutdown_tx) = result.unwrap();
assert!(shutdown_tx.is_some(), "should spawn watcher for client CA");
if let Some(tx) = shutdown_tx {
let _ = tx.send(true);
}
}
#[test]
#[cfg(feature = "config-reload")]
fn hot_reload_without_client_ca() {
ensure_crypto_provider();
let (_temp, _ca, cert, key) = gen_test_certs();
let tls = ListenerTls {
certificates: vec![CertKeyPair {
cert_path: cert,
key_path: key,
default: false,
server_names: vec![],
}],
cipher_suites: None,
client_ca: None, client_cert_mode: ClientCertMode::None,
trusted_spiffe_ids: vec![],
hot_reload: None,
min_version: None,
};
let result = build_tls_settings(&tls, "127.0.0.1:8443", "HTTP", true);
assert!(result.is_ok(), "should build hot-reload without client CA: ");
let (_, shutdown_tx) = result.unwrap();
assert!(shutdown_tx.is_some());
if let Some(tx) = shutdown_tx {
let _ = tx.send(true);
}
}
#[test]
fn context_label_used_in_logging() {
ensure_crypto_provider();
let (_temp, _ca, cert, key) = gen_test_certs();
let tls = ListenerTls {
certificates: vec![CertKeyPair {
cert_path: cert.clone(),
key_path: key.clone(),
default: false,
server_names: vec![],
}],
cipher_suites: None,
client_ca: None,
client_cert_mode: ClientCertMode::None,
trusted_spiffe_ids: vec![],
hot_reload: Some(false),
min_version: None,
};
let result = build_tls_settings(&tls, "127.0.0.1:8443", "HTTP", true);
assert!(result.is_ok());
let result = build_tls_settings(&tls, "127.0.0.1:8443", "TCP", false);
assert!(result.is_ok());
let result = build_tls_settings(&tls, "127.0.0.1:8443", "CUSTOM", false);
assert!(result.is_ok());
}
#[test]
fn address_included_in_error_message() {
ensure_crypto_provider();
let tls = ListenerTls {
certificates: vec![CertKeyPair {
cert_path: "/nonexistent/cert.pem".to_owned(),
key_path: "/nonexistent/key.pem".to_owned(),
default: false,
server_names: vec![],
}],
cipher_suites: None,
client_ca: None,
client_cert_mode: ClientCertMode::None,
trusted_spiffe_ids: vec![],
hot_reload: Some(false),
min_version: None,
};
let result = build_tls_settings(&tls, "192.168.1.1:443", "HTTP", false);
assert!(result.is_err());
if let Err(err) = result {
assert!(err.to_string().contains("192.168.1.1:443"));
}
let result = build_tls_settings(&tls, "[::1]:8443", "TCP", false);
assert!(result.is_err());
if let Err(err) = result {
assert!(err.to_string().contains("[::1]:8443"));
}
}
#[test]
fn alpn_variation_coverage() {
ensure_crypto_provider();
let (_temp, _ca, cert, key) = gen_test_certs();
let tls = ListenerTls {
certificates: vec![CertKeyPair {
cert_path: cert,
key_path: key,
default: false,
server_names: vec![],
}],
cipher_suites: None,
client_ca: None,
client_cert_mode: ClientCertMode::None,
trusted_spiffe_ids: vec![],
hot_reload: Some(false),
min_version: None,
};
let result = build_tls_settings(&tls, "127.0.0.1:8443", "HTTP", true);
assert!(result.is_ok(), "should succeed with ALPN enabled");
let result = build_tls_settings(&tls, "127.0.0.1:8443", "TCP", false);
assert!(result.is_ok(), "should succeed with ALPN disabled");
}
}