diff --git a/src/init.cpp b/src/init.cpp index f35a547c92..a151c9d0a9 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -1578,14 +1578,14 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) if (proxyArg != "" && proxyArg != "0") { Proxy addrProxy; if (IsUnixSocketPath(proxyArg)) { - addrProxy = Proxy(proxyArg, proxyRandomize); + addrProxy = Proxy(proxyArg, /*tor_stream_isolation=*/proxyRandomize); } else { const std::optional proxyAddr{Lookup(proxyArg, 9050, fNameLookup)}; if (!proxyAddr.has_value()) { return InitError(strprintf(_("Invalid -proxy address or hostname: '%s'"), proxyArg)); } - addrProxy = Proxy(proxyAddr.value(), proxyRandomize); + addrProxy = Proxy(proxyAddr.value(), /*tor_stream_isolation=*/proxyRandomize); } if (!addrProxy.IsValid()) @@ -1614,14 +1614,14 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) } } else { if (IsUnixSocketPath(onionArg)) { - onion_proxy = Proxy(onionArg, proxyRandomize); + onion_proxy = Proxy(onionArg, /*tor_stream_isolation=*/proxyRandomize); } else { const std::optional addr{Lookup(onionArg, 9050, fNameLookup)}; if (!addr.has_value() || !addr->IsValid()) { return InitError(strprintf(_("Invalid -onion address or hostname: '%s'"), onionArg)); } - onion_proxy = Proxy(addr.value(), proxyRandomize); + onion_proxy = Proxy(addr.value(), /*tor_stream_isolation=*/proxyRandomize); } } } diff --git a/src/netbase.cpp b/src/netbase.cpp index ecd5329b78..a5da1c9dce 100644 --- a/src/netbase.cpp +++ b/src/netbase.cpp @@ -749,6 +749,43 @@ bool IsProxy(const CNetAddr &addr) { return false; } +/** + * Generate unique credentials for Tor stream isolation. Tor will create + * separate circuits for SOCKS5 proxy connections with different credentials, which + * makes it harder to correlate the connections. + */ +class TorStreamIsolationCredentialsGenerator +{ +public: + TorStreamIsolationCredentialsGenerator(): + m_prefix(GenerateUniquePrefix()) { + } + + /** Return the next unique proxy credentials. */ + ProxyCredentials Generate() { + ProxyCredentials auth; + auth.username = auth.password = strprintf("%s%i", m_prefix, m_counter); + ++m_counter; + return auth; + } + + /** Size of session prefix in bytes. */ + static constexpr size_t PREFIX_BYTE_LENGTH = 8; +private: + const std::string m_prefix; + std::atomic m_counter; + + /** Generate a random prefix for each of the credentials returned by this generator. + * This makes sure that different launches of the application (either successively or in parallel) + * will not share the same circuits, as would be the case with a bare counter. + */ + static std::string GenerateUniquePrefix() { + std::array prefix_bytes; + GetRandBytes(prefix_bytes); + return HexStr(prefix_bytes) + "-"; + } +}; + std::unique_ptr ConnectThroughProxy(const Proxy& proxy, const std::string& dest, uint16_t port, @@ -762,10 +799,9 @@ std::unique_ptr ConnectThroughProxy(const Proxy& proxy, } // do socks negotiation - if (proxy.m_randomize_credentials) { - ProxyCredentials random_auth; - static std::atomic_int counter(0); - random_auth.username = random_auth.password = strprintf("%i", counter++); + if (proxy.m_tor_stream_isolation) { + static TorStreamIsolationCredentialsGenerator generator; + ProxyCredentials random_auth{generator.Generate()}; if (!Socks5(dest, port, &random_auth, *sock)) { return {}; } diff --git a/src/netbase.h b/src/netbase.h index d7c6e1c215..3ac2305011 100644 --- a/src/netbase.h +++ b/src/netbase.h @@ -58,14 +58,14 @@ bool IsUnixSocketPath(const std::string& name); class Proxy { public: - Proxy() : m_is_unix_socket(false), m_randomize_credentials(false) {} - explicit Proxy(const CService& _proxy, bool _randomize_credentials = false) : proxy(_proxy), m_is_unix_socket(false), m_randomize_credentials(_randomize_credentials) {} - explicit Proxy(const std::string path, bool _randomize_credentials = false) : m_unix_socket_path(path), m_is_unix_socket(true), m_randomize_credentials(_randomize_credentials) {} + Proxy() : m_is_unix_socket(false), m_tor_stream_isolation(false) {} + explicit Proxy(const CService& _proxy, bool tor_stream_isolation = false) : proxy(_proxy), m_is_unix_socket(false), m_tor_stream_isolation(tor_stream_isolation) {} + explicit Proxy(const std::string path, bool tor_stream_isolation = false) : m_unix_socket_path(path), m_is_unix_socket(true), m_tor_stream_isolation(tor_stream_isolation) {} CService proxy; std::string m_unix_socket_path; bool m_is_unix_socket; - bool m_randomize_credentials; + bool m_tor_stream_isolation; bool IsValid() const { diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp index fd81abb49d..0a0e44f6f5 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -482,7 +482,7 @@ QValidator::State ProxyAddressValidator::validate(QString &input, int &pos) cons if (!SplitHostPort(input.toStdString(), port, hostname) || port != 0) return QValidator::Invalid; CService serv(LookupNumeric(input.toStdString(), DEFAULT_GUI_PROXY_PORT)); - Proxy addrProxy = Proxy(serv, true); + Proxy addrProxy = Proxy(serv, /*tor_stream_isolation=*/true); if (addrProxy.IsValid()) return QValidator::Acceptable; diff --git a/src/rpc/net.cpp b/src/rpc/net.cpp index bda07365e0..3a5b8abdf2 100644 --- a/src/rpc/net.cpp +++ b/src/rpc/net.cpp @@ -609,7 +609,7 @@ static UniValue GetNetworksInfo() obj.pushKV("limited", !g_reachable_nets.Contains(network)); obj.pushKV("reachable", g_reachable_nets.Contains(network)); obj.pushKV("proxy", proxy.IsValid() ? proxy.ToString() : std::string()); - obj.pushKV("proxy_randomize_credentials", proxy.m_randomize_credentials); + obj.pushKV("proxy_randomize_credentials", proxy.m_tor_stream_isolation); networks.push_back(std::move(obj)); } return networks; diff --git a/src/test/fuzz/i2p.cpp b/src/test/fuzz/i2p.cpp index b8024f7b1c..29a11123d9 100644 --- a/src/test/fuzz/i2p.cpp +++ b/src/test/fuzz/i2p.cpp @@ -34,7 +34,7 @@ FUZZ_TARGET(i2p, .init = initialize_i2p) const fs::path private_key_path = gArgs.GetDataDirNet() / "fuzzed_i2p_private_key"; const CService addr{in6_addr(IN6ADDR_LOOPBACK_INIT), 7656}; - const Proxy sam_proxy{addr, false}; + const Proxy sam_proxy{addr, /*tor_stream_isolation=*/false}; CThreadInterrupt interrupt; i2p::sam::Session session{private_key_path, sam_proxy, &interrupt}; diff --git a/src/test/i2p_tests.cpp b/src/test/i2p_tests.cpp index bb9ca88019..bdb3408b66 100644 --- a/src/test/i2p_tests.cpp +++ b/src/test/i2p_tests.cpp @@ -52,7 +52,7 @@ BOOST_AUTO_TEST_CASE(unlimited_recv) CThreadInterrupt interrupt; const std::optional addr{Lookup("127.0.0.1", 9000, false)}; - const Proxy sam_proxy(addr.value(), false); + const Proxy sam_proxy(addr.value(), /*tor_stream_isolation=*/false); i2p::sam::Session session(gArgs.GetDataDirNet() / "test_i2p_private_key", sam_proxy, &interrupt); { @@ -114,7 +114,7 @@ BOOST_AUTO_TEST_CASE(listen_ok_accept_fail) CThreadInterrupt interrupt; const CService addr{in6_addr(IN6ADDR_LOOPBACK_INIT), /*port=*/7656}; - const Proxy sam_proxy(addr, false); + const Proxy sam_proxy(addr, /*tor_stream_isolation=*/false); i2p::sam::Session session(gArgs.GetDataDirNet() / "test_i2p_private_key", sam_proxy, &interrupt); @@ -157,7 +157,7 @@ BOOST_AUTO_TEST_CASE(damaged_private_key) CThreadInterrupt interrupt; const CService addr{in6_addr(IN6ADDR_LOOPBACK_INIT), /*port=*/7656}; - const Proxy sam_proxy{addr, false}; + const Proxy sam_proxy{addr, /*tor_stream_isolation=*/false}; i2p::sam::Session session(i2p_private_key_file, sam_proxy, &interrupt); { diff --git a/src/torcontrol.cpp b/src/torcontrol.cpp index 5b8f5c74fe..2b1df4f138 100644 --- a/src/torcontrol.cpp +++ b/src/torcontrol.cpp @@ -407,7 +407,7 @@ void TorController::get_socks_cb(TorControlConnection& _conn, const TorControlRe // With m_randomize_credentials = true, generates unique SOCKS credentials per proxy connection (e.g., Tor). // Prevents connection correlation and enhances privacy by forcing different Tor circuits. // Requires Tor's IsolateSOCKSAuth (default enabled) for effective isolation (see IsolateSOCKSAuth section in https://2019.www.torproject.org/docs/tor-manual.html.en). - Proxy addrOnion = Proxy(resolved, /*_randomize_credentials=*/ true); + Proxy addrOnion = Proxy(resolved, /*tor_stream_isolation=*/ true); SetProxy(NET_ONION, addrOnion); const auto onlynets = gArgs.GetArgs("-onlynet");