From c47f81e8ac110b1d7f78bce0232e8015366d13e7 Mon Sep 17 00:00:00 2001 From: laanwj <126646+laanwj@users.noreply.github.com> Date: Mon, 31 Mar 2025 16:18:07 +0200 Subject: [PATCH 1/2] net: Rename `_randomize_credentials` Proxy parameter to `tor_stream_isolation` Rename the `_randomize_credentials` parameter to Proxy's constructor to `tor_stream_isolation` to make it more clear, and more specific what its purpose is. Also change all call sites to use a named parameter. --- src/init.cpp | 8 ++++---- src/netbase.cpp | 2 +- src/netbase.h | 8 ++++---- src/qt/optionsdialog.cpp | 2 +- src/rpc/net.cpp | 2 +- src/torcontrol.cpp | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) 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 eaca5a16c1..61e4c8eb3e 100644 --- a/src/netbase.cpp +++ b/src/netbase.cpp @@ -738,7 +738,7 @@ std::unique_ptr ConnectThroughProxy(const Proxy& proxy, } // do socks negotiation - if (proxy.m_randomize_credentials) { + if (proxy.m_tor_stream_isolation) { ProxyCredentials random_auth; static std::atomic_int counter(0); random_auth.username = random_auth.password = strprintf("%i", counter++); 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 2cea1bf6ab..3c780b8e5c 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -486,7 +486,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/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"); From ec81a72b369ab9efe23681ebb6e8fab34ce2e0f2 Mon Sep 17 00:00:00 2001 From: laanwj <126646+laanwj@users.noreply.github.com> Date: Mon, 31 Mar 2025 17:42:00 +0200 Subject: [PATCH 2/2] net: Add randomized prefix to Tor stream isolation credentials Add a class TorsStreamIsolationCredentialsGenerator that generates unique credentials based on a randomly generated session prefix and an atomic counter. This makes sure that different launches of the application won't share the same credentials, and thus circuits, even in edge cases. Example with `-debug=proxy`: ``` 2025-03-31T16:30:27Z [proxy] SOCKS5 sending proxy authentication 0afb2da441f5c105-0:0afb2da441f5c105-0 2025-03-31T16:30:31Z [proxy] SOCKS5 sending proxy authentication 0afb2da441f5c105-1:0afb2da441f5c105-1 ``` Thanks to hodlinator for the idea. --- src/netbase.cpp | 42 +++++++++++++++++++++++++++++++++++++++--- src/test/fuzz/i2p.cpp | 2 +- src/test/i2p_tests.cpp | 6 +++--- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/netbase.cpp b/src/netbase.cpp index 61e4c8eb3e..1edcd69455 100644 --- a/src/netbase.cpp +++ b/src/netbase.cpp @@ -725,6 +725,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, @@ -739,9 +776,8 @@ std::unique_ptr ConnectThroughProxy(const Proxy& proxy, // do socks negotiation if (proxy.m_tor_stream_isolation) { - ProxyCredentials random_auth; - static std::atomic_int counter(0); - random_auth.username = random_auth.password = strprintf("%i", counter++); + static TorStreamIsolationCredentialsGenerator generator; + ProxyCredentials random_auth{generator.Generate()}; if (!Socks5(dest, port, &random_auth, *sock)) { return {}; } 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); {