Add Socks v5 support to daemon and wallet

This commit is contained in:
Lee *!* Clagett 2024-08-17 18:37:10 -04:00
parent caa62bc9ea
commit 64a613e838
17 changed files with 1540 additions and 78 deletions

204
docs/proxies.md Normal file
View File

@ -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.

View File

@ -94,7 +94,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 = {

View File

@ -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:

View File

@ -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`
};

View File

@ -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;
}

View File

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

View File

@ -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({reinterpret_cast<std::uint8_t*>(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<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());
}
std::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>());
std::optional<user_and_pass> 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> 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<uri_components> 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> 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;
}
}
}

View File

@ -32,12 +32,73 @@
#include <boost/asio/ip/tcp.hpp>
#include <boost/utility/string_ref.hpp>
#include <cstdint>
#include <optional>
#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<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. `std::nullopt` if
* bad percent encoding.
*/
static std::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,22 @@ 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;
};
}
}

View File

@ -30,14 +30,17 @@
#include <algorithm>
#include <boost/asio/buffer.hpp>
#include <boost/asio/coroutine.hpp>
#include <boost/asio/read.hpp>
#include <boost/asio/write.hpp>
#include <boost/endian/arithmetic.hpp>
#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"
@ -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<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())
@ -84,6 +205,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(2 <= std::numeric_limits<std::size_t>::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<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
@ -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<client> 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<client> 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<unsigned I>
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<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)).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<client> 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<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, alias.strand_.wrap(write{std::move(self)}));
@ -303,10 +772,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), 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;

View File

@ -28,6 +28,7 @@
#pragma once
#include <array>
#include <cstdint>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/io_service.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::io_service::strand 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,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<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);
//! 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<client>) override
virtual void done(boost::system::error_code error, const std::shared_ptr<client>&) override
{
handler_(error, take_socket());
}

View File

@ -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<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)
std::runtime_error{"Unexpected nullptr of net::socks::endpoint"};
struct future_socket
{
boost::promise<boost::asio::ip::tcp::socket> 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<boost::asio::ip::tcp::socket> 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)});

View File

@ -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

View File

@ -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");
@ -92,18 +92,25 @@ namespace
set = client->set_connect_command(remote.as<net::tor_address>());
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_service& 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_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<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&)
{}

View File

@ -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<boost::asio::ip::tcp::socket>
socks_connect_internal(const std::atomic<bool>& 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<bool>& stop_signal, boost::asio::io_service& service, const net::socks::endpoint& proxy, const epee::net_utils::network_address& remote);
template<class base_type>
@ -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<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;

View File

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

View File

@ -435,7 +435,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);
}

View File

@ -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<net::user_and_pass>& actual,
const std::optional<std::string>& 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<net::uri_components>& actual,
const std::optional<std::string>& 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<net::socks::endpoint>& actual,
const std::optional<boost::asio::ip::address>& 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<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{};
@ -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<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};
@ -1050,7 +1433,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();
@ -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<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{};
@ -1113,7 +1610,9 @@ TEST(socks_connector, host)
timeout.expires_from_now(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());
@ -1140,7 +1639,9 @@ TEST(socks_connector, ipv4)
timeout.expires_from_now(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());
@ -1166,7 +1667,9 @@ TEST(socks_connector, error)
timeout.expires_from_now(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());
@ -1192,7 +1695,9 @@ TEST(socks_connector, timeout)
timeout.expires_from_now(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);