use std::path::PathBuf;
use std::sync::OnceLock;
use aetherdb::ZmqTlsConfig;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WireTls {
pub cert_file: PathBuf,
pub key_file: PathBuf,
pub ca_file: PathBuf,
}
#[derive(Debug, Error, Clone)]
pub enum TunnelError {
#[error("AetherDB tunnel requires client cert (NAUTALID_AETHER_TUNNEL_CERT_PEM or _CERT_PATH)")]
MissingCert,
#[error("AetherDB tunnel requires client key (NAUTALID_AETHER_TUNNEL_KEY_PEM or _KEY_PATH)")]
MissingKey,
#[error("AetherDB tunnel requires CA (NAUTALID_AETHER_TUNNEL_CA_PEM or _CA_PATH)")]
MissingCa,
#[error("read {path}: {message}")]
Io { path: String, message: String },
#[error(
"host wire TLS is enabled (AETHERDB_HOST_TLS_*) but client tunnel material is missing (NAUTALID_AETHER_TUNNEL_*)"
)]
WireClientTlsRequired,
#[error(
"remote Aether wire endpoint `{endpoint}` requires mTLS (set NAUTALID_AETHER_TUNNEL_* or dev-only NAUTALID_AETHER_TUNNEL_INSECURE=1)"
)]
RemoteWireTlsRequired { endpoint: String },
}
struct TunnelMaterial {
_guard: TunnelGuard,
tls: WireTls,
}
enum TunnelGuard {
Mounted,
Materialized { _dir: tempfile::TempDir },
}
static TUNNEL: OnceLock<Result<TunnelMaterial, TunnelError>> = OnceLock::new();
#[must_use]
pub fn configured_from_env() -> bool {
tunnel_env_touched()
}
#[must_use]
pub fn insecure_dev_allowed() -> bool {
matches!(
std::env::var("NAUTALID_AETHER_TUNNEL_INSECURE")
.ok()
.as_deref(),
Some("1") | Some("true") | Some("yes")
) || matches!(
std::env::var("KWT_REPLAY_AETHERDB_TLS_INSECURE")
.ok()
.as_deref(),
Some("1") | Some("true") | Some("yes")
)
}
pub fn warm_up() -> Result<(), TunnelError> {
match load_tunnel()? {
Some(_) => {
tracing::info!("AetherDB wire tunnel ready (mTLS client)");
Ok(())
}
None => Ok(()),
}
}
#[must_use]
pub fn host_bind_tls_configured() -> bool {
host_tls_env_touched()
}
#[must_use]
pub fn wire_tls_to_zmq(tls: &WireTls) -> ZmqTlsConfig {
ZmqTlsConfig {
cert_file: Some(tls.cert_file.display().to_string()),
key_file: Some(tls.key_file.display().to_string()),
ca_file: Some(tls.ca_file.display().to_string()),
client_auth: true,
require_explicit_ca: true,
max_concurrent: None,
}
}
pub fn wire_client_zmq_tls() -> Result<Option<ZmqTlsConfig>, TunnelError> {
if insecure_dev_allowed() {
return Ok(None);
}
if let Some(tls) = load_tunnel()? {
return Ok(Some(wire_tls_to_zmq(&tls)));
}
if host_bind_tls_configured() {
return Err(TunnelError::WireClientTlsRequired);
}
Ok(None)
}
pub fn wire_client_zmq_tls_for_endpoint(endpoint: &str) -> Result<Option<ZmqTlsConfig>, TunnelError> {
let tls = wire_client_zmq_tls()?;
if tls.is_none() && remote_wire_endpoint(endpoint) {
return Err(TunnelError::RemoteWireTlsRequired {
endpoint: endpoint.to_string(),
});
}
Ok(tls)
}
fn remote_wire_endpoint(endpoint: &str) -> bool {
let connect = endpoint.strip_prefix("tcp://").unwrap_or(endpoint);
let host = connect.split(':').next().unwrap_or(connect);
!matches!(host, "127.0.0.1" | "localhost" | "0.0.0.0" | "::1")
}
pub fn load_tunnel() -> Result<Option<WireTls>, TunnelError> {
if !tunnel_env_touched() {
return Ok(None);
}
match TUNNEL.get_or_init(build_tunnel) {
Ok(material) => Ok(Some(material.tls.clone())),
Err(err) => Err(err.clone()),
}
}
fn build_tunnel() -> Result<TunnelMaterial, TunnelError> {
let cert = load_component(
"NAUTALID_AETHER_TUNNEL_CERT_PEM",
"NAUTALID_AETHER_TUNNEL_CERT_PATH",
"KWT_REPLAY_AETHERDB_TLS_CERT_PATH",
TunnelError::MissingCert,
)?;
let key = load_component(
"NAUTALID_AETHER_TUNNEL_KEY_PEM",
"NAUTALID_AETHER_TUNNEL_KEY_PATH",
"KWT_REPLAY_AETHERDB_TLS_KEY_PATH",
TunnelError::MissingKey,
)?;
let ca = load_component(
"NAUTALID_AETHER_TUNNEL_CA_PEM",
"NAUTALID_AETHER_TUNNEL_CA_PATH",
"KWT_REPLAY_AETHERDB_TLS_CA_PATH",
TunnelError::MissingCa,
)?;
let needs_materialize = cert.inline.is_some() || key.inline.is_some() || ca.inline.is_some();
if needs_materialize {
let dir = tempfile::tempdir().map_err(|e| TunnelError::Io {
path: "aether tunnel tempdir".into(),
message: e.to_string(),
})?;
let tls = WireTls {
cert_file: write_pem(&dir, "client.pem", cert.bytes()?)?,
key_file: write_pem(&dir, "client-key.pem", key.bytes()?)?,
ca_file: write_pem(&dir, "ca.pem", ca.bytes()?)?,
};
return Ok(TunnelMaterial {
_guard: TunnelGuard::Materialized { _dir: dir },
tls,
});
}
Ok(TunnelMaterial {
_guard: TunnelGuard::Mounted,
tls: WireTls {
cert_file: cert.path.ok_or(TunnelError::MissingCert)?,
key_file: key.path.ok_or(TunnelError::MissingKey)?,
ca_file: ca.path.ok_or(TunnelError::MissingCa)?,
},
})
}
struct Component {
inline: Option<String>,
path: Option<PathBuf>,
}
impl Component {
fn bytes(&self) -> Result<Vec<u8>, TunnelError> {
if let Some(pem) = &self.inline {
return Ok(pem.as_bytes().to_vec());
}
let path = self.path.as_ref().ok_or(TunnelError::MissingCert)?;
std::fs::read(path).map_err(|e| TunnelError::Io {
path: path.display().to_string(),
message: e.to_string(),
})
}
}
fn load_component(
pem_var: &str,
path_var: &str,
legacy_path_var: &str,
missing: TunnelError,
) -> Result<Component, TunnelError> {
if let Ok(pem) = std::env::var(pem_var) {
if !pem.trim().is_empty() {
return Ok(Component {
inline: Some(pem),
path: None,
});
}
}
for name in [path_var, legacy_path_var] {
if let Ok(path) = std::env::var(name) {
let path = path.trim();
if path.is_empty() {
continue;
}
let pb = PathBuf::from(path);
if !pb.is_file() {
return Err(TunnelError::Io {
path: path.to_string(),
message: "tunnel material not a file".into(),
});
}
return Ok(Component {
inline: None,
path: Some(pb),
});
}
}
Err(missing)
}
fn write_pem(dir: &tempfile::TempDir, name: &str, bytes: Vec<u8>) -> Result<PathBuf, TunnelError> {
let path = dir.path().join(name);
std::fs::write(&path, &bytes).map_err(|e| TunnelError::Io {
path: path.display().to_string(),
message: e.to_string(),
})?;
Ok(path)
}
fn host_tls_env_touched() -> bool {
[
"AETHERDB_HOST_TLS_CERT_PATH",
"AETHERDB_HUB_TLS_CERT_PATH",
"AETHERDB_HOST_TLS_KEY_PATH",
"AETHERDB_HUB_TLS_KEY_PATH",
"AETHERDB_HOST_TLS_CA_PATH",
"AETHERDB_HUB_TLS_CA_PATH",
]
.into_iter()
.any(|name| {
std::env::var(name)
.ok()
.is_some_and(|s| !s.trim().is_empty())
})
}
fn tunnel_env_touched() -> bool {
[
"NAUTALID_AETHER_TUNNEL_CERT_PEM",
"NAUTALID_AETHER_TUNNEL_CERT_PATH",
"NAUTALID_AETHER_TUNNEL_KEY_PEM",
"NAUTALID_AETHER_TUNNEL_KEY_PATH",
"NAUTALID_AETHER_TUNNEL_CA_PEM",
"NAUTALID_AETHER_TUNNEL_CA_PATH",
"KWT_REPLAY_AETHERDB_TLS_CERT_PATH",
"KWT_REPLAY_AETHERDB_TLS_KEY_PATH",
"KWT_REPLAY_AETHERDB_TLS_CA_PATH",
]
.into_iter()
.any(|name| {
std::env::var(name)
.ok()
.is_some_and(|s| !s.trim().is_empty())
})
}
#[cfg(test)]
mod tests {
use super::*;
fn install_crypto() {
let _ = crate::platform::install_defaults();
}
fn generated_pem_triple() -> (String, String, String) {
install_crypto();
let ca = rcgen::generate_simple_self_signed(vec!["aether-hub.test".into()]).unwrap();
let ca_pem = ca.cert.pem();
let client = rcgen::generate_simple_self_signed(vec!["nautalid-client".into()]).unwrap();
(
client.cert.pem(),
client.signing_key.serialize_pem(),
ca_pem,
)
}
#[test]
fn wire_tls_maps_to_zmq_config() {
let (cert, key, ca) = generated_pem_triple();
unsafe {
std::env::set_var("NAUTALID_AETHER_TUNNEL_CERT_PEM", &cert);
std::env::set_var("NAUTALID_AETHER_TUNNEL_KEY_PEM", &key);
std::env::set_var("NAUTALID_AETHER_TUNNEL_CA_PEM", &ca);
}
let zmq = wire_client_zmq_tls().expect("tls").expect("configured");
assert!(zmq.cert_file.is_some());
assert!(zmq.client_auth);
assert!(zmq.require_explicit_ca);
unsafe {
std::env::remove_var("NAUTALID_AETHER_TUNNEL_CERT_PEM");
std::env::remove_var("NAUTALID_AETHER_TUNNEL_KEY_PEM");
std::env::remove_var("NAUTALID_AETHER_TUNNEL_CA_PEM");
}
}
#[test]
fn remote_wire_requires_tunnel_material() {
let err = wire_client_zmq_tls_for_endpoint("tcp://aetherdb-hub:5555")
.expect_err("remote without tunnel");
assert!(matches!(err, TunnelError::RemoteWireTlsRequired { .. }));
}
#[test]
fn materializes_inline_pem_tunnel() {
let (cert, key, ca) = generated_pem_triple();
unsafe {
std::env::set_var("NAUTALID_AETHER_TUNNEL_CERT_PEM", &cert);
std::env::set_var("NAUTALID_AETHER_TUNNEL_KEY_PEM", &key);
std::env::set_var("NAUTALID_AETHER_TUNNEL_CA_PEM", &ca);
}
let tls = load_tunnel().expect("tunnel").expect("configured");
assert!(tls.cert_file.is_file());
assert!(tls.key_file.is_file());
assert!(tls.ca_file.is_file());
unsafe {
std::env::remove_var("NAUTALID_AETHER_TUNNEL_CERT_PEM");
std::env::remove_var("NAUTALID_AETHER_TUNNEL_KEY_PEM");
std::env::remove_var("NAUTALID_AETHER_TUNNEL_CA_PEM");
}
}
}