From be56b493db0447cac6bb3ad1a1860b7a32fba4d2 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 2 Oct 2026 17:17:04 -0500 Subject: [PATCH 1/2] fix(tor): send proxied hostnames to the SOCKS5 proxy unresolved socks5_proxy 1.0.3+dev.3 resolves the target host with the system DNS before issuing the SOCKS5 CONNECT, so every request made through the Tor proxy leaked its hostname to the local resolver and handed Tor an IP address instead of a domain. It also meant .onion hosts could not be reached through HttpClient at all. Upstream removed the local lookup in 1.0.5+dev.1 and fixed domain serialization in 2.1.1, so the CONNECT now carries the hostname (address type 3) and Tor does the resolution. The 2.x API is unchanged for everything Stack uses (assignToHttpClient and ProxySettings). --- pubspec.lock | 4 ++-- scripts/app_config/templates/pubspec.template.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 750c80fcd..464706030 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2135,10 +2135,10 @@ packages: dependency: "direct main" description: name: socks5_proxy - sha256: e0cba6917cd374de6f6cb0ce081e50e6efc24c61644b8e9f20c8bf8b91bb0b75 + sha256: "80fa31a9ebfc0dc8de7b0e568c8d8927b65558ef2c7591cbee5afac814fb8f74" url: "https://pub.dev" source: hosted - version: "1.0.3+dev.3" + version: "2.1.1" socks_socket: dependency: transitive description: diff --git a/scripts/app_config/templates/pubspec.template.yaml b/scripts/app_config/templates/pubspec.template.yaml index 12a7efaa3..067b50ede 100644 --- a/scripts/app_config/templates/pubspec.template.yaml +++ b/scripts/app_config/templates/pubspec.template.yaml @@ -216,7 +216,7 @@ dependencies: git: url: https://github.com/cypherstack/tezart.git ref: 84c563104f1a19c26e49bafccb7da404b210b666 - socks5_proxy: 1.0.3+dev.3 + socks5_proxy: ^2.1.1 convert: ^3.1.1 flutter_hooks: ^0.20.3 meta: ^1.9.1 From 1dd116fbda8f7d9770566748bff284698c1281e4 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 2 Oct 2026 17:17:04 -0500 Subject: [PATCH 2/2] test(networking): cover hostname pass-through for proxied requests Run the HTTP wrapper against a local fake SOCKS5 server and assert the CONNECT carries the domain name, including for .onion hosts, rather than a locally resolved address. --- test/networking/http_socks_proxy_test.dart | 82 ++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 test/networking/http_socks_proxy_test.dart diff --git a/test/networking/http_socks_proxy_test.dart b/test/networking/http_socks_proxy_test.dart new file mode 100644 index 000000000..62d259643 --- /dev/null +++ b/test/networking/http_socks_proxy_test.dart @@ -0,0 +1,82 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/networking/http.dart'; + +// Minimal SOCKS5 server that records the CONNECT target and answers any +// request with a canned HTTP 200. +class _FakeSocksServer { + late final ServerSocket _server; + int? addressType; + String? target; + int? port; + + int get listeningPort => _server.port; + + Future start() async { + _server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + _server.listen((socket) { + var stage = 0; + socket.listen((Uint8List bytes) { + switch (stage) { + case 0: + socket.add([0x05, 0x00]); + stage = 1; + case 1: + addressType = bytes[3]; + if (addressType == 0x03) { + target = String.fromCharCodes(bytes.sublist(5, 5 + bytes[4])); + } else { + target = bytes.sublist(4, bytes.length - 2).join('.'); + } + port = (bytes[bytes.length - 2] << 8) | bytes[bytes.length - 1]; + socket.add([0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0]); + stage = 2; + default: + socket.write( + 'HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok', + ); + socket.close(); + } + }); + }); + } + + Future stop() => _server.close(); +} + +void main() { + late _FakeSocksServer socks; + + setUp(() async { + socks = _FakeSocksServer(); + await socks.start(); + }); + + tearDown(() => socks.stop()); + + Future get(String host) => const HTTP().get( + url: Uri.http(host, '/'), + proxyInfo: (host: InternetAddress.loopbackIPv4, port: socks.listeningPort), + ); + + test('proxied request sends the hostname to the SOCKS5 proxy', () async { + final response = await get('example.invalid'); + + expect(response.code, 200); + expect(socks.addressType, 0x03); + expect(socks.target, 'example.invalid'); + expect(socks.port, 80); + }); + + test('proxied request can target an onion address', () async { + const onion = + 'trocadorfyhlu27aefre5u7zri66gudtzdyelymftvr4yjwcxhfaqsid.onion'; + final response = await get(onion); + + expect(response.code, 200); + expect(socks.addressType, 0x03); + expect(socks.target, onion); + }); +}