Skip to content

Commit d3008cc

Browse files
authored
Introduce SOCKS5 proxy support (#1180)
This commit introduces SOCKS5 proxy support for the synchronous version of MongoClient with SocketStreamFactory. JAVA-4347
1 parent a00c75a commit d3008cc

File tree

25 files changed

+2808
-35
lines changed

25 files changed

+2808
-35
lines changed

.evergreen/.evg.yml

+37
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,18 @@ functions:
728728
MONGODB_URIS="${atlas_free_tier_uri}|${atlas_replica_set_uri}|${atlas_sharded_uri}|${atlas_tls_v11_uri}|${atlas_tls_v12_uri}|${atlas_free_tier_uri_srv}|${atlas_replica_set_uri_srv}|${atlas_sharded_uri_srv}|${atlas_tls_v11_uri_srv}|${atlas_tls_v12_uri_srv}|${atlas_serverless_uri}|${atlas_serverless_uri_srv}" \
729729
.evergreen/run-connectivity-tests.sh
730730
731+
run socks5 tests:
732+
- command: shell.exec
733+
type: test
734+
params:
735+
working_dir: src
736+
script: |
737+
${PREPARE_SHELL}
738+
SOCKS_AUTH="${SOCKS_AUTH}" \
739+
SSL="${SSL}" MONGODB_URI="${MONGODB_URI}" \
740+
JAVA_VERSION="${JAVA_VERSION}" \
741+
.evergreen/run-socks5-tests.sh
742+
731743
start-kms-mock-server:
732744
- command: shell.exec
733745
params:
@@ -1615,6 +1627,14 @@ tasks:
16151627
export AZUREKMS_VMNAME=${AZUREKMS_VMNAME}
16161628
export AZUREKMS_PRIVATEKEYPATH=/tmp/testazurekms_privatekey
16171629
AZUREKMS_CMD="MONGODB_URI=mongodb://localhost:27017 PROVIDER=azure AZUREKMS_KEY_VAULT_ENDPOINT=${testazurekms_keyvaultendpoint} AZUREKMS_KEY_NAME=${testazurekms_keyname} ./.evergreen/run-fle-on-demand-credential-test.sh" $DRIVERS_TOOLS/.evergreen/csfle/azurekms/run-command.sh
1630+
- name: test-socks5
1631+
tags: []
1632+
commands:
1633+
- func: bootstrap mongo-orchestration
1634+
vars:
1635+
VERSION: latest
1636+
TOPOLOGY: replica_set
1637+
- func: run socks5 tests
16181638
axes:
16191639
- id: version
16201640
display_name: MongoDB Version
@@ -1705,6 +1725,17 @@ axes:
17051725
display_name: NoAuth
17061726
variables:
17071727
AUTH: "noauth"
1728+
- id: socks_auth
1729+
display_name: Socks Proxy Authentication
1730+
values:
1731+
- id: "auth"
1732+
display_name: Auth
1733+
variables:
1734+
SOCKS_AUTH: "auth"
1735+
- id: "noauth"
1736+
display_name: NoAuth
1737+
variables:
1738+
SOCKS_AUTH: "noauth"
17081739
- id: ssl
17091740
display_name: SSL
17101741
values:
@@ -2179,6 +2210,12 @@ buildvariants:
21792210
tasks:
21802211
- name: "csfle-tests-with-mongocryptd"
21812212

2213+
- matrix_name: "socks5-tests"
2214+
matrix_spec: { os: "linux", ssl: ["nossl", "ssl"], version: [ "latest" ], topology: ["replicaset"], socks_auth: ["auth", "noauth"] }
2215+
display_name: "SOCKS5 proxy ${socks_auth} : ${version} ${topology} ${ssl} ${jdk} ${os}"
2216+
tasks:
2217+
- name: test-socks5
2218+
21822219
- name: testgcpkms-variant
21832220
display_name: "GCP KMS"
21842221
run_on:

.evergreen/run-socks5-tests.sh

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#!/bin/bash
2+
3+
set -o xtrace # Write all commands first to stderr
4+
set -o errexit # Exit the script with error if any of the commands fail
5+
6+
SSL=${SSL:-nossl}
7+
SOCKS_AUTH=${SOCKS_AUTH:-noauth}
8+
MONGODB_URI=${MONGODB_URI:-}
9+
SOCKS5_SERVER_SCRIPT="$DRIVERS_TOOLS/.evergreen/socks5srv.py"
10+
PYTHON_BINARY=${PYTHON_BINARY:-python3}
11+
# Grab a connection string that only refers to *one* of the hosts in MONGODB_URI
12+
FIRST_HOST=$(echo "$MONGODB_URI" | awk -F[/:,] '{print $4":"$5}')
13+
# Use 127.0.0.1:12345 as the URL for the single host that we connect to,
14+
# we configure the Socks5 proxy server script to redirect from this to FIRST_HOST
15+
export MONGODB_URI_SINGLEHOST="mongodb://127.0.0.1:12345"
16+
17+
if [ "${SSL}" = "ssl" ]; then
18+
MONGODB_URI="${MONGODB_URI}&ssl=true&sslInvalidHostNameAllowed=true"
19+
MONGODB_URI_SINGLEHOST="${MONGODB_URI_SINGLEHOST}/?ssl=true&sslInvalidHostNameAllowed=true"
20+
fi
21+
22+
# Compute path to socks5 fake server script in a way that works on Windows
23+
if [ "Windows_NT" == "$OS" ]; then
24+
SOCKS5_SERVER_SCRIPT=$(cygpath -m $DRIVERS_TOOLS)
25+
fi
26+
27+
RELATIVE_DIR_PATH="$(dirname "${BASH_SOURCE:-$0}")"
28+
. "${RELATIVE_DIR_PATH}/javaConfig.bash"
29+
30+
############################################
31+
# Functions #
32+
############################################
33+
34+
provision_ssl () {
35+
# We generate the keystore and truststore on every run with the certs in the drivers-tools repo
36+
if [ ! -f client.pkc ]; then
37+
openssl pkcs12 -CAfile ${DRIVERS_TOOLS}/.evergreen/x509gen/ca.pem -export -in ${DRIVERS_TOOLS}/.evergreen/x509gen/client.pem -out client.pkc -password pass:bithere
38+
fi
39+
40+
cp ${JAVA_HOME}/lib/security/cacerts mongo-truststore
41+
${JAVA_HOME}/bin/keytool -importcert -trustcacerts -file ${DRIVERS_TOOLS}/.evergreen/x509gen/ca.pem -keystore mongo-truststore -storepass changeit -storetype JKS -noprompt
42+
43+
# We add extra gradle arguments for SSL
44+
export GRADLE_SSL_VARS="-Pssl.enabled=true -Pssl.keyStoreType=pkcs12 -Pssl.keyStore=`pwd`/client.pkc -Pssl.keyStorePassword=bithere -Pssl.trustStoreType=jks -Pssl.trustStore=`pwd`/mongo-truststore -Pssl.trustStorePassword=changeit"
45+
}
46+
47+
48+
run_socks5_proxy () {
49+
if [ "$SOCKS_AUTH" == "auth" ]; then
50+
"$PYTHON_BINARY" "$SOCKS5_SERVER_SCRIPT" --port 1080 --auth username:p4ssw0rd --map "127.0.0.1:12345 to $FIRST_HOST" &
51+
SOCKS5_SERVER_PID_1=$!
52+
trap "kill $SOCKS5_SERVER_PID_1" EXIT
53+
else
54+
"$PYTHON_BINARY" "$SOCKS5_SERVER_SCRIPT" --port 1080 --map "127.0.0.1:12345 to $FIRST_HOST" &
55+
SOCKS5_SERVER_PID_1=$!
56+
trap "kill $SOCKS5_SERVER_PID_1" EXIT
57+
fi
58+
}
59+
60+
run_socks5_prose_tests () {
61+
if [ "$SOCKS_AUTH" == "auth" ]; then
62+
local AUTH_ENABLED="true"
63+
else
64+
local AUTH_ENABLED="false"
65+
fi
66+
67+
echo "Running Socks5 tests with Java ${JAVA_VERSION} over $SSL for $TOPOLOGY and connecting to $MONGODB_URI with socks auth enabled: $AUTH_ENABLED"
68+
./gradlew -PjavaVersion=${JAVA_VERSION} -Dorg.mongodb.test.uri=${MONGODB_URI} \
69+
-Dorg.mongodb.test.uri.singleHost=${MONGODB_URI_SINGLEHOST} \
70+
-Dorg.mongodb.test.uri.proxyHost="127.0.0.1" \
71+
-Dorg.mongodb.test.uri.proxyPort="1080" \
72+
-Dorg.mongodb.test.uri.socks.auth.enabled=${AUTH_ENABLED} \
73+
${GRADLE_SSL_VARS} \
74+
--stacktrace --info --continue \
75+
driver-sync:test \
76+
--tests "com.mongodb.client.Socks5ProseTest*"
77+
}
78+
79+
############################################
80+
# Main Program #
81+
############################################
82+
83+
# Set up keystore/truststore
84+
provision_ssl
85+
./gradlew -version
86+
run_socks5_proxy
87+
run_socks5_prose_tests

THIRD-PARTY-NOTICES

+21
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,24 @@ https://github.com/mongodb/mongo-java-driver.
154154
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
155155
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
156156
SOFTWARE.
157+
158+
8) The following files (originally from https://github.com/google/guava):
159+
160+
InetAddressUtils.java (formerly InetAddresses.java)
161+
InetAddressUtilsTest.java (formerly InetAddressesTest.java)
162+
163+
Copyright 2008-present MongoDB, Inc.
164+
Copyright (C) 2008 The Guava Authors
165+
166+
Licensed under the Apache License, Version 2.0 (the "License");
167+
you may not use this file except in compliance with the License.
168+
You may obtain a copy of the License at
169+
170+
http://www.apache.org/licenses/LICENSE-2.0
171+
172+
Unless required by applicable law or agreed to in writing, software
173+
distributed under the License is distributed on an "AS IS" BASIS,
174+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
175+
See the License for the specific language governing permissions and
176+
limitations under the License.
177+

config/spotbugs/exclude.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@
192192

193193
<!-- Spotbugs assumes that SSLSocket#getSSLParameters never returns null, when that is not the case for all JDKs -->
194194
<Match>
195-
<Class name="com.mongodb.internal.connection.SocketStreamHelper"/>
195+
<Class name="com.mongodb.internal.connection.SslHelper"/>
196196
<Bug pattern="RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE"/>
197197
</Match>
198198

driver-core/src/main/com/mongodb/ConnectionString.java

+117-1
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,15 @@
133133
* <li>{@code maxIdleTimeMS=ms}: Maximum idle time of a pooled connection. A connection that exceeds this limit will be closed</li>
134134
* <li>{@code maxLifeTimeMS=ms}: Maximum life time of a pooled connection. A connection that exceeds this limit will be closed</li>
135135
* </ul>
136+
* <p>Proxy Configuration:</p>
137+
* <ul>
138+
* <li>{@code proxyHost=string}: The SOCKS5 proxy host to establish a connection through.
139+
* It can be provided as a valid IPv4 address, IPv6 address, or a domain name. Required if either proxyPassword, proxyUsername or
140+
* proxyPort are specified</li>
141+
* <li>{@code proxyPort=n}: The port number for the SOCKS5 proxy server. Must be a non-negative integer.</li>
142+
* <li>{@code proxyUsername=string}: Username for authenticating with the proxy server. Required if proxyPassword is specified.</li>
143+
* <li>{@code proxyPassword=string}: Password for authenticating with the proxy server. Required if proxyUsername is specified.</li>
144+
* </ul>
136145
* <p>Connection pool configuration:</p>
137146
* <ul>
138147
* <li>{@code maxPoolSize=n}: The maximum number of connections in the connection pool.</li>
@@ -290,6 +299,10 @@ public class ConnectionString {
290299
private Integer socketTimeout;
291300
private Boolean sslEnabled;
292301
private Boolean sslInvalidHostnameAllowed;
302+
private String proxyHost;
303+
private Integer proxyPort;
304+
private String proxyUsername;
305+
private String proxyPassword;
293306
private String requiredReplicaSetName;
294307
private Integer serverSelectionTimeout;
295308
private Integer localThreshold;
@@ -468,6 +481,8 @@ public ConnectionString(final String connectionString, @Nullable final DnsClient
468481
throw new IllegalArgumentException("srvMaxHosts can not be specified with replica set name");
469482
}
470483

484+
validateProxyParameters();
485+
471486
credential = createCredentials(combinedOptionsMaps, userName, password);
472487
warnOnUnsupportedOptions(combinedOptionsMaps);
473488
}
@@ -502,6 +517,12 @@ public ConnectionString(final String connectionString, @Nullable final DnsClient
502517
GENERAL_OPTIONS_KEYS.add("sslinvalidhostnameallowed");
503518
GENERAL_OPTIONS_KEYS.add("tlsallowinvalidhostnames");
504519

520+
//Socks5 proxy settings
521+
GENERAL_OPTIONS_KEYS.add("proxyhost");
522+
GENERAL_OPTIONS_KEYS.add("proxyport");
523+
GENERAL_OPTIONS_KEYS.add("proxyusername");
524+
GENERAL_OPTIONS_KEYS.add("proxypassword");
525+
505526
GENERAL_OPTIONS_KEYS.add("replicaset");
506527
GENERAL_OPTIONS_KEYS.add("readconcernlevel");
507528

@@ -599,6 +620,18 @@ private void translateOptions(final Map<String, List<String>> optionsMap) {
599620
case "sockettimeoutms":
600621
socketTimeout = parseInteger(value, "sockettimeoutms");
601622
break;
623+
case "proxyhost":
624+
proxyHost = value;
625+
break;
626+
case "proxyport":
627+
proxyPort = parseInteger(value, "proxyPort");
628+
break;
629+
case "proxyusername":
630+
proxyUsername = value;
631+
break;
632+
case "proxypassword":
633+
proxyPassword = value;
634+
break;
602635
case "tlsallowinvalidhostnames":
603636
sslInvalidHostnameAllowed = parseBoolean(value, "tlsAllowInvalidHostnames");
604637
tlsAllowInvalidHostnamesSet = true;
@@ -1158,6 +1191,41 @@ private void validatePort(final String host, final String port) {
11581191
}
11591192
}
11601193

1194+
private void validateProxyParameters() {
1195+
if (proxyHost == null) {
1196+
if (proxyPort != null) {
1197+
throw new IllegalArgumentException("proxyPort can only be specified with proxyHost");
1198+
} else if (proxyUsername != null) {
1199+
throw new IllegalArgumentException("proxyUsername can only be specified with proxyHost");
1200+
} else if (proxyPassword != null) {
1201+
throw new IllegalArgumentException("proxyPassword can only be specified with proxyHost");
1202+
}
1203+
}
1204+
if (proxyPort != null && (proxyPort < 0 || proxyPort > 65535)) {
1205+
throw new IllegalArgumentException("proxyPort should be within the valid range (0 to 65535)");
1206+
}
1207+
if (proxyUsername != null) {
1208+
if (proxyUsername.isEmpty()) {
1209+
throw new IllegalArgumentException("proxyUsername cannot be empty");
1210+
}
1211+
if (proxyUsername.getBytes(StandardCharsets.UTF_8).length >= 255) {
1212+
throw new IllegalArgumentException("username's length in bytes cannot be greater than 255");
1213+
}
1214+
}
1215+
if (proxyPassword != null) {
1216+
if (proxyPassword.isEmpty()) {
1217+
throw new IllegalArgumentException("proxyPassword cannot be empty");
1218+
}
1219+
if (proxyPassword.getBytes(StandardCharsets.UTF_8).length >= 255) {
1220+
throw new IllegalArgumentException("password's length in bytes cannot be greater than 255");
1221+
}
1222+
}
1223+
if (proxyUsername == null ^ proxyPassword == null) {
1224+
throw new IllegalArgumentException(
1225+
"Both proxyUsername and proxyPassword must be set together. They cannot be set individually");
1226+
}
1227+
}
1228+
11611229
private int countOccurrences(final String haystack, final String needle) {
11621230
return haystack.length() - haystack.replace(needle, "").length();
11631231
}
@@ -1473,6 +1541,49 @@ public Boolean getSslEnabled() {
14731541
return sslEnabled;
14741542
}
14751543

1544+
/**
1545+
* Gets the SOCKS5 proxy host specified in the connection string.
1546+
*
1547+
* @return the proxy host value.
1548+
* @since 4.11
1549+
*/
1550+
@Nullable
1551+
public String getProxyHost() {
1552+
return proxyHost;
1553+
}
1554+
1555+
/**
1556+
* Gets the SOCKS5 proxy port specified in the connection string.
1557+
*
1558+
* @return the proxy port value.
1559+
* @since 4.11
1560+
*/
1561+
@Nullable
1562+
public Integer getProxyPort() {
1563+
return proxyPort;
1564+
}
1565+
1566+
/**
1567+
* Gets the SOCKS5 proxy username specified in the connection string.
1568+
*
1569+
* @return the proxy username value.
1570+
* @since 4.11
1571+
*/
1572+
@Nullable
1573+
public String getProxyUsername() {
1574+
return proxyUsername;
1575+
}
1576+
1577+
/**
1578+
* Gets the SOCKS5 proxy password specified in the connection string.
1579+
*
1580+
* @return the proxy password value.
1581+
* @since 4.11
1582+
*/
1583+
@Nullable
1584+
public String getProxyPassword() {
1585+
return proxyPassword;
1586+
}
14761587
/**
14771588
* Gets the SSL invalidHostnameAllowed value specified in the connection string.
14781589
*
@@ -1594,6 +1705,10 @@ public boolean equals(final Object o) {
15941705
&& Objects.equals(maxConnecting, that.maxConnecting)
15951706
&& Objects.equals(connectTimeout, that.connectTimeout)
15961707
&& Objects.equals(socketTimeout, that.socketTimeout)
1708+
&& Objects.equals(proxyHost, that.proxyHost)
1709+
&& Objects.equals(proxyPort, that.proxyPort)
1710+
&& Objects.equals(proxyUsername, that.proxyUsername)
1711+
&& Objects.equals(proxyPassword, that.proxyPassword)
15971712
&& Objects.equals(sslEnabled, that.sslEnabled)
15981713
&& Objects.equals(sslInvalidHostnameAllowed, that.sslInvalidHostnameAllowed)
15991714
&& Objects.equals(requiredReplicaSetName, that.requiredReplicaSetName)
@@ -1613,6 +1728,7 @@ public int hashCode() {
16131728
writeConcern, retryWrites, retryReads, readConcern, minConnectionPoolSize, maxConnectionPoolSize, maxWaitTime,
16141729
maxConnectionIdleTime, maxConnectionLifeTime, maxConnecting, connectTimeout, socketTimeout, sslEnabled,
16151730
sslInvalidHostnameAllowed, requiredReplicaSetName, serverSelectionTimeout, localThreshold, heartbeatFrequency,
1616-
applicationName, compressorList, uuidRepresentation, srvServiceName, srvMaxHosts);
1731+
applicationName, compressorList, uuidRepresentation, srvServiceName, srvMaxHosts, proxyHost, proxyPort,
1732+
proxyUsername, proxyPassword);
16171733
}
16181734
}

driver-core/src/main/com/mongodb/MongoClientSettings.java

+1
Original file line numberDiff line numberDiff line change
@@ -1067,6 +1067,7 @@ private MongoClientSettings(final Builder builder) {
10671067
.connectTimeout(builder.heartbeatConnectTimeoutMS == 0
10681068
? socketSettings.getConnectTimeout(MILLISECONDS) : builder.heartbeatConnectTimeoutMS,
10691069
MILLISECONDS)
1070+
.applyToProxySettings(proxyBuilder -> proxyBuilder.applySettings(socketSettings.getProxySettings()))
10701071
.build();
10711072
heartbeatSocketTimeoutSetExplicitly = builder.heartbeatSocketTimeoutMS != 0;
10721073
heartbeatConnectTimeoutSetExplicitly = builder.heartbeatConnectTimeoutMS != 0;

0 commit comments

Comments
 (0)