diff --git a/docs/COMMAND_LINE.MD b/docs/COMMAND_LINE.MD index a8bf72d..d0694d1 100644 --- a/docs/COMMAND_LINE.MD +++ b/docs/COMMAND_LINE.MD @@ -41,6 +41,8 @@ --tls-cert-key file Load TLS certificate private key from "file" in the PEM format --rpc-ssl Enable SSL on RPC connections to the Monero node --rpc-ssl-fingerprint base64-encoded fingerprint of the Monero node's certificate (optional, use it for certificate pinning) +--stratum-proxy-protocol Enable HAProxy PROXY protocol v2 for incoming Stratum connections (use with HAProxy send-proxy-v2) +--p2p-proxy-protocol Enable HAProxy PROXY protocol v2 for incoming P2P connections (use with HAProxy send-proxy-v2) --no-stratum-http Disable HTTP on Stratum ports --full-validation Enables full share validation / increases CPU usage --onion-address Tell other peers to use this .onion address to connect to this node through TOR diff --git a/src/main.cpp b/src/main.cpp index 2e1f6e5..fa34c56 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -105,6 +105,8 @@ void p2pool_usage() "--rpc-ssl Enable SSL on RPC connections to the Monero node\n" "--rpc-ssl-fingerprint base64-encoded fingerprint of the Monero node's certificate (optional, use it for certificate pinning)\n" #endif + "--stratum-proxy-protocol Enable HAProxy PROXY protocol v2 for incoming Stratum connections (use with HAProxy send-proxy-v2)\n" + "--p2p-proxy-protocol Enable HAProxy PROXY protocol v2 for incoming P2P connections (use with HAProxy send-proxy-v2)\n" "--no-stratum-http Disable HTTP on Stratum ports\n" "--full-validation Enables full share validation / increases CPU usage\n" "--onion-address Tell other peers to use this .onion address to connect to this node through TOR\n" diff --git a/src/p2p_server.cpp b/src/p2p_server.cpp index 1f1934c..22d56a1 100644 --- a/src/p2p_server.cpp +++ b/src/p2p_server.cpp @@ -64,7 +64,7 @@ static constexpr hash seed_onion_nodes[] = { }; P2PServer::P2PServer(p2pool* pool) - : TCPServer(DEFAULT_BACKLOG, P2PClient::allocate, pool->params().m_socks5Proxy, pool->params().m_socks5ProxyType) + : TCPServer(DEFAULT_BACKLOG, P2PClient::allocate, pool->params().m_socks5Proxy, pool->params().m_socks5ProxyType, pool->params().m_p2pProxyProtocol) , m_pool(pool) , m_cache(pool->params().m_blockCache ? new BlockCache(pool->params()) : nullptr) , m_cacheLoaded(false) diff --git a/src/params.cpp b/src/params.cpp index 5a320bc..17871b1 100644 --- a/src/params.cpp +++ b/src/params.cpp @@ -317,6 +317,16 @@ Params::Params(const std::vector>& args) ok = true; } + if (arg[0] == "stratum-proxy-protocol") { + m_stratumProxyProtocol = true; + ok = true; + } + + if (arg[0] == "p2p-proxy-protocol") { + m_p2pProxyProtocol = true; + ok = true; + } + if (!ok) { fprintf(stderr, "Unknown or invalid command line parameter \"%s\"\n\n", arg[0].c_str()); p2pool_usage(); diff --git a/src/params.h b/src/params.h index a50d09f..87079be 100644 --- a/src/params.h +++ b/src/params.h @@ -149,6 +149,8 @@ struct Params std::string m_onionAddress; hash m_onionPubkey; bool m_noClearnetP2P = false; + bool m_stratumProxyProtocol = false; + bool m_p2pProxyProtocol = false; }; } // namespace p2pool diff --git a/src/stratum_server.cpp b/src/stratum_server.cpp index 2400ab9..c8414d1 100644 --- a/src/stratum_server.cpp +++ b/src/stratum_server.cpp @@ -44,7 +44,7 @@ static constexpr int32_t BAN_THRESHOLD_POINTS = -15; namespace p2pool { StratumServer::StratumServer(p2pool* pool) - : TCPServer(DEFAULT_BACKLOG, StratumClient::allocate, std::string(), Params::ProxyType::INVALID) + : TCPServer(DEFAULT_BACKLOG, StratumClient::allocate, std::string(), Params::ProxyType::INVALID, pool->params().m_stratumProxyProtocol) , m_pool(pool) , m_autoDiff(pool->params().m_autoDiff) , m_enableFullValidation(pool->params().m_enableFullValidation) diff --git a/src/tcp_server.cpp b/src/tcp_server.cpp index 263c5c3..1fdeb79 100644 --- a/src/tcp_server.cpp +++ b/src/tcp_server.cpp @@ -23,7 +23,7 @@ static thread_local const char* log_category_prefix = "TCPServer "; namespace p2pool { -TCPServer::TCPServer(int default_backlog, allocate_client_callback allocate_new_client, const std::string& socks5Proxy, Params::ProxyType socks5ProxyType) +TCPServer::TCPServer(int default_backlog, allocate_client_callback allocate_new_client, const std::string& socks5Proxy, Params::ProxyType socks5ProxyType, bool proxyProtocol) : m_allocateNewClient(allocate_new_client) , m_defaultBacklog(default_backlog) , m_loopThread{} @@ -36,6 +36,7 @@ TCPServer::TCPServer(int default_backlog, allocate_client_callback allocate_new_ , m_socks5ProxyV6(false) , m_socks5ProxyIP{} , m_socks5ProxyPort(-1) + , m_proxyProtocol(proxyProtocol) , m_finished(0) , m_listenPort(-1) , m_loop{} @@ -892,45 +893,51 @@ void TCPServer::on_new_client(uv_stream_t* server, Client* client) LOGINFO(5, "new connection " << (client->m_isIncoming ? "from " : "to ") << log::Gray() << static_cast(client->m_addrString)); - if (is_banned(client->isV6(), client->m_addr)) { - LOGINFO(5, "peer " << log::Gray() << static_cast(client->m_addrString) << log::NoColor() << " is banned, disconnecting"); - client->close(); - return; + if (m_proxyProtocol && client->m_isIncoming) { + // Defer ban check and on_connect until real IP is extracted from PROXY protocol header + client->m_proxyProtocolState = Client::ProxyProtocolState::ExpectingHeader; } - - TCPServer* owner = client->m_owner; - - if (owner->m_finished.load()) { - client->close(); - return; - } - - if (client->m_isIncoming || owner->m_socks5Proxy.empty()) { - if (!client->on_connect()) { + else { + if (is_banned(client->isV6(), client->m_addr)) { + LOGINFO(5, "peer " << log::Gray() << static_cast(client->m_addrString) << log::NoColor() << " is banned, disconnecting"); client->close(); return; } - } - else { - const bool result = owner->send(client, - [](uint8_t* buf, size_t buf_size) -> size_t - { - if (buf_size < 3) { - return 0; - } - buf[0] = 5; // Protocol version (SOCKS5) - buf[1] = 1; // NMETHODS - buf[2] = 0; // Method 0 (no authentication) + TCPServer* owner = client->m_owner; - return 3; - }); + if (owner->m_finished.load()) { + client->close(); + return; + } - if (result) { - client->m_socks5ProxyState = Client::Socks5ProxyState::MethodSelectionSent; + if (client->m_isIncoming || owner->m_socks5Proxy.empty()) { + if (!client->on_connect()) { + client->close(); + return; + } } else { - client->close(); + const bool result = owner->send(client, + [](uint8_t* buf, size_t buf_size) -> size_t + { + if (buf_size < 3) { + return 0; + } + + buf[0] = 5; // Protocol version (SOCKS5) + buf[1] = 1; // NMETHODS + buf[2] = 0; // Method 0 (no authentication) + + return 3; + }); + + if (result) { + client->m_socks5ProxyState = Client::Socks5ProxyState::MethodSelectionSent; + } + else { + client->close(); + } } } @@ -1085,6 +1092,7 @@ TCPServer::Client::Client(char* read_buf, size_t size) , m_port(0) , m_addrString{} , m_socks5ProxyState(Socks5ProxyState::Default) + , m_proxyProtocolState(ProxyProtocolState::None) , m_resetCounter{ 0 } #ifdef WITH_TLS , m_tlsChecked(false) @@ -1111,6 +1119,7 @@ void TCPServer::Client::reset() m_port = -1; m_addrString[0] = '\0'; m_socks5ProxyState = Socks5ProxyState::Default; + m_proxyProtocolState = ProxyProtocolState::None; m_readBuf[0] = '\0'; m_readBuf[m_readBufSize - 1] = '\0'; @@ -1155,7 +1164,12 @@ void TCPServer::Client::on_read(uv_stream_t* stream, ssize_t nread, const uv_buf if (nread > 0) { if (client->m_owner && !client->m_owner->m_finished.load()) { - if (client->m_socks5ProxyState == Socks5ProxyState::Default) { + if (client->m_proxyProtocolState == ProxyProtocolState::ExpectingHeader) { + if (!client->on_proxy_protocol(buf->base, static_cast(nread))) { + client->close(); + } + } + else if (client->m_socks5ProxyState == Socks5ProxyState::Default) { if (!client->on_read(buf->base, static_cast(nread))) { client->close(); } @@ -1315,6 +1329,134 @@ bool TCPServer::Client::on_proxy_handshake(const char* data, uint32_t size) return true; } +bool TCPServer::Client::on_proxy_protocol(const char* data, uint32_t size) +{ + // PROXY protocol v2 signature + static constexpr uint8_t ppv2_signature[12] = { 0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A }; + + if ((data != m_readBuf + m_numRead) || (data + size > m_readBuf + m_readBufSize)) { + LOGERR(1, "peer " << static_cast(m_addrString) << " invalid data pointer or size in on_proxy_protocol()"); + return false; + } + m_numRead += size; + + // Need at least 16 bytes for the fixed header + if (m_numRead < 16) { + return true; + } + + const uint8_t* p = reinterpret_cast(m_readBuf); + + // Validate 12-byte signature + if (memcmp(p, ppv2_signature, 12) != 0) { + LOGWARN(4, "peer " << static_cast(m_addrString) << " invalid PROXY protocol v2 signature"); + return false; + } + + // Byte 12: version (high nibble) and command (low nibble) + const uint8_t ver_cmd = p[12]; + const uint8_t version = (ver_cmd >> 4) & 0x0F; + const uint8_t command = ver_cmd & 0x0F; + + if (version != 2) { + LOGWARN(4, "peer " << static_cast(m_addrString) << " unsupported PROXY protocol version " << version); + return false; + } + + if (command > 1) { + LOGWARN(4, "peer " << static_cast(m_addrString) << " invalid PROXY protocol command " << command); + return false; + } + + // Byte 13: address family (high nibble) and protocol (low nibble) + const uint8_t family_proto = p[13]; + + // Bytes 14-15: address data length (big-endian), includes address block + optional TLV extensions + const uint16_t addr_len = (static_cast(p[14]) << 8) | static_cast(p[15]); + + const uint32_t total_header_len = 16 + static_cast(addr_len); + + if (total_header_len > m_readBufSize) { + LOGWARN(4, "peer " << static_cast(m_addrString) << " PROXY protocol v2 header too large (" << total_header_len << " bytes)"); + return false; + } + + // Wait for complete header + if (m_numRead < total_header_len) { + return true; + } + + // PROXY command: extract real client address + if (command == 1) { + const uint8_t family = (family_proto >> 4) & 0x0F; + + if (family == 1) { + // AF_INET: src(4) + dst(4) + sport(2) + dport(2) = 12 bytes minimum, may be larger with TLV extensions + if (addr_len < 12) { + LOGWARN(4, "peer " << static_cast(m_addrString) << " PROXY protocol v2 IPv4 address data too short (" << addr_len << " bytes)"); + return false; + } + + memcpy(m_addr.data, raw_ip::ipv4_prefix, sizeof(raw_ip::ipv4_prefix)); + memcpy(m_addr.data + sizeof(raw_ip::ipv4_prefix), p + 16, 4); + m_port = (static_cast(p[24]) << 8) | static_cast(p[25]); + m_addressType = AddressType::IPv4; + } + else if (family == 2) { + // AF_INET6: src(16) + dst(16) + sport(2) + dport(2) = 36 bytes minimum, may be larger with TLV extensions + if (addr_len < 36) { + LOGWARN(4, "peer " << static_cast(m_addrString) << " PROXY protocol v2 IPv6 address data too short (" << addr_len << " bytes)"); + return false; + } + + memcpy(m_addr.data, p + 16, 16); + m_port = (static_cast(p[48]) << 8) | static_cast(p[49]); + m_addressType = AddressType::IPv6; + } + else { + LOGWARN(4, "peer " << static_cast(m_addrString) << " PROXY protocol v2 unsupported address family " << family); + return false; + } + + init_addr_string(); + LOGINFO(5, "PROXY protocol v2: real address is " << log::Gray() << static_cast(m_addrString)); + } + // LOCAL command (health checks): keep original address + + // Move remaining data to beginning of buffer + m_numRead -= total_header_len; + if (m_numRead > 0) { + memmove(m_readBuf, m_readBuf + total_header_len, m_numRead); + } + + m_proxyProtocolState = ProxyProtocolState::HeaderReceived; + + // Deferred ban check with real IP + if (m_owner->is_banned(isV6(), m_addr)) { + LOGINFO(5, "peer " << log::Gray() << static_cast(m_addrString) << log::NoColor() << " is banned, disconnecting"); + return false; + } + + if (m_owner->m_finished.load()) { + return false; + } + + if (!on_connect()) { + return false; + } + + // Pass remaining data to the protocol handler + if (m_numRead > 0) { + const uint32_t nread = m_numRead; + m_numRead = 0; + if (!on_read(m_readBuf, nread)) { + return false; + } + } + + return true; +} + void TCPServer::Client::on_write(uv_write_t* req, int status) { WriteBuf* buf = static_cast(req->data); diff --git a/src/tcp_server.h b/src/tcp_server.h index b5ac073..47fcaa1 100644 --- a/src/tcp_server.h +++ b/src/tcp_server.h @@ -34,7 +34,7 @@ public: struct Client; typedef Client* (*allocate_client_callback)(); - TCPServer(int default_backlog, allocate_client_callback allocate_new_client, const std::string& socks5Proxy, Params::ProxyType socks5ProxyType); + TCPServer(int default_backlog, allocate_client_callback allocate_new_client, const std::string& socks5Proxy, Params::ProxyType socks5ProxyType, bool proxyProtocol = false); virtual ~TCPServer(); [[nodiscard]] bool connect_to_peer(bool is_v6, const char* ip, int port); @@ -67,6 +67,7 @@ public: [[nodiscard]] virtual bool on_connect() = 0; [[nodiscard]] virtual bool on_read(const char* data, uint32_t size) = 0; [[nodiscard]] bool on_proxy_handshake(const char* data, uint32_t size); + [[nodiscard]] bool on_proxy_protocol(const char* data, uint32_t size); virtual void on_connect_failed(int /*err*/) {} virtual void on_read_failed(int /*err*/) {} virtual void on_disconnected() {} @@ -121,6 +122,12 @@ public: ConnectRequestSent, } m_socks5ProxyState; + enum class ProxyProtocolState : uint8_t { + None, + ExpectingHeader, + HeaderReceived, + } m_proxyProtocolState; + std::atomic m_resetCounter; #ifdef WITH_TLS @@ -197,6 +204,8 @@ protected: raw_ip m_socks5ProxyIP; int m_socks5ProxyPort; + bool m_proxyProtocol; + std::atomic m_finished; int m_listenPort;