diff --git a/Makefile.am b/Makefile.am index ed4717a2e..692d83864 100644 --- a/Makefile.am +++ b/Makefile.am @@ -102,6 +102,7 @@ src_libbitcoin_network_la_SOURCES = \ src/messages/rpc/model.cpp \ src/net/acceptor.cpp \ src/net/connector.cpp \ + src/net/connector_socks.cpp \ src/net/deadline.cpp \ src/net/hosts.cpp \ src/net/proxy.cpp \ @@ -231,6 +232,7 @@ test_libbitcoin_network_test_SOURCES = \ test/messages/rpc/types.cpp \ test/net/acceptor.cpp \ test/net/connector.cpp \ + test/net/connector_socks.cpp \ test/net/deadline.cpp \ test/net/hosts.cpp \ test/net/proxy.cpp \ @@ -460,6 +462,7 @@ include_bitcoin_network_netdir = ${includedir}/bitcoin/network/net include_bitcoin_network_net_HEADERS = \ include/bitcoin/network/net/acceptor.hpp \ include/bitcoin/network/net/connector.hpp \ + include/bitcoin/network/net/connector_socks.hpp \ include/bitcoin/network/net/deadline.hpp \ include/bitcoin/network/net/hosts.hpp \ include/bitcoin/network/net/net.hpp \ diff --git a/builds/cmake/CMakeLists.txt b/builds/cmake/CMakeLists.txt index 715e2c6fc..f10daccbb 100644 --- a/builds/cmake/CMakeLists.txt +++ b/builds/cmake/CMakeLists.txt @@ -284,6 +284,7 @@ add_library( ${CANONICAL_LIB_NAME} "../../src/messages/rpc/model.cpp" "../../src/net/acceptor.cpp" "../../src/net/connector.cpp" + "../../src/net/connector_socks.cpp" "../../src/net/deadline.cpp" "../../src/net/hosts.cpp" "../../src/net/proxy.cpp" @@ -437,6 +438,7 @@ if (with-tests) "../../test/messages/rpc/types.cpp" "../../test/net/acceptor.cpp" "../../test/net/connector.cpp" + "../../test/net/connector_socks.cpp" "../../test/net/deadline.cpp" "../../test/net/hosts.cpp" "../../test/net/proxy.cpp" diff --git a/builds/msvc/vs2022/libbitcoin-network-test/libbitcoin-network-test.vcxproj b/builds/msvc/vs2022/libbitcoin-network-test/libbitcoin-network-test.vcxproj index c063528af..cb73b5d37 100644 --- a/builds/msvc/vs2022/libbitcoin-network-test/libbitcoin-network-test.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-network-test/libbitcoin-network-test.vcxproj @@ -220,6 +220,7 @@ + diff --git a/builds/msvc/vs2022/libbitcoin-network-test/libbitcoin-network-test.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-network-test/libbitcoin-network-test.vcxproj.filters index 9abeb9195..fa0cda05d 100644 --- a/builds/msvc/vs2022/libbitcoin-network-test/libbitcoin-network-test.vcxproj.filters +++ b/builds/msvc/vs2022/libbitcoin-network-test/libbitcoin-network-test.vcxproj.filters @@ -327,6 +327,9 @@ src\net + + src\net + src\net diff --git a/builds/msvc/vs2022/libbitcoin-network/libbitcoin-network.vcxproj b/builds/msvc/vs2022/libbitcoin-network/libbitcoin-network.vcxproj index 5fc555345..1fe97a12d 100644 --- a/builds/msvc/vs2022/libbitcoin-network/libbitcoin-network.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-network/libbitcoin-network.vcxproj @@ -191,6 +191,7 @@ + @@ -334,6 +335,7 @@ + diff --git a/builds/msvc/vs2022/libbitcoin-network/libbitcoin-network.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-network/libbitcoin-network.vcxproj.filters index 9e7c2d1f3..eb847360e 100644 --- a/builds/msvc/vs2022/libbitcoin-network/libbitcoin-network.vcxproj.filters +++ b/builds/msvc/vs2022/libbitcoin-network/libbitcoin-network.vcxproj.filters @@ -333,6 +333,9 @@ src\net + + src\net + src\net @@ -758,6 +761,9 @@ include\bitcoin\network\net + + include\bitcoin\network\net + include\bitcoin\network\net diff --git a/include/bitcoin/network.hpp b/include/bitcoin/network.hpp index 1759287bf..eabb36d6a 100644 --- a/include/bitcoin/network.hpp +++ b/include/bitcoin/network.hpp @@ -131,6 +131,7 @@ #include #include #include +#include #include #include #include diff --git a/include/bitcoin/network/net/acceptor.hpp b/include/bitcoin/network/net/acceptor.hpp index 6e4eb4cc2..b0427740b 100644 --- a/include/bitcoin/network/net/acceptor.hpp +++ b/include/bitcoin/network/net/acceptor.hpp @@ -78,8 +78,12 @@ class BCT_API acceptor virtual void accept(socket_handler&& handler) NOEXCEPT; protected: + /// Start listening on the endpoint. virtual code start(const asio::endpoint& point) NOEXCEPT; + /// Running in the strand. + bool stranded() const NOEXCEPT; + // These are thread safe. const size_t maximum_; asio::io_context& service_; diff --git a/include/bitcoin/network/net/connector.hpp b/include/bitcoin/network/net/connector.hpp index 69af2533a..fb7c7a939 100644 --- a/include/bitcoin/network/net/connector.hpp +++ b/include/bitcoin/network/net/connector.hpp @@ -25,10 +25,8 @@ #include #include #include -#include #include #include -#include namespace libbitcoin { namespace network { @@ -38,7 +36,7 @@ namespace network { /// All public/protected methods must be called from strand. /// Stop is thread safe and idempotent, may be called multiple times. class BCT_API connector - : public std::enable_shared_from_this, public reporter, + : public enable_shared_from_base, public reporter, protected tracker { public: @@ -82,11 +80,20 @@ class BCT_API connector protected: typedef race_speed racer; + typedef std::shared_ptr finish_ptr; /// Try to connect to host:port, starts timer. virtual void start(const std::string& hostname, uint16_t port, const config::address& host, socket_handler&& handler) NOEXCEPT; + virtual void handle_connected(const code& ec, const finish_ptr& finish, + socket::ptr socket) NOEXCEPT; + virtual void handle_timer(const code& ec, const finish_ptr& finish, + const socket::ptr& socket) NOEXCEPT; + + /// Running in the strand. + bool stranded() NOEXCEPT; + // These are thread safe const size_t maximum_; asio::io_context& service_; @@ -99,16 +106,12 @@ class BCT_API connector racer racer_{}; private: - typedef std::shared_ptr finish_ptr; - void handle_resolve(const boost_code& ec, const asio::endpoints& range, const finish_ptr& finish, const socket::ptr& socket) NOEXCEPT; void do_handle_connect(const code& ec, const finish_ptr& finish, const socket::ptr& socket) NOEXCEPT; - void handle_connect(const code& ec, const finish_ptr& finish, - const socket::ptr& socket) NOEXCEPT; - void handle_timer(const code& ec, const finish_ptr& finish, + void handle_connect(code ec, const finish_ptr& finish, const socket::ptr& socket) NOEXCEPT; }; diff --git a/include/bitcoin/network/net/connector_socks.hpp b/include/bitcoin/network/net/connector_socks.hpp new file mode 100644 index 000000000..52dcb4559 --- /dev/null +++ b/include/bitcoin/network/net/connector_socks.hpp @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2011-2025 libbitcoin developers (see AUTHORS) + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#ifndef LIBBITCOIN_NETWORK_NET_CONNECTOR_SOCKS_HPP +#define LIBBITCOIN_NETWORK_NET_CONNECTOR_SOCKS_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace libbitcoin { +namespace network { + +/// Not thread safe, virtual. +/// Create outbound socket connections via a socks5 proxy. +/// All public/protected methods must be called from strand. +/// Stop is thread safe and idempotent, may be called multiple times. +class BCT_API connector_socks + : public connector, + protected tracker +{ +public: + typedef std::shared_ptr ptr; + + DELETE_COPY_MOVE_DESTRUCT(connector_socks); + + /// Resolves socks5 endpoint and stores address as member for each connect. + connector_socks(const logger& log, asio::strand& strand, + asio::io_context& service, const config::endpoint& socks5_proxy, + const steady_clock::duration& timeout, size_t maximum_request, + std::atomic_bool& suspended) NOEXCEPT; + +protected: + void start(const std::string& hostname, uint16_t port, + const config::address& host, socket_handler&& handler) NOEXCEPT override; + + /// Connector overrides. + void handle_connected(const code& ec, const finish_ptr& finish, + socket::ptr socket) NOEXCEPT override; + void handle_timer(const code& ec, const finish_ptr& finish, + const socket::ptr& socket) NOEXCEPT override; + +private: + template + using data_ptr = std::shared_ptr>; + template + using data_cptr = std::shared_ptr>; + + // socks5 handshake + void do_socks(const code& ec, const socket::ptr& socket) NOEXCEPT; + void handle_socks_greeting_write(const code& ec, size_t size, + const socket::ptr& socket, const data_cptr<3>& greeting) NOEXCEPT; + void handle_socks_method_read(const code& ec, size_t size, + const socket::ptr& socket, const data_ptr<2>& response) NOEXCEPT; + void handle_socks_connect_write(const code& ec, size_t size, + const socket::ptr& socket, const system::chunk_ptr& request) NOEXCEPT; + void handle_socks_response_read(const code& ec, size_t size, + const socket::ptr& socket, const data_ptr<4>& response) NOEXCEPT; + void handle_socks_length_read(const code& ec, size_t size, + const socket::ptr& socket, const data_ptr<1>& host_length) NOEXCEPT; + void handle_socks_address_read(const code& ec, size_t size, + const socket::ptr& socket, const system::chunk_ptr& address) NOEXCEPT; + void do_socks_finish(const code& ec, const socket::ptr& socket) NOEXCEPT; + void socks_finish(const code& ec, const socket::ptr& socket) NOEXCEPT; + + // This is protected by strand. + const config::endpoint socks5_; +}; + +typedef std_vector socks_connectors; +typedef std::shared_ptr socks_connectors_ptr; + +} // namespace network +} // namespace libbitcoin + +#endif diff --git a/include/bitcoin/network/net/net.hpp b/include/bitcoin/network/net/net.hpp index 57a8daf53..831d4be95 100644 --- a/include/bitcoin/network/net/net.hpp +++ b/include/bitcoin/network/net/net.hpp @@ -21,6 +21,7 @@ #include #include +#include #include #include #include diff --git a/include/bitcoin/network/settings.hpp b/include/bitcoin/network/settings.hpp index 6e23b4461..3e5dc5178 100644 --- a/include/bitcoin/network/settings.hpp +++ b/include/bitcoin/network/settings.hpp @@ -31,7 +31,7 @@ namespace libbitcoin { namespace network { /// The largest p2p payload request when configured for witness blocks. -constexpr uint32_t maximum_request_ +constexpr uint32_t maximum_request_default { system::possible_narrow_cast( messages::peer::heading::maximum_payload( @@ -41,6 +41,25 @@ constexpr uint32_t maximum_request_ /// Common network configuration settings, properties not thread safe. struct BCT_API settings { + struct socks5_client + { + DEFAULT_COPY_MOVE_DESTRUCT(socks5_client); + socks5_client() NOEXCEPT; + + /// Proxy credentials are stored and passed in cleartext. + std::string username{}; + std::string password{}; + + /// Socks5 proxy (default port convention is 1080, but not defaulted). + config::endpoint socks{}; + + /// True if socks::port is non-zero. + virtual bool proxied() const NOEXCEPT; + + /// False if both username and password are empty. + virtual bool secured() const NOEXCEPT; + }; + struct tcp_server { DEFAULT_COPY_MOVE_DESTRUCT(tcp_server); @@ -54,8 +73,8 @@ struct BCT_API settings uint16_t connections{ 0 }; uint32_t inactivity_minutes{ 10 }; uint32_t expiration_minutes{ 60 }; - uint32_t maximum_request{ maximum_request_ }; - uint32_t minimum_buffer{ maximum_request_ }; + uint32_t maximum_request{ maximum_request_default }; + uint32_t minimum_buffer{ maximum_request_default }; /// Helpers. virtual bool enabled() const NOEXCEPT; @@ -91,11 +110,30 @@ struct BCT_API settings // TODO: settings unique to the websocket aspect. }; + struct peer_manual + : public tcp_server, public socks5_client + { + // The friends field must be initialized after peers is set. + peer_manual(system::chain::selection) NOEXCEPT + : tcp_server("manual"), socks5_client() + { + } + + config::endpoints peers{}; + config::authorities friends{}; + + /// Helpers. + void initialize() NOEXCEPT; + bool enabled() const NOEXCEPT override; + virtual bool peered( + const messages::peer::address_item& item) const NOEXCEPT; + }; + struct peer_outbound - : public tcp_server + : public tcp_server, public socks5_client { peer_outbound(system::chain::selection context) NOEXCEPT - : tcp_server("outbound") + : tcp_server("outbound"), socks5_client() { connections = 10; @@ -178,25 +216,6 @@ struct BCT_API settings config::authority first_self() const NOEXCEPT; }; - struct peer_manual - : public tcp_server - { - // The friends field must be initialized after peers is set. - peer_manual(system::chain::selection) NOEXCEPT - : tcp_server("manual") - { - } - - config::endpoints peers{}; - config::authorities friends{}; - - /// Helpers. - void initialize() NOEXCEPT; - bool enabled() const NOEXCEPT override; - virtual bool peered( - const messages::peer::address_item& item) const NOEXCEPT; - }; - // [network] // ---------------------------------------------------------------------------- // bitcoin p2p network common settings. diff --git a/src/net/acceptor.cpp b/src/net/acceptor.cpp index 54a06f04a..3156bf208 100644 --- a/src/net/acceptor.cpp +++ b/src/net/acceptor.cpp @@ -109,7 +109,7 @@ code acceptor::start(const asio::endpoint& point) NOEXCEPT void acceptor::stop() NOEXCEPT { - BC_ASSERT_MSG(strand_.running_in_this_thread(), "strand"); + BC_ASSERT(stranded()); // Posts handle_accept to strand (if not already posted). boost_code ignore; @@ -122,16 +122,22 @@ void acceptor::stop() NOEXCEPT config::authority acceptor::local() const NOEXCEPT { - BC_ASSERT_MSG(strand_.running_in_this_thread(), "strand"); + BC_ASSERT(stranded()); return { stopped_ ? asio::endpoint{} : acceptor_.local_endpoint() }; } +// protected +bool acceptor::stranded() const NOEXCEPT +{ + return strand_.running_in_this_thread(); +} + // Methods. // ---------------------------------------------------------------------------- void acceptor::accept(socket_handler&& handler) NOEXCEPT { - BC_ASSERT_MSG(strand_.running_in_this_thread(), "strand"); + BC_ASSERT(stranded()); if (stopped_) { @@ -160,7 +166,7 @@ void acceptor::accept(socket_handler&& handler) NOEXCEPT void acceptor::handle_accept(const code& ec, const socket::ptr& socket, const socket_handler& handler) NOEXCEPT { - BC_ASSERT_MSG(strand_.running_in_this_thread(), "strand"); + BC_ASSERT(stranded()); if (ec) { diff --git a/src/net/connector.cpp b/src/net/connector.cpp index 10c2be207..776560e38 100644 --- a/src/net/connector.cpp +++ b/src/net/connector.cpp @@ -64,7 +64,7 @@ connector::~connector() NOEXCEPT void connector::stop() NOEXCEPT { - BC_ASSERT_MSG(strand_.running_in_this_thread(), "strand"); + BC_ASSERT(stranded()); if (!racer_.running()) return; @@ -74,6 +74,15 @@ void connector::stop() NOEXCEPT timer_->stop(); } +// Properties. +// ---------------------------------------------------------------------------- + +// protected +bool connector::stranded() NOEXCEPT +{ + return strand_.running_in_this_thread(); +} + // Methods. // ---------------------------------------------------------------------------- @@ -93,7 +102,7 @@ void connector::connect(const authority& host, // TODO: this is getting a zero port for seeds (and maybe manual). // TODO: that results in the connection being interpreted as inbound. -// This used by seed and manual (endpoint from config). +// This used by seed, manual, and socks5 (endpoint from config). void connector::connect(const endpoint& host, socket_handler&& handler) NOEXCEPT { @@ -104,7 +113,7 @@ void connector::connect(const endpoint& host, void connector::start(const std::string& hostname, uint16_t port, const config::address& host, socket_handler&& handler) NOEXCEPT { - BC_ASSERT_MSG(strand_.running_in_this_thread(), "strand"); + BC_ASSERT(stranded()); if (racer_.running()) { @@ -142,7 +151,7 @@ void connector::handle_resolve(const boost_code& ec, const asio::endpoints& range, const finish_ptr& finish, const socket::ptr& socket) NOEXCEPT { - BC_ASSERT_MSG(strand_.running_in_this_thread(), "strand"); + BC_ASSERT(stranded()); // Timer stopped the socket, it wins (with timeout/failure). if (socket->stopped()) @@ -179,7 +188,7 @@ void connector::handle_resolve(const boost_code& ec, void connector::do_handle_connect(const code& ec, const finish_ptr& finish, const socket::ptr& socket) NOEXCEPT { - BC_ASSERT_MSG(socket->stranded(), "strand"); + BC_ASSERT(socket->stranded()); boost::asio::post(strand_, std::bind(&connector::handle_connect, @@ -187,46 +196,45 @@ void connector::do_handle_connect(const code& ec, const finish_ptr& finish, } // private -void connector::handle_connect(const code& ec, const finish_ptr& finish, +void connector::handle_connect(code ec, const finish_ptr& finish, const socket::ptr& socket) NOEXCEPT { - BC_ASSERT_MSG(strand_.running_in_this_thread(), "strand"); + BC_ASSERT(stranded()); // Timer stopped the socket, it wins (with timeout/failure). if (socket->stopped()) - { - racer_.finish(error::operation_canceled, nullptr); - return; - } + ec = error::operation_canceled; + else if (suspended_.load()) + ec = error::service_suspended; - if (suspended_.load()) - { - socket->stop(); - timer_->stop(); - racer_.finish(error::service_suspended, nullptr); - return; - } + handle_connected(ec, finish, socket); +} + +// protected/virtual +void connector::handle_connected(const code& ec, const finish_ptr& finish, + socket::ptr socket) NOEXCEPT +{ + BC_ASSERT(stranded()); - // Failure in connect, connector wins (with connect failure). if (ec) { socket->stop(); - timer_->stop(); - racer_.finish(ec, nullptr); - return; + socket.reset(); + } + else + { + *finish = true; } - // Successful connect (error::success), inform and cancel timer. - *finish = true; timer_->stop(); - racer_.finish(error::success, socket); + racer_.finish(ec, socket); } // private void connector::handle_timer(const code& ec, const finish_ptr& finish, const socket::ptr& socket) NOEXCEPT { - BC_ASSERT_MSG(strand_.running_in_this_thread(), "strand"); + BC_ASSERT(stranded()); // Successful connect, connector wins (error::success). if (*finish) diff --git a/src/net/connector_socks.cpp b/src/net/connector_socks.cpp new file mode 100644 index 000000000..285f15878 --- /dev/null +++ b/src/net/connector_socks.cpp @@ -0,0 +1,413 @@ +/** + * Copyright (c) 2011-2025 libbitcoin developers (see AUTHORS) + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace libbitcoin { +namespace network { + +// Shared pointers required in handler parameters so closures control lifetime. +BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR) +BC_PUSH_WARNING(SMART_PTR_NOT_NEEDED) +BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) +BC_PUSH_WARNING(NO_ARRAY_INDEXING) + +using namespace system; +using namespace std::placeholders; + +connector_socks::connector_socks(const logger& log, asio::strand& strand, + asio::io_context& service, const config::endpoint& socks5_proxy, + const steady_clock::duration& timeout, size_t maximum_request, + std::atomic_bool& suspended) NOEXCEPT + : connector(log, strand, service, timeout, maximum_request, suspended), + socks5_(socks5_proxy), + tracker(log) +{ +} + +// protected/override +void connector_socks::start(const std::string&, uint16_t, + const config::address& host, socket_handler&& handler) NOEXCEPT +{ + // hostname and port are redundant with host for outgoing connections. + connector::start(socks5_.host(), socks5_.port(), host, std::move(handler)); +} + +// protected/override +void connector_socks::handle_connected(const code& ec, const finish_ptr&, + socket::ptr socket) NOEXCEPT +{ + BC_ASSERT(stranded()); + + // Perform socks5 handshake on the socket strand. + do_socks(ec, socket); +} + +// protected/override +void connector_socks::handle_timer(const code& ec, const finish_ptr&, + const socket::ptr& socket) NOEXCEPT +{ + BC_ASSERT(stranded()); + + // Connector strand cannot access the socket at this point. + // Post to the socks connector strand to protect the socket. + boost::asio::post(socket->strand(), + std::bind(&connector_socks::do_socks_finish, + shared_from_base(), ec, socket)); +} + +// socks5 handshake (private) +// ---------------------------------------------------------------------------- +// datatracker.ietf.org/doc/html/rfc1928 + +enum socks : uint8_t +{ + // flags + version = 0x05, + connect = 0x01, + reserved = 0x00, + + // reply type + success = 0x00, + failure = 0x01, + disallowed = 0x02, + net_unreachable = 0x03, + host_unreachable = 0x04, + connection_refused = 0x05, + connection_expired = 0x06, + unsupported_command = 0x07, + unsupported_address = 0x08, + + // method (authentication) type + method_none = 0xff, + method_clear = 0x00, + method_gssapi = 0x01, + method_password = 0x02, + + // command type + command_connect = 0x01, + command_bind = 0x02, + command_udp = 0x03, + + // address type + address_ipv4 = 0x01, + address_fqdn = 0x03, + address_ipv6 = 0x04 +}; + +void connector_socks::do_socks(const code& ec, + const socket::ptr& socket) NOEXCEPT +{ + BC_ASSERT(stranded()); + + if (const auto result = (socket->stopped() ? error::channel_stopped : ec)) + { + socks_finish(result, socket); + return; + } + + // +----+----------+----------+ + // |VER | NMETHODS | METHODS | + // +----+----------+----------+ + // | 1 | 1 | 1 to 255 | + // +----+----------+----------+ + const auto greeting = to_shared>( + { + socks::version, + 1_u8, + socks::method_clear + }); + + // Start of socket strand sequence. + socket->write({ greeting->data(), greeting->size() }, + std::bind(&connector_socks::handle_socks_greeting_write, + shared_from_base(), + _1, _2, socket, greeting)); +} + +void connector_socks::handle_socks_greeting_write(const code& ec, size_t size, + const socket::ptr& socket, const data_cptr<3>& greeting) NOEXCEPT +{ + BC_ASSERT(socket->stranded()); + + if (const auto result = (socket->stopped() ? error::channel_stopped : ec)) + { + do_socks_finish(result, socket); + return; + } + + if (size != sizeof(*greeting)) + { + do_socks_finish(error::connect_failed, socket); + return; + } + + const auto response = emplace_shared>(); + + socket->read({ response->data(), response->size() }, + std::bind(&connector_socks::handle_socks_method_read, + shared_from_base(), + _1, _2, socket, response)); +} + +void connector_socks::handle_socks_method_read(const code& ec, size_t size, + const socket::ptr& socket, const data_ptr<2>& response) NOEXCEPT +{ + BC_ASSERT(socket->stranded()); + + if (const auto result = (socket->stopped() ? error::channel_stopped : ec)) + { + do_socks_finish(result, socket); + return; + } + + const auto& in = *response; + + // +----+--------+ + // |VER | METHOD | + // +----+--------+ + // | 1 | 1 | + // +----+--------+ + if (size != sizeof(*response) || + in[0] != socks::version || + in[1] != socks::method_clear) + { + do_socks_finish(error::connect_failed, socket); + return; + } + + // +----+-----+-------+------+----------+----------+ + // |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | + // +----+-----+-------+------+----------+----------+ + // | 1 | 1 | X'00' | 1 | Variable | 2 | + // +----+-----+-------+------+----------+----------+ + const auto host = socket->address().to_host(); + const auto port = to_big_endian(socket->address().port()); + const auto length = 5u + host.length() + sizeof(uint16_t); + const auto request = emplace_shared(length); + auto& out = *request; + out[0] = socks::version; + out[1] = socks::command_connect; + out[2] = socks::reserved; + out[3] = socks::address_fqdn; + out[4] = narrow_cast(host.size()); + auto it = std::next(request->begin(), 5u); + it = std::copy(host.begin(), host.end(), it); + it = std::copy(port.begin(), port.end(), it); + + socket->write({ request->data(), request->size() }, + std::bind(&connector_socks::handle_socks_connect_write, + shared_from_base(), + _1, _2, socket, request)); +} + +void connector_socks::handle_socks_connect_write(const code& ec, size_t size, + const socket::ptr& socket, const system::chunk_ptr& request) NOEXCEPT +{ + BC_ASSERT(socket->stranded()); + + if (const auto result = (socket->stopped() ? error::channel_stopped : ec)) + { + do_socks_finish(result, socket); + return; + } + + if (size != request->size()) + { + do_socks_finish(error::connect_failed, socket); + return; + } + + const auto response = emplace_shared>(); + + socket->read({ response->data(), response->size() }, + std::bind(&connector_socks::handle_socks_response_read, + shared_from_base(), + _1, _2, socket, response)); +} + +void connector_socks::handle_socks_response_read(const code& ec, size_t size, + const socket::ptr& socket, const data_ptr<4>& response) NOEXCEPT +{ + BC_ASSERT(socket->stranded()); + + if (const auto result = (socket->stopped() ? error::channel_stopped : ec)) + { + do_socks_finish(result, socket); + return; + } + + const auto& in = *response; + + // +----+-----+-------+------+ + // |VER | REP | RSV | ATYP | + // +----+-----+-------+------+ + // | 1 | 1 | X'00' | 1 | + // +----+-----+-------+------+ + if (size != sizeof(*response) || + in[0] != socks::version || + in[1] != socks::success || + in[2] != socks::reserved) + { + do_socks_finish(error::connect_failed, socket); + return; + } + + switch (in[3]) + { + case socks::address_ipv4: + { + // A version-4 IP address with length of 4 octets. + const auto address = emplace_shared(4_size); + + socket->read({ address.get(), sizeof(uint8_t) }, + std::bind(&connector_socks::handle_socks_address_read, + shared_from_base(), + _1, _2, socket, address)); + return; + } + case socks::address_ipv6: + { + // A version-6 IP address with length of 16 octets. + const auto address = emplace_shared(16_size); + + socket->read({ address->data(), address->size() }, + std::bind(&connector_socks::handle_socks_address_read, + shared_from_base(), + _1, _2, socket, address)); + return; + } + case socks::address_fqdn: + { + // The address field contains a fully-qualified domain name. The + // first octet of the address field contains the number of octets + // of name that follow (and excludes two byte length of the port). + const auto length = emplace_shared>(); + + socket->read({ length.get(), sizeof(uint8_t) }, + std::bind(&connector_socks::handle_socks_length_read, + shared_from_base(), + _1, _2, socket, length)); + return; + } + default: + { + do_socks_finish(error::operation_failed, socket); + return; + } + } +} + +void connector_socks::handle_socks_length_read(const code& ec, size_t size, + const socket::ptr& socket, const data_ptr<1>& host_length) NOEXCEPT +{ + BC_ASSERT(socket->stranded()); + + if (const auto result = (socket->stopped() ? error::channel_stopped : ec)) + { + do_socks_finish(result, socket); + return; + } + + if (size != sizeof(*host_length)) + { + do_socks_finish(error::connect_failed, socket); + return; + } + + const auto bytes = host_length->front() + sizeof(uint16_t); + const auto address = emplace_shared(bytes); + + socket->read({ address->data(), address->size() }, + std::bind(&connector_socks::handle_socks_address_read, + shared_from_base(), + _1, _2, socket, address)); +} + +void connector_socks::handle_socks_address_read(const code& ec, size_t size, + const socket::ptr& socket, const chunk_ptr& address) NOEXCEPT +{ + BC_ASSERT(socket->stranded()); + + if (const auto result = (socket->stopped() ? error::channel_stopped : ec)) + { + do_socks_finish(result, socket); + return; + } + + // +----------+----------+ + // | BND.ADDR | BND.PORT | + // +----------+----------+ + // | Variable | 2 | + // +----------+----------+ + if (size != address->size()) + { + do_socks_finish(error::connect_failed, socket); + return; + } + + // address:port isn't used (could convert to config::address, add setter), + // but the outbound address_/authority_ members are set by connect(). + ////socket->set_address(address); + + do_socks_finish(error::success, socket); +} + +void connector_socks::do_socks_finish(const code& ec, + const socket::ptr& socket) NOEXCEPT +{ + BC_ASSERT(socket->stranded()); + + // Either socks operation error or operation_canceled from connector timer. + // The socket is stopped here in case this is invoked by the timer (race). + // That causes any above asynchronous operation to cancel and post handler. + if (ec) + socket->stop(); + + // End of socket strand sequence. + boost::asio::post(strand_, + std::bind(&connector_socks::socks_finish, + shared_from_base(), ec, socket)); +} + +void connector_socks::socks_finish(const code& ec, + const socket::ptr& socket) NOEXCEPT +{ + BC_ASSERT(stranded()); + + // Stops the timer in all cases, and stops/resets the socket if ec set. + connector::handle_connected(ec, move_shared(false), socket); +} + +BC_POP_WARNING() +BC_POP_WARNING() +BC_POP_WARNING() +BC_POP_WARNING() + +} // namespace network +} // namespace libbitcoin diff --git a/src/settings.cpp b/src/settings.cpp index 8cb10d4ff..ac138b1ae 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -29,6 +29,23 @@ namespace network { using namespace system; using namespace messages::peer; +// socks5_client +// ---------------------------------------------------------------------------- + +settings::socks5_client::socks5_client() NOEXCEPT +{ +} + +bool settings::socks5_client::proxied() const NOEXCEPT +{ + return is_nonzero(socks.port()); +} + +bool settings::socks5_client::secured() const NOEXCEPT +{ + return !(username.empty() && password.empty()); +} + // tcp_server // ---------------------------------------------------------------------------- diff --git a/test/net/connector_socks.cpp b/test/net/connector_socks.cpp new file mode 100644 index 000000000..67bba549f --- /dev/null +++ b/test/net/connector_socks.cpp @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2011-2025 libbitcoin developers (see AUTHORS) + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#include "../test.hpp" + +BOOST_AUTO_TEST_SUITE(connector_socks_tests) + +BOOST_AUTO_TEST_CASE(connector_socks_test) +{ + BOOST_REQUIRE(true); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/settings.cpp b/test/settings.cpp index 7efcb2a81..5dbd056bf 100644 --- a/test/settings.cpp +++ b/test/settings.cpp @@ -307,6 +307,17 @@ BOOST_AUTO_TEST_CASE(settings__excluded__default__true) // ---------------------------------------------------------------------------- constexpr auto maximum_request = system::chain::max_block_weight; +BOOST_AUTO_TEST_CASE(settings__socks5_client__defaults__expected) +{ + const settings::socks5_client instance{}; + + BOOST_REQUIRE(!instance.secured()); + BOOST_REQUIRE(instance.username.empty()); + BOOST_REQUIRE(instance.password.empty()); + BOOST_REQUIRE(!instance.proxied()); + BOOST_REQUIRE(instance.socks == config::endpoint{}); +} + BOOST_AUTO_TEST_CASE(settings__tcp_server__defaults__expected) { constexpr auto name = "test"; @@ -383,6 +394,13 @@ BOOST_AUTO_TEST_CASE(settings__peer_outbound__mainnet__expected) { const settings::peer_outbound instance{ system::chain::selection::mainnet }; + // socks5_client + BOOST_REQUIRE(!instance.secured()); + BOOST_REQUIRE(instance.username.empty()); + BOOST_REQUIRE(instance.password.empty()); + BOOST_REQUIRE(!instance.proxied()); + BOOST_REQUIRE(instance.socks == config::endpoint{}); + // tcp_server BOOST_REQUIRE_EQUAL(instance.name, "outbound"); BOOST_REQUIRE(!instance.secure); @@ -651,6 +669,13 @@ BOOST_AUTO_TEST_CASE(settings__peer_manual__mainnet__expected) { const settings::peer_manual instance{ system::chain::selection::mainnet }; + // socks5_client + BOOST_REQUIRE(!instance.secured()); + BOOST_REQUIRE(instance.username.empty()); + BOOST_REQUIRE(instance.password.empty()); + BOOST_REQUIRE(!instance.proxied()); + BOOST_REQUIRE(instance.socks == config::endpoint{}); + // tcp_server BOOST_REQUIRE_EQUAL(instance.name, "manual"); BOOST_REQUIRE(!instance.secure);