use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ProbeError {
#[error("HTTP probe failed: {0}")]
Http(#[from] reqwest::Error),
#[error("probe got HTTP {0}")]
BadStatus(u16),
#[error("inbound TLS not configured (probe requires NAUTALID_TLS_*)")]
TlsRequired,
#[error(
"inbound mTLS requires probe client cert (NAUTALID_PROBE_CLIENT_CERT_* and NAUTALID_PROBE_CLIENT_KEY_*)"
)]
MtlsClientRequired,
#[error("read probe client material {path}: {source}")]
Io {
path: String,
source: std::io::Error,
},
#[error("H3 probe failed: {0}")]
H3(String),
}
pub async fn probe_ready_from_env() -> Result<(), ProbeError> {
if !crate::tls::configured_from_env() {
return Err(ProbeError::TlsRequired);
}
let addr = crate::app::probe_addr_from_env();
probe_url(&format!("https://{addr}/ready")).await?;
if crate::surfaces::env_enabled("NAUTALID_PROBE_H3") {
probe_h3_ready(addr).await?;
}
Ok(())
}
pub async fn probe_url(url: &str) -> Result<(), ProbeError> {
let client = probe_client_builder()?.build()?;
let resp = client.get(url).send().await?;
let status = resp.status().as_u16();
if (200..300).contains(&status) {
Ok(())
} else {
Err(ProbeError::BadStatus(status))
}
}
fn probe_client_builder() -> Result<reqwest::ClientBuilder, ProbeError> {
let mut builder = reqwest::Client::builder()
.timeout(Duration::from_secs(2))
.danger_accept_invalid_certs(true);
if let Some(identity_pem) = probe_client_identity_pem_from_env()? {
let identity = reqwest::Identity::from_pem(identity_pem.as_bytes())?;
builder = builder.identity(identity);
} else if crate::tls::client_auth_required_from_env() {
return Err(ProbeError::MtlsClientRequired);
}
Ok(builder)
}
fn probe_client_identity_pem_from_env() -> Result<Option<String>, ProbeError> {
let cert = match probe_pem_from_env(
"NAUTALID_PROBE_CLIENT_CERT_PEM",
"NAUTALID_PROBE_CLIENT_CERT_PATH",
) {
Ok(pem) => pem,
Err(ProbeError::MtlsClientRequired) => return Ok(None),
Err(err) => return Err(err),
};
let key = probe_pem_from_env(
"NAUTALID_PROBE_CLIENT_KEY_PEM",
"NAUTALID_PROBE_CLIENT_KEY_PATH",
)?;
Ok(Some(format!("{cert}\n{key}")))
}
fn probe_pem_from_env(inline: &str, path_var: &str) -> Result<String, ProbeError> {
if let Ok(pem) = std::env::var(inline) {
if !pem.trim().is_empty() {
return Ok(pem);
}
}
if let Ok(path) = std::env::var(path_var) {
if !path.is_empty() {
return std::fs::read_to_string(&path).map_err(|source| ProbeError::Io {
path: path.clone(),
source,
});
}
}
Err(ProbeError::MtlsClientRequired)
}
async fn probe_h3_ready(addr: SocketAddr) -> Result<(), ProbeError> {
use std::io::Cursor;
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime};
#[derive(Debug)]
struct SkipServerVerify;
impl ServerCertVerifier for SkipServerVerify {
fn verify_server_cert(
&self,
_end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>,
_ocsp_response: &[u8],
_now: UnixTime,
) -> Result<ServerCertVerified, rustls::Error> {
Ok(ServerCertVerified::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
rustls::crypto::CryptoProvider::get_default()
.map(|p| p.signature_verification_algorithms.supported_schemes())
.unwrap_or_else(|| {
rustls::crypto::ring::default_provider()
.signature_verification_algorithms
.supported_schemes()
})
}
}
let provider = rustls::crypto::CryptoProvider::get_default()
.ok_or_else(|| ProbeError::H3("rustls crypto provider not installed".into()))?;
let builder = rustls::ClientConfig::builder_with_provider(provider.clone())
.with_protocol_versions(&[&rustls::version::TLS13])
.map_err(|e| ProbeError::H3(e.to_string()))?
.dangerous()
.with_custom_certificate_verifier(Arc::new(SkipServerVerify));
let mut rustls_config = if let (Ok(cert_pem), Ok(key_pem)) = (
probe_pem_from_env(
"NAUTALID_PROBE_CLIENT_CERT_PEM",
"NAUTALID_PROBE_CLIENT_CERT_PATH",
),
probe_pem_from_env(
"NAUTALID_PROBE_CLIENT_KEY_PEM",
"NAUTALID_PROBE_CLIENT_KEY_PATH",
),
) {
let mut cert_reader = Cursor::new(cert_pem.as_bytes());
let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut cert_reader)
.collect::<Result<Vec<_>, _>>()
.map_err(|e| ProbeError::H3(e.to_string()))?;
let mut key_reader = Cursor::new(key_pem.as_bytes());
let key = rustls_pemfile::read_all(&mut key_reader)
.find_map(|item| {
item.ok().and_then(|i| match i {
rustls_pemfile::Item::Pkcs1Key(k) => Some(PrivateKeyDer::Pkcs1(k)),
rustls_pemfile::Item::Pkcs8Key(k) => Some(PrivateKeyDer::Pkcs8(k)),
rustls_pemfile::Item::Sec1Key(k) => Some(PrivateKeyDer::Sec1(k)),
_ => None,
})
})
.ok_or_else(|| ProbeError::H3("probe client key PEM missing".into()))?;
builder
.with_client_auth_cert(certs, key)
.map_err(|e| ProbeError::H3(e.to_string()))?
} else if crate::tls::client_auth_required_from_env() {
return Err(ProbeError::MtlsClientRequired);
} else {
builder.with_no_client_auth()
};
rustls_config.alpn_protocols = vec![b"h3".to_vec()];
let quic = quinn::crypto::rustls::QuicClientConfig::try_from(rustls_config)
.map_err(|e| ProbeError::H3(e.to_string()))?;
let mut endpoint = quinn::Endpoint::client("[::]:0".parse().expect("client bind"))
.map_err(|e| ProbeError::H3(e.to_string()))?;
endpoint.set_default_client_config(quinn::ClientConfig::new(Arc::new(quic)));
let conn = endpoint
.connect(addr, "localhost")
.map_err(|e| ProbeError::H3(e.to_string()))?
.await
.map_err(|e| ProbeError::H3(e.to_string()))?;
let quinn_conn = h3_quinn::Connection::new(conn);
let (driver, mut client) = h3::client::builder()
.build::<_, _, bytes::Bytes>(quinn_conn)
.await
.map_err(|e| ProbeError::H3(e.to_string()))?;
let mut stream = client
.send_request(
http::Request::builder()
.uri("http://localhost/ready")
.body(())
.expect("request"),
)
.await
.map_err(|e| ProbeError::H3(e.to_string()))?;
stream
.finish()
.await
.map_err(|e| ProbeError::H3(e.to_string()))?;
let response = stream
.recv_response()
.await
.map_err(|e| ProbeError::H3(e.to_string()))?;
drop(driver);
let status = response.status().as_u16();
if (200..300).contains(&status) {
Ok(())
} else {
Err(ProbeError::BadStatus(status))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http::router;
use crate::state::AppState;
fn install_crypto() {
let _ = crate::platform::install_defaults();
}
fn test_pem_pair() -> (String, String) {
let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap();
(cert.cert.pem(), cert.signing_key.serialize_pem())
}
#[tokio::test]
async fn probe_ready_against_h2_tls_server() {
install_crypto();
#[cfg(feature = "filter-control")]
crate::readiness::set_bus_ready(true);
let (cert, key) = test_pem_pair();
unsafe {
std::env::set_var("NAUTALID_TLS_CERT_PEM", &cert);
std::env::set_var("NAUTALID_TLS_KEY_PEM", &key);
}
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
drop(listener);
let tls = Arc::new(crate::tls::InboundTls::from_env().unwrap());
let app = router(AppState::empty());
let h2_config = tls.h2_config_store();
let server = tokio::spawn(crate::http_transport::serve_h2_tls(
addr,
h2_config,
None,
app,
false,
));
for _ in 0..50 {
if probe_url(&format!("https://{addr}/ready")).await.is_ok() {
server.abort();
unsafe {
std::env::remove_var("NAUTALID_TLS_CERT_PEM");
std::env::remove_var("NAUTALID_TLS_KEY_PEM");
}
return;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
server.abort();
panic!("h2 probe never succeeded");
}
}