org.apache.cassandra.net.OutboundConnectionInitiator Maven / Gradle / Ivy
Show all versions of cassandra-all Show documentation
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.net;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.ClosedChannelException;
import java.security.cert.Certificate;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import com.google.common.annotations.VisibleForTesting;
import io.netty.util.concurrent.Future; //checkstyle: permit this import
import io.netty.util.concurrent.Promise; //checkstyle: permit this import
import org.apache.cassandra.utils.concurrent.AsyncPromise;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoop;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.SslClosedEngineException;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslHandler;
import io.netty.util.concurrent.ScheduledFuture;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.net.OutboundConnectionInitiator.Result.MessagingSuccess;
import org.apache.cassandra.net.OutboundConnectionInitiator.Result.StreamingSuccess;
import org.apache.cassandra.security.ISslContextFactory;
import org.apache.cassandra.security.SSLFactory;
import org.apache.cassandra.utils.JVMStabilityInspector;
import org.apache.cassandra.utils.concurrent.ImmediateFuture;
import org.apache.cassandra.utils.memory.BufferPools;
import static java.util.concurrent.TimeUnit.*;
import static org.apache.cassandra.auth.IInternodeAuthenticator.InternodeConnectionDirection.OUTBOUND;
import static org.apache.cassandra.auth.IInternodeAuthenticator.InternodeConnectionDirection.OUTBOUND_PRECONNECT;
import static org.apache.cassandra.net.InternodeConnectionUtils.DISCARD_HANDLER_NAME;
import static org.apache.cassandra.net.InternodeConnectionUtils.SSL_HANDLER_NAME;
import static org.apache.cassandra.net.InternodeConnectionUtils.certificates;
import static org.apache.cassandra.net.HandshakeProtocol.*;
import static org.apache.cassandra.net.ConnectionType.STREAMING;
import static org.apache.cassandra.net.OutboundConnectionInitiator.Result.incompatible;
import static org.apache.cassandra.net.OutboundConnectionInitiator.Result.messagingSuccess;
import static org.apache.cassandra.net.OutboundConnectionInitiator.Result.streamingSuccess;
import static org.apache.cassandra.net.SocketFactory.*;
/**
* A {@link ChannelHandler} to execute the send-side of the internode handshake protocol.
* As soon as the handler is added to the channel via {@link ChannelInboundHandler#channelActive(ChannelHandlerContext)}
* (which is only invoked if the underlying TCP connection was properly established), the {@link Initiate}
* handshake is sent. See {@link HandshakeProtocol} for full details.
*
* Upon completion of the handshake (on success or fail), the {@link #resultPromise} is completed.
* See {@link Result} for details about the different result states.
*
* This class extends {@link ByteToMessageDecoder}, which is a {@link ChannelInboundHandler}, because this handler
* waits for the peer's handshake response (the {@link Accept} of the internode messaging handshake protocol).
*/
public class OutboundConnectionInitiator
{
private static final Logger logger = LoggerFactory.getLogger(OutboundConnectionInitiator.class);
private final ConnectionType type;
private final SslFallbackConnectionType sslConnectionType;
private final OutboundConnectionSettings settings;
private final Promise> resultPromise;
private boolean isClosed;
private OutboundConnectionInitiator(ConnectionType type, SslFallbackConnectionType sslConnectionType, OutboundConnectionSettings settings,
Promise> resultPromise)
{
this.type = type;
this.sslConnectionType = sslConnectionType;
this.settings = settings;
this.resultPromise = resultPromise;
}
/**
* Initiate a connection with the requested messaging version.
* if the other node supports a newer version, or doesn't support this version, we will fail to connect
* and try again with the version they reported
*
* The returned {@code Future} is guaranteed to be completed on the supplied eventLoop.
*/
public static Future> initiateStreaming(EventLoop eventLoop, OutboundConnectionSettings settings,
SslFallbackConnectionType sslConnectionType)
{
return new OutboundConnectionInitiator(STREAMING, sslConnectionType, settings, AsyncPromise.withExecutor(eventLoop))
.initiate(eventLoop);
}
/**
* Initiate a connection with the requested messaging version.
* if the other node supports a newer version, or doesn't support this version, we will fail to connect
* and try again with the version they reported
*
* The returned {@code Future} is guaranteed to be completed on the supplied eventLoop.
*/
static Future> initiateMessaging(EventLoop eventLoop, ConnectionType type, SslFallbackConnectionType sslConnectionType,
OutboundConnectionSettings settings, Promise> result)
{
return new OutboundConnectionInitiator<>(type, sslConnectionType, settings, result)
.initiate(eventLoop);
}
private Future> initiate(EventLoop eventLoop)
{
if (logger.isTraceEnabled())
logger.trace("creating outbound bootstrap to {}", settings);
if (!settings.authenticator.authenticate(settings.to.getAddress(), settings.to.getPort(), null, OUTBOUND_PRECONNECT))
{
// interrupt other connections, so they must attempt to re-authenticate
MessagingService.instance().interruptOutbound(settings.to);
logger.error("Authentication failed to " + settings.connectToId());
return ImmediateFuture.failure(new IOException("Authentication failed to " + settings.connectToId()));
}
// this is a bit ugly, but is the easiest way to ensure that if we timeout we can propagate a suitable error message
// and still guarantee that, if on timing out we raced with success, the successfully created channel is handled
AtomicBoolean timedout = new AtomicBoolean();
io.netty.util.concurrent.Future bootstrap = createBootstrap(eventLoop)
.connect()
.addListener(future -> {
eventLoop.execute(() -> {
if (!future.isSuccess())
{
if (future.isCancelled() && !timedout.get())
resultPromise.cancel(true);
else if (future.isCancelled())
resultPromise.tryFailure(new IOException("Timeout handshaking with " + settings.connectToId()));
else
resultPromise.tryFailure(future.cause());
}
});
});
ScheduledFuture> timeout = eventLoop.schedule(() -> {
timedout.set(true);
bootstrap.cancel(false);
}, TIMEOUT_MILLIS, MILLISECONDS);
bootstrap.addListener(future -> timeout.cancel(true));
// Note that the bootstrap future's listeners may be invoked outside of the eventLoop,
// as Epoll failures on connection and disconnect may be run on the GlobalEventExecutor
// Since this FutureResult's listeners are all given to our resultPromise, they are guaranteed to be invoked by the eventLoop.
return new FutureResult<>(resultPromise, bootstrap);
}
/**
* Create the {@link Bootstrap} for connecting to a remote peer. This method does not attempt to connect to the peer,
* and thus does not block.
*/
private Bootstrap createBootstrap(EventLoop eventLoop)
{
Bootstrap bootstrap = settings.socketFactory
.newClientBootstrap(eventLoop, settings.tcpUserTimeoutInMS)
.option(ChannelOption.ALLOCATOR, GlobalBufferPoolAllocator.instance)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, settings.tcpConnectTimeoutInMS)
.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.SO_REUSEADDR, true)
.option(ChannelOption.TCP_NODELAY, settings.tcpNoDelay)
.option(ChannelOption.MESSAGE_SIZE_ESTIMATOR, NoSizeEstimator.instance)
.handler(new Initializer());
if (settings.socketSendBufferSizeInBytes > 0)
bootstrap.option(ChannelOption.SO_SNDBUF, settings.socketSendBufferSizeInBytes);
InetAddressAndPort remoteAddress = settings.connectTo;
bootstrap.remoteAddress(new InetSocketAddress(remoteAddress.getAddress(), remoteAddress.getPort()));
return bootstrap;
}
public enum SslFallbackConnectionType
{
SERVER_CONFIG, // Original configuration of the server
MTLS,
SSL,
NO_SSL
}
private class Initializer extends ChannelInitializer
{
public void initChannel(SocketChannel channel) throws Exception
{
ChannelPipeline pipeline = channel.pipeline();
// order of handlers: ssl -> server-authentication -> logger -> handshakeHandler
if ((sslConnectionType == SslFallbackConnectionType.SERVER_CONFIG && settings.withEncryption())
|| sslConnectionType == SslFallbackConnectionType.SSL || sslConnectionType == SslFallbackConnectionType.MTLS)
{
SslContext sslContext = getSslContext(sslConnectionType);
// for some reason channel.remoteAddress() will return null
InetAddressAndPort address = settings.to;
InetSocketAddress peer = settings.encryption.require_endpoint_verification ? new InetSocketAddress(address.getAddress(), address.getPort()) : null;
SslHandler sslHandler = newSslHandler(channel, sslContext, peer);
logger.trace("creating outbound netty SslContext: context={}, engine={}", sslContext.getClass().getName(), sslHandler.engine().getClass().getName());
pipeline.addFirst(SSL_HANDLER_NAME, sslHandler);
}
pipeline.addLast("server-authentication", new ServerAuthenticationHandler(settings));
if (WIRETRACE)
pipeline.addLast("logger", new LoggingHandler(LogLevel.INFO));
pipeline.addLast("handshake", new Handler());
}
private SslContext getSslContext(SslFallbackConnectionType connectionType) throws IOException
{
boolean requireClientAuth = false;
if (connectionType == SslFallbackConnectionType.MTLS || connectionType == SslFallbackConnectionType.SSL)
{
requireClientAuth = true;
}
else if (connectionType == SslFallbackConnectionType.SERVER_CONFIG)
{
requireClientAuth = settings.withEncryption();
}
return SSLFactory.getOrCreateSslContext(settings.encryption, requireClientAuth, ISslContextFactory.SocketType.CLIENT);
}
}
/**
* Authenticates the server before an outbound connection is established. If a connection is SSL based connection
* Server's identity is verified during ssl handshake using root certificate in truststore. One may choose to ignore
* outbound authentication or perform required authentication for outbound connections in the implementation
* of IInternodeAuthenticator interface.
*/
@VisibleForTesting
static class ServerAuthenticationHandler extends ByteToMessageDecoder
{
final OutboundConnectionSettings settings;
ServerAuthenticationHandler(OutboundConnectionSettings settings)
{
this.settings = settings;
}
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List