diff options
Diffstat (limited to 'src/net')
| -rw-r--r-- | src/net/error.cpp | 6 | ||||
| -rw-r--r-- | src/net/error.h | 3 | ||||
| -rw-r--r-- | src/net/fwd.h | 6 | ||||
| -rw-r--r-- | src/net/http.cpp | 8 | ||||
| -rw-r--r-- | src/net/parse.cpp | 135 | ||||
| -rw-r--r-- | src/net/parse.h | 79 | ||||
| -rw-r--r-- | src/net/socks.cpp | 515 | ||||
| -rw-r--r-- | src/net/socks.h | 62 | ||||
| -rw-r--r-- | src/net/socks_connect.cpp | 26 | ||||
| -rw-r--r-- | src/net/socks_connect.h | 5 |
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 |
