Skip to content

Commit 0eb04a0

Browse files
committed
JAVA-6194 Add MongoSocksProxyException for CMAP backpressure labeling
1 parent 394f7b1 commit 0eb04a0

5 files changed

Lines changed: 379 additions & 10 deletions

File tree

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/*
2+
* Copyright 2008-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.mongodb;
18+
19+
import com.mongodb.lang.Nullable;
20+
21+
/**
22+
* Thrown when an error occurs while establishing a connection to a SOCKS5 proxy.
23+
*
24+
* <p>Per the CMAP specification, errors of this type are excluded from backpressure
25+
* error labels ({@link MongoException#SYSTEM_OVERLOADED_ERROR_LABEL},
26+
* {@link MongoException#RETRYABLE_ERROR_LABEL}).
27+
*
28+
* <p>The {@link #getHandshakePhase()} identifies which phase of the SOCKS5 handshake failed.
29+
* For {@link HandshakePhase#CONNECT_RELAY} failures, {@link #getProxyReplyCode()} returns
30+
* the RFC 1928 reply code sent by the proxy; for all other phases it returns {@code null}.
31+
*
32+
* <p>RFC 1928 reply codes: 1=general failure, 2=connection not allowed by ruleset,
33+
* 3=network unreachable, 4=host unreachable, 5=connection refused, 6=TTL expired,
34+
* 7=command not supported, 8=address type not supported.
35+
*
36+
* @since 5.8
37+
*/
38+
public class MongoSocksProxyException extends MongoSocketOpenException {
39+
private static final long serialVersionUID = 1L;
40+
41+
/**
42+
* The phase of the SOCKS5 handshake at which the failure occurred.
43+
*
44+
* @since 5.8
45+
*/
46+
public enum HandshakePhase {
47+
/**
48+
* TCP connection to the proxy host itself failed before any SOCKS5 exchange.
49+
* The proxy may be temporarily unreachable.
50+
*/
51+
PROXY_TCP_CONNECT,
52+
53+
/**
54+
* SOCKS5 method-selection exchange failed: the proxy version is incompatible,
55+
* no common authentication method was found, or the proxy returned an
56+
* unrecognised method. This is always a configuration error.
57+
*/
58+
NEGOTIATION,
59+
60+
/**
61+
* Credential verification with the proxy failed. This is always a
62+
* configuration error (wrong username or password).
63+
*/
64+
AUTHENTICATION,
65+
66+
/**
67+
* The proxy processed the CONNECT command for the target host and returned
68+
* a non-success reply code. See {@link MongoSocksProxyException#getProxyReplyCode()}
69+
* for the specific RFC 1928 reply code.
70+
*/
71+
CONNECT_RELAY
72+
}
73+
74+
private final HandshakePhase handshakePhase;
75+
76+
@Nullable
77+
private final Integer proxyReplyCode;
78+
79+
/**
80+
* Construct an instance for failures that have no RFC 1928 reply code and no cause
81+
* ({@link HandshakePhase#PROXY_TCP_CONNECT}, {@link HandshakePhase#NEGOTIATION},
82+
* {@link HandshakePhase#AUTHENTICATION}).
83+
*
84+
* @param message the message
85+
* @param serverAddress the server address
86+
* @param handshakePhase the phase at which the failure occurred
87+
*/
88+
public MongoSocksProxyException(final String message, final ServerAddress serverAddress, final HandshakePhase handshakePhase) {
89+
super(message, serverAddress);
90+
this.handshakePhase = handshakePhase;
91+
this.proxyReplyCode = null;
92+
}
93+
94+
95+
/**
96+
* Construct an instance for failures that have no RFC 1928 reply code
97+
* ({@link HandshakePhase#PROXY_TCP_CONNECT}, {@link HandshakePhase#NEGOTIATION},
98+
* {@link HandshakePhase#AUTHENTICATION}).
99+
*
100+
* @param message the message
101+
* @param address the server address
102+
* @param cause the cause
103+
* @param handshakePhase the phase at which the failure occurred
104+
*/
105+
public MongoSocksProxyException(final String message, final ServerAddress address,
106+
final Throwable cause, final HandshakePhase handshakePhase) {
107+
this(message, address, cause, handshakePhase, null);
108+
}
109+
110+
/**
111+
* Construct an instance for {@link HandshakePhase#CONNECT_RELAY} failures that
112+
* carry an RFC 1928 reply code.
113+
*
114+
* @param message the message
115+
* @param address the server address
116+
* @param handshakePhase the phase at which the failure occurred
117+
* @param proxyReplyCode the RFC 1928 reply code, or {@code null}
118+
*/
119+
public MongoSocksProxyException(final String message, final ServerAddress address, final HandshakePhase handshakePhase,
120+
@Nullable final Integer proxyReplyCode) {
121+
super(message, address);
122+
this.handshakePhase = handshakePhase;
123+
this.proxyReplyCode = proxyReplyCode;
124+
}
125+
126+
127+
/**
128+
* Construct an instance for {@link HandshakePhase#CONNECT_RELAY} failures that
129+
* carry an RFC 1928 reply code.
130+
*
131+
* @param message the message
132+
* @param address the server address
133+
* @param cause the cause
134+
* @param handshakePhase the phase at which the failure occurred
135+
* @param proxyReplyCode the RFC 1928 reply code, or {@code null}
136+
*/
137+
public MongoSocksProxyException(final String message, final ServerAddress address,
138+
final Throwable cause, final HandshakePhase handshakePhase,
139+
@Nullable final Integer proxyReplyCode) {
140+
super(message, address, cause);
141+
this.handshakePhase = handshakePhase;
142+
this.proxyReplyCode = proxyReplyCode;
143+
}
144+
145+
/**
146+
* Returns the phase of the SOCKS5 handshake at which the failure occurred.
147+
*
148+
* @return the handshake phase, never {@code null}
149+
*/
150+
public HandshakePhase getHandshakePhase() {
151+
return handshakePhase;
152+
}
153+
154+
/**
155+
* Returns the RFC 1928 reply code sent by the SOCKS5 proxy in response to a CONNECT request,
156+
* or {@code null} if the failure occurred before the proxy sent a CONNECT response
157+
* (i.e. phase is not {@link HandshakePhase#CONNECT_RELAY}).
158+
*
159+
* @return the RFC 1928 proxy reply code, or {@code null}
160+
*/
161+
@Nullable
162+
public Integer getProxyReplyCode() {
163+
return proxyReplyCode;
164+
}
165+
}

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.mongodb.MongoSocketException;
2020
import com.mongodb.MongoSocketOpenException;
2121
import com.mongodb.MongoSocketReadException;
22+
import com.mongodb.MongoSocksProxyException;
2223
import com.mongodb.ServerAddress;
2324
import com.mongodb.connection.AsyncCompletionHandler;
2425
import com.mongodb.connection.ProxySettings;
@@ -79,8 +80,17 @@ public void open(final OperationContext operationContext) {
7980
socket = initializeSocket(operationContext);
8081
outputStream = socket.getOutputStream();
8182
inputStream = socket.getInputStream();
83+
} catch (MongoSocksProxyException e) {
84+
close();
85+
throw e;
8286
} catch (IOException e) {
8387
close();
88+
if (settings.getProxySettings().isProxyEnabled()) {
89+
throw translateInterruptedException(e, "Interrupted while connecting")
90+
.orElseThrow(() -> new MongoSocksProxyException(
91+
"Exception connecting to SOCKS5 proxy", getAddress(), e,
92+
MongoSocksProxyException.HandshakePhase.PROXY_TCP_CONNECT));
93+
}
8494
throw translateInterruptedException(e, "Interrupted while connecting")
8595
.orElseThrow(() -> new MongoSocketOpenException("Exception opening socket", getAddress(), e));
8696
}
@@ -122,7 +132,6 @@ private SSLSocket initializeSslSocketOverSocksProxy(final OperationContext opera
122132
configureSocket(socksProxy, operationContext, settings);
123133
InetSocketAddress inetSocketAddress = toSocketAddress(serverHost, serverPort);
124134
socksProxy.connect(inetSocketAddress, operationContext.getTimeoutContext().getConnectTimeoutMs());
125-
126135
SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socksProxy, serverHost, serverPort, true);
127136
//Even though Socks proxy connection is already established, TLS handshake has not been performed yet.
128137
//So it is possible to set SSL parameters before handshake is done.

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

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
*/
1616
package com.mongodb.internal.connection;
1717

