FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

RATIS-2638. Add a TLS handshake failure listener by HTHou · Pull Request #1561 · apache/ratis · GitHub

/ ratis Public

RATIS-2638. Add a TLS handshake failure listener - #1561

Merged
szetszwo merged 3 commits into
apache:masterfrom
HTHou:codex/RATIS-2638
Aug 28, 2026
Merged

RATIS-2638. Add a TLS handshake failure listener#1561
szetszwo merged 3 commits into
apache:masterfrom
HTHou:codex/RATIS-2638

Conversation

HTHou commented Aug 24, 2026
edited
Loading

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Add an optional generic ServerCredentials configuration to ratis-grpc. Applications can register credentials through GrpcConfigKeys.Server.setCredentials(...), and Ratis passes them directly to NettyServerBuilder.forAddress(...). This supports custom credential implementations without specializing the configuration API for one use case.

This pull request also adds TlsHandshakeFailureServerCredentials.create(GrpcTlsConfig, Consumer<TlsHandshakeFailureServerCredentials.Event>) for applications that need to observe inbound gRPC TLS handshake failures. Ratis reports the original cause and the physical local and remote socket addresses. The callback runs on a transport event-loop thread and must not block. Listener failures are isolated from the TLS failure and channel-closing behavior. Ratis does not perform audit logging or require callers to persist the exception; applications may use only the endpoint information when that is sufficient for their audit policy.

The TLS handshake failure credentials wrap the shaded gRPC protocol negotiator inside Ratis. They report TLS and ALPN negotiation failures at most once per physical connection and ignore channel closure without an SSLException. Configured server credentials apply to SERVER, CLIENT, and ADMIN server endpoints, including shared ports. When no credentials are configured, the existing server creation and SslContext paths remain unchanged.

What is the link to the Apache JIRA

https://issues.apache.org/jira/browse/RATIS-2638

How was this patch tested?

Added TestRaftServerWithGrpc#testTlsHandshakeFailureListener, which verifies that a valid TLS request does not trigger the listener and that a plaintext connection to the TLS port reports one inbound failure with the expected local and remote addresses.

Also ran:

  • mvn -pl ratis-test -am -Dtest=TestRaftServerWithGrpc#testTlsHandshakeFailureListener -Dsurefire.failIfNoSpecifiedTests=false test
  • mvn -B -fae -DskipTests checkstyle:check
  • mvn -pl ratis-grpc -DskipTests clean compile spotbugs:check

HTHou force-pushed the codex/RATIS-2638 branch from a2338ec to 49bb081 Compare August 24, 2026 09:30
HTHou changed the title RATIS-2638. Add a TLS handshake failure listener for gRPC servers RATIS-2638. Add a gRPC server builder creation hook Aug 24, 2026
HTHou force-pushed the codex/RATIS-2638 branch from 49bb081 to d16bacd Compare August 24, 2026 10:44
HTHou changed the title RATIS-2638. Add a gRPC server builder creation hook RATIS-2638. Add a TLS handshake failure listener Aug 24, 2026

szetszwo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

@HTHou , thanks for working on this!

I only have a comment on the conf so far. We should try to make it more general to support any ServerCredentials.

This part of the grpc code is new to me. Will need some time to review the other code.

Comment on lines +296 to +298
String TLS_HANDSHAKE_FAILURE_LISTENER_PARAMETER = PREFIX + ".tls.handshake.failure.listener";
Class<TlsHandshakeFailureListener> TLS_HANDSHAKE_FAILURE_LISTENER_CLASS =
TlsHandshakeFailureListener.class;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality
NettyServerBuilder forAddress(SocketAddress address, ServerCredentials creds) 

Since the forAddress parameter is ServerCredentials, let's use ServerCredentials as the class. Then, it will also support other ServerCredentials.

    String CREDENTIALS_PARAMETER = PREFIX + ".credentials";
    Class<ServerCredentials> CREDENTIALS_CLASS = ServerCredentials.class;
    static ServerCredentials credentials(Parameters parameters) {
      return parameters == null ? null
          : parameters.get(CREDENTIALS_PARAMETER, CREDENTIALS_CLASS);
    }
    static void setCredentials(Parameters parameters, ServerCredentials listener) {
      parameters.put(CREDENTIALS_PARAMETER, listener, CREDENTIALS_CLASS);
    }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Thanks for the suggestion. Updated in 058baf3: the parameter now stores a generic ServerCredentials via GrpcConfigKeys.Server.setCredentials(...), and GrpcServicesImpl passes it directly to NettyServerBuilder.forAddress(...). The TLS listener is exposed through TlsHandshakeFailureServerCredentials.create(...), and the test registers that implementation through the generic credentials configuration, so other ServerCredentials implementations are supported as well.

