Skip to content

Commit 13d4aef

Browse files
authored
Resolve TLS channel address before opening socket
This change fixes the TLS channel stream connection-establishment path so address resolution happens before opening a SocketChannel. Previously, TlsChannelStream opened and configured a SocketChannel before calling getSocketAddresses. If the configured resolver failed, the exception was reported to the async handler, but the already-opened channel was not closed. Resolving the address before opening the channel avoids the resolver-failure leak and aligns TlsChannelStream with the existing async socket-channel and Netty stream implementations. The setup path now also closes the channel if any pre-registration step fails after the channel has been opened. JAVA-5855
1 parent 028fec3 commit 13d4aef

2 files changed

Lines changed: 108 additions & 12 deletions

File tree

driver-core/src/main/com/mongodb/internal/connection/TlsChannelStreamFactoryFactory.java

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import javax.net.ssl.SSLParameters;
3838
import java.io.Closeable;
3939
import java.io.IOException;
40+
import java.net.InetSocketAddress;
4041
import java.net.StandardSocketOptions;
4142
import java.nio.ByteBuffer;
4243
import java.nio.channels.CompletionHandler;
@@ -209,35 +210,60 @@ private static class TlsChannelStream extends AsynchronousChannelStream {
209210
@Override
210211
public void openAsync(final OperationContext operationContext, final AsyncCompletionHandler<Void> handler) {
211212
isTrue("unopened", getChannel() == null);
213+
SocketChannel socketChannel = null;
214+
SelectorMonitor.SocketRegistration socketRegistration = null;
215+
boolean registered = false;
212216
try {
213-
SocketChannel socketChannel = SocketChannel.open();
214-
socketChannel.configureBlocking(false);
217+
//getConnectTimeoutMs MUST be called before connection attempt, as it might throw MongoOperationTimeout exception.
218+
int connectTimeoutMs = operationContext.getTimeoutContext().getConnectTimeoutMs();
219+
InetSocketAddress socketAddress = getSocketAddresses(getServerAddress(), inetAddressResolver).get(0);
220+
SocketChannel openedSocketChannel = SocketChannel.open();
221+
socketChannel = openedSocketChannel;
222+
openedSocketChannel.configureBlocking(false);
215223

216-
socketChannel.setOption(StandardSocketOptions.TCP_NODELAY, true);
217-
socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);
224+
openedSocketChannel.setOption(StandardSocketOptions.TCP_NODELAY, true);
225+
openedSocketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);
218226
if (getSettings().getReceiveBufferSize() > 0) {
219-
socketChannel.setOption(StandardSocketOptions.SO_RCVBUF, getSettings().getReceiveBufferSize());
227+
openedSocketChannel.setOption(StandardSocketOptions.SO_RCVBUF, getSettings().getReceiveBufferSize());
220228
}
221229
if (getSettings().getSendBufferSize() > 0) {
222-
socketChannel.setOption(StandardSocketOptions.SO_SNDBUF, getSettings().getSendBufferSize());
230+
openedSocketChannel.setOption(StandardSocketOptions.SO_SNDBUF, getSettings().getSendBufferSize());
223231
}
224-
//getConnectTimeoutMs MUST be called before connection attempt, as it might throw MongoOperationTimeout exception.
225-
int connectTimeoutMs = operationContext.getTimeoutContext().getConnectTimeoutMs();
226-
socketChannel.connect(getSocketAddresses(getServerAddress(), inetAddressResolver).get(0));
227-
SelectorMonitor.SocketRegistration socketRegistration = new SelectorMonitor.SocketRegistration(
228-
socketChannel, () -> initializeTslChannel(handler, socketChannel));
232+
openedSocketChannel.connect(socketAddress);
233+
socketRegistration = new SelectorMonitor.SocketRegistration(
234+
openedSocketChannel, () -> initializeTslChannel(handler, openedSocketChannel));
229235

230236
if (connectTimeoutMs > 0) {
231237
scheduleTimeoutInterruption(handler, socketRegistration, connectTimeoutMs);
232238
}
233239
selectorMonitor.register(socketRegistration);
240+
registered = true;
234241
} catch (IOException e) {
242+
closeUnregisteredSocketChannel(socketChannel, socketRegistration, registered, e);
235243
handler.failed(new MongoSocketOpenException("Exception opening socket", getServerAddress(), e));
236244
} catch (Throwable t) {
245+
closeUnregisteredSocketChannel(socketChannel, socketRegistration, registered, t);
237246
handler.failed(t);
238247
}
239248
}
240249

