Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@
import javax.net.ssl.SSLEngineResult.Status;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLKeyException;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLProtocolException;
import javax.net.ssl.SSLSession;

import org.apache.kafka.common.errors.SslAuthenticationException;
import org.apache.kafka.common.security.auth.KafkaPrincipal;
Expand Down Expand Up @@ -255,17 +257,17 @@ public void handshake() throws IOException {

doHandshake();
} catch (SSLException e) {
handshakeFailure(e, true);
maybeProcessHandshakeFailure(e, true, null);
} catch (IOException e) {
maybeThrowSslAuthenticationException();

// this exception could be due to a write. If there is data available to unwrap,
// process the data so that any SSLExceptions are reported
// process the data so that any SSL handshake exceptions are reported

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It would be good to elaborate on why we need to do this as it's not obvious by just reading the code.

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.

Added comments in a new method to process the exceptions.

if (handshakeStatus == HandshakeStatus.NEED_UNWRAP && netReadBuffer.position() > 0) {
try {
handshakeUnwrap(false);
} catch (SSLException e1) {
handshakeFailure(e1, false);
maybeProcessHandshakeFailure(e1, false, e);
}
}
// If we get here, this is not a handshake failure, throw the original IOException
Expand Down Expand Up @@ -824,6 +826,32 @@ private void handshakeFailure(SSLException sslException, boolean flush) throws I
throw handshakeException;
}

// SSL handshake failures are typically thrown as SSLHandshakeException, SSLProtocolException,
// SSLPeerUnverifiedException or SSLKeyException if the cause is known. These exceptions indicate
// authentication failures (e.g. configuration errors) which should not be retried. But the SSL engine
// may also throw exceptions using the base class SSLException in a few cases:
// a) If there are no matching ciphers or TLS version or the private key is invalid, client will be
// unable to process the server message and an SSLException is thrown:
// javax.net.ssl.SSLException: Unrecognized SSL message, plaintext connection?
// b) If server closes the connection gracefully during handshake, client may receive close_notify
// and and an SSLException is thrown:
// javax.net.ssl.SSLException: Received close_notify during handshake
// We want to handle a) as a non-retriable SslAuthenticationException and b) as a retriable IOException.
// To do this we need to rely on the exception string. Since it is safer to throw a retriable exception
// when we are not sure, we will treat only the first exception string as a handshake exception.
private void maybeProcessHandshakeFailure(SSLException sslException, boolean flush, IOException ioException) throws IOException {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would it be easier to understand if this handled all of the unwrap exceptions after the IOException? And then we could call this method processUnwrapExceptionAfterIOException.

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.

@ijuma Thanks for the review. At the moment, this method is used to process SSLException in two places, regardless of whether the original exception was after IOException or not. I thought it would be better to do the String check in a single method rather than separate out handling of IOException. We need to handle both cases because SSLException due to close_notify may be processed before or after IOException. I can use two methods if a single method is confusing.

@ijuma ijuma Jul 17, 2018

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Good point. So the name doesn't work, but in both cases we have two catches like:

} catch (SSLHandshakeException | SSLProtocolException | SSLPeerUnverifiedException | SSLKeyException e) {
  ...
} catch (SSLException e1) {
 ...
}

Maybe we can catch SSLException and then pass it to the shared method. What do you think?

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.

@ijuma Yes, I was in two minds about that - whether to use instanceof in one place or catch the exception in two places. I have updated to use the common method. Thanks.

if (sslException instanceof SSLHandshakeException || sslException instanceof SSLProtocolException ||
sslException instanceof SSLPeerUnverifiedException || sslException instanceof SSLKeyException ||
sslException.getMessage().contains("Unrecognized SSL message"))
handshakeFailure(sslException, flush);
else if (ioException == null)
throw sslException;
else {
log.debug("SSLException while unwrapping data after IOException, original IOException will be propagated", sslException);
throw ioException;
}
}

