diff --git a/src/init.cpp b/src/init.cpp index 42331d37e8..2f9b354061 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -512,6 +512,11 @@ void SetupServerArgs(ArgsManager& argsman) argsman.AddArg("-timeout=", strprintf("Specify socket connection timeout in milliseconds. If an initial attempt to connect is unsuccessful after this amount of time, drop it (minimum: 1, default: %d)", DEFAULT_CONNECT_TIMEOUT), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-peertimeout=", strprintf("Specify a p2p connection timeout delay in seconds. After connecting to a peer, wait this amount of time before considering disconnection based on inactivity (minimum: 1, default: %d)", DEFAULT_PEER_CONNECT_TIMEOUT), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CONNECTION); argsman.AddArg("-torcontrol=:", strprintf("Tor control host and port to use if onion listening enabled (default: %s). If no port is specified, the default port of %i will be used.", DEFAULT_TOR_CONTROL, DEFAULT_TOR_CONTROL_PORT), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); +#ifdef HAVE_BOOST_PROCESS + argsman.AddArg("-torexecute=", strprintf("Tor command to use if not already running (default: %s)", DEFAULT_TOR_EXECUTE), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); +#else + hidden_args.emplace_back("-torexecute="); +#endif argsman.AddArg("-torpassword=", "Tor control port password (default: empty)", ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE, OptionsCategory::CONNECTION); #ifdef USE_UPNP #if USE_UPNP diff --git a/src/torcontrol.cpp b/src/torcontrol.cpp index 60cf31a964..bf1e3a388d 100644 --- a/src/torcontrol.cpp +++ b/src/torcontrol.cpp @@ -26,8 +26,10 @@ #include #include +#include #include #include +#include #include #include #include @@ -36,6 +38,27 @@ #include #include +#ifdef HAVE_BOOST_PROCESS +// Boost 1.77 requires the following workaround. +// See: https://github.com/boostorg/process/issues/213 +#include +#if defined(WIN32) && !defined(__kernel_entry) +// Boost 1.71-1.77 requires the following workaround for compatibility with mingw-w64 compiler. +// See: https://github.com/bitcoin/bitcoin/pull/22348 +#define __kernel_entry +#endif +#if defined(__GNUC__) +// Boost 1.78 requires the following workaround. +// See: https://github.com/boostorg/process/issues/235 +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnarrowing" +#endif +#include +#if defined(__GNUC__) +#pragma GCC diagnostic pop +#endif +#endif + #include #include #include @@ -44,6 +67,7 @@ /** Default control ip and port */ const std::string DEFAULT_TOR_CONTROL = "127.0.0.1:" + ToString(DEFAULT_TOR_CONTROL_PORT); +const std::string DEFAULT_TOR_EXECUTE = "tor"; /** Tor cookie size (from control-spec.txt) */ static const int TOR_COOKIE_SIZE = 32; /** Size of client/server nonce for SAFECOOKIE */ @@ -76,6 +100,10 @@ TorControlConnection::~TorControlConnection() bufferevent_free(b_conn); } +void TorControlConnection::IgnoreReplyHandler(TorControlConnection &a, const TorControlReply &b) +{ +} + void TorControlConnection::readcb(struct bufferevent *bev, void *ctx) { TorControlConnection *self = static_cast(ctx); @@ -314,18 +342,20 @@ std::map ParseTorReplyMapping(const std::string &s) return mapping; } -TorController::TorController(struct event_base* _base, const std::string& tor_control_center, const CService& target): +TorController::TorController(struct event_base* _base, const std::string& tor_control_center, const CService& target, const std::string& execute): base(_base), - m_tor_control_center(tor_control_center), conn(base), reconnect(true), reconnect_timeout(RECONNECT_TIMEOUT_START), + m_connect_tor_control_center(tor_control_center), conn(base), reconnect(true), reconnect_timeout(RECONNECT_TIMEOUT_START), + m_execute(execute), m_target(target) { reconnect_ev = event_new(base, -1, 0, reconnect_cb, this); if (!reconnect_ev) LogPrintf("tor: Failed to create event for reconnection: out of memory?\n"); // Start connection attempts immediately - if (!conn.Connect(m_tor_control_center, std::bind(&TorController::connected_cb, this, std::placeholders::_1), + m_current_tor_control_center = tor_control_center; + if (!conn.Connect(m_current_tor_control_center, std::bind(&TorController::connected_cb, this, std::placeholders::_1), std::bind(&TorController::disconnected_cb, this, std::placeholders::_1) )) { - LogPrintf("tor: Initiating connection to Tor control port %s failed\n", m_tor_control_center); + LogPrintf("tor: Initiating connection to Tor control port %s failed\n", m_current_tor_control_center); } // Read service private key if cached std::pair pkf = ReadBinaryFile(GetPrivateKeyFile()); @@ -344,6 +374,12 @@ TorController::~TorController() if (service.IsValid()) { RemoveLocal(service); } +#ifdef HAVE_BOOST_PROCESS + if (m_process) { + conn.Command("SIGNAL SHUTDOWN"); + delete m_process; + } +#endif } void TorController::get_socks_cb(TorControlConnection& _conn, const TorControlReply& reply) @@ -453,9 +489,20 @@ void TorController::auth_cb(TorControlConnection& _conn, const TorControlReply& if (reply.code == 250) { LogPrint(BCLog::TOR, "Authentication successful\n"); +#ifdef HAVE_BOOST_PROCESS + if (m_process) { + _conn.Command("TAKEOWNERSHIP"); + } +#endif + // 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", "") == "") { + // NOTE: Our own private Tor doesn't do SOCKS, so don't configure it + if (gArgs.GetArg("-onion", "") == "" +#ifdef HAVE_BOOST_PROCESS + && !m_process +#endif + ) { _conn.Command("GETINFO net/listeners/socks", std::bind(&TorController::get_socks_cb, this, std::placeholders::_1, std::placeholders::_2)); } @@ -612,22 +659,91 @@ void TorController::protocolinfo_cb(TorControlConnection& _conn, const TorContro void TorController::connected_cb(TorControlConnection& _conn) { + m_try_exec = false; reconnect_timeout = RECONNECT_TIMEOUT_START; // First send a PROTOCOLINFO command to figure out what authentication is expected if (!_conn.Command("PROTOCOLINFO 1", std::bind(&TorController::protocolinfo_cb, this, std::placeholders::_1, std::placeholders::_2))) LogPrintf("tor: Error sending initial protocolinfo command\n"); } +std::string TorController::LaunchTor() +{ +#ifdef HAVE_BOOST_PROCESS + fs::path tor_datadir = gArgs.GetDataDirNet() / "tor"; + const fs::path controlport_env_filepath = tor_datadir / "controlport.env"; + fs::remove(controlport_env_filepath); // may throw exceptions + + if (m_process) { + m_process->terminate(); + delete m_process; + m_process = nullptr; + } + + boost::process::opstream in; + try { + m_process = new boost::process::child(m_execute + " -f -", boost::process::std_in < in); + } catch (...) { + LogPrint(BCLog::TOR, "tor: Failed to execute Tor process\n"); + throw; + } + in << "SOCKSPort 0" << std::endl; + in << "DataDirectory " << fs::PathToString(tor_datadir) << std::endl; + in << "ControlPort auto" << std::endl; + in << "ControlPortWriteToFile " << fs::PathToString(controlport_env_filepath) << std::endl; + in << "CookieAuthentication 1" << std::endl; + in.pipe().close(); + + while (!fs::exists(controlport_env_filepath)) { + if (!m_process->running()) { + LogPrint(BCLog::TOR, "tor: Tor process died before making control port file\n"); + throw std::runtime_error("tor process died"); + } + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + + std::ifstream controlport_file(controlport_env_filepath); + std::string portline; + controlport_file >> portline; + if (portline.compare(0, 5, "PORT=")) { + LogPrint(BCLog::TOR, "tor: Unrecognized control port line in file\n"); + m_process->terminate(); + delete m_process; + m_process = nullptr; + throw std::runtime_error("port line unrecognized"); + } + + return portline.substr(5); +#else + throw std::runtime_error("not supported"); +#endif +} + void TorController::disconnected_cb(TorControlConnection& _conn) { // Stop advertising service when disconnected if (service.IsValid()) RemoveLocal(service); service = CService(); + +#ifdef HAVE_BOOST_PROCESS + if (m_try_exec && !m_execute.empty()) { + LogPrint(BCLog::TOR, "tor: Not connected to Tor control port %s, trying to launch via %s\n", m_current_tor_control_center, m_execute); + try { + m_current_tor_control_center = LaunchTor(); + Reconnect(); + return; + } catch (...) { + // fall through to normal reconnect logic + } + } +#endif + if (!reconnect) return; - LogPrint(BCLog::TOR, "Not connected to Tor control port %s, trying to reconnect\n", m_tor_control_center); + LogPrint(BCLog::TOR, "Not connected to Tor control port %s, trying to reconnect in %s seconds\n", m_current_tor_control_center, reconnect_timeout); + m_current_tor_control_center = m_connect_tor_control_center; + m_try_exec = true; // if this fails // Single-shot timer for reconnect. Use exponential backoff. struct timeval time = MillisToTimeval(int64_t(reconnect_timeout * 1000.0)); @@ -641,9 +757,9 @@ 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(&TorController::connected_cb, this, std::placeholders::_1), + if (!conn.Connect(m_current_tor_control_center, std::bind(&TorController::connected_cb, this, std::placeholders::_1), std::bind(&TorController::disconnected_cb, this, std::placeholders::_1) )) { - LogPrintf("tor: Re-initiating connection to Tor control port %s failed\n", m_tor_control_center); + LogPrintf("tor: Re-initiating connection to Tor control port %s failed\n", m_current_tor_control_center); } } @@ -664,7 +780,17 @@ static std::thread torControlThread; static void TorControlThread(CService onion_service_target) { - TorController ctrl(gBase, gArgs.GetArg("-torcontrol", DEFAULT_TOR_CONTROL), onion_service_target); +#ifdef HAVE_BOOST_PROCESS + std::string execute_command = gArgs.GetArg("-torexecute", DEFAULT_TOR_EXECUTE); + if (execute_command == "1") { + execute_command = DEFAULT_TOR_EXECUTE; + } else if (execute_command == "0") { + execute_command.clear(); + } +#else + const std::string execute_command; +#endif + TorController ctrl(gBase, gArgs.GetArg("-torcontrol", DEFAULT_TOR_CONTROL), onion_service_target, execute_command); event_base_dispatch(gBase); } diff --git a/src/torcontrol.h b/src/torcontrol.h index 4a0eef223e..b23d24e65c 100644 --- a/src/torcontrol.h +++ b/src/torcontrol.h @@ -8,9 +8,31 @@ #ifndef BITCOIN_TORCONTROL_H #define BITCOIN_TORCONTROL_H +#if defined(HAVE_CONFIG_H) +#include +#endif + #include #include +#ifdef HAVE_BOOST_PROCESS +#if defined(WIN32) && !defined(__kernel_entry) +// A workaround for boost 1.71 incompatibility with mingw-w64 compiler. +// For details see https://github.com/bitcoin/bitcoin/pull/22348. +#define __kernel_entry +#endif +#if defined(__GNUC__) +// Boost 1.78 requires the following workaround. +// See: https://github.com/boostorg/process/issues/235 +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnarrowing" +#endif +#include +#if defined(__GNUC__) +#pragma GCC diagnostic pop +#endif +#endif + #include #include @@ -21,6 +43,7 @@ constexpr int DEFAULT_TOR_CONTROL_PORT = 9051; extern const std::string DEFAULT_TOR_CONTROL; +extern const std::string DEFAULT_TOR_EXECUTE; static const bool DEFAULT_LISTEN_ONION = true; void StartTorControl(CService onion_service_target); @@ -53,6 +76,7 @@ class TorControlConnection public: typedef std::function ConnectionCB; typedef std::function ReplyHandlerCB; + static void IgnoreReplyHandler(TorControlConnection &, const TorControlReply &); /** Create a new TorControlConnection. */ @@ -77,7 +101,7 @@ public: * A trailing CRLF is automatically added. * Return true on success. */ - bool Command(const std::string &cmd, const ReplyHandlerCB& reply_handler); + bool Command(const std::string &cmd, const ReplyHandlerCB& reply_handler = IgnoreReplyHandler); private: /** Callback when ready for use */ @@ -106,7 +130,7 @@ private: class TorController { public: - TorController(struct event_base* base, const std::string& tor_control_center, const CService& target); + TorController(struct event_base* base, const std::string& tor_control_center, const CService& target, const std::string& execute); TorController() : conn{nullptr} { // Used for testing only. } @@ -119,13 +143,19 @@ public: void Reconnect(); private: struct event_base* base; - const std::string m_tor_control_center; + const std::string m_connect_tor_control_center; + std::string m_current_tor_control_center; TorControlConnection conn; std::string private_key; std::string service_id; + bool m_try_exec{true}; bool reconnect; struct event *reconnect_ev = nullptr; float reconnect_timeout; + std::string m_execute{DEFAULT_TOR_EXECUTE}; +#ifdef HAVE_BOOST_PROCESS + boost::process::child *m_process{nullptr}; +#endif CService service; const CService m_target; /** Cookie for SAFECOOKIE auth */ @@ -151,6 +181,8 @@ public: /** Callback for reconnect timer */ static void reconnect_cb(evutil_socket_t fd, short what, void *arg); + + std::string LaunchTor(); }; #endif // BITCOIN_TORCONTROL_H