Add Socks v5 support to daemon and wallet
What changed, and why it matters
This commit adds SOCKS5 proxy support to the Monero daemon and wallet, expanding the older SOCKS4/SOCKS4a support. It introduces new URI parsing for proxy strings, optional username/password authentication, IPv6 support, and new command-line options. The change is a feature addition, not a documented security fix. The code does include some security-relevant notes (for example, warning that proxy credentials may appear in the process list), but nothing in the commit message or diff states that a vulnerability is being patched.
Treat this as a normal feature commit. Reviewers should verify that SOCKS5 credential parsing does not introduce buffer overruns, that percent-decoding rejects malformed sequences, and that the new asynchronous state machine handles short reads and malformed server replies safely. Users should follow the documentation's advice to keep credentials out of the process list by using a config file.
Security signals we found
New network-facing SOCKS5 authentication code added
Proxy credentials are parsed from URI and transmitted in cleartext over the local SOCKS connection (expected per SOCKS5 user/pass auth)
Documentation explicitly warns that username/password will appear in the process list
Wallet restricts non-.onion/.i2p proxy destinations unless custom SSL verification is configured, which is a security control
No assertion of a vulnerability fix or security patch in commit message or diff
Evidence from the diff
The patch implements a full SOCKS5 client handshake (RFC 1928/1929), URI parsing for socks5://[user:pass@]host:port, percent-decoding of credentials, and IPv6 connect support. It wires the new net::socks::endpoint into the daemon’s --proxy and --tx-proxy options and the wallet’s --proxy option. The SOCKS4/4a path remains largely unchanged. Several new error codes are added for invalid encoding, scheme, and unexpected userinfo. Unit tests cover parsing and basic SOCKS5 handshake flows. No CVE, advisory, or vendor security disclosure is present in the supplied materials.
Changed components
src/net/socks.cppsrc/net/socks.hsrc/net/parse.cppsrc/net/parse.hsrc/net/socks_connect.cppsrc/net/socks_connect.hsrc/net/http.cppsrc/p2p/net_node.cppsrc/p2p/net_node.hsrc/p2p/net_node.inlsrc/wallet/wallet2.cppsrc/daemon/command_line_args.hInspect captured patch +1538 / −82
diff --git a/docs/proxies.md b/docs/proxies.md
new file mode 100644
index 0000000..eac141b
--- /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 428f6b9..291ebbd 100644
--- a/src/daemon/command_line_args.h
+++ b/src/daemon/command_line_args.h
@@ -95,7 +95,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 7570aba..f59bb52 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 56e7e07..1f1380a 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 ee6a14a..2fff162 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 b7d7adc..5e6425d 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 b761da3..f92f4ee 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({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.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 9795cdf..c2a0046 100644
--- a/src/net/parse.h
+++ b/src/net/parse.h
@@ -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;
+ };
+ }
}
diff --git a/src/net/socks.cpp b/src/net/socks.cpp
index 5de4fa9..4336556 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 812a6c3..6aa1696 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 7f07f07..b1a4b7c 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 2ef07a4..8df7cdf 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 0f75893..2c71bae 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 08db076..76372d0 100644
--- a/src/p2p/net_node.h
+++ b/src/p2p/net_node.h
@@ -55,7 +55,7 @@
#include "math_helper.h"
#include "net_node_common.h"
#include "net/enums.h"
-#include "net/fwd.h"
+#include "net/parse.h"
#include "common/command_line.h"
PUSH_WARNINGS
@@ -73,7 +73,7 @@ namespace nodetool
{}
std::int64_t max_connections;
- boost::asio::ip::tcp::endpoint address;
+ net::socks::endpoint address;
epee::net_utils::zone zone;
bool noise;
};
@@ -103,7 +103,7 @@ namespace nodetool
// hides boost::future and chrono stuff from mondo template file
boost::optional<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>
@@ -215,7 +215,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 1be3fc4..8dddde7 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 b7ec6d2..fbde0e2 100644
--- a/src/wallet/wallet2.cpp
+++ b/src/wallet/wallet2.cpp
@@ -437,7 +437,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 8c68030..acb3b95 100644
--- a/tests/unit_tests/net.cpp
+++ b/tests/unit_tests/net.cpp
@@ -883,6 +883,230 @@ 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)
+{
+ namespace ip = boost::asio::ip;
+ 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"), ip::make_address_v6("::ffff"), 8080, "", "", net::socks::version::v4a);
+ check(net::socks::endpoint::get("socks5://user:%70ass@[::ffff]:8080/path"), ip::make_address_v6("::ffff"), 8080, "user", "pass", net::socks::version::v5);
+ check(net::socks::endpoint::get("socks4a://192.168.0.1:1/path"), 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"), ip::make_address_v4("192.168.0.1"), 1, "u", "", net::socks::version::v5);
+ check(net::socks::endpoint::get("[::ffff]:8080/path"), ip::make_address_v6("::ffff"), 8080, "", "", net::socks::version::v4a);
+ check(net::socks::endpoint::get("192.168.0.1:50"), ip::make_address_v4("192.168.0.1"), 50, "", "", net::socks::version::v4a);
+ check(net::socks::endpoint::get("192.168.0.1:80/path"), 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;
@@ -952,6 +1176,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 +1275,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 +1367,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 +1432,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 +1488,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 +1609,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());
@@ -1140,7 +1638,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());
@@ -1166,7 +1666,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());
@@ -1192,7 +1694,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);
Why this scored 27/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.