#[cfg(feature = "filter-control")]
pub mod control;
pub mod acme;
mod material;
mod ocsp;
pub mod peer_cert;
mod sni;
use std::sync::{Arc, OnceLock};
use rustls::RootCertStore;
use rustls::ServerConfig;
use rustls::server::{ResolvesServerCert, WebPkiClientVerifier};
use rustls::version::TLS13;
use thiserror::Error;
use tokio::sync::Notify;
use tokio_rustls::TlsAcceptor;
use tracing::info;
use sni::{CompositeResolver, SniResolver};
pub use acme::AcmeHandle;
pub use peer_cert::PeerClientCert;
#[derive(Debug, Error)]
pub enum TlsConfigError {
#[error("inbound TLS is required (set NAUTALID_TLS_* or NAUTALID_TLS_ACME=1)")]
Required,
#[error("missing TLS certificate: set NAUTALID_TLS_CERT_PEM or NAUTALID_TLS_CERT_PATH")]
MissingCert,
#[error("missing TLS private key: set NAUTALID_TLS_KEY_PEM or NAUTALID_TLS_KEY_PATH")]
MissingKey,
#[error("missing client CA: set NAUTALID_TLS_CLIENT_CA_PEM or NAUTALID_TLS_CLIENT_CA_PATH")]
MissingClientCa,
#[error("read {path}: {source}")]
Io {
path: String,
source: std::io::Error,
},
#[error("parse TLS certificate PEM: {0}")]
BadCert(String),
#[error("parse TLS private key PEM: {0}")]
BadKey(String),
#[error("parse client CA PEM: {0}")]
BadClientCa(String),
#[error("rustls server config: {0}")]
Rustls(String),
#[error("rustls crypto provider not installed (call platform::install_defaults first)")]
NoCryptoProvider,
#[error("QUIC server config: {0}")]
Quic(String),
}
#[must_use]
pub fn configured_from_env() -> bool {
acme::acme_enabled_from_env()
|| (tls_env_touched()
&& material::pem_from_env_pair("NAUTALID_TLS").is_ok())
|| sni::sni_configured_from_env()
}
#[must_use]
pub fn mtls_configured_from_env() -> bool {
client_ca_env_touched()
&& material::pem_from_env("NAUTALID_TLS_CLIENT_CA_PEM", "NAUTALID_TLS_CLIENT_CA_PATH")
.is_ok()
}
#[must_use]
pub fn client_auth_required_from_env() -> bool {
if !mtls_configured_from_env() {
return false;
}
match std::env::var("NAUTALID_TLS_CLIENT_AUTH")
.ok()
.map(|raw| raw.trim().to_ascii_lowercase())
.as_deref()
{
Some("optional") => false,
Some("required") | Some("1") | Some("true") | Some("yes") => true,
None => true,
_ => true,
}
}
fn tls_env_touched() -> bool {
[
"NAUTALID_TLS_CERT_PEM",
"NAUTALID_TLS_CERT_PATH",
"NAUTALID_TLS_KEY_PEM",
"NAUTALID_TLS_KEY_PATH",
"NAUTALID_TLS_H2_CERT_PEM",
"NAUTALID_TLS_H2_CERT_PATH",
"NAUTALID_TLS_H3_CERT_PEM",
"NAUTALID_TLS_H3_CERT_PATH",
]
.into_iter()
.any(|name| {
std::env::var(name)
.ok()
.is_some_and(|s| !s.trim().is_empty())
})
}
fn client_ca_env_touched() -> bool {
["NAUTALID_TLS_CLIENT_CA_PEM", "NAUTALID_TLS_CLIENT_CA_PATH"]
.into_iter()
.any(|name| {
std::env::var(name)
.ok()
.is_some_and(|s| !s.trim().is_empty())
})
}
pub fn required_acceptor_from_env() -> Result<TlsAcceptor, TlsConfigError> {
let config = Arc::new(build_h2_server_config_from_env()?);
Ok(TlsAcceptor::from(config))
}
pub fn required_quinn_config_from_env() -> Result<quinn::ServerConfig, TlsConfigError> {
build_quinn_config_from_env()
}
pub fn h2_acceptor_from_pem(
cert_pem: &str,
key_pem: &str,
client_ca_pem: Option<(&str, bool)>,
) -> Result<TlsAcceptor, TlsConfigError> {
let resolver = sni::StaticResolver::from_pem(cert_pem, key_pem)?;
let config = Arc::new(build_server_config(
resolver,
client_ca_pem,
&[b"h2".as_slice()],
)?);
Ok(TlsAcceptor::from(config))
}
pub fn h2_server_config_from_pem(
cert_pem: &str,
key_pem: &str,
client_ca_pem: Option<(&str, bool)>,
) -> Result<Arc<ServerConfig>, TlsConfigError> {
let resolver = sni::StaticResolver::from_pem(cert_pem, key_pem)?;
Ok(Arc::new(build_server_config(
resolver,
client_ca_pem,
&[b"h2".as_slice()],
)?))
}
pub fn quinn_server_from_pem(
cert_pem: &str,
key_pem: &str,
client_ca_pem: Option<(&str, bool)>,
) -> Result<quinn::ServerConfig, TlsConfigError> {
let resolver = sni::StaticResolver::from_pem(cert_pem, key_pem)?;
let rustls = build_server_config(resolver, client_ca_pem, &[b"h3".as_slice()])?;
let quic = quinn::crypto::rustls::QuicServerConfig::try_from(rustls)
.map_err(|e| TlsConfigError::Quic(e.to_string()))?;
Ok(quinn::ServerConfig::with_crypto(Arc::new(quic)))
}
fn client_ca_pem_from_env() -> Result<Option<String>, TlsConfigError> {
if !client_ca_env_touched() {
return Ok(None);
}
material::pem_from_env("NAUTALID_TLS_CLIENT_CA_PEM", "NAUTALID_TLS_CLIENT_CA_PATH")
.map(Some)
.map_err(|err| match err {
TlsConfigError::MissingCert | TlsConfigError::MissingKey => {
TlsConfigError::MissingClientCa
}
other => other,
})
}
fn resolver_for_transport(prefix: &str) -> Result<Arc<dyn ResolvesServerCert>, TlsConfigError> {
if acme::acme_enabled_from_env() {
return Err(TlsConfigError::Rustls(
"use InboundTls::from_env for ACME (resolver owned by AcmeHandle)".into(),
));
}
if sni::sni_configured_from_env() {
let path = std::env::var("NAUTALID_TLS_SNI_PATH")
.expect("sni configured");
let sni = SniResolver::from_kdl_path(path.trim())?;
if let Ok(transport) = sni::transport_cert_from_env(prefix) {
return Ok(CompositeResolver::into_resolver(sni, transport));
}
let resolver: Arc<dyn ResolvesServerCert> = sni;
return Ok(resolver);
}
sni::StaticResolver::from_env(prefix)
}
fn build_h2_server_config_from_env() -> Result<ServerConfig, TlsConfigError> {
let prefix = material::h2_cert_env_prefix();
let resolver = resolver_for_transport(prefix)?;
let client_ca = client_ca_pem_from_env()?;
build_server_config(
resolver,
client_ca
.as_deref()
.map(|ca| (ca, client_auth_required_from_env())),
&[b"h2".as_slice()],
)
}
fn quinn_alpn_protocols() -> Vec<&'static [u8]> {
#[cfg(feature = "daht-vpn")]
{
vec![b"h3".as_slice(), b"marmot/1".as_slice()]
}
#[cfg(not(feature = "daht-vpn"))]
{
vec![b"h3".as_slice()]
}
}
fn build_quinn_config_from_env() -> Result<quinn::ServerConfig, TlsConfigError> {
let prefix = material::h3_cert_env_prefix();
let resolver = resolver_for_transport(prefix)?;
let client_ca = client_ca_pem_from_env()?;
let alpn = quinn_alpn_protocols();
let rustls = build_server_config(
resolver,
client_ca
.as_deref()
.map(|ca| (ca, client_auth_required_from_env())),
&alpn,
)?;
let quic = quinn::crypto::rustls::QuicServerConfig::try_from(rustls)
.map_err(|e| TlsConfigError::Quic(e.to_string()))?;
Ok(quinn::ServerConfig::with_crypto(Arc::new(quic)))
}
pub struct InboundTls {
h2: Arc<tokio::sync::RwLock<Arc<ServerConfig>>>,
quinn: Arc<tokio::sync::RwLock<quinn::ServerConfig>>,
acme: Option<AcmeHandle>,
}
impl InboundTls {
pub fn from_env() -> Result<Self, TlsConfigError> {
let acme = if acme::acme_enabled_from_env() {
Some(acme::start_from_env()?)
} else {
None
};
let h2_config = if let Some(acme) = &acme {
build_server_config_with_acme(acme, client_ca_pem_from_env()?, &[b"h2".as_slice()])?
} else {
build_h2_server_config_from_env()?
};
let quinn = if let Some(acme) = &acme {
let rustls = build_server_config_with_acme(
acme,
client_ca_pem_from_env()?,
&quinn_alpn_protocols(),
)?;
let quic = quinn::crypto::rustls::QuicServerConfig::try_from(rustls)
.map_err(|e| TlsConfigError::Quic(e.to_string()))?;
quinn::ServerConfig::with_crypto(Arc::new(quic))
} else {
build_quinn_config_from_env()?
};
if acme.is_some() {
info!("inbound TLS ACME enabled");
}
Ok(Self {
h2: Arc::new(tokio::sync::RwLock::new(Arc::new(h2_config))),
quinn: Arc::new(tokio::sync::RwLock::new(quinn)),
acme,
})
}
#[must_use]
pub fn acme(&self) -> Option<&AcmeHandle> {
self.acme.as_ref()
}
#[must_use]
pub fn h2_config_store(&self) -> Arc<tokio::sync::RwLock<Arc<ServerConfig>>> {
Arc::clone(&self.h2)
}
pub fn install_global(self: Arc<Self>) {
static INBOUND: OnceLock<Arc<InboundTls>> = OnceLock::new();
let _ = INBOUND.set(self);
}
#[must_use]
pub fn global() -> Option<Arc<Self>> {
static INBOUND: OnceLock<Arc<InboundTls>> = OnceLock::new();
INBOUND.get().cloned()
}
#[must_use]
pub async fn h2_acceptor(&self) -> TlsAcceptor {
let config = Arc::clone(&*self.h2.read().await);
TlsAcceptor::from(config)
}
#[must_use]
pub fn quinn_config(&self) -> Arc<tokio::sync::RwLock<quinn::ServerConfig>> {
Arc::clone(&self.quinn)
}
pub async fn reload_from_env(&self) -> Result<(), TlsConfigError> {
let h2 = if let Some(acme) = &self.acme {
Arc::new(build_server_config_with_acme(
acme,
client_ca_pem_from_env()?,
&[b"h2".as_slice()],
)?)
} else {
Arc::new(build_h2_server_config_from_env()?)
};
let quinn = if let Some(acme) = &self.acme {
let rustls = build_server_config_with_acme(
acme,
client_ca_pem_from_env()?,
&[b"h3".as_slice()],
)?;
let quic = quinn::crypto::rustls::QuicServerConfig::try_from(rustls)
.map_err(|e| TlsConfigError::Quic(e.to_string()))?;
quinn::ServerConfig::with_crypto(Arc::new(quic))
} else {
build_quinn_config_from_env()?
};
*self.h2.write().await = h2;
*self.quinn.write().await = quinn;
tls_reload_notify().notify_waiters();
tracing::info!("inbound TLS material reloaded from environment");
Ok(())
}
}
pub fn tls_reload_notify() -> Arc<Notify> {
static NOTIFY: OnceLock<Arc<Notify>> = OnceLock::new();
Arc::clone(NOTIFY.get_or_init(|| Arc::new(Notify::new())))
}
fn build_server_config_with_acme(
acme: &AcmeHandle,
client_ca_pem: Option<String>,
alpn: &[&[u8]],
) -> Result<ServerConfig, TlsConfigError> {
let resolver: Arc<dyn ResolvesServerCert> = acme.resolver();
build_server_config(
resolver,
client_ca_pem
.as_deref()
.map(|ca| (ca, client_auth_required_from_env())),
alpn,
)
}
fn build_server_config(
resolver: Arc<dyn ResolvesServerCert>,
client_ca_pem: Option<(&str, bool)>,
alpn: &[&[u8]],
) -> Result<ServerConfig, TlsConfigError> {
let provider = material::crypto_provider()?;
let builder = ServerConfig::builder_with_provider(provider)
.with_protocol_versions(&[&TLS13])
.map_err(|e| TlsConfigError::Rustls(e.to_string()))?;
let mut config = if let Some((ca_pem, require)) = client_ca_pem {
let verifier = build_client_verifier(ca_pem, require)?;
builder
.with_client_cert_verifier(verifier)
.with_cert_resolver(resolver)
} else {
builder
.with_no_client_auth()
.with_cert_resolver(resolver)
};
config.alpn_protocols = alpn.iter().map(|p| p.to_vec()).collect();
Ok(config)
}
fn build_client_verifier(
ca_pem: &str,
require_client_cert: bool,
) -> Result<Arc<dyn rustls::server::danger::ClientCertVerifier>, TlsConfigError> {
let mut roots = RootCertStore::empty();
for cert in material::load_ca_certs(ca_pem)? {
roots
.add(cert)
.map_err(|e| TlsConfigError::BadClientCa(e.to_string()))?;
}
let provider = material::crypto_provider()?;
let mut builder = WebPkiClientVerifier::builder_with_provider(Arc::new(roots), provider);
if !require_client_cert {
builder = builder.allow_unauthenticated();
}
builder
.build()
.map_err(|e| TlsConfigError::Rustls(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
fn generated_pem_pair() -> (String, String) {
let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap();
(cert.cert.pem(), cert.signing_key.serialize_pem())
}
fn install_crypto() {
let _ = crate::platform::install_defaults();
}
#[test]
fn builds_h2_acceptor_tls13_only() {
install_crypto();
let (cert, key) = generated_pem_pair();
h2_acceptor_from_pem(&cert, &key, None).expect("acceptor");
}
#[test]
fn builds_quinn_config_tls13_only() {
install_crypto();
let (cert, key) = generated_pem_pair();
quinn_server_from_pem(&cert, &key, None).expect("quinn");
}
#[test]
fn builds_mtls_server_config() {
install_crypto();
let (cert, key) = generated_pem_pair();
h2_acceptor_from_pem(&cert, &key, Some((&cert, true))).expect("mtls acceptor");
}
#[test]
fn quinn_mtls_config_from_pem() {
install_crypto();
let (cert, key) = generated_pem_pair();
quinn_server_from_pem(&cert, &key, Some((&cert, true))).expect("quinn mtls");
}
#[test]
fn configured_from_env_false_when_unset() {
unsafe {
std::env::remove_var("NAUTALID_TLS_CERT_PEM");
std::env::remove_var("NAUTALID_TLS_CERT_PATH");
std::env::remove_var("NAUTALID_TLS_KEY_PEM");
std::env::remove_var("NAUTALID_TLS_KEY_PATH");
std::env::remove_var("NAUTALID_TLS_CLIENT_CA_PEM");
std::env::remove_var("NAUTALID_TLS_CLIENT_CA_PATH");
std::env::remove_var("NAUTALID_TLS_ACME");
}
assert!(!configured_from_env());
assert!(!mtls_configured_from_env());
}
#[test]
fn client_auth_required_defaults_when_ca_set() {
install_crypto();
let (cert, key) = generated_pem_pair();
let _ = key;
unsafe {
std::env::set_var("NAUTALID_TLS_CLIENT_CA_PEM", &cert);
std::env::remove_var("NAUTALID_TLS_CLIENT_AUTH");
}
assert!(mtls_configured_from_env());
assert!(client_auth_required_from_env());
unsafe {
std::env::set_var("NAUTALID_TLS_CLIENT_AUTH", "optional");
}
assert!(!client_auth_required_from_env());
unsafe {
std::env::remove_var("NAUTALID_TLS_CLIENT_CA_PEM");
std::env::remove_var("NAUTALID_TLS_CLIENT_AUTH");
}
}
}