using System; using System.Net.Sockets; using Renci.SshNet.Abstractions; using Renci.SshNet.Common; namespace Renci.SshNet.Connection { /// /// Establishes a tunnel via a SOCKS5 proxy server. /// /// /// https://en.wikipedia.org/wiki/SOCKS#SOCKS5. /// internal sealed class Socks5Connector : ProxyConnector { public Socks5Connector(ISocketFactory socketFactory) : base(socketFactory) { } /// /// Establishes a connection to the server via a SOCKS5 proxy. /// /// The connection information. /// The . protected override void HandleProxyConnect(IConnectionInfo connectionInfo, Socket socket) { var greeting = new byte[] { // SOCKS version number 0x05, // Number of supported authentication methods 0x02, // No authentication 0x00, // Username/Password authentication 0x02 }; SocketAbstraction.Send(socket, greeting); var socksVersion = SocketReadByte(socket); if (socksVersion != 0x05) { throw new ProxyException(string.Format("SOCKS Version '{0}' is not supported.", socksVersion)); } var authenticationMethod = SocketReadByte(socket); switch (authenticationMethod) { case 0x00: // No authentication break; case 0x02: // Create username/password authentication request var authenticationRequest = CreateSocks5UserNameAndPasswordAuthenticationRequest(connectionInfo.ProxyUsername, connectionInfo.ProxyPassword); // Send authentication request SocketAbstraction.Send(socket, authenticationRequest); // Read authentication result var authenticationResult = SocketAbstraction.Read(socket, 2, connectionInfo.Timeout); if (authenticationResult[0] != 0x01) { throw new ProxyException("SOCKS5: Server authentication version is not valid."); } if (authenticationResult[1] != 0x00) { throw new ProxyException("SOCKS5: Username/Password authentication failed."); } break; case 0xFF: throw new ProxyException("SOCKS5: No acceptable authentication methods were offered."); default: throw new ProxyException($"SOCKS5: Chosen authentication method '0x{authenticationMethod:x2}' is not supported."); } var connectionRequest = CreateSocks5ConnectionRequest(connectionInfo.Host, (ushort) connectionInfo.Port); SocketAbstraction.Send(socket, connectionRequest); // Read Server SOCKS5 version if (SocketReadByte(socket) != 5) { throw new ProxyException("SOCKS5: Version 5 is expected."); } // Read response code var status = SocketReadByte(socket); switch (status) { case 0x00: break; case 0x01: throw new ProxyException("SOCKS5: General failure."); case 0x02: throw new ProxyException("SOCKS5: Connection not allowed by ruleset."); case 0x03: throw new ProxyException("SOCKS5: Network unreachable."); case 0x04: throw new ProxyException("SOCKS5: Host unreachable."); case 0x05: throw new ProxyException("SOCKS5: Connection refused by destination host."); case 0x06: throw new ProxyException("SOCKS5: TTL expired."); case 0x07: throw new ProxyException("SOCKS5: Command not supported or protocol error."); case 0x08: throw new ProxyException("SOCKS5: Address type not supported."); default: throw new ProxyException("SOCKS5: Not valid response."); } // Read reserved byte if (SocketReadByte(socket) != 0) { throw new ProxyException("SOCKS5: 0 byte is expected."); } var addressType = SocketReadByte(socket); switch (addressType) { case 0x01: var ipv4 = new byte[4]; _ = SocketRead(socket, ipv4, 0, 4); break; case 0x04: var ipv6 = new byte[16]; _ =SocketRead(socket, ipv6, 0, 16); break; default: throw new ProxyException(string.Format("Address type '{0}' is not supported.", addressType)); } var port = new byte[2]; // Read 2 bytes to be ignored _ = SocketRead(socket, port, 0, 2); } /// /// https://tools.ietf.org/html/rfc1929. /// private static byte[] CreateSocks5UserNameAndPasswordAuthenticationRequest(string username, string password) { if (username.Length > byte.MaxValue) { throw new ProxyException("Proxy username is too long."); } if (password.Length > byte.MaxValue) { throw new ProxyException("Proxy password is too long."); } var authenticationRequest = new byte [ // Version of the negotiation 1 + // Length of the username 1 + // Username username.Length + // Length of the password 1 + // Password password.Length ]; var index = 0; // Version of the negiotiation authenticationRequest[index++] = 0x01; // Length of the username authenticationRequest[index++] = (byte) username.Length; // Username _ = SshData.Ascii.GetBytes(username, 0, username.Length, authenticationRequest, index); index += username.Length; // Length of the password authenticationRequest[index++] = (byte) password.Length; // Password _ =SshData.Ascii.GetBytes(password, 0, password.Length, authenticationRequest, index); return authenticationRequest; } private static byte[] CreateSocks5ConnectionRequest(string hostname, ushort port) { var addressBytes = GetSocks5DestinationAddress(hostname, out var addressType); var connectionRequest = new byte [ // SOCKS version number 1 + // Command code 1 + // Reserved 1 + // Address type 1 + // Address addressBytes.Length + // Port number 2 ]; var index = 0; // SOCKS version number connectionRequest[index++] = 0x05; // Command code connectionRequest[index++] = 0x01; // establish a TCP/IP stream connection // Reserved connectionRequest[index++] = 0x00; // Address type connectionRequest[index++] = addressType; // Address Buffer.BlockCopy(addressBytes, 0, connectionRequest, index, addressBytes.Length); index += addressBytes.Length; // Port number Pack.UInt16ToBigEndian(port, connectionRequest, index); return connectionRequest; } private static byte[] GetSocks5DestinationAddress(string hostname, out byte addressType) { var ip = DnsAbstraction.GetHostAddresses(hostname)[0]; byte[] address; #pragma warning disable IDE0010 // Add missing cases switch (ip.AddressFamily) { case AddressFamily.InterNetwork: addressType = 0x01; // IPv4 address = ip.GetAddressBytes(); break; case AddressFamily.InterNetworkV6: addressType = 0x04; // IPv6 address = ip.GetAddressBytes(); break; default: throw new ProxyException(string.Format("SOCKS5: IP address '{0}' is not supported.", ip)); } #pragma warning restore IDE0010 // Add missing cases return address; } } }