Console: limit access via TCP

This commit is contained in:
SChernykh
2025-06-10 21:51:05 +02:00
parent e577b298b6
commit 17279708e5
6 changed files with 106 additions and 33 deletions

View File

@@ -18,3 +18,30 @@ start_mining **T**|start mining (**T** is the number of threads to use, must be
stop_mining|stop mining stop_mining|stop mining
exit|terminate p2pool exit|terminate p2pool
version|show p2pool version version|show p2pool version
### Non-interactive console access
It's possible to send console commands via a local TCP connection. For this, you need to enable the API: `--data-api api --local-api`.
A sample Python script that sends console commands to P2Pool via TCP:
```
import sys
import socket
import json
with open('api/local/console', 'r') as file:
data = json.load(file)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', data['tcp_port']))
command = data['cookie'];
command += sys.argv[1];
command += '\n';
s.sendall(command.encode('utf-8'))
s.close()
```
Run it in P2Pool's directory: `python3 ./p2pool_cmd.py status`

View File

@@ -84,8 +84,10 @@ ConsoleCommands::ConsoleCommands(p2pool* pool)
} }
} }
std::random_device rd; std::mt19937_64 rng(RandomDeviceSeed::instance);
std::mt19937_64 rng(rd());
// Diffuse the initial state in case it has low quality
rng.discard(10000);
for (int i = 0; i < 10; ++i) { for (int i = 0; i < 10; ++i) {
if (start_listening(false, "127.0.0.1", 49152 + (rng() % 16384))) { if (start_listening(false, "127.0.0.1", 49152 + (rng() % 16384))) {
@@ -98,6 +100,13 @@ ConsoleCommands::ConsoleCommands(p2pool* pool)
throw std::exception(); throw std::exception();
} }
// 20 random characters in the range [32, 126] should be enough for 128 bits of entropy
m_cookie.resize(20);
for (char& c : m_cookie) {
c = static_cast<char>(rng() % (127 - 32) + 32);
}
if (m_pool->api() && m_pool->params().m_localStats) { if (m_pool->api() && m_pool->params().m_localStats) {
m_pool->api()->set(p2pool_api::Category::LOCAL, "console", m_pool->api()->set(p2pool_api::Category::LOCAL, "console",
[stdin_type, this](log::Stream& s) [stdin_type, this](log::Stream& s)
@@ -114,7 +123,7 @@ ConsoleCommands::ConsoleCommands(p2pool* pool)
s << static_cast<int>(stdin_type); s << static_cast<int>(stdin_type);
} }
s << "\",\"tcp_port\":" << m_listenPort << '}'; s << "\",\"tcp_port\":" << m_listenPort << ",\"cookie\":\"" << log::EscapedString(m_cookie) << "\"}";
}); });
} }
@@ -349,7 +358,7 @@ void ConsoleCommands::stdinReadCallback(uv_stream_t* stream, ssize_t nread, cons
ConsoleCommands* pThis = static_cast<ConsoleCommands*>(stream->data); ConsoleCommands* pThis = static_cast<ConsoleCommands*>(stream->data);
if (nread > 0) { if (nread > 0) {
pThis->process_input(pThis->m_command, buf->base, static_cast<uint32_t>(nread)); pThis->process_input(pThis->m_command, buf->base, static_cast<uint32_t>(nread), false);
} }
else if (nread < 0) { else if (nread < 0) {
LOGWARN(4, "read error " << uv_err_name(static_cast<int>(nread))); LOGWARN(4, "read error " << uv_err_name(static_cast<int>(nread)));
@@ -359,7 +368,7 @@ void ConsoleCommands::stdinReadCallback(uv_stream_t* stream, ssize_t nread, cons
} }
void ConsoleCommands::process_input(std::string& command, const char* data, uint32_t size) void ConsoleCommands::process_input(std::string& command, const char* data, uint32_t size, bool check_cookie)
{ {
command.append(data, size); command.append(data, size);
@@ -370,6 +379,14 @@ void ConsoleCommands::process_input(std::string& command, const char* data, uint
} }
command[k] = '\0'; command[k] = '\0';
if (check_cookie && ((k <= m_cookie.length()) || (memcmp(command.data(), m_cookie.data(), m_cookie.length()) != 0))) {
LOGWARN(4, "cookie check failed, skipping command " << command);
}
else {
if (check_cookie) {
command.erase(0, m_cookie.length());
}
cmd* c = cmds; cmd* c = cmds;
for (; c->name.len; ++c) { for (; c->name.len; ++c) {
if (!strncmp(command.c_str(), c->name.str, c->name.len)) { if (!strncmp(command.c_str(), c->name.str, c->name.len)) {
@@ -396,6 +413,7 @@ void ConsoleCommands::process_input(std::string& command, const char* data, uint
LOGWARN(0, "Unknown command " << command.c_str()); LOGWARN(0, "Unknown command " << command.c_str());
do_help(nullptr, nullptr); do_help(nullptr, nullptr);
} }
}
k = command.find_first_not_of("\r\n", k + 1); k = command.find_first_not_of("\r\n", k + 1);
command.erase(0, k); command.erase(0, k);

View File

@@ -40,7 +40,7 @@ public:
size_t size() const override { return sizeof(ConsoleClient); } size_t size() const override { return sizeof(ConsoleClient); }
bool on_connect() override { return true; }; bool on_connect() override { return true; };
bool on_read(const char* data, uint32_t size) override { static_cast<ConsoleCommands*>(m_owner)->process_input(m_command, data, size); return true; }; bool on_read(const char* data, uint32_t size) override { static_cast<ConsoleCommands*>(m_owner)->process_input(m_command, data, size, true); return true; };
alignas(8) char m_consoleReadBuf[1024] = {}; alignas(8) char m_consoleReadBuf[1024] = {};
@@ -62,11 +62,12 @@ private:
bool m_readBufInUse; bool m_readBufInUse;
std::string m_command; std::string m_command;
std::string m_cookie;
static void allocCallback(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf); static void allocCallback(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf);
static void stdinReadCallback(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf); static void stdinReadCallback(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf);
void process_input(std::string& command, const char* data, uint32_t size); void process_input(std::string& command, const char* data, uint32_t size, bool check_cookie);
}; };
} // namespace p2pool } // namespace p2pool

View File

@@ -435,6 +435,28 @@ template<> struct log::Stream::Entry<Duration>
} }
}; };
struct EscapedString
{
explicit FORCEINLINE EscapedString(const std::string& data, const char* characters_to_escape = "\"\\", char escape_character = '\\')
{
m_data.reserve(data.length() * 2);
for (const char c : data) {
if (strchr(characters_to_escape, c)) {
m_data.append(1, escape_character);
}
m_data.append(1, c);
}
}
std::string m_data;
};
template<> struct log::Stream::Entry<EscapedString>
{
static FORCEINLINE void put(const EscapedString& value, Stream* wrapper) { *wrapper << value.m_data; }
};
template<typename T> template<typename T>
struct PadRight struct PadRight
{ {

View File

@@ -64,11 +64,11 @@ p2pool_api::p2pool_api(const std::string& api_path, const bool local_stats)
m_poolPath = m_apiPath + "pool/"; m_poolPath = m_apiPath + "pool/";
m_localPath = m_apiPath + "local/"; m_localPath = m_apiPath + "local/";
create_dir(m_networkPath); create_dir(m_networkPath, false);
create_dir(m_poolPath); create_dir(m_poolPath, false);
if (local_stats) { if (local_stats) {
create_dir(m_localPath); create_dir(m_localPath, true);
} }
} }
@@ -77,14 +77,16 @@ p2pool_api::~p2pool_api()
uv_mutex_destroy(&m_dumpDataLock); uv_mutex_destroy(&m_dumpDataLock);
} }
void p2pool_api::create_dir(const std::string& path) void p2pool_api::create_dir(const std::string& path, bool is_restricted)
{ {
(void) is_restricted;
#ifdef _MSC_VER #ifdef _MSC_VER
int result = _mkdir(path.c_str()); int result = _mkdir(path.c_str());
#else #else
int result = mkdir(path.c_str() int result = mkdir(path.c_str()
#ifndef _WIN32 #ifndef _WIN32
, 0775 , is_restricted ? 0750 : 0775
#endif #endif
); );
#endif #endif
@@ -169,7 +171,10 @@ void p2pool_api::dump_to_file()
#endif #endif
; ;
const int result = uv_fs_open(uv_default_loop_checked(), &work->req, work->tmp_name.c_str(), flags, 0644, on_fs_open); // LOCAL category has restricted access
const int mode = (work->tmp_name.find(m_localPath) == 0) ? 0640 : 0644;
const int result = uv_fs_open(uv_default_loop_checked(), &work->req, work->tmp_name.c_str(), flags, mode, on_fs_open);
if (result < 0) { if (result < 0) {
LOGWARN(4, "failed to open " << work->tmp_name << ", error " << uv_err_name(result)); LOGWARN(4, "failed to open " << work->tmp_name << ", error " << uv_err_name(result));
delete work; delete work;

View File

@@ -41,7 +41,7 @@ public:
void set(Category category, const char* filename, T&& callback) { dump_to_file_async_internal(category, filename, Callback<void, log::Stream&>::Derived<T>(std::move(callback))); } void set(Category category, const char* filename, T&& callback) { dump_to_file_async_internal(category, filename, Callback<void, log::Stream&>::Derived<T>(std::move(callback))); }
private: private:
void create_dir(const std::string& path); void create_dir(const std::string& path, bool is_restricted);
static void on_dump_to_file(uv_async_t* async) { reinterpret_cast<p2pool_api*>(async->data)->dump_to_file(); } static void on_dump_to_file(uv_async_t* async) { reinterpret_cast<p2pool_api*>(async->data)->dump_to_file(); }