mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-05-13 15:33:51 +02:00
This commit ensures the `TorControlConnection::m_message` buffer doesn't
grow unbounded and exhaust memory, by limiting the number of lines
handled by `TorControlConnection::ProcessBuffer()` to `MAX_LINE_COUNT =
1000`. Now the most memory that can be occupied by `m_message` is on the
order of `MAX_LINE_LENGTH * MAX_LINE_COUNT= 100MB`
Although this is not compliant with the tor control protocol in general,
where commands like `GETINFO ns/all` will likely return thousands of
lines, it is more than sufficient for handling the replies from the
commands that are used by a node:
`AUTHENTICATE`: 1 line:
The server responds with 250 OK on success or 515 Bad
authentication if the authentication cookie is incorrect. Tor closes
the connection on an authentication failure.
https://spec.torproject.org/control-spec/commands.html#authenticate
`GETINFO net/listener/socks`: 2 lines
A quoted, space-separated list of the locations where Tor is
listening...
https://spec.torproject.org/control-spec/commands.html#getinfo
`AUTHCHALLENGE SAFECOOKIE`: 1 line
If the server accepts the command, the server reply format is:
```
"250 AUTHCHALLENGE" SP "SERVERHASH=" ServerHash SP "SERVERNONCE="
ServerNonce CRLF
```
https://spec.torproject.org/control-spec/commands.html#authenticate
`PROTOCOLINFO`: 4-5 lines
The server reply format is:
```
250-PROTOCOLINFO" SP PIVERSION CRLF \*InfoLine "250 OK" CRLF
InfoLine = AuthLine / VersionLine / OtherLine
```
(https://spec.torproject.org/control-spec/commands.html#protocolinfo)
`ADD_ONION`: 2-3 lines for Bitcoin Core's tor control client.
The server reply format is:
```
"250-ServiceID=" ServiceID CRLF
["250-PrivateKey=" KeyType ":" KeyBlob CRLF]
*("250-ClientAuth=" ClientName ":" ClientBlob CRLF)
"250 OK" CRLF
```
...
The server response will only include a private key if the server
was requested to generate a new keypair
...
If client authorization is enabled using the “BasicAuth” flag (which
is v2 only), the service will not be accessible to clients without
valid authorization data (configured with the “HidServAuth” option).
The list of authorized clients is specified with one or more
“ClientAuth” parameters. If “ClientBlob” is not specified for a
client, a new credential will be randomly generated and returned."
https://spec.torproject.org/control-spec/commands.html#add_onion
We don't set the `BasicAuth` flag, so the response will not include any
`ClientAuthLines`.
756 lines
30 KiB
C++
756 lines
30 KiB
C++
// Copyright (c) 2015-present The Bitcoin Core developers
|
|
// Copyright (c) 2017 The Zcash developers
|
|
// Distributed under the MIT software license, see the accompanying
|
|
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
|
|
|
#include <torcontrol.h>
|
|
|
|
#include <chainparams.h>
|
|
#include <chainparamsbase.h>
|
|
#include <common/args.h>
|
|
#include <compat/compat.h>
|
|
#include <crypto/hmac_sha256.h>
|
|
#include <logging.h>
|
|
#include <net.h>
|
|
#include <netaddress.h>
|
|
#include <netbase.h>
|
|
#include <random.h>
|
|
#include <tinyformat.h>
|
|
#include <util/check.h>
|
|
#include <util/fs.h>
|
|
#include <util/readwritefile.h>
|
|
#include <util/strencodings.h>
|
|
#include <util/string.h>
|
|
#include <util/thread.h>
|
|
#include <util/time.h>
|
|
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
#include <chrono>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <deque>
|
|
#include <functional>
|
|
#include <map>
|
|
#include <optional>
|
|
#include <set>
|
|
#include <thread>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
using util::ReplaceAll;
|
|
using util::SplitString;
|
|
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) */
|
|
constexpr int TOR_COOKIE_SIZE = 32;
|
|
/** Size of client/server nonce for 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 */
|
|
constexpr std::chrono::duration<double> RECONNECT_TIMEOUT_START{1.0};
|
|
/** Exponential backoff configuration - growth factor */
|
|
constexpr double RECONNECT_TIMEOUT_EXP = 1.5;
|
|
/** Maximum reconnect timeout in seconds to prevent excessive delays */
|
|
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.
|
|
*/
|
|
constexpr int MAX_LINE_LENGTH = 100000;
|
|
/** Maximum number of lines received on TorControlConnection per reply to avoid
|
|
* memory exhaustion. The largest expected now is 5 (PROTOCOLINFO), but future
|
|
* changes to this file might need to re-evaluate MAX_LINE_COUNT.
|
|
*/
|
|
constexpr int MAX_LINE_COUNT = 1000;
|
|
/** Timeout for socket operations */
|
|
constexpr auto SOCKET_SEND_TIMEOUT = 10s;
|
|
|
|
/****** Low-level TorControlConnection ********/
|
|
|
|
TorControlConnection::TorControlConnection(CThreadInterrupt& interrupt)
|
|
: m_interrupt(interrupt)
|
|
{
|
|
}
|
|
|
|
TorControlConnection::~TorControlConnection()
|
|
{
|
|
Disconnect();
|
|
}
|
|
|
|
bool TorControlConnection::Connect(const std::string& tor_control_center)
|
|
{
|
|
if (m_sock) {
|
|
Disconnect();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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()
|
|
{
|
|
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()) {
|
|
if (m_message.lines.size() == MAX_LINE_COUNT) {
|
|
throw std::runtime_error(strprintf("Control port reply exceeded %d lines, disconnecting", MAX_LINE_COUNT));
|
|
}
|
|
// 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 (!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;
|
|
}
|
|
|
|
m_reply_handlers.push_back(reply_handler);
|
|
return true;
|
|
}
|
|
|
|
/****** General parsing utilities ********/
|
|
|
|
/* Split reply line in the form 'AUTH METHODS=...' into a type
|
|
* 'AUTH' and arguments 'METHODS=...'.
|
|
* Grammar is implicitly defined in https://spec.torproject.org/control-spec by
|
|
* the server reply formats for PROTOCOLINFO (S3.21) and AUTHCHALLENGE (S3.24).
|
|
*/
|
|
std::pair<std::string,std::string> SplitTorReplyLine(const std::string &s)
|
|
{
|
|
size_t ptr=0;
|
|
std::string type;
|
|
while (ptr < s.size() && s[ptr] != ' ') {
|
|
type.push_back(s[ptr]);
|
|
++ptr;
|
|
}
|
|
if (ptr < s.size())
|
|
++ptr; // skip ' '
|
|
return make_pair(type, s.substr(ptr));
|
|
}
|
|
|
|
/** Parse reply arguments in the form 'METHODS=COOKIE,SAFECOOKIE COOKIEFILE=".../control_auth_cookie"'.
|
|
* Returns a map of keys to values, or an empty map if there was an error.
|
|
* Grammar is implicitly defined in https://spec.torproject.org/control-spec by
|
|
* the server reply formats for PROTOCOLINFO (S3.21), AUTHCHALLENGE (S3.24),
|
|
* and ADD_ONION (S3.27). See also sections 2.1 and 2.3.
|
|
*/
|
|
std::map<std::string,std::string> ParseTorReplyMapping(const std::string &s)
|
|
{
|
|
std::map<std::string,std::string> mapping;
|
|
size_t ptr=0;
|
|
while (ptr < s.size()) {
|
|
std::string key, value;
|
|
while (ptr < s.size() && s[ptr] != '=' && s[ptr] != ' ') {
|
|
key.push_back(s[ptr]);
|
|
++ptr;
|
|
}
|
|
if (ptr == s.size()) // unexpected end of line
|
|
return std::map<std::string,std::string>();
|
|
if (s[ptr] == ' ') // The remaining string is an OptArguments
|
|
break;
|
|
++ptr; // skip '='
|
|
if (ptr < s.size() && s[ptr] == '"') { // Quoted string
|
|
++ptr; // skip opening '"'
|
|
bool escape_next = false;
|
|
while (ptr < s.size() && (escape_next || s[ptr] != '"')) {
|
|
// Repeated backslashes must be interpreted as pairs
|
|
escape_next = (s[ptr] == '\\' && !escape_next);
|
|
value.push_back(s[ptr]);
|
|
++ptr;
|
|
}
|
|
if (ptr == s.size()) // unexpected end of line
|
|
return std::map<std::string,std::string>();
|
|
++ptr; // skip closing '"'
|
|
/**
|
|
* Unescape value. Per https://spec.torproject.org/control-spec section 2.1.1:
|
|
*
|
|
* For future-proofing, controller implementers MAY use the following
|
|
* rules to be compatible with buggy Tor implementations and with
|
|
* future ones that implement the spec as intended:
|
|
*
|
|
* Read \n \t \r and \0 ... \377 as C escapes.
|
|
* Treat a backslash followed by any other character as that character.
|
|
*/
|
|
std::string escaped_value;
|
|
for (size_t i = 0; i < value.size(); ++i) {
|
|
if (value[i] == '\\') {
|
|
// This will always be valid, because if the QuotedString
|
|
// ended in an odd number of backslashes, then the parser
|
|
// would already have returned above, due to a missing
|
|
// terminating double-quote.
|
|
++i;
|
|
if (value[i] == 'n') {
|
|
escaped_value.push_back('\n');
|
|
} else if (value[i] == 't') {
|
|
escaped_value.push_back('\t');
|
|
} else if (value[i] == 'r') {
|
|
escaped_value.push_back('\r');
|
|
} else if ('0' <= value[i] && value[i] <= '7') {
|
|
size_t j;
|
|
// Octal escape sequences have a limit of three octal digits,
|
|
// but terminate at the first character that is not a valid
|
|
// octal digit if encountered sooner.
|
|
for (j = 1; j < 3 && (i+j) < value.size() && '0' <= value[i+j] && value[i+j] <= '7'; ++j) {}
|
|
// Tor restricts first digit to 0-3 for three-digit octals.
|
|
// A leading digit of 4-7 would therefore be interpreted as
|
|
// a two-digit octal.
|
|
if (j == 3 && value[i] > '3') {
|
|
j--;
|
|
}
|
|
const auto end{i + j};
|
|
uint8_t val{0};
|
|
while (i < end) {
|
|
val *= 8;
|
|
val += value[i++] - '0';
|
|
}
|
|
escaped_value.push_back(char(val));
|
|
// Account for automatic incrementing at loop end
|
|
--i;
|
|
} else {
|
|
escaped_value.push_back(value[i]);
|
|
}
|
|
} else {
|
|
escaped_value.push_back(value[i]);
|
|
}
|
|
}
|
|
value = escaped_value;
|
|
} else { // Unquoted value. Note that values can contain '=' at will, just no spaces
|
|
while (ptr < s.size() && s[ptr] != ' ') {
|
|
value.push_back(s[ptr]);
|
|
++ptr;
|
|
}
|
|
}
|
|
if (ptr < s.size() && s[ptr] == ' ')
|
|
++ptr; // skip ' ' after key=value
|
|
mapping[key] = value;
|
|
}
|
|
return mapping;
|
|
}
|
|
|
|
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)
|
|
{
|
|
// 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", fs::PathToString(GetPrivateKeyFile()));
|
|
m_private_key = pkf.second;
|
|
}
|
|
m_thread = std::thread(&util::TraceThread, "torcontrol", [this] { ThreadControl(); });
|
|
}
|
|
|
|
TorController::~TorController()
|
|
{
|
|
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<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
|
|
std::string socks_location;
|
|
if (reply.code == TOR_REPLY_OK) {
|
|
for (const auto& line : reply.lines) {
|
|
if (line.starts_with("net/listeners/socks=")) {
|
|
const std::string port_list_str = line.substr(20);
|
|
std::vector<std::string> port_list = SplitString(port_list_str, ' ');
|
|
|
|
for (auto& portstr : port_list) {
|
|
if (portstr.empty()) continue;
|
|
if ((portstr[0] == '"' || portstr[0] == '\'') && portstr.size() >= 2 && (*portstr.rbegin() == portstr[0])) {
|
|
portstr = portstr.substr(1, portstr.size() - 2);
|
|
if (portstr.empty()) continue;
|
|
}
|
|
socks_location = portstr;
|
|
if (portstr.starts_with("127.0.0.1:")) {
|
|
// Prefer localhost - ignore other ports
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!socks_location.empty()) {
|
|
LogDebug(BCLog::TOR, "Get SOCKS port command yielded %s", socks_location);
|
|
} else {
|
|
LogWarning("tor: Get SOCKS port command returned nothing");
|
|
}
|
|
} else if (reply.code == TOR_REPLY_UNRECOGNIZED) {
|
|
LogWarning("tor: Get SOCKS port command failed with unrecognized command (You probably should upgrade Tor)");
|
|
} else {
|
|
LogWarning("tor: Get SOCKS port command failed; error code %d", reply.code);
|
|
}
|
|
|
|
CService resolved;
|
|
Assume(!resolved.IsValid());
|
|
if (!socks_location.empty()) {
|
|
resolved = LookupNumeric(socks_location, DEFAULT_TOR_SOCKS_PORT);
|
|
}
|
|
if (!resolved.IsValid()) {
|
|
// Fallback to old behaviour
|
|
resolved = LookupNumeric("127.0.0.1", DEFAULT_TOR_SOCKS_PORT);
|
|
}
|
|
|
|
Assume(resolved.IsValid());
|
|
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.
|
|
// For this to work, the IsolateSOCKSAuth flag must be enabled on SOCKSPort (which is the default, see the IsolateSOCKSAuth section of Tor's manual page).
|
|
Proxy addrOnion = Proxy(resolved, /*tor_stream_isolation=*/ true);
|
|
SetProxy(NET_ONION, addrOnion);
|
|
|
|
const auto onlynets = gArgs.GetArgs("-onlynet");
|
|
|
|
const bool onion_allowed_by_onlynet{
|
|
onlynets.empty() ||
|
|
std::any_of(onlynets.begin(), onlynets.end(), [](const auto& n) {
|
|
return ParseNetwork(n) == NET_ONION;
|
|
})};
|
|
|
|
if (onion_allowed_by_onlynet) {
|
|
// If NET_ONION is reachable, then the below is a noop.
|
|
//
|
|
// If NET_ONION is not reachable, then none of -proxy or -onion was given.
|
|
// Since we are here, then -torcontrol and -torpassword were given.
|
|
g_reachable_nets.Add(NET_ONION);
|
|
}
|
|
}
|
|
|
|
static std::string MakeAddOnionCmd(const std::string& private_key, const std::string& target, bool enable_pow)
|
|
{
|
|
// Note that the 'virtual' port is always the default port to avoid decloaking nodes using other ports.
|
|
return strprintf("ADD_ONION %s%s Port=%i,%s",
|
|
private_key,
|
|
enable_pow ? " PoWDefensesEnabled=1" : "",
|
|
Params().GetDefaultPort(),
|
|
target);
|
|
}
|
|
|
|
void TorController::add_onion_cb(TorControlConnection& _conn, const TorControlReply& reply, bool pow_was_enabled)
|
|
{
|
|
if (reply.code == TOR_REPLY_OK) {
|
|
LogDebug(BCLog::TOR, "ADD_ONION successful (PoW defenses %s)", pow_was_enabled ? "enabled" : "disabled");
|
|
for (const std::string &s : reply.lines) {
|
|
std::map<std::string,std::string> m = ParseTorReplyMapping(s);
|
|
std::map<std::string,std::string>::iterator i;
|
|
if ((i = m.find("ServiceID")) != m.end())
|
|
m_service_id = i->second;
|
|
if ((i = m.find("PrivateKey")) != m.end())
|
|
m_private_key = i->second;
|
|
}
|
|
if (m_service_id.empty()) {
|
|
LogWarning("tor: Error parsing ADD_ONION parameters:");
|
|
for (const std::string &s : reply.lines) {
|
|
LogWarning(" %s", SanitizeString(s));
|
|
}
|
|
return;
|
|
}
|
|
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(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(m_private_key, m_target.ToStringAddrPort(), /*enable_pow=*/false),
|
|
[this](TorControlConnection& conn, const TorControlReply& reply) {
|
|
add_onion_cb(conn, reply, /*pow_was_enabled=*/false);
|
|
});
|
|
} else {
|
|
LogWarning("tor: Add onion failed; error code %d", reply.code);
|
|
}
|
|
}
|
|
|
|
void TorController::auth_cb(TorControlConnection& _conn, const TorControlReply& reply)
|
|
{
|
|
if (reply.code == TOR_REPLY_OK) {
|
|
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.
|
|
if (gArgs.GetArg("-onion", "") == "") {
|
|
_conn.Command("GETINFO net/listeners/socks", std::bind_front(&TorController::get_socks_cb, this));
|
|
}
|
|
|
|
// Finally - now create the service
|
|
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(m_private_key, m_target.ToStringAddrPort(), /*enable_pow=*/true),
|
|
[this](TorControlConnection& conn, const TorControlReply& reply) {
|
|
add_onion_cb(conn, reply, /*pow_was_enabled=*/true);
|
|
});
|
|
} else {
|
|
LogWarning("tor: Authentication failed");
|
|
}
|
|
}
|
|
|
|
/** Compute Tor SAFECOOKIE response.
|
|
*
|
|
* ServerHash is computed as:
|
|
* HMAC-SHA256("Tor safe cookie authentication server-to-controller hash",
|
|
* CookieString | ClientNonce | ServerNonce)
|
|
* (with the HMAC key as its first argument)
|
|
*
|
|
* After a controller sends a successful AUTHCHALLENGE command, the
|
|
* next command sent on the connection must be an AUTHENTICATE command,
|
|
* and the only authentication string which that AUTHENTICATE command
|
|
* will accept is:
|
|
*
|
|
* HMAC-SHA256("Tor safe cookie authentication controller-to-server hash",
|
|
* CookieString | ClientNonce | 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(client_nonce.data(), client_nonce.size());
|
|
computeHash.Write(server_nonce.data(), server_nonce.size());
|
|
computeHash.Finalize(computedHash.data());
|
|
return computedHash;
|
|
}
|
|
|
|
void TorController::authchallenge_cb(TorControlConnection& _conn, const TorControlReply& reply)
|
|
{
|
|
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<std::string,std::string> l = SplitTorReplyLine(reply.lines[0]);
|
|
if (l.first == "AUTHCHALLENGE") {
|
|
std::map<std::string,std::string> m = ParseTorReplyMapping(l.second);
|
|
if (m.empty()) {
|
|
LogWarning("tor: Error parsing AUTHCHALLENGE parameters: %s", SanitizeString(l.second));
|
|
return;
|
|
}
|
|
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> 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, 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");
|
|
}
|
|
} else {
|
|
LogWarning("tor: SAFECOOKIE authentication challenge failed");
|
|
}
|
|
}
|
|
|
|
void TorController::protocolinfo_cb(TorControlConnection& _conn, const TorControlReply& reply)
|
|
{
|
|
if (reply.code == TOR_REPLY_OK) {
|
|
std::set<std::string> methods;
|
|
std::string cookiefile;
|
|
/*
|
|
* 250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE="/home/x/.tor/control_auth_cookie"
|
|
* 250-AUTH METHODS=NULL
|
|
* 250-AUTH METHODS=HASHEDPASSWORD
|
|
*/
|
|
for (const std::string &s : reply.lines) {
|
|
std::pair<std::string,std::string> l = SplitTorReplyLine(s);
|
|
if (l.first == "AUTH") {
|
|
std::map<std::string,std::string> m = ParseTorReplyMapping(l.second);
|
|
std::map<std::string,std::string>::iterator i;
|
|
if ((i = m.find("METHODS")) != m.end()) {
|
|
std::vector<std::string> m_vec = SplitString(i->second, ',');
|
|
methods = std::set<std::string>(m_vec.begin(), m_vec.end());
|
|
}
|
|
if ((i = m.find("COOKIEFILE")) != m.end())
|
|
cookiefile = i->second;
|
|
} else if (l.first == "VERSION") {
|
|
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", i->second);
|
|
}
|
|
}
|
|
}
|
|
for (const std::string &s : methods) {
|
|
LogDebug(BCLog::TOR, "Supported authentication method: %s", s);
|
|
}
|
|
// Prefer NULL, otherwise SAFECOOKIE. If a password is provided, use HASHEDPASSWORD
|
|
/* Authentication:
|
|
* cookie: hex-encoded ~/.tor/control_auth_cookie
|
|
* password: "password"
|
|
*/
|
|
std::string torpassword = gArgs.GetArg("-torpassword", "");
|
|
if (!torpassword.empty()) {
|
|
if (methods.contains("HASHEDPASSWORD")) {
|
|
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");
|
|
_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", 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));
|
|
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);
|
|
} else {
|
|
LogWarning("tor: Authentication cookie %s could not be opened (check permissions)", cookiefile);
|
|
}
|
|
}
|
|
} else if (methods.contains("HASHEDPASSWORD")) {
|
|
LogWarning("tor: The only supported authentication mechanism left is password, but no password provided with -torpassword");
|
|
} else {
|
|
LogWarning("tor: No supported authentication method");
|
|
}
|
|
} else {
|
|
LogWarning("tor: Requesting protocol info failed");
|
|
}
|
|
}
|
|
|
|
void TorController::connected_cb(TorControlConnection& _conn)
|
|
{
|
|
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");
|
|
}
|
|
|
|
void TorController::disconnected_cb(TorControlConnection& _conn)
|
|
{
|
|
// Stop advertising service when disconnected
|
|
if (m_service.IsValid())
|
|
RemoveLocal(m_service);
|
|
m_service = CService();
|
|
if (!m_reconnect)
|
|
return;
|
|
|
|
LogDebug(BCLog::TOR, "Not connected to Tor control port %s, will retry", m_tor_control_center);
|
|
_conn.Disconnect();
|
|
}
|
|
|
|
fs::path TorController::GetPrivateKeyFile()
|
|
{
|
|
return gArgs.GetDataDirNet() / "onion_v3_private_key";
|
|
}
|
|
|
|
CService DefaultOnionServiceTarget(uint16_t port)
|
|
{
|
|
struct in_addr onion_service_target;
|
|
onion_service_target.s_addr = htonl(INADDR_LOOPBACK);
|
|
return {onion_service_target, port};
|
|
}
|