aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/proxies.md205
-rw-r--r--src/daemon/command_line_args.h2
-rw-r--r--src/net/error.cpp6
-rw-r--r--src/net/error.h3
-rw-r--r--src/net/fwd.h6
-rw-r--r--src/net/http.cpp8
-rw-r--r--src/net/parse.cpp135
-rw-r--r--src/net/parse.h79
-rw-r--r--src/net/socks.cpp515
-rw-r--r--src/net/socks.h62
-rw-r--r--src/net/socks_connect.cpp26
-rw-r--r--src/net/socks_connect.h5
-rw-r--r--src/p2p/net_node.cpp41
-rw-r--r--src/p2p/net_node.h8
-rw-r--r--src/p2p/net_node.inl4
-rw-r--r--src/wallet/wallet2.cpp2
-rw-r--r--tests/unit_tests/net.cpp525
17 files changed, 1548 insertions, 84 deletions
diff --git a/docs/proxies.md b/docs/proxies.md
new file mode 100644
index 000000000..c9f21ccf3
--- /dev/null
+++ b/docs/proxies.md
@@ -0,0 +1,205 @@
+# 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`,
+`--daemon-ssl-allow-any-cert`, 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:
+
+```
+--anonymous-inbound rveahdfho7wo4b2m.onion:18083,18083
+--anonymous-inbound rveahdfho7wo4b2m.onion:18083,18083,100
+--anonymous-inbound rveahdfho7wo4b2m.onion:18083,127.0.0.1:18083
+--anonymous-inbound rveahdfho7wo4b2m.onion:18083,127.0.0.1:18083,100
+--anonymous-inbound udhdrtrcetjm5sxzskjyr5ztpeszydbh4dpl3pl4utgqqw2v4jna.b32.i2p,18083
+--anonymous-inbound udhdrtrcetjm5sxzskjyr5ztpeszydbh4dpl3pl4utgqqw2v4jna.b32.i2p,18083,100
+--anonymous-inbound udhdrtrcetjm5sxzskjyr5ztpeszydbh4dpl3pl4utgqqw2v4jna.b32.i2p,127.0.0.1:18083
+--anonymous-inbound 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 a0b15d021..647a7c7b4 100644
--- a/src/daemon/command_line_args.h
+++ b/src/daemon/command_line_args.h
@@ -98,7 +98,7 @@ namespace daemon_args
const command_line::arg_descriptor<std::string> arg_proxy = {
"proxy",
- "Network communication through proxy: <socks-ip:port> i.e. \"127.0.0.1:9050\"",
+ "Network communication through proxy: [socks5://[user:pass@]]<socks-ip:port> i.e. \"127.0.0.1:9050\"",
"",
};
const command_line::arg_descriptor<bool> arg_proxy_allow_dns_leaks = {
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
diff --git a/src/p2p/net_node.cpp b/src/p2p/net_node.cpp
index 4111b0b71..a06511785 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<net::socks::client> client, const boost::asio::ip::tcp::endpoint& proxy, const epee::net_utils::network_address& remote)
+ bool start_socks(std::shared_ptr<net::socks::client> client, const net::socks::endpoint& proxy, const epee::net_utils::network_address& remote)
{
CHECK_AND_ASSERT_MES(client != nullptr, false, "Unexpected null client");
@@ -89,21 +89,28 @@ namespace
switch (remote.get_type_id())
{
case net::tor_address::get_type_id():
- set = client->set_connect_command(remote.as<net::tor_address>());
+ set = client->set_connect_command(remote.as<net::tor_address>(), std::addressof(proxy.userinfo));
break;
case net::i2p_address::get_type_id():
- set = client->set_connect_command(remote.as<net::i2p_address>());
+ set = client->set_connect_command(remote.as<net::i2p_address>(), std::addressof(proxy.userinfo));
break;
case epee::net_utils::ipv4_network_address::get_type_id():
- set = client->set_connect_command(remote.as<epee::net_utils::ipv4_network_address>());
+ set = client->set_connect_command(remote.as<epee::net_utils::ipv4_network_address>(), 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<epee::net_utils::ipv6_network_address>(), 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<std::vector<std::string> > 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<std::vector<std::string> > arg_p2p_seed_node = {"seed-node", "Connect to a node to retrieve peer addresses, and disconnect"};
- const command_line::arg_descriptor<std::vector<std::string> > arg_tx_proxy = {"tx-proxy", "Send local txes through proxy: <network-type>,<socks-ip:port>[,max_connections][,disable_noise] i.e. \"tor,127.0.0.1:9050,100,disable_noise\""};
+ const command_line::arg_descriptor<std::vector<std::string> > arg_tx_proxy = {"tx-proxy", "Send local txes through proxy: <network-type>,[socks5://[user:pass@]]<socks-ip:port>[,max_connections][,disable_noise] i.e. \"tor,127.0.0.1:9050,100,disable_noise\""};
const command_line::arg_descriptor<std::vector<std::string> > arg_anonymous_inbound = {"anonymous-inbound", "<hidden-service-address>,<[bind-ip:]port>[,max_connections] i.e. \"x.onion,127.0.0.1:18083,100\""};
const command_line::arg_descriptor<std::string> arg_ban_list = {"ban-list", "Specify ban list file, one IP address per line"};
const command_line::arg_descriptor<bool> 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<boost::asio::ip::tcp::socket>
- socks_connect_internal(const std::atomic<bool>& stop_signal, boost::asio::io_context& service, const boost::asio::ip::tcp::endpoint& proxy, const epee::net_utils::network_address& remote)
+ socks_connect_internal(const std::atomic<bool>& stop_signal, boost::asio::io_context& 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<boost::system::error_code, socket_type>;
@@ -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 423e2607e..edd3e31fa 100644
--- a/src/p2p/net_node.h
+++ b/src/p2p/net_node.h
@@ -56,7 +56,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
@@ -74,7 +74,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;
};
@@ -104,7 +104,7 @@ namespace nodetool
// hides boost::future and chrono stuff from mondo template file
boost::optional<boost::asio::ip::tcp::socket>
- socks_connect_internal(const std::atomic<bool>& stop_signal, boost::asio::io_context& service, const boost::asio::ip::tcp::endpoint& proxy, const epee::net_utils::network_address& remote);
+ socks_connect_internal(const std::atomic<bool>& stop_signal, boost::asio::io_context& service, const net::socks::endpoint& proxy, const epee::net_utils::network_address& remote);
template<class base_type>
@@ -216,7 +216,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<unsigned int> m_current_number_of_out_peers;
std::atomic<unsigned int> 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 bca8a52af..d22e7f9e5 100644
--- a/src/p2p/net_node.inl
+++ b/src/p2p/net_node.inl
@@ -928,8 +928,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 4a14c3b8c..db72ce4f6 100644
--- a/src/wallet/wallet2.cpp
+++ b/src/wallet/wallet2.cpp
@@ -436,7 +436,7 @@ std::unique_ptr<tools::wallet2> 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 16ec7dd21..30e5f5589 100644
--- a/tests/unit_tests/net.cpp
+++ b/tests/unit_tests/net.cpp
@@ -40,6 +40,7 @@
#include <boost/endian/conversion.hpp>
#include <boost/range/adaptor/sliced.hpp>
#include <boost/range/combine.hpp>
+#include <boost/optional/optional.hpp>
#include <boost/system/error_code.hpp>
#include <boost/thread/scoped_thread.hpp>
#include <boost/thread/thread.hpp>
@@ -966,6 +967,239 @@ 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 boost::optional<net::user_and_pass>& actual,
+ const boost::optional<std::string>& user = boost::none,
+ 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 present = [] (const boost::string_ref value)
+ {
+ return boost::optional<std::string>{std::string{value}};
+ };
+ const auto check = [] (
+ const boost::optional<net::uri_components>& actual,
+ const boost::optional<std::string>& scheme = boost::none,
+ 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"), present("socks"), "", "", "host:port");
+ check(net::uri_components::get("socks://[::ffff]:8080/path"), present("socks"), "", "", "[::ffff]:8080");
+ check(net::uri_components::get("socks://192.168.0.1/path"), present("socks"), "", "", "192.168.0.1");
+ check(net::uri_components::get("socks://host"), present("socks"), "", "", "host");
+ check(net::uri_components::get("socks://@host"), present("socks"), "", "", "host");
+ check(net::uri_components::get("socks://:@host"), present("socks"), "", "", "host");
+ check(net::uri_components::get("socks://user:@host"), present("socks"), "user", "", "host");
+ check(net::uri_components::get("socks://:pass@host"), present("socks"), "", "pass", "host");
+ check(net::uri_components::get("socks://user:pass@host"), present("socks"), "user", "pass", "host");
+ check(net::uri_components::get("host:port/path"), present(""), "", "", "host:port");
+ check(net::uri_components::get("[::ffff]:8080/path"), present(""), "", "", "[::ffff]:8080");
+ check(net::uri_components::get("192.168.0.1/path"), present(""), "", "", "192.168.0.1");
+ check(net::uri_components::get("host"), present(""), "", "", "host");
+ check(net::uri_components::get("192.168.0.1"), present(""), "", "", "192.168.0.1");
+ check(net::uri_components::get("192.168.0.1:80/path"), present(""), "", "", "192.168.0.1:80");
+ check(net::uri_components::get("::ffff"), present(""), "", "", "::ffff");
+ check(net::uri_components::get("[::ffff]:8080"), present(""), "", "", "[::ffff]:8080");
+ check(net::uri_components::get("example.com/some://valid/path"), present(""), "", "", "example.com");
+
+ // unsupported URIs (URN cases)
+ check(net::uri_components::get("urn:isbn:number"), present(""), "", "", "urn:isbn:number");
+ check(net::uri_components::get("urn:isbn/number"), present(""), "", "", "urn:isbn");
+
+ // invalid cases _not_ strictly rejected until hostname is parsed fully
+ check(net::uri_components::get(""), present(""), "", "", "");
+ check(net::uri_components::get("socks://"), present("socks"), "", "", "");
+ check(net::uri_components::get("socks:/"), present(""), "", "", "socks:");
+ check(net::uri_components::get("192.168.0.1:80://"), present(""), "", "", "192.168.0.1:80:");
+ check(net::uri_components::get("user@::ffff:443"), present(""), "user", "", "::ffff:443");
+ check(net::uri_components::get("socks://user:p%61ss@ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff:8080"), present("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)
+{
+ namespace ip = boost::asio::ip;
+ const auto present = [] (const boost::asio::ip::address& value)
+ {
+ return boost::optional<boost::asio::ip::address>{value};
+ };
+ const auto check = [] (
+ const expect<net::socks::endpoint>& actual,
+ const boost::optional<boost::asio::ip::address>& address = boost::none,
+ 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)
+ {
+ ASSERT_TRUE(bool(address));
+ 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"), present(ip::make_address_v6("::ffff")), 8080, "", "", net::socks::version::v4a);
+ check(net::socks::endpoint::get("socks5://user:%70ass@[::ffff]:8080/path"), present(ip::make_address_v6("::ffff")), 8080, "user", "pass", net::socks::version::v5);
+ check(net::socks::endpoint::get("socks4a://192.168.0.1:1/path"), present(ip::make_address_v4("192.168.0.1")), 1, "", "", net::socks::version::v4a);
+ check(net::socks::endpoint::get("socks5://%75@192.168.0.1:1/path"), present(ip::make_address_v4("192.168.0.1")), 1, "u", "", net::socks::version::v5);
+ check(net::socks::endpoint::get("[::ffff]:8080/path"), present(ip::make_address_v6("::ffff")), 8080, "", "", net::socks::version::v4a);
+ check(net::socks::endpoint::get("192.168.0.1:50"), present(ip::make_address_v4("192.168.0.1")), 50, "", "", net::socks::version::v4a);
+ check(net::socks::endpoint::get("192.168.0.1:80/path"), present(ip::make_address_v4("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;
@@ -1035,6 +1269,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)
@@ -1082,6 +1368,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<bool> 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{};
@@ -1118,6 +1460,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<bool> 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};
@@ -1133,7 +1525,7 @@ TEST(socks_client, resolve_command)
, expected_(false)
{};
- virtual void done(boost::system::error_code error, std::shared_ptr<client> self) override
+ virtual void done(boost::system::error_code error, const std::shared_ptr<client>& self) override
{
EXPECT_EQ(this, self.get());
EXPECT_EQ(expected_, bool(error)) << "Resolve failure: " << error.message();
@@ -1189,6 +1581,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<bool> 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<bool> 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{};
@@ -1196,7 +1702,9 @@ TEST(socks_connector, host)
timeout.expires_after(std::chrono::seconds{5});
boost::unique_future<boost::asio::ip::tcp::socket> sock =
- net::socks::connector{io.acceptor.local_endpoint()}("example.com", "8080", timeout);
+ net::socks::connector{
+ std::make_shared<net::socks::endpoint>(io.acceptor.local_endpoint())
+ }("example.com", "8080", timeout);
while (!io.connected)
ASSERT_FALSE(sock.is_ready());
@@ -1223,7 +1731,9 @@ TEST(socks_connector, ipv4)
timeout.expires_after(std::chrono::seconds{5});
boost::unique_future<boost::asio::ip::tcp::socket> sock =
- net::socks::connector{io.acceptor.local_endpoint()}("250.88.125.99", "8080", timeout);
+ net::socks::connector{
+ std::make_shared<net::socks::endpoint>(io.acceptor.local_endpoint())
+ }("250.88.125.99", "8080", timeout);
while (!io.connected)
ASSERT_FALSE(sock.is_ready());
@@ -1249,7 +1759,9 @@ TEST(socks_connector, error)
timeout.expires_after(std::chrono::seconds{5});
boost::unique_future<boost::asio::ip::tcp::socket> sock =
- net::socks::connector{io.acceptor.local_endpoint()}("250.88.125.99", "8080", timeout);
+ net::socks::connector{
+ std::make_shared<net::socks::endpoint>(io.acceptor.local_endpoint())
+ }("250.88.125.99", "8080", timeout);
while (!io.connected)
ASSERT_FALSE(sock.is_ready());
@@ -1275,7 +1787,9 @@ TEST(socks_connector, timeout)
timeout.expires_after(std::chrono::milliseconds{10});
boost::unique_future<boost::asio::ip::tcp::socket> sock =
- net::socks::connector{io.acceptor.local_endpoint()}("250.88.125.99", "8080", timeout);
+ net::socks::connector{
+ std::make_shared<net::socks::endpoint>(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);
@@ -1842,4 +2356,3 @@ TEST(zmq, read_write_termination)
ASSERT_FALSE(bool(received));
EXPECT_EQ(net::zmq::make_error_code(ETERM), received.error());
}
-