250+
private void closeUnregisteredSocketChannel(@Nullable final SocketChannel socketChannel,
251+
@Nullable final SelectorMonitor.SocketRegistration socketRegistration,
252+
final boolean registered, final Throwable failure) {
253+
if (!registered) {
254+
if (socketRegistration != null) {
255+
socketRegistration.tryCancelPendingConnection();
256+
}
257+
if (socketChannel != null) {
258+
try {
259+
socketChannel.close();
260+
} catch (IOException e) {
261+
failure.addSuppressed(e);
262+
}
263+
}
264+
}
265+
}
266+
241267
private void scheduleTimeoutInterruption(final AsyncCompletionHandler<Void> handler,
242268
final SelectorMonitor.SocketRegistration socketRegistration,
243269
final int connectTimeoutMs) {
@@ -384,4 +410,3 @@ public void close() throws IOException {
384410
}
385411
}
386412
}
387-

driver-core/src/test/functional/com/mongodb/internal/connection/TlsChannelStreamFunctionalTest.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,22 @@
1717
package com.mongodb.internal.connection;
1818

1919
import com.mongodb.ClusterFixture;
20+
import com.mongodb.MongoSocketException;
2021
import com.mongodb.MongoSocketOpenException;
2122
import com.mongodb.ServerAddress;
23+
import com.mongodb.connection.AsyncCompletionHandler;
2224
import com.mongodb.connection.SocketSettings;
2325
import com.mongodb.connection.SslSettings;
2426
import com.mongodb.internal.TimeoutContext;
2527
import com.mongodb.internal.TimeoutSettings;
28+
import com.mongodb.spi.dns.InetAddressResolver;
2629
import org.bson.ByteBuf;
2730
import org.bson.ByteBufNIO;
2831
import org.junit.jupiter.api.DisplayName;
2932
import org.junit.jupiter.api.Test;
3033
import org.junit.jupiter.params.ParameterizedTest;
3134
import org.junit.jupiter.params.provider.ValueSource;
35+
import org.mockito.ArgumentCaptor;
3236
import org.mockito.MockedStatic;
3337
import org.mockito.Mockito;
3438
import org.mockito.invocation.InvocationOnMock;
@@ -37,11 +41,13 @@
3741
import javax.net.ssl.SSLContext;
3842
import javax.net.ssl.SSLEngine;
3943
import java.io.IOException;
44+
import java.net.InetAddress;
4045
import java.net.ServerSocket;
4146
import java.nio.ByteBuffer;
4247
import java.nio.channels.InterruptedByTimeoutException;
4348
import java.nio.channels.SocketChannel;
4449
import java.util.Collections;
50+
import java.util.List;
4551
import java.util.concurrent.TimeUnit;
4652