szetszwo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

@HTHou , thanks for the update! Please see the comments inlined and also https://issues.apache.org/jira/secure/attachment/13084161/1561_review.patch

Comment on lines 237 to 241
if (serverCredentials != null) {
LOG.info("Setting server credentials for {}", address);
} else if (sslContext != null) {
LOG.info("Setting TLS for {}", address);
nettyServerBuilder.sslContext(sslContext);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Move the LOG to above if-statement.

      if (serverCredentials != null) {
        LOG.info("Setting server credentials for {}", address);
        nettyServerBuilder = NettyServerBuilder.forAddress(address, serverCredentials);
      } else {
        nettyServerBuilder = NettyServerBuilder.forAddress(address);
        if (sslContext != null) {
          LOG.info("Setting sslContext for {}", address);
          nettyServerBuilder.sslContext(sslContext);
        }
      }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Updated in 64ca1d7. The log statements are now inside the corresponding serverCredentials and sslContext branches.

import java.util.Objects;

/** Information about a failed TLS handshake. */
public final class TlsHandshakeFailureEvent {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Let's move it as an inner class TlsHandshakeFailureServerCredentials.Event.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Updated in 64ca1d7. The event is now the nested TlsHandshakeFailureServerCredentials.Event, and the standalone event class has been removed.

Comment on lines +22 to +28
public interface TlsHandshakeFailureListener {
/**
* Invoked on a transport event-loop thread. Implementations must not block and should hand off
* expensive work to another thread.
*/
void onFailure(TlsHandshakeFailureEvent event);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Let's use Consumer<TlsHandshakeFailureEvent> instead of adding a new interface.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Updated in 64ca1d7. create(...) now accepts Consumer<TlsHandshakeFailureServerCredentials.Event>, and the standalone listener interface has been removed. I kept the non-blocking transport event-loop requirement in the API documentation.

final ChannelHandler grpcNegotiationHandler =
InternalProtocolNegotiators.grpcNegotiationHandler(grpcHandler);
final ChannelHandler tlsHandler = new ServerTlsHandler(grpcNegotiationHandler, grpcHandler,
sslContext, listener, offloadExecutorPool);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

It seems better to pass the executor, instead of the offloadExecutorPool.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Updated in 64ca1d7. Negotiator now passes its acquired executor to ServerTlsHandler, avoiding a second getObject() call while retaining the existing returnObject() lifecycle.

.contains(sslHandler.applicationProtocol())) {
final RuntimeException cause = Status.UNAVAILABLE
.withDescription("Failed protocol negotiation: Unable to find compatible protocol")
.asRuntimeException();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

How about using asException()?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Updated in 64ca1d7. The ALPN negotiation failure now uses Status.asException().

Comment on lines +191 to +194
if (failureReported) {
return;
}
failureReported = true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

It is better to use AtomicBoolean and compareAndSet:

      if (failureReported.compareAndSet(false, true)) {
        return;
      }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Updated in 64ca1d7. failureReported is now an AtomicBoolean. I used if (!failureReported.compareAndSet(false, true)) { return; } so that the first failure reaches the listener and subsequent reports are skipped.

Comment on lines +178 to +181
if (!sslContext.applicationProtocolNegotiator().protocols()
.contains(sslHandler.applicationProtocol())) {
final RuntimeException cause = Status.UNAVAILABLE
.withDescription("Failed protocol negotiation: Unable to find compatible protocol")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Include the protocol string in the exception:

      final String protocol = sslHandler.applicationProtocol();
      if (!sslContext.applicationProtocolNegotiator().protocols().contains(protocol)) {
        final Exception cause = Status.UNAVAILABLE
            .withDescription("Failed protocol negotiation: Unable to find compatible protocol for " + protocol)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Updated in 64ca1d7. The negotiated protocol is captured and included in the ALPN failure description.

szetszwo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

+1 the change looks good.

szetszwo merged commit df1fc70 into apache:master Aug 28, 2026
16 checks passed
HTHou deleted the codex/RATIS-2638 branch August 30, 2026 04:41
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants


Back | FazBrowse Home | New Git URL