using System; using System.Linq; using System.Text; using System.Net; using System.Net.Sockets; using System.Threading; using Renci.SshNet.Abstractions; using Renci.SshNet.Channels; using Renci.SshNet.Common; namespace Renci.SshNet { public partial class ForwardedPortDynamic { private Socket _listener; private CountdownEvent _pendingChannelCountdown; partial void InternalStart() { InitializePendingChannelCountdown(); var ip = IPAddress.Any; if (!string.IsNullOrEmpty(BoundHost)) { ip = DnsAbstraction.GetHostAddresses(BoundHost)[0]; } var ep = new IPEndPoint(ip, (int) BoundPort); _listener = new Socket(ep.AddressFamily, SocketType.Stream, ProtocolType.Tcp) {NoDelay = true}; _listener.Bind(ep); _listener.Listen(5); Session.ErrorOccured += Session_ErrorOccured; Session.Disconnected += Session_Disconnected; // consider port started when we're listening for inbound connections _status = ForwardedPortStatus.Started; StartAccept(null); } private void StartAccept(SocketAsyncEventArgs e) { if (e == null) { e = new SocketAsyncEventArgs(); e.Completed += AcceptCompleted; } else { // clear the socket as we're reusing the context object e.AcceptSocket = null; } // only accept new connections while we are started if (IsStarted) { try { if (!_listener.AcceptAsync(e)) { AcceptCompleted(null, e); } } catch (ObjectDisposedException) { if (_status == ForwardedPortStatus.Stopped || _status == ForwardedPortStatus.Stopped) { // ignore ObjectDisposedException while stopping or stopped return; } throw; } } } private void AcceptCompleted(object sender, SocketAsyncEventArgs e) { if (e.SocketError == SocketError.OperationAborted || e.SocketError == SocketError.NotSocket) { // server was stopped return; } // capture client socket var clientSocket = e.AcceptSocket; if (e.SocketError != SocketError.Success) { // accept new connection StartAccept(e); // dispose broken client socket CloseClientSocket(clientSocket); return; } // accept new connection StartAccept(e); // 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; } // capture the countdown event that we're adding a count to, as we need to make sure that we'll be signaling // that same instance; the instance field for the countdown event is re-initialized when the port is restarted // and at that time there may still be pending requests var pendingChannelCountdown = _pendingChannelCountdown; pendingChannelCountdown.AddCount(); try { using (var channel = Session.CreateChannelDirectTcpip()) { channel.Exception += Channel_Exception; if (!HandleSocks(channel, clientSocket, Session.ConnectionInfo.Timeout)) { CloseClientSocket(clientSocket); return; } // start receiving from client socket (and sending to server) channel.Bind(); } } catch (Exception exp) { RaiseExceptionEvent(exp); CloseClientSocket(clientSocket); } finally { // take into account that CountdownEvent has since been disposed; when stopping the port we // wait for a given time for the channels to close, but once that timeout period has elapsed // the CountdownEvent will be disposed try { pendingChannelCountdown.Signal(); } catch (ObjectDisposedException) { } } } /// /// Initializes the . /// /// /// /// When the port is started for the first time, a is created with an initial count /// of 1. /// /// /// On subsequent (re)starts, we'll dispose the current and create a new one with /// initial count of 1. /// /// private void InitializePendingChannelCountdown() { var original = Interlocked.Exchange(ref _pendingChannelCountdown, new CountdownEvent(1)); if (original != null) { original.Dispose(); } } private bool HandleSocks(IChannelDirectTcpip channel, Socket clientSocket, TimeSpan timeout) { // create eventhandler which is to be invoked to interrupt a blocking receive // when we're closing the forwarded port EventHandler closeClientSocket = (_, args) => CloseClientSocket(clientSocket); Closing += closeClientSocket; try { var version = SocketAbstraction.ReadByte(clientSocket, timeout); switch (version) { case -1: // SOCKS client closed connection return false; case 4: return HandleSocks4(clientSocket, channel, timeout); case 5: return HandleSocks5(clientSocket, channel, timeout); default: throw new NotSupportedException(string.Format("SOCKS version {0} is not supported.", version)); } } catch (SocketException ex) { // ignore exception thrown by interrupting the blocking receive as part of closing // the forwarded port if (ex.SocketErrorCode != SocketError.Interrupted) { RaiseExceptionEvent(ex); } return false; } finally { // interrupt of blocking receive is now handled by channel (SOCKS4 and SOCKS5) // or no longer necessary Closing -= closeClientSocket; } } 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(); } /// /// Interrupts the listener, and unsubscribes from events. /// partial void StopListener() { // close listener socket var listener = _listener; if (listener != null) { listener.Dispose(); } // unsubscribe from session events var session = Session; if (session != null) { session.ErrorOccured -= Session_ErrorOccured; session.Disconnected -= Session_Disconnected; } } /// /// Waits for pending channels to close. /// /// The maximum time to wait for the pending channels to close. partial void InternalStop(TimeSpan timeout) { _pendingChannelCountdown.Signal(); _pendingChannelCountdown.Wait(timeout); } partial void InternalDispose(bool disposing) { if (disposing) { var listener = _listener; if (listener != null) { _listener = null; listener.Dispose(); } var pendingRequestsCountdown = _pendingChannelCountdown; if (pendingRequestsCountdown != null) { _pendingChannelCountdown = null; pendingRequestsCountdown.Dispose(); } } } private void Session_Disconnected(object sender, EventArgs e) { var session = Session; if (session != null) { StopPort(session.ConnectionInfo.Timeout); } } private void Session_ErrorOccured(object sender, ExceptionEventArgs e) { var session = 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) { var commandCode = SocketAbstraction.ReadByte(socket, timeout); if (commandCode == -1) { // SOCKS client closed connection return false; } // TODO: See what need to be done depends on the code var portBuffer = new byte[2]; if (SocketAbstraction.Read(socket, portBuffer, 0, portBuffer.Length, timeout) == 0) { // SOCKS client closed connection return false; } var port = (uint)(portBuffer[0] * 256 + portBuffer[1]); var ipBuffer = new byte[4]; if (SocketAbstraction.Read(socket, ipBuffer, 0, ipBuffer.Length, timeout) == 0) { // SOCKS client closed connection return false; } var ipAddress = new IPAddress(ipBuffer); var username = ReadString(socket, timeout); if (username == null) { // SOCKS client closed connection return false; } var host = ipAddress.ToString(); RaiseRequestReceived(host, port); channel.Open(host, port, this, socket); SocketAbstraction.SendByte(socket, 0x00); if (channel.IsOpen) { SocketAbstraction.SendByte(socket, 0x5a); SocketAbstraction.Send(socket, portBuffer, 0, portBuffer.Length); SocketAbstraction.Send(socket, ipBuffer, 0, ipBuffer.Length); return true; } // signal that request was rejected or failed SocketAbstraction.SendByte(socket, 0x5b); return false; } private bool HandleSocks5(Socket socket, IChannelDirectTcpip channel, TimeSpan timeout) { var authenticationMethodsCount = SocketAbstraction.ReadByte(socket, timeout); if (authenticationMethodsCount == -1) { // SOCKS client closed connection return false; } var authenticationMethods = new byte[authenticationMethodsCount]; if (SocketAbstraction.Read(socket, authenticationMethods, 0, authenticationMethods.Length, timeout) == 0) { // SOCKS client closed connection return false; } if (authenticationMethods.Min() == 0) { // no user authentication is one of the authentication methods supported // by the SOCKS client SocketAbstraction.Send(socket, new byte[] { 0x05, 0x00 }, 0, 2); } else { // the SOCKS client requires authentication, which we currently do not support SocketAbstraction.Send(socket, new byte[] { 0x05, 0xFF }, 0, 2); // we continue business as usual but expect the client to close the connection // so one of the subsequent reads should return -1 signaling that the client // has effectively closed the connection } var version = SocketAbstraction.ReadByte(socket, timeout); if (version == -1) { // SOCKS client closed connection return false; } if (version != 5) throw new ProxyException("SOCKS5: Version 5 is expected."); var commandCode = SocketAbstraction.ReadByte(socket, timeout); if (commandCode == -1) { // SOCKS client closed connection return false; } var reserved = SocketAbstraction.ReadByte(socket, timeout); if (reserved == -1) { // SOCKS client closed connection return false; } if (reserved != 0) { throw new ProxyException("SOCKS5: 0 is expected for reserved byte."); } var addressType = SocketAbstraction.ReadByte(socket, timeout); if (addressType == -1) { // SOCKS client closed connection return false; } var host = GetSocks5Host(addressType, socket, timeout); if (host == null) { // SOCKS client closed connection return false; } var portBuffer = new byte[2]; if (SocketAbstraction.Read(socket, portBuffer, 0, portBuffer.Length, timeout) == 0) { // SOCKS client closed connection return false; } var port = (uint)(portBuffer[0] * 256 + portBuffer[1]); RaiseRequestReceived(host, port); channel.Open(host, port, this, socket); var socksReply = CreateSocks5Reply(channel.IsOpen); SocketAbstraction.Send(socket, socksReply, 0, socksReply.Length); return true; } private static string GetSocks5Host(int addressType, Socket socket, TimeSpan timeout) { switch (addressType) { case 0x01: // IPv4 { var addressBuffer = new byte[4]; if (SocketAbstraction.Read(socket, addressBuffer, 0, 4, timeout) == 0) { // SOCKS client closed connection return null; } var ipv4 = new IPAddress(addressBuffer); return ipv4.ToString(); } case 0x03: // Domain name { var length = SocketAbstraction.ReadByte(socket, timeout); if (length == -1) { // SOCKS client closed connection return null; } var addressBuffer = new byte[length]; if (SocketAbstraction.Read(socket, addressBuffer, 0, addressBuffer.Length, timeout) == 0) { // SOCKS client closed connection return null; } var hostName = SshData.Ascii.GetString(addressBuffer, 0, addressBuffer.Length); return hostName; } case 0x04: // IPv6 { var addressBuffer = new byte[16]; if (SocketAbstraction.Read(socket, addressBuffer, 0, 16, timeout) == 0) { // SOCKS client closed connection return null; } var ipv6 = new IPAddress(addressBuffer); return ipv6.ToString(); } default: throw new ProxyException(string.Format("SOCKS5: Address type '{0}' is not supported.", addressType)); } } private static byte[] CreateSocks5Reply(bool channelOpen) { var socksReply = new byte[ // SOCKS version 1 + // Reply field 1 + // Reserved; fixed: 0x00 1 + // Address type; fixed: 0x01 1 + // IPv4 server bound address; fixed: {0x00, 0x00, 0x00, 0x00} 4 + // server bound port; fixed: {0x00, 0x00} 2]; socksReply[0] = 0x05; if (channelOpen) { socksReply[1] = 0x00; // succeeded } else { socksReply[1] = 0x01; // general SOCKS server failure } // reserved socksReply[2] = 0x00; // IPv4 address type socksReply[3] = 0x01; return socksReply; } /// /// Reads a null terminated string from a socket. /// /// The to read from. /// The timeout to apply to individual reads. /// /// The read, or null when the socket was closed. /// private static string ReadString(Socket socket, TimeSpan timeout) { var text = new StringBuilder(); var buffer = new byte[1]; while (true) { if (SocketAbstraction.Read(socket, buffer, 0, 1, timeout) == 0) { // SOCKS client closed connection return null; } var byteRead = buffer[0]; if (byteRead == 0) { // end of the string break; } var c = (char) byteRead; text.Append(c); } return text.ToString(); } } }