4753
import static com.mongodb.ClusterFixture.getPrimaryServerDescription;
@@ -52,10 +58,12 @@
5258
import static org.junit.jupiter.api.Assertions.assertFalse;
5359
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
5460
import static org.junit.jupiter.api.Assertions.assertNotNull;
61+
import static org.junit.jupiter.api.Assertions.assertSame;
5562
import static org.junit.jupiter.api.Assertions.assertThrows;
5663
import static org.junit.jupiter.api.Assertions.assertTrue;
5764
import static org.junit.jupiter.api.Assertions.fail;
5865
import static org.junit.jupiter.api.Assumptions.assumeTrue;
66+
import static org.mockito.ArgumentMatchers.any;
5967
import static org.mockito.ArgumentMatchers.anyInt;
6068
import static org.mockito.ArgumentMatchers.anyString;
6169
import static org.mockito.Mockito.atLeast;
@@ -68,6 +76,69 @@ class TlsChannelStreamFunctionalTest {
6876
private static final String UNREACHABLE_PRIVATE_IP_ADDRESS = "10.255.255.1";
6977
private static final int UNREACHABLE_PORT = 65333;
7078

79+
@Test
80+
void shouldFailAsyncCompletionHandlerWithoutOpeningSocketChannelIfNameResolutionFails() {
81+
//given
82+
ServerAddress serverAddress = new ServerAddress();
83+
MongoSocketException exception = new MongoSocketException("Temporary failure in name resolution", serverAddress);
84+
InetAddressResolver inetAddressResolver = new InetAddressResolver() {
85+
@Override
86+
public List<InetAddress> lookupByName(final String host) {
87+
throw exception;
88+
}
89+
};
90+
91+
try (StreamFactoryFactory streamFactoryFactory = new TlsChannelStreamFactoryFactory(inetAddressResolver);
92+
MockedStatic<SocketChannel> socketChannelMockedStatic = Mockito.mockStatic(SocketChannel.class)) {
93+
StreamFactory streamFactory = streamFactoryFactory.create(SocketSettings.builder()
94+
.connectTimeout(100, TimeUnit.MILLISECONDS)
95+
.build(), SSL_SETTINGS);
96+
Stream stream = streamFactory.create(serverAddress);
97+
@SuppressWarnings("unchecked")
98+
AsyncCompletionHandler<Void> handler = Mockito.mock(AsyncCompletionHandler.class);
99+
100+
//when
101+
stream.openAsync(createOperationContext(100), handler);
102+
103+
//then
104+
verify(handler).failed(exception);
105+
verify(handler, times(0)).completed(null);
106+
socketChannelMockedStatic.verify(SocketChannel::open, times(0));
107+
}
108+
}
109+
110+
@Test
111+
void shouldCloseSocketChannelIfConnectFailsBeforeRegistration() throws IOException {
112+
//given
113+
ServerAddress serverAddress = new ServerAddress();
114+
IOException exception = new IOException("connect failed");
115+
InetAddressResolver inetAddressResolver = host -> Collections.singletonList(InetAddress.getLoopbackAddress());
116+
117+
try (SocketChannel socketChannel = Mockito.spy(SocketChannel.open());
118+
StreamFactoryFactory streamFactoryFactory = new TlsChannelStreamFactoryFactory(inetAddressResolver);
119+
MockedStatic<SocketChannel> socketChannelMockedStatic = Mockito.mockStatic(SocketChannel.class)) {
120+
socketChannelMockedStatic.when(SocketChannel::open).thenReturn(socketChannel);
121+
Mockito.doThrow(exception).when(socketChannel).connect(any());
122+
StreamFactory streamFactory = streamFactoryFactory.create(SocketSettings.builder()
123+
.connectTimeout(100, TimeUnit.MILLISECONDS)
124+
.build(), SSL_SETTINGS);
125+
Stream stream = streamFactory.create(serverAddress);
126+
@SuppressWarnings("unchecked")
127+
AsyncCompletionHandler<Void> handler = Mockito.mock(AsyncCompletionHandler.class);
128+
ArgumentCaptor<Throwable> failureCaptor = ArgumentCaptor.forClass(Throwable.class);
129+
130+
//when
131+
stream.openAsync(createOperationContext(100), handler);
132+
133+
//then
134+
verify(handler).failed(failureCaptor.capture());
135+
MongoSocketOpenException actualException = assertInstanceOf(MongoSocketOpenException.class, failureCaptor.getValue());
136+
assertSame(exception, actualException.getCause());
137+
verify(handler, times(0)).completed(null);
138+
verify(socketChannel).close();
139+
}
140+
}
141+
71142
@ParameterizedTest
72143
@ValueSource(ints = {500, 1000, 2000})
73144
void shouldInterruptConnectionEstablishmentWhenConnectionTimeoutExpires(final int connectTimeoutMs) throws IOException {

0 commit comments

Comments
 (0)