diff --git a/curlx.cpp b/curlx.cpp index bb24b03..a2e3ca6 100644 --- a/curlx.cpp +++ b/curlx.cpp @@ -42,6 +42,25 @@ void CurlDeleter::operator()(CURL *handle) noexcept { Curl::Curl() noexcept {} +bool Curl::method_get_maybe_socks5(const std::string &proxy_port, + const std::string &url, long timeout, + std::string *body, + std::string *err) noexcept { + if (!init()) { + *err = "cannot initialize cURL"; + return false; + } + if (!proxy_port.empty()) { + std::stringstream ss; + ss << "socks5h://127.0.0.1:" << proxy_port; + if (setopt_proxy(ss.str()) != CURLE_OK) { + *err = "cannot set proxy"; + return false; + } + } + return method_get(url, timeout, body, err); +} + bool Curl::method_get(const std::string &url, long timeout, std::string *body, std::string *err) noexcept { if (body == nullptr || err == nullptr) { @@ -80,7 +99,7 @@ bool Curl::method_get(const std::string &url, long timeout, std::string *body, bool Curl::init() noexcept { if (!!handle_) { - return false; + return true; // make the method idempotent } auto handle = this->easy_init(); if (!handle) { @@ -95,6 +114,11 @@ CURLcode Curl::setopt_url(const std::string &url) noexcept { return ::curl_easy_setopt(handle_.get(), CURLOPT_URL, url.c_str()); } +CURLcode Curl::setopt_proxy(const std::string &url) noexcept { + assert(handle_); + return ::curl_easy_setopt(handle_.get(), CURLOPT_PROXY, url.c_str()); +} + CURLcode Curl::setopt_writefunction(size_t (*callback)( char *ptr, size_t size, size_t nmemb, void *userdata)) noexcept { assert(handle_); diff --git a/curlx.hpp b/curlx.hpp index 3fb75f8..1acb6b1 100644 --- a/curlx.hpp +++ b/curlx.hpp @@ -26,6 +26,10 @@ class Curl { Curl() noexcept; + bool method_get_maybe_socks5(const std::string &proxy_port, + const std::string &url, long timeout, + std::string *body, std::string *err) noexcept; + bool method_get(const std::string &url, long timeout, std::string *body, std::string *err) noexcept; @@ -35,6 +39,8 @@ class Curl { virtual CURLcode setopt_url(const std::string &url) noexcept; + virtual CURLcode setopt_proxy(const std::string &url) noexcept; + virtual CURLcode setopt_writefunction(size_t (*callback)( char *ptr, size_t size, size_t nmemb, void *userdata)) noexcept; diff --git a/curlx_test.cpp b/curlx_test.cpp index 5280df6..d7e86c2 100644 --- a/curlx_test.cpp +++ b/curlx_test.cpp @@ -9,6 +9,40 @@ using namespace measurement_kit; +// Curl::method_get_maybe_socks5() tests +// ------------------------------------- + +class FailInit : public libndt::Curl { + public: + using libndt::Curl::Curl; + virtual bool init() noexcept override { return false; } +}; + +TEST_CASE("Curl::method_get_maybe_socks5() deals with Curl::init() failure") { + FailInit curl; + std::string body; + std::string err; + REQUIRE(!curl.method_get_maybe_socks5("", "http://x.org", 1, &body, &err)); +} + +class FailSetoptProxy : public libndt::Curl { + public: + using libndt::Curl::Curl; + virtual CURLcode setopt_proxy(const std::string &) noexcept override { + return CURLE_UNSUPPORTED_PROTOCOL; // any error is okay here + } +}; + +TEST_CASE( + "Curl::method_get_maybe_socks5() deals with Curl::setopt_proxy() failure") { + FailSetoptProxy curl; + std::string body; + std::string err; + REQUIRE( + !curl.method_get_maybe_socks5("9050", "http://x.org", 1, &body, &err)); + REQUIRE(err == "cannot set proxy"); +} + // Curl::method_get() tests // ------------------------ @@ -24,12 +58,6 @@ TEST_CASE("Curl::method_get() deals with null err") { REQUIRE(curl.method_get("http://x.org", 1, &body, nullptr) == false); } -class FailInit : public libndt::Curl { - public: - using libndt::Curl::Curl; - virtual bool init() noexcept override { return false; } -}; - TEST_CASE("Curl::method_get() deals with Curl::init() failure") { FailInit curl; std::string body; @@ -129,7 +157,16 @@ TEST_CASE("Curl::init() deals with curl_easy_init() failure") { TEST_CASE("Curl::init() is idempotent") { libndt::Curl curl; REQUIRE(curl.init() == true); - REQUIRE(curl.init() == false); + REQUIRE(curl.init() == true); +} + +// Curl::setopt_proxy() tests +// -------------------------- + +TEST_CASE("Curl::setopt_proxy() works") { + libndt::Curl curl; + REQUIRE(curl.init() == true); + REQUIRE(curl.setopt_proxy("socks5h://127.0.0.1:9050") == CURLE_OK); } #endif // HAVE_CURL diff --git a/libndt-client.cpp b/libndt-client.cpp index 9181e0c..8fb2843 100644 --- a/libndt-client.cpp +++ b/libndt-client.cpp @@ -20,6 +20,8 @@ static void usage() { std::clog << " --download-ext : run multi-stream download test\n"; std::clog << " --json : use the JSON protocol\n"; std::clog << " --port : use the specified port\n"; + std::clog + << " --socks5h : use socks5h proxy at 127.0.0.1:\n"; std::clog << " --upload : run upload test\n"; std::clog << " --verbose : be verbose\n"; std::clog << "\n"; @@ -31,11 +33,12 @@ int main(int, char **argv) { using namespace measurement_kit; libndt::Settings settings; settings.verbosity = libndt::verbosity::quiet; - settings.test_suite = 0; // you need to enable tests explicitly + settings.test_suite = 0; // you need to enable tests explicitly { argh::parser cmdline; cmdline.add_param("port"); + cmdline.add_param("socks5h"); cmdline.parse(argv); for (auto &flag : cmdline.flags()) { if (flag == "download") { @@ -63,6 +66,10 @@ int main(int, char **argv) { if (param.first == "port") { settings.port = param.second; std::clog << "will use port: " << param.second << std::endl; + } else if (param.first == "socks5h") { + settings.socks5h_port = param.second; + std::clog << "will use socks5h proxy at: 127.0.0.1:" << param.second + << std::endl; } else { std::clog << "fatal: unrecognized param: " << param.first << std::endl; usage(); diff --git a/libndt.cpp b/libndt.cpp index 10ca096..e8be1a1 100644 --- a/libndt.cpp +++ b/libndt.cpp @@ -123,11 +123,11 @@ static std::string represent(std::string message) noexcept { return message; } std::stringstream ss; - ss << "binary([ "; + ss << "binary(["; for (auto &c : message) { if (c <= ' ' || c > '~') { - ss << " <0x" << std::fixed << std::setw(2) << std::setfill('0') - << std::hex << (unsigned)c << "> "; + ss << "<0x" << std::fixed << std::setw(2) << std::setfill('0') + << std::hex << (unsigned)(uint8_t)c << ">"; } else { ss << (char)c; } @@ -309,7 +309,9 @@ bool Client::query_mlabns() noexcept { } bool Client::connect() noexcept { - return connect_tcp(impl->settings.hostname, impl->settings.port, &impl->sock); + return connect_tcp_maybe_socks5(impl->settings.hostname, + impl->settings.port, + &impl->sock); } bool Client::send_login() noexcept { @@ -471,7 +473,7 @@ bool Client::run_download() noexcept { for (uint8_t i = 0; i < nflows; ++i) { Socket sock = -1; - if (!connect_tcp(impl->settings.hostname, port, &sock)) { + if (!connect_tcp_maybe_socks5(impl->settings.hostname, port, &sock)) { break; } dload_socks.sockets.push_back(sock); @@ -642,7 +644,7 @@ bool Client::run_upload() noexcept { { Socket sock = -1; - if (!connect_tcp(impl->settings.hostname, port, &sock)) { + if (!connect_tcp_maybe_socks5(impl->settings.hostname, port, &sock)) { return false; } upload_socks.sockets.push_back(sock); @@ -733,6 +735,235 @@ bool Client::run_upload() noexcept { // Low-level API +bool Client::connect_tcp_maybe_socks5(const std::string &hostname, + const std::string &port, + Socket *sock) noexcept { + if (impl->settings.socks5h_port.empty()) { + return connect_tcp(hostname, port, sock); + } + if (!connect_tcp("127.0.0.1", impl->settings.socks5h_port, sock)) { + return false; + } + EMIT_INFO("socks5h: connected to proxy"); + { + char auth_request[] = { + 5, // version + 1, // number of methods + 0 // "no auth" method + }; + auto rv = this->sendn(*sock, auth_request, sizeof(auth_request)); + if (rv <= 0) { + EMIT_WARNING("socks5h: cannot send auth_request"); + this->closesocket(*sock); + *sock = -1; + return false; + } + assert((Size)rv == sizeof(auth_request)); + EMIT_DEBUG("socks5h: sent this auth request: " + << represent(std::string{auth_request, sizeof(auth_request)})); + } + { + char auth_response[2] = { + 0, // version + 0 // method + }; + auto rv = this->recvn(*sock, auth_response, sizeof(auth_response)); + if (rv <= 0) { + EMIT_WARNING("socks5h: cannot recv auth_response"); + this->closesocket(*sock); + *sock = -1; + return false; + } + assert((Size)rv == sizeof(auth_response)); + constexpr uint8_t version = 5; + if (auth_response[0] != version) { + EMIT_WARNING("socks5h: received unexpected version number"); + this->closesocket(*sock); + *sock = -1; + return false; + } + constexpr uint8_t auth_method = 0; + if (auth_response[1] != auth_method) { + EMIT_WARNING("socks5h: received unexpected auth_method"); + this->closesocket(*sock); + *sock = -1; + return false; + } + // Make sure that we can cast `rv` to `size_t` + static_assert(sizeof(auth_response) < SIZE_MAX, "auth_response too big"); + assert((Size)rv < SIZE_MAX); + EMIT_DEBUG("socks5h: authenticated with proxy; response: " + << represent(std::string{auth_response, (size_t)rv})); + } + { + std::string connect_request; + { + std::stringstream ss; + ss << (uint8_t)5; // version + ss << (uint8_t)1; // CMD_CONNECT + ss << (uint8_t)0; // reserved + ss << (uint8_t)3; // ATYPE_DOMAINNAME + if (hostname.size() > UINT8_MAX) { + EMIT_WARNING("socks5h: hostname is too long"); + this->closesocket(*sock); + *sock = -1; + return false; + } + ss << (uint8_t)hostname.size(); + ss << hostname; + uint16_t portno{}; + { + const char *errstr = nullptr; + portno = (uint16_t)this->strtonum(port.c_str(), 0, UINT16_MAX, &errstr); + if (errstr != nullptr) { + EMIT_WARNING("socks5h: invalid port number: " << errstr); + this->closesocket(*sock); + *sock = -1; + return false; + } + } + portno = htons(portno); + ss << (uint8_t)((char *)&portno)[0] << (uint8_t)((char *)&portno)[1]; + connect_request = ss.str(); + EMIT_DEBUG("socks5h: connect_request: " << represent(connect_request)); + } + auto rv = this->sendn( // + *sock, connect_request.data(), connect_request.size()); + if (rv <= 0) { + EMIT_WARNING("socks5h: cannot send connect_request"); + this->closesocket(*sock); + *sock = -1; + return false; + } + assert((Size)rv == connect_request.size()); + EMIT_DEBUG("socks5h: sent connect request"); + } + { + char connect_response_hdr[] = { + 0, // version + 0, // error + 0, // reserved + 0 // type + }; + auto rv = this->recvn( // + *sock, connect_response_hdr, sizeof(connect_response_hdr)); + if (rv <= 0) { + EMIT_WARNING("socks5h: cannot recv connect_response_hdr"); + this->closesocket(*sock); + *sock = -1; + return false; + } + assert((Size)rv == sizeof(connect_response_hdr)); + // Make sure that we can cast `rv` to `size_t` + static_assert(sizeof(connect_response_hdr) < SIZE_MAX, + "connect_response_hdr too big"); + assert((Size)rv < SIZE_MAX); + EMIT_DEBUG("socks5h: connect_response_hdr: " + << represent(std::string{connect_response_hdr, (size_t)rv})); + constexpr uint8_t version = 5; + if (connect_response_hdr[0] != version) { + EMIT_WARNING("socks5h: invalid message version"); + this->closesocket(*sock); + *sock = -1; + return false; + } + if (connect_response_hdr[1] != 0) { + // TODO(bassosimone): map the socks5 error to a system error + EMIT_WARNING("socks5h: connect() failed: " + << (unsigned)(uint8_t)connect_response_hdr[1]); + this->closesocket(*sock); + *sock = -1; + return false; + } + if (connect_response_hdr[2] != 0) { + EMIT_WARNING("socks5h: invalid reserved field"); + this->closesocket(*sock); + *sock = -1; + return false; + } + // receive IP or domain + switch (connect_response_hdr[3]) { + case 1: // ipv4 + { + constexpr Size expected = 4; // ipv4 + char buf[expected]; + auto rv = this->recvn(*sock, buf, sizeof(buf)); + if (rv <= 0) { + EMIT_WARNING("socks5h: cannot recv ipv4 address"); + this->closesocket(*sock); + *sock = -1; + return false; + } + assert((Size)rv == sizeof(buf)); + // TODO(bassosimone): log the ipv4 address. However tor returns a zero + // ipv4 and so there is little added value in logging. + break; + } + case 3: // domain + { + uint8_t len = 0; + auto rv = this->recvn(*sock, &len, sizeof(len)); + if (rv <= 0) { + EMIT_WARNING("socks5h: cannot recv domain length"); + this->closesocket(*sock); + *sock = -1; + return false; + } + assert((Size)rv == sizeof(len)); + char domain[UINT8_MAX + 1]; // space for final '\0' + rv = this->recvn(*sock, domain, len); + if (rv <= 0) { + EMIT_WARNING("socks5h: cannot recv domain"); + this->closesocket(*sock); + *sock = -1; + return false; + } + assert((Size)rv == len); + domain[len] = 0; + EMIT_DEBUG("socks5h: domain: " << domain); + break; + } + case 4: // ipv6 + { + constexpr Size expected = 16; // ipv6 + char buf[expected]; + auto rv = this->recvn(*sock, buf, sizeof(buf)); + if (rv <= 0) { + EMIT_WARNING("socks5h: cannot recv ipv6 address"); + this->closesocket(*sock); + *sock = -1; + return false; + } + assert((Size)rv == sizeof(buf)); + // TODO(bassosimone): log the ipv6 address. However tor returns a zero + // ipv6 and so there is little added value in logging. + break; + } + default: + EMIT_WARNING("socks5h: invalid address type"); + this->closesocket(*sock); + *sock = -1; + return false; + } + // receive the port + { + uint16_t port = 0; + rv = this->recvn(*sock, &port, sizeof(port)); + if (rv <= 0) { + EMIT_WARNING("socks5h: cannot recv port"); + this->closesocket(*sock); + *sock = -1; + return false; + } + assert((Size)rv == sizeof(port)); + port = ntohs(port); + EMIT_DEBUG("socks5h: port number: " << port); + } + } + EMIT_INFO("socks5h: the proxy has successfully connected"); + return true; +} + bool Client::connect_tcp(const std::string &hostname, const std::string &port, Socket *sock) noexcept { assert(sock != nullptr); @@ -780,7 +1011,7 @@ bool Client::connect_tcp(const std::string &hostname, const std::string &port, } this->freeaddrinfo(rp); if (*sock != -1) { - break; // we have a connection! + break; // we have a connection! } } return *sock != -1; @@ -1123,7 +1354,7 @@ bool Client::resolve(const std::string &hostname, result = false; break; } - addrs->push_back(address); // we only care about address + addrs->push_back(address); // we only care about address EMIT_DEBUG("- " << address); } this->freeaddrinfo(rp); @@ -1136,7 +1367,9 @@ bool Client::query_mlabns_curl(const std::string &url, long timeout, std::string *body) noexcept { #ifdef HAVE_CURL std::string err = ""; - if (!Curl{}.method_get(url, timeout, body, &err)) { + Curl curl; + if (!curl.method_get_maybe_socks5( // + impl->settings.socks5h_port, url, timeout, body, &err)) { EMIT_WARNING("cannot query mlabns: " << err); return false; } diff --git a/libndt.hpp b/libndt.hpp index 9b4cd4e..b38fc9d 100644 --- a/libndt.hpp +++ b/libndt.hpp @@ -186,6 +186,10 @@ class Settings { /// is meant as a safeguard to prevent the test for running for much more time /// than anticipated, due to buffering and/or changing network conditions. double max_runtime = 14 /* seconds */; + + /// SOCKSv5h port to use for tunnelling traffic using, e.g., Tor. If non + /// empty, all DNS and TCP traffic should be tunnelled over such port. + std::string socks5h_port; }; /// NDT client. In the typical usage, you just need to construct a Client, @@ -290,6 +294,10 @@ class Client { // Low-level API + virtual bool connect_tcp_maybe_socks5(const std::string &hostname, + const std::string &port, + Socket *sock) noexcept; + virtual bool connect_tcp(const std::string &hostname, const std::string &port, Socket *sock) noexcept; diff --git a/libndt_test.cpp b/libndt_test.cpp index ed27a88..9a019bf 100644 --- a/libndt_test.cpp +++ b/libndt_test.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include "catch.hpp" @@ -561,20 +562,22 @@ TEST_CASE( REQUIRE(client.run_download() == false); } -class FailConnectTcp : public libndt::Client { +class FailConnectTcpMaybeSocks5 : public libndt::Client { public: using libndt::Client::Client; bool msg_expect_test_prepare(std::string *, uint8_t *) noexcept override { return true; } - bool connect_tcp(const std::string &, const std::string &, - libndt::Socket *) noexcept override { + bool connect_tcp_maybe_socks5(const std::string &, const std::string &, + libndt::Socket *) noexcept override { return false; } }; -TEST_CASE("Client::run_download() deals with Client::connect_tcp() failure") { - FailConnectTcp client; +TEST_CASE( + "Client::run_download() deals with Client::connect_tcp_maybe_socks5() " + "failure") { + FailConnectTcpMaybeSocks5 client; REQUIRE(client.run_download() == false); } @@ -584,8 +587,8 @@ class FailMsgExpectEmpty : public libndt::Client { bool msg_expect_test_prepare(std::string *, uint8_t *) noexcept override { return true; } - bool connect_tcp(const std::string &, const std::string &, - libndt::Socket *sock) noexcept override { + bool connect_tcp_maybe_socks5(const std::string &, const std::string &, + libndt::Socket *sock) noexcept override { *sock = 17 /* Something "valid" */; return true; } @@ -604,8 +607,8 @@ class FailSelectDuringDownload : public libndt::Client { bool msg_expect_test_prepare(std::string *, uint8_t *) noexcept override { return true; } - bool connect_tcp(const std::string &, const std::string &, - libndt::Socket *sock) noexcept override { + bool connect_tcp_maybe_socks5(const std::string &, const std::string &, + libndt::Socket *sock) noexcept override { *sock = 17 /* Something "valid" */; return true; } @@ -627,8 +630,8 @@ class FailRecvDuringDownload : public libndt::Client { bool msg_expect_test_prepare(std::string *, uint8_t *) noexcept override { return true; } - bool connect_tcp(const std::string &, const std::string &, - libndt::Socket *sock) noexcept override { + bool connect_tcp_maybe_socks5(const std::string &, const std::string &, + libndt::Socket *sock) noexcept override { *sock = 17 /* Something "valid" */; return true; } @@ -653,8 +656,8 @@ class RecvEofDuringDownload : public libndt::Client { bool msg_expect_test_prepare(std::string *, uint8_t *) noexcept override { return true; } - bool connect_tcp(const std::string &, const std::string &, - libndt::Socket *sock) noexcept override { + bool connect_tcp_maybe_socks5(const std::string &, const std::string &, + libndt::Socket *sock) noexcept override { *sock = 17 /* Something "valid" */; return true; } @@ -680,8 +683,8 @@ class FailMsgReadLegacyDuringDownload : public libndt::Client { bool msg_expect_test_prepare(std::string *, uint8_t *) noexcept override { return true; } - bool connect_tcp(const std::string &, const std::string &, - libndt::Socket *sock) noexcept override { + bool connect_tcp_maybe_socks5(const std::string &, const std::string &, + libndt::Socket *sock) noexcept override { *sock = 17 /* Something "valid" */; return true; } @@ -709,8 +712,8 @@ class RecvNonTestMsgDuringDownload : public libndt::Client { bool msg_expect_test_prepare(std::string *, uint8_t *) noexcept override { return true; } - bool connect_tcp(const std::string &, const std::string &, - libndt::Socket *sock) noexcept override { + bool connect_tcp_maybe_socks5(const std::string &, const std::string &, + libndt::Socket *sock) noexcept override { *sock = 17 /* Something "valid" */; return true; } @@ -738,8 +741,8 @@ class FailMsgWriteDuringDownload : public libndt::Client { bool msg_expect_test_prepare(std::string *, uint8_t *) noexcept override { return true; } - bool connect_tcp(const std::string &, const std::string &, - libndt::Socket *sock) noexcept override { + bool connect_tcp_maybe_socks5(const std::string &, const std::string &, + libndt::Socket *sock) noexcept override { *sock = 17 /* Something "valid" */; return true; } @@ -768,8 +771,8 @@ class FailMsgReadDuringDownload : public libndt::Client { bool msg_expect_test_prepare(std::string *, uint8_t *) noexcept override { return true; } - bool connect_tcp(const std::string &, const std::string &, - libndt::Socket *sock) noexcept override { + bool connect_tcp_maybe_socks5(const std::string &, const std::string &, + libndt::Socket *sock) noexcept override { *sock = 17 /* Something "valid" */; return true; } @@ -799,8 +802,8 @@ class RecvNonTestOrLogoutMsgDuringDownload : public libndt::Client { bool msg_expect_test_prepare(std::string *, uint8_t *) noexcept override { return true; } - bool connect_tcp(const std::string &, const std::string &, - libndt::Socket *sock) noexcept override { + bool connect_tcp_maybe_socks5(const std::string &, const std::string &, + libndt::Socket *sock) noexcept override { *sock = 17 /* Something "valid" */; return true; } @@ -833,8 +836,8 @@ class FailEmitResultDuringDownload : public libndt::Client { bool msg_expect_test_prepare(std::string *, uint8_t *) noexcept override { return true; } - bool connect_tcp(const std::string &, const std::string &, - libndt::Socket *sock) noexcept override { + bool connect_tcp_maybe_socks5(const std::string &, const std::string &, + libndt::Socket *sock) noexcept override { *sock = 17 /* Something "valid" */; return true; } @@ -868,8 +871,8 @@ class TooManyTestMsgsDuringDownload : public libndt::Client { bool msg_expect_test_prepare(std::string *, uint8_t *) noexcept override { return true; } - bool connect_tcp(const std::string &, const std::string &, - libndt::Socket *sock) noexcept override { + bool connect_tcp_maybe_socks5(const std::string &, const std::string &, + libndt::Socket *sock) noexcept override { *sock = 17 /* Something "valid" */; return true; } @@ -990,8 +993,10 @@ TEST_CASE("Client::run_upload() deals with more than one flow") { REQUIRE(client.run_upload() == false); } -TEST_CASE("Client::run_upload() deals with Client::connect_tcp() failure") { - FailConnectTcp client; +TEST_CASE( + "Client::run_upload() deals with Client::connect_tcp_maybe_socks5() " + "failure") { + FailConnectTcpMaybeSocks5 client; REQUIRE(client.run_upload() == false); } @@ -1012,8 +1017,8 @@ class FailSendDuringUpload : public libndt::Client { bool msg_expect_test_prepare(std::string *, uint8_t *) noexcept override { return true; } - bool connect_tcp(const std::string &, const std::string &, - libndt::Socket *sock) noexcept override { + bool connect_tcp_maybe_socks5(const std::string &, const std::string &, + libndt::Socket *sock) noexcept override { *sock = 17 /* Something "valid" */; return true; } @@ -1046,8 +1051,8 @@ class FailMsgExpectDuringUpload : public libndt::Client { bool msg_expect_test_prepare(std::string *, uint8_t *) noexcept override { return true; } - bool connect_tcp(const std::string &, const std::string &, - libndt::Socket *sock) noexcept override { + bool connect_tcp_maybe_socks5(const std::string &, const std::string &, + libndt::Socket *sock) noexcept override { *sock = 17 /* Something "valid" */; return true; } @@ -1074,8 +1079,8 @@ class FailFinalMsgExpectEmptyDuringUpload : public libndt::Client { bool msg_expect_test_prepare(std::string *, uint8_t *) noexcept override { return true; } - bool connect_tcp(const std::string &, const std::string &, - libndt::Socket *sock) noexcept override { + bool connect_tcp_maybe_socks5(const std::string &, const std::string &, + libndt::Socket *sock) noexcept override { *sock = 17 /* Something "valid" */; return true; } @@ -1100,6 +1105,513 @@ TEST_CASE( REQUIRE(client.run_upload() == false); } +// Client::connect_tcp_maybe_socks5() tests +// ---------------------------------------- + +class FailConnectTcp : public libndt::Client { + public: + using libndt::Client::Client; + bool connect_tcp(const std::string &, const std::string &, + libndt::Socket *) noexcept override { + return false; + } +}; + +TEST_CASE("Client::connect_tcp_maybe_socks5() deals with Client::connect_tcp() " + "error when a socks5 port is specified") { + libndt::Settings settings; + settings.socks5h_port = "9050"; + FailConnectTcp client{settings}; + libndt::Socket sock = -1; + REQUIRE(!client.connect_tcp_maybe_socks5("www.google.com", "80", &sock)); +} + +class ConnectTcpMaybeSocks5FailFirstSendn : public libndt::Client { + public: + using libndt::Client::Client; + bool connect_tcp(const std::string &, const std::string &, + libndt::Socket *sock) noexcept override { + *sock = 17 /* Something "valid" */; + return true; + } + libndt::Ssize sendn(libndt::Socket, const void *, + libndt::Size) noexcept override { + return -1; + } +}; + +TEST_CASE("Client::connect_tcp_maybe_socks5() deals with Client::sendn() " + "failure when sending auth_request") { + libndt::Settings settings; + settings.socks5h_port = "9050"; + ConnectTcpMaybeSocks5FailFirstSendn client{settings}; + libndt::Socket sock = -1; + REQUIRE(!client.connect_tcp_maybe_socks5("www.google.com", "80", &sock)); +} + +class ConnectTcpMaybeSocks5FailFirstRecvn : public libndt::Client { + public: + using libndt::Client::Client; + bool connect_tcp(const std::string &, const std::string &, + libndt::Socket *sock) noexcept override { + *sock = 17 /* Something "valid" */; + return true; + } + libndt::Ssize sendn(libndt::Socket, const void *, + libndt::Size size) noexcept override { + return (libndt::Ssize)size; + } + libndt::Ssize recvn(libndt::Socket, void *, + libndt::Size) noexcept override { + return -1; + } +}; + +TEST_CASE("Client::connect_tcp_maybe_socks5() deals with Client::sendn() " + "failure when receiving auth_response") { + libndt::Settings settings; + settings.socks5h_port = "9050"; + ConnectTcpMaybeSocks5FailFirstRecvn client{settings}; + libndt::Socket sock = -1; + REQUIRE(!client.connect_tcp_maybe_socks5("www.google.com", "80", &sock)); +} + +class ConnectTcpMaybeSocks5InvalidAuthResponseVersion : public libndt::Client { + public: + using libndt::Client::Client; + bool connect_tcp(const std::string &, const std::string &, + libndt::Socket *sock) noexcept override { + *sock = 17 /* Something "valid" */; + return true; + } + libndt::Ssize sendn(libndt::Socket, const void *, + libndt::Size size) noexcept override { + return (libndt::Ssize)size; + } + libndt::Ssize recvn(libndt::Socket, void *buf, + libndt::Size size) noexcept override { + assert(size == 2); + ((char *)buf)[0] = 4; // unexpected + ((char *)buf)[1] = 0; + return (libndt::Size)size; + } +}; + +TEST_CASE("Client::connect_tcp_maybe_socks5() deals with invalid version " + "number in the auth_response") { + libndt::Settings settings; + settings.socks5h_port = "9050"; + ConnectTcpMaybeSocks5InvalidAuthResponseVersion client{settings}; + libndt::Socket sock = -1; + REQUIRE(!client.connect_tcp_maybe_socks5("www.google.com", "80", &sock)); +} + +class ConnectTcpMaybeSocks5InvalidAuthResponseMethod : public libndt::Client { + public: + using libndt::Client::Client; + bool connect_tcp(const std::string &, const std::string &, + libndt::Socket *sock) noexcept override { + *sock = 17 /* Something "valid" */; + return true; + } + libndt::Ssize sendn(libndt::Socket, const void *, + libndt::Size size) noexcept override { + return (libndt::Ssize)size; + } + libndt::Ssize recvn(libndt::Socket, void *buf, + libndt::Size size) noexcept override { + assert(size == 2); + ((char *)buf)[0] = 5; + ((char *)buf)[1] = 1; + return (libndt::Size)size; + } +}; + +TEST_CASE("Client::connect_tcp_maybe_socks5() deals with invalid method " + "number in the auth_response") { + libndt::Settings settings; + settings.socks5h_port = "9050"; + ConnectTcpMaybeSocks5InvalidAuthResponseMethod client{settings}; + libndt::Socket sock = -1; + REQUIRE(!client.connect_tcp_maybe_socks5("www.google.com", "80", &sock)); +} + +class ConnectTcpMaybeSocks5InitialHandshakeOkay : public libndt::Client { + public: + using libndt::Client::Client; + bool connect_tcp(const std::string &, const std::string &, + libndt::Socket *sock) noexcept override { + *sock = 17 /* Something "valid" */; + return true; + } + libndt::Ssize sendn(libndt::Socket, const void *, + libndt::Size size) noexcept override { + return (libndt::Ssize)size; + } + libndt::Ssize recvn(libndt::Socket, void *buf, + libndt::Size size) noexcept override { + assert(size == 2); + ((char *)buf)[0] = 5; + ((char *)buf)[1] = 0; + return (libndt::Size)size; + } +}; + +TEST_CASE("Client::connect_tcp_maybe_socks5() deals with too long hostname") { + libndt::Settings settings; + settings.socks5h_port = "9050"; + ConnectTcpMaybeSocks5InitialHandshakeOkay client{settings}; + libndt::Socket sock = -1; + std::string hostname; + for (size_t i = 0; i < 300; ++i) { + hostname += "A"; + } + REQUIRE(!client.connect_tcp_maybe_socks5(hostname, "80", &sock)); +} + +TEST_CASE("Client::connect_tcp_maybe_socks5() deals with invalid port") { + libndt::Settings settings; + settings.socks5h_port = "9050"; + ConnectTcpMaybeSocks5InitialHandshakeOkay client{settings}; + libndt::Socket sock = -1; + REQUIRE(!client.connect_tcp_maybe_socks5("www.google.com", "xx", &sock)); +} + +class ConnectTcpMaybeSocks5FailSecondSendn : public libndt::Client { + public: + using libndt::Client::Client; + bool connect_tcp(const std::string &, const std::string &, + libndt::Socket *sock) noexcept override { + *sock = 17 /* Something "valid" */; + return true; + } + libndt::Ssize sendn(libndt::Socket, const void *, + libndt::Size size) noexcept override { + return size == 3 ? (libndt::Ssize)size : -1; + } + libndt::Ssize recvn(libndt::Socket, void *buf, + libndt::Size size) noexcept override { + assert(size == 2); + ((char *)buf)[0] = 5; + ((char *)buf)[1] = 0; + return (libndt::Size)size; + } +}; + +TEST_CASE("Client::connect_tcp_maybe_socks5() deals with Client::sendn() " + "error while sending connect_request") { + libndt::Settings settings; + settings.socks5h_port = "9050"; + ConnectTcpMaybeSocks5FailSecondSendn client{settings}; + libndt::Socket sock = -1; + REQUIRE(!client.connect_tcp_maybe_socks5("www.google.com", "80", &sock)); +} + +class ConnectTcpMaybeSocks5FailSecondRecvn : public libndt::Client { + public: + using libndt::Client::Client; + bool connect_tcp(const std::string &, const std::string &, + libndt::Socket *sock) noexcept override { + *sock = 17 /* Something "valid" */; + return true; + } + libndt::Ssize sendn(libndt::Socket, const void *, + libndt::Size size) noexcept override { + return (libndt::Ssize)size; + } + libndt::Ssize recvn(libndt::Socket, void *buf, + libndt::Size size) noexcept override { + if (size == 2) { + ((char *)buf)[0] = 5; + ((char *)buf)[1] = 0; + return (libndt::Size)size; + } + return -1; + } +}; + +TEST_CASE("Client::connect_tcp_maybe_socks5() deals with Client::recvn() " + "error while receiving connect_response_hdr") { + libndt::Settings settings; + settings.socks5h_port = "9050"; + ConnectTcpMaybeSocks5FailSecondRecvn client{settings}; + libndt::Socket sock = -1; + REQUIRE(!client.connect_tcp_maybe_socks5("www.google.com", "80", &sock)); +} + +class ConnectTcpMaybeSocks5InvalidSecondVersion : public libndt::Client { + public: + using libndt::Client::Client; + bool connect_tcp(const std::string &, const std::string &, + libndt::Socket *sock) noexcept override { + *sock = 17 /* Something "valid" */; + return true; + } + libndt::Ssize sendn(libndt::Socket, const void *, + libndt::Size size) noexcept override { + return (libndt::Ssize)size; + } + libndt::Ssize recvn(libndt::Socket, void *buf, + libndt::Size size) noexcept override { + if (size == 2) { + ((char *)buf)[0] = 5; + ((char *)buf)[1] = 0; + return (libndt::Size)size; + } + if (size == 4) { + ((char *)buf)[0] = 4; // unexpected + ((char *)buf)[1] = 0; + return (libndt::Size)size; + } + return -1; + } +}; + +TEST_CASE("Client::connect_tcp_maybe_socks5() deals with receiving " + "invalid version number in second Client::recvn()") { + libndt::Settings settings; + settings.socks5h_port = "9050"; + ConnectTcpMaybeSocks5InvalidSecondVersion client{settings}; + libndt::Socket sock = -1; + REQUIRE(!client.connect_tcp_maybe_socks5("www.google.com", "80", &sock)); +} + +class ConnectTcpMaybeSocks5ErrorResult : public libndt::Client { + public: + using libndt::Client::Client; + bool connect_tcp(const std::string &, const std::string &, + libndt::Socket *sock) noexcept override { + *sock = 17 /* Something "valid" */; + return true; + } + libndt::Ssize sendn(libndt::Socket, const void *, + libndt::Size size) noexcept override { + return (libndt::Ssize)size; + } + libndt::Ssize recvn(libndt::Socket, void *buf, + libndt::Size size) noexcept override { + if (size == 2) { + ((char *)buf)[0] = 5; + ((char *)buf)[1] = 0; + return (libndt::Size)size; + } + if (size == 4) { + ((char *)buf)[0] = 5; + ((char *)buf)[1] = 1; // error occurred + return (libndt::Size)size; + } + return -1; + } +}; + +TEST_CASE("Client::connect_tcp_maybe_socks5() deals with receiving " + "an error code in second Client::recvn()") { + libndt::Settings settings; + settings.socks5h_port = "9050"; + ConnectTcpMaybeSocks5ErrorResult client{settings}; + libndt::Socket sock = -1; + REQUIRE(!client.connect_tcp_maybe_socks5("www.google.com", "80", &sock)); +} + +class ConnectTcpMaybeSocks5InvalidReserved : public libndt::Client { + public: + using libndt::Client::Client; + bool connect_tcp(const std::string &, const std::string &, + libndt::Socket *sock) noexcept override { + *sock = 17 /* Something "valid" */; + return true; + } + libndt::Ssize sendn(libndt::Socket, const void *, + libndt::Size size) noexcept override { + return (libndt::Ssize)size; + } + libndt::Ssize recvn(libndt::Socket, void *buf, + libndt::Size size) noexcept override { + if (size == 2) { + ((char *)buf)[0] = 5; + ((char *)buf)[1] = 0; + return (libndt::Size)size; + } + if (size == 4) { + ((char *)buf)[0] = 5; + ((char *)buf)[1] = 0; + ((char *)buf)[2] = 1; // should instead be zero + return (libndt::Size)size; + } + return -1; + } +}; + +TEST_CASE("Client::connect_tcp_maybe_socks5() deals with receiving " + "an invalid reserved field in second Client::recvn()") { + libndt::Settings settings; + settings.socks5h_port = "9050"; + ConnectTcpMaybeSocks5InvalidReserved client{settings}; + libndt::Socket sock = -1; + REQUIRE(!client.connect_tcp_maybe_socks5("www.google.com", "80", &sock)); +} + +class ConnectTcpMaybeSocks5FailAddressRecvn : public libndt::Client { + public: + using libndt::Client::Client; + bool connect_tcp(const std::string &, const std::string &, + libndt::Socket *sock) noexcept override { + *sock = 17 /* Something "valid" */; + return true; + } + libndt::Ssize sendn(libndt::Socket, const void *, + libndt::Size size) noexcept override { + return (libndt::Ssize)size; + } + uint8_t type = 0; + bool seen = false; + libndt::Ssize recvn(libndt::Socket, void *buf, + libndt::Size size) noexcept override { + if (size == 2) { + ((char *)buf)[0] = 5; + ((char *)buf)[1] = 0; + return (libndt::Size)size; + } + if (size == 4 && !seen) { + seen = true; // use flag because IPv4 is also 4 bytes + assert(type != 0); + ((char *)buf)[0] = 5; + ((char *)buf)[1] = 0; + ((char *)buf)[2] = 0; + ((char *)buf)[3] = type; + return (libndt::Size)size; + } + // the subsequent recvn() will fail + return -1; + } +}; + +TEST_CASE("Client::connect_tcp_maybe_socks5() deals with Client::recvn() " + "error when reading a IPv4") { + libndt::Settings settings; + settings.socks5h_port = "9050"; + ConnectTcpMaybeSocks5FailAddressRecvn client{settings}; + client.type = 1; + libndt::Socket sock = -1; + REQUIRE(!client.connect_tcp_maybe_socks5("www.google.com", "80", &sock)); +} + +TEST_CASE("Client::connect_tcp_maybe_socks5() deals with Client::recvn() " + "error when reading a IPv6") { + libndt::Settings settings; + settings.socks5h_port = "9050"; + ConnectTcpMaybeSocks5FailAddressRecvn client{settings}; + client.type = 4; + libndt::Socket sock = -1; + REQUIRE(!client.connect_tcp_maybe_socks5("www.google.com", "80", &sock)); +} + +TEST_CASE("Client::connect_tcp_maybe_socks5() deals with Client::recvn() " + "error when reading a invalid address type") { + libndt::Settings settings; + settings.socks5h_port = "9050"; + ConnectTcpMaybeSocks5FailAddressRecvn client{settings}; + client.type = 7; + libndt::Socket sock = -1; + REQUIRE(!client.connect_tcp_maybe_socks5("www.google.com", "80", &sock)); +} + +class ConnectTcpMaybeSocks5WithArray : public libndt::Client { + public: + using libndt::Client::Client; + bool connect_tcp(const std::string &, const std::string &, + libndt::Socket *sock) noexcept override { + *sock = 17 /* Something "valid" */; + return true; + } + libndt::Ssize sendn(libndt::Socket, const void *, + libndt::Size size) noexcept override { + return (libndt::Ssize)size; + } + std::deque array; + libndt::Ssize recvn(libndt::Socket, void *buf, + libndt::Size size) noexcept override { + if (!array.empty() && size == array[0].size()) { + for (size_t idx = 0; idx < array[0].size(); ++idx) { + ((char *)buf)[idx] = array[0][idx]; + } + array.pop_front(); + return (libndt::Ssize)size; + } + return -1; + } +}; + +TEST_CASE("Client::connect_tcp_maybe_socks5() deals with Client::recvn() " + "error when failing to read domain length") { + libndt::Settings settings; + settings.socks5h_port = "9050"; + ConnectTcpMaybeSocks5WithArray client{settings}; + client.array = { + std::string{"\5\0", 2}, + std::string{"\5\0\0\3", 4}, + }; + libndt::Socket sock = -1; + REQUIRE(!client.connect_tcp_maybe_socks5("www.google.com", "80", &sock)); +} + +TEST_CASE("Client::connect_tcp_maybe_socks5() deals with Client::recvn() " + "error when failing to read domain") { + libndt::Settings settings; + settings.socks5h_port = "9050"; + ConnectTcpMaybeSocks5WithArray client{settings}; + client.array = { + std::string{"\5\0", 2}, + std::string{"\5\0\0\3", 4}, + std::string{"\7", 1}, + }; + libndt::Socket sock = -1; + REQUIRE(!client.connect_tcp_maybe_socks5("www.google.com", "80", &sock)); +} + +TEST_CASE("Client::connect_tcp_maybe_socks5() deals with Client::recvn() " + "error when failing to read port") { + libndt::Settings settings; + settings.socks5h_port = "9050"; + ConnectTcpMaybeSocks5WithArray client{settings}; + client.array = { + std::string{"\5\0", 2}, + std::string{"\5\0\0\3", 4}, + std::string{"\7", 1}, + std::string{"123.org", 7}, + }; + libndt::Socket sock = -1; + REQUIRE(!client.connect_tcp_maybe_socks5("www.google.com", "80", &sock)); +} + +TEST_CASE("Client::connect_tcp_maybe_socks5() works with IPv4 (mocked)") { + libndt::Settings settings; + settings.socks5h_port = "9050"; + ConnectTcpMaybeSocks5WithArray client{settings}; + client.array = { + std::string{"\5\0", 2}, + std::string{"\5\0\0\1", 4}, + std::string{"\0\0\0\0", 4}, + std::string{"\0\0", 2}, + }; + libndt::Socket sock = -1; + REQUIRE(!!client.connect_tcp_maybe_socks5("www.google.com", "80", &sock)); +} + +TEST_CASE("Client::connect_tcp_maybe_socks5() works with IPv6 (mocked)") { + libndt::Settings settings; + settings.socks5h_port = "9050"; + ConnectTcpMaybeSocks5WithArray client{settings}; + client.array = { + std::string{"\5\0", 2}, + std::string{"\5\0\0\4", 4}, + std::string{"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", 16}, + std::string{"\0\0", 2}, + }; + libndt::Socket sock = -1; + REQUIRE(!!client.connect_tcp_maybe_socks5("www.google.com", "80", &sock)); +} + // Client::connect_tcp() tests // ---------------------------