18+
import com.mongodb.MongoSocksProxyException;
19+
import com.mongodb.MongoSocksProxyException.HandshakePhase;
20+
import com.mongodb.ServerAddress;
1821
import com.mongodb.connection.ProxySettings;
1922
import com.mongodb.internal.time.Timeout;
2023
import com.mongodb.lang.Nullable;
@@ -223,7 +226,7 @@ private void checkServerReply(final Timeout timeout) throws IOException {
223226
}
224227
return;
225228
}
226-
throw new ConnectException(reply.getMessage());
229+
throw new MongoSocksProxyException(reply.message, targetServerAddress(), HandshakePhase.CONNECT_RELAY, reply.replyNumber);
227230
}
228231

229232
private void authenticate(final SocksAuthenticationMethod authenticationMethod, final Timeout timeout) throws IOException {
@@ -249,7 +252,9 @@ private void authenticate(final SocksAuthenticationMethod authenticationMethod,
249252
byte authStatus = authResult[1];
250253

251254
if (authStatus != AUTHENTICATION_SUCCEEDED_STATUS) {
252-
throw new ConnectException("Authentication failed. Proxy server returned status: " + authStatus);
255+
throw new MongoSocksProxyException(
256+
"Authentication failed. Proxy server returned status: " + authStatus,
257+
targetServerAddress(), HandshakePhase.AUTHENTICATION);
253258
}
254259
}
255260
}
@@ -273,21 +278,29 @@ private SocksAuthenticationMethod performNegotiation(final Timeout timeout) thro
273278
byte[] handshakeReply = readSocksReply(2, timeout);
274279