// If handshake has already failed, throw the authentication exception.
private void maybeThrowSslAuthenticationException() {
if (handshakeException != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ public class NioEchoServer extends Thread {
private volatile WritableByteChannel outputChannel;
private final CredentialCache credentialCache;
private final Metrics metrics;
private int numSent = 0;
private volatile int numSent = 0;
private volatile boolean closeKafkaChannels;
private final DelegationTokenCache tokenCache;

public NioEchoServer(ListenerName listenerName, SecurityProtocol securityProtocol, AbstractConfig config,
Expand Down Expand Up @@ -155,6 +156,11 @@ public void run() {
}
newChannels.clear();
}
if (closeKafkaChannels) {
for (KafkaChannel channel : selector.channels())
selector.close(channel.id());
closeKafkaChannels = false;
}

List<NetworkReceive> completedReceives = selector.completedReceives();
for (NetworkReceive rcv : completedReceives) {
Expand All @@ -174,7 +180,6 @@ public void run() {
selector.unmute(send.destination());
numSent += 1;
}

}
} catch (IOException e) {
// ignore
Expand Down Expand Up @@ -208,15 +213,26 @@ public Selector selector() {
return selector;
}

public void closeConnections() throws IOException {
for (SocketChannel channel : socketChannels)
public void closeKafkaChannels() throws IOException {
closeKafkaChannels = true;
selector.wakeup();
try {
TestUtils.waitForCondition(() -> selector.channels().isEmpty(), "Channels not closed");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}

public void closeSocketChannels() throws IOException {
for (SocketChannel channel : socketChannels) {
channel.close();
}
socketChannels.clear();
}

public void close() throws IOException, InterruptedException {
this.serverSocketChannel.close();
closeConnections();
closeSocketChannels();
acceptorThread.interrupt();
acceptorThread.join();
interrupt();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -699,28 +699,69 @@ public boolean conditionMet() {
*/
@Test
public void testIOExceptionsDuringHandshakeRead() throws Exception {
testIOExceptionsDuringHandshake(true, false);
server = createEchoServer(SecurityProtocol.SSL);
testIOExceptionsDuringHandshake(FailureAction.THROW_IO_EXCEPTION, FailureAction.NO_OP);
}

/**
* Tests that IOExceptions from write during SSL handshake are not treated as authentication failures.
*/
@Test
public void testIOExceptionsDuringHandshakeWrite() throws Exception {
testIOExceptionsDuringHandshake(false, true);
server = createEchoServer(SecurityProtocol.SSL);
testIOExceptionsDuringHandshake(FailureAction.NO_OP, FailureAction.THROW_IO_EXCEPTION);
}

/**
* Tests that if the remote end closes connection ungracefully during SSL handshake while reading data,
* the disconnection is not treated as an authentication failure.
*/
@Test
public void testUngracefulRemoteCloseDuringHandshakeRead() throws Exception {
server = createEchoServer(SecurityProtocol.SSL);
testIOExceptionsDuringHandshake(server::closeSocketChannels, FailureAction.NO_OP);
}

/**
* Tests that if the remote end closes connection ungracefully during SSL handshake while writing data,
* the disconnection is not treated as an authentication failure.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One question: how would the test fail if the disconnection was treated as an authentication failure?

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.

assertTrue("Unexpected channel state " + state, state == ChannelState.State.AUTHENTICATE || state == ChannelState.State.READY)

All the tests check the state when the channel is disconnected. state will be AUTHENTICATION_FAILED if the disconnection is treated as an authentication failure.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks, makes sense.

*/
@Test
public void testUngracefulRemoteCloseDuringHandshakeWrite() throws Exception {
server = createEchoServer(SecurityProtocol.SSL);
testIOExceptionsDuringHandshake(FailureAction.NO_OP, server::closeSocketChannels);
}

private void testIOExceptionsDuringHandshake(boolean failRead, boolean failWrite) throws Exception {
/**
* Tests that if the remote end closes the connection during SSL handshake while reading data,
* the disconnection is not treated as an authentication failure.
*/
@Test
public void testGracefulRemoteCloseDuringHandshakeRead() throws Exception {
server = createEchoServer(SecurityProtocol.SSL);
testIOExceptionsDuringHandshake(FailureAction.NO_OP, server::closeKafkaChannels);
}

/**
* Tests that if the remote end closes the connection during SSL handshake while writing data,
* the disconnection is not treated as an authentication failure.
*/
@Test
public void testGracefulRemoteCloseDuringHandshakeWrite() throws Exception {
server = createEchoServer(SecurityProtocol.SSL);
testIOExceptionsDuringHandshake(server::closeKafkaChannels, FailureAction.NO_OP);
}

private void testIOExceptionsDuringHandshake(FailureAction readFailureAction,
FailureAction flushFailureAction) throws Exception {
TestSslChannelBuilder channelBuilder = new TestSslChannelBuilder(Mode.CLIENT);
boolean done = false;
for (int i = 1; i <= 100; i++) {
int readFailureIndex = failRead ? i : Integer.MAX_VALUE;
int flushFailureIndex = failWrite ? i : Integer.MAX_VALUE;
String node = String.valueOf(i);

channelBuilder.readFailureIndex = readFailureIndex;
channelBuilder.flushFailureIndex = flushFailureIndex;
channelBuilder.readFailureAction = readFailureAction;
channelBuilder.flushFailureAction = flushFailureAction;
channelBuilder.failureIndex = i;
channelBuilder.configure(sslClientConfigs);
this.selector = new Selector(5000, new Metrics(), new MockTime(), "MetricGroup", channelBuilder, new LogContext());

Expand All @@ -734,7 +775,9 @@ private void testIOExceptionsDuringHandshake(boolean failRead, boolean failWrite
break;
}
if (selector.disconnected().containsKey(node)) {
assertEquals(ChannelState.State.AUTHENTICATE, selector.disconnected().get(node).state());
ChannelState.State state = selector.disconnected().get(node).state();
assertTrue("Unexpected channel state " + state,
state == ChannelState.State.AUTHENTICATE || state == ChannelState.State.READY);
break;
}
}
Expand Down Expand Up @@ -973,13 +1016,23 @@ private NioEchoServer createEchoServer(SecurityProtocol securityProtocol) throws
return createEchoServer(ListenerName.forSecurityProtocol(securityProtocol), securityProtocol);
}

@FunctionalInterface
private interface FailureAction {
FailureAction NO_OP = () -> { };
FailureAction THROW_IO_EXCEPTION = () -> {
throw new IOException("Test IO exception");
};
void run() throws IOException;
}

private static class TestSslChannelBuilder extends SslChannelBuilder {

private Integer netReadBufSizeOverride;
private Integer netWriteBufSizeOverride;
private Integer appBufSizeOverride;
long readFailureIndex = Long.MAX_VALUE;
long flushFailureIndex = Long.MAX_VALUE;
private long failureIndex = Long.MAX_VALUE;
FailureAction readFailureAction = FailureAction.NO_OP;
FailureAction flushFailureAction = FailureAction.NO_OP;
int flushDelayCount = 0;

public TestSslChannelBuilder(Mode mode) {
Expand Down Expand Up @@ -1029,8 +1082,8 @@ public TestSslTransportLayer(String channelId, SelectionKey key, SSLEngine sslEn
this.netReadBufSize = new ResizeableBufferSize(netReadBufSizeOverride);
this.netWriteBufSize = new ResizeableBufferSize(netWriteBufSizeOverride);
this.appBufSize = new ResizeableBufferSize(appBufSizeOverride);
numReadsRemaining = new AtomicLong(readFailureIndex);
numFlushesRemaining = new AtomicLong(flushFailureIndex);
numReadsRemaining = new AtomicLong(failureIndex);
numFlushesRemaining = new AtomicLong(failureIndex);
numDelayedFlushesRemaining = new AtomicInteger(flushDelayCount);
}

Expand Down Expand Up @@ -1058,14 +1111,14 @@ protected int applicationBufferSize() {
@Override
protected int readFromSocketChannel() throws IOException {
if (numReadsRemaining.decrementAndGet() == 0 && !ready())
throw new IOException("Test exception during read");
readFailureAction.run();
return super.readFromSocketChannel();
}

@Override
protected boolean flush(ByteBuffer buf) throws IOException {
if (numFlushesRemaining.decrementAndGet() == 0 && !ready())
throw new IOException("Test exception during write");
flushFailureAction.run();
else if (numDelayedFlushesRemaining.getAndDecrement() != 0)
return false;
resetDelayedFlush();
Expand Down