/// <summary> /// IsolateSOCKSAuth must be on (on by default) /// https://www.torproject.org/docs/tor-manual.html.en /// https://gitweb.torproject.org/torspec.git/tree/socks-extensions.txt#n35 /// </summary> /// <param name="identity">Isolates streams by identity. If identity is empty string, it won't isolate stream.</param> internal async Task HandshakeAsync(string identity) { if (TorSocks5EndPoint is null) { return; } Guard.NotNull(nameof(identity), identity); MethodsField methods = string.IsNullOrWhiteSpace(identity) ? new MethodsField(MethodField.NoAuthenticationRequired) : new MethodsField(MethodField.UsernamePassword); var sendBuffer = new VersionMethodRequest(methods).ToBytes(); var receiveBuffer = await SendAsync(sendBuffer, 2).ConfigureAwait(false); var methodSelection = new MethodSelectionResponse(); methodSelection.FromBytes(receiveBuffer); if (methodSelection.Ver != VerField.Socks5) { throw new NotSupportedException($"SOCKS{methodSelection.Ver.Value} not supported. Only SOCKS5 is supported."); } if (methodSelection.Method == MethodField.NoAcceptableMethods) { // https://www.ietf.org/rfc/rfc1928.txt // If the selected METHOD is X'FF', none of the methods listed by the // client are acceptable, and the client MUST close the connection. DisposeTcpClient(); throw new NotSupportedException("Tor's SOCKS5 proxy does not support any of the client's authentication methods."); } if (methodSelection.Method == MethodField.UsernamePassword) { // https://tools.ietf.org/html/rfc1929#section-2 // Once the SOCKS V5 server has started, and the client has selected the // Username / Password Authentication protocol, the Username / Password // subnegotiation begins. This begins with the client producing a // Username / Password request: var username = identity; var password = identity; var uName = new UNameField(username); var passwd = new PasswdField(password); var usernamePasswordRequest = new UsernamePasswordRequest(uName, passwd); sendBuffer = usernamePasswordRequest.ToBytes(); Array.Clear(receiveBuffer, 0, receiveBuffer.Length); receiveBuffer = await SendAsync(sendBuffer, 2).ConfigureAwait(false); var userNamePasswordResponse = new UsernamePasswordResponse(); userNamePasswordResponse.FromBytes(receiveBuffer); if (userNamePasswordResponse.Ver != usernamePasswordRequest.Ver) { throw new NotSupportedException($"Authentication version {userNamePasswordResponse.Ver.Value} not supported. Only version {usernamePasswordRequest.Ver} is supported."); } if (!userNamePasswordResponse.Status.IsSuccess()) // Tor authentication is different, this will never happen; { // https://tools.ietf.org/html/rfc1929#section-2 // A STATUS field of X'00' indicates success. If the server returns a // `failure' (STATUS value other than X'00') status, it MUST close the // connection. DisposeTcpClient(); throw new InvalidOperationException("Wrong username and/or password."); } } }
/// <summary> /// Do the authentication part of Tor's SOCKS5 protocol. /// </summary> /// <param name="isolateStream">Whether random username/password should be used for authentication and thus effectively create a new Tor circuit.</param> /// <remarks>Tor process must be started with enabled <c>IsolateSOCKSAuth</c> option. It's ON by default.</remarks> /// <seealso href="https://www.torproject.org/docs/tor-manual.html.en"/> /// <seealso href="https://linux.die.net/man/1/tor">For <c>IsolateSOCKSAuth</c> option explanation.</seealso> /// <seealso href="https://gitweb.torproject.org/torspec.git/tree/socks-extensions.txt#n35"/> public async Task HandshakeAsync(bool isolateStream = false, CancellationToken cancellationToken = default) { Logger.LogDebug($"> {nameof(isolateStream)}={isolateStream}"); // https://github.com/torproject/torspec/blob/master/socks-extensions.txt // The "NO AUTHENTICATION REQUIRED" (SOCKS5) authentication method [00] is // supported; and as of Tor 0.2.3.2 - alpha, the "USERNAME/PASSWORD"(SOCKS5) // authentication method[02] is supported too, and used as a method to // implement stream isolation.As an extension to support some broken clients, // we allow clients to pass "USERNAME/PASSWORD" authentication message to us // even if no authentication was selected.Furthermore, we allow // username / password fields of this message to be empty. This technically // violates RFC1929[4], but ensures interoperability with somewhat broken // SOCKS5 client implementations. var methods = new MethodsField(isolateStream ? MethodField.UsernamePassword : MethodField.NoAuthenticationRequired); byte[] sendBuffer = new VersionMethodRequest(methods).ToBytes(); byte[] receiveBuffer = await SendAsync(sendBuffer, receiveBufferSize : 2, cancellationToken).ConfigureAwait(false); var methodSelection = new MethodSelectionResponse(receiveBuffer); if (methodSelection.Ver != VerField.Socks5) { throw new NotSupportedException($"SOCKS{methodSelection.Ver.Value} not supported. Only SOCKS5 is supported."); } else if (methodSelection.Method == MethodField.NoAcceptableMethods) { // https://www.ietf.org/rfc/rfc1928.txt // If the selected METHOD is X'FF', none of the methods listed by the // client are acceptable, and the client MUST close the connection. DisposeTcpClient(); throw new NotSupportedException("Tor's SOCKS5 proxy does not support any of the client's authentication methods."); } else if (methodSelection.Method == MethodField.UsernamePassword) { // https://tools.ietf.org/html/rfc1929#section-2 // Once the SOCKS V5 server has started, and the client has selected the // Username / Password Authentication protocol, the Username / Password // sub-negotiation begins. This begins with the client producing a // Username / Password request: var identity = RandomString.CapitalAlphaNumeric(21); var uName = new UNameField(uName: identity); var passwd = new PasswdField(passwd: identity); var usernamePasswordRequest = new UsernamePasswordRequest(uName, passwd); sendBuffer = usernamePasswordRequest.ToBytes(); Array.Clear(receiveBuffer, 0, receiveBuffer.Length); receiveBuffer = await SendAsync(sendBuffer, receiveBufferSize : 2, cancellationToken).ConfigureAwait(false); var userNamePasswordResponse = new UsernamePasswordResponse(receiveBuffer); if (userNamePasswordResponse.Ver != usernamePasswordRequest.Ver) { throw new NotSupportedException($"Authentication version {userNamePasswordResponse.Ver.Value} not supported. Only version {usernamePasswordRequest.Ver} is supported."); } if (!userNamePasswordResponse.Status.IsSuccess()) // Tor authentication is different, this will never happen; { // https://tools.ietf.org/html/rfc1929#section-2 // A STATUS field of X'00' indicates success. If the server returns a // `failure' (STATUS value other than X'00') status, it MUST close the // connection. DisposeTcpClient(); throw new InvalidOperationException("Wrong username and/or password."); } } Logger.LogDebug("<"); }