ForwardedPortDynamic
Provides functionality for forwarding connections from the client to destination servers via the SSH server,
also known as dynamic port forwarding.
using Renci.SshNet.Abstractions;
using Renci.SshNet.Channels;
using Renci.SshNet.Common;
using System;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace Renci.SshNet
{
public class ForwardedPortDynamic : ForwardedPort
{
private ForwardedPortStatus _status;
private bool _isDisposed;
private Socket _listener;
private CountdownEvent _pendingChannelCountdown;
public string BoundHost { get; set; }
public uint BoundPort { get; set; }
public override bool IsStarted => _status == ForwardedPortStatus.Started;
public ForwardedPortDynamic(uint port)
: this(string.Empty, port)
{
}
public ForwardedPortDynamic(string host, uint port)
{
BoundHost = host;
BoundPort = port;
_status = ForwardedPortStatus.Stopped;
}
protected override void StartPort()
{
if (ForwardedPortStatus.ToStarting(ref _status))
try {
InternalStart();
} catch (Exception) {
_status = ForwardedPortStatus.Stopped;
throw;
}
}
protected override void StopPort(TimeSpan timeout)
{
if (ForwardedPortStatus.ToStopping(ref _status)) {
base.StopPort(timeout);
StopListener();
InternalStop(timeout);
_status = ForwardedPortStatus.Stopped;
}
}
protected override void CheckDisposed()
{
if (_isDisposed)
throw new ObjectDisposedException(GetType().FullName);
}
private void InternalStart()
{
InitializePendingChannelCountdown();
IPAddress address = IPAddress.Any;
if (!string.IsNullOrEmpty(BoundHost))
address = DnsAbstraction.GetHostAddresses(BoundHost)[0];
IPEndPoint iPEndPoint = new IPEndPoint(address, (int)BoundPort);
_listener = new Socket(iPEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp) {
NoDelay = true
};
_listener.Bind(iPEndPoint);
_listener.Listen(5);
base.Session.ErrorOccured += Session_ErrorOccured;
base.Session.Disconnected += Session_Disconnected;
_status = ForwardedPortStatus.Started;
StartAccept(null);
}
private void StopListener()
{
_listener?.Dispose();
ISession session = base.Session;
if (session != null) {
session.ErrorOccured -= Session_ErrorOccured;
session.Disconnected -= Session_Disconnected;
}
}
private void InternalStop(TimeSpan timeout)
{
_pendingChannelCountdown.Signal();
_pendingChannelCountdown.Wait(timeout);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void InternalDispose(bool disposing)
{
if (disposing) {
Socket listener = _listener;
if (listener != null) {
_listener = null;
listener.Dispose();
}
CountdownEvent pendingChannelCountdown = _pendingChannelCountdown;
if (pendingChannelCountdown != null) {
_pendingChannelCountdown = null;
pendingChannelCountdown.Dispose();
}
}
}
protected override void Dispose(bool disposing)
{
if (!_isDisposed) {
base.Dispose(disposing);
InternalDispose(disposing);
_isDisposed = true;
}
}
~ForwardedPortDynamic()
{
Dispose(false);
}
private void StartAccept(SocketAsyncEventArgs e)
{
if (e == null) {
e = new SocketAsyncEventArgs();
e.Completed += AcceptCompleted;
} else
e.AcceptSocket = null;
if (IsStarted)
try {
if (!_listener.AcceptAsync(e))
AcceptCompleted(null, e);
} catch (ObjectDisposedException) {
if (!(_status == ForwardedPortStatus.Stopped) && !(_status == ForwardedPortStatus.Stopped))
throw;
}
}
private void AcceptCompleted(object sender, SocketAsyncEventArgs e)
{
if (e.SocketError != SocketError.OperationAborted && e.SocketError != SocketError.NotSocket) {
Socket acceptSocket = e.AcceptSocket;
if (e.SocketError != 0) {
StartAccept(e);
CloseClientSocket(acceptSocket);
} else {
StartAccept(e);
ProcessAccept(acceptSocket);
}
}
}
private void ProcessAccept(Socket clientSocket)
{
if (!IsStarted)
CloseClientSocket(clientSocket);
else {
CountdownEvent pendingChannelCountdown = _pendingChannelCountdown;
pendingChannelCountdown.AddCount();
try {
using (IChannelDirectTcpip channelDirectTcpip = base.Session.CreateChannelDirectTcpip()) {
channelDirectTcpip.Exception += Channel_Exception;
if (!HandleSocks(channelDirectTcpip, clientSocket, base.Session.ConnectionInfo.Timeout))
CloseClientSocket(clientSocket);
else
channelDirectTcpip.Bind();
}
} catch (Exception exception) {
RaiseExceptionEvent(exception);
CloseClientSocket(clientSocket);
} finally {
try {
pendingChannelCountdown.Signal();
} catch (ObjectDisposedException) {
}
}
}
}
private void InitializePendingChannelCountdown()
{
Interlocked.Exchange(ref _pendingChannelCountdown, new CountdownEvent(1))?.Dispose();
}
private bool HandleSocks(IChannelDirectTcpip channel, Socket clientSocket, TimeSpan timeout)
{
EventHandler value = delegate {
CloseClientSocket(clientSocket);
};
base.Closing += value;
try {
int num = SocketAbstraction.ReadByte(clientSocket, timeout);
switch (num) {
case -1:
return false;
case 4:
return HandleSocks4(clientSocket, channel, timeout);
case 5:
return HandleSocks5(clientSocket, channel, timeout);
default:
throw new NotSupportedException($"""{num}""");
}
} catch (SocketException ex) {
if (ex.SocketErrorCode != SocketError.Interrupted)
RaiseExceptionEvent(ex);
return false;
} finally {
base.Closing -= value;
}
}
private static void CloseClientSocket(Socket clientSocket)
{
if (clientSocket.Connected)
try {
clientSocket.Shutdown(SocketShutdown.Send);
} catch (Exception) {
}
clientSocket.Dispose();
}
private void Session_Disconnected(object sender, EventArgs e)
{
ISession session = base.Session;
if (session != null)
StopPort(session.ConnectionInfo.Timeout);
}
private void Session_ErrorOccured(object sender, ExceptionEventArgs e)
{
ISession session = base.Session;
if (session != null)
StopPort(session.ConnectionInfo.Timeout);
}
private void Channel_Exception(object sender, ExceptionEventArgs e)
{
RaiseExceptionEvent(e.Exception);
}
private bool HandleSocks4(Socket socket, IChannelDirectTcpip channel, TimeSpan timeout)
{
if (SocketAbstraction.ReadByte(socket, timeout) == -1)
return false;
byte[] array = new byte[2];
if (SocketAbstraction.Read(socket, array, 0, array.Length, timeout) == 0)
return false;
ushort port = Pack.BigEndianToUInt16(array);
byte[] array2 = new byte[4];
if (SocketAbstraction.Read(socket, array2, 0, array2.Length, timeout) == 0)
return false;
IPAddress iPAddress = new IPAddress(array2);
if (ReadString(socket, timeout) == null)
return false;
string text = iPAddress.ToString();
RaiseRequestReceived(text, port);
channel.Open(text, port, this, socket);
SocketAbstraction.SendByte(socket, 0);
if (channel.IsOpen) {
SocketAbstraction.SendByte(socket, 90);
SocketAbstraction.Send(socket, array, 0, array.Length);
SocketAbstraction.Send(socket, array2, 0, array2.Length);
return true;
}
SocketAbstraction.SendByte(socket, 91);
return false;
}
private bool HandleSocks5(Socket socket, IChannelDirectTcpip channel, TimeSpan timeout)
{
int num = SocketAbstraction.ReadByte(socket, timeout);
if (num == -1)
return false;
byte[] array = new byte[num];
if (SocketAbstraction.Read(socket, array, 0, array.Length, timeout) != 0) {
if (array.Min() == 0)
SocketAbstraction.Send(socket, new byte[2] {
5,
0
}, 0, 2);
else
SocketAbstraction.Send(socket, new byte[2] {
5,
byte.MaxValue
}, 0, 2);
switch (SocketAbstraction.ReadByte(socket, timeout)) {
case -1:
return false;
default:
throw new ProxyException("SOCKS5: Version 5 is expected.");
case 5:
if (SocketAbstraction.ReadByte(socket, timeout) != -1) {
switch (SocketAbstraction.ReadByte(socket, timeout)) {
case -1:
return false;
default:
throw new ProxyException("SOCKS5: 0 is expected for reserved byte.");
case 0: {
int num2 = SocketAbstraction.ReadByte(socket, timeout);
if (num2 == -1)
return false;
string socks5Host = GetSocks5Host(num2, socket, timeout);
if (socks5Host == null)
return false;
byte[] array2 = new byte[2];
if (SocketAbstraction.Read(socket, array2, 0, array2.Length, timeout) == 0)
return false;
ushort port = Pack.BigEndianToUInt16(array2);
RaiseRequestReceived(socks5Host, port);
channel.Open(socks5Host, port, this, socket);
byte[] array3 = CreateSocks5Reply(channel.IsOpen);
SocketAbstraction.Send(socket, array3, 0, array3.Length);
return true;
}
}
}
return false;
}
}
return false;
}
private static string GetSocks5Host(int addressType, Socket socket, TimeSpan timeout)
{
switch (addressType) {
case 1: {
byte[] array3 = new byte[4];
if (SocketAbstraction.Read(socket, array3, 0, 4, timeout) == 0)
return null;
return new IPAddress(array3).ToString();
}
case 3: {
int num = SocketAbstraction.ReadByte(socket, timeout);
if (num == -1)
return null;
byte[] array2 = new byte[num];
if (SocketAbstraction.Read(socket, array2, 0, array2.Length, timeout) == 0)
return null;
return SshData.Ascii.GetString(array2, 0, array2.Length);
}
case 4: {
byte[] array = new byte[16];
if (SocketAbstraction.Read(socket, array, 0, 16, timeout) == 0)
return null;
return new IPAddress(array).ToString();
}
default:
throw new ProxyException($"""{addressType}""");
}
}
private static byte[] CreateSocks5Reply(bool channelOpen)
{
byte[] array = new byte[10] {
5,
0,
0,
0,
0,
0,
0,
0,
0,
0
};
if (channelOpen)
array[1] = 0;
else
array[1] = 1;
array[2] = 0;
array[3] = 1;
return array;
}
private static string ReadString(Socket socket, TimeSpan timeout)
{
StringBuilder stringBuilder = new StringBuilder();
byte[] array = new byte[1];
while (true) {
if (SocketAbstraction.Read(socket, array, 0, 1, timeout) == 0)
return null;
byte b = array[0];
if (b == 0)
break;
char value = (char)b;
stringBuilder.Append(value);
}
return stringBuilder.ToString();
}
}
}