#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;
const std::string DEFAULT_TOR_CONTROL = "127.0.0.1:" + ToString(DEFAULT_TOR_CONTROL_PORT);
constexpr int TOR_COOKIE_SIZE = 32;
constexpr int TOR_NONCE_SIZE = 32;
static const std::string TOR_SAFE_SERVERKEY = "Tor safe cookie authentication server-to-controller hash";
static const std::string TOR_SAFE_CLIENTKEY = "Tor safe cookie authentication controller-to-server hash";
constexpr std::chrono::duration<double> RECONNECT_TIMEOUT_START{1.0};
constexpr double RECONNECT_TIMEOUT_EXP = 1.5;
constexpr std::chrono::duration<double> RECONNECT_TIMEOUT_MAX{600.0};
constexpr int MAX_LINE_LENGTH = 100000;
constexpr int MAX_LINE_COUNT = 1000;
constexpr auto SOCKET_SEND_TIMEOUT = 10s;
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(), 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) {
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));
}
if (line->size() < 4) continue;
m_message.code = ToIntegral<int>(line->substr(0, 3)).value_or(0);
m_message.lines.push_back(line->substr(4));
char separator = (*line)[3];
if (separator == ' ') {
if (m_message.code >= 600) {
LogDebug(BCLog::TOR, "Received async notification %i", m_message.code);
} else if (!m_reply_handlers.empty()) {
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;
}
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; return make_pair(type, s.substr(ptr));
}
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()) return std::map<std::string,std::string>();
if (s[ptr] == ' ') break;
++ptr; if (ptr < s.size() && s[ptr] == '"') { ++ptr; bool escape_next = false;
while (ptr < s.size() && (escape_next || s[ptr] != '"')) {
escape_next = (s[ptr] == '\\' && !escape_next);
value.push_back(s[ptr]);
++ptr;
}
if (ptr == s.size()) return std::map<std::string,std::string>();
++ptr;
std::string escaped_value;
for (size_t i = 0; i < value.size(); ++i) {
if (value[i] == '\\') {
++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;
for (j = 1; j < 3 && (i+j) < value.size() && '0' <= value[i+j] && value[i+j] <= '7'; ++j) {}
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));
--i;
} else {
escaped_value.push_back(value[i]);
}
} else {
escaped_value.push_back(value[i]);
}
}
value = escaped_value;
} else { while (ptr < s.size() && s[ptr] != ' ') {
value.push_back(s[ptr]);
++ptr;
}
}
if (ptr < s.size() && s[ptr] == ' ')
++ptr; 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)
{
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) {
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;
}
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;
}
m_reconnect_timeout = RECONNECT_TIMEOUT_START;
connected_cb(m_conn);
}
if (!m_conn.WaitForData(std::chrono::seconds(1))) {
if (!m_conn.IsConnected()) {
LogDebug(BCLog::TOR, "Lost connection to Tor control port");
disconnected_cb(m_conn);
continue;
}
continue;
}
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)
{
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:")) {
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()) {
resolved = LookupNumeric("127.0.0.1", DEFAULT_TOR_SOCKS_PORT);
}
Assume(resolved.IsValid());
LogDebug(BCLog::TOR, "Configuring onion proxy for %s", resolved.ToStringAddrPort());
Proxy addrOnion = Proxy(resolved, 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) {
g_reachable_nets.Add(NET_ONION);
}
}
static std::string MakeAddOnionCmd(const std::string& private_key, const std::string& target, bool enable_pow)
{
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);
} 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(), false),
[this](TorControlConnection& conn, const TorControlReply& reply) {
add_onion_cb(conn, reply, 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");
if (gArgs.GetArg("-onion", "") == "") {
_conn.Command("GETINFO net/listeners/socks", std::bind_front(&TorController::get_socks_cb, this));
}
if (m_private_key.empty()) { m_private_key = "NEW:ED25519-V3"; }
_conn.Command(MakeAddOnionCmd(m_private_key, m_target.ToStringAddrPort(), true),
[this](TorControlConnection& conn, const TorControlReply& reply) {
add_onion_cb(conn, reply, true);
});
} else {
LogWarning("tor: Authentication failed");
}
}
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;
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);
}
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")) {
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) {
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;
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)
{
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};
}