-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Description
What is the problem your feature solves, or the need it fulfills?
When building an API gateway or reverse proxy, we need to log TLS handshake information for security auditing and debugging purposes. This includes:
-
Successful handshakes: negotiated TLS version, cipher suite, SNI, client certificate info (for mTLS)
-
Failed handshakes: reason for failure, client IP, attempted SNI
Currently, Pingora only provides certificate_callback in the TlsAccept trait, which is called during the handshake (before cipher negotiation completes). There's no way to:
-
Get notified when a handshake succeeds with complete negotiated parameters
-
Get notified when a handshake fails with error details
-
Log this information in a custom format (e.g., JSON for structured logging)
The only way to capture successful TLS info is in HTTP request handlers (like early_request_filter), but this approach:
-
Requires deduplication logic for HTTP/2 (multiple requests per connection)
-
Cannot capture failed handshakes at all
-
Mixes TLS layer concerns with HTTP layer code
Describe the solution you'd like
Add two new optional methods to the TlsAccept trait:
#[async_trait]
pub trait TlsAccept {
/// Called during TLS handshake to provide certificate (existing)
async fn certificate_callback(&self, _ssl: &mut TlsRef) {}
/// Called after TLS handshake completes successfully
/// `ssl` contains negotiated parameters (version, cipher, etc.)
async fn handshake_done(&self, _ssl: &TlsRef, _peer_addr: Option<&SocketAddr>) {}
/// Called when TLS handshake fails
/// `error` contains the failure reason
async fn handshake_failed(&self, _peer_addr: Option<&SocketAddr>, _error: &Error) {}
}
These callbacks would be invoked in Service::run_endpoint after io.handshake() completes:
match timeout(Duration::from_secs(60), io.handshake()).await {
Ok(Ok(io)) => {
// NEW: Call handshake_done callback
if let Some(callbacks) = &callbacks {
callbacks.handshake_done(io.ssl(), peer_addr.as_ref()).await;
}
Self::handle_event(io, app, shutdown).await
}
Ok(Err(e)) => {
// NEW: Call handshake_failed callback
if let Some(callbacks) = &callbacks {
callbacks.handshake_failed(peer_addr.as_ref(), &e).await;
}
error!("Downstream handshake error: {e}");
}
Err(_) => {
// NEW: Call handshake_failed for timeout
if let Some(callbacks) = &callbacks {
callbacks.handshake_failed(peer_addr.as_ref(), &timeout_error).await;
}
error!("Downstream handshake timeout");
}
}
Describe alternatives you've considered
- Log in HTTP request handlers (current workaround)
-
Pros: Works for successful handshakes
-
Cons: Cannot capture failures; requires dedup for HTTP/2; mixes HTTP/TLS concerns
- Parse Pingora's error logs
-
Pros: No code changes needed
-
Cons: Unstructured; cannot access full TLS context; harder to correlate
- Wrap Pingora's TLS stream
-
Pros: Full control
-
Cons: Complex; may break with Pingora updates; duplicates Pingora's code
- Use BoringSSL info callback (SSL_CTX_set_info_callback)
-
Pros: Gets notified on handshake state changes
-
Cons: Low-level FFI; doesn't integrate with Pingora's async flow; no peer address context
Additional context
This is a common requirement for:
-
Security auditing: Log all TLS connections with negotiated parameters
-
Debugging: Understand why certain clients fail to connect
-
Compliance: Record TLS versions/ciphers for security policies
-
Observability: Metrics on TLS handshake success/failure rates
Similar functionality exists in:
-
Nginx: ssl_session_fetch_callback, access to $ssl_* variables in logs
-
Envoy: ConnectionManager callbacks, access log with TLS fields
-
HAProxy: ssl_fc_* fetch methods for logging
Reference: Current TlsAccept trait location: pingora-core/src/listeners/mod.rs