Skip to content

Commit

Permalink
WIP: Unix Socket Local & Remote Forwarding
Browse files Browse the repository at this point in the history
netstandard2.1 includes Unix-Socket-Support, which can also be used
on Windows OS since 1803.

Add UnixDomainSocketEndPoint Forwarding to Remote and Local Forwardings,
as in OpenSSH.
  • Loading branch information
darinkes committed Mar 13, 2021
1 parent ff55c49 commit f17cf7f
Show file tree
Hide file tree
Showing 20 changed files with 1,349 additions and 28 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ project.lock.json

# Build outputs
build/target/

# Rider Directory
.idea/
11 changes: 10 additions & 1 deletion src/Renci.SshNet/Abstractions/SocketAbstraction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,21 @@ public static Socket Connect(IPEndPoint remoteEndpoint, TimeSpan connectTimeout)
return socket;
}

#if FEATURE_UNIX_SOCKETS
public static Socket Connect(UnixDomainSocketEndPoint remoteEndpoint, TimeSpan connectTimeout)
{
var socket = new Socket(remoteEndpoint.AddressFamily, SocketType.Stream, ProtocolType.Unspecified);
ConnectCore(socket, remoteEndpoint, connectTimeout, true);
return socket;
}
#endif

public static void Connect(Socket socket, IPEndPoint remoteEndpoint, TimeSpan connectTimeout)
{
ConnectCore(socket, remoteEndpoint, connectTimeout, false);
}

