mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-04-10 07:37:59 +02:00
Merge bitcoin/bitcoin#34158: torcontrol: Remove libevent usage
1401011f71test: Add test for exceeding max line length in torcontrol (Fabian Jahr)84c1f32071test: Add torcontrol coverage for PoW defense enablement (Fabian Jahr)7dff9ec298test: Add test for partial message handling in torcontrol (Fabian Jahr)569383356etest: Add simple functional test for torcontrol (Fabian Jahr)4117b92e67fuzz: Improve torcontrol fuzz test (Fabian Jahr)b1869e9a2dtorcontrol: Move tor controller into node context (Fabian Jahr)eae193e750torcontrol: Remove libevent usage (Fabian Jahr)8444efbd4arefactor: Get rid of unnecessary newlines in logs (Fabian Jahr)6bcb60354erefactor: Modernize member variable names in torcontrol (Fabian Jahr)a36591d194refactor: Use constexpr in torcontrol where possible (Fabian Jahr) Pull request description: This is part of the effort to remove the libevent dependency from our code base: https://github.com/bitcoin/bitcoin/issues/31194 The current approach tries to reuse existing code and follows roughly similar design decisions. It replaces the libevent-based async I/O with blocking I/O utilizing the existing `Sock` and `CThreadInterrupt`. The controller runs in a dedicated thread. There are some optional code modernizations thrown in made along the way (namings, constexpr etc.). These are not strictly necessary but make the end result with the new code more consistent. ACKs for top commit: achow101: ACK1401011f71janb84: re ACK1401011f71pinheadmz: ACK1401011f71Tree-SHA512: 167f1d98a634524568cb1d723e7bdb7234bade2c5686586caf2accea58c3308f83a32e0705edc570d6db691ae578a91e474ae4773f126ec2e1619d3adf7df622
This commit is contained in:
11
src/init.cpp
11
src/init.cpp
@@ -276,7 +276,9 @@ void Interrupt(NodeContext& node)
|
||||
InterruptHTTPRPC();
|
||||
InterruptRPC();
|
||||
InterruptREST();
|
||||
InterruptTorControl();
|
||||
if (node.tor_controller) {
|
||||
node.tor_controller->Interrupt();
|
||||
}
|
||||
InterruptMapPort();
|
||||
if (node.connman)
|
||||
node.connman->Interrupt();
|
||||
@@ -319,7 +321,10 @@ void Shutdown(NodeContext& node)
|
||||
if (node.peerman && node.validation_signals) node.validation_signals->UnregisterValidationInterface(node.peerman.get());
|
||||
if (node.connman) node.connman->Stop();
|
||||
|
||||
StopTorControl();
|
||||
if (node.tor_controller) {
|
||||
node.tor_controller->Join();
|
||||
node.tor_controller.reset();
|
||||
}
|
||||
|
||||
if (node.background_init_thread.joinable()) node.background_init_thread.join();
|
||||
// After everything has been shut down, but before things get flushed, stop the
|
||||
@@ -2187,7 +2192,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
|
||||
"for the automatically created Tor onion service."),
|
||||
onion_service_target.ToStringAddrPort()));
|
||||
}
|
||||
StartTorControl(onion_service_target);
|
||||
node.tor_controller = std::make_unique<TorController>(gArgs.GetArg("-torcontrol", DEFAULT_TOR_CONTROL), onion_service_target);
|
||||
}
|
||||
|
||||
if (connOptions.bind_on_any) {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <node/warnings.h>
|
||||
#include <policy/fees/block_policy_estimator.h>
|
||||
#include <scheduler.h>
|
||||
#include <torcontrol.h>
|
||||
#include <txmempool.h>
|
||||
#include <validation.h>
|
||||
#include <validationinterface.h>
|
||||
|
||||
@@ -25,6 +25,7 @@ class ChainstateManager;
|
||||
class ECC_Context;
|
||||
class NetGroupManager;
|
||||
class PeerManager;
|
||||
class TorController;
|
||||
namespace interfaces {
|
||||
class Chain;
|
||||
class ChainClient;
|
||||
@@ -69,6 +70,7 @@ struct NodeContext {
|
||||
std::unique_ptr<const NetGroupManager> netgroupman;
|
||||
std::unique_ptr<CBlockPolicyEstimator> fee_estimator;
|
||||
std::unique_ptr<PeerManager> peerman;
|
||||
std::unique_ptr<TorController> tor_controller;
|
||||
std::unique_ptr<ChainstateManager> chainman;
|
||||
std::unique_ptr<BanMan> banman;
|
||||
ArgsManager* args{nullptr}; // Currently a raw pointer because the memory is not managed by this struct
|
||||
|
||||
@@ -12,28 +12,6 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class DummyTorControlConnection : public TorControlConnection
|
||||
{
|
||||
public:
|
||||
DummyTorControlConnection() : TorControlConnection{nullptr}
|
||||
{
|
||||
}
|
||||
|
||||
bool Connect(const std::string&, const ConnectionCB&, const ConnectionCB&)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void Disconnect()
|
||||
{
|
||||
}
|
||||
|
||||
bool Command(const std::string&, const ReplyHandlerCB&)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
void initialize_torcontrol()
|
||||
{
|
||||
static const auto testing_setup = MakeNoLogFileContext<>();
|
||||
@@ -44,6 +22,9 @@ FUZZ_TARGET(torcontrol, .init = initialize_torcontrol)
|
||||
FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
|
||||
|
||||
TorController tor_controller;
|
||||
CThreadInterrupt interrupt;
|
||||
TorControlConnection conn{interrupt};
|
||||
|
||||
LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 10000) {
|
||||
TorControlReply tor_control_reply;
|
||||
CallOneOf(
|
||||
@@ -61,26 +42,26 @@ FUZZ_TARGET(torcontrol, .init = initialize_torcontrol)
|
||||
tor_control_reply.code = fuzzed_data_provider.ConsumeIntegral<int>();
|
||||
});
|
||||
tor_control_reply.lines = ConsumeRandomLengthStringVector(fuzzed_data_provider);
|
||||
if (tor_control_reply.lines.empty()) {
|
||||
break;
|
||||
}
|
||||
DummyTorControlConnection dummy_tor_control_connection;
|
||||
|
||||
CallOneOf(
|
||||
fuzzed_data_provider,
|
||||
[&] {
|
||||
tor_controller.add_onion_cb(dummy_tor_control_connection, tor_control_reply, /*pow_was_enabled=*/true);
|
||||
tor_controller.add_onion_cb(conn, 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.add_onion_cb(conn, tor_control_reply, /*pow_was_enabled=*/false);
|
||||
},
|
||||
[&] {
|
||||
tor_controller.auth_cb(dummy_tor_control_connection, tor_control_reply);
|
||||
tor_controller.auth_cb(conn, tor_control_reply);
|
||||
},
|
||||
[&] {
|
||||
tor_controller.authchallenge_cb(dummy_tor_control_connection, tor_control_reply);
|
||||
tor_controller.authchallenge_cb(conn, tor_control_reply);
|
||||
},
|
||||
[&] {
|
||||
tor_controller.protocolinfo_cb(dummy_tor_control_connection, tor_control_reply);
|
||||
tor_controller.protocolinfo_cb(conn, tor_control_reply);
|
||||
},
|
||||
[&] {
|
||||
tor_controller.get_socks_cb(conn, tor_control_reply);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <deque>
|
||||
@@ -37,12 +38,6 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <event2/buffer.h>
|
||||
#include <event2/bufferevent.h>
|
||||
#include <event2/event.h>
|
||||
#include <event2/thread.h>
|
||||
#include <event2/util.h>
|
||||
|
||||
using util::ReplaceAll;
|
||||
using util::SplitString;
|
||||
using util::ToString;
|
||||
@@ -50,154 +45,180 @@ using util::ToString;
|
||||
/** Default control ip and port */
|
||||
const std::string DEFAULT_TOR_CONTROL = "127.0.0.1:" + ToString(DEFAULT_TOR_CONTROL_PORT);
|
||||
/** Tor cookie size (from control-spec.txt) */
|
||||
static const int TOR_COOKIE_SIZE = 32;
|
||||
constexpr int TOR_COOKIE_SIZE = 32;
|
||||
/** Size of client/server nonce for SAFECOOKIE */
|
||||
static const int TOR_NONCE_SIZE = 32;
|
||||
/** For computing serverHash in SAFECOOKIE */
|
||||
constexpr int TOR_NONCE_SIZE = 32;
|
||||
/** For computing server_hash in SAFECOOKIE */
|
||||
static const std::string TOR_SAFE_SERVERKEY = "Tor safe cookie authentication server-to-controller hash";
|
||||
/** For computing clientHash in SAFECOOKIE */
|
||||
static const std::string TOR_SAFE_CLIENTKEY = "Tor safe cookie authentication controller-to-server hash";
|
||||
/** Exponential backoff configuration - initial timeout in seconds */
|
||||
static const float RECONNECT_TIMEOUT_START = 1.0;
|
||||
constexpr std::chrono::duration<double> RECONNECT_TIMEOUT_START{1.0};
|
||||
/** Exponential backoff configuration - growth factor */
|
||||
static const float RECONNECT_TIMEOUT_EXP = 1.5;
|
||||
constexpr double RECONNECT_TIMEOUT_EXP = 1.5;
|
||||
/** Maximum reconnect timeout in seconds to prevent excessive delays */
|
||||
static const float RECONNECT_TIMEOUT_MAX = 600.0;
|
||||
constexpr std::chrono::duration<double> RECONNECT_TIMEOUT_MAX{600.0};
|
||||
/** Maximum length for lines received on TorControlConnection.
|
||||
* tor-control-spec.txt mentions that there is explicitly no limit defined to line length,
|
||||
* this is belt-and-suspenders sanity limit to prevent memory exhaustion.
|
||||
*/
|
||||
static const int MAX_LINE_LENGTH = 100000;
|
||||
constexpr int MAX_LINE_LENGTH = 100000;
|
||||
/** Timeout for socket operations */
|
||||
constexpr auto SOCKET_SEND_TIMEOUT = 10s;
|
||||
|
||||
/****** Low-level TorControlConnection ********/
|
||||
|
||||
TorControlConnection::TorControlConnection(struct event_base* _base)
|
||||
: base(_base)
|
||||
TorControlConnection::TorControlConnection(CThreadInterrupt& interrupt)
|
||||
: m_interrupt(interrupt)
|
||||
{
|
||||
}
|
||||
|
||||
TorControlConnection::~TorControlConnection()
|
||||
{
|
||||
if (b_conn)
|
||||
bufferevent_free(b_conn);
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
void TorControlConnection::readcb(struct bufferevent *bev, void *ctx)
|
||||
bool TorControlConnection::Connect(const std::string& tor_control_center)
|
||||
{
|
||||
TorControlConnection *self = static_cast<TorControlConnection*>(ctx);
|
||||
struct evbuffer *input = bufferevent_get_input(bev);
|
||||
size_t n_read_out = 0;
|
||||
char *line;
|
||||
assert(input);
|
||||
// If there is not a whole line to read, evbuffer_readln returns nullptr
|
||||
while((line = evbuffer_readln(input, &n_read_out, EVBUFFER_EOL_CRLF)) != nullptr)
|
||||
{
|
||||
std::string s(line, n_read_out);
|
||||
free(line);
|
||||
if (s.size() < 4) // Short line
|
||||
continue;
|
||||
// <status>(-|+| )<data><CRLF>
|
||||
self->message.code = ToIntegral<int>(s.substr(0, 3)).value_or(0);
|
||||
self->message.lines.push_back(s.substr(4));
|
||||
char ch = s[3]; // '-','+' or ' '
|
||||
if (ch == ' ') {
|
||||
// Final line, dispatch reply and clean up
|
||||
if (self->message.code >= 600) {
|
||||
// (currently unused)
|
||||
// Dispatch async notifications to async handler
|
||||
// Synchronous and asynchronous messages are never interleaved
|
||||
} else {
|
||||
if (!self->reply_handlers.empty()) {
|
||||
// Invoke reply handler with message
|
||||
self->reply_handlers.front()(*self, self->message);
|
||||
self->reply_handlers.pop_front();
|
||||
} else {
|
||||
LogDebug(BCLog::TOR, "Received unexpected sync reply %i\n", self->message.code);
|
||||
}
|
||||
}
|
||||
self->message.Clear();
|
||||
}
|
||||
}
|
||||
// Check for size of buffer - protect against memory exhaustion with very long lines
|
||||
// Do this after evbuffer_readln to make sure all full lines have been
|
||||
// removed from the buffer. Everything left is an incomplete line.
|
||||
if (evbuffer_get_length(input) > MAX_LINE_LENGTH) {
|
||||
LogWarning("tor: Disconnecting because MAX_LINE_LENGTH exceeded");
|
||||
self->Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
void TorControlConnection::eventcb(struct bufferevent *bev, short what, void *ctx)
|
||||
{
|
||||
TorControlConnection *self = static_cast<TorControlConnection*>(ctx);
|
||||
if (what & BEV_EVENT_CONNECTED) {
|
||||
LogDebug(BCLog::TOR, "Successfully connected!\n");
|
||||
self->connected(*self);
|
||||
} else if (what & (BEV_EVENT_EOF|BEV_EVENT_ERROR)) {
|
||||
if (what & BEV_EVENT_ERROR) {
|
||||
LogDebug(BCLog::TOR, "Error connecting to Tor control socket\n");
|
||||
} else {
|
||||
LogDebug(BCLog::TOR, "End of stream\n");
|
||||
}
|
||||
self->Disconnect();
|
||||
self->disconnected(*self);
|
||||
}
|
||||
}
|
||||
|
||||
bool TorControlConnection::Connect(const std::string& tor_control_center, const ConnectionCB& _connected, const ConnectionCB& _disconnected)
|
||||
{
|
||||
if (b_conn) {
|
||||
if (m_sock) {
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
const std::optional<CService> control_service{Lookup(tor_control_center, DEFAULT_TOR_CONTROL_PORT, fNameLookup)};
|
||||
std::optional<CService> control_service = Lookup(tor_control_center, DEFAULT_TOR_CONTROL_PORT, fNameLookup);
|
||||
if (!control_service.has_value()) {
|
||||
LogWarning("tor: Failed to look up control center %s", tor_control_center);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct sockaddr_storage control_address;
|
||||
socklen_t control_address_len = sizeof(control_address);
|
||||
if (!control_service.value().GetSockAddr(reinterpret_cast<struct sockaddr*>(&control_address), &control_address_len)) {
|
||||
LogWarning("tor: Error parsing socket address %s", tor_control_center);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create a new socket, set up callbacks and enable notification bits
|
||||
b_conn = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE);
|
||||
if (!b_conn) {
|
||||
return false;
|
||||
}
|
||||
bufferevent_setcb(b_conn, TorControlConnection::readcb, nullptr, TorControlConnection::eventcb, this);
|
||||
bufferevent_enable(b_conn, EV_READ|EV_WRITE);
|
||||
this->connected = _connected;
|
||||
this->disconnected = _disconnected;
|
||||
|
||||
// Finally, connect to tor_control_center
|
||||
if (bufferevent_socket_connect(b_conn, reinterpret_cast<struct sockaddr*>(&control_address), control_address_len) < 0) {
|
||||
m_sock = ConnectDirectly(control_service.value(), /*manual_connection=*/true);
|
||||
if (!m_sock) {
|
||||
LogWarning("tor: Error connecting to address %s", tor_control_center);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_recv_buffer.clear();
|
||||
m_message.Clear();
|
||||
m_reply_handlers.clear();
|
||||
|
||||
LogDebug(BCLog::TOR, "Successfully connected to Tor control port");
|
||||
return true;
|
||||
}
|
||||
|
||||
void TorControlConnection::Disconnect()
|
||||
{
|
||||
if (b_conn)
|
||||
bufferevent_free(b_conn);
|
||||
b_conn = nullptr;
|
||||
m_sock.reset();
|
||||
m_recv_buffer.clear();
|
||||
m_message.Clear();
|
||||
m_reply_handlers.clear();
|
||||
}
|
||||
|
||||
bool TorControlConnection::IsConnected() const
|
||||
{
|
||||
if (!m_sock) return false;
|
||||
std::string errmsg;
|
||||
const bool connected{m_sock->IsConnected(errmsg)};
|
||||
if (!connected && !errmsg.empty()) {
|
||||
LogDebug(BCLog::TOR, "Connection check failed: %s", errmsg);
|
||||
}
|
||||
return connected;
|
||||
}
|
||||
|
||||
bool TorControlConnection::WaitForData(std::chrono::milliseconds timeout)
|
||||
{
|
||||
if (!m_sock) return false;
|
||||
|
||||
Sock::Event event{0};
|
||||
if (!m_sock->Wait(timeout, Sock::RECV, &event)) {
|
||||
return false;
|
||||
}
|
||||
if (event & Sock::ERR) {
|
||||
LogDebug(BCLog::TOR, "Socket error detected");
|
||||
Disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
return (event & Sock::RECV);
|
||||
}
|
||||
|
||||
bool TorControlConnection::ReceiveAndProcess()
|
||||
{
|
||||
if (!m_sock) return false;
|
||||
|
||||
std::byte buf[4096];
|
||||
ssize_t nread = m_sock->Recv(buf, sizeof(buf), MSG_DONTWAIT);
|
||||
|
||||
if (nread < 0) {
|
||||
int err = WSAGetLastError();
|
||||
if (err == WSAEWOULDBLOCK || err == WSAEINTR || err == WSAEINPROGRESS) {
|
||||
// No data available currently
|
||||
return true;
|
||||
}
|
||||
LogWarning("tor: Error reading from socket: %s", NetworkErrorString(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nread == 0) {
|
||||
LogDebug(BCLog::TOR, "End of stream");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_recv_buffer.insert(m_recv_buffer.end(), buf, buf + nread);
|
||||
try {
|
||||
return ProcessBuffer();
|
||||
} catch (const std::runtime_error& e) {
|
||||
LogWarning("tor: Error processing receive buffer: %s", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool TorControlConnection::ProcessBuffer()
|
||||
{
|
||||
util::LineReader reader(m_recv_buffer, MAX_LINE_LENGTH);
|
||||
auto start = reader.it;
|
||||
|
||||
while (auto line = reader.ReadLine()) {
|
||||
// Skip short lines
|
||||
if (line->size() < 4) continue;
|
||||
|
||||
// Parse: <code><separator><data>
|
||||
// <status>(-|+| )<data>
|
||||
m_message.code = ToIntegral<int>(line->substr(0, 3)).value_or(0);
|
||||
m_message.lines.push_back(line->substr(4));
|
||||
char separator = (*line)[3]; // '-', '+', or ' '
|
||||
|
||||
if (separator == ' ') {
|
||||
if (m_message.code >= 600) {
|
||||
// Async notifications are currently unused
|
||||
// Synchronous and asynchronous messages are never interleaved
|
||||
LogDebug(BCLog::TOR, "Received async notification %i", m_message.code);
|
||||
} else if (!m_reply_handlers.empty()) {
|
||||
// Invoke reply handler with message
|
||||
m_reply_handlers.front()(*this, m_message);
|
||||
m_reply_handlers.pop_front();
|
||||
} else {
|
||||
LogDebug(BCLog::TOR, "Received unexpected sync reply %i", m_message.code);
|
||||
}
|
||||
m_message.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
m_recv_buffer.erase(m_recv_buffer.begin(), m_recv_buffer.begin() + std::distance(start, reader.it));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TorControlConnection::Command(const std::string &cmd, const ReplyHandlerCB& reply_handler)
|
||||
{
|
||||
if (!b_conn)
|
||||
if (!m_sock) return false;
|
||||
|
||||
std::string command = cmd + "\r\n";
|
||||
try {
|
||||
m_sock->SendComplete(std::span<const char>{command}, SOCKET_SEND_TIMEOUT, m_interrupt);
|
||||
} catch (const std::runtime_error& e) {
|
||||
LogWarning("tor: Error sending command: %s", e.what());
|
||||
return false;
|
||||
struct evbuffer *buf = bufferevent_get_output(b_conn);
|
||||
if (!buf)
|
||||
return false;
|
||||
evbuffer_add(buf, cmd.data(), cmd.size());
|
||||
evbuffer_add(buf, "\r\n", 2);
|
||||
reply_handlers.push_back(reply_handler);
|
||||
}
|
||||
|
||||
m_reply_handlers.push_back(reply_handler);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -320,38 +341,89 @@ std::map<std::string,std::string> ParseTorReplyMapping(const std::string &s)
|
||||
return mapping;
|
||||
}
|
||||
|
||||
TorController::TorController(struct event_base* _base, const std::string& tor_control_center, const CService& target):
|
||||
base(_base),
|
||||
m_tor_control_center(tor_control_center), conn(base), reconnect(true), reconnect_timeout(RECONNECT_TIMEOUT_START),
|
||||
m_target(target)
|
||||
TorController::TorController(const std::string& tor_control_center, const CService& target)
|
||||
: m_tor_control_center(tor_control_center),
|
||||
m_conn(m_interrupt),
|
||||
m_reconnect(true),
|
||||
m_reconnect_timeout(RECONNECT_TIMEOUT_START),
|
||||
m_target(target)
|
||||
{
|
||||
reconnect_ev = event_new(base, -1, 0, reconnect_cb, this);
|
||||
if (!reconnect_ev)
|
||||
LogWarning("tor: Failed to create event for reconnection: out of memory?");
|
||||
// Start connection attempts immediately
|
||||
if (!conn.Connect(m_tor_control_center, std::bind_front(&TorController::connected_cb, this),
|
||||
std::bind_front(&TorController::disconnected_cb, this) )) {
|
||||
LogWarning("tor: Initiating connection to Tor control port %s failed", m_tor_control_center);
|
||||
}
|
||||
// Read service private key if cached
|
||||
std::pair<bool,std::string> pkf = ReadBinaryFile(GetPrivateKeyFile());
|
||||
if (pkf.first) {
|
||||
LogDebug(BCLog::TOR, "Reading cached private key from %s\n", fs::PathToString(GetPrivateKeyFile()));
|
||||
private_key = pkf.second;
|
||||
LogDebug(BCLog::TOR, "Reading cached private key from %s", fs::PathToString(GetPrivateKeyFile()));
|
||||
m_private_key = pkf.second;
|
||||
}
|
||||
m_thread = std::thread(&util::TraceThread, "torcontrol", [this] { ThreadControl(); });
|
||||
}
|
||||
|
||||
TorController::~TorController()
|
||||
{
|
||||
if (reconnect_ev) {
|
||||
event_free(reconnect_ev);
|
||||
reconnect_ev = nullptr;
|
||||
Interrupt();
|
||||
Join();
|
||||
if (m_service.IsValid()) {
|
||||
RemoveLocal(m_service);
|
||||
}
|
||||
if (service.IsValid()) {
|
||||
RemoveLocal(service);
|
||||
}
|
||||
|
||||
void TorController::Interrupt()
|
||||
{
|
||||
m_reconnect = false;
|
||||
m_interrupt();
|
||||
}
|
||||
|
||||
void TorController::Join()
|
||||
{
|
||||
if (m_thread.joinable()) {
|
||||
m_thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
void TorController::ThreadControl()
|
||||
{
|
||||
LogDebug(BCLog::TOR, "Entering Tor control thread");
|
||||
|
||||
while (!m_interrupt) {
|
||||
// Try to connect if not connected already
|
||||
if (!m_conn.IsConnected()) {
|
||||
LogDebug(BCLog::TOR, "Attempting to connect to Tor control port %s", m_tor_control_center);
|
||||
|
||||
if (!m_conn.Connect(m_tor_control_center)) {
|
||||
LogWarning("tor: Initiating connection to Tor control port %s failed", m_tor_control_center);
|
||||
if (!m_reconnect) {
|
||||
break;
|
||||
}
|
||||
// Wait before retrying with exponential backoff
|
||||
LogDebug(BCLog::TOR, "Retrying in %.1f seconds", m_reconnect_timeout.count());
|
||||
if (!m_interrupt.sleep_for(std::chrono::duration_cast<std::chrono::milliseconds>(m_reconnect_timeout))) {
|
||||
break;
|
||||
}
|
||||
m_reconnect_timeout = std::min(m_reconnect_timeout * RECONNECT_TIMEOUT_EXP, RECONNECT_TIMEOUT_MAX);
|
||||
continue;
|
||||
}
|
||||
// Successfully connected, reset timeout and trigger connected callback
|
||||
m_reconnect_timeout = RECONNECT_TIMEOUT_START;
|
||||
connected_cb(m_conn);
|
||||
}
|
||||
// Wait for data with a timeout
|
||||
if (!m_conn.WaitForData(std::chrono::seconds(1))) {
|
||||
// Check if still connected
|
||||
if (!m_conn.IsConnected()) {
|
||||
LogDebug(BCLog::TOR, "Lost connection to Tor control port");
|
||||
disconnected_cb(m_conn);
|
||||
continue;
|
||||
}
|
||||
// Just a timeout, continue waiting
|
||||
continue;
|
||||
}
|
||||
// Process incoming data
|
||||
if (!m_conn.ReceiveAndProcess()) {
|
||||
disconnected_cb(m_conn);
|
||||
}
|
||||
}
|
||||
LogDebug(BCLog::TOR, "Exited Tor control thread");
|
||||
}
|
||||
|
||||
void TorController::get_socks_cb(TorControlConnection& _conn, const TorControlReply& reply)
|
||||
{
|
||||
// NOTE: We can only get here if -onion is unset
|
||||
@@ -377,7 +449,7 @@ void TorController::get_socks_cb(TorControlConnection& _conn, const TorControlRe
|
||||
}
|
||||
}
|
||||
if (!socks_location.empty()) {
|
||||
LogDebug(BCLog::TOR, "Get SOCKS port command yielded %s\n", socks_location);
|
||||
LogDebug(BCLog::TOR, "Get SOCKS port command yielded %s", socks_location);
|
||||
} else {
|
||||
LogWarning("tor: Get SOCKS port command returned nothing");
|
||||
}
|
||||
@@ -398,7 +470,7 @@ void TorController::get_socks_cb(TorControlConnection& _conn, const TorControlRe
|
||||
}
|
||||
|
||||
Assume(resolved.IsValid());
|
||||
LogDebug(BCLog::TOR, "Configuring onion proxy for %s\n", resolved.ToStringAddrPort());
|
||||
LogDebug(BCLog::TOR, "Configuring onion proxy for %s", resolved.ToStringAddrPort());
|
||||
|
||||
// Add Tor as proxy for .onion addresses.
|
||||
// Enable stream isolation to prevent connection correlation and enhance privacy, by forcing a different Tor circuit for every connection.
|
||||
@@ -441,31 +513,31 @@ void TorController::add_onion_cb(TorControlConnection& _conn, const TorControlRe
|
||||
std::map<std::string,std::string> m = ParseTorReplyMapping(s);
|
||||
std::map<std::string,std::string>::iterator i;
|
||||
if ((i = m.find("ServiceID")) != m.end())
|
||||
service_id = i->second;
|
||||
m_service_id = i->second;
|
||||
if ((i = m.find("PrivateKey")) != m.end())
|
||||
private_key = i->second;
|
||||
m_private_key = i->second;
|
||||
}
|
||||
if (service_id.empty()) {
|
||||
if (m_service_id.empty()) {
|
||||
LogWarning("tor: Error parsing ADD_ONION parameters:");
|
||||
for (const std::string &s : reply.lines) {
|
||||
LogWarning(" %s", SanitizeString(s));
|
||||
}
|
||||
return;
|
||||
}
|
||||
service = LookupNumeric(std::string(service_id+".onion"), Params().GetDefaultPort());
|
||||
LogInfo("Got tor service ID %s, advertising service %s\n", service_id, service.ToStringAddrPort());
|
||||
if (WriteBinaryFile(GetPrivateKeyFile(), private_key)) {
|
||||
LogDebug(BCLog::TOR, "Cached service private key to %s\n", fs::PathToString(GetPrivateKeyFile()));
|
||||
m_service = LookupNumeric(std::string(m_service_id+".onion"), Params().GetDefaultPort());
|
||||
LogInfo("Got tor service ID %s, advertising service %s", m_service_id, m_service.ToStringAddrPort());
|
||||
if (WriteBinaryFile(GetPrivateKeyFile(), m_private_key)) {
|
||||
LogDebug(BCLog::TOR, "Cached service private key to %s", fs::PathToString(GetPrivateKeyFile()));
|
||||
} else {
|
||||
LogWarning("tor: Error writing service private key to %s", fs::PathToString(GetPrivateKeyFile()));
|
||||
}
|
||||
AddLocal(service, LOCAL_MANUAL);
|
||||
AddLocal(m_service, LOCAL_MANUAL);
|
||||
// ... 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),
|
||||
_conn.Command(MakeAddOnionCmd(m_private_key, m_target.ToStringAddrPort(), /*enable_pow=*/false),
|
||||
[this](TorControlConnection& conn, const TorControlReply& reply) {
|
||||
add_onion_cb(conn, reply, /*pow_was_enabled=*/false);
|
||||
});
|
||||
@@ -477,7 +549,7 @@ void TorController::add_onion_cb(TorControlConnection& _conn, const TorControlRe
|
||||
void TorController::auth_cb(TorControlConnection& _conn, const TorControlReply& reply)
|
||||
{
|
||||
if (reply.code == TOR_REPLY_OK) {
|
||||
LogDebug(BCLog::TOR, "Authentication successful\n");
|
||||
LogDebug(BCLog::TOR, "Authentication successful");
|
||||
|
||||
// Now that we know Tor is running setup the proxy for onion addresses
|
||||
// if -onion isn't set to something else.
|
||||
@@ -486,11 +558,11 @@ void TorController::auth_cb(TorControlConnection& _conn, const TorControlReply&
|
||||
}
|
||||
|
||||
// Finally - now create the service
|
||||
if (private_key.empty()) { // No private key, generate one
|
||||
private_key = "NEW:ED25519-V3"; // Explicitly request key type - see issue #9214
|
||||
if (m_private_key.empty()) { // No private key, generate one
|
||||
m_private_key = "NEW:ED25519-V3"; // Explicitly request key type - see issue #9214
|
||||
}
|
||||
// Request onion service, redirect port.
|
||||
_conn.Command(MakeAddOnionCmd(private_key, m_target.ToStringAddrPort(), /*enable_pow=*/true),
|
||||
_conn.Command(MakeAddOnionCmd(m_private_key, m_target.ToStringAddrPort(), /*enable_pow=*/true),
|
||||
[this](TorControlConnection& conn, const TorControlReply& reply) {
|
||||
add_onion_cb(conn, reply, /*pow_was_enabled=*/true);
|
||||
});
|
||||
@@ -515,13 +587,13 @@ void TorController::auth_cb(TorControlConnection& _conn, const TorControlReply&
|
||||
* CookieString | ClientNonce | ServerNonce)
|
||||
*
|
||||
*/
|
||||
static std::vector<uint8_t> ComputeResponse(const std::string &key, const std::vector<uint8_t> &cookie, const std::vector<uint8_t> &clientNonce, const std::vector<uint8_t> &serverNonce)
|
||||
static std::vector<uint8_t> ComputeResponse(std::string_view key, std::span<const uint8_t> cookie, std::span<const uint8_t> client_nonce, std::span<const uint8_t> server_nonce)
|
||||
{
|
||||
CHMAC_SHA256 computeHash((const uint8_t*)key.data(), key.size());
|
||||
std::vector<uint8_t> computedHash(CHMAC_SHA256::OUTPUT_SIZE, 0);
|
||||
computeHash.Write(cookie.data(), cookie.size());
|
||||
computeHash.Write(clientNonce.data(), clientNonce.size());
|
||||
computeHash.Write(serverNonce.data(), serverNonce.size());
|
||||
computeHash.Write(client_nonce.data(), client_nonce.size());
|
||||
computeHash.Write(server_nonce.data(), server_nonce.size());
|
||||
computeHash.Finalize(computedHash.data());
|
||||
return computedHash;
|
||||
}
|
||||
@@ -529,7 +601,11 @@ static std::vector<uint8_t> ComputeResponse(const std::string &key, const std::v
|
||||
void TorController::authchallenge_cb(TorControlConnection& _conn, const TorControlReply& reply)
|
||||
{
|
||||
if (reply.code == TOR_REPLY_OK) {
|
||||
LogDebug(BCLog::TOR, "SAFECOOKIE authentication challenge successful\n");
|
||||
LogDebug(BCLog::TOR, "SAFECOOKIE authentication challenge successful");
|
||||
if (reply.lines.empty()) {
|
||||
LogWarning("tor: AUTHCHALLENGE reply was empty");
|
||||
return;
|
||||
}
|
||||
std::pair<std::string,std::string> l = SplitTorReplyLine(reply.lines[0]);
|
||||
if (l.first == "AUTHCHALLENGE") {
|
||||
std::map<std::string,std::string> m = ParseTorReplyMapping(l.second);
|
||||
@@ -537,21 +613,21 @@ void TorController::authchallenge_cb(TorControlConnection& _conn, const TorContr
|
||||
LogWarning("tor: Error parsing AUTHCHALLENGE parameters: %s", SanitizeString(l.second));
|
||||
return;
|
||||
}
|
||||
std::vector<uint8_t> serverHash = ParseHex(m["SERVERHASH"]);
|
||||
std::vector<uint8_t> serverNonce = ParseHex(m["SERVERNONCE"]);
|
||||
LogDebug(BCLog::TOR, "AUTHCHALLENGE ServerHash %s ServerNonce %s\n", HexStr(serverHash), HexStr(serverNonce));
|
||||
if (serverNonce.size() != 32) {
|
||||
std::vector<uint8_t> server_hash = ParseHex(m["SERVERHASH"]);
|
||||
std::vector<uint8_t> server_nonce = ParseHex(m["SERVERNONCE"]);
|
||||
LogDebug(BCLog::TOR, "AUTHCHALLENGE ServerHash %s ServerNonce %s", HexStr(server_hash), HexStr(server_nonce));
|
||||
if (server_nonce.size() != 32) {
|
||||
LogWarning("tor: ServerNonce is not 32 bytes, as required by spec");
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> computedServerHash = ComputeResponse(TOR_SAFE_SERVERKEY, cookie, clientNonce, serverNonce);
|
||||
if (computedServerHash != serverHash) {
|
||||
LogWarning("tor: ServerHash %s does not match expected ServerHash %s", HexStr(serverHash), HexStr(computedServerHash));
|
||||
std::vector<uint8_t> computed_server_hash = ComputeResponse(TOR_SAFE_SERVERKEY, m_cookie, m_client_nonce, server_nonce);
|
||||
if (computed_server_hash != server_hash) {
|
||||
LogWarning("tor: ServerHash %s does not match expected ServerHash %s", HexStr(server_hash), HexStr(computed_server_hash));
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> computedClientHash = ComputeResponse(TOR_SAFE_CLIENTKEY, cookie, clientNonce, serverNonce);
|
||||
std::vector<uint8_t> computedClientHash = ComputeResponse(TOR_SAFE_CLIENTKEY, m_cookie, m_client_nonce, server_nonce);
|
||||
_conn.Command("AUTHENTICATE " + HexStr(computedClientHash), std::bind_front(&TorController::auth_cb, this));
|
||||
} else {
|
||||
LogWarning("tor: Invalid reply to AUTHCHALLENGE");
|
||||
@@ -586,12 +662,12 @@ void TorController::protocolinfo_cb(TorControlConnection& _conn, const TorContro
|
||||
std::map<std::string,std::string> m = ParseTorReplyMapping(l.second);
|
||||
std::map<std::string,std::string>::iterator i;
|
||||
if ((i = m.find("Tor")) != m.end()) {
|
||||
LogDebug(BCLog::TOR, "Connected to Tor version %s\n", i->second);
|
||||
LogDebug(BCLog::TOR, "Connected to Tor version %s", i->second);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const std::string &s : methods) {
|
||||
LogDebug(BCLog::TOR, "Supported authentication method: %s\n", s);
|
||||
LogDebug(BCLog::TOR, "Supported authentication method: %s", s);
|
||||
}
|
||||
// Prefer NULL, otherwise SAFECOOKIE. If a password is provided, use HASHEDPASSWORD
|
||||
/* Authentication:
|
||||
@@ -601,25 +677,25 @@ void TorController::protocolinfo_cb(TorControlConnection& _conn, const TorContro
|
||||
std::string torpassword = gArgs.GetArg("-torpassword", "");
|
||||
if (!torpassword.empty()) {
|
||||
if (methods.contains("HASHEDPASSWORD")) {
|
||||
LogDebug(BCLog::TOR, "Using HASHEDPASSWORD authentication\n");
|
||||
LogDebug(BCLog::TOR, "Using HASHEDPASSWORD authentication");
|
||||
ReplaceAll(torpassword, "\"", "\\\"");
|
||||
_conn.Command("AUTHENTICATE \"" + torpassword + "\"", std::bind_front(&TorController::auth_cb, this));
|
||||
} else {
|
||||
LogWarning("tor: Password provided with -torpassword, but HASHEDPASSWORD authentication is not available");
|
||||
}
|
||||
} else if (methods.contains("NULL")) {
|
||||
LogDebug(BCLog::TOR, "Using NULL authentication\n");
|
||||
LogDebug(BCLog::TOR, "Using NULL authentication");
|
||||
_conn.Command("AUTHENTICATE", std::bind_front(&TorController::auth_cb, this));
|
||||
} else if (methods.contains("SAFECOOKIE")) {
|
||||
// Cookie: hexdump -e '32/1 "%02x""\n"' ~/.tor/control_auth_cookie
|
||||
LogDebug(BCLog::TOR, "Using SAFECOOKIE authentication, reading cookie authentication from %s\n", cookiefile);
|
||||
LogDebug(BCLog::TOR, "Using SAFECOOKIE authentication, reading cookie authentication from %s", cookiefile);
|
||||
std::pair<bool,std::string> status_cookie = ReadBinaryFile(fs::PathFromString(cookiefile), TOR_COOKIE_SIZE);
|
||||
if (status_cookie.first && status_cookie.second.size() == TOR_COOKIE_SIZE) {
|
||||
// _conn.Command("AUTHENTICATE " + HexStr(status_cookie.second), std::bind_front(&TorController::auth_cb, this));
|
||||
cookie = std::vector<uint8_t>(status_cookie.second.begin(), status_cookie.second.end());
|
||||
clientNonce = std::vector<uint8_t>(TOR_NONCE_SIZE, 0);
|
||||
GetRandBytes(clientNonce);
|
||||
_conn.Command("AUTHCHALLENGE SAFECOOKIE " + HexStr(clientNonce), std::bind_front(&TorController::authchallenge_cb, this));
|
||||
m_cookie = std::vector<uint8_t>(status_cookie.second.begin(), status_cookie.second.end());
|
||||
m_client_nonce = std::vector<uint8_t>(TOR_NONCE_SIZE, 0);
|
||||
GetRandBytes(m_client_nonce);
|
||||
_conn.Command("AUTHCHALLENGE SAFECOOKIE " + HexStr(m_client_nonce), std::bind_front(&TorController::authchallenge_cb, this));
|
||||
} else {
|
||||
if (status_cookie.first) {
|
||||
LogWarning("tor: Authentication cookie %s is not exactly %i bytes, as is required by the spec", cookiefile, TOR_COOKIE_SIZE);
|
||||
@@ -639,7 +715,7 @@ void TorController::protocolinfo_cb(TorControlConnection& _conn, const TorContro
|
||||
|
||||
void TorController::connected_cb(TorControlConnection& _conn)
|
||||
{
|
||||
reconnect_timeout = RECONNECT_TIMEOUT_START;
|
||||
m_reconnect_timeout = RECONNECT_TIMEOUT_START;
|
||||
// First send a PROTOCOLINFO command to figure out what authentication is expected
|
||||
if (!_conn.Command("PROTOCOLINFO 1", std::bind_front(&TorController::protocolinfo_cb, this)))
|
||||
LogWarning("tor: Error sending initial protocolinfo command");
|
||||
@@ -648,32 +724,14 @@ void TorController::connected_cb(TorControlConnection& _conn)
|
||||
void TorController::disconnected_cb(TorControlConnection& _conn)
|
||||
{
|
||||
// Stop advertising service when disconnected
|
||||
if (service.IsValid())
|
||||
RemoveLocal(service);
|
||||
service = CService();
|
||||
if (!reconnect)
|
||||
if (m_service.IsValid())
|
||||
RemoveLocal(m_service);
|
||||
m_service = CService();
|
||||
if (!m_reconnect)
|
||||
return;
|
||||
|
||||
LogDebug(BCLog::TOR, "Not connected to Tor control port %s, retrying in %.2f s\n",
|
||||
m_tor_control_center, reconnect_timeout);
|
||||
|
||||
// Single-shot timer for reconnect. Use exponential backoff with a maximum.
|
||||
struct timeval time = MillisToTimeval(int64_t(reconnect_timeout * 1000.0));
|
||||
if (reconnect_ev)
|
||||
event_add(reconnect_ev, &time);
|
||||
|
||||
reconnect_timeout = std::min(reconnect_timeout * RECONNECT_TIMEOUT_EXP, RECONNECT_TIMEOUT_MAX);
|
||||
}
|
||||
|
||||
void TorController::Reconnect()
|
||||
{
|
||||
/* Try to reconnect and reestablish if we get booted - for example, Tor
|
||||
* may be restarting.
|
||||
*/
|
||||
if (!conn.Connect(m_tor_control_center, std::bind_front(&TorController::connected_cb, this),
|
||||
std::bind_front(&TorController::disconnected_cb, this) )) {
|
||||
LogWarning("tor: Re-initiating connection to Tor control port %s failed", m_tor_control_center);
|
||||
}
|
||||
LogDebug(BCLog::TOR, "Not connected to Tor control port %s, will retry", m_tor_control_center);
|
||||
_conn.Disconnect();
|
||||
}
|
||||
|
||||
fs::path TorController::GetPrivateKeyFile()
|
||||
@@ -681,61 +739,6 @@ fs::path TorController::GetPrivateKeyFile()
|
||||
return gArgs.GetDataDirNet() / "onion_v3_private_key";
|
||||
}
|
||||
|
||||
void TorController::reconnect_cb(evutil_socket_t fd, short what, void *arg)
|
||||
{
|
||||
TorController *self = static_cast<TorController*>(arg);
|
||||
self->Reconnect();
|
||||
}
|
||||
|
||||
/****** Thread ********/
|
||||
static struct event_base *gBase;
|
||||
static std::thread torControlThread;
|
||||
|
||||
static void TorControlThread(CService onion_service_target)
|
||||
{
|
||||
TorController ctrl(gBase, gArgs.GetArg("-torcontrol", DEFAULT_TOR_CONTROL), onion_service_target);
|
||||
|
||||
event_base_dispatch(gBase);
|
||||
}
|
||||
|
||||
void StartTorControl(CService onion_service_target)
|
||||
{
|
||||
assert(!gBase);
|
||||
#ifdef WIN32
|
||||
evthread_use_windows_threads();
|
||||
#else
|
||||
evthread_use_pthreads();
|
||||
#endif
|
||||
gBase = event_base_new();
|
||||
if (!gBase) {
|
||||
LogWarning("tor: Unable to create event_base");
|
||||
return;
|
||||
}
|
||||
|
||||
torControlThread = std::thread(&util::TraceThread, "torcontrol", [onion_service_target] {
|
||||
TorControlThread(onion_service_target);
|
||||
});
|
||||
}
|
||||
|
||||
void InterruptTorControl()
|
||||
{
|
||||
if (gBase) {
|
||||
LogInfo("tor: Thread interrupt\n");
|
||||
event_base_once(gBase, -1, EV_TIMEOUT, [](evutil_socket_t, short, void*) {
|
||||
event_base_loopbreak(gBase);
|
||||
}, nullptr, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void StopTorControl()
|
||||
{
|
||||
if (gBase) {
|
||||
torControlThread.join();
|
||||
event_base_free(gBase);
|
||||
gBase = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
CService DefaultOnionServiceTarget(uint16_t port)
|
||||
{
|
||||
struct in_addr onion_service_target;
|
||||
|
||||
100
src/torcontrol.h
100
src/torcontrol.h
@@ -10,13 +10,15 @@
|
||||
|
||||
#include <netaddress.h>
|
||||
#include <util/fs.h>
|
||||
|
||||
#include <event2/util.h>
|
||||
#include <util/sock.h>
|
||||
#include <util/threadinterrupt.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
constexpr uint16_t DEFAULT_TOR_SOCKS_PORT{9050};
|
||||
@@ -29,10 +31,6 @@ 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();
|
||||
void StopTorControl();
|
||||
|
||||
CService DefaultOnionServiceTarget(uint16_t port);
|
||||
|
||||
/** Reply from Tor, can be single or multi-line */
|
||||
@@ -57,22 +55,19 @@ public:
|
||||
class TorControlConnection
|
||||
{
|
||||
public:
|
||||
typedef std::function<void(TorControlConnection&)> ConnectionCB;
|
||||
typedef std::function<void(TorControlConnection &,const TorControlReply &)> ReplyHandlerCB;
|
||||
|
||||
/** Create a new TorControlConnection.
|
||||
*/
|
||||
explicit TorControlConnection(struct event_base *base);
|
||||
explicit TorControlConnection(CThreadInterrupt& interrupt);
|
||||
~TorControlConnection();
|
||||
|
||||
/**
|
||||
* Connect to a Tor control port.
|
||||
* tor_control_center is address of the form host:port.
|
||||
* connected is the handler that is called when connection is successfully established.
|
||||
* disconnected is a handler that is called when the connection is broken.
|
||||
* Return true on success.
|
||||
*/
|
||||
bool Connect(const std::string& tor_control_center, const ConnectionCB& connected, const ConnectionCB& disconnected);
|
||||
bool Connect(const std::string& tor_control_center);
|
||||
|
||||
/**
|
||||
* Disconnect from Tor control port.
|
||||
@@ -85,23 +80,38 @@ public:
|
||||
*/
|
||||
bool Command(const std::string &cmd, const ReplyHandlerCB& reply_handler);
|
||||
|
||||
private:
|
||||
/** Callback when ready for use */
|
||||
std::function<void(TorControlConnection&)> connected;
|
||||
/** Callback when connection lost */
|
||||
std::function<void(TorControlConnection&)> disconnected;
|
||||
/** Libevent event base */
|
||||
struct event_base *base;
|
||||
/** Connection to control socket */
|
||||
struct bufferevent* b_conn{nullptr};
|
||||
/** Message being received */
|
||||
TorControlReply message;
|
||||
/** Response handlers */
|
||||
std::deque<ReplyHandlerCB> reply_handlers;
|
||||
/**
|
||||
* Check if the connection is established.
|
||||
*/
|
||||
bool IsConnected() const;
|
||||
|
||||
/** Libevent handlers: internal */
|
||||
static void readcb(struct bufferevent *bev, void *ctx);
|
||||
static void eventcb(struct bufferevent *bev, short what, void *ctx);
|
||||
/**
|
||||
* Wait for data to be available on the socket.
|
||||
* @param[in] timeout Maximum time to wait
|
||||
* @return true if data is available, false on timeout or error
|
||||
*/
|
||||
bool WaitForData(std::chrono::milliseconds timeout);
|
||||
|
||||
/**
|
||||
* Read available data from socket and process complete replies.
|
||||
* Dispatches to registered reply handlers.
|
||||
* @return true if connection is still open, false if connection was closed
|
||||
*/
|
||||
bool ReceiveAndProcess();
|
||||
|
||||
private:
|
||||
/** Reference to interrupt object for clean shutdown */
|
||||
CThreadInterrupt& m_interrupt;
|
||||
/** Socket for the connection */
|
||||
std::unique_ptr<Sock> m_sock;
|
||||
/** Message being received */
|
||||
TorControlReply m_message;
|
||||
/** Response handlers */
|
||||
std::deque<ReplyHandlerCB> m_reply_handlers;
|
||||
/** Buffer for incoming data */
|
||||
std::vector<std::byte> m_recv_buffer;
|
||||
/** Process complete lines from the receive buffer */
|
||||
bool ProcessBuffer();
|
||||
};
|
||||
|
||||
/****** Bitcoin specific TorController implementation ********/
|
||||
@@ -112,8 +122,8 @@ private:
|
||||
class TorController
|
||||
{
|
||||
public:
|
||||
TorController(struct event_base* base, const std::string& tor_control_center, const CService& target);
|
||||
TorController() : conn{nullptr} {
|
||||
TorController(const std::string& tor_control_center, const CService& target);
|
||||
TorController() : m_conn(m_interrupt) {
|
||||
// Used for testing only.
|
||||
}
|
||||
~TorController();
|
||||
@@ -121,23 +131,28 @@ public:
|
||||
/** Get name of file to store private key in */
|
||||
fs::path GetPrivateKeyFile();
|
||||
|
||||
/** Reconnect, after getting disconnected */
|
||||
void Reconnect();
|
||||
/** Interrupt the controller thread */
|
||||
void Interrupt();
|
||||
|
||||
/** Wait for the controller thread to exit */
|
||||
void Join();
|
||||
private:
|
||||
struct event_base* base;
|
||||
CThreadInterrupt m_interrupt;
|
||||
std::thread m_thread;
|
||||
const std::string m_tor_control_center;
|
||||
TorControlConnection conn;
|
||||
std::string private_key;
|
||||
std::string service_id;
|
||||
bool reconnect;
|
||||
struct event *reconnect_ev = nullptr;
|
||||
float reconnect_timeout;
|
||||
CService service;
|
||||
TorControlConnection m_conn;
|
||||
std::string m_private_key;
|
||||
std::string m_service_id;
|
||||
std::atomic<bool> m_reconnect;
|
||||
std::chrono::duration<double> m_reconnect_timeout;
|
||||
CService m_service;
|
||||
const CService m_target;
|
||||
/** Cookie for SAFECOOKIE auth */
|
||||
std::vector<uint8_t> cookie;
|
||||
std::vector<uint8_t> m_cookie;
|
||||
/** ClientNonce for SAFECOOKIE auth */
|
||||
std::vector<uint8_t> clientNonce;
|
||||
std::vector<uint8_t> m_client_nonce;
|
||||
/** Main control thread */
|
||||
void ThreadControl();
|
||||
|
||||
public:
|
||||
/** Callback for GETINFO net/listeners/socks result */
|
||||
@@ -154,9 +169,6 @@ public:
|
||||
void connected_cb(TorControlConnection& conn);
|
||||
/** Callback after connection lost or failed connection attempt */
|
||||
void disconnected_cb(TorControlConnection& conn);
|
||||
|
||||
/** Callback for reconnect timer */
|
||||
static void reconnect_cb(evutil_socket_t fd, short what, void *arg);
|
||||
};
|
||||
|
||||
#endif // BITCOIN_TORCONTROL_H
|
||||
|
||||
233
test/functional/feature_torcontrol.py
Executable file
233
test/functional/feature_torcontrol.py
Executable file
@@ -0,0 +1,233 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) The Bitcoin Core developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
"""Test torcontrol functionality with a mock Tor control server."""
|
||||
import socket
|
||||
import threading
|
||||
from test_framework.test_framework import BitcoinTestFramework
|
||||
from test_framework.util import (
|
||||
assert_equal,
|
||||
ensure_for,
|
||||
p2p_port,
|
||||
)
|
||||
|
||||
|
||||
class MockTorControlServer:
|
||||
def __init__(self, port, manual_mode=False):
|
||||
self.port = port
|
||||
self.sock = None
|
||||
self.conn = None
|
||||
self.running = False
|
||||
self.thread = None
|
||||
self.received_commands = []
|
||||
self.manual_mode = manual_mode
|
||||
self.conn_ready = threading.Event()
|
||||
|
||||
def start(self):
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self.sock.settimeout(1.0)
|
||||
self.sock.bind(('127.0.0.1', self.port))
|
||||
self.sock.listen(1)
|
||||
self.running = True
|
||||
self.thread = threading.Thread(target=self._serve)
|
||||
self.thread.daemon = True
|
||||
self.thread.start()
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
if self.conn:
|
||||
self.conn.close()
|
||||
if self.sock:
|
||||
self.sock.close()
|
||||
if self.thread:
|
||||
self.thread.join(timeout=5)
|
||||
|
||||
def _serve(self):
|
||||
while self.running:
|
||||
try:
|
||||
self.conn, _ = self.sock.accept()
|
||||
self.conn.settimeout(1.0)
|
||||
self.conn_ready.set()
|
||||
self._handle_connection(self.conn)
|
||||
except socket.timeout:
|
||||
continue
|
||||
except OSError:
|
||||
break
|
||||
|
||||
def _handle_connection(self, conn):
|
||||
try:
|
||||
buf = b""
|
||||
while self.running:
|
||||
try:
|
||||
data = conn.recv(1024)
|
||||
if not data:
|
||||
break
|
||||
buf += data
|
||||
while b"\r\n" in buf:
|
||||
line, buf = buf.split(b"\r\n", 1)
|
||||
command = line.decode('utf-8').strip()
|
||||
if command:
|
||||
self.received_commands.append(command)
|
||||
if not self.manual_mode:
|
||||
response = self._get_response(command)
|
||||
conn.sendall(response.encode('utf-8'))
|
||||
except socket.timeout:
|
||||
continue
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def send_raw(self, data):
|
||||
if self.conn:
|
||||
self.conn.sendall(data.encode('utf-8'))
|
||||
|
||||
def _get_response(self, command):
|
||||
if command == "PROTOCOLINFO 1":
|
||||
return (
|
||||
"250-PROTOCOLINFO 1\r\n"
|
||||
"250-AUTH METHODS=NULL\r\n"
|
||||
"250-VERSION Tor=\"0.1.2.3\"\r\n"
|
||||
"250 OK\r\n"
|
||||
)
|
||||
elif command == "AUTHENTICATE":
|
||||
return "250 OK\r\n"
|
||||
elif command.startswith("ADD_ONION"):
|
||||
return (
|
||||
"250-ServiceID=testserviceid1234567890123456789012345678901234567890123456\r\n"
|
||||
"250 OK\r\n"
|
||||
)
|
||||
elif command.startswith("GETINFO"):
|
||||
return "250-net/listeners/socks=\"127.0.0.1:9050\"\r\n250 OK\r\n"
|
||||
else:
|
||||
return "510 Unrecognized command\r\n"
|
||||
|
||||
|
||||
class TorControlTest(BitcoinTestFramework):
|
||||
def set_test_params(self):
|
||||
self.num_nodes = 1
|
||||
|
||||
def next_port(self):
|
||||
self._port_counter = getattr(self, '_port_counter', 0) + 1
|
||||
return p2p_port(self.num_nodes + self._port_counter)
|
||||
|
||||
def restart_with_mock(self, mock_tor):
|
||||
mock_tor.start()
|
||||
self.restart_node(0, extra_args=[
|
||||
f"-torcontrol=127.0.0.1:{mock_tor.port}",
|
||||
"-listenonion=1",
|
||||
"-debug=tor",
|
||||
])
|
||||
|
||||
def test_basic(self):
|
||||
self.log.info("Test Tor control basic functionality")
|
||||
|
||||
mock_tor = MockTorControlServer(self.next_port())
|
||||
self.restart_with_mock(mock_tor)
|
||||
|
||||
# Waiting for Tor control commands
|
||||
self.wait_until(lambda: len(mock_tor.received_commands) >= 4, timeout=10)
|
||||
|
||||
# Verify expected protocol sequence
|
||||
assert_equal(mock_tor.received_commands[0], "PROTOCOLINFO 1")
|
||||
assert_equal(mock_tor.received_commands[1], "AUTHENTICATE")
|
||||
assert_equal(mock_tor.received_commands[2], "GETINFO net/listeners/socks")
|
||||
assert mock_tor.received_commands[3].startswith("ADD_ONION ")
|
||||
assert "PoWDefensesEnabled=1" in mock_tor.received_commands[3]
|
||||
|
||||
# Clean up
|
||||
mock_tor.stop()
|
||||
|
||||
def test_partial_data(self):
|
||||
self.log.info("Test that partial Tor control responses are buffered until complete")
|
||||
|
||||
mock_tor = MockTorControlServer(self.next_port(), manual_mode=True)
|
||||
self.restart_with_mock(mock_tor)
|
||||
|
||||
# Wait for connection and PROTOCOLINFO command
|
||||
mock_tor.conn_ready.wait(timeout=10)
|
||||
self.wait_until(lambda: len(mock_tor.received_commands) >= 1, timeout=10)
|
||||
assert_equal(mock_tor.received_commands[0], "PROTOCOLINFO 1")
|
||||
|
||||
# Send partial response (no \r\n on last line)
|
||||
mock_tor.send_raw(
|
||||
"250-PROTOCOLINFO 1\r\n"
|
||||
"250-AUTH METHODS=NULL\r\n"
|
||||
"250 OK"
|
||||
)
|
||||
|
||||
# Verify AUTHENTICATE is not sent
|
||||
ensure_for(duration=2, f=lambda: len(mock_tor.received_commands) == 1)
|
||||
|
||||
# Complete the response
|
||||
mock_tor.send_raw("\r\n")
|
||||
|
||||
# Should now process the complete response and send AUTHENTICATE
|
||||
self.wait_until(lambda: len(mock_tor.received_commands) >= 2, timeout=5)
|
||||
assert_equal(mock_tor.received_commands[1], "AUTHENTICATE")
|
||||
|
||||
# Clean up
|
||||
mock_tor.stop()
|
||||
|
||||
def test_pow_fallback(self):
|
||||
self.log.info("Test that ADD_ONION retries without PoW on 512 error")
|
||||
|
||||
class NoPowServer(MockTorControlServer):
|
||||
def _get_response(self, command):
|
||||
if command.startswith("ADD_ONION"):
|
||||
if "PoWDefensesEnabled=1" in command:
|
||||
return "512 Unrecognized option\r\n"
|
||||
else:
|
||||
return (
|
||||
"250-ServiceID=testserviceid1234567890123456789012345678901234567890123456\r\n"
|
||||
"250 OK\r\n"
|
||||
)
|
||||
return super()._get_response(command)
|
||||
|
||||
mock_tor = NoPowServer(self.next_port())
|
||||
self.restart_with_mock(mock_tor)
|
||||
|
||||
# Expect: PROTOCOLINFO, AUTHENTICATE, GETINFO, ADD_ONION (with PoW), ADD_ONION (without PoW)
|
||||
self.wait_until(lambda: len(mock_tor.received_commands) >= 5, timeout=10)
|
||||
|
||||
# First ADD_ONION should have PoW enabled
|
||||
assert mock_tor.received_commands[3].startswith("ADD_ONION ")
|
||||
assert "PoWDefensesEnabled=1" in mock_tor.received_commands[3]
|
||||
|
||||
# Retry should be ADD_ONION without PoW
|
||||
assert mock_tor.received_commands[4].startswith("ADD_ONION ")
|
||||
assert "PoWDefensesEnabled=1" not in mock_tor.received_commands[4]
|
||||
|
||||
# Clean up
|
||||
mock_tor.stop()
|
||||
|
||||
def test_oversized_line(self):
|
||||
self.log.info("Test that Tor control disconnects on oversized response lines")
|
||||
|
||||
mock_tor = MockTorControlServer(self.next_port(), manual_mode=True)
|
||||
self.restart_with_mock(mock_tor)
|
||||
|
||||
# Wait for connection and PROTOCOLINFO command.
|
||||
mock_tor.conn_ready.wait(timeout=10)
|
||||
self.wait_until(lambda: len(mock_tor.received_commands) >= 1, timeout=10)
|
||||
assert_equal(mock_tor.received_commands[0], "PROTOCOLINFO 1")
|
||||
|
||||
# Send a single line longer than MAX_LINE_LENGTH. The node should disconnect.
|
||||
MAX_LINE_LENGTH = 100000
|
||||
mock_tor.send_raw("250-" + ("A" * (MAX_LINE_LENGTH + 1)) + "\r\n")
|
||||
ensure_for(duration=2, f=lambda: self.nodes[0].process.poll() is None)
|
||||
|
||||
# Connection should be dropped and retried, causing another PROTOCOLINFO.
|
||||
self.wait_until(lambda: len(mock_tor.received_commands) >= 2, timeout=10)
|
||||
assert_equal(mock_tor.received_commands[1], "PROTOCOLINFO 1")
|
||||
|
||||
mock_tor.stop()
|
||||
|
||||
def run_test(self):
|
||||
self.test_basic()
|
||||
self.test_partial_data()
|
||||
self.test_pow_fallback()
|
||||
self.test_oversized_line()
|
||||
|
||||
if __name__ == '__main__':
|
||||
TorControlTest(__file__).main()
|
||||
@@ -352,6 +352,7 @@ BASE_SCRIPTS = [
|
||||
'tool_bitcoin.py',
|
||||
'p2p_sendtxrcncl.py',
|
||||
'rpc_scantxoutset.py',
|
||||
'feature_torcontrol.py',
|
||||
'feature_unsupported_utxo_db.py',
|
||||
'mempool_cluster.py',
|
||||
'feature_logging.py',
|
||||
|
||||
Reference in New Issue
Block a user