From 6b71d35e5e1398a280b666bbb2b657155c162cc4 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Mon, 29 Sep 2025 11:11:32 +0200 Subject: [PATCH] reorganize tor package --- packages/tor/analysis_options.yaml | 2 +- packages/tor/ffigen.yaml | 23 -- packages/tor/lib/socks_socket.dart | 376 ------------------ .../tor/lib/{ => src}/generated_bindings.dart | 0 packages/tor/lib/src/tor.dart | 325 +++++++++++++++ packages/tor/lib/tor.dart | 326 +-------------- packages/tor/lib/util.dart | 14 - packages/tor/pubspec.yaml | 2 +- 8 files changed, 328 insertions(+), 740 deletions(-) delete mode 100644 packages/tor/ffigen.yaml delete mode 100644 packages/tor/lib/socks_socket.dart rename packages/tor/lib/{ => src}/generated_bindings.dart (100%) create mode 100644 packages/tor/lib/src/tor.dart delete mode 100644 packages/tor/lib/util.dart diff --git a/packages/tor/analysis_options.yaml b/packages/tor/analysis_options.yaml index 8da17cd8..f27bfdc0 100644 --- a/packages/tor/analysis_options.yaml +++ b/packages/tor/analysis_options.yaml @@ -31,7 +31,7 @@ analyzer: - "**.swagger.dart" - "**.freezed.dart" - "**.chopper.dart" - - "lib/generated_bindings.dart" + - "lib/src/generated_bindings.dart" # For more information about the core and recommended set of lints, see # https://dart.dev/go/core-lints diff --git a/packages/tor/ffigen.yaml b/packages/tor/ffigen.yaml deleted file mode 100644 index 62108b01..00000000 --- a/packages/tor/ffigen.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# SPDX-FileCopyrightText: 2024 Foundation Devices Inc. -# -# SPDX-License-Identifier: MIT - -# Run with `flutter pub run ffigen --config ffigen.yaml`. -name: NativeLibrary -description: | - Bindings for `tor.h`. - - Regenerate bindings with `flutter pub run ffigen --config ffigen.yaml`. -output: 'lib/tor_bindings_generated.dart' -headers: - entry-points: - - 'rust/target/tor.h' - include-directives: - - 'rust/target/tor.h' -preamble: | - // ignore_for_file: always_specify_types - // ignore_for_file: camel_case_types - // ignore_for_file: non_constant_identifier_names -comments: - style: any - length: full diff --git a/packages/tor/lib/socks_socket.dart b/packages/tor/lib/socks_socket.dart deleted file mode 100644 index 93d0b9f4..00000000 --- a/packages/tor/lib/socks_socket.dart +++ /dev/null @@ -1,376 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Cypher Stack LLC -// -// SPDX-License-Identifier: MIT - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:flutter/foundation.dart'; - -/// A SOCKS5 socket. -/// -/// A Dart 3 Socket wrapper that implements the SOCKS5 protocol. Now with SSL! -/// -/// Properties: -/// - [proxyHost]: The host of the SOCKS5 proxy server. -/// - [proxyPort]: The port of the SOCKS5 proxy server. -/// - [_socksSocket]: The underlying Socket that connects to the SOCKS5 proxy -/// server. -/// - [_responseController]: A StreamController that listens to the -/// [_socksSocket] and broadcasts the response. -/// -/// Methods: -/// - connect: Connects to the SOCKS5 proxy server. -/// - connectTo: Connects to the specified [domain] and [port] through the -/// SOCKS5 proxy server. -/// - write: Converts [object] to a String by invoking [Object.toString] and -/// sends the encoding of the result to the socket. -/// - sendServerFeaturesCommand: Sends the server.features command to the -/// proxy server. -/// - close: Closes the connection to the Tor proxy. -/// -/// Usage: -/// ```dart -/// // Instantiate a socks socket at localhost and on the port selected by the -/// // tor service. -/// var socksSocket = await SOCKSSocket.create( -/// proxyHost: InternetAddress.loopbackIPv4.address, -/// proxyPort: tor.port, -/// // sslEnabled: true, // For SSL connections. -/// ); -/// -/// // Connect to the socks instantiated above. -/// await socksSocket.connect(); -/// -/// // Connect to bitcoincash.stackwallet.com on port 50001 via socks socket. -/// await socksSocket.connectTo( -/// 'bitcoincash.stackwallet.com', 50001); -/// -/// // Send a server features command to the connected socket, see method for -/// // more specific usage example.. -/// await socksSocket.sendServerFeaturesCommand(); -/// await socksSocket.close(); -/// ``` -/// -/// See also: -/// - SOCKS5 protocol(https://www.ietf.org/rfc/rfc1928.txt) -class SOCKSSocket { - /// The host of the SOCKS5 proxy server. - final String proxyHost; - - /// The port of the SOCKS5 proxy server. - final int proxyPort; - - /// The underlying Socket that connects to the SOCKS5 proxy server. - late final Socket _socksSocket; - - /// Getter for the underlying Socket that connects to the SOCKS5 proxy server. - Socket get socket => sslEnabled ? _secureSocksSocket : _socksSocket; - - /// A wrapper around the _socksSocket that enables SSL connections. - late final Socket _secureSocksSocket; - - /// A StreamController that listens to the _socksSocket and broadcasts. - final StreamController> _responseController = - StreamController.broadcast(); - - /// A StreamController that listens to the _secureSocksSocket and broadcasts. - final StreamController> _secureResponseController = - StreamController.broadcast(); - - /// Getter for the StreamController that listens to the _socksSocket and - /// broadcasts, or the _secureSocksSocket and broadcasts if SSL is enabled. - StreamController> get responseController => - sslEnabled ? _secureResponseController : _responseController; - - /// A StreamSubscription that listens to the _socksSocket or the - /// _secureSocksSocket if SSL is enabled. - StreamSubscription>? _subscription; - - /// Getter for the StreamSubscription that listens to the _socksSocket or the - /// _secureSocksSocket if SSL is enabled. - StreamSubscription>? get subscription => _subscription; - - /// Is SSL enabled? - final bool sslEnabled; - - /// Constructor. - SOCKSSocket({ - required this.proxyHost, - required this.proxyPort, - required this.sslEnabled, - }) { - unawaited(_init()); - } - - /// Private constructor. - SOCKSSocket._(this.proxyHost, this.proxyPort, this.sslEnabled); - - /// Provides a stream of data as List``. - Stream> get inputStream => sslEnabled - ? _secureResponseController.stream - : _responseController.stream; - - /// Provides a StreamSink compatible with List`` for sending data. - StreamSink> get outputStream { - // Create a simple StreamSink wrapper for _socksSocket and - // _secureSocksSocket that accepts List and forwards it to write method. - final sink = StreamController>(); - sink.stream.listen((data) { - if (sslEnabled) { - _secureSocksSocket.add(data); - } else { - _socksSocket.add(data); - } - }); - return sink.sink; - } - - /// Creates a SOCKS5 socket to the specified [proxyHost] and [proxyPort]. - /// - /// This method is a factory constructor that returns a Future that resolves - /// to a SOCKSSocket instance. - /// - /// Parameters: - /// - [proxyHost]: The host of the SOCKS5 proxy server. - /// - [proxyPort]: The port of the SOCKS5 proxy server. - /// - /// Returns: - /// A Future that resolves to a SOCKSSocket instance. - static Future create({ - required String proxyHost, - required int proxyPort, - bool sslEnabled = false, - }) async { - // Create a SOCKS socket instance. - final instance = SOCKSSocket._(proxyHost, proxyPort, sslEnabled); - - // Initialize the SOCKS socket. - await instance._init(); - - // Return the SOCKS socket instance. - return instance; - } - - /// Initializes the SOCKS socket. - /// - /// This method is a private method that is called by the constructor. - /// - /// Returns: - /// A Future that resolves to void. - Future _init() async { - // Connect to the SOCKS proxy server. - _socksSocket = await Socket.connect(proxyHost, proxyPort); - - // Listen to the socket. - _subscription = _socksSocket.listen( - (data) { - // Add the data to the response controller. - _responseController.add(data); - }, - onError: (e) { - // Handle errors. - if (e is Object) { - _responseController.addError(e); - } - - // If the error is not an object, send the error as a string. - _responseController.addError("$e"); - // TODO make sure sending error as string is acceptable. - }, - onDone: () { - // Close the response controller when the socket is closed. - // _responseController.close(); - }, - ); - } - - /// Connects to the SOCKS socket. - /// - /// Returns: - /// A Future that resolves to void. - Future connect() async { - // Greeting and method selection. - _socksSocket.add([0x05, 0x01, 0x00]); - - // Wait for server response. - final response = await _responseController.stream.first; - - // Check if the connection was successful. - if (response[1] != 0x00) { - throw Exception( - 'socks_socket.connect(): Failed to connect to SOCKS5 proxy.', - ); - } - - return; - } - - /// Connects to the specified [domain] and [port] through the SOCKS socket. - /// - /// Parameters: - /// - [domain]: The domain to connect to. - /// - [port]: The port to connect to. - /// - /// Returns: - /// A Future that resolves to void. - Future connectTo(String domain, int port) async { - // Connect command. - final request = [ - 0x05, // SOCKS version. - 0x01, // Connect command. - 0x00, // Reserved. - 0x03, // Domain name. - domain.length, - ...domain.codeUnits, - (port >> 8) & 0xFF, - port & 0xFF, - ]; - - // Send the connect command to the SOCKS proxy server. - _socksSocket.add(request); - - // Wait for server response. - final response = await _responseController.stream.first; - - // Check if the connection was successful. - if (response[1] != 0x00) { - throw Exception( - 'socks_socket.connectTo(): Failed to connect to target through SOCKS5 proxy.', - ); - } - - // Upgrade to SSL if needed. - if (sslEnabled) { - // Upgrade to SSL. - _secureSocksSocket = await SecureSocket.secure( - _socksSocket, - host: domain, - // onBadCertificate: (_) => true, // Uncomment this to bypass certificate validation (NOT recommended for production). - ); - - // Listen to the secure socket. - _subscription = _secureSocksSocket.listen( - (data) { - // Add the data to the response controller. - _secureResponseController.add(data); - }, - onError: (e) { - // Handle errors. - if (e is Object) { - _secureResponseController.addError(e); - } - - // If the error is not an object, send the error as a string. - _secureResponseController.addError("$e"); - // TODO make sure sending error as string is acceptable. - }, - onDone: () async { - // Close the response controller when the socket is closed. - await _secureResponseController.close(); - }, - ); - } - - return; - } - - /// Converts [object] to a String by invoking [Object.toString] and - /// sends the encoding of the result to the socket. - /// - /// Parameters: - /// - [object]: The object to write to the socket. - /// - /// Returns: - /// A Future that resolves to void. - void write(Object? object) { - // Don't write null. - if (object == null) return; - - // Write the data to the socket. - final List data = utf8.encode(object.toString()); - if (sslEnabled) { - _secureSocksSocket.add(data); - } else { - _socksSocket.add(data); - } - } - - /// Closes the connection to the Tor proxy. - /// - /// Returns: - /// A Future that resolves to void. - Future close() async { - // Ensure all data is sent before closing. - try { - if (sslEnabled) { - await _secureSocksSocket.flush(); - } - await _socksSocket.flush(); - } finally { - await _subscription?.cancel(); - await _socksSocket.close(); - await _responseController.close(); - if (sslEnabled) { - await _secureResponseController.close(); - } - } - } - - StreamSubscription> listen( - void Function(List data)? onData, { - Function? onError, - void Function()? onDone, - bool? cancelOnError, - }) { - return sslEnabled - ? _secureResponseController.stream.listen( - onData, - onError: onError, - onDone: onDone, - cancelOnError: cancelOnError, - ) - : _responseController.stream.listen( - onData, - onError: onError, - onDone: onDone, - cancelOnError: cancelOnError, - ); - } - - /// Sends the server.features command to the proxy server. - /// - /// This demos how to send the server.features command. Use as an example - /// for sending other commands. - /// - /// Returns: - /// A Future that resolves to void. - Future sendServerFeaturesCommand() async { - // The server.features command. - const String command = - '{"jsonrpc":"2.0","id":"0","method":"server.features","params":[]}'; - - if (!sslEnabled) { - // Send the command to the proxy server. - _socksSocket.writeln(command); - - // Wait for the response from the proxy server. - final responseData = await _responseController.stream.first; - if (kDebugMode) { - print("responseData: ${utf8.decode(responseData)}"); - } - } else { - // Send the command to the proxy server. - _secureSocksSocket.writeln(command); - - // Wait for the response from the proxy server. - final responseData = await _secureResponseController.stream.first; - if (kDebugMode) { - print("secure responseData: ${utf8.decode(responseData)}"); - } - } - - return; - } -} diff --git a/packages/tor/lib/generated_bindings.dart b/packages/tor/lib/src/generated_bindings.dart similarity index 100% rename from packages/tor/lib/generated_bindings.dart rename to packages/tor/lib/src/generated_bindings.dart diff --git a/packages/tor/lib/src/tor.dart b/packages/tor/lib/src/tor.dart new file mode 100644 index 00000000..6f0669d9 --- /dev/null +++ b/packages/tor/lib/src/tor.dart @@ -0,0 +1,325 @@ +// SPDX-FileCopyrightText: 2022 Foundation Devices Inc. +// SPDX-FileCopyrightText: 2024 Foundation Devices Inc. +// +// SPDX-License-Identifier: MIT + +import 'dart:async'; +import 'dart:ffi'; +import 'dart:io'; +import 'dart:isolate'; +import 'dart:math'; + +import 'package:ffi/ffi.dart'; +import 'package:flutter/foundation.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:tor/src/generated_bindings.dart' as rust; + +DynamicLibrary load(String name) { + if (Platform.isAndroid || Platform.isLinux) { + return DynamicLibrary.open('lib$name.so'); + } else if (Platform.isIOS || Platform.isMacOS) { + return DynamicLibrary.open('$name.framework/$name'); + } else if (Platform.isWindows) { + return DynamicLibrary.open('$name.dll'); + } else { + throw NotSupportedPlatform('${Platform.operatingSystem} is not supported!'); + } +} + +class CouldntBootstrapDirectory implements Exception { + String? rustError; + + CouldntBootstrapDirectory({this.rustError}); +} + +class NotSupportedPlatform implements Exception { + String reason; + + NotSupportedPlatform(this.reason); +} + +class ClientNotActive implements Exception {} + +class Tor { + static const String libName = "tor"; + static late DynamicLibrary _lib; + + Pointer _clientPtr = nullptr; + Pointer _proxyPtr = nullptr; + + /// Flag to indicate that Tor client and proxy have started. Traffic is routed through the proxy only if it is also [enabled]. + bool get started => _proxyPort > -1; + + bool get hasClient => _clientPtr != nullptr; + + /// Flag to indicate that traffic should flow through the proxy. + bool _enabled = false; + + /// Getter for the enabled flag. + bool get enabled => _enabled; + + /// Flag to indicate that a Tor circuit is thought to have been established + /// (true means that Tor has bootstrapped). + bool get bootstrapped => _bootstrapped; + + /// Getter for the bootstrapped flag. + bool _bootstrapped = false; + + /// A stream of Tor events. + /// + /// This stream broadcast just the port for now (-1 if circuit not established or proxy not enabled) + final StreamController events = StreamController.broadcast(); + + /// Getter for the proxy port. + /// + /// Returns -1 if Tor is not enabled or if the circuit is not established. + /// + /// Returns the proxy port if Tor is enabled and the circuit is established. + /// + /// This is the port that should be used for all requests. + int get port { + if (!_enabled) { + return -1; + } + return _proxyPort; + } + + /// The proxy port. + int _proxyPort = -1; + + /// Singleton instance of the Tor class. + static final Tor _instance = Tor._internal(); + + /// Getter for the singleton instance of the Tor class. + static Tor get instance => _instance; + + /// Initialize the Tor ffi lib instance if it hasn't already been set. Nothing + /// changes if _tor is already been set. + /// + /// Returns a Future that completes when the Tor service has started. + /// + /// Throws an exception if the Tor service fails to start. + static Tor init({bool enabled = true}) { + final singleton = Tor._instance; + singleton._enabled = enabled; + return singleton; + } + + /// Private constructor for the Tor class. + Tor._internal() { + _lib = load(libName); + + if (kDebugMode) { + print("Instance of Tor created!"); + } + } + + void broadcastState() { + events.add(port); + } + + Future _getRandomUnusedPort({List excluded = const []}) async { + final random = Random.secure(); + int potentialPort = 0; + + retry: + while (potentialPort <= 0 || excluded.contains(potentialPort)) { + potentialPort = random.nextInt(65535); + try { + final socket = await ServerSocket.bind("0.0.0.0", potentialPort); + await socket.close(); + return potentialPort; + } catch (_) { + continue retry; + } + } + + return -1; + } + + /// Start the Tor service. + /// + /// This will start the Tor service and establish a Tor circuit. + /// + /// Throws an exception if the Tor service fails to start. + /// + /// Returns a Future that completes when the Tor service has started. + Future start({ + int? obfs4Port, + int? snowflakePort, + String? bridgeLines, + }) async { + broadcastState(); + + // Set the state and cache directories. + final Directory appSupportDir = await getApplicationSupportDirectory(); + final stateDir = await Directory( + '${appSupportDir.path}/tor_state', + ).create(); + final cacheDir = await Directory( + '${appSupportDir.path}/tor_cache', + ).create(); + + // Generate a random port. + final newPort = await _getRandomUnusedPort(); + + // Start the Tor service in an isolate. + final tor = await Isolate.run(() { + // Load the Tor library. + final lib = rust.NativeLibrary(load(libName)); + + // Start the Tor service. + final tor = lib.tor_start( + newPort, + stateDir.path.toNativeUtf8() as Pointer, + cacheDir.path.toNativeUtf8() as Pointer, + obfs4Port ?? -1, + snowflakePort ?? -1, + bridgeLines?.toNativeUtf8() as Pointer? ?? nullptr, + ); + + // Throw an exception if the Tor service fails to start. + if (tor.client == nullptr) { + throwRustException(lib); + } + + return tor; + }); + + // Set the client pointer and started flag. + _clientPtr = Pointer.fromAddress(tor.client.address); + _proxyPtr = Pointer.fromAddress(tor.proxy.address); + + // Bootstrap the Tor service. + bootstrap(); + + // Set the proxy port. + _proxyPort = newPort; + broadcastState(); + } + + Future reconfigure({ + int? obfs4Port, + int? snowflakePort, + String? bridgeLines, + }) async { + final lib = rust.NativeLibrary(_lib); + + // Set the state and cache directories. + final Directory appSupportDir = await getApplicationSupportDirectory(); + final stateDir = await Directory( + '${appSupportDir.path}/tor_state', + ).create(); + final cacheDir = await Directory( + '${appSupportDir.path}/tor_cache', + ).create(); + + final reconfigured = lib.tor_reconfigure( + _clientPtr, + stateDir.path.toNativeUtf8() as Pointer, + cacheDir.path.toNativeUtf8() as Pointer, + obfs4Port ?? -1, + snowflakePort ?? -1, + bridgeLines?.toNativeUtf8() as Pointer? ?? nullptr, + ); + + if (!reconfigured) { + throwRustException(lib); + } + } + + /// Bootstrap the Tor service. + /// + /// This will bootstrap the Tor service and establish a Tor circuit. This + /// function should only be called after the Tor service has been started. + /// + /// This function will block until the Tor service has bootstrapped. + /// + /// Throws an exception if the Tor service fails to bootstrap. + /// + /// Returns void. + void bootstrap() { + // Load the Tor library. + final lib = rust.NativeLibrary(_lib); + + // Bootstrap the Tor service. + _bootstrapped = lib.tor_client_bootstrap(_clientPtr); + + // Throw an exception if the Tor service fails to bootstrap. + if (!bootstrapped) { + throwRustException(lib); + } + } + + /// Prevent traffic flowing through the proxy + void disable() { + stop(); + + _enabled = false; + broadcastState(); + } + + /// Stops the proxy + void stop() { + final lib = rust.NativeLibrary(_lib); + if (_proxyPtr != nullptr) { + lib.tor_proxy_stop(_proxyPtr); + _proxyPtr = nullptr; + + _bootstrapped = false; + _proxyPort = -1; + + broadcastState(); + } + } + + void setClientDormant(bool dormant) { + if (_clientPtr == nullptr || !started || !bootstrapped) { + throw ClientNotActive(); + } + + final lib = rust.NativeLibrary(_lib); + lib.tor_client_set_dormant(_clientPtr, dormant); + } + + Future isReady() async { + return await Future.doWhile( + () => Future.delayed(const Duration(seconds: 1)).then((_) { + // We are waiting and making absolutely no request unless: + // Tor is disabled + if (!enabled) { + return false; + } + + // ...or Tor circuit is established + if (bootstrapped) { + return false; + } + + // This way we avoid making clearnet req's while Tor is initialising + return true; + }), + ); + } + + static void throwRustException(rust.NativeLibrary lib) { + final String rustError = lib + .tor_last_error_message() + .cast() + .toDartString(); + + throw _getRustException(rustError); + } + + static Exception _getRustException(String rustError) { + if (rustError.contains('Unable to bootstrap a working directory')) { + return CouldntBootstrapDirectory(rustError: rustError); + } else { + return Exception(rustError); + } + } + + void hello() { + rust.NativeLibrary(_lib).tor_hello(); + } +} diff --git a/packages/tor/lib/tor.dart b/packages/tor/lib/tor.dart index 67f2474d..e30a2602 100644 --- a/packages/tor/lib/tor.dart +++ b/packages/tor/lib/tor.dart @@ -1,325 +1 @@ -// SPDX-FileCopyrightText: 2022 Foundation Devices Inc. -// SPDX-FileCopyrightText: 2024 Foundation Devices Inc. -// -// SPDX-License-Identifier: MIT - -import 'dart:async'; -import 'dart:ffi'; -import 'dart:io'; -import 'dart:isolate'; -import 'dart:math'; - -import 'package:ffi/ffi.dart'; -import 'package:flutter/foundation.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:tor/generated_bindings.dart' as rust; - -DynamicLibrary load(String name) { - if (Platform.isAndroid || Platform.isLinux) { - return DynamicLibrary.open('lib$name.so'); - } else if (Platform.isIOS || Platform.isMacOS) { - return DynamicLibrary.open('$name.framework/$name'); - } else if (Platform.isWindows) { - return DynamicLibrary.open('$name.dll'); - } else { - throw NotSupportedPlatform('${Platform.operatingSystem} is not supported!'); - } -} - -class CouldntBootstrapDirectory implements Exception { - String? rustError; - - CouldntBootstrapDirectory({this.rustError}); -} - -class NotSupportedPlatform implements Exception { - String reason; - - NotSupportedPlatform(this.reason); -} - -class ClientNotActive implements Exception {} - -class Tor { - static const String libName = "tor"; - static late DynamicLibrary _lib; - - Pointer _clientPtr = nullptr; - Pointer _proxyPtr = nullptr; - - /// Flag to indicate that Tor client and proxy have started. Traffic is routed through the proxy only if it is also [enabled]. - bool get started => _proxyPort > -1; - - bool get hasClient => _clientPtr != nullptr; - - /// Flag to indicate that traffic should flow through the proxy. - bool _enabled = false; - - /// Getter for the enabled flag. - bool get enabled => _enabled; - - /// Flag to indicate that a Tor circuit is thought to have been established - /// (true means that Tor has bootstrapped). - bool get bootstrapped => _bootstrapped; - - /// Getter for the bootstrapped flag. - bool _bootstrapped = false; - - /// A stream of Tor events. - /// - /// This stream broadcast just the port for now (-1 if circuit not established or proxy not enabled) - final StreamController events = StreamController.broadcast(); - - /// Getter for the proxy port. - /// - /// Returns -1 if Tor is not enabled or if the circuit is not established. - /// - /// Returns the proxy port if Tor is enabled and the circuit is established. - /// - /// This is the port that should be used for all requests. - int get port { - if (!_enabled) { - return -1; - } - return _proxyPort; - } - - /// The proxy port. - int _proxyPort = -1; - - /// Singleton instance of the Tor class. - static final Tor _instance = Tor._internal(); - - /// Getter for the singleton instance of the Tor class. - static Tor get instance => _instance; - - /// Initialize the Tor ffi lib instance if it hasn't already been set. Nothing - /// changes if _tor is already been set. - /// - /// Returns a Future that completes when the Tor service has started. - /// - /// Throws an exception if the Tor service fails to start. - static Tor init({bool enabled = true}) { - final singleton = Tor._instance; - singleton._enabled = enabled; - return singleton; - } - - /// Private constructor for the Tor class. - Tor._internal() { - _lib = load(libName); - - if (kDebugMode) { - print("Instance of Tor created!"); - } - } - - void broadcastState() { - events.add(port); - } - - Future _getRandomUnusedPort({List excluded = const []}) async { - final random = Random.secure(); - int potentialPort = 0; - - retry: - while (potentialPort <= 0 || excluded.contains(potentialPort)) { - potentialPort = random.nextInt(65535); - try { - final socket = await ServerSocket.bind("0.0.0.0", potentialPort); - await socket.close(); - return potentialPort; - } catch (_) { - continue retry; - } - } - - return -1; - } - - /// Start the Tor service. - /// - /// This will start the Tor service and establish a Tor circuit. - /// - /// Throws an exception if the Tor service fails to start. - /// - /// Returns a Future that completes when the Tor service has started. - Future start({ - int? obfs4Port, - int? snowflakePort, - String? bridgeLines, - }) async { - broadcastState(); - - // Set the state and cache directories. - final Directory appSupportDir = await getApplicationSupportDirectory(); - final stateDir = await Directory( - '${appSupportDir.path}/tor_state', - ).create(); - final cacheDir = await Directory( - '${appSupportDir.path}/tor_cache', - ).create(); - - // Generate a random port. - final newPort = await _getRandomUnusedPort(); - - // Start the Tor service in an isolate. - final tor = await Isolate.run(() { - // Load the Tor library. - final lib = rust.NativeLibrary(load(libName)); - - // Start the Tor service. - final tor = lib.tor_start( - newPort, - stateDir.path.toNativeUtf8() as Pointer, - cacheDir.path.toNativeUtf8() as Pointer, - obfs4Port ?? -1, - snowflakePort ?? -1, - bridgeLines?.toNativeUtf8() as Pointer? ?? nullptr, - ); - - // Throw an exception if the Tor service fails to start. - if (tor.client == nullptr) { - throwRustException(lib); - } - - return tor; - }); - - // Set the client pointer and started flag. - _clientPtr = Pointer.fromAddress(tor.client.address); - _proxyPtr = Pointer.fromAddress(tor.proxy.address); - - // Bootstrap the Tor service. - bootstrap(); - - // Set the proxy port. - _proxyPort = newPort; - broadcastState(); - } - - Future reconfigure({ - int? obfs4Port, - int? snowflakePort, - String? bridgeLines, - }) async { - final lib = rust.NativeLibrary(_lib); - - // Set the state and cache directories. - final Directory appSupportDir = await getApplicationSupportDirectory(); - final stateDir = await Directory( - '${appSupportDir.path}/tor_state', - ).create(); - final cacheDir = await Directory( - '${appSupportDir.path}/tor_cache', - ).create(); - - final reconfigured = lib.tor_reconfigure( - _clientPtr, - stateDir.path.toNativeUtf8() as Pointer, - cacheDir.path.toNativeUtf8() as Pointer, - obfs4Port ?? -1, - snowflakePort ?? -1, - bridgeLines?.toNativeUtf8() as Pointer? ?? nullptr, - ); - - if (!reconfigured) { - throwRustException(lib); - } - } - - /// Bootstrap the Tor service. - /// - /// This will bootstrap the Tor service and establish a Tor circuit. This - /// function should only be called after the Tor service has been started. - /// - /// This function will block until the Tor service has bootstrapped. - /// - /// Throws an exception if the Tor service fails to bootstrap. - /// - /// Returns void. - void bootstrap() { - // Load the Tor library. - final lib = rust.NativeLibrary(_lib); - - // Bootstrap the Tor service. - _bootstrapped = lib.tor_client_bootstrap(_clientPtr); - - // Throw an exception if the Tor service fails to bootstrap. - if (!bootstrapped) { - throwRustException(lib); - } - } - - /// Prevent traffic flowing through the proxy - void disable() { - stop(); - - _enabled = false; - broadcastState(); - } - - /// Stops the proxy - void stop() { - final lib = rust.NativeLibrary(_lib); - if (_proxyPtr != nullptr) { - lib.tor_proxy_stop(_proxyPtr); - _proxyPtr = nullptr; - - _bootstrapped = false; - _proxyPort = -1; - - broadcastState(); - } - } - - void setClientDormant(bool dormant) { - if (_clientPtr == nullptr || !started || !bootstrapped) { - throw ClientNotActive(); - } - - final lib = rust.NativeLibrary(_lib); - lib.tor_client_set_dormant(_clientPtr, dormant); - } - - Future isReady() async { - return await Future.doWhile( - () => Future.delayed(const Duration(seconds: 1)).then((_) { - // We are waiting and making absolutely no request unless: - // Tor is disabled - if (!enabled) { - return false; - } - - // ...or Tor circuit is established - if (bootstrapped) { - return false; - } - - // This way we avoid making clearnet req's while Tor is initialising - return true; - }), - ); - } - - static void throwRustException(rust.NativeLibrary lib) { - final String rustError = lib - .tor_last_error_message() - .cast() - .toDartString(); - - throw _getRustException(rustError); - } - - static Exception _getRustException(String rustError) { - if (rustError.contains('Unable to bootstrap a working directory')) { - return CouldntBootstrapDirectory(rustError: rustError); - } else { - return Exception(rustError); - } - } - - void hello() { - rust.NativeLibrary(_lib).tor_hello(); - } -} +export 'src/tor.dart'; diff --git a/packages/tor/lib/util.dart b/packages/tor/lib/util.dart deleted file mode 100644 index e6557a03..00000000 --- a/packages/tor/lib/util.dart +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Foundation Devices Inc. -// -// SPDX-License-Identifier: MIT - -import 'package:tor/generated_bindings.dart' as rust; -import 'package:tor/tor.dart'; - -int getNofileLimit() { - return rust.NativeLibrary(load(Tor.libName)).tor_get_nofile_limit(); -} - -int setNofileLimit(int limit) { - return rust.NativeLibrary(load(Tor.libName)).tor_set_nofile_limit(limit); -} diff --git a/packages/tor/pubspec.yaml b/packages/tor/pubspec.yaml index 5ba98593..a2158f95 100644 --- a/packages/tor/pubspec.yaml +++ b/packages/tor/pubspec.yaml @@ -33,7 +33,7 @@ dev_dependencies: lint: ^2.8.0 ffigen: - output: 'lib/generated_bindings.dart' + output: 'lib/src/generated_bindings.dart' llvm-path: - '/usr/lib/llvm-14/lib/libclang.so.1' functions: