Last active
November 5, 2024 07:51
-
-
Save zHaytam/3730d512eb5eaf37fb3bd3d176185541 to your computer and use it in GitHub Desktop.
A Socks5 implementation in .NET Core (C# 8)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Globalization; | |
using System.Net; | |
using System.Net.Sockets; | |
using System.Text; | |
using System.Threading.Tasks; | |
namespace Socks | |
{ | |
public static class Socks5 | |
{ | |
public static async Task<Socket> Connect(Func<Socket> socketFactory, Socks5Options options) | |
{ | |
if (options == null) | |
throw new ArgumentNullException(nameof(options)); | |
var socket = socketFactory(); | |
await socket.ConnectAsync(options.ProxyHost, options.ProxyPort); | |
await SelectAuth(socket, options); | |
await Connect(socket, options); | |
return socket; | |
} | |
private static async Task SelectAuth(Socket socket, Socks5Options options) | |
{ | |
/* | |
+----+----------+----------+ | |
| VER | NMETHODS | METHODS | | |
+----+----------+----------+ | |
| 1 | 1 | 1 to 255 | | |
+----+----------+----------+ | |
*/ | |
var buffer = new byte[4] { | |
5, | |
2, | |
Socks5Constants.AuthMethodNoAuthenticationRequired, Socks5Constants.AuthMethodUsernamePassword | |
}; | |
await socket.SendAsync(buffer, SocketFlags.None); | |
/* | |
+-----+--------+ | |
| VER | METHOD | | |
+-----+--------+ | |
| 1 | 1 | | |
+-----+--------+ | |
*/ | |
var response = new byte[2]; | |
var read = await socket.ReceiveAsync(response, SocketFlags.None); | |
if (read != 2) | |
throw new SocksocketException($"Failed to select an authentication method, the server sent {read} bytes."); | |
if (response[1] == Socks5Constants.AuthMethodReplyNoAcceptableMethods) | |
{ | |
socket.Close(); | |
throw new SocksocketException("The proxy destination does not accept the supported proxy client authentication methods."); | |
} | |
if (response[1] == Socks5Constants.AuthMethodUsernamePassword && options.Auth == AuthType.None) | |
{ | |
socket.Close(); | |
throw new SocksocketException("The proxy destination requires a username and password for authentication."); | |
} | |
if (response[1] == Socks5Constants.AuthMethodNoAuthenticationRequired) | |
return; | |
await PerformAuth(socket, options); | |
} | |
private static async Task PerformAuth(Socket socket, Socks5Options options) | |
{ | |
/* | |
+-----+------+----------+------+----------+ | |
| VER | ULEN | UNAME | PLEN | PASSWD | | |
+----+-------+----------+------+----------+ | |
| 1 | 1 | 1 to 255 | 1 | 1 to 255 | | |
+----+-------+----------+------+----------+ | |
*/ | |
var buffer = ConstructAuthBuffer(options.Credentials.Username, options.Credentials.Password); | |
await socket.SendAsync(buffer, SocketFlags.None); | |
/* | |
+----+--------+ | |
|VER | STATUS | | |
+----+--------+ | |
| 1 | 1 | | |
+----+--------+ | |
*/ | |
var response = new byte[2]; | |
var read = await socket.ReceiveAsync(response, SocketFlags.None); | |
if (read != 2) | |
throw new SocksocketException($"Failed to perform authentication, the server sent {read} bytes."); | |
if (response[1] != 0) | |
{ | |
socket.Close(); | |
throw new SocksocketException("Proxy authentication failed."); | |
} | |
} | |
private static async Task Connect(Socket socket, Socks5Options options) | |
{ | |
/* | |
+-----+-----+-------+------+----------+----------+ | |
| VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | | |
+--- -+-----+-------+------+----------+----------+ | |
| 1 | 1 | X'00' | 1 | Variable | 2 | | |
+-----+-----+-------+------+----------+----------+ | |
*/ | |
var addressType = GetDestAddressType(options.DestinationHost); | |
var destAddr = GetDestAddressBytes(addressType, options.DestinationHost); | |
var destPort = GetDestPortBytes(options.DestinationPort); | |
var buffer = new byte[6 + options.DestinationHost.Length]; | |
buffer[0] = 5; | |
buffer[1] = Socks5Constants.CmdConnect; | |
buffer[2] = Socks5Constants.Reserved; | |
buffer[3] = addressType; | |
destAddr.CopyTo(buffer, 4); | |
destPort.CopyTo(buffer, 4 + destAddr.Length); | |
await socket.SendAsync(buffer, SocketFlags.None); | |
/* | |
+---- +-----+-------+------+----------+----------+ | |
| VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | | |
+-----+-----+-------+------+----------+----------+ | |
| 1 | 1 | X'00' | 1 | Variable | 2 | | |
+-----+-----+-------+------+----------+----------+ | |
*/ | |
var response = new byte[255]; | |
await socket.ReceiveAsync(response, SocketFlags.None); | |
if (response[1] != Socks5Constants.CmdReplySucceeded) | |
HandleProxyCommandError(response, options.DestinationHost, options.DestinationPort); | |
} | |
private static void HandleProxyCommandError(byte[] response, string destinationHost, int destinationPort) | |
{ | |
var replyCode = response[1]; | |
var proxyErrorText = replyCode switch | |
{ | |
Socks5Constants.CmdReplyGeneralSocksServerFailure => "a general socks destination failure occurred", | |
Socks5Constants.CmdReplyConnectionNotAllowedByRuleset => "the connection is not allowed by proxy destination rule set", | |
Socks5Constants.CmdReplyNetworkUnreachable => "the network was unreachable", | |
Socks5Constants.CmdReplyHostUnreachable => "the host was unreachable", | |
Socks5Constants.CmdReplyConnectionRefused => "the connection was refused by the remote network", | |
Socks5Constants.CmdReplyTtlExpired => "the time to live (TTL) has expired", | |
Socks5Constants.CmdReplyCommandNotSupported => "the command issued by the proxy client is not supported by the proxy destination", | |
Socks5Constants.CmdReplyAddressTypeNotSupported => "the address type specified is not supported", | |
_ => string.Format(CultureInfo.InvariantCulture, | |
"an unknown SOCKS reply with the code value '{0}' was received", | |
replyCode.ToString(CultureInfo.InvariantCulture)), | |
}; | |
string exceptionMsg = string.Format(CultureInfo.InvariantCulture, | |
"proxy error: {0} for destination host {1} port number {2}.", | |
proxyErrorText, destinationHost, destinationPort); | |
throw new SocksocketException(exceptionMsg); | |
} | |
private static byte[] ConstructAuthBuffer(string username, string password) | |
{ | |
var credentials = new byte[3 + username.Length + password.Length]; | |
credentials[0] = 0x01; | |
credentials[1] = (byte)username.Length; | |
Array.Copy(Encoding.ASCII.GetBytes(username), 0, credentials, 2, username.Length); | |
credentials[username.Length + 2] = (byte)password.Length; | |
Array.Copy(Encoding.ASCII.GetBytes(password), 0, credentials, 2, password.Length); | |
return credentials; | |
} | |
private static byte GetDestAddressType(string host) | |
{ | |
if (!IPAddress.TryParse(host, out var ipAddr)) | |
return Socks5Constants.AddrtypeDomainName; | |
switch (ipAddr.AddressFamily) | |
{ | |
case AddressFamily.InterNetwork: | |
return Socks5Constants.AddrtypeIpv4; | |
case AddressFamily.InterNetworkV6: | |
return Socks5Constants.AddrtypeIpv6; | |
default: | |
throw new SocksocketException( | |
string.Format("The host addess {0} of type '{1}' is not a supported address type.\n" + | |
"The supported types are InterNetwork and InterNetworkV6.", host, | |
Enum.GetName(typeof(AddressFamily), ipAddr.AddressFamily))); | |
} | |
} | |
private static byte[] GetDestAddressBytes(byte addressType, string host) | |
{ | |
switch (addressType) | |
{ | |
case Socks5Constants.AddrtypeIpv4: | |
case Socks5Constants.AddrtypeIpv6: | |
return IPAddress.Parse(host).GetAddressBytes(); | |
case Socks5Constants.AddrtypeDomainName: | |
byte[] bytes = new byte[host.Length + 1]; | |
bytes[0] = Convert.ToByte(host.Length); | |
Encoding.ASCII.GetBytes(host).CopyTo(bytes, 1); | |
return bytes; | |
default: | |
return null; | |
} | |
} | |
private static byte[] GetDestPortBytes(int value) | |
{ | |
return new byte[2] | |
{ | |
Convert.ToByte(value / 256), | |
Convert.ToByte(value % 256) | |
}; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace Socks | |
{ | |
public class Socks5Constants | |
{ | |
public const byte Reserved = 0x00; | |
public const byte AuthNumberOfAuthMethodsSupported = 2; | |
public const byte AuthMethodNoAuthenticationRequired = 0x00; | |
public const byte AuthMethodGssapi = 0x01; | |
public const byte AuthMethodUsernamePassword = 0x02; | |
public const byte AuthMethodIanaAssignedRangeBegin = 0x03; | |
public const byte AuthMethodIanaAssignedRangeEnd = 0x7f; | |
public const byte AuthMethodReservedRangeBegin = 0x80; | |
public const byte AuthMethodReservedRangeEnd = 0xfe; | |
public const byte AuthMethodReplyNoAcceptableMethods = 0xff; | |
public const byte CmdConnect = 0x01; | |
public const byte CmdBind = 0x02; | |
public const byte CmdUdpAssociate = 0x03; | |
public const byte CmdReplySucceeded = 0x00; | |
public const byte CmdReplyGeneralSocksServerFailure = 0x01; | |
public const byte CmdReplyConnectionNotAllowedByRuleset = 0x02; | |
public const byte CmdReplyNetworkUnreachable = 0x03; | |
public const byte CmdReplyHostUnreachable = 0x04; | |
public const byte CmdReplyConnectionRefused = 0x05; | |
public const byte CmdReplyTtlExpired = 0x06; | |
public const byte CmdReplyCommandNotSupported = 0x07; | |
public const byte CmdReplyAddressTypeNotSupported = 0x08; | |
public const byte AddrtypeIpv4 = 0x01; | |
public const byte AddrtypeDomainName = 0x03; | |
public const byte AddrtypeIpv6 = 0x04; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace Socks | |
{ | |
public class Socks5Options | |
{ | |
public string ProxyHost { get; } | |
public int ProxyPort { get; } | |
public string DestinationHost { get; } | |
public int DestinationPort { get; } | |
public AuthType? Auth { get; } | |
public (string Username, string Password) Credentials { get; } | |
public Socks5Options(string proxyHost, int proxyPort, string destHost, int destPort) | |
{ | |
ProxyHost = proxyHost; | |
ProxyPort = proxyPort; | |
DestinationHost = destHost; | |
DestinationPort = destPort; | |
Auth = AuthType.None; | |
} | |
public Socks5Options(string proxyHost, string destHost, int destPort) : this(proxyHost, 1080, destHost, destPort) { } | |
public Socks5Options(string proxyHost, int proxyPort, string destHost, int destPort, string username, | |
string password) : this(proxyHost, proxyPort, destHost, destPort) | |
{ | |
Auth = AuthType.UsernamePassword; | |
Credentials = (username, password); | |
} | |
public Socks5Options(string proxyHost, string destHost, int destPort, string username, string password) : | |
this(proxyHost, 1080, destHost, destPort, username, password) | |
{ } | |
} | |
public enum AuthType | |
{ | |
None, | |
UsernamePassword | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hello! Impressive. I am trying to make a .net app able to communicate with databases on-premises via SAP Cloud Connector using Socks5 prot. Do you have any implementation of this Socks5 in app ?