private static void ConnectCore(Socket socket, IPEndPoint remoteEndpoint, TimeSpan connectTimeout, bool ownsSocket)
private static void ConnectCore(Socket socket, EndPoint remoteEndpoint, TimeSpan connectTimeout, bool ownsSocket)
{
#if FEATURE_SOCKET_EAP
var connectCompleted = new ManualResetEvent(false);
Expand Down
309 changes: 309 additions & 0 deletions src/Renci.SshNet/Channels/ChannelDirectStreamLocal.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using Renci.SshNet.Abstractions;
using Renci.SshNet.Common;
using Renci.SshNet.Messages.Connection;

namespace Renci.SshNet.Channels
{
/// <summary>
/// Implements "direct-streamlocal@openssh.com" SSH channel.
/// </summary>
internal class ChannelDirectStreamLocal : ClientChannel, IChannelDirectStreamLocal
{
private readonly object _socketLock = new object();

private EventWaitHandle _channelOpen = new AutoResetEvent(false);
private EventWaitHandle _channelData = new AutoResetEvent(false);
private IForwardedPort _forwardedPort;
private Socket _socket;

/// <summary>
/// Initializes a new <see cref="ChannelDirectStreamLocal"/> instance.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="localChannelNumber">The local channel number.</param>
/// <param name="localWindowSize">Size of the window.</param>
/// <param name="localPacketSize">Size of the packet.</param>
public ChannelDirectStreamLocal(ISession session, uint localChannelNumber, uint localWindowSize, uint localPacketSize)
: base(session, localChannelNumber, localWindowSize, localPacketSize)
{
}

/// <summary>
/// Gets the type of the channel.
/// </summary>
/// <value>
/// The type of the channel.
/// </value>
public override ChannelTypes ChannelType
{
get { return ChannelTypes.DirectStreamLocal; }
}

public void Open(string remoteSocket, IForwardedPort forwardedPort, Socket socket)
{
if (IsOpen)
throw new SshException("Channel is already open.");
if (!IsConnected)
throw new SshException("Session is not connected.");

lock (_socketLock)
{
_socket = socket;
}
_forwardedPort = forwardedPort;
_forwardedPort.Closing += ForwardedPort_Closing;

var originatorAddress = "";
var originatorPort = (uint)0;

if (socket.RemoteEndPoint is IPEndPoint)
{
var ep = (IPEndPoint)socket.RemoteEndPoint;
originatorAddress = ep.Address.ToString();
originatorPort = (uint) ep.Port;
}

SendMessage(new ChannelOpenMessage(LocalChannelNumber, LocalWindowSize, LocalPacketSize,
new DirectStreamLocalChannelInfo(remoteSocket, originatorAddress, originatorPort)));
// Wait for channel to open
WaitOnHandle(_channelOpen);
}

/// <summary>
/// Occurs as the forwarded port is being stopped.
/// </summary>
private void ForwardedPort_Closing(object sender, EventArgs eventArgs)
{
// signal to the client that we will not send anything anymore; this should also interrupt the
// blocking receive in Bind if the client sends FIN/ACK in time
ShutdownSocket(SocketShutdown.Send);

// if the FIN/ACK is not sent in time by the remote client, then interrupt the blocking receive
// by closing the socket
CloseSocket();
}

/// <summary>
/// Binds channel to remote host.
/// </summary>
public void Bind()
{
// Cannot bind if channel is not open
if (!IsOpen)
return;

var buffer = new byte[RemotePacketSize];

SocketAbstraction.ReadContinuous(_socket, buffer, 0, buffer.Length, SendData);

// even though the client has disconnected, we still want to properly close the
// channel
//
// we'll do this in in Close() - invoked through Dispose(bool) - that way we have
// a single place from which we send an SSH_MSG_CHANNEL_EOF message and wait for
// the SSH_MSG_CHANNEL_CLOSE message
}

/// <summary>
/// Closes the socket, hereby interrupting the blocking receive in <see cref="Bind()"/>.
/// </summary>
private void CloseSocket()
{
if (_socket == null)
return;

lock (_socketLock)
{
if (_socket == null)
return;

// closing a socket actually disposes the socket, so we can safely dereference
// the field to avoid entering the lock again later
_socket.Dispose();
_socket = null;
}
}

/// <summary>
/// Shuts down the socket.
/// </summary>
/// <param name="how">One of the <see cref="SocketShutdown"/> values that specifies the operation that will no longer be allowed.</param>
private void ShutdownSocket(SocketShutdown how)
{
if (_socket == null)
return;

lock (_socketLock)
{
if (!_socket.IsConnected())
return;

try
{
_socket.Shutdown(how);
}
catch (SocketException ex)
{
// TODO: log as warning
DiagnosticAbstraction.Log("Failure shutting down socket: " + ex);
}
}
}

/// <summary>
/// Closes the channel, waiting for the SSH_MSG_CHANNEL_CLOSE message to be received from the server.
/// </summary>
protected override void Close()
{
var forwardedPort = _forwardedPort;
if (forwardedPort != null)
{
forwardedPort.Closing -= ForwardedPort_Closing;
_forwardedPort = null;
}

// signal to the client that we will not send anything anymore; this will also interrupt the
// blocking receive in Bind if the client sends FIN/ACK in time
//
// if the FIN/ACK is not sent in time, the socket will be closed after the channel is closed
ShutdownSocket(SocketShutdown.Send);

// close the SSH channel
base.Close();

// close the socket
CloseSocket();
}

/// <summary>
/// Called when channel data is received.
/// </summary>
/// <param name="data">The data.</param>
protected override void OnData(byte[] data)
{
base.OnData(data);

if (_socket != null)
{
lock (_socketLock)
{
if (_socket.IsConnected())
{
SocketAbstraction.Send(_socket, data, 0, data.Length);
}
}
}
}

/// <summary>
/// Called when channel is opened by the server.
/// </summary>
/// <param name="remoteChannelNumber">The remote channel number.</param>
/// <param name="initialWindowSize">Initial size of the window.</param>
/// <param name="maximumPacketSize">Maximum size of the packet.</param>
protected override void OnOpenConfirmation(uint remoteChannelNumber, uint initialWindowSize, uint maximumPacketSize)
{
base.OnOpenConfirmation(remoteChannelNumber, initialWindowSize, maximumPacketSize);

_channelOpen.Set();
}

protected override void OnOpenFailure(uint reasonCode, string description, string language)
{
base.OnOpenFailure(reasonCode, description, language);

_channelOpen.Set();
}

/// <summary>
/// Called when channel has no more data to receive.
/// </summary>
protected override void OnEof()
{
base.OnEof();

// the channel will send no more data, and hence it does not make sense to receive
// any more data from the client to send to the remote party (and we surely won't
// send anything anymore)
//
// this will also interrupt the blocking receive in Bind()
ShutdownSocket(SocketShutdown.Send);
}

/// <summary>
/// Called whenever an unhandled <see cref="Exception"/> occurs in <see cref="Session"/> causing
/// the message loop to be interrupted, or when an exception occurred processing a channel message.
/// </summary>
protected override void OnErrorOccured(Exception exp)
{
base.OnErrorOccured(exp);

// signal to the client that we will not send anything anymore; this will also interrupt the
// blocking receive in Bind if the client sends FIN/ACK in time
//
// if the FIN/ACK is not sent in time, the socket will be closed in Close(bool)
ShutdownSocket(SocketShutdown.Send);
}

/// <summary>
/// Called when the server wants to terminate the connection immmediately.
/// </summary>
/// <remarks>
/// The sender MUST NOT send or receive any data after this message, and
/// the recipient MUST NOT accept any data after receiving this message.
/// </remarks>
protected override void OnDisconnected()
{
base.OnDisconnected();

// the channel will accept or send no more data, and hence it does not make sense
// to accept any more data from the client (and we surely won't send anything
// anymore)
//
// so lets signal to the client that we will not send or receive anything anymore
// this will also interrupt the blocking receive in Bind()
ShutdownSocket(SocketShutdown.Both);
}

protected override void Dispose(bool disposing)
{
// make sure we've unsubscribed from all session events and closed the channel
// before we starting disposing
base.Dispose(disposing);

if (disposing)
{
if (_socket != null)
{
lock (_socketLock)
{
var socket = _socket;
if (socket != null)
{
_socket = null;
socket.Dispose();
}
}
}

var channelOpen = _channelOpen;
if (channelOpen != null)
{
_channelOpen = null;
channelOpen.Dispose();
}

var channelData = _channelData;
if (channelData != null)
{
_channelData = null;
channelData.Dispose();
}
}
}
}
}
Loading

0 comments on commit f17cf7f

Please sign in to comment.