aboutsummaryrefslogtreecommitdiff
path: root/src/net
diff options
context:
space:
mode:
authortobtoht <tob@featherwallet.org>2026-04-20 07:49:04 +0000
committertobtoht <tob@featherwallet.org>2026-04-20 07:49:04 +0000
commit8b6be66bef8cce6e074074b7329be9509b93dbb8 (patch)
tree6dee62eec590447f49e6a29d649c294f4757a2a4 /src/net
parente75125a30b1f70e74eded99d3fc4f3e98938c6d5 (diff)
parentce5437f0677966c15d2946bc7c068975aa6bfd83 (diff)
downloadmonzero-core-8b6be66bef8cce6e074074b7329be9509b93dbb8.tar.gz
monzero-core-8b6be66bef8cce6e074074b7329be9509b93dbb8.tar.xz
monzero-core-8b6be66bef8cce6e074074b7329be9509b93dbb8.zip
Merge pull request #10411
ce5437f Add Socks v5 support to daemon and wallet (Lee *!* Clagett)
Diffstat (limited to 'src/net')
-rw-r--r--src/net/error.cpp6
-rw-r--r--src/net/error.h3
-rw-r--r--src/net/fwd.h6
-rw-r--r--src/net/http.cpp8
-rw-r--r--src/net/parse.cpp135
-rw-r--r--src/net/parse.h79
-rw-r--r--src/net/socks.cpp515
-rw-r--r--src/net/socks.h62
-rw-r--r--src/net/socks_connect.cpp26
-rw-r--r--src/net/socks_connect.h5
10 files changed, 792 insertions, 53 deletions
diff --git a/src/net/error.cpp b/src/net/error.cpp
index 254db7ae1..621a0b1d3 100644
--- a/src/net/error.cpp
+++ b/src/net/error.cpp
@@ -54,6 +54,8 @@ namespace
return "Failed to retrieve desired DNS record";
case net::error::expected_tld:
return "Expected top-level domain";
+ case net::error::invalid_encoding:
+ return "Invalid encoding";
case net::error::invalid_host:
return "Host value is not valid";
case net::error::invalid_i2p_address:
@@ -62,8 +64,12 @@ namespace
return "CIDR netmask outside of 0-32 range";
case net::error::invalid_port:
return "Invalid port value (expected 0-65535)";
+ case net::error::invalid_scheme:
+ return "Invalid/unsupported scheme was provided";
case net::error::invalid_tor_address:
return "Invalid Tor address";
+ case net::error::unexpected_userinfo:
+ return "User or pass was provided unexpectedly";
case net::error::unsupported_address:
return "Network address not supported";
default:
diff --git a/src/net/error.h b/src/net/error.h
index 969eefc41..27b376546 100644
--- a/src/net/error.h
+++ b/src/net/error.h
@@ -41,11 +41,14 @@ namespace net
bogus_dnssec = 1, //!< Invalid response signature from DNSSEC enabled domain
dns_query_failure, //!< Failed to retrieve desired DNS record
expected_tld, //!< Expected a tld
+ invalid_encoding, //!< Invalid percent encoding
invalid_host, //!< Hostname is not valid
invalid_i2p_address,
invalid_mask, //!< Outside of 0-32 range
invalid_port, //!< Outside of 0-65535 range
+ invalid_scheme, //!< Provided URI scheme was unspported
invalid_tor_address,//!< Invalid base32 or length
+ unexpected_userinfo,//!< User or pass was provided unexpectedly
unsupported_address,//!< Type not supported by `get_network_address`
};
diff --git a/src/net/fwd.h b/src/net/fwd.h
index b105da115..ffae07cc9 100644
--- a/src/net/fwd.h
+++ b/src/net/fwd.h
@@ -34,13 +34,19 @@
namespace net
{
enum class error : int;
+ struct scheme_and_authority;
class tor_address;
+ struct uri_components;
+ struct user_and_pass;
+ struct userinfo_and_hostport;
class i2p_address;
namespace socks
{
class client;
template<typename> class connect_handler;
+ struct connector;
+ struct endpoint;
enum class error : int;
enum class version : std::uint8_t;
}
diff --git a/src/net/http.cpp b/src/net/http.cpp
index c0ed3d430..ed7d5a889 100644
--- a/src/net/http.cpp
+++ b/src/net/http.cpp
@@ -45,15 +45,17 @@ bool client::set_proxy(const std::string &address)
}
else
{
- const auto endpoint = get_tcp_endpoint(address);
+ auto endpoint = socks::endpoint::get(address);
if (!endpoint)
{
- auto always_fail = net::socks::connector{boost::asio::ip::tcp::endpoint()};
+ auto always_fail = net::socks::connector{};
set_connector(always_fail);
}
else
{
- set_connector(net::socks::connector{*endpoint});
+ set_connector(
+ net::socks::connector{std::make_shared<socks::endpoint>(std::move(*endpoint))}
+ );
}
}
diff --git a/src/net/parse.cpp b/src/net/parse.cpp
index f989d7de4..27fd5db43 100644
--- a/src/net/parse.cpp
+++ b/src/net/parse.cpp
@@ -29,6 +29,9 @@
#include "parse.h"
+#include <type_traits>
+#include "hex.h"
+#include "net/socks.h"
#include "net/tor_address.h"
#include "net/i2p_address.h"
#include "string_tools.h"
@@ -36,6 +39,95 @@
namespace net
{
+ namespace
+ {
+ bool percent_decoding(std::string& out)
+ {
+ auto pos = out.find('%');
+ while (pos != std::string::npos)
+ {
+ if (out.size() - pos < 3)
+ return false;
+ if (!epee::from_hex::to_buffer(epee::as_mut_byte_span(out[pos]), {out.data() + pos + 1, 2}))
+ return false;
+ out.erase(pos + 1, 2);
+ pos = out.find('%', pos + 1);
+ }
+
+ return true;
+ }
+ } // anonymous
+
+ scheme_and_authority::scheme_and_authority(boost::string_ref uri)
+ : scheme(), authority()
+ {
+ static_assert(std::is_same<std::string::size_type, boost::string_ref::size_type>());
+
+ // Stop at scheme end or path begin. URN not supported
+ const auto split = uri.find_first_of(":/");
+ if (split != boost::string_ref::npos && uri.substr(split).starts_with("://"))
+ {
+ scheme.assign(uri.data(), split);
+ uri = uri.substr(split + 3);
+ }
+
+ uri = uri.substr(0, uri.find('/'));
+ authority.assign(uri.data(), uri.size());
+ }
+
+ userinfo_and_hostport::userinfo_and_hostport(boost::string_ref authority)
+ : userinfo(), hostport()
+ {
+ static_assert(std::is_same<std::string::size_type, boost::string_ref::size_type>());
+
+ const auto split = authority.find('@');
+ if (split != boost::string_ref::npos)
+ {
+ userinfo.assign(authority.data(), split);
+ authority = authority.substr(split + 1);
+ }
+
+ hostport.assign(authority.data(), authority.size());
+ }
+
+ boost::optional<user_and_pass> user_and_pass::get(boost::string_ref userinfo)
+ {
+ static_assert(std::is_same<std::string::size_type, boost::string_ref::size_type>());
+ boost::optional<user_and_pass> out = user_and_pass{};
+
+ const auto split = userinfo.find(':');
+ if (split != boost::string_ref::npos)
+ {
+ out->user.assign(userinfo.data(), split);
+ userinfo = userinfo.substr(split + 1);
+ }
+ else
+ {
+ out->user.assign(userinfo.data(), userinfo.size());
+ userinfo = {};
+ }
+
+ out->pass.assign(userinfo.data(), userinfo.size());
+ if (percent_decoding(out->user) && percent_decoding(out->pass))
+ return out;
+ return boost::none;
+ }
+
+ boost::optional<uri_components> uri_components::get(const boost::string_ref uri)
+ {
+ scheme_and_authority result1{uri};
+ userinfo_and_hostport result2{result1.authority};
+ auto result3 = user_and_pass::get(result2.userinfo);
+ if (!result3)
+ return boost::none;
+
+ boost::optional<uri_components> out = uri_components{};
+ out->scheme = std::move(result1.scheme);
+ out->userinfo = std::move(*result3);
+ out->hostport = std::move(result2.hostport);
+ return out;
+ }
+
void get_network_address_host_and_port(const std::string& address, std::string& host, std::string& port)
{
// If IPv6 address format with port "[addr:addr:addr:...:addr]:port"
@@ -104,7 +196,6 @@ namespace net
if (epee::string_tools::get_ip_int32_from_string(ip, host_str))
return {epee::net_utils::ipv4_network_address{ip, port}};
}
-
return make_error_code(net::error::unsupported_address);
}
@@ -165,4 +256,46 @@ namespace net
return result;
}
+
+ namespace socks
+ {
+ endpoint::endpoint()
+ : endpoint(boost::asio::ip::tcp::endpoint{})
+ {}
+
+ endpoint::endpoint(const boost::asio::ip::tcp::endpoint& address)
+ : address(address), userinfo(), ver(version::v4a)
+ {}
+
+ expect<endpoint> endpoint::get(const boost::string_ref uri)
+ {
+ auto components = uri_components::get(uri);
+ if (!components)
+ return {net::error::invalid_encoding};
+ auto tcp_endpoint = get_tcp_endpoint(components->hostport);
+ if (!tcp_endpoint)
+ return tcp_endpoint.error();
+
+ endpoint out{};
+ if (components->scheme.empty() || components->scheme == "socks" || components->scheme == "socks4a")
+ out.ver = version::v4a;
+ else if (components->scheme == "socks4")
+ out.ver = version::v4;
+ else if (components->scheme == "socks5")
+ out.ver = version::v5;
+ else
+ return {net::error::invalid_scheme};
+
+ // Only version 5 supports user/pass authentication
+ if (!components->userinfo.user.empty() || !components->userinfo.pass.empty())
+ {
+ if (out.ver != version::v5)
+ return {net::error::unexpected_userinfo};
+ }
+
+ out.address = std::move(*tcp_endpoint);
+ out.userinfo = std::move(components->userinfo);
+ return out;
+ }
+ }
}
diff --git a/src/net/parse.h b/src/net/parse.h
index 6ece931c6..68f63fdb9 100644
--- a/src/net/parse.h
+++ b/src/net/parse.h
@@ -30,14 +30,75 @@
#pragma once
#include <boost/asio/ip/tcp.hpp>
+#include <boost/optional/optional.hpp>
#include <boost/utility/string_ref.hpp>
#include <cstdint>
#include "common/expect.h"
+#include "net/fwd.h"
#include "net/net_utils_base.h"
namespace net
{
+ //! \brief Separates scheme, authority, and path sections of a URI.
+ struct scheme_and_authority
+ {
+ //! \param uri with optional scheme, authority, and optional path. No URNs.
+ explicit scheme_and_authority(boost::string_ref uri);
+
+ std::string scheme;
+ std::string authority;
+ };
+
+ //! \brief Separates the userinfo and host+port from URI authority.
+ struct userinfo_and_hostport
+ {
+ //! \param authority portion of a URI.
+ explicit userinfo_and_hostport(boost::string_ref authority);
+
+ std::string userinfo;
+ std::string hostport;
+ };
+
+ //! \brief Separates the user and pass sections from URI userinfo.
+ struct user_and_pass
+ {
+ user_and_pass()
+ : user(), pass()
+ {}
+
+ /*!
+ * \param userinfo section of a URI.
+ * \return User and pass with percent encoding removed. `boost::none`
+ * if bad percent encoding
+ */
+ static boost::optional<user_and_pass> get(boost::string_ref userinfo);
+
+ std::string user;
+ std::string pass;
+ };
+
+ //! \brief Separates scheme, user, pass, and host+port sections of a URI.
+ struct uri_components
+ {
+ uri_components()
+ : scheme(), userinfo(), hostport()
+ {}
+
+ /*!
+ * \param uri with optional scheme, optional user, optional pass,
+ * authority, and optional path. URN not supported.
+ * \return Scheme, user, pass, and host+port sections of a URI with
+ * percent encoding removed on user and pass. `boost::none` if
+ * bad percent encoding.
+ */
+ static boost::optional<uri_components> get(boost::string_ref uri);
+
+ std::string scheme;
+ user_and_pass userinfo;
+ std::string hostport;
+ };
+
/*!
* \brief Takes a valid address string (IP, Tor, I2P, or DNS name) and splits it into host and port
*
@@ -79,5 +140,21 @@ namespace net
get_ipv4_subnet_address(boost::string_ref address, bool allow_implicit_32 = false);
expect<boost::asio::ip::tcp::endpoint> get_tcp_endpoint(const boost::string_ref address);
-}
+ namespace socks
+ {
+ //! \brief Separates TCP address, user+pass, and socks version
+ struct endpoint
+ {
+ endpoint();
+ explicit endpoint(const boost::asio::ip::tcp::endpoint& address);
+
+ //! \param uri with optional scheme, optional userinfo, and host+port.
+ static expect<endpoint> get(boost::string_ref uri);
+
+ boost::asio::ip::tcp::endpoint address;
+ user_and_pass userinfo;
+ version ver;
+ };
+ }
+}
diff --git a/src/net/socks.cpp b/src/net/socks.cpp
index 43d98024b..fc5be424e 100644
--- a/src/net/socks.cpp
+++ b/src/net/socks.cpp
@@ -31,6 +31,7 @@
#include <algorithm>
#include <boost/asio/bind_executor.hpp>
#include <boost/asio/buffer.hpp>
+#include <boost/asio/coroutine.hpp>
#include <boost/asio/dispatch.hpp>
#include <boost/asio/read.hpp>
#include <boost/asio/write.hpp>
@@ -38,8 +39,10 @@
#include <boost/endian/conversion.hpp>
#include <cstring>
#include <limits>
+#include <numeric>
#include <string>
+#include "net/parse.h"
#include "net/net_utils_base.h"
#include "net/tor_address.h"
#include "net/i2p_address.h"
@@ -54,6 +57,16 @@ namespace socks
constexpr const std::uint8_t v4tor_resolve_command = 0xf0;
constexpr const std::uint8_t v4_request_granted = 90;
+ constexpr const std::uint8_t v5_noauth_method = 0;
+ constexpr const std::uint8_t v5_userpass_method = 2;
+ constexpr const std::uint8_t v5_connect_command = 1;
+ constexpr const std::uint8_t v5_reserved = 0;
+ constexpr const std::uint8_t v5_ipv4_type = 1;
+ constexpr const std::uint8_t v5_domain_type = 3;
+ constexpr const std::uint8_t v5_ipv6_type = 4;
+ constexpr const std::uint8_t v5_reply_success = 0;
+ constexpr const std::uint8_t v5_userpass_version = 1;
+
struct v4_header
{
std::uint8_t version;
@@ -62,6 +75,114 @@ namespace socks
boost::endian::big_uint32_t ip;
};
+ struct v5_noauth_initial
+ {
+ std::uint8_t version;
+ std::uint8_t n_methods;
+ std::uint8_t method;
+
+ static constexpr v5_noauth_initial make() noexcept
+ {
+ return {5, 1, v5_noauth_method};
+ }
+ };
+
+ struct v5_auth_initial
+ {
+ std::uint8_t version;
+ std::uint8_t n_methods;
+ std::uint8_t method1;
+ std::uint8_t method2;
+
+ static constexpr v5_auth_initial make() noexcept
+ {
+ return {5, 2, v5_noauth_method, v5_userpass_method};
+ }
+ };
+
+ struct v5_response_initial
+ {
+ std::uint8_t version;
+ std::uint8_t method;
+ };
+
+ struct v5_ipv4_connect
+ {
+ std::uint8_t version;
+ std::uint8_t command;
+ std::uint8_t reserved;
+ std::uint8_t type;
+ boost::endian::big_uint32_t ip;
+ boost::endian::big_uint16_t port;
+
+ static v5_ipv4_connect make(const std::uint32_t ip, const std::uint16_t port) noexcept
+ {
+ return {5, v5_connect_command, v5_reserved, v5_ipv4_type, ip, port};
+ }
+ };
+
+ struct v5_domain_connect
+ {
+ std::uint8_t version;
+ std::uint8_t command;
+ std::uint8_t reserved;
+ std::uint8_t type;
+ std::uint8_t length;
+
+ static constexpr v5_domain_connect make(const std::uint8_t length) noexcept
+ {
+ return {5, v5_connect_command, v5_reserved, v5_domain_type, length};
+ }
+ };
+
+ struct v5_ipv6_connect
+ {
+ std::uint8_t version;
+ std::uint8_t command;
+ std::uint8_t reserved;
+ std::uint8_t type;
+ char ip[16];
+ boost::endian::big_uint16_t port;
+
+ static v5_ipv6_connect make(const boost::asio::ip::address_v6& ip, const std::uint16_t port)
+ {
+ v5_ipv6_connect out{5, v5_connect_command, v5_reserved, v5_ipv6_type};
+ out.port = port;
+
+ const auto ip_bytes = ip.to_bytes();
+ static_assert(sizeof(out.ip) == sizeof(ip_bytes), "unexpected ipv6 bytes size");
+ std::memcpy(std::addressof(out.ip), std::addressof(ip_bytes), sizeof(out.ip));
+
+ return out;
+ }
+ };
+
+ struct v5_response_auth
+ {
+ std::uint8_t version;
+ std::uint8_t status;
+ };
+
+ struct v5_response_connect
+ {
+ std::uint8_t version;
+ std::uint8_t reply;
+ std::uint8_t reserved;
+ std::uint8_t type;
+ };
+
+ struct v5_response_ipv4
+ {
+ boost::endian::big_uint32_t ip;
+ boost::endian::big_uint16_t port;
+ };
+
+ struct v5_response_ipv6
+ {
+ char ip[16];
+ boost::endian::big_uint16_t port;
+ };
+
std::size_t write_domain_header(epee::span<std::uint8_t> out, const std::uint8_t command, const std::uint16_t port, const boost::string_ref domain)
{
if (std::numeric_limits<std::size_t>::max() - sizeof(v4_header) - 2 < domain.size())
@@ -86,6 +207,131 @@ namespace socks
return buf_size;
}
+ std::size_t write_v5_userpass(epee::span<std::uint8_t> out, const user_and_pass& userinfo)
+ {
+ static constexpr const std::uint8_t max_length = std::numeric_limits<std::uint8_t>::max();
+ if (max_length < userinfo.user.size())
+ return 0;
+ if (max_length < userinfo.pass.size())
+ return 0;
+
+ static_assert(max_length < std::numeric_limits<std::size_t>::max());
+ static_assert(max_length < std::numeric_limits<std::size_t>::max() - max_length);
+ static_assert(3 <= std::numeric_limits<std::size_t>::max() - max_length - max_length);
+
+ if (out.size() < 3 + userinfo.user.size() + userinfo.pass.size())
+ return 0;
+
+ const std::size_t initial = out.size();
+
+ out[0] = v5_userpass_version;
+ out[1] = std::uint8_t(userinfo.user.size());
+ out.remove_prefix(2);
+
+ std::memcpy(out.data(), userinfo.user.data(), userinfo.user.size());
+ out.remove_prefix(userinfo.user.size());
+
+ out[0] = std::uint8_t(userinfo.pass.size());
+ out.remove_prefix(1);
+
+ std::memcpy(out.data(), userinfo.pass.data(), userinfo.pass.size());
+ out.remove_prefix(userinfo.pass.size());
+ return initial - out.size();
+ }
+
+ std::array<std::uint16_t, 2> write_v5_initial(epee::span<std::uint8_t> out, const user_and_pass* userinfo)
+ {
+ std::array<std::uint16_t, 2> sizes{{}};
+
+ if (userinfo && (!userinfo->user.empty() || !userinfo->pass.empty()))
+ {
+ const auto header = v5_auth_initial::make();
+ if (out.size() < sizeof(header))
+ return sizes;
+ std::memcpy(out.data(), std::addressof(header), sizeof(header));
+ out.remove_prefix(sizeof(header));
+
+ const std::size_t auth = write_v5_userpass(out, *userinfo);
+ if (!auth)
+ return sizes;
+ out.remove_prefix(auth);
+
+ std::get<0>(sizes) = sizeof(header);
+ std::get<1>(sizes) = auth;
+ }
+ else
+ {
+ const auto header = v5_noauth_initial::make();
+ if (out.size() < sizeof(header))
+ return sizes;
+ std::memcpy(out.data(), std::addressof(header), sizeof(header));
+ out.remove_prefix(sizeof(header));
+
+ std::get<0>(sizes) = sizeof(header);
+ }
+
+ return sizes;
+ }
+
+ template<typename T>
+ std::array<std::uint16_t, 3> write_v5_address_connect(epee::span<std::uint8_t> out, const T& address, const user_and_pass* userinfo)
+ {
+ std::array<std::uint16_t, 3> sizes{{}};
+
+ const auto result = write_v5_initial(out, userinfo);
+ if (!std::get<0>(result))
+ return sizes;
+
+ for (std::size_t length : result)
+ out.remove_prefix(length);
+
+ if (out.size() < sizeof(address))
+ return sizes;
+ std::memcpy(out.data(), std::addressof(address), sizeof(address));
+
+ std::get<0>(sizes) = std::get<0>(result);
+ std::get<1>(sizes) = std::get<1>(result);
+ std::get<2>(sizes) = sizeof(address);
+ return sizes;
+ }
+
+ std::array<std::uint16_t, 3> write_v5_domain_connect(epee::span<std::uint8_t> out, const std::uint16_t port, const boost::string_ref domain, const user_and_pass* userinfo)
+ {
+ std::array<std::uint16_t, 3> sizes{{}};
+ if (std::numeric_limits<std::uint8_t>::max() < domain.size())
+ return sizes;
+
+ const auto result = write_v5_initial(out, userinfo);
+ if (!std::get<0>(result))
+ return sizes;
+
+ for (std::size_t length : result)
+ out.remove_prefix(length);
+
+ const auto request = v5_domain_connect::make(std::uint8_t(domain.size()));
+ static_assert(sizeof(port) <= std::numeric_limits<std::size_t>::max() - sizeof(request));
+ if (std::numeric_limits<std::size_t>::max() - sizeof(request) - sizeof(port) < domain.size())
+ return sizes;
+
+ const std::size_t last_size = sizeof(request) + sizeof(port) + domain.size();
+ if (out.size() < last_size)
+ return sizes;
+
+ std::memcpy(out.data(), std::addressof(request), sizeof(request));
+ out.remove_prefix(sizeof(request));
+
+ std::memcpy(out.data(), domain.data(), domain.size());
+ out.remove_prefix(domain.size());
+
+ const boost::endian::big_uint16_t big_port{port};
+ std::memcpy(out.data(), std::addressof(big_port), sizeof(big_port));
+
+ std::get<0>(sizes) = std::get<0>(result);
+ std::get<1>(sizes) = std::get<1>(result);
+ std::get<2>(sizes) = last_size;
+ return sizes;
+ }
+
struct socks_category : boost::system::error_category
{
explicit socks_category() noexcept
@@ -101,6 +347,23 @@ namespace socks
{
switch (socks::error(value))
{
+ case socks::error::general_failure:
+ return "Socks general server failure";
+ case socks::error::not_allowed:
+ return "Socks connection not allowed by ruleset";
+ case socks::error::network_unreachable:
+ return "Socks network unreachable";
+ case socks::error::host_unreachable:
+ return "Socks host unreachable";
+ case socks::error::connection_refused:
+ return "Socks connection refused";
+ case socks::error::ttl_expired:
+ return "Socks TTL expired";
+ case socks::error::command_not_supported:
+ return "Socks command not supported";
+ case socks::error::address_type_not_supported:
+ return "Socks address type not supported";
+
case socks::error::rejected:
return "Socks request rejected or failed";
case socks::error::identd_connection:
@@ -108,6 +371,8 @@ namespace socks
case socks::error::identd_user:
return "Socks request rejected because the client program and identd report different user-ids";
+ case socks::error::auth_failure:
+ return "Socks authentication failure";
case socks::error::bad_read:
return "Socks boost::async_read read fewer bytes than expected";
case socks::error::bad_write:
@@ -125,6 +390,10 @@ namespace socks
{
switch (socks::error(value))
{
+ case socks::error::network_unreachable:
+ return boost::system::errc::host_unreachable;
+ case socks::error::connection_refused:
+ return boost::system::errc::connection_refused;
case socks::error::bad_read:
case socks::error::bad_write:
return boost::system::errc::io_error;
@@ -158,18 +427,18 @@ namespace socks
if (self_)
{
client& self = *self_;
- self.buffer_size_ = std::min(bytes, sizeof(self.buffer_));
+ std::get<0>(self.buffer_size_) = std::min(bytes, sizeof(self.buffer_));
if (error)
- self.done(error, std::move(self_));
- else if (self.buffer().size() < sizeof(v4_header))
- self.done(socks::error::bad_read, std::move(self_));
+ self.done(error, self_);
+ else if (std::get<0>(self.buffer_size_) < sizeof(v4_header))
+ self.done(socks::error::bad_read, self_);
else if (self.buffer_[0] != 0) // response version
- self.done(socks::error::unexpected_version, std::move(self_));
+ self.done(socks::error::unexpected_version, self_);
else if (self.buffer_[1] != v4_request_granted)
- self.done(socks::error(int(self.buffer_[1]) + 1), std::move(self_));
+ self.done(socks::error(int(self.buffer_[1]) + 1), self_);
else
- self.done(boost::system::error_code{}, std::move(self_));
+ self.done(boost::system::error_code{}, self_);
}
}
};
@@ -181,6 +450,7 @@ namespace socks
static boost::asio::mutable_buffer get_buffer(client& self) noexcept
{
static_assert(sizeof(v4_header) <= sizeof(self.buffer_), "buffer too small for v4 response");
+ std::get<0>(self.buffer_size_) = sizeof(v4_header);
return boost::asio::buffer(self.buffer_, sizeof(v4_header));
}
@@ -190,22 +460,179 @@ namespace socks
{
client& self = *self_;
if (error)
- self.done(error, std::move(self_));
- else if (bytes < self.buffer().size())
- self.done(socks::error::bad_write, std::move(self_));
+ self.done(error, self_);
+ else if (bytes < std::get<0>(self.buffer_size_))
+ self.done(socks::error::bad_write, self_);
else
boost::asio::async_read(self.proxy_, get_buffer(self), boost::asio::bind_executor(self.strand_, completed{std::move(self_)}));
}
}
};
+ struct client::process_v5 : boost::asio::coroutine
+ {
+ std::shared_ptr<client> self_;
+
+ explicit process_v5(std::shared_ptr<client> self)
+ : boost::asio::coroutine(), self_(std::move(self))
+ {}
+
+ static boost::asio::mutable_buffer get_read_buffer(client& self, const std::size_t size)
+ {
+ const std::size_t offset =
+ std::accumulate(self.buffer_size_.begin(), self.buffer_size_.end(), std::size_t(0));
+ if (sizeof(self.buffer_) < offset || sizeof(self.buffer_) - offset < size)
+ throw std::runtime_error{"Not enough room for reading socks v5 buffer"};
+ return boost::asio::buffer(self.buffer_ + offset, size);
+ }
+
+ template<unsigned I>
+ static boost::asio::const_buffer get_write_buffer(const client& self) noexcept
+ {
+ const std::size_t offset =
+ std::accumulate(self.buffer_size_.begin(), self.buffer_size_.begin() + I, std::size_t(0));
+ return boost::asio::buffer(
+ self.buffer_ + offset, std::get<I>(self.buffer_size_)
+ );
+ }
+
+ void operator()(const boost::system::error_code error, std::size_t bytes)
+ {
+ if (!self_)
+ return;
+
+ client& self = *self_;
+ if (error)
+ {
+ self.done(error, self_);
+ return;
+ }
+
+ bool send_userpass = false;
+ BOOST_ASIO_CORO_REENTER(this)
+ {
+ // initial header already written
+
+ BOOST_ASIO_CORO_YIELD boost::asio::async_read(
+ self.proxy_,
+ get_read_buffer(self, sizeof(v5_response_initial)),
+ boost::asio::bind_executor(self.strand_, std::move(*this))
+ );
+ {
+ v5_response_initial header{};
+
+ assert(bytes == sizeof(header));
+ const auto buf = get_read_buffer(self, sizeof(header));
+ std::memcpy(std::addressof(header), buf.data(), sizeof(header));
+ if (header.version != 5)
+ {
+ self.done(socks::error::unexpected_version, self_);
+ return;
+ }
+ if (header.method != v5_noauth_method && header.method != v5_userpass_method)
+ {
+ self.done(socks::error::auth_failure, self_);
+ return;
+ }
+ send_userpass = (header.method == v5_userpass_method);
+ }
+
+ if (send_userpass)
+ {
+ if (!std::get<1>(self.buffer_size_))
+ {
+ self.done(socks::error::auth_failure, self_);
+ return;
+ }
+
+ BOOST_ASIO_CORO_YIELD boost::asio::async_write(
+ self.proxy_, get_write_buffer<1>(self), boost::asio::bind_executor(self.strand_, std::move(*this))
+ );
+ assert(bytes == std::get<1>(self.buffer_size_));
+
+ BOOST_ASIO_CORO_YIELD boost::asio::async_read(
+ self.proxy_,
+ get_read_buffer(self, sizeof(v5_response_auth)),
+ boost::asio::bind_executor(self.strand_, std::move(*this))
+ );
+ {
+ v5_response_auth header{};
+
+ assert(bytes == sizeof(header));
+ const auto buf = get_read_buffer(self, sizeof(header));
+ std::memcpy(std::addressof(header), buf.data(), sizeof(header));
+ if (header.version != v5_userpass_version)
+ {
+ self.done(socks::error::unexpected_version, self_);
+ return;
+ }
+ if (header.status != v5_reply_success)
+ {
+ self.done(socks::error::auth_failure, self_);
+ return;
+ }
+ }
+ }
+
+ BOOST_ASIO_CORO_YIELD boost::asio::async_write(
+ self.proxy_, get_write_buffer<2>(self), boost::asio::bind_executor(self.strand_, std::move(*this))
+ );
+ assert(bytes == std::get<2>(self.buffer_size_));
+
+ self.buffer_size_ = {};
+ BOOST_ASIO_CORO_YIELD boost::asio::async_read(
+ self.proxy_,
+ get_read_buffer(self, sizeof(v5_response_connect)),
+ boost::asio::bind_executor(self.strand_, std::move(*this))
+ );
+ {
+ v5_response_connect header{};
+
+ assert(bytes == sizeof(header));
+ const auto buf = get_read_buffer(self, sizeof(header));
+ std::memcpy(std::addressof(header), buf.data(), sizeof(header));
+ if (header.version != 5)
+ {
+ self.done(socks::error::unexpected_version, self_);
+ return;
+ }
+ if (header.reply != v5_reply_success)
+ {
+ self.done(socks::error(int(header.reply)), self_);
+ return;
+ }
+
+ if (header.type == v5_ipv4_type)
+ bytes = sizeof(v5_response_ipv4);
+ else if (header.type == v5_ipv6_type)
+ bytes = sizeof(v5_response_ipv6);
+ else
+ {
+ self.done(socks::error::unexpected_version, self_);
+ return;
+ }
+ }
+
+ std::get<0>(self.buffer_size_) = sizeof(v5_response_connect);
+ BOOST_ASIO_CORO_YIELD boost::asio::async_read(
+ self.proxy_,
+ get_read_buffer(self, bytes),
+ boost::asio::bind_executor(self.strand_, std::move(*this))
+ );
+ std::get<0>(self.buffer_size_) =
+ std::min(sizeof(self.buffer_), sizeof(v5_response_connect) + bytes);
+ self.done(error, self_);
+ }
+ }
+ };
+
struct client::write
{
std::shared_ptr<client> self_;
static boost::asio::const_buffer get_buffer(client const& self) noexcept
{
- return boost::asio::buffer(self.buffer_, self.buffer_size_);
+ return boost::asio::buffer(self.buffer_, std::get<0>(self.buffer_size_));
}
void operator()(const boost::system::error_code error)
@@ -214,20 +641,24 @@ namespace socks
{
client& self = *self_;
if (error)
- self.done(error, std::move(self_));
+ self.done(error, self_);
+ else if (self.ver_ == version::v5)
+ boost::asio::async_write(self.proxy_, get_buffer(self), boost::asio::bind_executor(self.strand_, process_v5{std::move(self_)}));
else
boost::asio::async_write(self.proxy_, get_buffer(self), boost::asio::bind_executor(self.strand_, read{std::move(self_)}));
}
}
};
+
+
client::client(stream_type::socket&& proxy, socks::version ver)
- : proxy_(std::move(proxy)), strand_(proxy_.get_executor()), buffer_size_(0), buffer_(), ver_(ver)
+ : proxy_(std::move(proxy)), strand_(proxy_.get_executor()), buffer_size_{{}}, buffer_(), ver_(ver)
{}
client::~client() {}
- bool client::set_connect_command(const epee::net_utils::ipv4_network_address& address)
+ bool client::set_connect_command(const epee::net_utils::ipv4_network_address& address, const user_and_pass* userinfo)
{
switch (socks_version())
{
@@ -235,6 +666,13 @@ namespace socks
case version::v4a:
case version::v4a_tor:
break;
+ case version::v5:
+ buffer_size_ = write_v5_address_connect(
+ buffer_,
+ v5_ipv4_connect::make(boost::endian::big_to_native(address.ip()), address.port()),
+ userinfo
+ );
+ return std::get<0>(buffer_size_) != 0;
default:
return false;
}
@@ -242,43 +680,64 @@ namespace socks
static_assert(sizeof(v4_header) < sizeof(buffer_), "buffer size too small for request");
static_assert(0 < sizeof(buffer_), "buffer size too small for null termination");
+ if (userinfo && (!userinfo->user.empty() || !userinfo->pass.empty()))
+ return false;
+
// version 4
const v4_header temp{4, v4_connect_command, address.port(), boost::endian::big_to_native(address.ip())};
std::memcpy(std::addressof(buffer_), std::addressof(temp), sizeof(temp));
buffer_[sizeof(temp)] = 0;
- buffer_size_ = sizeof(temp) + 1;
+
+ buffer_size_ = {};
+ std::get<0>(buffer_size_) = sizeof(temp) + 1;
return true;
}
- bool client::set_connect_command(const boost::string_ref domain, std::uint16_t port)
+ bool client::set_connect_command(const epee::net_utils::ipv6_network_address& address, const user_and_pass* userinfo)
+ {
+ if (socks_version() != version::v5)
+ return false;
+ buffer_size_ = write_v5_address_connect(
+ buffer_, v5_ipv6_connect::make(address.ip(), address.port()), userinfo
+ );
+ return std::get<0>(buffer_size_) != 0;
+ }
+
+ bool client::set_connect_command(const boost::string_ref domain, std::uint16_t port, const user_and_pass* userinfo)
{
switch (socks_version())
{
case version::v4a:
case version::v4a_tor:
break;
-
+ case version::v5:
+ buffer_size_ = write_v5_domain_connect(buffer_, port, domain, userinfo);
+ return std::get<0>(buffer_size_) != 0;
default:
return false;
}
+ if (userinfo && (!userinfo->user.empty() || !userinfo->pass.empty()))
+ return false;
+
const std::size_t buf_used = write_domain_header(buffer_, v4_connect_command, port, domain);
- buffer_size_ = buf_used;
+ buffer_size_ = {};
+ std::get<0>(buffer_size_) = buf_used;
return buf_used != 0;
}
- bool client::set_connect_command(const net::tor_address& address)
+ bool client::set_connect_command(const net::tor_address& address, const user_and_pass* userinfo)
{
if (!address.is_unknown())
- return set_connect_command(address.host_str(), address.port());
+ return set_connect_command(address.host_str(), address.port(), userinfo);
return false;
}
- bool client::set_connect_command(const net::i2p_address& address)
+ bool client::set_connect_command(const net::i2p_address& address, const user_and_pass* userinfo)
{
if (!address.is_unknown())
- return set_connect_command(address.host_str(), address.port());
+ return set_connect_command(address.host_str(), address.port(), userinfo);
return false;
}
@@ -288,13 +747,14 @@ namespace socks
return false;
const std::size_t buf_used = write_domain_header(buffer_, v4tor_resolve_command, 0, domain);
- buffer_size_ = buf_used;
+ buffer_size_ = {};
+ std::get<0>(buffer_size_) = buf_used;
return buf_used != 0;
}
bool client::connect_and_send(std::shared_ptr<client> self, const stream_type::endpoint& proxy_address)
{
- if (self && !self->buffer().empty())
+ if (self && std::get<0>(self->buffer_size_))
{
client& alias = *self;
alias.proxy_.async_connect(proxy_address, boost::asio::bind_executor(alias.strand_, write{std::move(self)}));
@@ -305,10 +765,13 @@ namespace socks
bool client::send(std::shared_ptr<client> self)
{
- if (self && !self->buffer().empty())
+ if (self && std::get<0>(self->buffer_size_))
{
client& alias = *self;
- boost::asio::async_write(alias.proxy_, write::get_buffer(alias), boost::asio::bind_executor(alias.strand_, read{std::move(self)}));
+ if (alias.ver_ == version::v5)
+ boost::asio::async_write(alias.proxy_, write::get_buffer(alias), boost::asio::bind_executor(alias.strand_, process_v5{std::move(self)}));
+ else
+ boost::asio::async_write(alias.proxy_, write::get_buffer(alias), boost::asio::bind_executor(alias.strand_, read{std::move(self)}));
return true;
}
return false;
diff --git a/src/net/socks.h b/src/net/socks.h
index 1c80ece2c..06d946d6a 100644
--- a/src/net/socks.h
+++ b/src/net/socks.h
@@ -28,6 +28,7 @@
#pragma once
+#include <array>
#include <cstdint>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
@@ -46,6 +47,7 @@ namespace epee
namespace net_utils
{
class ipv4_network_address;
+ class ipv6_network_address;
}
}
@@ -58,19 +60,30 @@ namespace socks
{
v4 = 0,
v4a,
- v4a_tor //!< Extensions defined in Tor codebase
+ v4a_tor, //!< Extensions defined in Tor codebase
+ v5
};
//! Possible errors with socks communication. Defined in https://www.openssh.com/txt/socks4.protocol
enum class error : int
{
// 0 is reserved for success value
- // 1-256 -> reserved for error values from socks server (+1 from wire value).
+ // v5 errors
+ general_failure = 1,
+ not_allowed,
+ network_unreachable,
+ host_unreachable,
+ connection_refused,
+ ttl_expired,
+ command_not_supported,
+ address_type_not_supported,
+ // v4 errors
rejected = 92,
identd_connection,
identd_user,
// Specific to application
- bad_read = 257,
+ auth_failure = 257,
+ bad_read,
bad_write,
unexpected_version
};
@@ -94,7 +107,7 @@ namespace socks
{
boost::asio::ip::tcp::socket proxy_;
boost::asio::strand<boost::asio::ip::tcp::socket::executor_type> strand_;
- std::uint16_t buffer_size_;
+ std::array<std::uint16_t, 3> buffer_size_;
std::uint8_t buffer_[1024];
socks::version ver_;
@@ -109,7 +122,7 @@ namespace socks
\param error when processing last command (if any).
\param self `shared_ptr<client>` handle to `this`.
*/
- virtual void done(boost::system::error_code error, std::shared_ptr<client> self) = 0;
+ virtual void done(boost::system::error_code error, const std::shared_ptr<client>& self) = 0;
public:
using stream_type = boost::asio::ip::tcp;
@@ -118,6 +131,7 @@ namespace socks
struct write;
struct read;
struct completed;
+ struct process_v5;
/*!
\param proxy ownership is passed into `this`. Does not have to be
@@ -139,33 +153,47 @@ namespace socks
//! \return Socks version.
socks::version socks_version() const noexcept { return ver_; }
- //! \return Contents of internal buffer.
+ //! \return Contents of first internal buffer
epee::span<const std::uint8_t> buffer() const noexcept
{
- return {buffer_, buffer_size_};
+ return {buffer_, std::get<0>(buffer_size_)};
}
- //! \post `buffer.empty()`.
- void clear_command() noexcept { buffer_size_ = 0; }
+ //! \post `buffer_[0] = 0, buffer_[1] = 0`.
+ void clear_command() noexcept { buffer_size_ = {}; }
//! Try to set `address` as remote connection request.
- bool set_connect_command(const epee::net_utils::ipv4_network_address& address);
+ bool set_connect_command(
+ const epee::net_utils::ipv4_network_address& address,
+ const user_and_pass* userinfo = nullptr);
+
+ //! Try to set `address` as remote connection request.
+ bool set_connect_command(
+ const epee::net_utils::ipv6_network_address& address,
+ const user_and_pass* userinfo = nullptr);
//! Try to set `domain` + `port` as remote connection request.
- bool set_connect_command(boost::string_ref domain, std::uint16_t port);
+ bool set_connect_command(
+ boost::string_ref domain,
+ std::uint16_t port,
+ const user_and_pass* userinfo = nullptr);
//! Try to set `address` as remote Tor hidden service connection request.
- bool set_connect_command(const net::tor_address& address);
+ bool set_connect_command(
+ const net::tor_address& address,
+ const user_and_pass* userinfo = nullptr);
//! Try to set `address` as remote i2p hidden service connection request.
- bool set_connect_command(const net::i2p_address& address);
+ bool set_connect_command(
+ const net::i2p_address& address,
+ const user_and_pass* userinfo = nullptr);
//! Try to set `domain` as remote DNS A record lookup request.
bool set_resolve_command(boost::string_ref domain);
/*!
- Asynchronously connect to `proxy_address` then issue command in
- `buffer()`. The `done(...)` method will be invoked upon completion
+ Asynchronously connect to `proxy_address` then issue command(s) in
+ `buffer_`. The `done(...)` method will be invoked upon completion
with `self` and potential `error`s.
\note Must use one of the `self->set_*_command` calls before using
@@ -181,7 +209,7 @@ namespace socks
/*!
Assume existing connection to proxy server; asynchronously issue
- command in `buffer()`. The `done(...)` method will be invoked
+ command in `buffer_`. The `done(...)` method will be invoked
upon completion with `self` and potential `error`s.
\note Must use one of the `self->set_*_command` calls before using
@@ -215,7 +243,7 @@ namespace socks
{
Handler handler_;
- virtual void done(boost::system::error_code error, std::shared_ptr<client>) override
+ virtual void done(boost::system::error_code error, const std::shared_ptr<client>&) override
{
handler_(error, take_socket());
}
diff --git a/src/net/socks_connect.cpp b/src/net/socks_connect.cpp
index 8ecbf6d08..c7c963cb7 100644
--- a/src/net/socks_connect.cpp
+++ b/src/net/socks_connect.cpp
@@ -28,6 +28,7 @@
#include "socks_connect.h"
+#include <boost/asio/ip/address_v6.hpp>
#include <boost/system/error_code.hpp>
#include <boost/system/system_error.hpp>
#include <cstdint>
@@ -36,6 +37,7 @@
#include "net/error.h"
#include "net/net_utils_base.h"
+#include "net/parse.h"
#include "net/socks.h"
#include "string_tools.h"
#include "string_tools_lexical.h"
@@ -44,9 +46,22 @@ namespace net
{
namespace socks
{
+ namespace
+ {
+ bool get_v6_address(boost::asio::ip::address_v6& out, const std::string& source)
+ {
+ boost::system::error_code error{};
+ out = boost::asio::ip::make_address_v6(source, error);
+ return !error;
+ }
+ } // anonymous
+
boost::unique_future<boost::asio::ip::tcp::socket>
connector::operator()(const std::string& remote_host, const std::string& remote_port, boost::asio::steady_timer& timeout) const
{
+ if (!proxy_address)
+ throw std::runtime_error{"Unexpected nullptr of net::socks::endpoint"};
+
struct future_socket
{
boost::promise<boost::asio::ip::tcp::socket> result_;
@@ -68,18 +83,21 @@ namespace socks
bool is_set = false;
std::uint32_t ip_address = 0;
+ boost::asio::ip::address_v6 v6_address{};
boost::promise<boost::asio::ip::tcp::socket> result{};
out = result.get_future();
const auto proxy = net::socks::make_connect_client(
- boost::asio::ip::tcp::socket{MONERO_GET_EXECUTOR(timeout)}, net::socks::version::v4a, future_socket{std::move(result)}
+ boost::asio::ip::tcp::socket{MONERO_GET_EXECUTOR(timeout)}, proxy_address->ver, future_socket{std::move(result)}
);
if (epee::string_tools::get_ip_int32_from_string(ip_address, remote_host))
- is_set = proxy->set_connect_command(epee::net_utils::ipv4_network_address{ip_address, port});
+ is_set = proxy->set_connect_command(epee::net_utils::ipv4_network_address{ip_address, port}, std::addressof(proxy_address->userinfo));
+ else if (get_v6_address(v6_address, remote_host))
+ is_set = proxy->set_connect_command(epee::net_utils::ipv6_network_address{v6_address, port}, std::addressof(proxy_address->userinfo));
else
- is_set = proxy->set_connect_command(remote_host, port);
+ is_set = proxy->set_connect_command(remote_host, port, std::addressof(proxy_address->userinfo));
- if (!is_set || !net::socks::client::connect_and_send(proxy, proxy_address))
+ if (!is_set || !net::socks::client::connect_and_send(proxy, proxy_address->address))
throw std::system_error{net::error::invalid_host, "Address for socks proxy"};
timeout.async_wait(net::socks::client::async_close{std::move(proxy)});
diff --git a/src/net/socks_connect.h b/src/net/socks_connect.h
index 587e3cd3c..2baef8f94 100644
--- a/src/net/socks_connect.h
+++ b/src/net/socks_connect.h
@@ -31,8 +31,11 @@
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/thread/future.hpp>
+#include <memory>
#include <string>
+#include "net/fwd.h"
+
namespace net
{
namespace socks
@@ -40,7 +43,7 @@ namespace socks
//! Primarily for use with `epee::net_utils::http_client`.
struct connector
{
- boost::asio::ip::tcp::endpoint proxy_address;
+ std::shared_ptr<endpoint> proxy_address;
/*! Creates a new socket, asynchronously connects to `proxy_address`,
and requests a connection to `remote_host` on `remote_port`. Sets