Tweak semaphore usage in Session (#1304)

- Change _connectAndLazySemaphoreInitLock to a SemaphoreSlim and use it in
  ConnectAsync.
- Rename it to _connectLock and only use it for connecting. Replace its
  other usages (on SessionSemaphore and NextChannelNumber) with Interlocked
  operations.
- Remove AuthenticationConnection semaphore. This static member placed a
  process-wide limit on the number of connections an application can make.
  I agree with the argument in
  https://github.com/sshnet/SSH.NET/issues/409#issuecomment-457415542
  (and in several other issues/PRs) that this should not be something
  that the library attempts to control.

The last change broke a few tests which do things like making 100 connections.
I was tempted to delete these tests as I don't think they have much value, but
instead I just limited their concurrency.

Co-authored-by: Wojciech Nagórski <wojtpl2@gmail.com>
This commit is contained in:
Rob Hague
2024-02-06 12:16:11 +00:00
committed by GitHub
parent 54a001adea
commit 47eabe7574
3 changed files with 215 additions and 250 deletions
+208 -214
View File
@@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Net.Sockets;
@@ -81,14 +82,6 @@ namespace Renci.SshNet
/// </remarks>
internal static readonly TimeSpan InfiniteTimeSpan = new TimeSpan(0, 0, 0, 0, -1);
/// <summary>
/// Controls how many authentication attempts can take place at the same time.
/// </summary>
/// <remarks>
/// Some server may restrict number to prevent authentication attacks.
/// </remarks>
private static readonly SemaphoreSlim AuthenticationConnection = new SemaphoreSlim(3);
/// <summary>
/// Holds the factory to use for creating new services.
/// </summary>
@@ -123,9 +116,9 @@ namespace Renci.SshNet
/// <summary>
/// Holds an object that is used to ensure only a single thread can connect
/// and lazy initialize the <see cref="SessionSemaphore"/> at any given time.
/// at any given time.
/// </summary>
private readonly object _connectAndLazySemaphoreInitLock = new object();
private readonly SemaphoreSlim _connectLock = new SemaphoreSlim(1, 1);
/// <summary>
/// Holds metadata about session messages.
@@ -195,7 +188,7 @@ namespace Renci.SshNet
private bool _isDisconnectMessageSent;
private uint _nextChannelNumber;
private int _nextChannelNumber;
/// <summary>
/// Holds connection socket.
@@ -212,12 +205,18 @@ namespace Renci.SshNet
{
get
{
if (_sessionSemaphore is null)
if (_sessionSemaphore is SemaphoreSlim sessionSemaphore)
{
lock (_connectAndLazySemaphoreInitLock)
{
_sessionSemaphore ??= new SemaphoreSlim(ConnectionInfo.MaxSessions);
}
return sessionSemaphore;
}
sessionSemaphore = new SemaphoreSlim(ConnectionInfo.MaxSessions);
if (Interlocked.CompareExchange(ref _sessionSemaphore, sessionSemaphore, comparand: null) is not null)
{
// Another thread has set _sessionSemaphore. Dispose our one.
Debug.Assert(_sessionSemaphore != sessionSemaphore);
sessionSemaphore.Dispose();
}
return _sessionSemaphore;
@@ -234,14 +233,7 @@ namespace Renci.SshNet
{
get
{
uint result;
lock (_connectAndLazySemaphoreInitLock)
{
result = _nextChannelNumber++;
}
return result;
return (uint)Interlocked.Increment(ref _nextChannelNumber);
}
}
@@ -583,128 +575,116 @@ namespace Renci.SshNet
return;
}
_connectLock.Wait();
try
{
AuthenticationConnection.Wait();
if (IsConnected)
{
return;
}
lock (_connectAndLazySemaphoreInitLock)
// Reset connection specific information
Reset();
// Build list of available messages while connecting
_sshMessageFactory = new SshMessageFactory();
_socket = _serviceFactory.CreateConnector(ConnectionInfo, _socketFactory)
.Connect(ConnectionInfo);
var serverIdentification = _serviceFactory.CreateProtocolVersionExchange()
.Start(ClientVersion, _socket, ConnectionInfo.Timeout);
// Set connection versions
ServerVersion = ConnectionInfo.ServerVersion = serverIdentification.ToString();
ConnectionInfo.ClientVersion = ClientVersion;
DiagnosticAbstraction.Log(string.Format("Server version '{0}'.", serverIdentification));
if (!(serverIdentification.ProtocolVersion.Equals("2.0") || serverIdentification.ProtocolVersion.Equals("1.99")))
{
// If connected don't connect again
if (IsConnected)
{
return;
}
// Reset connection specific information
Reset();
// Build list of available messages while connecting
_sshMessageFactory = new SshMessageFactory();
_socket = _serviceFactory.CreateConnector(ConnectionInfo, _socketFactory)
.Connect(ConnectionInfo);
var serverIdentification = _serviceFactory.CreateProtocolVersionExchange()
.Start(ClientVersion, _socket, ConnectionInfo.Timeout);
// Set connection versions
ServerVersion = ConnectionInfo.ServerVersion = serverIdentification.ToString();
ConnectionInfo.ClientVersion = ClientVersion;
DiagnosticAbstraction.Log(string.Format("Server version '{0}'.", serverIdentification));
if (!(serverIdentification.ProtocolVersion.Equals("2.0") || serverIdentification.ProtocolVersion.Equals("1.99")))
{
throw new SshConnectionException(string.Format(CultureInfo.CurrentCulture, "Server version '{0}' is not supported.", serverIdentification.ProtocolVersion),
DisconnectReason.ProtocolVersionNotSupported);
}
ServerIdentificationReceived?.Invoke(this, new SshIdentificationEventArgs(serverIdentification));
// Register Transport response messages
RegisterMessage("SSH_MSG_DISCONNECT");
RegisterMessage("SSH_MSG_IGNORE");
RegisterMessage("SSH_MSG_UNIMPLEMENTED");
RegisterMessage("SSH_MSG_DEBUG");
RegisterMessage("SSH_MSG_SERVICE_ACCEPT");
RegisterMessage("SSH_MSG_KEXINIT");
RegisterMessage("SSH_MSG_NEWKEYS");
// Some server implementations might sent this message first, prior to establishing encryption algorithm
RegisterMessage("SSH_MSG_USERAUTH_BANNER");
// Send our key exchange init.
// We need to do this before starting the message listener to avoid the case where we receive the server
// key exchange init and we continue the key exchange before having sent our own init.
SendMessage(ClientInitMessage);
// Mark the message listener threads as started
_ = _messageListenerCompleted.Reset();
// Start incoming request listener
// ToDo: Make message pump async, to not consume a thread for every session
_ = ThreadAbstraction.ExecuteThreadLongRunning(MessageListener);
// Wait for key exchange to be completed
WaitOnHandle(_keyExchangeCompletedWaitHandle.WaitHandle);
// If sessionId is not set then its not connected
if (SessionId is null)
{
Disconnect();
return;
}
// Request user authorization service
SendMessage(new ServiceRequestMessage(ServiceName.UserAuthentication));
// Wait for service to be accepted
WaitOnHandle(_serviceAccepted);
if (string.IsNullOrEmpty(ConnectionInfo.Username))
{
throw new SshException("Username is not specified.");
}
// Some servers send a global request immediately after successful authentication
// Avoid race condition by already enabling SSH_MSG_GLOBAL_REQUEST before authentication
RegisterMessage("SSH_MSG_GLOBAL_REQUEST");
ConnectionInfo.Authenticate(this, _serviceFactory);
_isAuthenticated = true;
// Register Connection messages
RegisterMessage("SSH_MSG_REQUEST_SUCCESS");
RegisterMessage("SSH_MSG_REQUEST_FAILURE");
RegisterMessage("SSH_MSG_CHANNEL_OPEN_CONFIRMATION");
RegisterMessage("SSH_MSG_CHANNEL_OPEN_FAILURE");
RegisterMessage("SSH_MSG_CHANNEL_WINDOW_ADJUST");
RegisterMessage("SSH_MSG_CHANNEL_EXTENDED_DATA");
RegisterMessage("SSH_MSG_CHANNEL_REQUEST");
RegisterMessage("SSH_MSG_CHANNEL_SUCCESS");
RegisterMessage("SSH_MSG_CHANNEL_FAILURE");
RegisterMessage("SSH_MSG_CHANNEL_DATA");
RegisterMessage("SSH_MSG_CHANNEL_EOF");
RegisterMessage("SSH_MSG_CHANNEL_CLOSE");
throw new SshConnectionException(string.Format(CultureInfo.CurrentCulture, "Server version '{0}' is not supported.", serverIdentification.ProtocolVersion),
DisconnectReason.ProtocolVersionNotSupported);
}
ServerIdentificationReceived?.Invoke(this, new SshIdentificationEventArgs(serverIdentification));
// Register Transport response messages
RegisterMessage("SSH_MSG_DISCONNECT");
RegisterMessage("SSH_MSG_IGNORE");
RegisterMessage("SSH_MSG_UNIMPLEMENTED");
RegisterMessage("SSH_MSG_DEBUG");
RegisterMessage("SSH_MSG_SERVICE_ACCEPT");
RegisterMessage("SSH_MSG_KEXINIT");
RegisterMessage("SSH_MSG_NEWKEYS");
// Some server implementations might sent this message first, prior to establishing encryption algorithm
RegisterMessage("SSH_MSG_USERAUTH_BANNER");
// Send our key exchange init.
// We need to do this before starting the message listener to avoid the case where we receive the server
// key exchange init and we continue the key exchange before having sent our own init.
SendMessage(ClientInitMessage);
// Mark the message listener threads as started
_ = _messageListenerCompleted.Reset();
// Start incoming request listener
// ToDo: Make message pump async, to not consume a thread for every session
_ = ThreadAbstraction.ExecuteThreadLongRunning(MessageListener);
// Wait for key exchange to be completed
WaitOnHandle(_keyExchangeCompletedWaitHandle.WaitHandle);
// If sessionId is not set then its not connected
if (SessionId is null)
{
Disconnect();
return;
}
// Request user authorization service
SendMessage(new ServiceRequestMessage(ServiceName.UserAuthentication));
// Wait for service to be accepted
WaitOnHandle(_serviceAccepted);
if (string.IsNullOrEmpty(ConnectionInfo.Username))
{
throw new SshException("Username is not specified.");
}
// Some servers send a global request immediately after successful authentication
// Avoid race condition by already enabling SSH_MSG_GLOBAL_REQUEST before authentication
RegisterMessage("SSH_MSG_GLOBAL_REQUEST");
ConnectionInfo.Authenticate(this, _serviceFactory);
_isAuthenticated = true;
// Register Connection messages
RegisterMessage("SSH_MSG_REQUEST_SUCCESS");
RegisterMessage("SSH_MSG_REQUEST_FAILURE");
RegisterMessage("SSH_MSG_CHANNEL_OPEN_CONFIRMATION");
RegisterMessage("SSH_MSG_CHANNEL_OPEN_FAILURE");
RegisterMessage("SSH_MSG_CHANNEL_WINDOW_ADJUST");
RegisterMessage("SSH_MSG_CHANNEL_EXTENDED_DATA");
RegisterMessage("SSH_MSG_CHANNEL_REQUEST");
RegisterMessage("SSH_MSG_CHANNEL_SUCCESS");
RegisterMessage("SSH_MSG_CHANNEL_FAILURE");
RegisterMessage("SSH_MSG_CHANNEL_DATA");
RegisterMessage("SSH_MSG_CHANNEL_EOF");
RegisterMessage("SSH_MSG_CHANNEL_CLOSE");
}
finally
{
_ = AuthenticationConnection.Release();
_ = _connectLock.Release();
}
}
/// <summary>
/// Asynchronously connects to the server.
/// </summary>
/// <remarks>
/// Please note this function is NOT thread safe.<br/>
/// The caller SHOULD limit the number of simultaneous connection attempts to a server to a single connection attempt.</remarks>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
/// <returns>A <see cref="Task"/> that represents the asynchronous connect operation.</returns>
/// <exception cref="SocketException">Socket connection to the SSH server or proxy server could not be established, or an error occurred while resolving the hostname.</exception>
@@ -719,97 +699,111 @@ namespace Renci.SshNet
return;
}
// Reset connection specific information
Reset();
await _connectLock.WaitAsync(cancellationToken).ConfigureAwait(false);
// Build list of available messages while connecting
_sshMessageFactory = new SshMessageFactory();
_socket = await _serviceFactory.CreateConnector(ConnectionInfo, _socketFactory)
.ConnectAsync(ConnectionInfo, cancellationToken).ConfigureAwait(false);
var serverIdentification = await _serviceFactory.CreateProtocolVersionExchange()
.StartAsync(ClientVersion, _socket, cancellationToken).ConfigureAwait(false);
// Set connection versions
ServerVersion = ConnectionInfo.ServerVersion = serverIdentification.ToString();
ConnectionInfo.ClientVersion = ClientVersion;
DiagnosticAbstraction.Log(string.Format("Server version '{0}'.", serverIdentification));
if (!(serverIdentification.ProtocolVersion.Equals("2.0") || serverIdentification.ProtocolVersion.Equals("1.99")))
try
{
throw new SshConnectionException(string.Format(CultureInfo.CurrentCulture, "Server version '{0}' is not supported.", serverIdentification.ProtocolVersion),
DisconnectReason.ProtocolVersionNotSupported);
if (IsConnected)
{
return;
}
// Reset connection specific information
Reset();
// Build list of available messages while connecting
_sshMessageFactory = new SshMessageFactory();
_socket = await _serviceFactory.CreateConnector(ConnectionInfo, _socketFactory)
.ConnectAsync(ConnectionInfo, cancellationToken).ConfigureAwait(false);
var serverIdentification = await _serviceFactory.CreateProtocolVersionExchange()
.StartAsync(ClientVersion, _socket, cancellationToken).ConfigureAwait(false);
// Set connection versions
ServerVersion = ConnectionInfo.ServerVersion = serverIdentification.ToString();
ConnectionInfo.ClientVersion = ClientVersion;
DiagnosticAbstraction.Log(string.Format("Server version '{0}'.", serverIdentification));
if (!(serverIdentification.ProtocolVersion.Equals("2.0") || serverIdentification.ProtocolVersion.Equals("1.99")))
{
throw new SshConnectionException(string.Format(CultureInfo.CurrentCulture, "Server version '{0}' is not supported.", serverIdentification.ProtocolVersion),
DisconnectReason.ProtocolVersionNotSupported);
}
ServerIdentificationReceived?.Invoke(this, new SshIdentificationEventArgs(serverIdentification));
// Register Transport response messages
RegisterMessage("SSH_MSG_DISCONNECT");
RegisterMessage("SSH_MSG_IGNORE");
RegisterMessage("SSH_MSG_UNIMPLEMENTED");
RegisterMessage("SSH_MSG_DEBUG");
RegisterMessage("SSH_MSG_SERVICE_ACCEPT");
RegisterMessage("SSH_MSG_KEXINIT");
RegisterMessage("SSH_MSG_NEWKEYS");
// Some server implementations might sent this message first, prior to establishing encryption algorithm
RegisterMessage("SSH_MSG_USERAUTH_BANNER");
// Send our key exchange init.
// We need to do this before starting the message listener to avoid the case where we receive the server
// key exchange init and we continue the key exchange before having sent our own init.
SendMessage(ClientInitMessage);
// Mark the message listener threads as started
_ = _messageListenerCompleted.Reset();
// Start incoming request listener
// ToDo: Make message pump async, to not consume a thread for every session
_ = ThreadAbstraction.ExecuteThreadLongRunning(MessageListener);
// Wait for key exchange to be completed
WaitOnHandle(_keyExchangeCompletedWaitHandle.WaitHandle);
// If sessionId is not set then its not connected
if (SessionId is null)
{
Disconnect();
return;
}
// Request user authorization service
SendMessage(new ServiceRequestMessage(ServiceName.UserAuthentication));
// Wait for service to be accepted
WaitOnHandle(_serviceAccepted);
if (string.IsNullOrEmpty(ConnectionInfo.Username))
{
throw new SshException("Username is not specified.");
}
// Some servers send a global request immediately after successful authentication
// Avoid race condition by already enabling SSH_MSG_GLOBAL_REQUEST before authentication
RegisterMessage("SSH_MSG_GLOBAL_REQUEST");
ConnectionInfo.Authenticate(this, _serviceFactory);
_isAuthenticated = true;
// Register Connection messages
RegisterMessage("SSH_MSG_REQUEST_SUCCESS");
RegisterMessage("SSH_MSG_REQUEST_FAILURE");
RegisterMessage("SSH_MSG_CHANNEL_OPEN_CONFIRMATION");
RegisterMessage("SSH_MSG_CHANNEL_OPEN_FAILURE");
RegisterMessage("SSH_MSG_CHANNEL_WINDOW_ADJUST");
RegisterMessage("SSH_MSG_CHANNEL_EXTENDED_DATA");
RegisterMessage("SSH_MSG_CHANNEL_REQUEST");
RegisterMessage("SSH_MSG_CHANNEL_SUCCESS");
RegisterMessage("SSH_MSG_CHANNEL_FAILURE");
RegisterMessage("SSH_MSG_CHANNEL_DATA");
RegisterMessage("SSH_MSG_CHANNEL_EOF");
RegisterMessage("SSH_MSG_CHANNEL_CLOSE");
}
ServerIdentificationReceived?.Invoke(this, new SshIdentificationEventArgs(serverIdentification));
// Register Transport response messages
RegisterMessage("SSH_MSG_DISCONNECT");
RegisterMessage("SSH_MSG_IGNORE");
RegisterMessage("SSH_MSG_UNIMPLEMENTED");
RegisterMessage("SSH_MSG_DEBUG");
RegisterMessage("SSH_MSG_SERVICE_ACCEPT");
RegisterMessage("SSH_MSG_KEXINIT");
RegisterMessage("SSH_MSG_NEWKEYS");
// Some server implementations might sent this message first, prior to establishing encryption algorithm
RegisterMessage("SSH_MSG_USERAUTH_BANNER");
// Send our key exchange init.
// We need to do this before starting the message listener to avoid the case where we receive the server
// key exchange init and we continue the key exchange before having sent our own init.
SendMessage(ClientInitMessage);
// Mark the message listener threads as started
_ = _messageListenerCompleted.Reset();
// Start incoming request listener
// ToDo: Make message pump async, to not consume a thread for every session
_ = ThreadAbstraction.ExecuteThreadLongRunning(MessageListener);
// Wait for key exchange to be completed
WaitOnHandle(_keyExchangeCompletedWaitHandle.WaitHandle);
// If sessionId is not set then its not connected
if (SessionId is null)
finally
{
Disconnect();
return;
_ = _connectLock.Release();
}
// Request user authorization service
SendMessage(new ServiceRequestMessage(ServiceName.UserAuthentication));
// Wait for service to be accepted
WaitOnHandle(_serviceAccepted);
if (string.IsNullOrEmpty(ConnectionInfo.Username))
{
throw new SshException("Username is not specified.");
}
// Some servers send a global request immediately after successful authentication
// Avoid race condition by already enabling SSH_MSG_GLOBAL_REQUEST before authentication
RegisterMessage("SSH_MSG_GLOBAL_REQUEST");
ConnectionInfo.Authenticate(this, _serviceFactory);
_isAuthenticated = true;
// Register Connection messages
RegisterMessage("SSH_MSG_REQUEST_SUCCESS");
RegisterMessage("SSH_MSG_REQUEST_FAILURE");
RegisterMessage("SSH_MSG_CHANNEL_OPEN_CONFIRMATION");
RegisterMessage("SSH_MSG_CHANNEL_OPEN_FAILURE");
RegisterMessage("SSH_MSG_CHANNEL_WINDOW_ADJUST");
RegisterMessage("SSH_MSG_CHANNEL_EXTENDED_DATA");
RegisterMessage("SSH_MSG_CHANNEL_REQUEST");
RegisterMessage("SSH_MSG_CHANNEL_SUCCESS");
RegisterMessage("SSH_MSG_CHANNEL_FAILURE");
RegisterMessage("SSH_MSG_CHANNEL_DATA");
RegisterMessage("SSH_MSG_CHANNEL_EOF");
RegisterMessage("SSH_MSG_CHANNEL_CLOSE");
}
/// <summary>
@@ -442,47 +442,18 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
}
}
[TestMethod]
public void Test_MultipleThread_Example_MultipleConnections()
{
try
{
#region Example SshCommand RunCommand Parallel
Parallel.For(0, 100,
() =>
{
var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password);
client.Connect();
return client;
},
(int counter, ParallelLoopState pls, SshClient client) =>
{
var result = client.RunCommand("echo 123");
Debug.WriteLine(string.Format("TestMultipleThreadMultipleConnections #{0}", counter));
return client;
},
(SshClient client) =>
{
client.Disconnect();
client.Dispose();
}
);
#endregion
}
catch (Exception exp)
{
Assert.Fail(exp.ToString());
}
}
[TestMethod]
public void Test_MultipleThread_100_MultipleConnections()
{
try
{
Parallel.For(0, 100,
var options = new ParallelOptions()
{
MaxDegreeOfParallelism = 8
};
Parallel.For(0, 100, options,
() =>
{
var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password);
@@ -83,7 +83,7 @@ namespace Renci.SshNet.IntegrationTests
public void Sftp_ConnectDisconnect_Parallel()
{
const int iterations = 10;
const int threads = 20;
const int threads = 5;
var startEvent = new ManualResetEvent(false);