diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml index e635173..74bc20a 100644 --- a/.github/workflows/c-cpp.yml +++ b/.github/workflows/c-cpp.yml @@ -423,11 +423,12 @@ jobs: strategy: matrix: config: - - {vs: Visual Studio 17 2022, os: 2022, vspath: "C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise", rx: "ON", upnp: "ON", grpc: "ON"} - - {vs: Visual Studio 16 2019, os: 2019, vspath: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Enterprise", rx: "ON", upnp: "ON", grpc: "ON"} - - {vs: Visual Studio 16 2019, os: 2019, vspath: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Enterprise", rx: "ON", upnp: "ON", grpc: "OFF"} - - {vs: Visual Studio 16 2019, os: 2019, vspath: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Enterprise", rx: "OFF", upnp: "ON", grpc: "OFF"} - - {vs: Visual Studio 16 2019, os: 2019, vspath: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Enterprise", rx: "OFF", upnp: "OFF", grpc: "OFF"} + - {vs: Visual Studio 17 2022, os: 2022, vspath: "C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise", rx: "ON", upnp: "ON", grpc: "ON", tls: "ON"} + - {vs: Visual Studio 16 2019, os: 2019, vspath: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Enterprise", rx: "ON", upnp: "ON", grpc: "ON", tls: "ON"} + - {vs: Visual Studio 16 2019, os: 2019, vspath: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Enterprise", rx: "ON", upnp: "ON", grpc: "OFF", tls: "ON"} + - {vs: Visual Studio 16 2019, os: 2019, vspath: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Enterprise", rx: "OFF", upnp: "ON", grpc: "OFF", tls: "ON"} + - {vs: Visual Studio 16 2019, os: 2019, vspath: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Enterprise", rx: "OFF", upnp: "OFF", grpc: "OFF", tls: "ON"} + - {vs: Visual Studio 16 2019, os: 2019, vspath: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Enterprise", rx: "OFF", upnp: "OFF", grpc: "OFF", tls: "OFF"} steps: - name: Checkout repository @@ -442,7 +443,7 @@ jobs: run: | mkdir build cd build - cmake .. -G "${{ matrix.config.vs }}" -DCMAKE_SYSTEM_VERSION="10.0" -DWITH_RANDOMX=${{ matrix.config.rx }} -DWITH_UPNP=${{ matrix.config.upnp }} -DWITH_GRPC=${{ matrix.config.grpc }} + cmake .. -G "${{ matrix.config.vs }}" -DCMAKE_SYSTEM_VERSION="10.0" -DWITH_RANDOMX=${{ matrix.config.rx }} -DWITH_UPNP=${{ matrix.config.upnp }} -DWITH_GRPC=${{ matrix.config.grpc }} -DWITH_TLS=${{ matrix.config.tls }} & "${{ matrix.config.vspath }}\\MSBuild\\Current\\Bin\\amd64\\msbuild" -v:m /m /p:Configuration=Release p2pool.vcxproj - name: Check Windows 7 compatibility @@ -480,7 +481,7 @@ jobs: - name: Archive binary uses: actions/upload-artifact@v4 with: - name: p2pool-vs-${{ matrix.config.os }}-randomx-${{ matrix.config.rx }}-upnp-${{ matrix.config.upnp }}-grpc-${{ matrix.config.grpc }}.exe + name: p2pool-vs-${{ matrix.config.os }}-randomx-${{ matrix.config.rx }}-upnp-${{ matrix.config.upnp }}-grpc-${{ matrix.config.grpc }}-tls-${{ matrix.config.tls }}.exe path: build/Release/p2pool.exe build-macos: diff --git a/CMakeLists.txt b/CMakeLists.txt index 8dd9c8c..a69055d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,7 @@ option(WITH_RANDOMX "Include the RandomX library in the build. If this is turned option(WITH_LTO "Use link-time compiler optimization (if linking fails for you, run cmake with -DWITH_LTO=OFF)" ON) option(WITH_UPNP "Include UPnP support. If this is turned off, p2pool will not be able to configure port forwarding on UPnP-enabled routers." ON) option(WITH_GRPC "Include gRPC support. If this is turned off, p2pool will not be able to merge mine with Tari." ON) +option(WITH_TLS "Include TLS support. If this is turned off, p2pool will not support Stratum TLS connections." ON) option(DEV_TEST_SYNC "[Developer only] Sync test, stop p2pool after sync is complete" OFF) option(DEV_WITH_TSAN "[Developer only] Compile with thread sanitizer" OFF) @@ -37,6 +38,9 @@ if (WITH_GRPC) include(cmake/grpc.cmake) add_subdirectory(external/src/Tari) +elseif (WITH_TLS) + add_subdirectory(cmake/ssl) + include_directories(external/src/grpc/third_party/boringssl-with-bazel/src/include) endif() if (WITH_RANDOMX) @@ -166,6 +170,13 @@ if (WITH_GRPC) set(SOURCES ${SOURCES} src/merge_mining_client_tari.cpp) endif() +if (WITH_TLS) + add_definitions(-DWITH_TLS) + + set(HEADERS ${HEADERS} src/tls.h) + set(SOURCES ${SOURCES} src/tls.cpp) +endif() + source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "Header Files" FILES ${HEADERS}) source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "Source Files" FILES ${SOURCES}) @@ -404,6 +415,8 @@ if (STATIC_BINARY OR STATIC_LIBS) if (WITH_GRPC) set(STATIC_LIBS ${STATIC_LIBS} Tari_gRPC grpc grpc++ libprotobuf) + elseif(WITH_TLS) + set(STATIC_LIBS ${STATIC_LIBS} ssl crypto) endif() target_link_libraries(${CMAKE_PROJECT_NAME} @@ -415,6 +428,8 @@ if (STATIC_BINARY OR STATIC_LIBS) else() if (WITH_GRPC) set(LIBS ${LIBS} Tari_gRPC grpc grpc++ libprotobuf) + elseif(WITH_TLS) + set(LIBS ${LIBS} ssl crypto) endif() target_link_libraries(${CMAKE_PROJECT_NAME} debug ${ZMQ_LIBRARY_DEBUG} debug ${UV_LIBRARY_DEBUG} debug ${CURL_LIBRARY_DEBUG} optimized ${ZMQ_LIBRARY} optimized ${UV_LIBRARY} optimized ${CURL_LIBRARY} ${LIBS}) diff --git a/cmake/grpc.cmake b/cmake/grpc.cmake index dbafb60..5d3e58f 100644 --- a/cmake/grpc.cmake +++ b/cmake/grpc.cmake @@ -35,5 +35,6 @@ add_definitions(-DPROTOBUF_ENABLE_DEBUG_LOGGING_MAY_LEAK_PII=0) add_subdirectory(external/src/grpc) include_directories(external/src/grpc/third_party/abseil-cpp) +include_directories(external/src/grpc/third_party/boringssl-with-bazel/src/include) include_directories(external/src/grpc/third_party/protobuf/src) include_directories(external/src/grpc/include) diff --git a/cmake/ssl/CMakeLists.txt b/cmake/ssl/CMakeLists.txt new file mode 100644 index 0000000..40992a8 --- /dev/null +++ b/cmake/ssl/CMakeLists.txt @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 3.12) + +project(P2Pool_SSL LANGUAGES C CXX) + +if (CMAKE_CXX_COMPILER_ID MATCHES MSVC) + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} /W0 /Zi /Od /Ob0 /MP /MTd") + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /W0 /Zi /Od /Ob0 /MP /MTd") + set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} /W0 /O1 /Ob2 /Oi /Os /Oy /MP /MT") + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /W0 /O1 /Ob2 /Oi /Os /Oy /MP /MT") + set(CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS_RELWITHDEBINFO} /W0 /Ob1 /Ot /Zi /MP /MT") + set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} /W0 /Ob1 /Ot /Zi /MP /MT") +else() + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Os -w") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Os -w") +endif() + +if(CMAKE_GENERATOR MATCHES "Visual Studio") + if(CMAKE_VERSION VERSION_LESS 3.13) + message(WARNING "Disabling SSL assembly support because CMake version ${CMAKE_VERSION} is too old (less than 3.13)") + set(OPENSSL_NO_ASM ON) + else() + include(CheckLanguage) + check_language(ASM_NASM) + if(NOT CMAKE_ASM_NASM_COMPILER) + message(WARNING "Disabling SSL assembly support because NASM could not be found") + set(OPENSSL_NO_ASM ON) + endif() + endif() +endif() + +add_subdirectory(../../external/src/grpc/third_party/boringssl-with-bazel BoringSSL) diff --git a/docs/COMMAND_LINE.MD b/docs/COMMAND_LINE.MD index 3c7c213..f3035eb 100644 --- a/docs/COMMAND_LINE.MD +++ b/docs/COMMAND_LINE.MD @@ -31,6 +31,8 @@ --upnp-stratum Port forward Stratum port (it's not forwarded by default) --merge-mine IP:port and wallet address for another blockchain to merge mine with --version Print p2pool's version and build details +--tls-cert file Load TLS certificate chain from "file" in the PEM format +--tls-cert-key file Load TLS certificate private key from "file" in the PEM format ``` ### Example command line @@ -68,3 +70,13 @@ Merge mining will be available in P2Pool after the fork on October 12th, 2024. V p2pool.exe --wallet YOUR_MONERO_WALLET_ADDRESS --merge-mine tari://IP:port TARI_WALLET_ADDRESS ``` Merge mining is available for testing in Tari's [v1.0.0-pre.14 release](https://github.com/tari-project/tari/releases/tag/v1.0.0-pre.14) (Esmeralda testnet). + +### TLS + +All `--tls...` parameters are optional. If they are not provided, P2Pool will generate a self-signed certificate upon startup. + +If you want to use your own certificate, please refer to your certificate provider's documentation (or OpenSSL documentation) on how to generate the appropriate PEM files. + +Note that you need to use certificate files and `--tls...` parameters to be able to use certificate pinning in XMRig miners that connect to your P2Pool instance. + +`--tls-cert` and `--tls-cert-key` parameters must always be used together. diff --git a/src/main.cpp b/src/main.cpp index 8dd463b..4bf3485 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -64,6 +64,10 @@ void p2pool_usage() #endif "--merge-mine IP:port and wallet address for another blockchain to merge mine with\n" "--version Print p2pool's version and build details\n" +#ifdef WITH_TLS + "--tls-cert file Load TLS certificate chain from \"file\" in the PEM format\n" + "--tls-cert-key file Load TLS certificate private key from \"file\" in the PEM format\n" +#endif "--help Show this help message\n\n" "Example command line:\n\n" "%s --host 127.0.0.1 --rpc-port 18081 --zmq-port 18083 --wallet YOUR_WALLET_ADDRESS --stratum 0.0.0.0:%d --p2p 0.0.0.0:%d\n\n", diff --git a/src/p2pool.cpp b/src/p2pool.cpp index 9f9b356..966eb37 100644 --- a/src/p2pool.cpp +++ b/src/p2pool.cpp @@ -1903,6 +1903,15 @@ int p2pool::run() return 1; } +#ifdef WITH_TLS + if (!m_params->m_tlsCert.empty() && !m_params->m_tlsCertKey.empty()) { + if (!ServerTls::load_from_files(m_params->m_tlsCert.c_str(), m_params->m_tlsCertKey.c_str())) { + LOGERR(1, "Failed to load TLS files"); + return 1; + } + } +#endif + // Init default loop user data before running it uv_loop_t* loop = uv_default_loop_checked(); loop->data = nullptr; diff --git a/src/params.cpp b/src/params.cpp index 9789b83..3abeafe 100644 --- a/src/params.cpp +++ b/src/params.cpp @@ -195,6 +195,18 @@ Params::Params(int argc, char* const argv[]) ok = true; } +#ifdef WITH_TLS + if ((strcmp(argv[i], "--tls-cert") == 0) && (i + 1 < argc)) { + m_tlsCert = argv[++i]; + ok = true; + } + + if ((strcmp(argv[i], "--tls-cert-key") == 0) && (i + 1 < argc)) { + m_tlsCertKey = argv[++i]; + ok = true; + } +#endif + if (!ok) { fprintf(stderr, "Unknown command line parameter %s\n\n", argv[i]); p2pool_usage(); @@ -240,6 +252,13 @@ bool Params::valid() const return false; } +#ifdef WITH_TLS + if (m_tlsCert.empty() != m_tlsCertKey.empty()) { + LOGERR(1, "Both --tls-cert and --tls-cert-key files must be specified"); + return false; + } +#endif + return true; } diff --git a/src/params.h b/src/params.h index 2d6827b..95f7c16 100644 --- a/src/params.h +++ b/src/params.h @@ -92,6 +92,10 @@ struct Params bool m_upnp = false; bool m_upnpStratum = false; #endif +#ifdef WITH_TLS + std::string m_tlsCert; + std::string m_tlsCertKey; +#endif }; } // namespace p2pool diff --git a/src/stratum_server.cpp b/src/stratum_server.cpp index beda290..1e6092c 100644 --- a/src/stratum_server.cpp +++ b/src/stratum_server.cpp @@ -272,7 +272,7 @@ bool StratumServer::on_login(StratumClient* client, uint32_t id, const char* log target = std::max(target, aux_diff.target()); if (get_custom_diff(login, client->m_customDiff)) { - LOGINFO(5, "client " << log::Gray() << static_cast(client->m_addrString) << " set custom difficulty " << client->m_customDiff); + LOGINFO(5, "client " << log::Gray() << static_cast(client->m_addrString) << log::NoColor() << " set custom difficulty " << client->m_customDiff); target = std::max(target, client->m_customDiff.target()); } else if (m_autoDiff) { @@ -282,7 +282,7 @@ bool StratumServer::on_login(StratumClient* client, uint32_t id, const char* log if (get_custom_user(login, client->m_customUser)) { const char* s = client->m_customUser; - LOGINFO(5, "client " << log::Gray() << static_cast(client->m_addrString) << " set custom user " << s); + LOGINFO(5, "client " << log::Gray() << static_cast(client->m_addrString) << log::NoColor() << " set custom user " << s); } uint32_t job_id; @@ -541,6 +541,7 @@ void StratumServer::show_workers() size_t n = 0; LOGINFO(0, log::pad_right("IP:port", addr_len + 8) + << "TLS " << log::pad_right("uptime", 20) << log::pad_right("difficulty", 20) << log::pad_right("hashrate", 15) @@ -557,7 +558,15 @@ void StratumServer::show_workers() ++diff.lo; } } + +#ifdef WITH_TLS + const bool is_tls = c->m_tls.is_empty(); +#else + constexpr bool is_tls = false; +#endif + LOGINFO(0, log::pad_right(static_cast(c->m_addrString), addr_len + 8) + << (is_tls ? "no " : "yes ") << log::pad_right(log::Duration(cur_time - c->m_connectedTime), 20) << log::pad_right(diff, 20) << log::pad_right(log::Hashrate(c->m_autoDiff.lo / AUTO_DIFF_TARGET_TIME, m_autoDiff && (c->m_autoDiff != 0)), 15) @@ -1080,7 +1089,8 @@ void StratumServer::on_shutdown() } StratumServer::StratumClient::StratumClient() - : Client(m_stratumReadBuf, sizeof(m_stratumReadBuf)) + : Client(m_rawReadBuf, sizeof(m_rawReadBuf)) + , m_stratumReadBufBytes(0) , m_rpcId(0) , m_perConnectionJobId(0) , m_connectedTime(0) @@ -1100,6 +1110,10 @@ StratumServer::StratumClient::StratumClient() void StratumServer::StratumClient::reset() { Client::reset(); + + m_stratumReadBuf[0] = '\0'; + m_stratumReadBufBytes = 0; + m_rpcId = 0; m_perConnectionJobId = 0; m_connectedTime = 0; @@ -1127,35 +1141,78 @@ bool StratumServer::StratumClient::on_connect() bool StratumServer::StratumClient::on_read(char* data, uint32_t size) { - if ((data != m_readBuf + m_numRead) || (data + size > m_readBuf + m_readBufSize)) { - LOGERR(1, "client: invalid data pointer or size in on_read()"); - ban(DEFAULT_BAN_TIME); - return false; - } - - m_numRead += size; - - char* line_start = m_readBuf; - for (char* c = data; c < m_readBuf + m_numRead; ++c) { - if (*c == '\n') { - *c = '\0'; - if (!process_request(line_start, static_cast(c - line_start))) { - ban(DEFAULT_BAN_TIME); +#ifdef WITH_TLS + if (!m_tlsChecked) { + if (data[0] == 0x16) { + if (!m_tls.init()) { + LOGWARN(5, "client " << static_cast(m_addrString) << ": TLS init failed"); return false; } - line_start = c + 1; + LOGINFO(5, "client " << log::Gray() << static_cast(m_addrString) << log::NoColor() << " is using TLS"); } + m_tlsChecked = true; } +#endif - // Move the possible unfinished line to the beginning of m_readBuf to free up more space for reading - if (line_start != m_readBuf) { - m_numRead = static_cast(m_readBuf + m_numRead - line_start); - if (m_numRead > 0) { - memmove(m_readBuf, line_start, m_numRead); + auto on_parse = [this](char* data, uint32_t size) { + if (static_cast(m_stratumReadBufBytes) + size > STRATUM_BUF_SIZE) { + LOGWARN(4, "client " << static_cast(m_addrString) << " sent too long Stratum message"); + ban(DEFAULT_BAN_TIME); + return false; } - } - return true; + memcpy(m_stratumReadBuf + m_stratumReadBufBytes, data, size); + m_stratumReadBufBytes += size; + + char* line_start = m_stratumReadBuf; + for (char *e = line_start + m_stratumReadBufBytes, *c = e - size; c < e; ++c) { + if (*c == '\n') { + // Check if the line starts with "GET " (an HTTP request) + if ((c - line_start >= 4) && (*reinterpret_cast(line_start) == 0x20544547U)) { + LOGINFO(5, "client " << log::Gray() << static_cast(m_addrString) << log::NoColor() << " sent an HTTP request"); + send_http_response(); + close(); + return true; + } + + *c = '\0'; + if (!process_request(line_start, static_cast(c - line_start))) { + ban(DEFAULT_BAN_TIME); + return false; + } + + line_start = c + 1; + } + } + + // Move the possible unfinished line to the beginning of m_stratumReadBuf to free up more space for reading + if (line_start != m_stratumReadBuf) { + m_stratumReadBufBytes = static_cast(m_stratumReadBuf + m_stratumReadBufBytes - line_start); + if (m_stratumReadBufBytes > 0) { + memmove(m_stratumReadBuf, line_start, m_stratumReadBufBytes); + } + } + + return true; + }; + +#ifdef WITH_TLS + if (!m_tls.is_empty()) { + auto on_write = [this](const uint8_t* data, size_t size) { + return m_owner->send(this, [data, size](uint8_t* buf, size_t buf_size) -> size_t { + if (buf_size < size) { + return 0; + } + memcpy(buf, data, size); + return size; + }, true); + }; + + return m_tls.on_read(data, size, std::move(on_parse), std::move(on_write)); + } +#endif + + return on_parse(data, size); } bool StratumServer::StratumClient::process_request(char* data, uint32_t size) @@ -1317,6 +1374,25 @@ bool StratumServer::StratumClient::process_submit(rapidjson::Document& doc, uint return static_cast(m_owner)->on_submit(this, id, job_id.GetString(), nonce.GetString(), result.GetString()); } +bool StratumServer::StratumClient::send_http_response() +{ + return m_owner->send(this, [](uint8_t *buf, size_t buf_size) -> size_t { + static constexpr uint8_t data[] = + "HTTP/1.1 200 OK\r\n" + "Content-Length: 21\r\n" + "Content-Type: text/html\r\n" + "Connection: Closed\r\n\r\n" + "P2Pool Stratum online"; + + if (buf_size < sizeof(data)) { + return 0; + } + + memcpy(buf, data, sizeof(data)); + return sizeof(data); + }); +} + void StratumServer::api_update_local_stats(uint64_t timestamp) { if (!m_pool->api() || !m_pool->params().m_localStats || m_pool->stopped()) { diff --git a/src/stratum_server.h b/src/stratum_server.h index 5007d91..ed09440 100644 --- a/src/stratum_server.h +++ b/src/stratum_server.h @@ -52,7 +52,12 @@ public: [[nodiscard]] bool process_login(rapidjson::Document& doc, uint32_t id); [[nodiscard]] bool process_submit(rapidjson::Document& doc, uint32_t id); + bool send_http_response(); + + alignas(8) char m_rawReadBuf[STRATUM_BUF_SIZE]; + alignas(8) char m_stratumReadBuf[STRATUM_BUF_SIZE]; + uint32_t m_stratumReadBufBytes; uint32_t m_rpcId; uint32_t m_perConnectionJobId; diff --git a/src/tcp_server.cpp b/src/tcp_server.cpp index fb3df62..68d7884 100644 --- a/src/tcp_server.cpp +++ b/src/tcp_server.cpp @@ -538,7 +538,7 @@ void TCPServer::print_bans() } } -bool TCPServer::send_internal(Client* client, Callback::Base&& callback) +bool TCPServer::send_internal(Client* client, Callback::Base&& callback, bool raw) { check_event_loop_thread(__func__); @@ -559,34 +559,50 @@ bool TCPServer::send_internal(Client* client, Callback return false; } - WriteBuf* buf = get_write_buffer(bytes_written); + auto on_write = [this, client](const uint8_t* data, size_t size) { + WriteBuf* buf = get_write_buffer(size); - buf->m_write.data = buf; - buf->m_client = client; + buf->m_write.data = buf; + buf->m_client = client; - if (buf->m_dataCapacity < bytes_written) { - buf->m_dataCapacity = round_up(bytes_written, 64); - buf->m_data = realloc_hook(buf->m_data, buf->m_dataCapacity); - if (!buf->m_data) { - LOGERR(0, "failed to allocate " << buf->m_dataCapacity << " bytes to send data"); - PANIC_STOP(); + if (buf->m_dataCapacity < size) { + buf->m_dataCapacity = round_up(size, 64); + buf->m_data = realloc_hook(buf->m_data, buf->m_dataCapacity); + if (!buf->m_data) { + LOGERR(0, "failed to allocate " << buf->m_dataCapacity << " bytes to send data"); + PANIC_STOP(); + } } + + memcpy(buf->m_data, data, size); + + uv_buf_t bufs[1]; + bufs[0].base = reinterpret_cast(buf->m_data); + bufs[0].len = static_cast(size); + + const int err = uv_write(&buf->m_write, reinterpret_cast(&client->m_socket), bufs, 1, Client::on_write); + if (err) { + LOGWARN(1, "failed to start writing data to client connection " << static_cast(client->m_addrString) << ", error " << uv_err_name(err)); + return_write_buffer(buf); + return false; + } + + return true; + }; + +#ifdef WITH_TLS + if (!client->m_tls.is_empty() && !raw) { + if (!client->m_tls.on_write(m_callbackBuf.data(), bytes_written, std::move(on_write))) { + LOGWARN(1, "TLS write failed to client connection " << static_cast(client->m_addrString)); + return false; + } + return true; } +#else + (void)raw; +#endif - memcpy(buf->m_data, m_callbackBuf.data(), bytes_written); - - uv_buf_t bufs[1]; - bufs[0].base = reinterpret_cast(buf->m_data); - bufs[0].len = static_cast(bytes_written); - - const int err = uv_write(&buf->m_write, reinterpret_cast(&client->m_socket), bufs, 1, Client::on_write); - if (err) { - LOGWARN(1, "failed to start writing data to client connection " << static_cast(client->m_addrString) << ", error " << uv_err_name(err)); - return_write_buffer(buf); - return false; - } - - return true; + return on_write(m_callbackBuf.data(), bytes_written); } const char* TCPServer::get_log_category() const @@ -999,6 +1015,9 @@ TCPServer::Client::Client(char* read_buf, size_t size) , m_addrString{} , m_socks5ProxyState(Socks5ProxyState::Default) , m_resetCounter{ 0 } +#ifdef WITH_TLS + , m_tlsChecked(false) +#endif { m_readBuf[0] = '\0'; m_readBuf[m_readBufSize - 1] = '\0'; @@ -1023,6 +1042,11 @@ void TCPServer::Client::reset() m_socks5ProxyState = Socks5ProxyState::Default; m_readBuf[0] = '\0'; m_readBuf[m_readBufSize - 1] = '\0'; + +#ifdef WITH_TLS + m_tls.reset(); + m_tlsChecked = false; +#endif } void TCPServer::Client::on_alloc(uv_handle_t* handle, size_t /*suggested_size*/, uv_buf_t* buf) diff --git a/src/tcp_server.h b/src/tcp_server.h index 372854f..262ed06 100644 --- a/src/tcp_server.h +++ b/src/tcp_server.h @@ -18,6 +18,11 @@ #pragma once #include "uv_util.h" + +#ifdef WITH_TLS +#include "tls.h" +#endif + #include namespace p2pool { @@ -106,6 +111,11 @@ public: } m_socks5ProxyState; std::atomic m_resetCounter; + +#ifdef WITH_TLS + ServerTls m_tls; + bool m_tlsChecked; +#endif }; struct WriteBuf @@ -128,7 +138,7 @@ public: } template - [[nodiscard]] FORCEINLINE bool send(Client* client, T&& callback) { return send_internal(client, Callback::Derived(std::move(callback))); } + [[nodiscard]] FORCEINLINE bool send(Client* client, T&& callback, bool raw = false) { return send_internal(client, Callback::Derived(std::move(callback)), raw); } private: static void on_new_connection(uv_stream_t* server, int status); @@ -138,7 +148,7 @@ private: void on_new_client(uv_stream_t* server); void on_new_client(uv_stream_t* server, Client* client); - [[nodiscard]] bool send_internal(Client* client, Callback::Base&& callback); + [[nodiscard]] bool send_internal(Client* client, Callback::Base&& callback, bool raw); allocate_client_callback m_allocateNewClient; diff --git a/src/tls.cpp b/src/tls.cpp new file mode 100644 index 0000000..5d4aa5e --- /dev/null +++ b/src/tls.cpp @@ -0,0 +1,321 @@ +/* + * This file is part of the Monero P2Pool + * Copyright (c) 2021-2024 SChernykh + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "common.h" +#include "tls.h" + +LOG_CATEGORY(TLS) + +namespace p2pool { + +static bssl::UniquePtr init_evp_pkey() +{ + bssl::UniquePtr evp_pkey(EVP_PKEY_new()); + + if (!evp_pkey.get()) { + return nullptr; + } + + bssl::UniquePtr ec_key(EC_KEY_new_by_curve_name(NID_X9_62_prime256v1)); + + if (!ec_key || !EC_KEY_generate_key(ec_key.get())) { + return nullptr; + } + + if (!EVP_PKEY_assign_EC_KEY(evp_pkey.get(), ec_key.release())) { + return nullptr; + } + + //FILE* fp; + //if (fopen_s(&fp, "cert_key.pem", "wb") == 0) { + // PEM_write_PrivateKey(fp, evp_pkey.get(), nullptr, nullptr, 0, nullptr, nullptr); + // fclose(fp); + //} + + return evp_pkey; +} + +static bssl::UniquePtr s_evp_pkey = init_evp_pkey(); + +static bssl::UniquePtr init_cert() +{ + bssl::UniquePtr x509(X509_new()); + + if (!x509.get()) { + return nullptr; + } + + if (!X509_set_version(x509.get(), X509_VERSION_3)) { + return nullptr; + } + + std::mt19937_64 rng(RandomDeviceSeed::instance); + rng.discard(10000); + + const uint64_t serial = rng(); + + if (!ASN1_INTEGER_set_uint64(X509_get_serialNumber(x509.get()), serial)) { + return nullptr; + } + + constexpr int64_t YEAR = 31557600; + + const time_t cur_time = time(nullptr); + + const time_t t0 = cur_time - (cur_time % YEAR); + const time_t t1 = t0 - YEAR * 10; + const time_t t2 = t0 + YEAR * 10; + + if (!ASN1_TIME_set(X509_get_notBefore(x509.get()), t1) || !ASN1_TIME_set(X509_get_notAfter(x509.get()), t2)) { + return nullptr; + } + + X509_NAME* subject = X509_get_subject_name(x509.get()); + + if (!X509_NAME_add_entry_by_txt(subject, "C", MBSTRING_ASC, reinterpret_cast("US"), -1, -1, 0) || + !X509_NAME_add_entry_by_txt(subject, "O", MBSTRING_ASC, reinterpret_cast("BoringSSL"), -1, -1, 0) || + !X509_set_issuer_name(x509.get(), subject)) { + return nullptr; + } + + bssl::UniquePtr ekus(sk_ASN1_OBJECT_new_null()); + + if (!ekus || !sk_ASN1_OBJECT_push(ekus.get(), OBJ_nid2obj(NID_server_auth)) || !X509_add1_ext_i2d(x509.get(), NID_ext_key_usage, ekus.get(), 1, 0)) { + return nullptr; + } + + if (!X509_set_pubkey(x509.get(), s_evp_pkey.get())) { + return nullptr; + } + + if (!X509_sign(x509.get(), s_evp_pkey.get(), EVP_sha256())) { + return nullptr; + } + + //FILE* fp; + //if (fopen_s(&fp, "cert.pem", "wb") == 0) { + // PEM_write_X509(fp, x509.get()); + // fclose(fp); + //} + + return x509; +} + +static bssl::UniquePtr s_cert = init_cert(); + +static bssl::UniquePtr init_ctx() +{ + if (!s_evp_pkey.get() || !s_cert.get()) { + return nullptr; + } + + bssl::UniquePtr ctx(SSL_CTX_new(TLS_method())); + + if (!ctx.get()) { + return nullptr; + } + + if (!SSL_CTX_use_PrivateKey(ctx.get(), s_evp_pkey.get())) { + return nullptr; + } + + if (!SSL_CTX_use_certificate(ctx.get(), s_cert.get())) { + return nullptr; + } + + return ctx; +} + +static bssl::UniquePtr s_ctx = init_ctx(); + +bool ServerTls::load_from_files(const char* cert, const char* cert_key) +{ + if (!cert) { + LOGERR(0, "No cert file specified"); + return false; + } + + if (!cert_key) { + LOGERR(0, "No cert_key file specified"); + return false; + } + + bssl::UniquePtr ctx(SSL_CTX_new(TLS_method())); + + if (!ctx.get()) { + LOGERR(0, "Failed to create SSL context"); + return false; + } + + if (SSL_CTX_use_certificate_chain_file(ctx.get(), cert) <= 0) { + LOGERR(0, "Failed to load " << cert); + return false; + } + + if (SSL_CTX_use_PrivateKey_file(ctx.get(), cert_key, SSL_FILETYPE_PEM) <= 0) { + LOGERR(0, "Failed to load " << cert_key); + return false; + } + + SSL_CTX_set_options(ctx.get(), SSL_OP_CIPHER_SERVER_PREFERENCE); + + LOGINFO(1, log::LightCyan() << "Loaded " << cert << ", " << cert_key); + + s_ctx.reset(ctx.release()); + return true; +} + +void ServerTls::reset() +{ + m_ssl.reset(nullptr); +} + +bool ServerTls::init() +{ + if (!s_ctx.get()) { + static std::atomic ctx_error_shown = 0; + if (ctx_error_shown.exchange(1) == 0) { + LOGERR(0, "Failed to initialize an SSL context"); + } + return false; + } + + m_ssl.reset(SSL_new(s_ctx.get())); + + if (!m_ssl.get()) { + return false; + } + + SSL_set_accept_state(m_ssl.get()); + + BIO* rbio = BIO_new(BIO_s_mem()); + BIO* wbio = BIO_new(BIO_s_mem()); + + if (!rbio || !wbio) { + BIO_free(rbio); + BIO_free(wbio); + + m_ssl.reset(nullptr); + return false; + } + + SSL_set_bio(m_ssl.get(), rbio, wbio); + return true; +} + +bool ServerTls::on_read_internal(char* data, uint32_t size, ReadCallback::Base&& read_callback, WriteCallback::Base&& write_callback) +{ + SSL* ssl = m_ssl.get(); + if (!ssl) { + return false; + } + + if (!BIO_write_all(SSL_get_rbio(ssl), data, size)) { + return false; + } + + if (!SSL_is_init_finished(ssl)) { + const int result = SSL_do_handshake(ssl); + + if (!result) { + // EOF + return false; + } + + // Send pending handshake data, if any + BIO* wbio = SSL_get_wbio(ssl); + if (!wbio) { + return false; + } + + const uint8_t* bio_data; + size_t bio_len; + + if (!BIO_mem_contents(wbio, &bio_data, &bio_len)) { + return false; + } + + if (bio_len > 0) { + if (!write_callback(bio_data, bio_len)) { + return false; + } + if (!BIO_reset(wbio)) { + return false; + } + } + + if ((result < 0) && (SSL_get_error(ssl, result) == SSL_ERROR_WANT_READ)) { + // Continue handshake, nothing to read yet + return true; + } + else if (result == 1) { + // Handshake finished, skip to "SSL_read" further down + } + else { + // Some other error + return false; + } + } + + int bytes_read; + char buf[1024]; + + while ((bytes_read = SSL_read(ssl, buf, sizeof(buf))) > 0) { + if (!read_callback(buf, static_cast(bytes_read))) { + return false; + } + } + + return true; +} + +bool ServerTls::on_write_internal(const uint8_t* data, size_t size, WriteCallback::Base&& write_callback) +{ + SSL* ssl = m_ssl.get(); + if (!ssl) { + return false; + } + + if (SSL_write(ssl, data, static_cast(size)) <= 0) { + return false; + } + + BIO* wbio = SSL_get_wbio(ssl); + if (!wbio) { + return false; + } + + const uint8_t* bio_data; + size_t bio_len; + + if (!BIO_mem_contents(wbio, &bio_data, &bio_len)) { + return false; + } + + if (bio_len > 0) { + if (!write_callback(bio_data, bio_len)) { + return false; + } + if (!BIO_reset(wbio)) { + return false; + } + } + + return true; +} + +} // namespace p2pool diff --git a/src/tls.h b/src/tls.h new file mode 100644 index 0000000..e4387ac --- /dev/null +++ b/src/tls.h @@ -0,0 +1,70 @@ +/* + * This file is part of the Monero P2Pool + * Copyright (c) 2021-2024 SChernykh + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcast-qual" +#endif + +#include + +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic pop +#endif + +namespace p2pool { + +class ServerTls +{ +public: + FORCEINLINE ServerTls() { reset(); } + + [[nodiscard]] static bool load_from_files(const char* cert, const char* cert_key); + + void reset(); + [[nodiscard]] bool init(); + + template + [[nodiscard]] FORCEINLINE bool on_read(char* data, uint32_t size, T&& read_callback, U&& write_callback) + { + return on_read_internal(data, size, ReadCallback::Derived(std::move(read_callback)), WriteCallback::Derived(std::move(write_callback))); + } + + template + [[nodiscard]] FORCEINLINE bool on_write(const uint8_t* data, size_t size, T&& write_callback) + { + return on_write_internal(data, size, WriteCallback::Derived(std::move(write_callback))); + } + + [[nodiscard]] FORCEINLINE bool is_empty() const { return m_ssl.get() == nullptr; } + +private: + typedef Callback ReadCallback; + typedef Callback WriteCallback; + + [[nodiscard]] bool on_read_internal(char* data, uint32_t size, ReadCallback::Base&& read_callback, WriteCallback::Base&& write_callback); + [[nodiscard]] bool on_write_internal(const uint8_t* data, size_t size, WriteCallback::Base&& write_callback); + +private: + bssl::UniquePtr m_ssl; +}; + +} // namespace p2pool diff --git a/tests/src/stratum_dummy.py b/tests/src/stratum_dummy.py index 9471578..3677136 100644 --- a/tests/src/stratum_dummy.py +++ b/tests/src/stratum_dummy.py @@ -17,17 +17,25 @@ import socket import time import sys import json +import ssl f = open('stratum_dummy' + sys.argv[1] + '.log', 'wb') f.write(b'Connecting') f.flush() +context = ssl.create_default_context() +context.check_hostname = False +context.verify_mode = ssl.CERT_NONE + while True: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(1) sock.setblocking(True) + if (sys.argv[1] == '1'): + sock = context.wrap_socket(sock); + if sock.connect_ex(('127.0.0.1', 3333)) == 0: break;