Summary
Please add supported Tunnel hooks for the outcome of every physical connection attempt.
The hooks should:
- observe initial connections and reconnects;
- report successful establishment after the Redis handshake completes;
- report every failed attempt, including reconnect failures suppressed by the public
ConnectionFailed event;
- include an identifier for the client certificate selected for that physical attempt;
- include the exception and failure stage when unsuccessful.
Motivation
We maintain a managed client-certificate integration for StackExchange.Redis. It selects a client certificate for each physical TLS connection and reports whether that exact certificate succeeded or failed, allowing fallback to a previously known-good certificate.
In short:
I want to provide a client certificate and know whether the physical connection using that certificate succeeded.
Tunnel is already a configuration-time extension point and is available before ConnectionMultiplexer.Connect returns, so it can observe initial connections as well as later reconnects. Adding physical-attempt outcome hooks there would avoid introducing another independent configuration callback.
Current limitations
Reconnect failures can be suppressed
PhysicalBridge.OnConnectionFailed raises the public ConnectionFailed event only while reportNextFailure is true.
After the first failure, the flag remains false until that bridge establishes a connection. Subsequent reconnect failures are therefore not reported through ConnectionFailed.
This creates the following sequence:
- Redis is connected successfully.
- The established connection closes with
SocketClosed.
ConnectionFailed is raised, consuming the bridge's notification.
- A newly rotated client certificate is selected for reconnect.
- The Redis server rejects that certificate.
- The reconnect fails, but no new
ConnectionFailed event is raised because connectivity was never restored.
- The integration cannot report the rejected certificate or activate its known-good fallback.
- Subsequent reconnects continue using the rejected certificate.
A custom failure classifier cannot fix a notification that is never delivered.
Certificate selection and outcomes are separate
LocalCertificateSelectionCallback reports which certificate was selected, while ConnectionFailed and ConnectionRestored report outcomes independently.
The outcome does not identify the selected certificate. Maintaining a global FIFO queue is unsafe because interactive and subscription connections can attempt reconnection concurrently and complete out of order.
For example:
- Certificate A is selected for one physical connection.
- Certificate B is selected for another.
- The attempts complete in the opposite order.
- Certificate B's rejection can be incorrectly attributed to A.
This can produce incorrect fallback decisions, incorrect telemetry, and retained certificate references.
Proposed Tunnel hooks
Names and argument organization are illustrative:
public abstract class Tunnel
{
public virtual ValueTask OnConnectionEstablishedAsync(
ConnectionAttemptEventArgs args,
CancellationToken cancellationToken) => default;
public virtual ValueTask OnConnectionAttemptFailedAsync(
ConnectionAttemptFailedEventArgs args,
CancellationToken cancellationToken) => default;
}
Suggested arguments:
public class ConnectionAttemptEventArgs : EventArgs
{
public EndPoint EndPoint { get; }
public ConnectionType ConnectionType { get; }
public string? PhysicalName { get; }
public string? ClientCertificateThumbprint { get; }
}
public sealed class ConnectionAttemptFailedEventArgs
: ConnectionAttemptEventArgs
{
public ConnectionAttemptStage Stage { get; }
public ConnectionFailureType FailureType { get; }
public Exception Exception { get; }
public bool? ServerCertificateAccepted { get; }
public SslPolicyErrors? ServerCertificatePolicyErrors { get; }
}
Suggested stages:
public enum ConnectionAttemptStage
{
SocketConnect,
TlsAuthentication,
RedisAuthentication,
RedisHandshake,
}
An alternative is one exactly-once completion hook:
public virtual ValueTask OnConnectionAttemptCompletedAsync(
ConnectionAttemptCompletedEventArgs args,
CancellationToken cancellationToken) => default;
where the arguments include success/failure, stage, failure type, and exception.
Either shape works provided that every physical attempt produces exactly one terminal outcome.
ClientCertificateThumbprint can be either a sha1 or sha256 thumbprint, with the end user determining which based on length.
Alternatively, a second property ClientCertificateThumbprintHashAlgorithm can be added which states the algorithm used (using the .net HashAlgorithmName).
The stage and server-validation result help consumers exclude failures unrelated to the client certificate. Consumers may still need platform-specific classification unless StackExchange.Redis can expose a structured positive signal such as ClientCertificateRejected or a peer TLS alert.
Example usage
internal sealed class ManagedCertificateTunnel : Tunnel
{
public override ValueTask OnConnectionEstablishedAsync(
ConnectionAttemptEventArgs args,
CancellationToken cancellationToken)
{
if (args.ClientCertificateThumbprint is not null)
{
// Report that the used certificate was healthy.
// This can be used for telemetry or to mark a last-known-good certificate.
}
return default;
}
public override ValueTask OnConnectionAttemptFailedAsync(
ConnectionAttemptFailedEventArgs args,
CancellationToken cancellationToken)
{
if (args.ClientCertificateThumbprint is not null
&& IsClientCertificateRejection(args))
{
// Report that the used certificate was unhealthy.
// This can be used for telemetry or to trigger fallback to a LKG certificate.
}
return default;
}
}
Existing alternative
Tunnel.BeforeAuthenticateAsync makes a workaround possible today:
- The integration creates and authenticates its own
SslStream.
- It disables StackExchange.Redis's normal TLS layer.
- It returns a custom stream that tracks the first Redis read and all pre-establishment I/O
failures.
This provides deterministic certificate correlation, including delayed TLS 1.3 alerts, but it requires the integration to take ownership of substantial transport behavior:
- TLS configuration and authentication;
- stream and socket lifetime;
- framework-specific synchronous and asynchronous stream members;
- cloning
SslClientAuthenticationOptions;
- preserving existing custom tunnels;
- introducing a replacement for the write-only
ConfigurationOptions.CertificateValidation
event;
- tracking StackExchange.Redis I/O behavior across versions.
Physical-attempt hooks would allow StackExchange.Redis to continue owning TLS and transport details while providing the small amount of lifecycle information required by certificate integrations.
Logging is not a suitable alternative application-control-flow contract. It does not provide the selected certificate, structured stage information, or a stable guarantee that every physical attempt will continue to be logged.
Compatibility
The proposed methods are additive virtual members with default no-op implementations.
The existing ConnectionFailed and ConnectionRestored events can retain their current suppression and notification behavior.
Summary
Please add supported
Tunnelhooks for the outcome of every physical connection attempt.The hooks should:
ConnectionFailedevent;Motivation
We maintain a managed client-certificate integration for StackExchange.Redis. It selects a client certificate for each physical TLS connection and reports whether that exact certificate succeeded or failed, allowing fallback to a previously known-good certificate.
In short:
Tunnelis already a configuration-time extension point and is available beforeConnectionMultiplexer.Connectreturns, so it can observe initial connections as well as later reconnects. Adding physical-attempt outcome hooks there would avoid introducing another independent configuration callback.Current limitations
Reconnect failures can be suppressed
PhysicalBridge.OnConnectionFailedraises the publicConnectionFailedevent only whilereportNextFailureistrue.After the first failure, the flag remains
falseuntil that bridge establishes a connection. Subsequent reconnect failures are therefore not reported throughConnectionFailed.This creates the following sequence:
SocketClosed.ConnectionFailedis raised, consuming the bridge's notification.ConnectionFailedevent is raised because connectivity was never restored.A custom failure classifier cannot fix a notification that is never delivered.
Certificate selection and outcomes are separate
LocalCertificateSelectionCallbackreports which certificate was selected, whileConnectionFailedandConnectionRestoredreport outcomes independently.The outcome does not identify the selected certificate. Maintaining a global FIFO queue is unsafe because interactive and subscription connections can attempt reconnection concurrently and complete out of order.
For example:
This can produce incorrect fallback decisions, incorrect telemetry, and retained certificate references.
Proposed
TunnelhooksNames and argument organization are illustrative:
Suggested arguments:
Suggested stages:
An alternative is one exactly-once completion hook:
where the arguments include success/failure, stage, failure type, and exception.
Either shape works provided that every physical attempt produces exactly one terminal outcome.
ClientCertificateThumbprintcan be either a sha1 or sha256 thumbprint, with the end user determining which based on length.Alternatively, a second property
ClientCertificateThumbprintHashAlgorithmcan be added which states the algorithm used (using the .netHashAlgorithmName).The stage and server-validation result help consumers exclude failures unrelated to the client certificate. Consumers may still need platform-specific classification unless StackExchange.Redis can expose a structured positive signal such as
ClientCertificateRejectedor a peer TLS alert.Example usage
Existing alternative
Tunnel.BeforeAuthenticateAsyncmakes a workaround possible today:SslStream.failures.
This provides deterministic certificate correlation, including delayed TLS 1.3 alerts, but it requires the integration to take ownership of substantial transport behavior:
SslClientAuthenticationOptions;ConfigurationOptions.CertificateValidationevent;
Physical-attempt hooks would allow StackExchange.Redis to continue owning TLS and transport details while providing the small amount of lifecycle information required by certificate integrations.
Logging is not a suitable alternative application-control-flow contract. It does not provide the selected certificate, structured stage information, or a stable guarantee that every physical attempt will continue to be logged.
Compatibility
The proposed methods are additive virtual members with default no-op implementations.
The existing
ConnectionFailedandConnectionRestoredevents can retain their current suppression and notification behavior.