tor: enable PoW defenses for automatically created hidden services #33414

pull vasild wants to merge 4 commits into bitcoin:master from vasild:tor_pow changing 5 files +50 −12
  1. vasild commented at 10:40 am on September 17, 2025: contributor

    Enable PoW defenses for hidden services that we create via Tor Control using the ADD_ONION command.

    The ability to do that has been added in tor-0.4.9.2-alpha. 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.

    Also update doc/tor.md with a hint on enabling PoW on manually configured Tor hidden services.

  2. DrahtBot added the label P2P on Sep 17, 2025
  3. DrahtBot commented at 10:40 am on September 17, 2025: contributor

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/33414.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK willcl-ark, fjahr, sedited

    If your review is incorrectly listed, please copy-paste <!–meta-tag:bot-skip–> into the comment that the bot should ignore.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #34486 (net: Reduce local network activity when networkactive=0 by willcl-ark)
    • #34158 (torcontrol: Remove libevent usage by fjahr)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

  4. dergoegge commented at 12:52 pm on September 17, 2025: member
    Should we then also add PoW to the connections that we make to other nodes running behind hidden services?
  5. willcl-ark commented at 2:27 pm on September 17, 2025: member

    Should we then also add PoW to the connections that we make to other nodes running behind hidden services?

    Reading the linked FAQ, the feature still supports “older clients” (which don’t have PoW defence capability), but they may take a lower priority when a service considers itself under DoS. So no PoW is required on the client side.

    When the client-side tor is new-enough, my understanding is that the puzzle-solving is automatically handled by Tor, and doesn’t need client-side changes to the connection code, as it happens during the introduction. But I am not 100% certain.

  6. fanquake commented at 1:03 pm on September 23, 2025: member
    @laanwj you might have some thoughts here?
  7. DrahtBot added the label Needs rebase on Dec 2, 2025
  8. fanquake commented at 12:27 pm on February 16, 2026: member
    What is the status of this?
  9. in src/test/fuzz/torcontrol.cpp:56 in 5aefa08017 outdated


    willcl-ark commented at 9:34 pm on February 16, 2026:
    I think tor_control_reply_code = 512 could be added here, to hit this expected path more frequently than random?

    vasild commented at 11:12 am on February 19, 2026:
    Right, good catch! Done and also moved the TOR_REPLY_* constants to torcontrol.h so this fuzz test can use them instead of hardcoding numbers. Thanks!
  10. willcl-ark commented at 9:55 pm on February 16, 2026: member

    Approach and lightly-tested ACK on PoW-enabled Tor (version 0.4.8.22).

    02026-02-16T21:44:26Z [tor] Get SOCKS port command yielded 127.0.0.1:9050
    12026-02-16T21:44:26Z [tor] Configuring onion proxy for 127.0.0.1:9050
    22026-02-16T21:44:26Z [tor] ADD_ONION failed with PoW defenses, retrying without
    32026-02-16T21:44:26Z [tor] ADD_ONION successful (PoW defenses disabled)
    

    Seems very reasonable to me to implement this configuration in order that we fare better during DoS attacks on the Tor network. Noting for others that as far as I read, due to the dynamic difficulty the (tor) PoW is ~ free on idle, and the cost only applies during an attack, which seems nice.

    I quite like the pragmatism of “detecting” the tor version via the failure mode. It appears that the 512 failure happens early on the Tor side, before any service is created. So failing and retrying is “clean”.

    What is the status of this?

    Looks like it needs a pretty trivial rebase at the moment is all.

  11. vasild force-pushed on Feb 19, 2026
  12. vasild commented at 10:50 am on February 19, 2026: contributor

    8a526d39d8a00edff2361ceaa012574d1337b77b...206da5e5e420ab43857e4d15ddb7d1c603d6e762: rebase due to conflicts

    What is the status of this?

    Needed a rebase.

  13. 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.
    fb993f7604
  14. 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
    4c6798a3d3
  15. doc: add a hint to enable PoW defenses to manual hidden services 4bae84c94a
  16. doc: add release notes for Tor PoW defenses c68e3d2c57
  17. vasild force-pushed on Feb 19, 2026
  18. DrahtBot added the label CI failed on Feb 19, 2026
  19. vasild commented at 11:11 am on February 19, 2026: contributor
    206da5e5e420ab43857e4d15ddb7d1c603d6e762...c68e3d2c57dcab5cea22ad5986fcd2b147a7daaa: address suggestion
  20. DrahtBot removed the label Needs rebase on Feb 19, 2026
  21. willcl-ark commented at 11:34 am on February 19, 2026: member

    Great, the range-diff looks good:

     0git range-diff 2d6a0c464912c325faf35d4ad28b1990e828b414..8a526d3 8ee24d764a2820259fe42f8def93fd8a2c36a4cf..c68e3d2c57d
     1-:  ----------- > 1:  fb993f76047 tor, fuzz: reuse constants instead of duplicating
     21:  5aefa080178 ! 2:  4c6798a3d38 tor: enable PoW defenses for automatically created hidden services
     3    @@ Commit message
     4         [3] https://gitlab.torproject.org/tpo/core/tor/-/commit/02c18044464bfe45f168b55297a785244094cfd5
     5
     6      ## src/test/fuzz/torcontrol.cpp ##
     7    +@@ src/test/fuzz/torcontrol.cpp: FUZZ_TARGET(torcontrol, .init = initialize_torcontrol)
     8    +             [&] {
     9    +                 tor_control_reply.code = TOR_REPLY_UNRECOGNIZED;
    10    +             },
    11    ++            [&] {
    12    ++                tor_control_reply.code = TOR_REPLY_SYNTAX_ERROR;
    13    ++            },
    14    +             [&] {
    15    +                 tor_control_reply.code = fuzzed_data_provider.ConsumeIntegral<int>();
    16    +             });
    17     @@ src/test/fuzz/torcontrol.cpp: FUZZ_TARGET(torcontrol, .init = initialize_torcontrol)
    18              CallOneOf(
    19                  fuzzed_data_provider,
    20    @@ src/test/fuzz/torcontrol.cpp: FUZZ_TARGET(torcontrol, .init = initialize_torcont
    21                      tor_controller.auth_cb(dummy_tor_control_connection, tor_control_reply);
    22
    23      ## src/torcontrol.cpp ##
    24    -@@ src/torcontrol.cpp: static const int TOR_NONCE_SIZE = 32;
    25    - /** Tor control reply code. Ref: https://spec.torproject.org/control-spec/replies.html */
    26    - static const int TOR_REPLY_OK = 250;
    27    - static const int TOR_REPLY_UNRECOGNIZED = 510;
    28    -+static const int TOR_REPLY_SYNTAX_ERROR = 512; //!< Syntax error in command argument
    29    - /** For computing serverHash in SAFECOOKIE */
    30    - static const std::string TOR_SAFE_SERVERKEY = "Tor safe cookie authentication server-to-controller hash";
    31    - /** For computing clientHash in SAFECOOKIE */
    32     @@ src/torcontrol.cpp: void TorController::get_socks_cb(TorControlConnection& _conn, const TorControlRe
    33          }
    34      }
    35    @@ src/torcontrol.cpp: void TorController::get_socks_cb(TorControlConnection& _conn
    36     @@ src/torcontrol.cpp: void TorController::add_onion_cb(TorControlConnection& _conn, const TorControlRe
    37              // ... onion requested - keep connection open
    38          } else if (reply.code == TOR_REPLY_UNRECOGNIZED) {
    39    -         LogPrintf("tor: Add onion failed with unrecognized command (You probably need to upgrade Tor)\n");
    40    +         LogWarning("tor: Add onion failed with unrecognized command (You probably need to upgrade Tor)");
    41     +    } else if (pow_was_enabled && reply.code == TOR_REPLY_SYNTAX_ERROR) {
    42     +        LogDebug(BCLog::TOR, "ADD_ONION failed with PoW defenses, retrying without");
    43     +        _conn.Command(MakeAddOnionCmd(private_key, m_target.ToStringAddrPort(), /*enable_pow=*/false),
    44    -+                      std::bind(&TorController::add_onion_cb,
    45    -+                                this,
    46    -+                                std::placeholders::_1,
    47    -+                                std::placeholders::_2,
    48    -+                                /*pow_was_enabled=*/false));
    49    ++                      [this](TorControlConnection& conn, const TorControlReply& reply) {
    50    ++                          add_onion_cb(conn, reply, /*pow_was_enabled=*/false);
    51    ++                      });
    52          } else {
    53    -         LogPrintf("tor: Add onion failed; error code %d\n", reply.code);
    54    +         LogWarning("tor: Add onion failed; error code %d", reply.code);
    55          }
    56     @@ src/torcontrol.cpp: void TorController::auth_cb(TorControlConnection& _conn, const TorControlReply&
    57                  private_key = "NEW:ED25519-V3"; // Explicitly request key type - see issue [#9214](/bitcoin-bitcoin/9214/)
    58    @@ src/torcontrol.cpp: void TorController::auth_cb(TorControlConnection& _conn, con
    59              // Request onion service, redirect port.
    60     -        // Note that the 'virtual' port is always the default port to avoid decloaking nodes using other ports.
    61     -        _conn.Command(strprintf("ADD_ONION %s Port=%i,%s", private_key, Params().GetDefaultPort(), m_target.ToStringAddrPort()),
    62    --            std::bind(&TorController::add_onion_cb, this, std::placeholders::_1, std::placeholders::_2));
    63    +-            std::bind_front(&TorController::add_onion_cb, this));
    64     +        _conn.Command(MakeAddOnionCmd(private_key, m_target.ToStringAddrPort(), /*enable_pow=*/true),
    65    -+                      std::bind(&TorController::add_onion_cb,
    66    -+                                this,
    67    -+                                std::placeholders::_1,
    68    -+                                std::placeholders::_2,
    69    -+                                /*pow_was_enabled=*/true));
    70    ++                      [this](TorControlConnection& conn, const TorControlReply& reply) {
    71    ++                          add_onion_cb(conn, reply, /*pow_was_enabled=*/true);
    72    ++                      });
    73          } else {
    74    -         LogPrintf("tor: Authentication failed\n");
    75    +         LogWarning("tor: Authentication failed");
    76          }
    77
    78      ## src/torcontrol.h ##
    79    +@@ src/torcontrol.h: static const bool DEFAULT_LISTEN_ONION = true;
    80    + /** Tor control reply code. Ref: https://spec.torproject.org/control-spec/replies.html */
    81    + constexpr int TOR_REPLY_OK{250};
    82    + constexpr int TOR_REPLY_UNRECOGNIZED{510};
    83    ++constexpr int TOR_REPLY_SYNTAX_ERROR{512}; //!< Syntax error in command argument
    84    +
    85    + void StartTorControl(CService onion_service_target);
    86    + void InterruptTorControl();
    87     @@ src/torcontrol.h: public:
    88          /** Callback for GETINFO net/listeners/socks result */
    89          void get_socks_cb(TorControlConnection& conn, const TorControlReply& reply);
    902:  a61080aef87 = 3:  4bae84c94af doc: add a hint to enable PoW defenses to manual hidden services
    913:  8a526d39d8a = 4:  c68e3d2c57d doc: add release notes for Tor PoW defenses
    

    Just realised though that my tor, although it has PoWDefenses compiled in, does not support it via ADD_ONION yet

     0❯ tor --list-modules
     1relay: yes
     2dirauth: yes
     3dircache: yes
     4pow: yes
     5
     6❯ nc 127.0.0.1 9051
     7AUTHENTICATE ""
     8250 OK
     9ADD_ONION NEW:ED25519-V3 PoWDefensesEnabled=1 Port=38333,127.0.0.1:38333
    10512 Bad arguments to ADD_ONION: Unrecognized keyword argument "PoWDefensesEnabled"
    

    I think it might only be for pre-configured services unti addition to the ONION keyword in 0.4.9.5. That said, it does show the fallback mechanism, even in this edge case, works as intended… (and in fact that querying the binary modules would have been a bug here?).

  22. vasild commented at 11:50 am on February 19, 2026: contributor

    and in fact that querying the binary modules would have been a bug here?

    I think yes, better to actually check if the ADD_ONION command supports pow (like in this PR).

  23. willcl-ark approved
  24. willcl-ark commented at 12:07 pm on February 19, 2026: member

    ACK c68e3d2c57dcab5cea22ad5986fcd2b147a7daaa

    Tested with two versions of tor. ON 0.4.8.22 automatic PoWDefense via ONION message (correctly) does not work and falls back to without.

    On 0.4.9.5 I see:

    02026-02-19T12:04:04Z torcontrol thread start
    12026-02-19T12:04:04Z [tor] Reading cached private key from /xxxxxxxx/onion_v3_private_key
    22026-02-19T12:04:04Z [tor] Successfully connected!
    32026-02-19T12:04:04Z [tor] Connected to Tor version 0.4.9.5
    42026-02-19T12:04:04Z [tor] Supported authentication method: NULL
    52026-02-19T12:04:04Z [tor] Using NULL authentication
    62026-02-19T12:04:04Z [tor] Authentication successful
    72026-02-19T12:04:04Z [tor] Get SOCKS port command yielded 127.0.0.1:9050
    82026-02-19T12:04:04Z [tor] Configuring onion proxy for 127.0.0.1:9050
    92026-02-19T12:04:04Z [tor] ADD_ONION successful (PoW defenses enabled)
    

    The changes look clean and correct to me.

  25. DrahtBot removed the label CI failed on Feb 19, 2026
  26. fjahr commented at 11:36 am on March 15, 2026: contributor
    Concept ACK
  27. sedited requested review from fjahr on Mar 19, 2026
  28. in doc/tor.md:185 in c68e3d2c57
    180@@ -181,6 +181,10 @@ Add these lines to your `/etc/tor/torrc` (or equivalent config file):
    181 
    182     HiddenServiceDir /var/lib/tor/bitcoin-service/
    183     HiddenServicePort 8333 127.0.0.1:8334
    184+    # If `tor --list-modules` shows "pow: yes", then enable PoW protection.
    185+    # It is available in tor-0.4.8.1-alpha and newer when configured with
    


    fjahr commented at 2:48 pm on March 19, 2026:
    In the PR description you say that it has been available since 0.4.9.2-alpha. Does this mean something different or is this an inconsistency?

    vasild commented at 11:32 am on March 20, 2026:

    It means something different.

    First in 0.4.8.1-alpha PoW defenses were added. This makes it possible to put HiddenServicePoWDefensesEnabled in torrc which our doc/tor.md advises here. Still with that change it was not possible to enable PoW for services that are created dynamically via the Tor server (instead of configured in torrc).

    Then in 0.4.9.2-alpha it was made possible to enable PoW also for the programatically created services: ADD_ONION ... PoWDefensesEnabled.

  29. in src/torcontrol.cpp:426 in 4c6798a3d3 outdated
    422@@ -423,10 +423,20 @@ void TorController::get_socks_cb(TorControlConnection& _conn, const TorControlRe
    423     }
    424 }
    425 
    426-void TorController::add_onion_cb(TorControlConnection& _conn, const TorControlReply& reply)
    427+static std::string MakeAddOnionCmd(const std::string& private_key, const std::string& target, bool enable_pow)
    


    fjahr commented at 4:52 pm on March 19, 2026:
    nit: Could have made this a member function of TorController to avoid having to pass private_key and target.

    vasild commented at 11:45 am on March 20, 2026:

    Right! Here is a diff for that:

     0diff --git i/src/torcontrol.cpp w/src/torcontrol.cpp
     1index b948de3e5b..77227112b2 100644
     2--- i/src/torcontrol.cpp
     3+++ w/src/torcontrol.cpp
     4@@ -420,20 +420,20 @@ void TorController::get_socks_cb(TorControlConnection& _conn, const TorControlRe
     5         // If NET_ONION is not reachable, then none of -proxy or -onion was given.
     6         // Since we are here, then -torcontrol and -torpassword were given.
     7         g_reachable_nets.Add(NET_ONION);
     8     }
     9 }
    10 
    11-static std::string MakeAddOnionCmd(const std::string& private_key, const std::string& target, bool enable_pow)
    12+std::string TorController::make_add_onion_cmd(bool enable_pow) const
    13 {
    14     // Note that the 'virtual' port is always the default port to avoid decloaking nodes using other ports.
    15     return strprintf("ADD_ONION %s%s Port=%i,%s",
    16                      private_key,
    17                      enable_pow ? " PoWDefensesEnabled=1" : "",
    18                      Params().GetDefaultPort(),
    19-                     target);
    20+                     m_target.ToStringAddrPort());
    21 }
    22 
    23 void TorController::add_onion_cb(TorControlConnection& _conn, const TorControlReply& reply, bool pow_was_enabled)
    24 {
    25     if (reply.code == TOR_REPLY_OK) {
    26         LogDebug(BCLog::TOR, "ADD_ONION successful (PoW defenses %s)", pow_was_enabled ? "enabled" : "disabled");
    27@@ -462,13 +462,13 @@ void TorController::add_onion_cb(TorControlConnection& _conn, const TorControlRe
    28         AddLocal(service, LOCAL_MANUAL);
    29         // ... onion requested - keep connection open
    30     } else if (reply.code == TOR_REPLY_UNRECOGNIZED) {
    31         LogWarning("tor: Add onion failed with unrecognized command (You probably need to upgrade Tor)");
    32     } else if (pow_was_enabled && reply.code == TOR_REPLY_SYNTAX_ERROR) {
    33         LogDebug(BCLog::TOR, "ADD_ONION failed with PoW defenses, retrying without");
    34-        _conn.Command(MakeAddOnionCmd(private_key, m_target.ToStringAddrPort(), /*enable_pow=*/false),
    35+        _conn.Command(make_add_onion_cmd(/*enable_pow=*/false),
    36                       [this](TorControlConnection& conn, const TorControlReply& reply) {
    37                           add_onion_cb(conn, reply, /*pow_was_enabled=*/false);
    38                       });
    39     } else {
    40         LogWarning("tor: Add onion failed; error code %d", reply.code);
    41     }
    42@@ -487,13 +487,13 @@ void TorController::auth_cb(TorControlConnection& _conn, const TorControlReply&
    43 
    44         // Finally - now create the service
    45         if (private_key.empty()) { // No private key, generate one
    46             private_key = "NEW:ED25519-V3"; // Explicitly request key type - see issue [#9214](/bitcoin-bitcoin/9214/)
    47         }
    48         // Request onion service, redirect port.
    49-        _conn.Command(MakeAddOnionCmd(private_key, m_target.ToStringAddrPort(), /*enable_pow=*/true),
    50+        _conn.Command(make_add_onion_cmd(/*enable_pow=*/true),
    51                       [this](TorControlConnection& conn, const TorControlReply& reply) {
    52                           add_onion_cb(conn, reply, /*pow_was_enabled=*/true);
    53                       });
    54     } else {
    55         LogWarning("tor: Authentication failed");
    56     }
    57diff --git i/src/torcontrol.h w/src/torcontrol.h
    58index b8a1d6540b..1d4f1581fd 100644
    59--- i/src/torcontrol.h
    60+++ w/src/torcontrol.h
    61@@ -139,12 +139,14 @@ private:
    62     /** ClientNonce for SAFECOOKIE auth */
    63     std::vector<uint8_t> clientNonce;
    64 
    65 public:
    66     /** Callback for GETINFO net/listeners/socks result */
    67     void get_socks_cb(TorControlConnection& conn, const TorControlReply& reply);
    68+    /** Create the "ADD_ONION ..." command with enabled PoW defenses if `enable_pow` is `true`. */
    69+    std::string make_add_onion_cmd(bool enable_pow) const;
    70     /** Callback for ADD_ONION result */
    71     void add_onion_cb(TorControlConnection& conn, const TorControlReply& reply, bool pow_was_enabled);
    72     /** Callback for AUTHENTICATE result */
    73     void auth_cb(TorControlConnection& conn, const TorControlReply& reply);
    74     /** Callback for AUTHCHALLENGE result */
    75     void authchallenge_cb(TorControlConnection& conn, const TorControlReply& reply);
    

    I think this is worth including, but this PR has a few ACKs already. Include it?


    fjahr commented at 11:48 am on March 20, 2026:
    As you like, I can re-ack quickly but you can also leave it as is. It was just a minor style-nit.

    vasild commented at 8:54 am on March 23, 2026:
    Merged now, leaving it as it is.
  30. fjahr commented at 4:57 pm on March 19, 2026: contributor

    tACK c68e3d2c57dcab5cea22ad5986fcd2b147a7daaa

    Only tested that PoW enable worked with the latest tor version (0.4.9.5), not the fallback with an older tor version. Code looks good, note to self: I will extend the test coverage in #34158 with additional coverage for this.

  31. sedited approved
  32. sedited commented at 9:01 am on March 20, 2026: contributor
    ACK c68e3d2c57dcab5cea22ad5986fcd2b147a7daaa
  33. fanquake merged this on Mar 23, 2026
  34. fanquake closed this on Mar 23, 2026

  35. vasild deleted the branch on Mar 23, 2026

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-03-30 03:13 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me