Skip to content

Feature Request: TLS Handshake Completion Callback #786

@QiiHeng3

Description

@QiiHeng3

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:

  1. Get notified when a handshake succeeds with complete negotiated parameters

  2. Get notified when a handshake fails with error details

  3. 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

  1. 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

  1. Parse Pingora's error logs
  • Pros: No code changes needed

  • Cons: Unstructured; cannot access full TLS context; harder to correlate

  1. Wrap Pingora's TLS stream
  • Pros: Full control

  • Cons: Complex; may break with Pingora updates; duplicates Pingora's code

  1. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions