From a36591d1948359450746b1e7aa5da588c3a020eb Mon Sep 17 00:00:00 2001 From: Fabian Jahr Date: Sat, 27 Dec 2025 01:32:42 +0100 Subject: [PATCH 01/10] refactor: Use constexpr in torcontrol where possible --- src/torcontrol.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/torcontrol.cpp b/src/torcontrol.cpp index b948de3e5bc..9abc1fc032f 100644 --- a/src/torcontrol.cpp +++ b/src/torcontrol.cpp @@ -50,24 +50,24 @@ 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; +constexpr int TOR_NONCE_SIZE = 32; /** 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 */ 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 float RECONNECT_TIMEOUT_START = 1.0; /** Exponential backoff configuration - growth factor */ -static const float RECONNECT_TIMEOUT_EXP = 1.5; +constexpr float RECONNECT_TIMEOUT_EXP = 1.5; /** Maximum reconnect timeout in seconds to prevent excessive delays */ -static const float RECONNECT_TIMEOUT_MAX = 600.0; +constexpr float 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; /****** Low-level TorControlConnection ********/ From 6bcb60354e678b8da6173cc64aaee1071ba54ef1 Mon Sep 17 00:00:00 2001 From: Fabian Jahr Date: Sat, 27 Dec 2025 01:46:05 +0100 Subject: [PATCH 02/10] refactor: Modernize member variable names in torcontrol --- src/torcontrol.cpp | 94 +++++++++++++++++++++++----------------------- src/torcontrol.h | 20 +++++----- 2 files changed, 57 insertions(+), 57 deletions(-) diff --git a/src/torcontrol.cpp b/src/torcontrol.cpp index 9abc1fc032f..ffb783162ba 100644 --- a/src/torcontrol.cpp +++ b/src/torcontrol.cpp @@ -53,7 +53,7 @@ const std::string DEFAULT_TOR_CONTROL = "127.0.0.1:" + ToString(DEFAULT_TOR_CONT constexpr int TOR_COOKIE_SIZE = 32; /** Size of client/server nonce for SAFECOOKIE */ constexpr int TOR_NONCE_SIZE = 32; -/** For computing serverHash in SAFECOOKIE */ +/** 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"; @@ -97,25 +97,25 @@ void TorControlConnection::readcb(struct bufferevent *bev, void *ctx) if (s.size() < 4) // Short line continue; // (-|+| ) - self->message.code = ToIntegral(s.substr(0, 3)).value_or(0); - self->message.lines.push_back(s.substr(4)); + self->m_message.code = ToIntegral(s.substr(0, 3)).value_or(0); + self->m_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) { + if (self->m_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.front()(*self, self->m_message); self->reply_handlers.pop_front(); } else { - LogDebug(BCLog::TOR, "Received unexpected sync reply %i\n", self->message.code); + LogDebug(BCLog::TOR, "Received unexpected sync reply %i\n", self->m_message.code); } } - self->message.Clear(); + self->m_message.Clear(); } } // Check for size of buffer - protect against memory exhaustion with very long lines @@ -322,14 +322,14 @@ std::map ParseTorReplyMapping(const std::string &s) 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_tor_control_center(tor_control_center), m_conn(base), 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), + if (!m_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); } @@ -337,7 +337,7 @@ TorController::TorController(struct event_base* _base, const std::string& tor_co std::pair pkf = ReadBinaryFile(GetPrivateKeyFile()); if (pkf.first) { LogDebug(BCLog::TOR, "Reading cached private key from %s\n", fs::PathToString(GetPrivateKeyFile())); - private_key = pkf.second; + m_private_key = pkf.second; } } @@ -347,8 +347,8 @@ TorController::~TorController() event_free(reconnect_ev); reconnect_ev = nullptr; } - if (service.IsValid()) { - RemoveLocal(service); + if (m_service.IsValid()) { + RemoveLocal(m_service); } } @@ -441,31 +441,31 @@ void TorController::add_onion_cb(TorControlConnection& _conn, const TorControlRe std::map m = ParseTorReplyMapping(s); std::map::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)) { + m_service = LookupNumeric(std::string(m_service_id+".onion"), Params().GetDefaultPort()); + LogInfo("Got tor service ID %s, advertising service %s\n", m_service_id, m_service.ToStringAddrPort()); + if (WriteBinaryFile(GetPrivateKeyFile(), m_private_key)) { LogDebug(BCLog::TOR, "Cached service private key to %s\n", 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); }); @@ -486,11 +486,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 +515,13 @@ void TorController::auth_cb(TorControlConnection& _conn, const TorControlReply& * CookieString | ClientNonce | ServerNonce) * */ -static std::vector ComputeResponse(const std::string &key, const std::vector &cookie, const std::vector &clientNonce, const std::vector &serverNonce) +static std::vector ComputeResponse(const std::string &key, const std::vector &cookie, const std::vector &client_nonce, const std::vector &server_nonce) { CHMAC_SHA256 computeHash((const uint8_t*)key.data(), key.size()); std::vector 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; } @@ -537,21 +537,21 @@ void TorController::authchallenge_cb(TorControlConnection& _conn, const TorContr LogWarning("tor: Error parsing AUTHCHALLENGE parameters: %s", SanitizeString(l.second)); return; } - std::vector serverHash = ParseHex(m["SERVERHASH"]); - std::vector serverNonce = ParseHex(m["SERVERNONCE"]); - LogDebug(BCLog::TOR, "AUTHCHALLENGE ServerHash %s ServerNonce %s\n", HexStr(serverHash), HexStr(serverNonce)); - if (serverNonce.size() != 32) { + std::vector server_hash = ParseHex(m["SERVERHASH"]); + std::vector server_nonce = ParseHex(m["SERVERNONCE"]); + LogDebug(BCLog::TOR, "AUTHCHALLENGE ServerHash %s ServerNonce %s\n", 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 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 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 computedClientHash = ComputeResponse(TOR_SAFE_CLIENTKEY, cookie, clientNonce, serverNonce); + std::vector 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"); @@ -616,10 +616,10 @@ void TorController::protocolinfo_cb(TorControlConnection& _conn, const TorContro std::pair 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(status_cookie.second.begin(), status_cookie.second.end()); - clientNonce = std::vector(TOR_NONCE_SIZE, 0); - GetRandBytes(clientNonce); - _conn.Command("AUTHCHALLENGE SAFECOOKIE " + HexStr(clientNonce), std::bind_front(&TorController::authchallenge_cb, this)); + m_cookie = std::vector(status_cookie.second.begin(), status_cookie.second.end()); + m_client_nonce = std::vector(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 +639,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,21 +648,21 @@ 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); + m_tor_control_center, m_reconnect_timeout); // Single-shot timer for reconnect. Use exponential backoff with a maximum. - struct timeval time = MillisToTimeval(int64_t(reconnect_timeout * 1000.0)); + struct timeval time = MillisToTimeval(int64_t(m_reconnect_timeout * 1000.0)); if (reconnect_ev) event_add(reconnect_ev, &time); - reconnect_timeout = std::min(reconnect_timeout * RECONNECT_TIMEOUT_EXP, RECONNECT_TIMEOUT_MAX); + m_reconnect_timeout = std::min(m_reconnect_timeout * RECONNECT_TIMEOUT_EXP, RECONNECT_TIMEOUT_MAX); } void TorController::Reconnect() @@ -670,7 +670,7 @@ 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), + if (!m_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); } diff --git a/src/torcontrol.h b/src/torcontrol.h index b8a1d6540ba..9f4970296cc 100644 --- a/src/torcontrol.h +++ b/src/torcontrol.h @@ -95,7 +95,7 @@ private: /** Connection to control socket */ struct bufferevent* b_conn{nullptr}; /** Message being received */ - TorControlReply message; + TorControlReply m_message; /** Response handlers */ std::deque reply_handlers; @@ -113,7 +113,7 @@ class TorController { public: TorController(struct event_base* base, const std::string& tor_control_center, const CService& target); - TorController() : conn{nullptr} { + TorController() : m_conn{nullptr} { // Used for testing only. } ~TorController(); @@ -126,18 +126,18 @@ public: private: struct event_base* base; const std::string m_tor_control_center; - TorControlConnection conn; - std::string private_key; - std::string service_id; - bool reconnect; + TorControlConnection m_conn; + std::string m_private_key; + std::string m_service_id; + bool m_reconnect; struct event *reconnect_ev = nullptr; - float reconnect_timeout; - CService service; + float m_reconnect_timeout; + CService m_service; const CService m_target; /** Cookie for SAFECOOKIE auth */ - std::vector cookie; + std::vector m_cookie; /** ClientNonce for SAFECOOKIE auth */ - std::vector clientNonce; + std::vector m_client_nonce; public: /** Callback for GETINFO net/listeners/socks result */ From 8444efbd4a0fee5531f7310400e0616d2e37e910 Mon Sep 17 00:00:00 2001 From: Fabian Jahr Date: Sat, 27 Dec 2025 15:13:24 +0100 Subject: [PATCH 03/10] refactor: Get rid of unnecessary newlines in logs --- src/torcontrol.cpp | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/torcontrol.cpp b/src/torcontrol.cpp index ffb783162ba..39ecb6d3200 100644 --- a/src/torcontrol.cpp +++ b/src/torcontrol.cpp @@ -112,7 +112,7 @@ void TorControlConnection::readcb(struct bufferevent *bev, void *ctx) self->reply_handlers.front()(*self, self->m_message); self->reply_handlers.pop_front(); } else { - LogDebug(BCLog::TOR, "Received unexpected sync reply %i\n", self->m_message.code); + LogDebug(BCLog::TOR, "Received unexpected sync reply %i", self->m_message.code); } } self->m_message.Clear(); @@ -131,13 +131,13 @@ void TorControlConnection::eventcb(struct bufferevent *bev, short what, void *ct { TorControlConnection *self = static_cast(ctx); if (what & BEV_EVENT_CONNECTED) { - LogDebug(BCLog::TOR, "Successfully connected!\n"); + LogDebug(BCLog::TOR, "Successfully connected!"); 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"); + LogDebug(BCLog::TOR, "Error connecting to Tor control socket"); } else { - LogDebug(BCLog::TOR, "End of stream\n"); + LogDebug(BCLog::TOR, "End of stream"); } self->Disconnect(); self->disconnected(*self); @@ -336,7 +336,7 @@ TorController::TorController(struct event_base* _base, const std::string& tor_co // Read service private key if cached std::pair pkf = ReadBinaryFile(GetPrivateKeyFile()); if (pkf.first) { - LogDebug(BCLog::TOR, "Reading cached private key from %s\n", fs::PathToString(GetPrivateKeyFile())); + LogDebug(BCLog::TOR, "Reading cached private key from %s", fs::PathToString(GetPrivateKeyFile())); m_private_key = pkf.second; } } @@ -377,7 +377,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 +398,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. @@ -453,9 +453,9 @@ void TorController::add_onion_cb(TorControlConnection& _conn, const TorControlRe return; } m_service = LookupNumeric(std::string(m_service_id+".onion"), Params().GetDefaultPort()); - LogInfo("Got tor service ID %s, advertising service %s\n", m_service_id, m_service.ToStringAddrPort()); + 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\n", fs::PathToString(GetPrivateKeyFile())); + 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())); } @@ -477,7 +477,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. @@ -529,7 +529,7 @@ static std::vector 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"); std::pair l = SplitTorReplyLine(reply.lines[0]); if (l.first == "AUTHCHALLENGE") { std::map m = ParseTorReplyMapping(l.second); @@ -539,7 +539,7 @@ void TorController::authchallenge_cb(TorControlConnection& _conn, const TorContr } std::vector server_hash = ParseHex(m["SERVERHASH"]); std::vector server_nonce = ParseHex(m["SERVERNONCE"]); - LogDebug(BCLog::TOR, "AUTHCHALLENGE ServerHash %s ServerNonce %s\n", HexStr(server_hash), HexStr(server_nonce)); + 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; @@ -586,12 +586,12 @@ void TorController::protocolinfo_cb(TorControlConnection& _conn, const TorContro std::map m = ParseTorReplyMapping(l.second); std::map::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,18 +601,18 @@ 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 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)); @@ -654,7 +654,7 @@ void TorController::disconnected_cb(TorControlConnection& _conn) if (!m_reconnect) return; - LogDebug(BCLog::TOR, "Not connected to Tor control port %s, retrying in %.2f s\n", + LogDebug(BCLog::TOR, "Not connected to Tor control port %s, retrying in %.2f s", m_tor_control_center, m_reconnect_timeout); // Single-shot timer for reconnect. Use exponential backoff with a maximum. @@ -720,7 +720,7 @@ void StartTorControl(CService onion_service_target) void InterruptTorControl() { if (gBase) { - LogInfo("tor: Thread interrupt\n"); + LogInfo("tor: Thread interrupt"); event_base_once(gBase, -1, EV_TIMEOUT, [](evutil_socket_t, short, void*) { event_base_loopbreak(gBase); }, nullptr, nullptr); From eae193e750235e4023b3e6313284eede2cd7a022 Mon Sep 17 00:00:00 2001 From: Fabian Jahr Date: Sat, 27 Dec 2025 01:29:04 +0100 Subject: [PATCH 04/10] torcontrol: Remove libevent usage Replace libevent-based approach with using the Sock class and CThreadInterrupt. --- src/test/fuzz/torcontrol.cpp | 6 +- src/torcontrol.cpp | 396 +++++++++++++++++++---------------- src/torcontrol.h | 78 ++++--- 3 files changed, 263 insertions(+), 217 deletions(-) diff --git a/src/test/fuzz/torcontrol.cpp b/src/test/fuzz/torcontrol.cpp index 1ea3069000a..47874ddf63d 100644 --- a/src/test/fuzz/torcontrol.cpp +++ b/src/test/fuzz/torcontrol.cpp @@ -14,12 +14,14 @@ class DummyTorControlConnection : public TorControlConnection { + CThreadInterrupt m_dummy_interrupt; + public: - DummyTorControlConnection() : TorControlConnection{nullptr} + DummyTorControlConnection() : TorControlConnection{m_dummy_interrupt} { } - bool Connect(const std::string&, const ConnectionCB&, const ConnectionCB&) + bool Connect(const std::string&) { return true; } diff --git a/src/torcontrol.cpp b/src/torcontrol.cpp index 39ecb6d3200..e8e680d7906 100644 --- a/src/torcontrol.cpp +++ b/src/torcontrol.cpp @@ -26,6 +26,7 @@ #include #include +#include #include #include #include @@ -37,12 +38,6 @@ #include #include -#include -#include -#include -#include -#include - using util::ReplaceAll; using util::SplitString; using util::ToString; @@ -58,146 +53,172 @@ static const std::string TOR_SAFE_SERVERKEY = "Tor safe cookie authentication se /** 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 */ -constexpr float RECONNECT_TIMEOUT_START = 1.0; +constexpr std::chrono::duration RECONNECT_TIMEOUT_START{1.0}; /** Exponential backoff configuration - growth factor */ -constexpr float RECONNECT_TIMEOUT_EXP = 1.5; +constexpr double RECONNECT_TIMEOUT_EXP = 1.5; /** Maximum reconnect timeout in seconds to prevent excessive delays */ -constexpr float RECONNECT_TIMEOUT_MAX = 600.0; +constexpr std::chrono::duration 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. */ 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(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; - // (-|+| ) - self->m_message.code = ToIntegral(s.substr(0, 3)).value_or(0); - self->m_message.lines.push_back(s.substr(4)); - char ch = s[3]; // '-','+' or ' ' - if (ch == ' ') { - // Final line, dispatch reply and clean up - if (self->m_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->m_message); - self->reply_handlers.pop_front(); - } else { - LogDebug(BCLog::TOR, "Received unexpected sync reply %i", self->m_message.code); - } - } - self->m_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(ctx); - if (what & BEV_EVENT_CONNECTED) { - LogDebug(BCLog::TOR, "Successfully connected!"); - 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"); - } else { - LogDebug(BCLog::TOR, "End of stream"); - } - 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 control_service{Lookup(tor_control_center, DEFAULT_TOR_CONTROL_PORT, fNameLookup)}; + std::optional 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(&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(&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: + // (-|+| ) + m_message.code = ToIntegral(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{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 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), m_conn(base), m_reconnect(true), m_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 (!m_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 pkf = ReadBinaryFile(GetPrivateKeyFile()); if (pkf.first) { 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); } } +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(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 @@ -515,7 +587,7 @@ void TorController::auth_cb(TorControlConnection& _conn, const TorControlReply& * CookieString | ClientNonce | ServerNonce) * */ -static std::vector ComputeResponse(const std::string &key, const std::vector &cookie, const std::vector &client_nonce, const std::vector &server_nonce) +static std::vector ComputeResponse(std::string_view key, std::span cookie, std::span client_nonce, std::span server_nonce) { CHMAC_SHA256 computeHash((const uint8_t*)key.data(), key.size()); std::vector computedHash(CHMAC_SHA256::OUTPUT_SIZE, 0); @@ -654,26 +726,8 @@ void TorController::disconnected_cb(TorControlConnection& _conn) if (!m_reconnect) return; - LogDebug(BCLog::TOR, "Not connected to Tor control port %s, retrying in %.2f s", - m_tor_control_center, m_reconnect_timeout); - - // Single-shot timer for reconnect. Use exponential backoff with a maximum. - struct timeval time = MillisToTimeval(int64_t(m_reconnect_timeout * 1000.0)); - if (reconnect_ev) - event_add(reconnect_ev, &time); - - m_reconnect_timeout = std::min(m_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 (!m_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,59 +735,33 @@ 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(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); -} +/** + * TODO: TBD if introducing a global is the preferred approach here since we + * usually try to avoid them. We could let init manage the lifecycle or make + * this a part of NodeContext maybe instead. + */ +static std::unique_ptr g_tor_controller; 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); - }); + assert(!g_tor_controller); + g_tor_controller = std::make_unique(gArgs.GetArg("-torcontrol", DEFAULT_TOR_CONTROL), onion_service_target); } void InterruptTorControl() { - if (gBase) { - LogInfo("tor: Thread interrupt"); - event_base_once(gBase, -1, EV_TIMEOUT, [](evutil_socket_t, short, void*) { - event_base_loopbreak(gBase); - }, nullptr, nullptr); - } + if (!g_tor_controller) return; + LogInfo("tor: Thread interrupt"); + g_tor_controller->Interrupt(); } void StopTorControl() { - if (gBase) { - torControlThread.join(); - event_base_free(gBase); - gBase = nullptr; - } + if (!g_tor_controller) return; + g_tor_controller->Join(); + g_tor_controller.reset(); } CService DefaultOnionServiceTarget(uint16_t port) diff --git a/src/torcontrol.h b/src/torcontrol.h index 9f4970296cc..7dfc6207a9a 100644 --- a/src/torcontrol.h +++ b/src/torcontrol.h @@ -10,13 +10,15 @@ #include #include - -#include +#include +#include #include #include #include +#include #include +#include #include constexpr uint16_t DEFAULT_TOR_SOCKS_PORT{9050}; @@ -57,22 +59,19 @@ public: class TorControlConnection { public: - typedef std::function ConnectionCB; typedef std::function 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 +84,38 @@ public: */ bool Command(const std::string &cmd, const ReplyHandlerCB& reply_handler); + /** + * Check if the connection is established. + */ + bool IsConnected() const; + + /** + * 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: - /** Callback when ready for use */ - std::function connected; - /** Callback when connection lost */ - std::function disconnected; - /** Libevent event base */ - struct event_base *base; - /** Connection to control socket */ - struct bufferevent* b_conn{nullptr}; + /** Reference to interrupt object for clean shutdown */ + CThreadInterrupt& m_interrupt; + /** Socket for the connection */ + std::unique_ptr m_sock; /** Message being received */ TorControlReply m_message; /** Response handlers */ - std::deque reply_handlers; - - /** Libevent handlers: internal */ - static void readcb(struct bufferevent *bev, void *ctx); - static void eventcb(struct bufferevent *bev, short what, void *ctx); + std::deque m_reply_handlers; + /** Buffer for incoming data */ + std::vector m_recv_buffer; + /** Process complete lines from the receive buffer */ + bool ProcessBuffer(); }; /****** Bitcoin specific TorController implementation ********/ @@ -112,8 +126,8 @@ private: class TorController { public: - TorController(struct event_base* base, const std::string& tor_control_center, const CService& target); - TorController() : m_conn{nullptr} { + TorController(const std::string& tor_control_center, const CService& target); + TorController() : m_conn(m_interrupt) { // Used for testing only. } ~TorController(); @@ -121,23 +135,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 m_conn; std::string m_private_key; std::string m_service_id; - bool m_reconnect; - struct event *reconnect_ev = nullptr; - float m_reconnect_timeout; + std::atomic m_reconnect; + std::chrono::duration m_reconnect_timeout; CService m_service; const CService m_target; /** Cookie for SAFECOOKIE auth */ std::vector m_cookie; /** ClientNonce for SAFECOOKIE auth */ std::vector m_client_nonce; + /** Main control thread */ + void ThreadControl(); public: /** Callback for GETINFO net/listeners/socks result */ @@ -154,9 +173,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 From b1869e9a2db71427000b2000ea3ebf6ea1d28754 Mon Sep 17 00:00:00 2001 From: Fabian Jahr Date: Fri, 13 Mar 2026 15:19:48 +0100 Subject: [PATCH 05/10] torcontrol: Move tor controller into node context Co-authored-by: sedited --- src/init.cpp | 11 ++++++++--- src/node/context.cpp | 1 + src/node/context.h | 2 ++ src/torcontrol.cpp | 29 ----------------------------- src/torcontrol.h | 4 ---- 5 files changed, 11 insertions(+), 36 deletions(-) diff --git a/src/init.cpp b/src/init.cpp index fb4b8fa9e27..f52d8d8d7e4 100644 --- a/src/init.cpp +++ b/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(gArgs.GetArg("-torcontrol", DEFAULT_TOR_CONTROL), onion_service_target); } if (connOptions.bind_on_any) { diff --git a/src/node/context.cpp b/src/node/context.cpp index d7d01b494ee..164361601f9 100644 --- a/src/node/context.cpp +++ b/src/node/context.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include diff --git a/src/node/context.h b/src/node/context.h index 3a7488fd25f..848c872fd4a 100644 --- a/src/node/context.h +++ b/src/node/context.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 netgroupman; std::unique_ptr fee_estimator; std::unique_ptr peerman; + std::unique_ptr tor_controller; std::unique_ptr chainman; std::unique_ptr banman; ArgsManager* args{nullptr}; // Currently a raw pointer because the memory is not managed by this struct diff --git a/src/torcontrol.cpp b/src/torcontrol.cpp index e8e680d7906..ecc75f15ccf 100644 --- a/src/torcontrol.cpp +++ b/src/torcontrol.cpp @@ -735,35 +735,6 @@ fs::path TorController::GetPrivateKeyFile() return gArgs.GetDataDirNet() / "onion_v3_private_key"; } -/****** Thread ********/ - -/** - * TODO: TBD if introducing a global is the preferred approach here since we - * usually try to avoid them. We could let init manage the lifecycle or make - * this a part of NodeContext maybe instead. - */ -static std::unique_ptr g_tor_controller; - -void StartTorControl(CService onion_service_target) -{ - assert(!g_tor_controller); - g_tor_controller = std::make_unique(gArgs.GetArg("-torcontrol", DEFAULT_TOR_CONTROL), onion_service_target); -} - -void InterruptTorControl() -{ - if (!g_tor_controller) return; - LogInfo("tor: Thread interrupt"); - g_tor_controller->Interrupt(); -} - -void StopTorControl() -{ - if (!g_tor_controller) return; - g_tor_controller->Join(); - g_tor_controller.reset(); -} - CService DefaultOnionServiceTarget(uint16_t port) { struct in_addr onion_service_target; diff --git a/src/torcontrol.h b/src/torcontrol.h index 7dfc6207a9a..75b81b87b2d 100644 --- a/src/torcontrol.h +++ b/src/torcontrol.h @@ -31,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 */ From 4117b92e67c8ba556fb83d6394291bc180df0c3a Mon Sep 17 00:00:00 2001 From: Fabian Jahr Date: Sun, 28 Dec 2025 15:07:51 +0100 Subject: [PATCH 06/10] fuzz: Improve torcontrol fuzz test Gets rid of the Dummy class and adds coverage of get_socks_cb. Also explicitly handles an exception case within Torcontrol rather than relying on a guard in the fuzz test. --- src/test/fuzz/torcontrol.cpp | 45 ++++++++++-------------------------- src/torcontrol.cpp | 4 ++++ 2 files changed, 16 insertions(+), 33 deletions(-) diff --git a/src/test/fuzz/torcontrol.cpp b/src/test/fuzz/torcontrol.cpp index 47874ddf63d..e45f1e62d1c 100644 --- a/src/test/fuzz/torcontrol.cpp +++ b/src/test/fuzz/torcontrol.cpp @@ -12,30 +12,6 @@ #include #include -class DummyTorControlConnection : public TorControlConnection -{ - CThreadInterrupt m_dummy_interrupt; - -public: - DummyTorControlConnection() : TorControlConnection{m_dummy_interrupt} - { - } - - bool Connect(const std::string&) - { - return true; - } - - void Disconnect() - { - } - - bool Command(const std::string&, const ReplyHandlerCB&) - { - return true; - } -}; - void initialize_torcontrol() { static const auto testing_setup = MakeNoLogFileContext<>(); @@ -46,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( @@ -63,26 +42,26 @@ FUZZ_TARGET(torcontrol, .init = initialize_torcontrol) tor_control_reply.code = fuzzed_data_provider.ConsumeIntegral(); }); 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); }); } } diff --git a/src/torcontrol.cpp b/src/torcontrol.cpp index ecc75f15ccf..17b37f6fdd4 100644 --- a/src/torcontrol.cpp +++ b/src/torcontrol.cpp @@ -602,6 +602,10 @@ void TorController::authchallenge_cb(TorControlConnection& _conn, const TorContr { if (reply.code == TOR_REPLY_OK) { LogDebug(BCLog::TOR, "SAFECOOKIE authentication challenge successful"); + if (reply.lines.empty()) { + LogWarning("tor: AUTHCHALLENGE reply was empty"); + return; + } std::pair l = SplitTorReplyLine(reply.lines[0]); if (l.first == "AUTHCHALLENGE") { std::map m = ParseTorReplyMapping(l.second); From 569383356ecd5baa65a87563d776809a63f8f7a3 Mon Sep 17 00:00:00 2001 From: Fabian Jahr Date: Sun, 28 Dec 2025 22:56:26 +0100 Subject: [PATCH 07/10] test: Add simple functional test for torcontrol --- test/functional/feature_torcontrol.py | 122 ++++++++++++++++++++++++++ test/functional/test_runner.py | 1 + 2 files changed, 123 insertions(+) create mode 100755 test/functional/feature_torcontrol.py diff --git a/test/functional/feature_torcontrol.py b/test/functional/feature_torcontrol.py new file mode 100755 index 00000000000..6efdafe3607 --- /dev/null +++ b/test/functional/feature_torcontrol.py @@ -0,0 +1,122 @@ +#!/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, p2p_port + + +class MockTorControlServer: + def __init__(self, port): + self.port = port + self.sock = None + self.running = False + self.thread = None + self.received_commands = [] + + 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.sock: + self.sock.close() + if self.thread: + self.thread.join(timeout=5) + + def _serve(self): + while self.running: + try: + conn, _ = self.sock.accept() + conn.settimeout(1.0) + self._handle_connection(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) + response = self._get_response(command) + conn.sendall(response.encode('utf-8')) + except socket.timeout: + continue + finally: + conn.close() + + 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 run_test(self): + self.log.info("Test Tor control basic functionality") + tor_control_port = p2p_port(self.num_nodes + 1) + mock_tor = MockTorControlServer(tor_control_port) + mock_tor.start() + + self.restart_node(0, extra_args=[ + f"-torcontrol=127.0.0.1:{tor_control_port}", + "-listenonion=1", + "-debug=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 ") + + # Clean up + mock_tor.stop() + + +if __name__ == '__main__': + TorControlTest(__file__).main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index cef49a3845e..882a7d2ecd5 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -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', From 7dff9ec29890cd9160e51d39d687f7023d56c6ba Mon Sep 17 00:00:00 2001 From: Fabian Jahr Date: Sat, 17 Jan 2026 15:22:19 +0100 Subject: [PATCH 08/10] test: Add test for partial message handling in torcontrol --- test/functional/feature_torcontrol.py | 73 +++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 9 deletions(-) diff --git a/test/functional/feature_torcontrol.py b/test/functional/feature_torcontrol.py index 6efdafe3607..dfc649a6fc6 100755 --- a/test/functional/feature_torcontrol.py +++ b/test/functional/feature_torcontrol.py @@ -3,20 +3,26 @@ # 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, p2p_port +from test_framework.util import ( + assert_equal, + ensure_for, + p2p_port, +) class MockTorControlServer: - def __init__(self, port): + 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) @@ -31,6 +37,8 @@ class MockTorControlServer: def stop(self): self.running = False + if self.conn: + self.conn.close() if self.sock: self.sock.close() if self.thread: @@ -39,9 +47,10 @@ class MockTorControlServer: def _serve(self): while self.running: try: - conn, _ = self.sock.accept() - conn.settimeout(1.0) - self._handle_connection(conn) + 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: @@ -61,13 +70,18 @@ class MockTorControlServer: command = line.decode('utf-8').strip() if command: self.received_commands.append(command) - response = self._get_response(command) - conn.sendall(response.encode('utf-8')) + 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 ( @@ -93,7 +107,7 @@ class TorControlTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 - def run_test(self): + def test_basic(self): self.log.info("Test Tor control basic functionality") tor_control_port = p2p_port(self.num_nodes + 1) mock_tor = MockTorControlServer(tor_control_port) @@ -117,6 +131,47 @@ class TorControlTest(BitcoinTestFramework): # Clean up mock_tor.stop() + def test_partial_data(self): + self.log.info("Test that partial Tor control responses are buffered until complete") + + tor_port = p2p_port(self.num_nodes + 2) + mock_tor = MockTorControlServer(tor_port, manual_mode=True) + mock_tor.start() + + self.restart_node(0, extra_args=[ + f"-torcontrol=127.0.0.1:{tor_port}", + "-listenonion=1", + "-debug=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 run_test(self): + self.test_basic() + self.test_partial_data() if __name__ == '__main__': TorControlTest(__file__).main() From 84c1f3207102256fa3b9bf3dbc6b41692b2403b0 Mon Sep 17 00:00:00 2001 From: Fabian Jahr Date: Tue, 24 Mar 2026 10:12:38 +0100 Subject: [PATCH 09/10] test: Add torcontrol coverage for PoW defense enablement --- test/functional/feature_torcontrol.py | 42 +++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/test/functional/feature_torcontrol.py b/test/functional/feature_torcontrol.py index dfc649a6fc6..084f3dfe234 100755 --- a/test/functional/feature_torcontrol.py +++ b/test/functional/feature_torcontrol.py @@ -127,6 +127,7 @@ class TorControlTest(BitcoinTestFramework): 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() @@ -169,9 +170,50 @@ class TorControlTest(BitcoinTestFramework): # Clean up mock_tor.stop() + def test_pow_fallback(self): + self.log.info("Test that ADD_ONION retries without PoW on 512 error") + + tor_port = p2p_port(self.num_nodes + 3) + + 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(tor_port) + mock_tor.start() + + self.restart_node(0, extra_args=[ + f"-torcontrol=127.0.0.1:{tor_port}", + "-listenonion=1", + "-debug=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 run_test(self): self.test_basic() self.test_partial_data() + self.test_pow_fallback() if __name__ == '__main__': TorControlTest(__file__).main() From 1401011f71c22f1652acb3cadcd0a5fb1a3c0d5d Mon Sep 17 00:00:00 2001 From: Fabian Jahr Date: Wed, 25 Mar 2026 18:48:56 +0100 Subject: [PATCH 10/10] test: Add test for exceeding max line length in torcontrol Also deduplicates the repetitive tor mock setup for each test case a bit. Co-authored-by: janb84 --- test/functional/feature_torcontrol.py | 68 ++++++++++++++++----------- 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/test/functional/feature_torcontrol.py b/test/functional/feature_torcontrol.py index 084f3dfe234..2148fc10352 100755 --- a/test/functional/feature_torcontrol.py +++ b/test/functional/feature_torcontrol.py @@ -107,17 +107,23 @@ 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") - tor_control_port = p2p_port(self.num_nodes + 1) - mock_tor = MockTorControlServer(tor_control_port) - mock_tor.start() - self.restart_node(0, extra_args=[ - f"-torcontrol=127.0.0.1:{tor_control_port}", - "-listenonion=1", - "-debug=tor" - ]) + 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) @@ -135,15 +141,8 @@ class TorControlTest(BitcoinTestFramework): def test_partial_data(self): self.log.info("Test that partial Tor control responses are buffered until complete") - tor_port = p2p_port(self.num_nodes + 2) - mock_tor = MockTorControlServer(tor_port, manual_mode=True) - mock_tor.start() - - self.restart_node(0, extra_args=[ - f"-torcontrol=127.0.0.1:{tor_port}", - "-listenonion=1", - "-debug=tor" - ]) + 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) @@ -173,8 +172,6 @@ class TorControlTest(BitcoinTestFramework): def test_pow_fallback(self): self.log.info("Test that ADD_ONION retries without PoW on 512 error") - tor_port = p2p_port(self.num_nodes + 3) - class NoPowServer(MockTorControlServer): def _get_response(self, command): if command.startswith("ADD_ONION"): @@ -187,14 +184,8 @@ class TorControlTest(BitcoinTestFramework): ) return super()._get_response(command) - mock_tor = NoPowServer(tor_port) - mock_tor.start() - - self.restart_node(0, extra_args=[ - f"-torcontrol=127.0.0.1:{tor_port}", - "-listenonion=1", - "-debug=tor", - ]) + 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) @@ -210,10 +201,33 @@ class TorControlTest(BitcoinTestFramework): # 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()