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
exit|terminate p2pool
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(rd());
std::mt19937_64 rng(RandomDeviceSeed::instance);
// Diffuse the initial state in case it has low quality
rng.discard(10000);
for (int i = 0; i < 10; ++i) {
if (start_listening(false, "127.0.0.1", 49152 + (rng() % 16384))) {
@@ -98,6 +100,13 @@ ConsoleCommands::ConsoleCommands(p2pool* pool)
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) {
m_pool->api()->set(p2pool_api::Category::LOCAL, "console",
[stdin_type, this](log::Stream& s)
@@ -114,7 +123,7 @@ ConsoleCommands::ConsoleCommands(p2pool* pool)
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);
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) {
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);
@@ -370,31 +379,40 @@ void ConsoleCommands::process_input(std::string& command, const char* data, uint
}
command[k] = '\0';
cmd* c = cmds;
for (; c->name.len; ++c) {
if (!strncmp(command.c_str(), c->name.str, c->name.len)) {
const char* args = (c->name.len + 1 <= k) ? (command.c_str() + c->name.len + 1) : "";
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());
}
// Skip spaces
while ((args[0] == ' ') || (args[0] == '\t')) {
++args;
}
cmd* c = cmds;
for (; c->name.len; ++c) {
if (!strncmp(command.c_str(), c->name.str, c->name.len)) {
const char* args = (c->name.len + 1 <= k) ? (command.c_str() + c->name.len + 1) : "";
// Check if an argument is required
if (strlen(c->arg) && !strlen(args)) {
LOGWARN(0, c->name.str << " requires arguments");
do_help(nullptr, nullptr);
// Skip spaces
while ((args[0] == ' ') || (args[0] == '\t')) {
++args;
}
// Check if an argument is required
if (strlen(c->arg) && !strlen(args)) {
LOGWARN(0, c->name.str << " requires arguments");
do_help(nullptr, nullptr);
break;
}
c->func(m_pool, args);
break;
}
c->func(m_pool, args);
break;
}
}
if (!c->name.len) {
LOGWARN(0, "Unknown command " << command.c_str());
do_help(nullptr, nullptr);
if (!c->name.len) {
LOGWARN(0, "Unknown command " << command.c_str());
do_help(nullptr, nullptr);
}
}
k = command.find_first_not_of("\r\n", k + 1);

View File

@@ -40,7 +40,7 @@ public:
size_t size() const override { return sizeof(ConsoleClient); }
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] = {};
@@ -62,11 +62,12 @@ private:
bool m_readBufInUse;
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 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

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>
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_localPath = m_apiPath + "local/";
create_dir(m_networkPath);
create_dir(m_poolPath);
create_dir(m_networkPath, false);
create_dir(m_poolPath, false);
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);
}
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
int result = _mkdir(path.c_str());
#else
int result = mkdir(path.c_str()
#ifndef _WIN32
, 0775
, is_restricted ? 0750 : 0775
#endif
);
#endif
@@ -169,7 +171,10 @@ void p2pool_api::dump_to_file()
#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) {
LOGWARN(4, "failed to open " << work->tmp_name << ", error " << uv_err_name(result));
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))); }
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(); }