From 64a613e83807c8515a1b1d70249067cd51e4ae80 Mon Sep 17 00:00:00 2001 From: Lee *!* Clagett Date: Sat, 17 Aug 2024 18:37:10 -0400 Subject: [PATCH] Add Socks v5 support to daemon and wallet --- docs/proxies.md | 204 +++++++++++++ src/daemon/command_line_args.h | 2 +- src/net/error.cpp | 6 + src/net/error.h | 3 + src/net/fwd.h | 6 + src/net/http.cpp | 8 +- src/net/parse.cpp | 135 ++++++++- src/net/parse.h | 78 +++++ src/net/socks.cpp | 520 +++++++++++++++++++++++++++++++-- src/net/socks.h | 58 +++- src/net/socks_connect.cpp | 25 +- src/net/socks_connect.h | 5 +- src/p2p/net_node.cpp | 39 ++- src/p2p/net_node.h | 8 +- src/p2p/net_node.inl | 4 +- src/wallet/wallet2.cpp | 2 +- tests/unit_tests/net.cpp | 515 +++++++++++++++++++++++++++++++- 17 files changed, 1540 insertions(+), 78 deletions(-) create mode 100644 docs/proxies.md diff --git a/docs/proxies.md b/docs/proxies.md new file mode 100644 index 00000000000..65622e5c260 --- /dev/null +++ b/docs/proxies.md @@ -0,0 +1,204 @@ +# Proxy usage in the Monero ecosystem +The CLI/RPC wallets and daemon both support proxies and use the same parameters +to configure them. Currently socks 4, 4a, and 5 are supported and can be +selected with command-line options. + +## Wallet +The CLI and RPC wallets support proxies via the `--proxy` option. The format +for usage is `[socks5://[user:pass]]host:port`. The square brackets indicate +an optional portion. This option can only be specified once. Examples: + +``` +--proxy 192.168.0.10:1050 +--proxy socks5://192.168.0.10:1050 +--proxy socks5://username:password@192.168.0.10:1050 +--proxy [::1]:1050 +--proxy socks5://[::1]:1050 +--proxy socks5://username:password@[::1]:1050 +``` + +The first connects to `192.168.0.10` on port `1050` using socks 4a. The second +connects to the same location using socks 5. The third uses socks 5 at the same +location and sends user authentication if prompted by the proxy server. The +last three are identical to the first 3, except an IPv6 address is used +instead. While IPv6 connections are invalid for Socks 4 and 4a, the proxy +server itself can be connected using IPv6. + +The username and password fields both support "percent-encoding" for special +character support. As an example, `%40` gets converted to `@`, such that +`username:p%40ssword` gets converted to `username:p@ssword`. This allows that +specific character to be used; specifying the character directly will +incorrectly change the specification of the hostname. + +> NOTE: The username+password will show up in the process list and can be read +> by other programs. It is recommended that `--config-file` be used to store +> username+password options. The format for a config file is `option=value`, +> so in this example the file would contain: +> `proxy=socks5://username:password@192.168.0.10:1080`. + +The CLI and RPC wallets currently reject hosts that do **NOT** end in`.onion` +or `.i2p` **unless** `--daemon-ssl-ca-certificates` or +`--daemon-ssl-allowed-fingerprints` is used. If an onion or i2p address is used, +the hostname contains the certificate verification, providing decent security +against man-in-the-middle (MitM) attacks. The two `--daemon-ssl-*` options +support specifying exact certificates, also preventing MitM attacks. + +> Perhaps the wallets should be relaxed to allow system-CA checks, but for now +> certificates must be strictly provided. + +## Daemon +The daemon has two options for proxies `--proxy` and `--tx-proxy` which can be +used in isolation or together. The `--proxy` option controls how +IPv4/IPv6/hostname connections are performed, whereas `--tx-proxy` controls +how local transactions are relayed. Both options support Socks 4, 4a, and 5. + +### `--proxy` +This option should be used when outbound connections to IPv4/IPv6 addresses and +hostnames (other than `.onion` `.i2p`) need to be proxied. Common examples +include using Tor exit nodes or a VPN to conceal your local IP. This option +will **not** use Tor or I2P hidden services for P2P connections; this is +primarily used for proxying standard IPv4 or IPv6 connections to some remote +host. Hidden services are not used because this is designed to be more general +purpose (i.e. a standard socks VPN can be used). + +> An additional option for hidden services (separate from `--tx-proxy`) could +> arguably be added, which could optionally turn off IPv4/IPv6 connections for +> P2P. + +The format for `--proxy` usage: `[socks5://[user:pass]@127.0.0.1`. The square +bracket indicate optional portion. See [wallet](#wallet) section above for +examples and other information on the format. The option can only be specified +once. The restrictions for MitM attacks apply only to the wallet usage, and not +to the daemon. + +> When using `--proxy`, inbound connections will be impossible unless the +> proxy server is somehow setup to forward connections. This setup is a +> difficult because each outgoing socks connections can have a unique binding +> port. Such a setup is currently out-of-scope for this document. + +### `--tx-proxy` +This option should be used to specify a proxy that can resolve hidden service +hostnames, so that local transactions can be forwarded over a privacy +preserving network. Currently only Tor or I2P hidden services are supported. +This option be specified multiple times, but only once per network (see below). + +The format for `--tx-proxy` is +`network,[socks5://[user:pass@]]ip:port[,max_connections][,disable_noise]`. +Examples: + +``` +--tx-proxy tor,127.0.0.1:1050 +--tx-proxy tor,127.0.0.1:1050,100 +--tx-proxy tor,127.0.0.1:1050,disable_noise +--tx-proxy tor,127.0.0.1:1050,100,disable_noise +--tx-proxy tor,socks5://127.0.0.1:1050 +--tx-proxy tor,socks5://127.0.0.1:1050,100 +--tx-proxy tor,socks5://127.0.0.1:1050,disable_noise +--tx-proxy tor,socks5://127.0.0.1:1050,100,disable_noise +--tx-proxy tor,socks5://username:password@127.0.0.1:1050 +--tx-proxy tor,socks5://username:password@127.0.0.1:1050,100 +--tx-proxy tor,socks5://username:password@127.0.0.1:1050,disable_noise +--tx-proxy tor,socks5://username:password@127.0.0.1:1050,100,disable_noise +--tx-proxy tor,[::1]:1050 +--tx-proxy tor,[::1]:1050,100 +--tx-proxy tor,[::1]:1050,disable_noise +--tx-proxy tor,[::1]:1050,100,disable_noise +--tx-proxy tor,socks5://[::1]:1050 +--tx-proxy tor,socks5://[::1]:1050,100 +--tx-proxy tor,socks5://[::1]:1050,disable_noise +--tx-proxy tor,socks5://[::1]:1050,100,disable_noise +--tx-proxy tor,socks5://username:password@[::1]:1050 +--tx-proxy tor,socks5://username:password@[::1]:1050,100 +--tx-proxy tor,socks5://username:password@[::1]:1050,disable_noise +--tx-proxy tor,socks5://username:password@[::1]:1050,100,disable_noise +--tx-proxy i2p,127.0.0.1:1050 +--tx-proxy i2p,127.0.0.1:1050,100 +--tx-proxy i2p,127.0.0.1:1050,disable_noise +--tx-proxy i2p,127.0.0.1:1050,100,disable_noise +--tx-proxy i2p,socks5://127.0.0.1:1050 +--tx-proxy i2p,socks5://127.0.0.1:1050,100 +--tx-proxy i2p,socks5://127.0.0.1:1050,disable_noise +--tx-proxy i2p,socks5://127.0.0.1:1050,100,disable_noise +--tx-proxy i2p,socks5://username:password@127.0.0.1:1050 +--tx-proxy i2p,socks5://username:password@127.0.0.1:1050,100 +--tx-proxy i2p,socks5://username:password@127.0.0.1:1050,disable_noise +--tx-proxy i2p,socks5://username:password@127.0.0.1:1050,100,disable_noise +--tx-proxy i2p,[::1]:1050 +--tx-proxy i2p,[::1]:1050,100 +--tx-proxy i2p,[::1]:1050,disable_noise +--tx-proxy i2p,[::1]:1050,100,disable_noise +--tx-proxy i2p,socks5://[::1]:1050 +--tx-proxy i2p,socks5://[::1]:1050,100 +--tx-proxy i2p,socks5://[::1]:1050,disable_noise +--tx-proxy i2p,socks5://[::1]:1050,100,disable_noise +--tx-proxy i2p,socks5://username:password@[::1]:1050 +--tx-proxy i2p,socks5://username:password@[::1]:1050,100 +--tx-proxy i2p,socks5://username:password@[::1]:1050,disable_noise +--tx-proxy i2p,socks5://username:password@[::1]:1050,100,disable_noise +``` + +The above examples are fairly exhaustive of all the possible option scenarios +that will be incurred by the typical user. + +#### The `network` portion of the option +The first section (before the first `,`) indicates the network - only `tor` or +`i2p` are valid here. + +This portion of the option tells `--add-node`, `--add-priority-node`, and +`--add-exclusive-node` options to use the specified proxy for those nodes. In +other words, command-line specified hidden services are forwarded to their +corresponding `--tx-proxy` server. Hidden services do **NOT** have to be +specified on the command-line, there are built-in seed nodes for each network. + +#### The `ip:port` portion of the option +The second portion of the option (after the first `,` and _optionally_ ending +in the next `,`) indicates the location of the socks server. The location +**must** include an IPv4/IPv6 AND port. The location can optionally include the +socks version - `socks4`, `socks4a`, and `socks5` are all valid here. If +the socks version is not specified, `socks4a` is assumed. + +An optional username and password can also be included. These fields support +percent-encoding, see [wallet](#wallet) section for more information. + +#### The last portion of the option +After the ip:port section two options can be specified: the number of max +connections and `disable_noise`. They can be specified in either order, but +must be after the ip:port section. + +The max connections does exactly as advertised, it limits the number of +outgoing connections to the proxy. The `disable_noise` feature lowers the +bandwidth requirements, and decreases the tx-relay time. When **NOT** +specified, dummy P2P packets are sent periodically to connections (via the +proxy) to conceal when a transaction is forwarded over the connection. When +the option is specified, P2P links only send data for peerlist information and +local outgoing transactions. + +### `--anonymous-inbound` +Currently the daemon cannot configure incoming hidden services connections. +Instead, the user must manually configure Tor or I2P to accept inbound +connections. Then, `--anonymous-inbound` must be used to tell the daemon where +to listen for incoming connections, and the incoming hidden service address. +The option can be specified once for each network type. The format for usage +is: `hidden-service-address,[bind-ip:]port[,max_connections]`. Examples: + +``` +--tx-proxy rveahdfho7wo4b2m.onion:18083,18083 +--tx-proxy rveahdfho7wo4b2m.onion:18083,18083,100 +--tx-proxy rveahdfho7wo4b2m.onion:18083,127.0.0.1:18083 +--tx-proxy rveahdfho7wo4b2m.onion:18083,127.0.0.1:18083,100 +--tx-proxy udhdrtrcetjm5sxzskjyr5ztpeszydbh4dpl3pl4utgqqw2v4jna.b32.i2p,18083 +--tx-proxy udhdrtrcetjm5sxzskjyr5ztpeszydbh4dpl3pl4utgqqw2v4jna.b32.i2p,18083,100 +--tx-proxy udhdrtrcetjm5sxzskjyr5ztpeszydbh4dpl3pl4utgqqw2v4jna.b32.i2p,127.0.0.1:18083 +--tx-proxy udhdrtrcetjm5sxzskjyr5ztpeszydbh4dpl3pl4utgqqw2v4jna.b32.i2p,127.0.0.1:18083,100 +``` + +Everything before the first `,` is the hidden service hostname. This must be +a valid Tor or I2P address. This tells the daemon the **inbound** hidden +service as configured for the local Tor or I2P daemons. + +Everything between `,`s specify the bind ip and bind port. The IP address is +optional, and defaults to `127.0.0.1`. The Tor and I2P daemons must be +configured to forward incoming hidden service connections to this IP/Port pair. + +Everything after the second `,` is used to specify the number of max inbound +connections. The field is optional. diff --git a/src/daemon/command_line_args.h b/src/daemon/command_line_args.h index 2b8b2818604..60abdc00cb7 100644 --- a/src/daemon/command_line_args.h +++ b/src/daemon/command_line_args.h @@ -94,7 +94,7 @@ namespace daemon_args const command_line::arg_descriptor arg_proxy = { "proxy", - "Network communication through proxy: i.e. \"127.0.0.1:9050\"", + "Network communication through proxy: [socks5://[user:pass@]] i.e. \"127.0.0.1:9050\"", "", }; const command_line::arg_descriptor arg_proxy_allow_dns_leaks = { diff --git a/src/net/error.cpp b/src/net/error.cpp index 7570aba502e..f59bb520be0 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 56e7e074ecf..1f1380a731d 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 ee6a14aae25..2fff1628469 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 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 b7d7adcacce..5e6425d04e1 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(std::move(*endpoint))} + ); } } diff --git a/src/net/parse.cpp b/src/net/parse.cpp index 6d580f6889c..94a8f7e8330 100644 --- a/src/net/parse.cpp +++ b/src/net/parse.cpp @@ -29,6 +29,9 @@ #include "parse.h" +#include +#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({reinterpret_cast(out.data()) + pos, 1}, {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()); + + // 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()); + + 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()); + } + + std::optional user_and_pass::get(boost::string_ref userinfo) + { + static_assert(std::is_same()); + std::optional out{std::in_place}; + + 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 std::nullopt; + } + + std::optional 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 std::nullopt; + + std::optional out{std::in_place}; + 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::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.user.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 9795cdf14ce..c2a004618c3 100644 --- a/src/net/parse.h +++ b/src/net/parse.h @@ -32,12 +32,73 @@ #include #include #include +#include #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. `std::nullopt` + * if bad percent encoding + */ + static std::optional 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. `std::nullopt` if + * bad percent encoding. + */ + static std::optional 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,22 @@ namespace net get_ipv4_subnet_address(boost::string_ref address, bool allow_implicit_32 = false); expect 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 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 734537b446b..052d7c0df08 100644 --- a/src/net/socks.cpp +++ b/src/net/socks.cpp @@ -30,14 +30,17 @@ #include #include +#include #include #include #include #include #include #include +#include #include +#include "net/parse.h" #include "net/net_utils_base.h" #include "net/tor_address.h" #include "net/i2p_address.h" @@ -52,6 +55,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; @@ -60,6 +73,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 out, const std::uint8_t command, const std::uint16_t port, const boost::string_ref domain) { if (std::numeric_limits::max() - sizeof(v4_header) - 2 < domain.size()) @@ -84,6 +205,131 @@ namespace socks return buf_size; } + std::size_t write_v5_userpass(epee::span out, const user_and_pass& userinfo) + { + static constexpr const std::uint8_t max_length = std::numeric_limits::max(); + if (max_length < userinfo.user.size()) + return 0; + if (max_length < userinfo.pass.size()) + return 0; + + static_assert(max_length < std::numeric_limits::max()); + static_assert(max_length < std::numeric_limits::max() - max_length); + static_assert(2 <= std::numeric_limits::max() - max_length - max_length); + + if (out.size() < 2 + 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 write_v5_initial(epee::span out, const user_and_pass* userinfo) + { + std::array 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 + std::array write_v5_address_connect(epee::span out, const T& address, const user_and_pass* userinfo) + { + std::array 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 write_v5_domain_connect(epee::span out, const std::uint16_t port, const boost::string_ref domain, const user_and_pass* userinfo) + { + std::array sizes{{}}; + if (std::numeric_limits::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::max() - sizeof(request)); + if (std::numeric_limits::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 @@ -99,6 +345,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: @@ -106,6 +369,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: @@ -123,6 +388,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; @@ -156,18 +425,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_); } } }; @@ -179,6 +448,7 @@ namespace socks static boost::asio::mutable_buffers_1 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)); } @@ -188,22 +458,188 @@ 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), self.strand_.wrap(completed{std::move(self_)})); } } }; + struct client::process_v5 : boost::asio::coroutine + { + std::shared_ptr self_; + + struct read_bytes + { + std::uint8_t* data; + std::size_t length; + + boost::asio::mutable_buffers_1 to_asio() const noexcept + { + return boost::asio::buffer(data, length); + } + }; + + explicit process_v5(std::shared_ptr self) + : boost::asio::coroutine(), self_(std::move(self)) + {} + + static read_bytes 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 {self.buffer_ + offset, size}; + } + + template + static boost::asio::const_buffers_1 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(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)).to_asio(), + self.strand_.wrap(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), self.strand_.wrap(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)).to_asio(), + self.strand_.wrap(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), self.strand_.wrap(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)).to_asio(), + self.strand_.wrap(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).to_asio(), self.strand_.wrap(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 self_; static boost::asio::const_buffers_1 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) @@ -212,20 +648,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), self.strand_.wrap(process_v5{std::move(self_)})); else boost::asio::async_write(self.proxy_, get_buffer(self), self.strand_.wrap(read{std::move(self_)})); } } }; + + client::client(stream_type::socket&& proxy, socks::version ver) - : proxy_(std::move(proxy)), strand_(GET_IO_SERVICE(proxy_)), buffer_size_(0), buffer_(), ver_(ver) + : proxy_(std::move(proxy)), strand_(GET_IO_SERVICE(proxy_)), 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()) { @@ -233,6 +673,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; } @@ -240,29 +687,50 @@ 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; } @@ -273,10 +741,10 @@ namespace socks 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; } @@ -286,13 +754,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 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, alias.strand_.wrap(write{std::move(self)})); @@ -303,10 +772,13 @@ namespace socks bool client::send(std::shared_ptr 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), alias.strand_.wrap(read{std::move(self)})); + if (alias.ver_ == version::v5) + boost::asio::async_write(alias.proxy_, write::get_buffer(alias), alias.strand_.wrap(process_v5{std::move(self)})); + else + boost::asio::async_write(alias.proxy_, write::get_buffer(alias), alias.strand_.wrap(read{std::move(self)})); return true; } return false; diff --git a/src/net/socks.h b/src/net/socks.h index 35273057658..47a216fd783 100644 --- a/src/net/socks.h +++ b/src/net/socks.h @@ -28,6 +28,7 @@ #pragma once +#include #include #include #include @@ -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::io_service::strand strand_; - std::uint16_t buffer_size_; + std::array 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` handle to `this`. */ - virtual void done(boost::system::error_code error, std::shared_ptr self) = 0; + virtual void done(boost::system::error_code error, const std::shared_ptr& 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,45 @@ 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 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); //! 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 +207,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 +241,7 @@ namespace socks { Handler handler_; - virtual void done(boost::system::error_code error, std::shared_ptr) override + virtual void done(boost::system::error_code error, const std::shared_ptr&) override { handler_(error, take_socket()); } diff --git a/src/net/socks_connect.cpp b/src/net/socks_connect.cpp index febcd218bbc..42536b626d2 100644 --- a/src/net/socks_connect.cpp +++ b/src/net/socks_connect.cpp @@ -36,6 +36,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 +45,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::address_v6::from_string(source, error); + return !error; + } + } // anonymous + boost::unique_future connector::operator()(const std::string& remote_host, const std::string& remote_port, boost::asio::steady_timer& timeout) const { + if (!proxy_address) + std::runtime_error{"Unexpected nullptr of net::socks::endpoint"}; + struct future_socket { boost::promise result_; @@ -68,18 +82,21 @@ namespace socks bool is_set = false; std::uint32_t ip_address = 0; + boost::asio::ip::address_v6 v6_address{}; boost::promise result{}; out = result.get_future(); const auto proxy = net::socks::make_connect_client( - boost::asio::ip::tcp::socket{GET_IO_SERVICE(timeout)}, net::socks::version::v4a, future_socket{std::move(result)} + boost::asio::ip::tcp::socket{GET_IO_SERVICE(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 2ef07a47859..8df7cdf4455 100644 --- a/src/net/socks_connect.h +++ b/src/net/socks_connect.h @@ -31,8 +31,11 @@ #include #include #include +#include #include +#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 proxy_address; /*! Creates a new socket, asynchronously connects to `proxy_address`, and requests a connection to `remote_host` on `remote_port`. Sets diff --git a/src/p2p/net_node.cpp b/src/p2p/net_node.cpp index d6681238ffd..95c64cc2de0 100644 --- a/src/p2p/net_node.cpp +++ b/src/p2p/net_node.cpp @@ -81,7 +81,7 @@ namespace return {std::move(*address)}; } - bool start_socks(std::shared_ptr client, const boost::asio::ip::tcp::endpoint& proxy, const epee::net_utils::network_address& remote) + bool start_socks(std::shared_ptr client, const net::socks::endpoint& proxy, const epee::net_utils::network_address& remote) { CHECK_AND_ASSERT_MES(client != nullptr, false, "Unexpected null client"); @@ -92,18 +92,25 @@ namespace set = client->set_connect_command(remote.as()); break; case net::i2p_address::get_type_id(): - set = client->set_connect_command(remote.as()); + set = client->set_connect_command(remote.as(), std::addressof(proxy.userinfo)); break; case epee::net_utils::ipv4_network_address::get_type_id(): - set = client->set_connect_command(remote.as()); + set = client->set_connect_command(remote.as(), std::addressof(proxy.userinfo)); break; + case epee::net_utils::ipv6_network_address::get_type_id(): + if (client->socks_version() == net::socks::version::v5) + { + set = client->set_connect_command(remote.as(), std::addressof(proxy.userinfo)); + break; + } + /* fallthrough */ default: - MERROR("Unsupported network address in socks_connect"); + MERROR("Unsupported network address in socks_connect. Try socks5://"); return false; } const bool sent = - set && net::socks::client::connect_and_send(std::move(client), proxy); + set && net::socks::client::connect_and_send(std::move(client), proxy.address); CHECK_AND_ASSERT_MES(sent, false, "Unexpected failure to init socks client"); return true; } @@ -147,7 +154,7 @@ namespace nodetool const command_line::arg_descriptor > arg_p2p_add_exclusive_node = {"add-exclusive-node", "Specify list of peers to connect to only." " If this option is given the options add-priority-node and seed-node are ignored"}; const command_line::arg_descriptor > arg_p2p_seed_node = {"seed-node", "Connect to a node to retrieve peer addresses, and disconnect"}; - const command_line::arg_descriptor > arg_tx_proxy = {"tx-proxy", "Send local txes through proxy: ,[,max_connections][,disable_noise] i.e. \"tor,127.0.0.1:9050,100,disable_noise\""}; + const command_line::arg_descriptor > arg_tx_proxy = {"tx-proxy", "Send local txes through proxy: ,[socks5://[user:pass@]][,max_connections][,disable_noise] i.e. \"tor,127.0.0.1:9050,100,disable_noise\""}; const command_line::arg_descriptor > arg_anonymous_inbound = {"anonymous-inbound", ",<[bind-ip:]port>[,max_connections] i.e. \"x.onion,127.0.0.1:18083,100\""}; const command_line::arg_descriptor arg_ban_list = {"ban-list", "Specify ban list file, one IP address per line"}; const command_line::arg_descriptor arg_p2p_hide_my_port = {"hide-my-port", "Do not announce yourself as peerlist candidate", false, true}; @@ -189,7 +196,7 @@ namespace nodetool const boost::string_ref zone{next->begin(), next->size()}; ++next; - CHECK_AND_ASSERT_MES(!next.eof() && !next->empty(), boost::none, "No ipv4:port given for --" << arg_tx_proxy.name); + CHECK_AND_ASSERT_MES(!next.eof() && !next->empty(), boost::none, "No ip:port given for --" << arg_tx_proxy.name); const boost::string_ref proxy{next->begin(), next->size()}; ++next; @@ -227,14 +234,14 @@ namespace nodetool return boost::none; } - std::uint32_t ip = 0; - std::uint16_t port = 0; - if (!epee::string_tools::parse_peer_from_string(ip, port, std::string{proxy}) || port == 0) + auto endpoint = net::socks::endpoint::get(proxy); + if (!endpoint) { - MERROR("Invalid ipv4:port given for --" << arg_tx_proxy.name); + MERROR("Invalid --" << arg_tx_proxy.name << " value: " << endpoint.error().message()); return boost::none; } - proxies.back().address = ip::tcp::endpoint{ip::address_v4{boost::endian::native_to_big(ip)}, port}; + + proxies.back().address = std::move(*endpoint); } return proxies; @@ -327,7 +334,7 @@ namespace nodetool } boost::optional - socks_connect_internal(const std::atomic& stop_signal, boost::asio::io_service& service, const boost::asio::ip::tcp::endpoint& proxy, const epee::net_utils::network_address& remote) + socks_connect_internal(const std::atomic& stop_signal, boost::asio::io_service& service, const net::socks::endpoint& proxy, const epee::net_utils::network_address& remote) { using socket_type = net::socks::client::stream_type::socket; using client_result = std::pair; @@ -349,7 +356,7 @@ namespace nodetool socks_result = socks_promise.get_future(); auto client = net::socks::make_connect_client( - boost::asio::ip::tcp::socket{service}, net::socks::version::v4a, notify{std::move(socks_promise)} + boost::asio::ip::tcp::socket{service}, proxy.ver, notify{std::move(socks_promise)} ); close_client.self = client; if (!start_socks(std::move(client), proxy, remote)) @@ -361,7 +368,7 @@ namespace nodetool { if (socks_connect_timeout < std::chrono::steady_clock::now() - start) { - MERROR("Timeout on socks connect (" << proxy << " to " << remote.str() << ")"); + MERROR("Timeout on socks connect (" << proxy.address << " to " << remote.str() << ")"); return boost::none; } @@ -378,7 +385,7 @@ namespace nodetool return {std::move(result.second)}; } - MERROR("Failed to make socks connection to " << remote.str() << " (via " << proxy << "): " << result.first.message()); + MERROR("Failed to make socks connection to " << remote.str() << " (via " << proxy.address << "): " << result.first.message()); } catch (boost::broken_promise const&) {} diff --git a/src/p2p/net_node.h b/src/p2p/net_node.h index 7b3477e1f7d..f1d95eef8d1 100644 --- a/src/p2p/net_node.h +++ b/src/p2p/net_node.h @@ -55,7 +55,7 @@ #include "math_helper.h" #include "net_node_common.h" #include "net/enums.h" -#include "net/fwd.h" +#include "net/parse.h" #include "common/command_line.h" PUSH_WARNINGS @@ -73,7 +73,7 @@ namespace nodetool {} std::int64_t max_connections; - boost::asio::ip::tcp::endpoint address; + net::socks::endpoint address; epee::net_utils::zone zone; bool noise; }; @@ -103,7 +103,7 @@ namespace nodetool // hides boost::future and chrono stuff from mondo template file boost::optional - socks_connect_internal(const std::atomic& stop_signal, boost::asio::io_service& service, const boost::asio::ip::tcp::endpoint& proxy, const epee::net_utils::network_address& remote); + socks_connect_internal(const std::atomic& stop_signal, boost::asio::io_service& service, const net::socks::endpoint& proxy, const epee::net_utils::network_address& remote); template @@ -212,7 +212,7 @@ namespace nodetool epee::net_utils::network_address m_our_address; // in anonymity networks peerlist_manager m_peerlist; config m_config; - boost::asio::ip::tcp::endpoint m_proxy_address; + net::socks::endpoint m_proxy_address; std::atomic m_current_number_of_out_peers; std::atomic m_current_number_of_in_peers; boost::shared_mutex m_seed_nodes_lock; diff --git a/src/p2p/net_node.inl b/src/p2p/net_node.inl index 662e598e8a1..926074d65b3 100644 --- a/src/p2p/net_node.inl +++ b/src/p2p/net_node.inl @@ -900,8 +900,8 @@ namespace nodetool CHECK_AND_ASSERT_MES(res, false, "Failed to handle command line"); if (proxy.size()) { - const auto endpoint = net::get_tcp_endpoint(proxy); - CHECK_AND_ASSERT_MES(endpoint, false, "Failed to parse proxy: " << proxy << " - " << endpoint.error()); + const auto endpoint = net::socks::endpoint::get(proxy); + CHECK_AND_ASSERT_MES(endpoint, false, "Failed to parse proxy: " << proxy << " - " << endpoint.error().message()); network_zone& public_zone = m_network_zones[epee::net_utils::zone::public_]; public_zone.m_connect = &socks_connect; public_zone.m_proxy_address = *endpoint; diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 0f5aaae69dd..b7f731ae6a1 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -435,7 +435,7 @@ std::unique_ptr make_basic(const boost::program_options::variabl { proxy = command_line::get_arg(vm, opts.proxy); THROW_WALLET_EXCEPTION_IF( - !net::get_tcp_endpoint(proxy), + !net::socks::endpoint::get(proxy), tools::error::wallet_internal_error, std::string{"Invalid address specified for --"} + opts.proxy.name); } diff --git a/tests/unit_tests/net.cpp b/tests/unit_tests/net.cpp index abdd2558d7f..0144e77e4bf 100644 --- a/tests/unit_tests/net.cpp +++ b/tests/unit_tests/net.cpp @@ -883,6 +883,231 @@ TEST(get_network_address_host_and_port, hostname) na_host_and_port_test("xmrchain.net:18081", "xmrchain.net", "18081"); } +TEST(scheme_and_authority, basic) +{ + const auto check = [] (const net::scheme_and_authority& actual, const boost::string_ref scheme, const boost::string_ref authority) + { + EXPECT_EQ(actual.scheme, scheme); + EXPECT_EQ(actual.authority, authority); + }; + + // valid (some ipv6 hostnames are non-standard but allowed) + check(net::scheme_and_authority{"socks://host:port/path"}, "socks", "host:port"); + check(net::scheme_and_authority{"socks://[::ffff]:8080/path"}, "socks", "[::ffff]:8080"); + check(net::scheme_and_authority{"socks://192.168.0.1/path"}, "socks", "192.168.0.1"); + check(net::scheme_and_authority{"socks://host"}, "socks", "host"); + check(net::scheme_and_authority{"socks://@host"}, "socks", "@host"); + check(net::scheme_and_authority{"socks://user:pass@host"}, "socks", "user:pass@host"); + check(net::scheme_and_authority{"host:port/path"}, "", "host:port"); + check(net::scheme_and_authority{"[::ffff]:8080/path"}, "", "[::ffff]:8080"); + check(net::scheme_and_authority{"192.168.0.1/path"}, "", "192.168.0.1"); + check(net::scheme_and_authority{"host"}, "", "host"); + check(net::scheme_and_authority{"192.168.0.1"}, "", "192.168.0.1"); + check(net::scheme_and_authority{"192.168.0.1:80/path"}, "", "192.168.0.1:80"); + check(net::scheme_and_authority{"::ffff"}, "", "::ffff"); + check(net::scheme_and_authority{"[::ffff]:8080"}, "", "[::ffff]:8080"); + check(net::scheme_and_authority{"example.com/some://valid/path"}, "", "example.com"); + + // unsupported URIs (URN cases) + check(net::scheme_and_authority{"urn:isbn:number"}, "", "urn:isbn:number"); + check(net::scheme_and_authority{"urn:isbn/number"}, "", "urn:isbn"); + + // invalid cases _not_ strictly rejected until hostname is parsed fully + check(net::scheme_and_authority{""}, "", ""); + check(net::scheme_and_authority{"socks://"}, "socks", ""); + check(net::scheme_and_authority{"socks:/"}, "", "socks:"); + check(net::scheme_and_authority{"192.168.0.1:80://"}, "", "192.168.0.1:80:"); + check(net::scheme_and_authority{"user@::ffff:443"}, "", "user@::ffff:443"); + check(net::scheme_and_authority{"socks://user:p%61ss@ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff:8080"}, "socks", "user:p%61ss@ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff:8080"); +} + + +TEST(userinfo_and_hostport, basic) +{ + const auto check = [] (const net::userinfo_and_hostport& actual, const boost::string_ref userinfo, const boost::string_ref hostport) + { + EXPECT_EQ(actual.userinfo, userinfo); + EXPECT_EQ(actual.hostport, hostport); + }; + + check(net::userinfo_and_hostport{"@host:port"}, "", "host:port"); + check(net::userinfo_and_hostport{"host:port"}, "", "host:port"); + check(net::userinfo_and_hostport{"[::ffff]"}, "", "[::ffff]"); + check(net::userinfo_and_hostport{"::ffff"}, "", "::ffff"); + check(net::userinfo_and_hostport{"user@[::ffff]"}, "user", "[::ffff]"); + check(net::userinfo_and_hostport{"user:%70ass@192.168.0.1"}, "user:%70ass", "192.168.0.1"); + check(net::userinfo_and_hostport{"user:pass@host:8080"}, "user:pass", "host:8080"); + + // invalid hostname not strictly rejected + check(net::userinfo_and_hostport{""}, "", ""); + check(net::userinfo_and_hostport{"@"}, "", ""); + check(net::userinfo_and_hostport{":@"}, ":", ""); + check(net::userinfo_and_hostport{"user@::ffff:443"}, "user", "::ffff:443"); +} + +TEST(user_and_pass, basic) +{ + const auto check = [] ( + const std::optional& actual, + const std::optional& user = std::nullopt, + const boost::string_ref pass = "") + { + ASSERT_EQ(bool(actual), bool(user)); + if (actual) + { + EXPECT_EQ(actual->user, *user); + EXPECT_EQ(actual->pass, pass); + } + }; + + check(net::user_and_pass::get(""), std::string{""}, ""); + check(net::user_and_pass::get("user"), std::string{"user"}, ""); + check(net::user_and_pass::get("user:"), std::string{"user"}, ""); + check(net::user_and_pass::get(":pass"), std::string{}, "pass"); + check(net::user_and_pass::get("user:pass"), std::string("user"), "pass"); + check(net::user_and_pass::get("user:p%3Ass"), std::string("user"), "p:ss"); + check(net::user_and_pass::get("%2fser:"), std::string{"/ser"}, ""); + check(net::user_and_pass::get("user:pas%21"), std::string{"user"}, "pas!"); + check(net::user_and_pass::get("user::pass"), std::string{"user"}, ":pass"); + check(net::user_and_pass::get("user%3A:pass"), std::string{"user:"}, "pass"); + check(net::user_and_pass::get("%25%3A:pass"), std::string{"%:"}, "pass"); + check(net::user_and_pass::get("user:%00%FF"), std::string{"user"}, boost::string_ref{"\x00\xFF", 2}); + + // invalid percent encodings + check(net::user_and_pass::get("user%3T")); + check(net::user_and_pass::get("user%T")); + check(net::user_and_pass::get("user%3")); + check(net::user_and_pass::get("user%")); +} + +TEST(uri_components, get) +{ + const auto check = [] ( + const std::optional& actual, + const std::optional& scheme = std::nullopt, + const boost::string_ref user = "", + const boost::string_ref pass = "", + const boost::string_ref hostport = "") + { + ASSERT_EQ(bool(actual), bool(scheme)); + if (actual) + { + EXPECT_EQ(actual->scheme, *scheme); + EXPECT_EQ(actual->userinfo.user, user); + EXPECT_EQ(actual->userinfo.pass, pass); + EXPECT_EQ(actual->hostport, hostport); + } + }; + + // valid (some ipv6 hostnames are non-standard but allowed) + check(net::uri_components::get("socks://host:port/path"), std::string{"socks"}, "", "", "host:port"); + check(net::uri_components::get("socks://[::ffff]:8080/path"), std::string{"socks"}, "", "", "[::ffff]:8080"); + check(net::uri_components::get("socks://192.168.0.1/path"), "socks", "", "", "192.168.0.1"); + check(net::uri_components::get("socks://host"), "socks", "", "", "host"); + check(net::uri_components::get("socks://@host"), std::string{"socks"}, "", "", "host"); + check(net::uri_components::get("socks://:@host"), std::string{"socks"}, "", "", "host"); + check(net::uri_components::get("socks://user:@host"), std::string{"socks"}, "user", "", "host"); + check(net::uri_components::get("socks://:pass@host"), std::string{"socks"}, "", "pass", "host"); + check(net::uri_components::get("socks://user:pass@host"), std::string{"socks"}, "user", "pass", "host"); + check(net::uri_components::get("host:port/path"), "", "", "", "host:port"); + check(net::uri_components::get("[::ffff]:8080/path"), "", "", "", "[::ffff]:8080"); + check(net::uri_components::get("192.168.0.1/path"), "", "", "", "192.168.0.1"); + check(net::uri_components::get("host"), "", "", "", "host"); + check(net::uri_components::get("192.168.0.1"), "", "", "", "192.168.0.1"); + check(net::uri_components::get("192.168.0.1:80/path"), "", "", "", "192.168.0.1:80"); + check(net::uri_components::get("::ffff"), std::string{}, "", "", "::ffff"); + check(net::uri_components::get("[::ffff]:8080"), std::string{}, "", "", "[::ffff]:8080"); + check(net::uri_components::get("example.com/some://valid/path"), "", "", "", "example.com"); + + // unsupported URIs (URN cases) + check(net::uri_components::get("urn:isbn:number"), std::string{}, "", "", "urn:isbn:number"); + check(net::uri_components::get("urn:isbn/number"), std::string{}, "", "", "urn:isbn"); + + // invalid cases _not_ strictly rejected until hostname is parsed fully + check(net::uri_components::get(""), std::string{}, "", "", ""); + check(net::uri_components::get("socks://"), std::string{"socks"}, "", "", ""); + check(net::uri_components::get("socks:/"), std::string{}, "", "", "socks:"); + check(net::uri_components::get("192.168.0.1:80://"), std::string{}, "", "", "192.168.0.1:80:"); + check(net::uri_components::get("user@::ffff:443"), std::string{}, "user", "", "::ffff:443"); + check(net::uri_components::get("socks://user:p%61ss@ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff:8080"), std::string{"socks"}, "user", "pass", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff:8080"); + + // invalid percent encodings + check(net::uri_components::get("scheme://user%3T@host")); + check(net::uri_components::get("user%T@host")); + check(net::uri_components::get("scheme://user:%3pass@host")); + check(net::uri_components::get("user%@host")); +} + +TEST(socks_endpoint, get) +{ + using v4_address = boost::asio::ip::address_v4; + using v6_address = boost::asio::ip::address_v6; + const auto check = [] ( + const expect& actual, + const std::optional& address = std::nullopt, + const std::uint16_t port = 0, + const boost::string_ref user = "", + const boost::string_ref pass = "", + const net::socks::version ver = net::socks::version::v4a) + { + ASSERT_EQ(bool(actual), bool(address)) << actual.error().message(); + if (actual) + { + EXPECT_EQ(actual->address.address(), address); + EXPECT_EQ(actual->address.port(), port); + EXPECT_EQ(actual->userinfo.user, user); + EXPECT_EQ(actual->userinfo.pass, pass); + EXPECT_EQ(actual->ver, ver); + } + }; + + check(net::socks::endpoint::get("socks://[::ffff]:8080/path"), v6_address::from_string("::ffff"), 8080, "", "", net::socks::version::v4a); + check(net::socks::endpoint::get("socks5://user:%70ass@[::ffff]:8080/path"), v6_address::from_string("::ffff"), 8080, "user", "pass", net::socks::version::v5); + check(net::socks::endpoint::get("socks4a://192.168.0.1:1/path"), v4_address::from_string("192.168.0.1"), 1, "", "", net::socks::version::v4a); + check(net::socks::endpoint::get("socks5://%75@192.168.0.1:1/path"), v4_address::from_string("192.168.0.1"), 1, "u", "", net::socks::version::v5); + check(net::socks::endpoint::get("[::ffff]:8080/path"), v6_address::from_string("::ffff"), 8080, "", "", net::socks::version::v4a); + check(net::socks::endpoint::get("192.168.0.1:50"), v4_address::from_string("192.168.0.1"), 50, "", "", net::socks::version::v4a); + check(net::socks::endpoint::get("192.168.0.1:80/path"), v4_address::from_string("192.168.0.1"), 80, "", "", net::socks::version::v4a); + + // URNs should be rejected + check(net::socks::endpoint::get("urn:isbn:number")); + check(net::socks::endpoint::get("urn:isbn/number")); + + // port required for socks + check(net::socks::endpoint::get("socks5://192.168.0.1/path")); + check(net::socks::endpoint::get("192.168.0.1/path")); + check(net::socks::endpoint::get("::ffff")); + + // invalid for socks - hostnames not allowed + check(net::socks::endpoint::get("socks://host:/path")); + check(net::socks::endpoint::get("socks://host:1")); + check(net::socks::endpoint::get("socks://@host:1")); + check(net::socks::endpoint::get("socks://:@host:1")); + check(net::socks::endpoint::get("socks://user:@host:1")); + check(net::socks::endpoint::get("socks://:pass@host:1")); + check(net::socks::endpoint::get("socks://user:pass@host:1")); + check(net::socks::endpoint::get("host:1")); + check(net::socks::endpoint::get("host:1/path")); + check(net::socks::endpoint::get("example.com:1/some://valid/path")); + + // invalid cases rejected - more bad hostnames + check(net::socks::endpoint::get("")); + check(net::socks::endpoint::get("socks://")); + check(net::socks::endpoint::get("socks:/")); + check(net::socks::endpoint::get("192.168.0.1:80://")); + check(net::socks::endpoint::get("::ffff:443")); + check(net::socks::endpoint::get("socks5://user:p%61ss@ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff:8080")); + + // invalid percent encodings + check(net::socks::endpoint::get("sock5://user%3T@127.0.0.1:1")); + check(net::socks::endpoint::get("socks5::user%T@127.0.0.1:1")); + check(net::socks::endpoint::get("socks5://user:%3pass@127.0.0.1:1")); + check(net::socks::endpoint::get("socks5://user%@127.0.0.1:1")); + + // user+pass requires socks5 + check(net::socks::endpoint::get("socks://user:pass@[::ffff]:8080")); +} + namespace { using stream_type = boost::asio::ip::tcp; @@ -952,6 +1177,58 @@ TEST(socks_client, unsupported_command) EXPECT_FALSE(test_client->set_resolve_command("example.com")); EXPECT_TRUE(test_client->buffer().empty()); + + EXPECT_FALSE(test_client->set_connect_command(epee::net_utils::ipv6_network_address{})); + EXPECT_TRUE(test_client->buffer().empty()); + + test_client = net::socks::make_connect_client( + stream_type::socket{io_service}, net::socks::version::v5, std::bind( [] {} ) + ); + ASSERT_TRUE(bool(test_client)); + EXPECT_TRUE(test_client->buffer().empty()); + + net::user_and_pass userinfo{}; + userinfo.user = std::string(256, 'a'); + + EXPECT_FALSE(test_client->set_connect_command(userinfo.user, 8080)); + EXPECT_TRUE(test_client->buffer().empty()); + + EXPECT_FALSE(test_client->set_connect_command("a", 8080, std::addressof(userinfo))); + EXPECT_TRUE(test_client->buffer().empty()); + + EXPECT_FALSE( + test_client->set_connect_command( + epee::net_utils::ipv4_network_address{}, std::addressof(userinfo) + ) + ); + EXPECT_TRUE(test_client->buffer().empty()); + + EXPECT_FALSE( + test_client->set_connect_command( + epee::net_utils::ipv6_network_address{}, std::addressof(userinfo) + ) + ); + EXPECT_TRUE(test_client->buffer().empty()); + + userinfo.pass = std::move(userinfo.user); + userinfo.user.clear(); + + EXPECT_FALSE(test_client->set_connect_command("a", 8080, std::addressof(userinfo))); + EXPECT_TRUE(test_client->buffer().empty()); + + EXPECT_FALSE( + test_client->set_connect_command( + epee::net_utils::ipv4_network_address{}, std::addressof(userinfo) + ) + ); + EXPECT_TRUE(test_client->buffer().empty()); + + EXPECT_FALSE( + test_client->set_connect_command( + epee::net_utils::ipv6_network_address{}, std::addressof(userinfo) + ) + ); + EXPECT_TRUE(test_client->buffer().empty()); } TEST(socks_client, no_command) @@ -999,6 +1276,62 @@ TEST(socks_client, connect_command) while (!called); } +TEST(socks_client, v5_ipv6_connect_command) +{ + io_thread io{}; + stream_type::socket client{io.io_service}; + + const boost::asio::ip::address_v6::bytes_type address{0xDE, 0xAD, 0xBE, 0xEF}; + + std::atomic called{false}; + auto test_client = net::socks::make_connect_client( + std::move(client), net::socks::version::v5, checked_client{std::addressof(called), false} + ); + ASSERT_TRUE(bool(test_client)); + + ASSERT_TRUE( + test_client->set_connect_command( + epee::net_utils::ipv6_network_address{boost::asio::ip::address_v6{address}, 80} + ) + ); + EXPECT_FALSE(test_client->buffer().empty()); + ASSERT_TRUE(net::socks::client::connect_and_send(std::move(test_client), io.acceptor.local_endpoint())); + while (!io.connected) + ASSERT_FALSE(called); + + { + const std::uint8_t expected_bytes[] = {5, 1, 0}; + + std::uint8_t actual_bytes[sizeof(expected_bytes)]; + boost::asio::read(io.server, boost::asio::buffer(actual_bytes)); + EXPECT_TRUE(std::memcmp(expected_bytes, actual_bytes, sizeof(actual_bytes)) == 0); + + const std::uint8_t reply_bytes[] = {5, 0}; + boost::asio::write(io.server, boost::asio::buffer(reply_bytes)); + } + { + const std::uint8_t expected_bytes[] = { + 5, 1, 0, 4, 0xDE, 0xAD, 0xBE, 0xEF, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0x50 + }; + + std::uint8_t actual_bytes[sizeof(expected_bytes)]; + boost::asio::read(io.server, boost::asio::buffer(actual_bytes)); + EXPECT_TRUE(std::memcmp(expected_bytes, actual_bytes, sizeof(actual_bytes)) == 0); + + const std::uint8_t reply_bytes[] = { + 5, 0, 0, 4, 0xBE, 0xEF, 0xDE, 0xAD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0x50 + }; + + boost::asio::write(io.server, boost::asio::buffer(reply_bytes)); + } + + // yikes! + while (!called); +} + + TEST(socks_client, connect_command_failed) { io_thread io{}; @@ -1035,6 +1368,56 @@ TEST(socks_client, connect_command_failed) while (!called); } + +TEST(socks_client, v5_ipv4_connect_command_failed) +{ + io_thread io{}; + stream_type::socket client{io.io_service}; + + std::atomic called{false}; + auto test_client = net::socks::make_connect_client( + std::move(client), net::socks::version::v5, checked_client{std::addressof(called), true} + ); + ASSERT_TRUE(bool(test_client)); + + ASSERT_TRUE( + test_client->set_connect_command( + epee::net_utils::ipv4_network_address{boost::endian::native_to_big(std::uint32_t(5000)), 80} + ) + ); + EXPECT_FALSE(test_client->buffer().empty()); + ASSERT_TRUE(net::socks::client::connect_and_send(std::move(test_client), io.acceptor.local_endpoint())); + while (!io.connected) + ASSERT_FALSE(called); + + { + const std::uint8_t expected_bytes[] = {5, 1, 0}; + + std::uint8_t actual_bytes[sizeof(expected_bytes)]; + boost::asio::read(io.server, boost::asio::buffer(actual_bytes)); + EXPECT_TRUE(std::memcmp(expected_bytes, actual_bytes, sizeof(actual_bytes)) == 0); + + const std::uint8_t reply_bytes[] = {5, 0}; + boost::asio::write(io.server, boost::asio::buffer(reply_bytes)); + } + { + const std::uint8_t expected_bytes[] = { + 5, 1, 0, 1, 0, 0, 0x13, 0x88, 0, 0x50 + }; + + std::uint8_t actual_bytes[sizeof(expected_bytes)]; + boost::asio::read(io.server, boost::asio::buffer(actual_bytes)); + EXPECT_TRUE(std::memcmp(expected_bytes, actual_bytes, sizeof(actual_bytes)) == 0); + + const std::uint8_t reply_bytes[] = {5, 2, 0, 1, 0, 0, 0, 0, 0, 0x50}; + + boost::asio::write(io.server, boost::asio::buffer(reply_bytes)); + } + + // yikes! + while (!called); +} + TEST(socks_client, resolve_command) { static std::uint8_t reply_bytes[] = {0, 90, 0, 0, 0xff, 0, 0xad, 0}; @@ -1050,7 +1433,7 @@ TEST(socks_client, resolve_command) , expected_(false) {}; - virtual void done(boost::system::error_code error, std::shared_ptr self) override + virtual void done(boost::system::error_code error, const std::shared_ptr& self) override { EXPECT_EQ(this, self.get()); EXPECT_EQ(expected_, bool(error)) << "Resolve failure: " << error.message(); @@ -1106,6 +1489,120 @@ TEST(socks_client, resolve_command) while (test_client->called_ == 1); } +TEST(socks_client, v5_username_host_connect) +{ + io_thread io{}; + stream_type::socket client{io.io_service}; + + std::atomic called{false}; + auto test_client = net::socks::make_connect_client( + std::move(client), net::socks::version::v5, checked_client{std::addressof(called), false} + ); + ASSERT_TRUE(bool(test_client)); + + const auto userinfo = + net::user_and_pass::get("user:pass").value_or(net::user_and_pass{}); + ASSERT_TRUE( + test_client->set_connect_command("example.com", 80, std::addressof(userinfo)) + ); + EXPECT_FALSE(test_client->buffer().empty()); + ASSERT_TRUE(net::socks::client::connect_and_send(std::move(test_client), io.acceptor.local_endpoint())); + while (!io.connected) + ASSERT_FALSE(called); + + { + const std::uint8_t expected_bytes[] = {5, 2, 0, 2}; + + std::uint8_t actual_bytes[sizeof(expected_bytes)]; + boost::asio::read(io.server, boost::asio::buffer(actual_bytes)); + EXPECT_TRUE(std::memcmp(expected_bytes, actual_bytes, sizeof(actual_bytes)) == 0); + + const std::uint8_t reply_bytes[] = {5, 2}; + boost::asio::write(io.server, boost::asio::buffer(reply_bytes)); + } + { + const std::uint8_t expected_bytes[] = { + 1, 4, 'u', 's', 'e', 'r', 4, 'p', 'a', 's', 's' + }; + + std::uint8_t actual_bytes[sizeof(expected_bytes)]; + boost::asio::read(io.server, boost::asio::buffer(actual_bytes)); + EXPECT_TRUE(std::memcmp(expected_bytes, actual_bytes, sizeof(actual_bytes)) == 0); + + const std::uint8_t reply_bytes[] = {1, 0}; + boost::asio::write(io.server, boost::asio::buffer(reply_bytes)); + } + { + const std::uint8_t expected_bytes[] = { + 5, 1, 0, 3, 11, 'e','x', 'a', 'm', 'p', 'l', 'e', '.', 'c', 'o', + 'm', 0, 0x50 + }; + + std::uint8_t actual_bytes[sizeof(expected_bytes)]; + boost::asio::read(io.server, boost::asio::buffer(actual_bytes)); + EXPECT_TRUE(std::memcmp(expected_bytes, actual_bytes, sizeof(actual_bytes)) == 0); + + const std::uint8_t reply_bytes[] = { + 5, 0, 0, 1, 0xDE, 0xAD, 0xBE, 0xEF, 0x50, 00 + }; + boost::asio::write(io.server, boost::asio::buffer(reply_bytes)); + } + + // yikes! + while (!called); +} + +TEST(socks_client, v5_usernameskipped_host_connect) +{ + io_thread io{}; + stream_type::socket client{io.io_service}; + + std::atomic called{false}; + auto test_client = net::socks::make_connect_client( + std::move(client), net::socks::version::v5, checked_client{std::addressof(called), false} + ); + ASSERT_TRUE(bool(test_client)); + + const auto userinfo = + net::user_and_pass::get("user:pass").value_or(net::user_and_pass{}); + ASSERT_TRUE( + test_client->set_connect_command("example.com", 80, std::addressof(userinfo)) + ); + EXPECT_FALSE(test_client->buffer().empty()); + ASSERT_TRUE(net::socks::client::connect_and_send(std::move(test_client), io.acceptor.local_endpoint())); + while (!io.connected) + ASSERT_FALSE(called); + + { + const std::uint8_t expected_bytes[] = {5, 2, 0, 2}; + + std::uint8_t actual_bytes[sizeof(expected_bytes)]; + boost::asio::read(io.server, boost::asio::buffer(actual_bytes)); + EXPECT_TRUE(std::memcmp(expected_bytes, actual_bytes, sizeof(actual_bytes)) == 0); + + const std::uint8_t reply_bytes[] = {5, 0}; + boost::asio::write(io.server, boost::asio::buffer(reply_bytes)); + } + { + const std::uint8_t expected_bytes[] = { + 5, 1, 0, 3, 11, 'e','x', 'a', 'm', 'p', 'l', 'e', '.', 'c', 'o', + 'm', 0, 0x50 + }; + + std::uint8_t actual_bytes[sizeof(expected_bytes)]; + boost::asio::read(io.server, boost::asio::buffer(actual_bytes)); + EXPECT_TRUE(std::memcmp(expected_bytes, actual_bytes, sizeof(actual_bytes)) == 0); + + const std::uint8_t reply_bytes[] = { + 5, 0, 0, 1, 0xDE, 0xAD, 0xBE, 0xEF, 0x50, 00 + }; + boost::asio::write(io.server, boost::asio::buffer(reply_bytes)); + } + + // yikes! + while (!called); +} + TEST(socks_connector, host) { io_thread io{}; @@ -1113,7 +1610,9 @@ TEST(socks_connector, host) timeout.expires_from_now(std::chrono::seconds{5}); boost::unique_future sock = - net::socks::connector{io.acceptor.local_endpoint()}("example.com", "8080", timeout); + net::socks::connector{ + std::make_shared(io.acceptor.local_endpoint()) + }("example.com", "8080", timeout); while (!io.connected) ASSERT_FALSE(sock.is_ready()); @@ -1140,7 +1639,9 @@ TEST(socks_connector, ipv4) timeout.expires_from_now(std::chrono::seconds{5}); boost::unique_future sock = - net::socks::connector{io.acceptor.local_endpoint()}("250.88.125.99", "8080", timeout); + net::socks::connector{ + std::make_shared(io.acceptor.local_endpoint()) + }("250.88.125.99", "8080", timeout); while (!io.connected) ASSERT_FALSE(sock.is_ready()); @@ -1166,7 +1667,9 @@ TEST(socks_connector, error) timeout.expires_from_now(std::chrono::seconds{5}); boost::unique_future sock = - net::socks::connector{io.acceptor.local_endpoint()}("250.88.125.99", "8080", timeout); + net::socks::connector{ + std::make_shared(io.acceptor.local_endpoint()) + }("250.88.125.99", "8080", timeout); while (!io.connected) ASSERT_FALSE(sock.is_ready()); @@ -1192,7 +1695,9 @@ TEST(socks_connector, timeout) timeout.expires_from_now(std::chrono::milliseconds{10}); boost::unique_future sock = - net::socks::connector{io.acceptor.local_endpoint()}("250.88.125.99", "8080", timeout); + net::socks::connector{ + std::make_shared(io.acceptor.local_endpoint()) + }("250.88.125.99", "8080", timeout); ASSERT_EQ(boost::future_status::ready, sock.wait_for(boost::chrono::seconds{3})); EXPECT_THROW(sock.get().is_open(), boost::system::system_error);