From fb993f76047993b8de1b1bebef9ee1af759d59bc Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Thu, 19 Feb 2026 12:03:18 +0100 Subject: [PATCH 1/4] tor, fuzz: reuse constants instead of duplicating `src/torcontrol.cpp` used to define some constants that are used explicitly in `src/torcontrol.cpp` and implicitly in `src/test/fuzz/torcontrol.cpp` by duplicating their values. Move the constants to `src/torcontrol.h` and reuse them in `src/test/fuzz/torcontrol.cpp` to avoid duplication and magic numbers. --- src/test/fuzz/torcontrol.cpp | 4 ++-- src/torcontrol.cpp | 3 --- src/torcontrol.h | 4 ++++ 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/test/fuzz/torcontrol.cpp b/src/test/fuzz/torcontrol.cpp index 62c2ad57658..114bc524c20 100644 --- a/src/test/fuzz/torcontrol.cpp +++ b/src/test/fuzz/torcontrol.cpp @@ -49,10 +49,10 @@ FUZZ_TARGET(torcontrol, .init = initialize_torcontrol) CallOneOf( fuzzed_data_provider, [&] { - tor_control_reply.code = 250; + tor_control_reply.code = TOR_REPLY_OK; }, [&] { - tor_control_reply.code = 510; + tor_control_reply.code = TOR_REPLY_UNRECOGNIZED; }, [&] { tor_control_reply.code = fuzzed_data_provider.ConsumeIntegral(); diff --git a/src/torcontrol.cpp b/src/torcontrol.cpp index d37fc2dd4a3..04092639e8f 100644 --- a/src/torcontrol.cpp +++ b/src/torcontrol.cpp @@ -53,9 +53,6 @@ const std::string DEFAULT_TOR_CONTROL = "127.0.0.1:" + ToString(DEFAULT_TOR_CONT static const int TOR_COOKIE_SIZE = 32; /** Size of client/server nonce for SAFECOOKIE */ static const int TOR_NONCE_SIZE = 32; -/** Tor control reply code. Ref: https://spec.torproject.org/control-spec/replies.html */ -static const int TOR_REPLY_OK = 250; -static const int TOR_REPLY_UNRECOGNIZED = 510; /** For computing serverHash in SAFECOOKIE */ static const std::string TOR_SAFE_SERVERKEY = "Tor safe cookie authentication server-to-controller hash"; /** For computing clientHash in SAFECOOKIE */ diff --git a/src/torcontrol.h b/src/torcontrol.h index e64f9d38df0..f331e76a20f 100644 --- a/src/torcontrol.h +++ b/src/torcontrol.h @@ -24,6 +24,10 @@ constexpr int DEFAULT_TOR_CONTROL_PORT = 9051; extern const std::string DEFAULT_TOR_CONTROL; static const bool DEFAULT_LISTEN_ONION = true; +/** Tor control reply code. Ref: https://spec.torproject.org/control-spec/replies.html */ +constexpr int TOR_REPLY_OK{250}; +constexpr int TOR_REPLY_UNRECOGNIZED{510}; + void StartTorControl(CService onion_service_target); void InterruptTorControl(); void StopTorControl(); From 4c6798a3d386c2c1a4bcc4a8694281a8f0bef92d Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Mon, 15 Sep 2025 15:44:03 +0200 Subject: [PATCH 2/4] tor: enable PoW defenses for automatically created hidden services Enable PoW defenses [1] for hidden services that we create via Tor Control using the `ADD_ONION` command [2]. The ability to do that has been added in tor-0.4.9.2-alpha [3]. Previous versions return a syntax error to the `ADD_ONION` command with `PoWDefensesEnabled=1`, so the approach here is to try with PoW and if we get syntax error, then retry without PoW. [1] https://tpo.pages.torproject.net/onion-services/ecosystem/technology/security/pow/ [2] https://spec.torproject.org/control-spec/commands.html#add_onion [3] https://gitlab.torproject.org/tpo/core/tor/-/commit/02c18044464bfe45f168b55297a785244094cfd5 --- src/test/fuzz/torcontrol.cpp | 8 +++++++- src/torcontrol.cpp | 27 ++++++++++++++++++++++----- src/torcontrol.h | 3 ++- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/test/fuzz/torcontrol.cpp b/src/test/fuzz/torcontrol.cpp index 114bc524c20..1ea3069000a 100644 --- a/src/test/fuzz/torcontrol.cpp +++ b/src/test/fuzz/torcontrol.cpp @@ -54,6 +54,9 @@ FUZZ_TARGET(torcontrol, .init = initialize_torcontrol) [&] { tor_control_reply.code = TOR_REPLY_UNRECOGNIZED; }, + [&] { + tor_control_reply.code = TOR_REPLY_SYNTAX_ERROR; + }, [&] { tor_control_reply.code = fuzzed_data_provider.ConsumeIntegral(); }); @@ -65,7 +68,10 @@ FUZZ_TARGET(torcontrol, .init = initialize_torcontrol) CallOneOf( fuzzed_data_provider, [&] { - tor_controller.add_onion_cb(dummy_tor_control_connection, tor_control_reply); + tor_controller.add_onion_cb(dummy_tor_control_connection, tor_control_reply, /*pow_was_enabled=*/true); + }, + [&] { + tor_controller.add_onion_cb(dummy_tor_control_connection, tor_control_reply, /*pow_was_enabled=*/false); }, [&] { tor_controller.auth_cb(dummy_tor_control_connection, tor_control_reply); diff --git a/src/torcontrol.cpp b/src/torcontrol.cpp index 04092639e8f..b948de3e5bc 100644 --- a/src/torcontrol.cpp +++ b/src/torcontrol.cpp @@ -423,10 +423,20 @@ void TorController::get_socks_cb(TorControlConnection& _conn, const TorControlRe } } -void TorController::add_onion_cb(TorControlConnection& _conn, const TorControlReply& reply) +static std::string MakeAddOnionCmd(const std::string& private_key, const std::string& target, bool enable_pow) +{ + // Note that the 'virtual' port is always the default port to avoid decloaking nodes using other ports. + return strprintf("ADD_ONION %s%s Port=%i,%s", + private_key, + enable_pow ? " PoWDefensesEnabled=1" : "", + Params().GetDefaultPort(), + target); +} + +void TorController::add_onion_cb(TorControlConnection& _conn, const TorControlReply& reply, bool pow_was_enabled) { if (reply.code == TOR_REPLY_OK) { - LogDebug(BCLog::TOR, "ADD_ONION successful\n"); + LogDebug(BCLog::TOR, "ADD_ONION successful (PoW defenses %s)", pow_was_enabled ? "enabled" : "disabled"); for (const std::string &s : reply.lines) { std::map m = ParseTorReplyMapping(s); std::map::iterator i; @@ -453,6 +463,12 @@ void TorController::add_onion_cb(TorControlConnection& _conn, const TorControlRe // ... onion requested - keep connection open } else if (reply.code == TOR_REPLY_UNRECOGNIZED) { LogWarning("tor: Add onion failed with unrecognized command (You probably need to upgrade Tor)"); + } else if (pow_was_enabled && reply.code == TOR_REPLY_SYNTAX_ERROR) { + LogDebug(BCLog::TOR, "ADD_ONION failed with PoW defenses, retrying without"); + _conn.Command(MakeAddOnionCmd(private_key, m_target.ToStringAddrPort(), /*enable_pow=*/false), + [this](TorControlConnection& conn, const TorControlReply& reply) { + add_onion_cb(conn, reply, /*pow_was_enabled=*/false); + }); } else { LogWarning("tor: Add onion failed; error code %d", reply.code); } @@ -474,9 +490,10 @@ void TorController::auth_cb(TorControlConnection& _conn, const TorControlReply& private_key = "NEW:ED25519-V3"; // Explicitly request key type - see issue #9214 } // Request onion service, redirect port. - // Note that the 'virtual' port is always the default port to avoid decloaking nodes using other ports. - _conn.Command(strprintf("ADD_ONION %s Port=%i,%s", private_key, Params().GetDefaultPort(), m_target.ToStringAddrPort()), - std::bind_front(&TorController::add_onion_cb, this)); + _conn.Command(MakeAddOnionCmd(private_key, m_target.ToStringAddrPort(), /*enable_pow=*/true), + [this](TorControlConnection& conn, const TorControlReply& reply) { + add_onion_cb(conn, reply, /*pow_was_enabled=*/true); + }); } else { LogWarning("tor: Authentication failed"); } diff --git a/src/torcontrol.h b/src/torcontrol.h index f331e76a20f..b8a1d6540ba 100644 --- a/src/torcontrol.h +++ b/src/torcontrol.h @@ -27,6 +27,7 @@ static const bool DEFAULT_LISTEN_ONION = true; /** Tor control reply code. Ref: https://spec.torproject.org/control-spec/replies.html */ constexpr int TOR_REPLY_OK{250}; constexpr int TOR_REPLY_UNRECOGNIZED{510}; +constexpr int TOR_REPLY_SYNTAX_ERROR{512}; //!< Syntax error in command argument void StartTorControl(CService onion_service_target); void InterruptTorControl(); @@ -142,7 +143,7 @@ public: /** Callback for GETINFO net/listeners/socks result */ void get_socks_cb(TorControlConnection& conn, const TorControlReply& reply); /** Callback for ADD_ONION result */ - void add_onion_cb(TorControlConnection& conn, const TorControlReply& reply); + void add_onion_cb(TorControlConnection& conn, const TorControlReply& reply, bool pow_was_enabled); /** Callback for AUTHENTICATE result */ void auth_cb(TorControlConnection& conn, const TorControlReply& reply); /** Callback for AUTHCHALLENGE result */ From 4bae84c94af43d0345b9ca9a1b0db3e57f3aa31c Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Mon, 15 Sep 2025 15:57:33 +0200 Subject: [PATCH 3/4] doc: add a hint to enable PoW defenses to manual hidden services --- doc/tor.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/tor.md b/doc/tor.md index e9db555fc98..7003fcfe300 100644 --- a/doc/tor.md +++ b/doc/tor.md @@ -181,6 +181,10 @@ Add these lines to your `/etc/tor/torrc` (or equivalent config file): HiddenServiceDir /var/lib/tor/bitcoin-service/ HiddenServicePort 8333 127.0.0.1:8334 + # If `tor --list-modules` shows "pow: yes", then enable PoW protection. + # It is available in tor-0.4.8.1-alpha and newer when configured with + # `./configure --enable-gpl`. + HiddenServicePoWDefensesEnabled 1 The directory can be different of course, but virtual port numbers should be equal to your bitcoind's P2P listen port (8333 by default), and target addresses and ports From c68e3d2c57dcab5cea22ad5986fcd2b147a7daaa Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Tue, 16 Sep 2025 16:02:43 +0200 Subject: [PATCH 4/4] doc: add release notes for Tor PoW defenses --- doc/release-notes-33414.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 doc/release-notes-33414.md diff --git a/doc/release-notes-33414.md b/doc/release-notes-33414.md new file mode 100644 index 00000000000..977b88d7977 --- /dev/null +++ b/doc/release-notes-33414.md @@ -0,0 +1,9 @@ +Notable changes +=============== + +P2P and network changes +----------------------- + +Tor hidden services that are created automatically by Bitcoin Core will have +[PoW defenses](https://tpo.pages.torproject.net/onion-services/ecosystem/technology/security/pow/) +enabled if the Tor daemon supports that. (#33414)