Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jump channel #954

Open
wants to merge 14 commits into
base: develop
Choose a base branch
from
Open
2 changes: 1 addition & 1 deletion src/Renci.SshNet/Channels/Channel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ protected bool IsConnected
/// Gets the connection info.
/// </summary>
/// <value>The connection info.</value>
protected IConnectionInfo ConnectionInfo
protected ISshConnectionInfo ConnectionInfo
{
get { return _session.ConnectionInfo; }
}
Expand Down
5 changes: 4 additions & 1 deletion src/Renci.SshNet/Channels/ChannelDirectTcpip.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ public void Open(string remoteHost, uint port, IForwardedPort forwardedPort, Soc

_socket = socket;
_forwardedPort = forwardedPort;
_forwardedPort.Closing += ForwardedPort_Closing;
if (_forwardedPort != null)
{
_forwardedPort.Closing += ForwardedPort_Closing;
}

var ep = (IPEndPoint)socket.RemoteEndPoint;

Expand Down
215 changes: 215 additions & 0 deletions src/Renci.SshNet/Channels/JumpChannel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;

using Renci.SshNet.Common;

namespace Renci.SshNet.Channels
{
/// <summary>
/// Implements "direct-tcpip" SSH channel.
/// </summary>
internal sealed class JumpChannel : IDisposable
{
private readonly ISession _session;
private readonly EventWaitHandle _channelOpen = new AutoResetEvent(initialState: false);

private Socket _listener;

/// <summary>
/// Gets the bound host.
/// </summary>
public string BoundHost { get; private set; }

/// <summary>
/// Gets the bound port.
/// </summary>
public uint BoundPort { get; private set; }

/// <summary>
/// Gets the forwarded host.
/// </summary>
public string Host { get; private set; }

/// <summary>
/// Gets the forwarded port.
/// </summary>
public uint Port { get; private set; }

/// <summary>
/// Gets a value indicating whether port forwarding is started.
/// </summary>
/// <value>
/// <c>true</c> if port forwarding is started; otherwise, <c>false</c>.
/// </value>
public bool IsStarted
{ get; private set; }

/// <summary>
/// Initializes a new instance of the <see cref="JumpChannel"/> class.
/// </summary>
/// <param name="session">The session used to create the channel.</param>
/// <param name="host">The host.</param>
/// <param name="port">The port.</param>
/// <exception cref="ArgumentNullException"><paramref name="host"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="port" /> is greater than <see cref="IPEndPoint.MaxPort" />.</exception>
public JumpChannel(ISession session, string host, uint port)
{
if (host == null)
{
throw new ArgumentNullException(nameof(host));
}

port.ValidatePort("port");

Host = host;
Port = port;

_session = session;
}

public Socket Connect()
{
var ep = new IPEndPoint(IPAddress.Loopback, 0);
_listener = new Socket(ep.AddressFamily, SocketType.Stream, ProtocolType.Tcp) { NoDelay = true };
_listener.Bind(ep);
_listener.Listen(1);

IsStarted = true;

// update bound port (in case original was passed as zero)
ep.Port = ((IPEndPoint)_listener.LocalEndPoint).Port;

using (var e = new SocketAsyncEventArgs())
{
e.Completed += AcceptCompleted;

// only accept new connections while we are started
if (!_listener.AcceptAsync(e))
{
AcceptCompleted(sender: null, e);
}
}

var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.Connect(ep);

// Wait for channel to open
_session.WaitOnHandle(_channelOpen);
_listener.Dispose();
_listener = null;

return socket;
}

#region IDisposable Members

private bool _isDisposed;

/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}

/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
private void Dispose(bool disposing)
{
if (_isDisposed)
{
return;
}

if (disposing)
{
// Don't dispose the _session here, as it's considered 'owned' by the object that instantiated this JumpChannel (usually SSHConnector)
}

_isDisposed = true;
}

/// <summary>
/// Releases unmanaged resources and performs other cleanup operations before the
/// <see cref="ForwardedPortLocal"/> is reclaimed by garbage collection.
/// </summary>
~JumpChannel()
{
Dispose(disposing: false);
}

#endregion

private void AcceptCompleted(object sender, SocketAsyncEventArgs e)
{
if (e.SocketError is SocketError.OperationAborted or SocketError.NotSocket)
{
// server was stopped
return;
}

// capture client socket
var clientSocket = e.AcceptSocket;

if (e.SocketError != SocketError.Success)
{
// dispose broken client socket
CloseClientSocket(clientSocket);
return;
}

_ = _channelOpen.Set();

// process connection
ProcessAccept(clientSocket);
}

private void ProcessAccept(Socket clientSocket)
{
// close the client socket if we're no longer accepting new connections
if (!IsStarted)
{
CloseClientSocket(clientSocket);
return;
}

try
{
var originatorEndPoint = (IPEndPoint)clientSocket.RemoteEndPoint;

using (var channel = _session.CreateChannelDirectTcpip())
{
channel.Open(Host, Port, forwardedPort: null, clientSocket);
channel.Bind();
}
}
catch
{
CloseClientSocket(clientSocket);
}
}

private static void CloseClientSocket(Socket clientSocket)
{
if (clientSocket.Connected)
{
try
{
clientSocket.Shutdown(SocketShutdown.Send);
}
catch (Exception)
{
// ignore exception when client socket was already closed
}
}

clientSocket.Dispose();
}
}
}
41 changes: 40 additions & 1 deletion src/Renci.SshNet/Connection/ConnectorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,27 @@ namespace Renci.SshNet.Connection
{
internal abstract class ConnectorBase : IConnector
{
protected ConnectorBase(ISocketFactory socketFactory)
private bool _disposedValue;

protected ConnectorBase(IServiceFactory serviceFactory, ISocketFactory socketFactory)
{
if (serviceFactory is null)
{
throw new ArgumentNullException(nameof(serviceFactory));
}

if (socketFactory is null)
{
throw new ArgumentNullException(nameof(socketFactory));
}

ServiceFactory = serviceFactory;
SocketFactory = socketFactory;
}

internal IServiceFactory ServiceFactory { get; private set; }
internal ISocketFactory SocketFactory { get; private set; }
internal IConnector ProxyConnection { get; set; }

public abstract Socket Connect(IConnectionInfo connectionInfo);

Expand Down Expand Up @@ -143,5 +153,34 @@ protected static int SocketRead(Socket socket, byte[] buffer, int offset, int le

return bytesRead;
}

protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
var proxyConnection = ProxyConnection;
if (proxyConnection != null)
{
proxyConnection.Dispose();
ProxyConnection = null;
}

// TODO: dispose managed state (managed objects)
}

// TODO: free unmanaged resources (unmanaged objects) and override finalizer
// TODO: set large fields to null
_disposedValue = true;
}
}

