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);