275280
if (handshakeReply[0] != SOCKS_VERSION) {
276-
throw new ConnectException("Remote server doesn't support socks version 5"
277-
+ " Received version: " + handshakeReply[0]);
281+
throw new MongoSocksProxyException("Remote server doesn't support socks version 5"
282+
+ " Received version: " + handshakeReply[0],
283+
targetServerAddress(), HandshakePhase.NEGOTIATION);
278284
}
279285
byte authMethodNumber = handshakeReply[1];
280286
if (authMethodNumber == (byte) 0xFF) {
281-
throw new ConnectException("None of the authentication methods listed are acceptable. Attempted methods: "
282-
+ Arrays.toString(authenticationMethods));
287+
throw new MongoSocksProxyException(
288+
"None of the authentication methods listed are acceptable. Attempted methods: "
289+
+ Arrays.toString(authenticationMethods),
290+
targetServerAddress(), HandshakePhase.NEGOTIATION);
283291
}
284292
if (authMethodNumber == SocksAuthenticationMethod.NO_AUTH.getMethodNumber()) {
285293
return SocksAuthenticationMethod.NO_AUTH;
286294
} else if (authMethodNumber == SocksAuthenticationMethod.USERNAME_PASSWORD.getMethodNumber()) {
287295
return SocksAuthenticationMethod.USERNAME_PASSWORD;
288296
}
289297

290-
throw new ConnectException("Proxy returned unsupported authentication method: " + authMethodNumber);
298+
throw new MongoSocksProxyException("Proxy returned unsupported authentication method: " + authMethodNumber,
299+
targetServerAddress(), HandshakePhase.NEGOTIATION);
300+
}
301+
302+
private ServerAddress targetServerAddress() {
303+
return new ServerAddress(remoteAddress.getHostName(), remoteAddress.getPort());
291304
}
292305

293306
private SocksAuthenticationMethod[] getSocksAuthenticationMethods() {

0 commit comments

Comments
 (0)