public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}
4 changes: 2 additions & 2 deletions src/Renci.SshNet/Connection/DirectConnector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ namespace Renci.SshNet.Connection
{
internal sealed class DirectConnector : ConnectorBase
{
public DirectConnector(ISocketFactory socketFactory)
: base(socketFactory)
public DirectConnector(IServiceFactory serviceFactory, ISocketFactory socketFactory)
: base(serviceFactory, socketFactory)
{
}

Expand Down
9 changes: 5 additions & 4 deletions src/Renci.SshNet/Connection/HttpConnector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,24 +48,25 @@ internal sealed partial class HttpConnector : ProxyConnector
private static readonly Regex HttpHeaderRegex = new Regex(HttpHeaderPattern, RegexOptions.Compiled);
#endif

public HttpConnector(ISocketFactory socketFactory)
: base(socketFactory)
public HttpConnector(IServiceFactory serviceFactory, ISocketFactory socketFactory)
: base(serviceFactory, socketFactory)
{
}

protected override void HandleProxyConnect(IConnectionInfo connectionInfo, Socket socket)
{
var proxyConnection = (IProxyConnectionInfo)connectionInfo.ProxyConnection;
SocketAbstraction.Send(socket, SshData.Ascii.GetBytes(string.Format(CultureInfo.InvariantCulture,
"CONNECT {0}:{1} HTTP/1.0\r\n",
connectionInfo.Host,
connectionInfo.Port)));

// Send proxy authorization if specified
if (!string.IsNullOrEmpty(connectionInfo.ProxyUsername))
if (!string.IsNullOrEmpty(proxyConnection.Username))
{
var authorization = string.Format(CultureInfo.InvariantCulture,
"Proxy-Authorization: Basic {0}\r\n",
Convert.ToBase64String(SshData.Ascii.GetBytes($"{connectionInfo.ProxyUsername}:{connectionInfo.ProxyPassword}")));
Convert.ToBase64String(SshData.Ascii.GetBytes($"{proxyConnection.Username}:{proxyConnection.Password}")));
SocketAbstraction.Send(socket, SshData.Ascii.GetBytes(authorization));
}

Expand Down
5 changes: 3 additions & 2 deletions src/Renci.SshNet/Connection/IConnector.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
using System.Net.Sockets;
using System;
using System.Net.Sockets;
using System.Threading;

namespace Renci.SshNet.Connection
{
/// <summary>
/// Represents a means to connect to a SSH endpoint.
/// </summary>
internal interface IConnector
internal interface IConnector : IDisposable
{
/// <summary>
/// Connects to a SSH endpoint using the specified <see cref="IConnectionInfo"/>.
Expand Down
Loading