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;
}
}
}