diff --git a/specifications/uri-options/tests/proxy-options.json b/specifications/uri-options/tests/proxy-options.json
new file mode 100644
index 00000000000..585546ead7f
--- /dev/null
+++ b/specifications/uri-options/tests/proxy-options.json
@@ -0,0 +1,139 @@
+{
+ "tests": [
+ {
+ "description": "proxyPort without proxyHost",
+ "uri": "mongodb://localhost/?proxyPort=1080",
+ "valid": false,
+ "warning": false,
+ "hosts": null,
+ "auth": null,
+ "options": null
+ },
+ {
+ "description": "proxyUsername without proxyHost",
+ "uri": "mongodb://localhost/?proxyUsername=abc",
+ "valid": false,
+ "warning": false,
+ "hosts": null,
+ "auth": null,
+ "options": null
+ },
+ {
+ "description": "proxyPassword without proxyHost",
+ "uri": "mongodb://localhost/?proxyPassword=def",
+ "valid": false,
+ "warning": false,
+ "hosts": null,
+ "auth": null,
+ "options": null
+ },
+ {
+ "description": "all other proxy options without proxyHost",
+ "uri": "mongodb://localhost/?proxyPort=1080&proxyUsername=abc&proxyPassword=def",
+ "valid": false,
+ "warning": false,
+ "hosts": null,
+ "auth": null,
+ "options": null
+ },
+ {
+ "description": "proxyUsername without proxyPassword",
+ "uri": "mongodb://localhost/?proxyHost=localhost&proxyUsername=abc",
+ "valid": false,
+ "warning": false,
+ "hosts": null,
+ "auth": null,
+ "options": null
+ },
+ {
+ "description": "proxyPassword without proxyUsername",
+ "uri": "mongodb://localhost/?proxyHost=localhost&proxyPassword=def",
+ "valid": false,
+ "warning": false,
+ "hosts": null,
+ "auth": null,
+ "options": null
+ },
+ {
+ "description": "multiple proxyHost parameters",
+ "uri": "mongodb://localhost/?proxyHost=localhost&proxyHost=localhost2",
+ "valid": false,
+ "warning": false,
+ "hosts": null,
+ "auth": null,
+ "options": null
+ },
+ {
+ "description": "multiple proxyPort parameters",
+ "uri": "mongodb://localhost/?proxyHost=localhost&proxyPort=1234&proxyPort=12345",
+ "valid": false,
+ "warning": false,
+ "hosts": null,
+ "auth": null,
+ "options": null
+ },
+ {
+ "description": "multiple proxyUsername parameters",
+ "uri": "mongodb://localhost/?proxyHost=localhost&proxyUsername=abc&proxyUsername=def&proxyPassword=123",
+ "valid": false,
+ "warning": false,
+ "hosts": null,
+ "auth": null,
+ "options": null
+ },
+ {
+ "description": "multiple proxyPassword parameters",
+ "uri": "mongodb://localhost/?proxyHost=localhost&proxyUsername=abc&proxyPassword=123&proxyPassword=456",
+ "valid": false,
+ "warning": false,
+ "hosts": null,
+ "auth": null,
+ "options": null
+ },
+ {
+ "description": "only host present",
+ "uri": "mongodb://localhost/?proxyHost=localhost",
+ "valid": true,
+ "warning": false,
+ "hosts": null,
+ "auth": null,
+ "options": {}
+ },
+ {
+ "description": "host and default port present",
+ "uri": "mongodb://localhost/?proxyHost=localhost&proxyPort=1080",
+ "valid": true,
+ "warning": false,
+ "hosts": null,
+ "auth": null,
+ "options": {}
+ },
+ {
+ "description": "host and non-default port present",
+ "uri": "mongodb://localhost/?proxyHost=localhost&proxyPort=12345",
+ "valid": true,
+ "warning": false,
+ "hosts": null,
+ "auth": null,
+ "options": {}
+ },
+ {
+ "description": "replicaset, host and non-default port present",
+ "uri": "mongodb://rs1,rs2,rs3/?proxyHost=localhost&proxyPort=12345",
+ "valid": true,
+ "warning": false,
+ "hosts": null,
+ "auth": null,
+ "options": {}
+ },
+ {
+ "description": "all options present",
+ "uri": "mongodb://rs1,rs2,rs3/?proxyHost=localhost&proxyPort=12345&proxyUsername=asdf&proxyPassword=qwerty",
+ "valid": true,
+ "warning": false,
+ "hosts": null,
+ "auth": null,
+ "options": {}
+ }
+ ]
+}
diff --git a/specifications/uri-options/tests/proxy-options.yml b/specifications/uri-options/tests/proxy-options.yml
new file mode 100644
index 00000000000..a97863dd599
--- /dev/null
+++ b/specifications/uri-options/tests/proxy-options.yml
@@ -0,0 +1,121 @@
+tests:
+ -
+ description: "proxyPort without proxyHost"
+ uri: "mongodb://localhost/?proxyPort=1080"
+ valid: false
+ warning: false
+ hosts: ~
+ auth: ~
+ options: ~
+ -
+ description: "proxyUsername without proxyHost"
+ uri: "mongodb://localhost/?proxyUsername=abc"
+ valid: false
+ warning: false
+ hosts: ~
+ auth: ~
+ options: ~
+ -
+ description: "proxyPassword without proxyHost"
+ uri: "mongodb://localhost/?proxyPassword=def"
+ valid: false
+ warning: false
+ hosts: ~
+ auth: ~
+ options: ~
+ -
+ description: "all other proxy options without proxyHost"
+ uri: "mongodb://localhost/?proxyPort=1080&proxyUsername=abc&proxyPassword=def"
+ valid: false
+ warning: false
+ hosts: ~
+ auth: ~
+ options: ~
+ -
+ description: "proxyUsername without proxyPassword"
+ uri: "mongodb://localhost/?proxyHost=localhost&proxyUsername=abc"
+ valid: false
+ warning: false
+ hosts: ~
+ auth: ~
+ options: ~
+ -
+ description: "proxyPassword without proxyUsername"
+ uri: "mongodb://localhost/?proxyHost=localhost&proxyPassword=def"
+ valid: false
+ warning: false
+ hosts: ~
+ auth: ~
+ options: ~
+ -
+ description: "multiple proxyHost parameters"
+ uri: "mongodb://localhost/?proxyHost=localhost&proxyHost=localhost2"
+ valid: false
+ warning: false
+ hosts: ~
+ auth: ~
+ options: ~
+ -
+ description: "multiple proxyPort parameters"
+ uri: "mongodb://localhost/?proxyHost=localhost&proxyPort=1234&proxyPort=12345"
+ valid: false
+ warning: false
+ hosts: ~
+ auth: ~
+ options: ~
+ -
+ description: "multiple proxyUsername parameters"
+ uri: "mongodb://localhost/?proxyHost=localhost&proxyUsername=abc&proxyUsername=def&proxyPassword=123"
+ valid: false
+ warning: false
+ hosts: ~
+ auth: ~
+ options: ~
+ -
+ description: "multiple proxyPassword parameters"
+ uri: "mongodb://localhost/?proxyHost=localhost&proxyUsername=abc&proxyPassword=123&proxyPassword=456"
+ valid: false
+ warning: false
+ hosts: ~
+ auth: ~
+ options: ~
+ -
+ description: "only host present"
+ uri: "mongodb://localhost/?proxyHost=localhost"
+ valid: true
+ warning: false
+ hosts: ~
+ auth: ~
+ options: {}
+ -
+ description: "host and default port present"
+ uri: "mongodb://localhost/?proxyHost=localhost&proxyPort=1080"
+ valid: true
+ warning: false
+ hosts: ~
+ auth: ~
+ options: {}
+ -
+ description: "host and non-default port present"
+ uri: "mongodb://localhost/?proxyHost=localhost&proxyPort=12345"
+ valid: true
+ warning: false
+ hosts: ~
+ auth: ~
+ options: {}
+ -
+ description: "replicaset, host and non-default port present"
+ uri: "mongodb://rs1,rs2,rs3/?proxyHost=localhost&proxyPort=12345"
+ valid: true
+ warning: false
+ hosts: ~
+ auth: ~
+ options: {}
+ -
+ description: "all options present"
+ uri: "mongodb://rs1,rs2,rs3/?proxyHost=localhost&proxyPort=12345&proxyUsername=asdf&proxyPassword=qwerty"
+ valid: true
+ warning: false
+ hosts: ~
+ auth: ~
+ options: {}
diff --git a/src/MongoDB.Driver/ClusterKey.cs b/src/MongoDB.Driver/ClusterKey.cs
index d208a7b60e4..dec3b93bad6 100644
--- a/src/MongoDB.Driver/ClusterKey.cs
+++ b/src/MongoDB.Driver/ClusterKey.cs
@@ -17,6 +17,7 @@
using System.Collections.Generic;
using System.Linq;
using MongoDB.Driver.Core.Configuration;
+using MongoDB.Driver.Core.Connections;
using MongoDB.Driver.Core.Servers;
using MongoDB.Shared;
@@ -55,6 +56,7 @@ internal sealed class ClusterKey
private readonly ServerMonitoringMode _serverMonitoringMode;
private readonly TimeSpan _serverSelectionTimeout;
private readonly TimeSpan _socketTimeout;
+ private readonly Socks5ProxySettings _socks5ProxySettings;
private readonly int _srvMaxHosts;
private readonly string _srvServiceName;
private readonly SslSettings _sslSettings;
@@ -93,6 +95,7 @@ public ClusterKey(
ServerMonitoringMode serverMonitoringMode,
TimeSpan serverSelectionTimeout,
TimeSpan socketTimeout,
+ Socks5ProxySettings socks5ProxySettings,
int srvMaxHosts,
string srvServiceName,
SslSettings sslSettings,
@@ -129,6 +132,7 @@ public ClusterKey(
_serverMonitoringMode = serverMonitoringMode;
_serverSelectionTimeout = serverSelectionTimeout;
_socketTimeout = socketTimeout;
+ _socks5ProxySettings = socks5ProxySettings;
_srvMaxHosts = srvMaxHosts;
_srvServiceName = srvServiceName;
_sslSettings = sslSettings;
@@ -169,6 +173,7 @@ public ClusterKey(
public ServerMonitoringMode ServerMonitoringMode { get { return _serverMonitoringMode; } }
public TimeSpan ServerSelectionTimeout { get { return _serverSelectionTimeout; } }
public TimeSpan SocketTimeout { get { return _socketTimeout; } }
+ public Socks5ProxySettings Socks5ProxySettings { get { return _socks5ProxySettings; } }
public int SrvMaxHosts { get { return _srvMaxHosts; } }
public string SrvServiceName { get { return _srvServiceName; } }
public SslSettings SslSettings { get { return _sslSettings; } }
@@ -224,6 +229,7 @@ public override bool Equals(object obj)
_serverMonitoringMode == rhs._serverMonitoringMode &&
_serverSelectionTimeout == rhs._serverSelectionTimeout &&
_socketTimeout == rhs._socketTimeout &&
+ object.Equals(_socks5ProxySettings, rhs._socks5ProxySettings) &&
_srvMaxHosts == rhs._srvMaxHosts &&
_srvServiceName == rhs.SrvServiceName &&
object.Equals(_sslSettings, rhs._sslSettings) &&
diff --git a/src/MongoDB.Driver/ClusterRegistry.cs b/src/MongoDB.Driver/ClusterRegistry.cs
index 3359cd4b612..e6763cf0ba8 100644
--- a/src/MongoDB.Driver/ClusterRegistry.cs
+++ b/src/MongoDB.Driver/ClusterRegistry.cs
@@ -171,7 +171,8 @@ private TcpStreamSettings ConfigureTcp(TcpStreamSettings settings, ClusterKey cl
readTimeout: clusterKey.SocketTimeout,
receiveBufferSize: clusterKey.ReceiveBufferSize,
sendBufferSize: clusterKey.SendBufferSize,
- writeTimeout: clusterKey.SocketTimeout);
+ writeTimeout: clusterKey.SocketTimeout,
+ socks5ProxySettings: clusterKey.Socks5ProxySettings);
}
internal IClusterInternal GetOrCreateCluster(ClusterKey clusterKey)
diff --git a/src/MongoDB.Driver/Core/Configuration/ClusterBuilderExtensions.cs b/src/MongoDB.Driver/Core/Configuration/ClusterBuilderExtensions.cs
index 3fe6c4a3da2..7e8c7a0b994 100644
--- a/src/MongoDB.Driver/Core/Configuration/ClusterBuilderExtensions.cs
+++ b/src/MongoDB.Driver/Core/Configuration/ClusterBuilderExtensions.cs
@@ -23,6 +23,7 @@
using MongoDB.Driver.Authentication;
using MongoDB.Driver.Authentication.Gssapi;
using MongoDB.Driver.Authentication.Oidc;
+using MongoDB.Driver.Core.Connections;
using MongoDB.Driver.Core.Events.Diagnostics;
using MongoDB.Driver.Core.Misc;
@@ -118,6 +119,16 @@ public static ClusterBuilder ConfigureWithConnectionString(
builder = builder.ConfigureTcp(s => s.With(addressFamily: AddressFamily.InterNetworkV6));
}
+ if (connectionString.ProxyHost != null)
+ {
+ builder = builder.ConfigureTcp(s => s.With(
+ socks5ProxySettings: Socks5ProxySettings.Create(
+ connectionString.ProxyHost,
+ connectionString.ProxyPort,
+ connectionString.ProxyUsername,
+ connectionString.ProxyPassword)));
+ }
+
if (connectionString.SocketTimeout != null)
{
builder = builder.ConfigureTcp(s => s.With(
diff --git a/src/MongoDB.Driver/Core/Configuration/ConnectionString.cs b/src/MongoDB.Driver/Core/Configuration/ConnectionString.cs
index 79349c035cc..eadc83aad9b 100644
--- a/src/MongoDB.Driver/Core/Configuration/ConnectionString.cs
+++ b/src/MongoDB.Driver/Core/Configuration/ConnectionString.cs
@@ -92,6 +92,10 @@ public sealed class ConnectionString
private string _replicaSet;
private bool? _retryReads;
private bool? _retryWrites;
+ private string _proxyHost;
+ private int? _proxyPort;
+ private string _proxyUsername;
+ private string _proxyPassword;
private ConnectionStringScheme _scheme;
private ServerMonitoringMode? _serverMonitoringMode;
private TimeSpan? _serverSelectionTimeout;
@@ -356,6 +360,26 @@ public string Password
get { return _password; }
}
+ ///
+ /// Gets the proxy host.
+ ///
+ public string ProxyHost => _proxyHost;
+
+ ///
+ /// Gets the proxy port.
+ ///
+ public int? ProxyPort => _proxyPort;
+
+ ///
+ /// Gets the proxy username.
+ ///
+ public string ProxyUsername => _proxyUsername;
+
+ ///
+ /// Gets the proxy password.
+ ///
+ public string ProxyPassword => _proxyPassword;
+
///
/// Gets the read concern level.
///
@@ -903,6 +927,29 @@ private void Parse()
}
}
+ if (string.IsNullOrEmpty(_proxyHost))
+ {
+ if (_proxyPort is not null)
+ {
+ throw new MongoConfigurationException("proxyPort cannot be specified without proxyHost.");
+ }
+
+ if (!string.IsNullOrEmpty(_proxyUsername))
+ {
+ throw new MongoConfigurationException("proxyUsername cannot be specified without proxyHost.");
+ }
+
+ if (!string.IsNullOrEmpty(_proxyPassword))
+ {
+ throw new MongoConfigurationException("proxyPassword cannot be specified without proxyHost.");
+ }
+ }
+
+ if (string.IsNullOrEmpty(_proxyUsername) != string.IsNullOrEmpty(_proxyPassword))
+ {
+ throw new MongoConfigurationException("proxyUsername and proxyPassword must both be specified or neither.");
+ }
+
string ProtectConnectionString(string connectionString)
{
var protectedString = Regex.Replace(connectionString, @"(?<=://)[^/]*(?=@)", "");
@@ -995,6 +1042,55 @@ private void ParseOption(string name, string value)
case "minpoolsize":
_minPoolSize = ParseInt32(name, value);
break;
+ case "proxyhost":
+ if (!string.IsNullOrEmpty(_proxyHost))
+ {
+ throw new MongoConfigurationException("Multiple proxyHost options are not allowed.");
+ }
+
+ _proxyHost = value;
+ if (_proxyHost.Length == 0)
+ {
+ throw new MongoConfigurationException("proxyHost cannot be empty.");
+ }
+ break;
+ case "proxyport":
+ if (_proxyPort != null)
+ {
+ throw new MongoConfigurationException("Multiple proxyPort options are not allowed.");
+ }
+
+ var proxyPortValue = ParseInt32(name, value);
+ if (proxyPortValue is < 0 or > 65535)
+ {
+ throw new MongoConfigurationException("proxyPort must be between 0 and 65535.");
+ }
+ _proxyPort = proxyPortValue;
+ break;
+ case "proxyusername":
+ if (!string.IsNullOrEmpty(_proxyUsername))
+ {
+ throw new MongoConfigurationException("Multiple proxyUsername options are not allowed.");
+ }
+
+ _proxyUsername = value;
+ if (_proxyUsername.Length == 0)
+ {
+ throw new MongoConfigurationException("proxyUsername cannot be empty.");
+ }
+ break;
+ case "proxypassword":
+ if (!string.IsNullOrEmpty(_proxyPassword))
+ {
+ throw new MongoConfigurationException("Multiple proxyPassword options are not allowed.");
+ }
+
+ _proxyPassword = value;
+ if (_proxyPassword.Length == 0)
+ {
+ throw new MongoConfigurationException("proxyPassword cannot be empty.");
+ }
+ break;
case "readconcernlevel":
_readConcernLevel = ParseEnum(name, value);
break;
diff --git a/src/MongoDB.Driver/Core/Configuration/TcpStreamSettings.cs b/src/MongoDB.Driver/Core/Configuration/TcpStreamSettings.cs
index 5a6550fa592..1ba8f38a3c4 100644
--- a/src/MongoDB.Driver/Core/Configuration/TcpStreamSettings.cs
+++ b/src/MongoDB.Driver/Core/Configuration/TcpStreamSettings.cs
@@ -16,6 +16,7 @@
using System;
using System.Net.Sockets;
using System.Threading;
+using MongoDB.Driver.Core.Connections;
using MongoDB.Driver.Core.Misc;
namespace MongoDB.Driver.Core.Configuration
@@ -32,6 +33,7 @@ public class TcpStreamSettings
private readonly int _receiveBufferSize;
private readonly int _sendBufferSize;
private readonly Action _socketConfigurator;
+ private readonly Socks5ProxySettings _socks5ProxySettings;
private readonly TimeSpan? _writeTimeout;
// constructors
@@ -44,6 +46,7 @@ public class TcpStreamSettings
/// Size of the receive buffer.
/// Size of the send buffer.
/// The socket configurator.
+ /// The SOCKS5 proxy settings.
/// The write timeout.
public TcpStreamSettings(
Optional addressFamily = default(Optional),
@@ -52,7 +55,8 @@ public TcpStreamSettings(
Optional receiveBufferSize = default(Optional),
Optional sendBufferSize = default(Optional),
Optional> socketConfigurator = default(Optional>),
- Optional writeTimeout = default(Optional))
+ Optional writeTimeout = default(Optional),
+ Optional socks5ProxySettings = default(Optional))
{
_addressFamily = addressFamily.WithDefault(AddressFamily.InterNetwork);
_connectTimeout = Ensure.IsInfiniteOrGreaterThanOrEqualToZero(connectTimeout.WithDefault(Timeout.InfiniteTimeSpan), "connectTimeout");
@@ -61,8 +65,31 @@ public TcpStreamSettings(
_sendBufferSize = Ensure.IsGreaterThanZero(sendBufferSize.WithDefault(64 * 1024), "sendBufferSize");
_socketConfigurator = socketConfigurator.WithDefault(null);
_writeTimeout = Ensure.IsNullOrInfiniteOrGreaterThanOrEqualToZero(writeTimeout.WithDefault(null), "writeTimeout");
+ _socks5ProxySettings = socks5ProxySettings.WithDefault(null);
}
+ // ///
+ // ///
+ // ///
+ // ///
+ // ///
+ // ///
+ // ///
+ // ///
+ // ///
+ // ///
+ // public TcpStreamSettings(
+ // Optional addressFamily,
+ // Optional connectTimeout,
+ // Optional readTimeout,
+ // Optional receiveBufferSize,
+ // Optional sendBufferSize,
+ // Optional> socketConfigurator,
+ // Optional writeTimeout)
+ // {
+ //
+ // }
+
internal TcpStreamSettings(TcpStreamSettings other)
{
_addressFamily = other.AddressFamily;
@@ -71,6 +98,7 @@ internal TcpStreamSettings(TcpStreamSettings other)
_receiveBufferSize = other.ReceiveBufferSize;
_sendBufferSize = other.SendBufferSize;
_socketConfigurator = other.SocketConfigurator;
+ _socks5ProxySettings = other._socks5ProxySettings;
_writeTimeout = other.WriteTimeout;
}
@@ -141,6 +169,11 @@ public Action SocketConfigurator
get { return _socketConfigurator; }
}
+ ///
+ /// Gets the SOCKS5 proxy settings.
+ ///
+ public Socks5ProxySettings Socks5ProxySettings => _socks5ProxySettings;
+
///
/// Gets the write timeout.
///
@@ -162,6 +195,7 @@ public TimeSpan? WriteTimeout
/// Size of the receive buffer.
/// Size of the send buffer.
/// The socket configurator.
+ /// The SOCKS5 proxy settings.
/// The write timeout.
/// A new TcpStreamSettings instance.
public TcpStreamSettings With(
@@ -171,7 +205,8 @@ public TcpStreamSettings With(
Optional receiveBufferSize = default(Optional),
Optional sendBufferSize = default(Optional),
Optional> socketConfigurator = default(Optional>),
- Optional writeTimeout = default(Optional))
+ Optional writeTimeout = default(Optional),
+ Optional socks5ProxySettings = default(Optional))
{
return new TcpStreamSettings(
addressFamily: addressFamily.WithDefault(_addressFamily),
@@ -180,6 +215,7 @@ public TcpStreamSettings With(
receiveBufferSize: receiveBufferSize.WithDefault(_receiveBufferSize),
sendBufferSize: sendBufferSize.WithDefault(_sendBufferSize),
socketConfigurator: socketConfigurator.WithDefault(_socketConfigurator),
+ socks5ProxySettings: socks5ProxySettings.WithDefault(_socks5ProxySettings),
writeTimeout: writeTimeout.WithDefault(_writeTimeout));
}
}
diff --git a/src/MongoDB.Driver/Core/Connections/Socks5AuthenticationSettings.cs b/src/MongoDB.Driver/Core/Connections/Socks5AuthenticationSettings.cs
new file mode 100644
index 00000000000..8b7bbc6dbd4
--- /dev/null
+++ b/src/MongoDB.Driver/Core/Connections/Socks5AuthenticationSettings.cs
@@ -0,0 +1,99 @@
+/* Copyright 2010-present MongoDB Inc.
+ *
+ * Licensed 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.
+ */
+
+using MongoDB.Driver.Core.Misc;
+using MongoDB.Shared;
+
+namespace MongoDB.Driver.Core.Connections;
+
+///
+/// Represents the settings for SOCKS5 authentication.
+///
+public abstract class Socks5AuthenticationSettings
+{
+ ///
+ /// Creates authentication settings that do not require any authentication.
+ ///
+ public static Socks5AuthenticationSettings None => new NoAuthenticationSettings();
+
+ ///
+ /// Creates authentication settings for username and password.
+ ///
+ /// The username
+ /// The password
+ ///
+ public static Socks5AuthenticationSettings UsernamePassword(string username, string password)
+ => new UsernamePasswordAuthenticationSettings(username, password);
+
+ ///
+ /// Represents settings for no authentication in SOCKS5.
+ ///
+ public sealed class NoAuthenticationSettings : Socks5AuthenticationSettings
+ {
+ ///
+ public override bool Equals(object obj)
+ {
+ return obj is Socks5AuthenticationSettings;
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ return 1;
+ }
+ }
+
+ ///
+ /// Represents settings for username and password authentication in SOCKS5.
+ ///
+ public sealed class UsernamePasswordAuthenticationSettings : Socks5AuthenticationSettings
+ {
+ ///
+ /// Gets the username for authentication.
+ ///
+ public string Username { get; }
+
+ ///
+ /// Gets the password for authentication.
+ ///
+ public string Password { get; }
+
+ internal UsernamePasswordAuthenticationSettings(string username, string password)
+ {
+ Username = Ensure.IsNotNullOrEmpty(username, nameof(username));
+ Password = Ensure.IsNotNullOrEmpty(password, nameof(password));
+ }
+
+ ///
+ public override bool Equals(object obj)
+ {
+ if (obj is UsernamePasswordAuthenticationSettings other)
+ {
+ return Username == other.Username && Password == other.Password;
+ }
+
+ return false;
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ return new Hasher()
+ .Hash(Username)
+ .Hash(Password)
+ .GetHashCode();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/MongoDB.Driver/Core/Connections/Socks5Helper.cs b/src/MongoDB.Driver/Core/Connections/Socks5Helper.cs
new file mode 100644
index 00000000000..33ca004957e
--- /dev/null
+++ b/src/MongoDB.Driver/Core/Connections/Socks5Helper.cs
@@ -0,0 +1,316 @@
+/* Copyright 2010-present MongoDB Inc.
+ *
+ * Licensed 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.
+ */
+
+using System;
+using System.Buffers;
+using System.Buffers.Binary;
+using System.IO;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using MongoDB.Driver.Core.Misc;
+using MongoDB.Driver.GridFS;
+
+namespace MongoDB.Driver.Core.Connections;
+
+internal static class Socks5Helper
+{
+ // Schemas for requests/responses are taken from the following RFCs:
+ // SOCKS Protocol Version 5 - https://datatracker.ietf.org/doc/html/rfc1928
+ // Username/Password Authentication for SOCKS V5 - https://datatracker.ietf.org/doc/html/rfc1929
+
+ // Greeting request
+ // +----+----------+----------+
+ // |VER | NMETHODS | METHODS |
+ // +----+----------+----------+
+ // | 1 | 1 | 1 to 255 |
+ // +----+----------+----------+
+
+ // Greeting response
+ // +----+--------+
+ // |VER | METHOD |
+ // +----+--------+
+ // | 1 | 1 |
+ // +----+--------+
+
+ // Authentication request -- if using username/password authentication
+ // +----+------+----------+------+----------+
+ // |VER | ULEN | UNAME | PLEN | PASSWD |
+ // +----+------+----------+------+----------+
+ // | 1 | 1 | 1 to 255 | 1 | 1 to 255 |
+ // +----+------+----------+------+----------+
+
+ // Authentication response
+ // +----+--------+
+ // |VER | STATUS |
+ // +----+--------+
+ // | 1 | 1 |
+ // +----+--------+
+
+ // Connect request
+ // +----+-----+-------+------+----------+----------+
+ // |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
+ // +----+-----+-------+------+----------+----------+
+ // | 1 | 1 | X'00' | 1 | Variable | 2 |
+ // +----+-----+-------+------+----------+----------+
+
+ // Connect response
+ // +----+-----+-------+------+----------+----------+
+ // |VER | REP | RSV | ATYP | DST.ADDR | DST.PORT |
+ // +----+-----+-------+------+----------+----------+
+ // | 1 | 1 | X'00' | 1 | Variable | 2 |
+ // +----+-----+-------+------+----------+----------+
+
+ //General use constants
+ private const byte ProtocolVersion5 = 0x05;
+ private const byte Socks5Success = 0x00;
+ private const byte Reserved = 0x00;
+ private const byte CmdConnect = 0x01;
+
+ //Auth constants
+ private const byte MethodNoAuth = 0x00;
+ private const byte MethodUsernamePassword = 0x02;
+ private const byte SubnegotiationVersion = 0x01;
+
+ //Address type constants
+ private const byte AddressTypeIPv4 = 0x01;
+ private const byte AddressTypeIPv6 = 0x04;
+ private const byte AddressTypeDomain = 0x03;
+
+ // Largest possible message size when using username and password auth.
+ private const int BufferSize = 513;
+
+ public static void PerformSocks5Handshake(Stream stream, EndPoint endPoint, Socks5AuthenticationSettings authenticationSettings, CancellationToken cancellationToken)
+ {
+ var (targetHost, targetPort) = endPoint.GetHostAndPort();
+ var buffer = ArrayPool.Shared.Rent(BufferSize);
+ try
+ {
+ var useAuth = authenticationSettings is Socks5AuthenticationSettings.UsernamePasswordAuthenticationSettings;
+
+ var greetingRequestLength = CreateGreetingRequest(buffer, useAuth);
+ stream.Write(buffer, 0, greetingRequestLength);
+
+ stream.ReadBytes(buffer, 0, 2, cancellationToken);
+ var acceptsUsernamePasswordAuth = ProcessGreetingResponse(buffer, useAuth);
+
+ // If we have username and password, but the proxy doesn't need them, we skip.
+ if (useAuth && acceptsUsernamePasswordAuth)
+ {
+ var authenticationRequestLength = CreateAuthenticationRequest(buffer, authenticationSettings);
+ stream.Write(buffer, 0, authenticationRequestLength);
+
+ stream.ReadBytes(buffer, 0, 2, cancellationToken);
+ ProcessAuthenticationResponse(buffer);
+ }
+
+ var connectRequestLength = CreateConnectRequest(buffer, targetHost, targetPort);
+ stream.Write(buffer, 0, connectRequestLength);
+
+ stream.ReadBytes(buffer, 0, 5, cancellationToken);
+ var skip = ProcessConnectResponse(buffer);
+ stream.ReadBytes(buffer, 0, skip, cancellationToken);
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(buffer);
+ }
+ }
+
+ public static async Task PerformSocks5HandshakeAsync(Stream stream, EndPoint endPoint, Socks5AuthenticationSettings authenticationSettings, CancellationToken cancellationToken)
+ {
+ var (targetHost, targetPort) = endPoint.GetHostAndPort();
+ var buffer = ArrayPool.Shared.Rent(BufferSize);
+ try
+ {
+ var useAuth = authenticationSettings is Socks5AuthenticationSettings.UsernamePasswordAuthenticationSettings;
+
+ var greetingRequestLength = CreateGreetingRequest(buffer, useAuth);
+ await stream.WriteAsync(buffer, 0, greetingRequestLength, cancellationToken).ConfigureAwait(false);
+
+ await stream.ReadBytesAsync(buffer, 0, 2, cancellationToken).ConfigureAwait(false);
+ var acceptsUsernamePasswordAuth = ProcessGreetingResponse(buffer, useAuth);
+
+ // If we have username and password, but the proxy doesn't need them, we skip.
+ if (useAuth && acceptsUsernamePasswordAuth)
+ {
+ var authenticationRequestLength = CreateAuthenticationRequest(buffer, authenticationSettings);
+ await stream.WriteAsync(buffer, 0, authenticationRequestLength, cancellationToken).ConfigureAwait(false);
+
+ await stream.ReadBytesAsync(buffer, 0, 2, cancellationToken).ConfigureAwait(false);
+ ProcessAuthenticationResponse(buffer);
+ }
+
+ var connectRequestLength = CreateConnectRequest(buffer, targetHost, targetPort);
+ await stream.WriteAsync(buffer, 0, connectRequestLength, cancellationToken).ConfigureAwait(false);
+
+ await stream.ReadBytesAsync(buffer, 0, 5, cancellationToken).ConfigureAwait(false);
+ var skip = ProcessConnectResponse(buffer);
+ await stream.ReadBytesAsync(buffer, 0, skip, cancellationToken).ConfigureAwait(true);
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(buffer);
+ }
+ }
+
+ private static int CreateGreetingRequest(byte[] buffer, bool useAuth)
+ {
+ buffer[0] = ProtocolVersion5;
+
+ //buffer[1] is the number of methods supported by the client.
+ if (!useAuth)
+ {
+ buffer[1] = 1;
+ buffer[2] = MethodNoAuth;
+ return 3;
+ }
+
+ buffer[1] = 2;
+ buffer[2] = MethodNoAuth;
+ buffer[3] = MethodUsernamePassword;
+ return 4;
+ }
+
+ private static bool ProcessGreetingResponse(byte[] buffer, bool useAuth)
+ {
+ VerifyProtocolVersion(buffer[0]);
+ var acceptedMethod = buffer[1];
+ if (acceptedMethod == MethodUsernamePassword)
+ {
+ if (!useAuth)
+ {
+ throw new IOException("SOCKS5 proxy requires authentication, but no credentials were provided.");
+ }
+
+ return true;
+ }
+
+ if (acceptedMethod != MethodNoAuth)
+ {
+ throw new IOException("SOCKS5 proxy requires unsupported authentication method.");
+ }
+
+ return false;
+ }
+
+ private static int CreateAuthenticationRequest(byte[] buffer, Socks5AuthenticationSettings authenticationSettings)
+ {
+ var usernamePasswordAuthenticationSettings = (Socks5AuthenticationSettings.UsernamePasswordAuthenticationSettings)authenticationSettings;
+ var proxyUsername = usernamePasswordAuthenticationSettings.Username;
+ var proxyPassword = usernamePasswordAuthenticationSettings.Password;
+
+ // We need to add version, username.length, username, password.length, password (in this order)
+ buffer[0] = SubnegotiationVersion;
+ var usernameLength = EncodeString(proxyUsername, buffer, 2, nameof(proxyUsername));
+ buffer[1] = usernameLength;
+ var passwordLength = EncodeString(proxyPassword, buffer, 3 + usernameLength, nameof(proxyPassword));
+ buffer[2 + usernameLength] = passwordLength;
+
+ return 3 + usernameLength + passwordLength;
+ }
+
+ private static void ProcessAuthenticationResponse(byte[] buffer)
+ {
+ if (buffer[0] != SubnegotiationVersion || buffer[1] != Socks5Success)
+ {
+ throw new IOException("SOCKS5 authentication failed.");
+ }
+ }
+
+ private static int CreateConnectRequest(byte[] buffer, string targetHost, int targetPort)
+ {
+ buffer[0] = ProtocolVersion5;
+ buffer[1] = CmdConnect;
+ buffer[2] = Reserved;
+ int addressLength;
+
+ if (IPAddress.TryParse(targetHost, out var ip))
+ {
+ switch (ip.AddressFamily)
+ {
+ case AddressFamily.InterNetwork:
+ buffer[3] = AddressTypeIPv4;
+ Array.Copy(ip.GetAddressBytes(), 0, buffer, 4, 4);
+ addressLength = 4;
+ break;
+ case AddressFamily.InterNetworkV6:
+ buffer[3] = AddressTypeIPv6;
+ Array.Copy(ip.GetAddressBytes(), 0, buffer, 4, 16);
+ addressLength = 16;
+ break;
+ default:
+ throw new IOException("Invalid target host address family.");
+ }
+ }
+ else
+ {
+ buffer[3] = AddressTypeDomain;
+ var hostLength = EncodeString(targetHost, buffer, 5, nameof(targetHost));
+ buffer[4] = hostLength;
+ addressLength = hostLength + 1;
+ }
+
+ BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(addressLength + 4), (ushort)targetPort);
+
+ return addressLength + 6;
+ }
+
+ // Reads the SOCKS5 connect response and returns the number of bytes to skip in the buffer.
+ private static int ProcessConnectResponse(byte[] buffer)
+ {
+ VerifyProtocolVersion(buffer[0]);
+
+ if (buffer[1] != Socks5Success)
+ {
+ throw new IOException($"SOCKS5 connect failed"); //TODO Need to add the reason here.
+ }
+
+ // We skip the last bytes of the response as we do not need them.
+ // We skip length(dst.address) + length(dst.port) - 1 --- length(dst.port) is always 2
+ // -1 because we already ready the first byte of the address type
+ // (used for the variable length domain-type addresses)
+ return buffer[3] switch
+ {
+ AddressTypeIPv4 => 5,
+ AddressTypeIPv6 => 17,
+ AddressTypeDomain => buffer[4] + 2,
+ _ => throw new IOException("Unknown address type in SOCKS5 reply.")
+ };
+ }
+
+ private static void VerifyProtocolVersion(byte version)
+ {
+ if (version != ProtocolVersion5)
+ {
+ throw new IOException("Invalid SOCKS version in response.");
+ }
+ }
+
+ private static byte EncodeString(string input, byte[] buffer, int offset, string parameterName)
+ {
+ try
+ {
+ var written = Encoding.UTF8.GetBytes(input, 0, input.Length, buffer, offset);
+ return checked((byte)written);
+ }
+ catch
+ {
+ throw new IOException($"The {parameterName} could not be encoded as UTF-8.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/MongoDB.Driver/Core/Connections/Socks5ProxySettings.cs b/src/MongoDB.Driver/Core/Connections/Socks5ProxySettings.cs
new file mode 100644
index 00000000000..21291159670
--- /dev/null
+++ b/src/MongoDB.Driver/Core/Connections/Socks5ProxySettings.cs
@@ -0,0 +1,103 @@
+/* Copyright 2010-present MongoDB Inc.
+ *
+ * Licensed 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.
+ */
+
+using System.Text;
+using MongoDB.Driver.Core.Misc;
+using MongoDB.Shared;
+
+namespace MongoDB.Driver.Core.Connections;
+
+///
+/// Represents the settings for a SOCKS5 proxy connection.
+///
+public sealed class Socks5ProxySettings
+{
+ private const int DefaultPort = 1080;
+
+ ///
+ /// Gets the host of the SOCKS5 proxy.
+ ///
+ public string Host { get; }
+
+ ///
+ /// Gets the port of the SOCKS5 proxy.
+ ///
+ public int Port { get; }
+
+ ///
+ /// Gets the authentication settings of the SOCKS5 proxy.
+ ///
+ public Socks5AuthenticationSettings Authentication { get; }
+
+ internal Socks5ProxySettings(string host, int? port, Socks5AuthenticationSettings authentication)
+ {
+ Host = Ensure.IsNotNullOrEmpty(host, nameof(host));
+ Port = port is null ? DefaultPort : Ensure.IsBetween(port.Value, 1, 65535, nameof(port));
+ Authentication = authentication ?? Socks5AuthenticationSettings.None;
+ }
+
+ // Convenience method used internally.
+ internal static Socks5ProxySettings Create(string host, int? port, string username, string password)
+ {
+ var authentication = !string.IsNullOrEmpty(username) ?
+ Socks5AuthenticationSettings.UsernamePassword(username, password) : Socks5AuthenticationSettings.None;
+
+ return new Socks5ProxySettings(host, port, authentication);
+ }
+
+ ///
+ public override bool Equals(object obj)
+ {
+ if (obj is Socks5ProxySettings other)
+ {
+ return Host == other.Host &&
+ Port == other.Port &&
+ Equals(Authentication, other.Authentication);
+ }
+
+ return false;
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ return new Hasher()
+ .Hash(Host)
+ .Hash(Port)
+ .Hash(Authentication)
+ .GetHashCode();
+ }
+
+ ///
+ public override string ToString()
+ {
+ var sb = new StringBuilder();
+ sb.Append("{ Host : ");
+ sb.Append(Host);
+ sb.Append(", Port : ");
+ sb.Append(Port);
+ sb.Append(", Authentication : ");
+
+ sb.Append(Authentication switch
+ {
+ Socks5AuthenticationSettings.UsernamePasswordAuthenticationSettings up =>
+ $"UsernamePassword (Username: {up.Username}, Password: {up.Password})",
+ _ => "None"
+ });
+
+ sb.Append(" }");
+ return sb.ToString();
+ }
+}
\ No newline at end of file
diff --git a/src/MongoDB.Driver/Core/Connections/Socks5ProxySettingsBuilder.cs b/src/MongoDB.Driver/Core/Connections/Socks5ProxySettingsBuilder.cs
new file mode 100644
index 00000000000..df4f3e84dfc
--- /dev/null
+++ b/src/MongoDB.Driver/Core/Connections/Socks5ProxySettingsBuilder.cs
@@ -0,0 +1,66 @@
+/* Copyright 2010-present MongoDB Inc.
+ *
+ * Licensed 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.
+ */
+
+namespace MongoDB.Driver.Core.Connections;
+
+///
+/// Builder for creating .
+///
+public class Socks5ProxySettingsBuilder
+{
+ private readonly string _host;
+ private int? _port;
+ private Socks5AuthenticationSettings _authentication;
+
+ ///
+ /// Initializes a new instance of the class with the specified host.
+ ///
+ /// The host of the SOCKS5 proxy.
+ public Socks5ProxySettingsBuilder(string host)
+ {
+ _host = host;
+ }
+
+ ///
+ /// Sets the port for the SOCKS5 proxy.
+ ///
+ /// The port of the SOCKS5 proxy.
+ /// The builder instance for method chaining.
+ public Socks5ProxySettingsBuilder Port(int port)
+ {
+ _port = port;
+ return this;
+ }
+
+ ///
+ /// Sets the authentication for the SOCKS5 proxy using username and password.
+ ///
+ /// The username for authentication.
+ /// The password for authentication.
+ /// The builder instance for method chaining.
+ public Socks5ProxySettingsBuilder UsernameAndPasswordAuth(string username, string password)
+ {
+ _authentication = Socks5AuthenticationSettings.UsernamePassword(username, password);
+ return this;
+ }
+
+ ///
+ /// Builds the instance with the specified settings.
+ ///
+ public Socks5ProxySettings Build()
+ {
+ return new Socks5ProxySettings(_host, _port, _authentication);
+ }
+}
\ No newline at end of file
diff --git a/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs b/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs
index 9bb93eea097..344e3b2e51e 100644
--- a/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs
+++ b/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs
@@ -48,19 +48,36 @@ public TcpStreamFactory(TcpStreamSettings settings)
// methods
public Stream CreateStream(EndPoint endPoint, CancellationToken cancellationToken)
{
+ var socks5ProxySettings = _settings.Socks5ProxySettings;
+ var useProxy = socks5ProxySettings != null;
+ var targetEndpoint = socks5ProxySettings != null ? new DnsEndPoint(socks5ProxySettings.Host, socks5ProxySettings.Port) : endPoint;
+
#if NET472
- var socket = CreateSocket(endPoint);
- Connect(socket, endPoint, cancellationToken);
- return CreateNetworkStream(socket);
+ var socket = CreateSocket(targetEndpoint);
+ Connect(socket, targetEndpoint, cancellationToken);
+ var stream = CreateNetworkStream(socket);
+ if (useProxy)
+ {
+ Socks5Helper.PerformSocks5Handshake(stream, endPoint, socks5ProxySettings.Authentication, cancellationToken);
+ }
+
+ return stream;
#else
- var resolved = ResolveEndPoints(endPoint);
+ var resolved = ResolveEndPoints(targetEndpoint);
for (int i = 0; i < resolved.Length; i++)
{
try
{
var socket = CreateSocket(resolved[i]);
Connect(socket, resolved[i], cancellationToken);
- return CreateNetworkStream(socket);
+ var stream = CreateNetworkStream(socket);
+
+ if (useProxy)
+ {
+ Socks5Helper.PerformSocks5Handshake(stream, endPoint, socks5ProxySettings.Authentication, cancellationToken);
+ }
+
+ return stream;
}
catch
{
@@ -74,25 +91,42 @@ public Stream CreateStream(EndPoint endPoint, CancellationToken cancellationToke
}
// we should never get here...
- throw new InvalidOperationException("Unabled to resolve endpoint.");
+ throw new InvalidOperationException("Unable to resolve endpoint.");
#endif
}
public async Task CreateStreamAsync(EndPoint endPoint, CancellationToken cancellationToken)
{
+ var socks5ProxySettings = _settings.Socks5ProxySettings;
+ var useProxy = socks5ProxySettings != null;
+ var targetEndpoint = socks5ProxySettings != null ? new DnsEndPoint(socks5ProxySettings.Host, socks5ProxySettings.Port) : endPoint;
+
#if NET472
- var socket = CreateSocket(endPoint);
- await ConnectAsync(socket, endPoint, cancellationToken).ConfigureAwait(false);
- return CreateNetworkStream(socket);
+ var socket = CreateSocket(targetEndpoint);
+ await ConnectAsync(socket, targetEndpoint, cancellationToken).ConfigureAwait(false);
+ var stream = CreateNetworkStream(socket);
+ if (useProxy)
+ {
+ await Socks5Helper.PerformSocks5HandshakeAsync(stream, endPoint, socks5ProxySettings.Authentication, cancellationToken).ConfigureAwait(false);
+ }
+
+ return stream;
#else
- var resolved = await ResolveEndPointsAsync(endPoint).ConfigureAwait(false);
+ var resolved = await ResolveEndPointsAsync(targetEndpoint).ConfigureAwait(false);
for (int i = 0; i < resolved.Length; i++)
{
try
{
var socket = CreateSocket(resolved[i]);
await ConnectAsync(socket, resolved[i], cancellationToken).ConfigureAwait(false);
- return CreateNetworkStream(socket);
+ var stream = CreateNetworkStream(socket);
+
+ if (useProxy)
+ {
+ await Socks5Helper.PerformSocks5HandshakeAsync(stream, endPoint, socks5ProxySettings.Authentication, cancellationToken).ConfigureAwait(false);
+ }
+
+ return stream;
}
catch
{
@@ -258,20 +292,18 @@ private Socket CreateSocket(EndPoint endPoint)
private EndPoint[] ResolveEndPoints(EndPoint initial)
{
- var dnsInitial = initial as DnsEndPoint;
- if (dnsInitial == null)
+ if (initial is not DnsEndPoint dnsInitial)
{
- return new[] { initial };
+ return [initial];
}
- IPAddress address;
- if (IPAddress.TryParse(dnsInitial.Host, out address))
+ if (IPAddress.TryParse(dnsInitial.Host, out var address))
{
- return new[] { new IPEndPoint(address, dnsInitial.Port) };
+ return [new IPEndPoint(address, dnsInitial.Port)];
}
var preferred = initial.AddressFamily;
- if (preferred == AddressFamily.Unspecified || preferred == AddressFamily.Unknown)
+ if (preferred is AddressFamily.Unspecified or AddressFamily.Unknown)
{
preferred = _settings.AddressFamily;
}
@@ -285,20 +317,18 @@ private EndPoint[] ResolveEndPoints(EndPoint initial)
private async Task ResolveEndPointsAsync(EndPoint initial)
{
- var dnsInitial = initial as DnsEndPoint;
- if (dnsInitial == null)
+ if (initial is not DnsEndPoint dnsInitial)
{
- return new[] { initial };
+ return [initial];
}
- IPAddress address;
- if (IPAddress.TryParse(dnsInitial.Host, out address))
+ if (IPAddress.TryParse(dnsInitial.Host, out var address))
{
- return new[] { new IPEndPoint(address, dnsInitial.Port) };
+ return [new IPEndPoint(address, dnsInitial.Port)];
}
var preferred = initial.AddressFamily;
- if (preferred == AddressFamily.Unspecified || preferred == AddressFamily.Unknown)
+ if (preferred is AddressFamily.Unspecified or AddressFamily.Unknown)
{
preferred = _settings.AddressFamily;
}
diff --git a/src/MongoDB.Driver/MongoClientSettings.cs b/src/MongoDB.Driver/MongoClientSettings.cs
index d7da362ca92..40c877b812b 100644
--- a/src/MongoDB.Driver/MongoClientSettings.cs
+++ b/src/MongoDB.Driver/MongoClientSettings.cs
@@ -20,9 +20,9 @@
using System.Text;
using MongoDB.Driver.Core.Compression;
using MongoDB.Driver.Core.Configuration;
+using MongoDB.Driver.Core.Connections;
using MongoDB.Driver.Core.Misc;
using MongoDB.Driver.Core.Servers;
-using MongoDB.Driver.Encryption;
using MongoDB.Shared;
namespace MongoDB.Driver
@@ -71,6 +71,7 @@ public class MongoClientSettings : IEquatable, IInheritable
private ServerMonitoringMode _serverMonitoringMode;
private TimeSpan _serverSelectionTimeout;
private TimeSpan _socketTimeout;
+ private Socks5ProxySettings _socks5ProxySettings;
private int _srvMaxHosts;
private string _srvServiceName;
private SslSettings _sslSettings;
@@ -122,6 +123,7 @@ public MongoClientSettings()
_serverMonitoringMode = ServerMonitoringMode.Auto;
_serverSelectionTimeout = MongoDefaults.ServerSelectionTimeout;
_socketTimeout = MongoDefaults.SocketTimeout;
+ _socks5ProxySettings = null;
_srvMaxHosts = 0;
_srvServiceName = MongoInternalDefaults.MongoClientSettings.SrvServiceName;
_sslSettings = null;
@@ -428,6 +430,19 @@ public int MinConnectionPoolSize
}
}
+ ///
+ /// Gets or sets the SOCKS5 proxy settings.
+ ///
+ public Socks5ProxySettings Socks5ProxySettings
+ {
+ get => _socks5ProxySettings;
+ set
+ {
+ if (_isFrozen) { throw new InvalidOperationException("MongoClientSettings is frozen."); }
+ _socks5ProxySettings = value;
+ }
+ }
+
///
/// Gets or sets the read concern.
///
@@ -874,6 +889,10 @@ public static MongoClientSettings FromUrl(MongoUrl url)
clientSettings.ServerMonitoringMode = url.ServerMonitoringMode ?? ServerMonitoringMode.Auto;
clientSettings.ServerSelectionTimeout = url.ServerSelectionTimeout;
clientSettings.SocketTimeout = url.SocketTimeout;
+ if (!string.IsNullOrEmpty(url.ProxyHost))
+ {
+ clientSettings.Socks5ProxySettings = Socks5ProxySettings.Create(url.ProxyHost, url.ProxyPort, url.ProxyUsername, url.ProxyPassword);
+ }
clientSettings.SrvMaxHosts = url.SrvMaxHosts.GetValueOrDefault(0);
clientSettings.SrvServiceName = url.SrvServiceName;
clientSettings.SslSettings = null;
@@ -932,6 +951,7 @@ public MongoClientSettings Clone()
clone._serverMonitoringMode = _serverMonitoringMode;
clone._serverSelectionTimeout = _serverSelectionTimeout;
clone._socketTimeout = _socketTimeout;
+ clone._socks5ProxySettings = _socks5ProxySettings;
clone._srvMaxHosts = _srvMaxHosts;
clone._srvServiceName = _srvServiceName;
clone._sslSettings = (_sslSettings == null) ? null : _sslSettings.Clone();
@@ -1001,6 +1021,7 @@ public override bool Equals(object obj)
_serverMonitoringMode == rhs._serverMonitoringMode &&
_serverSelectionTimeout == rhs._serverSelectionTimeout &&
_socketTimeout == rhs._socketTimeout &&
+ object.Equals(_socks5ProxySettings, rhs._socks5ProxySettings) &&
_srvMaxHosts == rhs._srvMaxHosts &&
_srvServiceName == rhs._srvServiceName &&
_sslSettings == rhs._sslSettings &&
@@ -1088,6 +1109,7 @@ public override int GetHashCode()
.Hash(_serverMonitoringMode)
.Hash(_serverSelectionTimeout)
.Hash(_socketTimeout)
+ .Hash(_socks5ProxySettings)
.Hash(_srvMaxHosts)
.Hash(_srvServiceName)
.Hash(_sslSettings)
@@ -1145,6 +1167,10 @@ public override string ToString()
sb.AppendFormat("MaxConnectionLifeTime={0};", _maxConnectionLifeTime);
sb.AppendFormat("MaxConnectionPoolSize={0};", _maxConnectionPoolSize);
sb.AppendFormat("MinConnectionPoolSize={0};", _minConnectionPoolSize);
+ if (_socks5ProxySettings!= null)
+ {
+ sb.AppendFormat("ProxyHost={0};", _socks5ProxySettings);
+ }
if (_readEncoding != null)
{
sb.Append("ReadEncoding=UTF8Encoding;");
@@ -1221,6 +1247,7 @@ internal ClusterKey ToClusterKey()
_serverMonitoringMode,
_serverSelectionTimeout,
_socketTimeout,
+ _socks5ProxySettings,
_srvMaxHosts,
_srvServiceName,
_sslSettings,
diff --git a/src/MongoDB.Driver/MongoUrl.cs b/src/MongoDB.Driver/MongoUrl.cs
index 2c1446fd57c..912c0b15c49 100644
--- a/src/MongoDB.Driver/MongoUrl.cs
+++ b/src/MongoDB.Driver/MongoUrl.cs
@@ -64,6 +64,10 @@ public class MongoUrl : IEquatable
private readonly bool? _retryReads;
private readonly bool? _retryWrites;
private readonly TimeSpan _localThreshold;
+ private readonly string _proxyHost;
+ private readonly int? _proxyPort;
+ private readonly string _proxyUsername;
+ private readonly string _proxyPassword;
private readonly ConnectionStringScheme _scheme;
private readonly IEnumerable _servers;
private readonly ServerMonitoringMode? _serverMonitoringMode;
@@ -117,6 +121,10 @@ internal MongoUrl(MongoUrlBuilder builder)
_maxConnectionPoolSize = builder.MaxConnectionPoolSize;
_minConnectionPoolSize = builder.MinConnectionPoolSize;
_password = builder.Password;
+ _proxyHost = builder.ProxyHost;
+ _proxyPort = builder.ProxyPort;
+ _proxyUsername = builder.ProxyUsername;
+ _proxyPassword = builder.ProxyPassword;
_readConcernLevel = builder.ReadConcernLevel;
_readPreference = builder.ReadPreference;
_replicaSetName = builder.ReplicaSetName;
@@ -358,6 +366,26 @@ public string Password
get { return _password; }
}
+ ///
+ /// Gets the proxy host.
+ ///
+ public string ProxyHost => _proxyHost;
+
+ ///
+ /// Gets the proxy port.
+ ///
+ public int? ProxyPort => _proxyPort;
+
+ ///
+ /// Gets the proxy username.
+ ///
+ public string ProxyUsername => _proxyUsername;
+
+ ///
+ /// Gets the proxy password.
+ ///
+ public string ProxyPassword => _proxyPassword;
+
///
/// Gets the read concern level.
///
diff --git a/src/MongoDB.Driver/MongoUrlBuilder.cs b/src/MongoDB.Driver/MongoUrlBuilder.cs
index 3858228cbf6..c50288823c2 100644
--- a/src/MongoDB.Driver/MongoUrlBuilder.cs
+++ b/src/MongoDB.Driver/MongoUrlBuilder.cs
@@ -60,6 +60,10 @@ public class MongoUrlBuilder
private string _replicaSetName;
private bool? _retryReads;
private bool? _retryWrites;
+ private string _proxyHost;
+ private int? _proxyPort;
+ private string _proxyUsername;
+ private string _proxyPassword;
private ConnectionStringScheme _scheme;
private IEnumerable _servers;
private ServerMonitoringMode? _serverMonitoringMode;
@@ -104,6 +108,10 @@ public MongoUrlBuilder()
_maxConnectionPoolSize = MongoDefaults.MaxConnectionPoolSize;
_minConnectionPoolSize = MongoDefaults.MinConnectionPoolSize;
_password = null;
+ _proxyHost = null;
+ _proxyPort = null;
+ _proxyUsername = null;
+ _proxyPassword = null;
_readConcernLevel = null;
_readPreference = null;
_replicaSetName = null;
@@ -438,6 +446,59 @@ public string Password
set { _password = value; }
}
+ ///
+ ///
+ ///
+ public string ProxyHost
+ {
+ get => _proxyHost;
+ set
+ {
+ _proxyHost = Ensure.IsNotNullOrEmpty(value, nameof(ProxyHost));
+ }
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public int? ProxyPort
+ {
+ get => _proxyPort;
+ set
+ {
+ if (value is < 0 or > 65535)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), "ProxyPort must be between 0 and 65535.");
+ }
+ _proxyPort = value;
+ }
+ }
+
+ ///
+ ///
+ ///
+ public string ProxyUsername
+ {
+ get => _proxyUsername;
+ set
+ {
+ _proxyUsername = Ensure.IsNotNullOrEmpty(value, nameof(ProxyUsername));
+ }
+ }
+
+ ///
+ ///
+ ///
+ public string ProxyPassword
+ {
+ get => _proxyPassword;
+ set
+ {
+ _proxyPassword = Ensure.IsNotNullOrEmpty(value, nameof(ProxyPassword));
+ }
+ }
+
///
/// Gets or sets the read concern level.
///
@@ -760,6 +821,7 @@ public MongoUrl ToMongoUrl()
/// The canonical URL.
public override string ToString()
{
+ //TODO Need to add options here too
StringBuilder url = new StringBuilder();
if (_scheme == ConnectionStringScheme.MongoDB)
{
@@ -980,6 +1042,22 @@ public override string ToString()
{
query.AppendFormat("retryWrites={0}&", JsonConvert.ToString(_retryWrites.Value));
}
+ if(!string.IsNullOrEmpty(_proxyHost))
+ {
+ query.AppendFormat("proxyHost={0}&", _proxyHost);
+ }
+ if (_proxyPort.HasValue)
+ {
+ query.AppendFormat("proxyPort={0}&", _proxyPort);
+ }
+ if (!string.IsNullOrEmpty(_proxyUsername))
+ {
+ query.AppendFormat("proxyUsername={0}&", _proxyUsername);
+ }
+ if (!string.IsNullOrEmpty(_proxyPassword))
+ {
+ query.AppendFormat("proxyPassword={0}&", _proxyPassword);
+ }
if (_srvMaxHosts.HasValue)
{
query.AppendFormat("srvMaxHosts={0}&", _srvMaxHosts);
@@ -1026,6 +1104,10 @@ private void InitializeFromConnectionString(ConnectionString connectionString)
_maxConnectionPoolSize = connectionString.MaxPoolSize.GetValueOrDefault(MongoDefaults.MaxConnectionPoolSize);
_minConnectionPoolSize = connectionString.MinPoolSize.GetValueOrDefault(MongoDefaults.MinConnectionPoolSize);
_password = connectionString.Password;
+ _proxyHost = connectionString.ProxyHost;
+ _proxyPort = connectionString.ProxyPort;
+ _proxyUsername = connectionString.ProxyUsername;
+ _proxyPassword = connectionString.ProxyPassword;
_readConcernLevel = connectionString.ReadConcernLevel;
if (connectionString.ReadPreference.HasValue || connectionString.ReadPreferenceTags != null || connectionString.MaxStaleness.HasValue)
{
diff --git a/tests/MongoDB.Driver.Tests/ClusterKeyTests.cs b/tests/MongoDB.Driver.Tests/ClusterKeyTests.cs
index 481da3442c7..220c35d4f8b 100644
--- a/tests/MongoDB.Driver.Tests/ClusterKeyTests.cs
+++ b/tests/MongoDB.Driver.Tests/ClusterKeyTests.cs
@@ -268,6 +268,7 @@ private ClusterKey CreateSubject(string notEqualFieldName = null)
serverMonitoringMode,
serverSelectionTimeout,
socketTimeout,
+ null, //TODO Add correct proxy for tests
srvMaxHosts,
srvServiceName,
sslSettings,
@@ -353,6 +354,7 @@ internal ClusterKey CreateSubjectWith(
serverMonitoringMode,
serverSelectionTimeout,
socketTimeout,
+ null, //TODO Add correct proxy for tests
srvMaxHosts,
srvServiceName,
sslSettings,
diff --git a/tests/MongoDB.Driver.Tests/Core/Configuration/ConnectionStringTests.cs b/tests/MongoDB.Driver.Tests/Core/Configuration/ConnectionStringTests.cs
index ded3872bc19..2de66202c09 100644
--- a/tests/MongoDB.Driver.Tests/Core/Configuration/ConnectionStringTests.cs
+++ b/tests/MongoDB.Driver.Tests/Core/Configuration/ConnectionStringTests.cs
@@ -374,6 +374,10 @@ public void When_nothing_is_specified(string connectionString)
subject.MaxPoolSize.Should().Be(null);
subject.MinPoolSize.Should().Be(null);
subject.Password.Should().BeNull();
+ subject.ProxyHost.Should().Be(null);
+ subject.ProxyPort.Should().Be(null);
+ subject.ProxyPassword.Should().Be(null);
+ subject.ProxyUsername.Should().Be(null);
subject.ReadConcernLevel.Should().BeNull();
subject.ReadPreference.Should().BeNull();
subject.ReadPreferenceTags.Should().BeNull();
@@ -420,6 +424,10 @@ public void When_everything_is_specified()
"maxLifeTime=5ms;" +
"maxPoolSize=20;" +
"minPoolSize=15;" +
+ "proxyHost=host.com;" +
+ "proxyPort=2020;" +
+ "proxyUsername=user;" +
+ "proxyPassword=passw;" +
"readConcernLevel=majority;" +
"readPreference=primary;" +
"readPreferenceTags=dc:1;" +
@@ -462,6 +470,10 @@ public void When_everything_is_specified()
subject.MaxPoolSize.Should().Be(20);
subject.MinPoolSize.Should().Be(15);
subject.Password.Should().Be("pass");
+ subject.ProxyHost.Should().Be("host.com");
+ subject.ProxyPort.Should().Be(2020);
+ subject.ProxyUsername.Should().Be("user");
+ subject.ProxyPassword.Should().Be("passw");
subject.ReadConcernLevel.Should().Be(ReadConcernLevel.Majority);
subject.ReadPreference.Should().Be(ReadPreferenceMode.Primary);
subject.ReadPreferenceTags.Single().Should().Be(new TagSet(new[] { new Tag("dc", "1") }));
@@ -1200,6 +1212,46 @@ public void When_srvServiceName_is_specified_without_a_srv_scheme()
exception.Message.Should().Contain("srvServiceName");
}
+ [Theory]
+ [InlineData("mongodb://localhost?proxyHost=222.222.222.12", "222.222.222.12", null, null, null)]
+ [InlineData("mongodb://localhost?proxyHost=222.222.222.12&proxyPort=8080", "222.222.222.12", 8080, null, null)]
+ [InlineData("mongodb://localhost?proxyHost=example.com", "example.com", null, null, null)]
+ [InlineData("mongodb://localhost?proxyHost=example.com&proxyPort=8080", "example.com", 8080, null, null)]
+ [InlineData("mongodb://localhost?proxyHost=example.com&proxyUsername=user&proxyPassword=passw", "example.com", null, "user", "passw")]
+ [InlineData("mongodb://localhost?proxyHost=example.com&proxyPort=8080&proxyUsername=user&proxyPassword=passw", "example.com", 8080, "user", "passw")]
+ public void When_proxy_parameters_are_specified(string connectionString, string host, int? port, string username, string password)
+ {
+ var subject = new ConnectionString(connectionString);
+
+ subject.ProxyHost.Should().Be(host);
+ subject.ProxyPort.Should().Be(port);
+ subject.ProxyUsername.Should().Be(username);
+ subject.ProxyPassword.Should().Be(password);
+ }
+
+ [Theory]
+ [InlineData("mongodb://localhost?proxyPort=2020", "proxyPort")]
+ [InlineData("mongodb://localhost?proxyUsername=user", "proxyUsername")]
+ [InlineData("mongodb://localhost?proxyPassword=pasw", "proxyPassword")]
+ public void When_proxyParameter_is_specified_without_proxyHost(string connectionString, string parameterName)
+ {
+ var exception = Record.Exception(() => new ConnectionString(connectionString));
+
+ exception.Should().BeOfType();
+ exception.Message.Should().Contain(parameterName);
+ }
+
+ [Theory]
+ [InlineData("mongodb://localhost?proxyHost=host.com&proxyUsername=user")]
+ [InlineData("mongodb://localhost?proxyHost=host.com&proxyPassword=pasw")]
+ public void When_proxyPassword_and_proxyUsername_are_not_specified_together(string connectionString)
+ {
+ var exception = Record.Exception(() => new ConnectionString(connectionString));
+
+ exception.Should().BeOfType();
+ exception.Message.Should().Contain("proxyUsername and proxyPassword");
+ }
+
[Theory]
[ParameterAttributeData]
public void Valid_srvMaxHosts_with_mongodbsrv_scheme_should_be_valid([Values(0, 42)]int srvMaxHosts)
diff --git a/tests/MongoDB.Driver.Tests/MongoClientSettingsTests.cs b/tests/MongoDB.Driver.Tests/MongoClientSettingsTests.cs
index 6c188b5c6a4..5f23fdee5d2 100644
--- a/tests/MongoDB.Driver.Tests/MongoClientSettingsTests.cs
+++ b/tests/MongoDB.Driver.Tests/MongoClientSettingsTests.cs
@@ -25,9 +25,9 @@
using MongoDB.Driver.Core.Clusters;
using MongoDB.Driver.Core.Compression;
using MongoDB.Driver.Core.Configuration;
+using MongoDB.Driver.Core.Connections;
using MongoDB.Driver.Core.Servers;
using MongoDB.Driver.Core.TestHelpers.XunitExtensions;
-using MongoDB.Driver.Encryption;
using MongoDB.TestHelpers.XunitExtensions;
using Moq;
using Xunit;
@@ -233,6 +233,8 @@ public void TestConnectTimeout()
Assert.Throws(() => { settings.ConnectTimeout = connectTimeout; });
}
+ //TODO I understand we want to keep tests in alphabetical order, but I think it would make sense to group them by scope
+ //Tests like this, that should be modified for every new setting added (for instance) should be at the top of this test suite.
[Fact]
public void TestDefaults()
{
@@ -266,6 +268,7 @@ public void TestDefaults()
Assert.Equal(ServerMonitoringMode.Auto, settings.ServerMonitoringMode);
Assert.Equal(MongoDefaults.ServerSelectionTimeout, settings.ServerSelectionTimeout);
Assert.Equal(MongoDefaults.SocketTimeout, settings.SocketTimeout);
+ Assert.Equal(null, settings.Socks5ProxySettings);
Assert.Null(settings.SslSettings);
#pragma warning disable 618
Assert.Equal(false, settings.UseSsl);
@@ -435,6 +438,10 @@ public void TestEquals()
clone.SocketTimeout = new TimeSpan(1, 2, 3);
Assert.False(clone.Equals(settings));
+ clone = settings.Clone();
+ clone.Socks5ProxySettings = Socks5ProxySettings.Create("host.com", null, null, null);
+ Assert.False(clone.Equals(settings));
+
clone = settings.Clone();
clone.SslSettings = new SslSettings { CheckCertificateRevocation = false };
Assert.False(clone.Equals(settings));
@@ -475,6 +482,7 @@ public void TestEquals()
settings.ReadConcern = ReadConcern.Majority;
settings.ReadEncoding = new UTF8Encoding(false, false);
settings.ServerApi = new ServerApi(ServerApiVersion.V1);
+ settings.Socks5ProxySettings = Socks5ProxySettings.Create("host.com", 8080, null, null);
settings.WriteConcern = WriteConcern.W2;
settings.WriteEncoding = new UTF8Encoding(false, false);
@@ -485,6 +493,7 @@ public void TestEquals()
clone.ReadEncoding = new UTF8Encoding(false, false);
clone.ReadPreference = clone.ReadPreference.With(settings.ReadPreference.ReadPreferenceMode);
clone.ServerApi = new ServerApi(settings.ServerApi.Version);
+ clone.Socks5ProxySettings = Socks5ProxySettings.Create("host.com", 8080, null, null);
clone.WriteConcern = WriteConcern.FromBsonDocument(settings.WriteConcern.ToBsonDocument());
clone.WriteEncoding = new UTF8Encoding(false, false);
@@ -582,7 +591,8 @@ public void TestFromUrl()
"maxConnecting=3;maxIdleTime=124;maxLifeTime=125;maxPoolSize=126;minPoolSize=127;readConcernLevel=majority;" +
"readPreference=secondary;readPreferenceTags=a:1,b:2;readPreferenceTags=c:3,d:4;retryReads=false;retryWrites=true;socketTimeout=129;" +
"serverMonitoringMode=Stream;serverSelectionTimeout=20s;tls=true;sslVerifyCertificate=false;waitqueuesize=130;waitQueueTimeout=131;" +
- "w=1;fsync=true;journal=true;w=2;wtimeout=131;gssapiServiceName=other";
+ "w=1;fsync=true;journal=true;w=2;wtimeout=131;gssapiServiceName=other" +
+ "&proxyHost=host.com&proxyPort=2020&proxyUsername=user&proxyPassword=passw";
var builder = new MongoUrlBuilder(connectionString);
var url = builder.ToMongoUrl();
@@ -620,6 +630,10 @@ public void TestFromUrl()
Assert.Equal(ServerMonitoringMode.Stream, settings.ServerMonitoringMode);
Assert.Equal(url.ServerSelectionTimeout, settings.ServerSelectionTimeout);
Assert.Equal(url.SocketTimeout, settings.SocketTimeout);
+ Assert.Equal(url.ProxyHost, settings.Socks5ProxySettings.Host);
+ Assert.Equal(url.ProxyPort, settings.Socks5ProxySettings.Port);
+ Assert.Equal(url.ProxyUsername, ((Socks5AuthenticationSettings.UsernamePasswordAuthenticationSettings)settings.Socks5ProxySettings.Authentication).Username);
+ Assert.Equal(url.ProxyPassword, ((Socks5AuthenticationSettings.UsernamePasswordAuthenticationSettings)settings.Socks5ProxySettings.Authentication).Password);
#pragma warning disable 618
Assert.Equal(url.TlsDisableCertificateRevocationCheck, !settings.SslSettings.CheckCertificateRevocation);
Assert.Equal(url.UseSsl, settings.UseSsl);
@@ -1175,6 +1189,21 @@ public void TestSocketTimeout()
Assert.Throws(() => { settings.SocketTimeout = socketTimeout; });
}
+ [Fact]
+ public void TestSocks5ProxySettings()
+ {
+ var settings = new MongoClientSettings();
+ Assert.Equal(null, settings.Socks5ProxySettings);
+
+ var newProxySettings = Socks5ProxySettings.Create("host.com", 280, "test", "test");
+ settings.Socks5ProxySettings = newProxySettings;
+ Assert.Equal(newProxySettings, settings.Socks5ProxySettings);
+
+ settings.Freeze();
+ Assert.Equal(newProxySettings, settings.Socks5ProxySettings);
+ Assert.Throws(() => { settings.Socks5ProxySettings = newProxySettings; });
+ }
+
[Fact]
public void TestSslSettings()
{
@@ -1326,6 +1355,7 @@ public void ToClusterKey_should_copy_relevant_values()
ServerMonitoringMode = ServerMonitoringMode.Poll,
ServerSelectionTimeout = TimeSpan.FromSeconds(6),
SocketTimeout = TimeSpan.FromSeconds(4),
+ Socks5ProxySettings = Socks5ProxySettings.Create("host", 2020, null, null),
SslSettings = sslSettings,
UseTls = true,
#pragma warning disable 618
@@ -1362,6 +1392,7 @@ public void ToClusterKey_should_copy_relevant_values()
result.ServerMonitoringMode.Should().Be(ServerMonitoringMode.Poll);
result.ServerSelectionTimeout.Should().Be(subject.ServerSelectionTimeout);
result.SocketTimeout.Should().Be(subject.SocketTimeout);
+ result.Socks5ProxySettings.Should().Be(subject.Socks5ProxySettings);
result.SslSettings.Should().Be(subject.SslSettings);
result.UseTls.Should().Be(subject.UseTls);
#pragma warning disable 618
diff --git a/tests/MongoDB.Driver.Tests/Specifications/socks5-support/Socks5SupportProseTests.cs b/tests/MongoDB.Driver.Tests/Specifications/socks5-support/Socks5SupportProseTests.cs
new file mode 100644
index 00000000000..4cc21fada48
--- /dev/null
+++ b/tests/MongoDB.Driver.Tests/Specifications/socks5-support/Socks5SupportProseTests.cs
@@ -0,0 +1,80 @@
+/* Copyright 2010-present MongoDB Inc.
+ *
+ * Licensed 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.
+ */
+
+using System;
+using System.Threading.Tasks;
+using MongoDB.Bson;
+using MongoDB.Driver.Core.TestHelpers.Logging;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace MongoDB.Driver.Tests.Specifications.socks5_support;
+
+[Trait("Category", "Integration")]
+public class Socks5SupportProseTests(ITestOutputHelper testOutputHelper) : LoggableTestClass(testOutputHelper)
+{
+ [Theory]
+ [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1080&directConnection=true", false, false)]
+ [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1080&directConnection=true", false, true)]
+ [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1081&directConnection=true", true, false)]
+ [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1081&directConnection=true", true, true)]
+ [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1080", false, false)]
+ [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1080", false, true)]
+ [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1081", true, false)]
+ [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1081", true, true)]
+ [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1080&proxyUsername=nonexistentuser&proxyPassword=badauth&directConnection=true", false, false)]
+ [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1080&proxyUsername=nonexistentuser&proxyPassword=badauth&directConnection=true", false, true)]
+ [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1081&proxyUsername=nonexistentuser&proxyPassword=badauth&directConnection=true", true, false)]
+ [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1081&proxyUsername=nonexistentuser&proxyPassword=badauth&directConnection=true", true, true)]
+ [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1081&proxyUsername=nonexistentuser&proxyPassword=badauth", true, false)]
+ [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1081&proxyUsername=nonexistentuser&proxyPassword=badauth", true, true)]
+ [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1080&proxyUsername=username&proxyPassword=p4ssw0rd&directConnection=true", true, false)]
+ [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1080&proxyUsername=username&proxyPassword=p4ssw0rd&directConnection=true", true, true)]
+ [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1081&directConnection=true", true, false)]
+ [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1081&directConnection=true", true, true)]
+ [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1080&proxyUsername=username&proxyPassword=p4ssw0rd", true, false)]
+ [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1080&proxyUsername=username&proxyPassword=p4ssw0rd", true, true)]
+ [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1081", true, false)]
+ [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1081", true, true)]
+ public async Task TestConnectionStrings(string connectionString, bool expectedResult, bool async)
+ {
+ //Requires server versions > 5.0 according to spec tests, not sure why
+
+ connectionString = connectionString.Replace("", "localhost:27017").Replace("", "localhost:27017");
+ var mongoClientSettings = MongoClientSettings.FromConnectionString(connectionString);
+ mongoClientSettings.ServerSelectionTimeout = TimeSpan.FromSeconds(1.5);
+ var client = new MongoClient(mongoClientSettings);
+
+ var database = client.GetDatabase("admin");
+ var command = new BsonDocument("hello", 1);
+
+ if (expectedResult)
+ {
+ var result = async
+ ? await database.RunCommandAsync(command)
+ : database.RunCommand(command);
+
+ Assert.NotEmpty(result);
+ }
+ else
+ {
+ var exception = async
+ ? await Record.ExceptionAsync(() => database.RunCommandAsync(command))
+ : Record.Exception(() => database.RunCommand(command));
+
+ Assert.IsType(exception);
+ }
+ }
+}
\ No newline at end of file