mirror of
https://github.com/wownero/wownero.git
synced 2026-10-17 07:31:35 +02:00
net: detect half-closed sockets before connection reuse
Detect peer EOF and TLS closure before reusing idle connections. Make disconnect idempotent while preserving buffered data and pending operation lifetimes. Keep the release branch's multi-byte recv_n behavior, and cover partial EOF, socket modes, and TLS shutdown with loopback regression tests. Use fixed test certificates and retain compatibility with the release branch's Google Test version. Co-authored-by: woodser <13068859+woodser@users.noreply.github.com> (cherry picked from commit 096abdaa22b71ade9ca673ed8012c500cecb58d3)
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
# TLS unit-test credentials
|
||||
|
||||
`test.key` and `test.crt` are public, test-only credentials for loopback TLS tests.
|
||||
Never use this private key outside tests. The tests disable peer verification;
|
||||
they exercise connection and record handling, not certificate trust or validity.
|
||||
|
||||
Loading these fixtures avoids generating a new RSA key for each client and server
|
||||
context. They were generated with:
|
||||
|
||||
```sh
|
||||
openssl req -x509 -newkey rsa:4096 -sha256 -nodes \
|
||||
-keyout test.key -out test.crt -days 36500 \
|
||||
-subj '/CN=monero-unit-tests.invalid'
|
||||
```
|
||||
@@ -0,0 +1,30 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFKzCCAxOgAwIBAgIUd0AScwuUiPylHvlBJq4rFZaKN6IwDQYJKoZIhvcNAQEL
|
||||
BQAwJDEiMCAGA1UEAwwZbW9uZXJvLXVuaXQtdGVzdHMuaW52YWxpZDAgFw0yNjA5
|
||||
MjEwNTM2MDBaGA8yMTI2MDgyODA1MzYwMFowJDEiMCAGA1UEAwwZbW9uZXJvLXVu
|
||||
aXQtdGVzdHMuaW52YWxpZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
|
||||
AJ+j7lLEHRJSORlw9mR6Stz99TC6KaWqp/KXPKQzQzomhfqrr9MCm07gp5VrmIrD
|
||||
MU2tObhsJdq4u+8q1FDwZ781O9qm17sBDWwiF/OCEIWMiILqF+FmwunbJWrn9EV8
|
||||
70aWbqRo6INcA2SvgMqigXPQ4GMtCBsL8ft/WyMmavTlR1rqzWpMvxoPlYFmMS0X
|
||||
CfSEOb9/PoRyBqa2eTjVveqbwPlFAuKy1xX37nNJGEaqCZEhr1qdQ3fAZhAkPcAs
|
||||
FPMk7XhQH0Bpcrwi6ld4JlyisQgg0lP4hjU+RcUOy5ZMxV+cWX8FNs9pek7XxjVB
|
||||
UpIunMmemwUNzR8r5Wzn8qHKzcn5KR56iy+c7nEq1LrHsAsNnaELuDRYOrJdLoWo
|
||||
e+qt6oUIYzkt2COQuDQrQ8dsehpsbpYnrptfv0+4xn1hngw3zvZHNJG+v+Z2Cp0Q
|
||||
9fBBHJWfLq+wylKzp2O0S3HXqc36CIi8qwGVhT8DUKsoYg/OLZUckxjRMpUqq+8w
|
||||
TPDA0viiXx/k8rm7iEfSkGf0Q9aC7YbH+E8lg5J+xCb7Uis3+nwsmCwG1yyUn1Dk
|
||||
qCL28MeB42MSunhaG/vVg2Mc8SRS9BTj7V1itnbwI4vCRAdjp0KCn95tNhTuujBw
|
||||
OR/wBcoEtYaAtZpOi5A9J3ZSXFuJYEEZe0tSbGrRn2ZhAgMBAAGjUzBRMB0GA1Ud
|
||||
DgQWBBSIboly3I6SE/1UixxQOVRp/6uoTTAfBgNVHSMEGDAWgBSIboly3I6SE/1U
|
||||
ixxQOVRp/6uoTTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQA1
|
||||
bXapt3SuuZGvCU13+4K9TVJDX15TYqga1NJbomoUsFtvcio2PNGwPLj8O3Po6god
|
||||
W+uEpJNF30FT8AzDxy90dRSBLjVBhfuHyNb0YAdkSVejndvbH0Pe25eZ/o4dHRIz
|
||||
Xg6slE2192K6Rahq6Yeb7npNfanrdt2akNMidEzwno1BTQ78kr1jWdvaUd+HXIMT
|
||||
QgUuKYvEKE93lB7elgkEtlNp8sHWdTF8h78VxdDoyV+1RBAz5C5v9HppEekUaeia
|
||||
a5h50SkkltADfWhE9bDv/vqpjJ8FyHYsyqNJbLbHWMYnlP0/AJblGDst0D4XH0ZV
|
||||
++iH2bLxM9AITytZOdoPtvT5cvUNqYtyvIlq5OCrCBGcvo681yiBm7azuIFt7fS5
|
||||
Al6IpUi0MfdqpefYqVAm5uXDcDrZQuxauXdeRqrM8QJ4HxbK9STMzHVDmx3wkEA2
|
||||
KXDiNE+EfwH76ae3SS0cPIw/ng5mctQ5aLlmP7Ezt14bQ0/dJBH8Jpj6uUj2R6s+
|
||||
ASEPm20qtnwDAq1nSceKAvh8HwOlaTc4SKr7faAIIRhfPXIJQN0DqJhQWgcCN/Zu
|
||||
CTg/dFP/fe4TM2L9/tRXiArp/JSdx+7n9J68krU7Vnq1MJ43dAE4TlGEBNNPMzDM
|
||||
hG7YKywPL38eWhv0rVNUE9B7nHvfZVC82Wl1vUGzBQ==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,52 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
MEPbK7BIR9js26unXYswf1Cy8rfO
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -76,6 +76,7 @@
|
||||
#include "p2p/net_peerlist_boost_serialization.h"
|
||||
#include "serialization/keyvalue_serialization.h"
|
||||
#include "storages/portable_storage.h"
|
||||
#include "unit_tests_utils.h"
|
||||
|
||||
TEST(host, canonicalize_host)
|
||||
{
|
||||
@@ -270,6 +271,527 @@ TEST(blocked_mode_client, shutdown_is_permanent)
|
||||
EXPECT_FALSE(client.connect("127.0.0.1", port, std::chrono::seconds{5}));
|
||||
}
|
||||
|
||||
TEST(blocked_mode_client, eof_marks_disconnected_and_allows_reconnect)
|
||||
{
|
||||
boost::asio::io_context server_io;
|
||||
boost::asio::ip::tcp::acceptor acceptor{
|
||||
server_io,
|
||||
{boost::asio::ip::address_v4::loopback(), 0}
|
||||
};
|
||||
|
||||
epee::net_utils::blocked_mode_client client;
|
||||
client.set_ssl(epee::net_utils::ssl_options_t{
|
||||
epee::net_utils::ssl_support_t::e_ssl_support_disabled
|
||||
});
|
||||
const std::string port = std::to_string(acceptor.local_endpoint().port());
|
||||
ASSERT_TRUE(client.connect("127.0.0.1", port, std::chrono::seconds{5}));
|
||||
|
||||
boost::system::error_code error;
|
||||
boost::asio::ip::tcp::socket first_peer{server_io};
|
||||
acceptor.accept(first_peer, error);
|
||||
ASSERT_FALSE(error);
|
||||
boost::asio::write(first_peer, boost::asio::buffer("x", 1), error);
|
||||
ASSERT_FALSE(error);
|
||||
first_peer.shutdown(boost::asio::ip::tcp::socket::shutdown_send, error);
|
||||
ASSERT_FALSE(error);
|
||||
|
||||
std::string response;
|
||||
ASSERT_TRUE(client.recv(response, std::chrono::seconds{5}));
|
||||
EXPECT_EQ("x", response);
|
||||
|
||||
ASSERT_TRUE(client.recv(response, std::chrono::seconds{5}));
|
||||
EXPECT_TRUE(response.empty());
|
||||
EXPECT_FALSE(client.is_connected());
|
||||
|
||||
ASSERT_TRUE(client.connect("127.0.0.1", port, std::chrono::seconds{5}));
|
||||
boost::asio::ip::tcp::socket second_peer{server_io};
|
||||
acceptor.accept(second_peer, error);
|
||||
ASSERT_FALSE(error);
|
||||
boost::asio::write(second_peer, boost::asio::buffer("y", 1), error);
|
||||
ASSERT_FALSE(error);
|
||||
ASSERT_TRUE(client.recv(response, std::chrono::seconds{5}));
|
||||
EXPECT_EQ("y", response);
|
||||
}
|
||||
|
||||
TEST(blocked_mode_client, peer_eof_detected_before_reuse)
|
||||
{
|
||||
boost::asio::io_context server_io;
|
||||
boost::asio::ip::tcp::acceptor acceptor{
|
||||
server_io,
|
||||
{boost::asio::ip::address_v4::loopback(), 0}
|
||||
};
|
||||
|
||||
struct test_client : epee::net_utils::blocked_mode_client
|
||||
{
|
||||
boost::asio::ip::tcp::socket& socket() { return m_ssl_socket->next_layer(); }
|
||||
} client;
|
||||
client.set_ssl(epee::net_utils::ssl_options_t{
|
||||
epee::net_utils::ssl_support_t::e_ssl_support_disabled
|
||||
});
|
||||
const std::string port = std::to_string(acceptor.local_endpoint().port());
|
||||
ASSERT_TRUE(client.connect("127.0.0.1", port, std::chrono::seconds{5}));
|
||||
|
||||
boost::system::error_code error;
|
||||
boost::asio::ip::tcp::socket peer{server_io};
|
||||
acceptor.accept(peer, error);
|
||||
ASSERT_FALSE(error);
|
||||
for (const bool non_blocking : {false, true})
|
||||
{
|
||||
client.socket().non_blocking(non_blocking);
|
||||
boost::asio::write(peer, boost::asio::buffer("x", 1));
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds{5};
|
||||
while (!client.socket().available() && std::chrono::steady_clock::now() < deadline)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds{1});
|
||||
ASSERT_GT(client.socket().available(), 0u);
|
||||
EXPECT_TRUE(client.is_connected());
|
||||
EXPECT_EQ(non_blocking, client.socket().non_blocking());
|
||||
std::string response;
|
||||
ASSERT_TRUE(client.recv(response, std::chrono::seconds{5}));
|
||||
EXPECT_EQ("x", response);
|
||||
}
|
||||
|
||||
// close the idle connection from the peer; is_connected must observe the EOF without a request failing first
|
||||
peer.shutdown(boost::asio::ip::tcp::socket::shutdown_send, error);
|
||||
ASSERT_FALSE(error);
|
||||
bool disconnected = false;
|
||||
for (int i = 0; i < 500 && !disconnected; ++i)
|
||||
{
|
||||
disconnected = !client.is_connected();
|
||||
if (!disconnected)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds{10});
|
||||
}
|
||||
EXPECT_TRUE(disconnected);
|
||||
|
||||
ASSERT_TRUE(client.connect("127.0.0.1", port, std::chrono::seconds{5}));
|
||||
boost::asio::ip::tcp::socket second_peer{server_io};
|
||||
acceptor.accept(second_peer, error);
|
||||
ASSERT_FALSE(error);
|
||||
EXPECT_TRUE(client.is_connected());
|
||||
}
|
||||
|
||||
TEST(blocked_mode_client, disconnect_is_idempotent_after_peer_close)
|
||||
{
|
||||
using tcp = boost::asio::ip::tcp;
|
||||
boost::asio::io_context io;
|
||||
tcp::acceptor acceptor{io, {boost::asio::ip::address_v4::loopback(), 0}};
|
||||
struct test_client : epee::net_utils::blocked_mode_client
|
||||
{
|
||||
bool socket_is_open() const { return m_ssl_socket->next_layer().is_open(); }
|
||||
} client;
|
||||
client.set_ssl(epee::net_utils::ssl_options_t{
|
||||
epee::net_utils::ssl_support_t::e_ssl_support_disabled
|
||||
});
|
||||
ASSERT_TRUE(client.connect("127.0.0.1", std::to_string(acceptor.local_endpoint().port()),
|
||||
std::chrono::seconds{5}));
|
||||
tcp::socket peer{io};
|
||||
acceptor.accept(peer);
|
||||
peer.shutdown(tcp::socket::shutdown_both);
|
||||
peer.close();
|
||||
std::string response;
|
||||
ASSERT_TRUE(client.recv(response, std::chrono::seconds{5}));
|
||||
ASSERT_TRUE(response.empty());
|
||||
for (unsigned i = 0; i < 3; ++i)
|
||||
{
|
||||
EXPECT_TRUE(client.disconnect());
|
||||
EXPECT_FALSE(client.socket_is_open());
|
||||
EXPECT_FALSE(client.is_connected());
|
||||
}
|
||||
// disconnect is not the permanent shutdown: reconnect must still work.
|
||||
ASSERT_TRUE(client.connect("127.0.0.1", std::to_string(acceptor.local_endpoint().port()),
|
||||
std::chrono::seconds{5}));
|
||||
acceptor.accept(peer);
|
||||
EXPECT_TRUE(client.is_connected());
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
class blocked_mode_client_ssl : public testing::TestWithParam<int>
|
||||
{
|
||||
protected:
|
||||
using tcp = boost::asio::ip::tcp;
|
||||
using ssl_stream = boost::asio::ssl::stream<tcp::socket>;
|
||||
|
||||
static epee::net_utils::ssl_options_t ssl_options()
|
||||
{
|
||||
epee::net_utils::ssl_options_t options{epee::net_utils::ssl_support_t::e_ssl_support_enabled};
|
||||
options.verification = epee::net_utils::ssl_verification_t::none;
|
||||
// Public test-only credentials avoid generating RSA keys for every context.
|
||||
options.auth = epee::net_utils::ssl_authentication_t{
|
||||
(unit_test::data_dir / "ssl" / "test.key").string(),
|
||||
(unit_test::data_dir / "ssl" / "test.crt").string()
|
||||
};
|
||||
return options;
|
||||
}
|
||||
|
||||
boost::asio::io_context io;
|
||||
boost::asio::ssl::context context{ssl_options().create_context()};
|
||||
tcp::acceptor acceptor{io, {boost::asio::ip::address_v4::loopback(), 0}};
|
||||
std::unique_ptr<ssl_stream> peer;
|
||||
struct test_client : epee::net_utils::blocked_mode_client
|
||||
{
|
||||
bool socket_is_open() const { return m_ssl_socket->next_layer().is_open(); }
|
||||
} client;
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
#if (OPENSSL_VERSION_NUMBER >= 0x10101000L && !defined(LIBRESSL_VERSION_NUMBER)) \
|
||||
|| (defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER >= 0x2070000fL)
|
||||
ASSERT_EQ(1, SSL_CTX_set_min_proto_version(context.native_handle(), GetParam()));
|
||||
ASSERT_EQ(1, SSL_CTX_set_max_proto_version(context.native_handle(), GetParam()));
|
||||
#endif
|
||||
client.set_ssl(ssl_options());
|
||||
ASSERT_TRUE(connect());
|
||||
ASSERT_EQ(GetParam(), SSL_version(peer->native_handle()));
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
boost::system::error_code ignored;
|
||||
if (peer)
|
||||
peer->next_layer().close(ignored);
|
||||
io.restart();
|
||||
io.poll();
|
||||
}
|
||||
|
||||
bool connect()
|
||||
{
|
||||
if (peer)
|
||||
{
|
||||
boost::system::error_code ignored;
|
||||
peer->next_layer().close(ignored);
|
||||
io.restart();
|
||||
io.poll();
|
||||
}
|
||||
peer.reset(new ssl_stream{io, context});
|
||||
io.restart();
|
||||
boost::system::error_code server_error = boost::asio::error::would_block;
|
||||
boost::asio::steady_timer deadline{io, std::chrono::seconds{5}};
|
||||
deadline.async_wait([this](const boost::system::error_code& error) {
|
||||
if (!error)
|
||||
{
|
||||
boost::system::error_code ignored;
|
||||
acceptor.cancel(ignored);
|
||||
peer->next_layer().close(ignored);
|
||||
}
|
||||
});
|
||||
acceptor.async_accept(peer->next_layer(), [&](const boost::system::error_code& error) {
|
||||
if (error)
|
||||
{
|
||||
server_error = error;
|
||||
deadline.cancel();
|
||||
return;
|
||||
}
|
||||
peer->async_handshake(boost::asio::ssl::stream_base::server,
|
||||
[&](const boost::system::error_code& error) {
|
||||
server_error = error;
|
||||
deadline.cancel();
|
||||
});
|
||||
});
|
||||
std::thread server{[this] { io.run(); }};
|
||||
const bool connected = client.connect("127.0.0.1",
|
||||
std::to_string(acceptor.local_endpoint().port()), std::chrono::seconds{5});
|
||||
server.join();
|
||||
return connected && !server_error;
|
||||
}
|
||||
|
||||
bool wait_for_disconnect()
|
||||
{
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds{2};
|
||||
do
|
||||
{
|
||||
if (!client.is_connected())
|
||||
return true;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds{1});
|
||||
} while (std::chrono::steady_clock::now() < deadline);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
TEST_P(blocked_mode_client_ssl, close_notify_detected_before_reuse)
|
||||
{
|
||||
bool ssl = false;
|
||||
ASSERT_TRUE(client.is_connected(&ssl));
|
||||
ASSERT_TRUE(ssl);
|
||||
ASSERT_TRUE(client.send("x", std::chrono::seconds{5}));
|
||||
char request = 0;
|
||||
boost::asio::read(*peer, boost::asio::buffer(&request, 1));
|
||||
ASSERT_EQ('x', request);
|
||||
boost::asio::write(*peer, boost::asio::buffer("y", 1));
|
||||
std::string response;
|
||||
ASSERT_TRUE(client.recv(response, std::chrono::seconds{5}));
|
||||
ASSERT_EQ("y", response);
|
||||
ASSERT_TRUE(client.is_connected());
|
||||
// close only after the client has received the response; leave TCP open
|
||||
// while waiting for the client's alert
|
||||
io.restart();
|
||||
peer->async_shutdown([](const boost::system::error_code&) {});
|
||||
io.poll();
|
||||
ASSERT_TRUE(wait_for_disconnect());
|
||||
ASSERT_TRUE(connect());
|
||||
ASSERT_TRUE(client.send("x", std::chrono::seconds{5}));
|
||||
request = 0;
|
||||
boost::asio::read(*peer, boost::asio::buffer(&request, 1));
|
||||
EXPECT_EQ('x', request);
|
||||
}
|
||||
|
||||
TEST_P(blocked_mode_client_ssl, preserves_unread_data)
|
||||
{
|
||||
boost::asio::write(*peer, boost::asio::buffer("xyz", 3));
|
||||
EXPECT_TRUE(client.is_connected());
|
||||
EXPECT_TRUE(client.is_connected());
|
||||
std::string response, received;
|
||||
while (received.size() < 3)
|
||||
{
|
||||
ASSERT_TRUE(client.recv(response, std::chrono::seconds{5}));
|
||||
ASSERT_FALSE(response.empty());
|
||||
received += response;
|
||||
}
|
||||
EXPECT_EQ("xyz", received);
|
||||
EXPECT_EQ(3u, client.get_bytes_received());
|
||||
EXPECT_TRUE(client.is_connected());
|
||||
}
|
||||
|
||||
TEST_P(blocked_mode_client_ssl, abrupt_eof_detected_before_reuse)
|
||||
{
|
||||
peer->next_layer().shutdown(tcp::socket::shutdown_send);
|
||||
EXPECT_TRUE(wait_for_disconnect());
|
||||
}
|
||||
|
||||
TEST_P(blocked_mode_client_ssl, partial_record_does_not_block)
|
||||
{
|
||||
// an incomplete TLS record must not turn a status check into a blocking read
|
||||
const unsigned char header[] = {0x17, 0x03, 0x03, 0x00, 0x20};
|
||||
boost::asio::write(peer->next_layer(), boost::asio::buffer(header));
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
EXPECT_TRUE(client.is_connected());
|
||||
EXPECT_LT(std::chrono::steady_clock::now() - start, std::chrono::seconds{1});
|
||||
peer->next_layer().shutdown(tcp::socket::shutdown_send);
|
||||
EXPECT_TRUE(wait_for_disconnect());
|
||||
}
|
||||
|
||||
TEST_P(blocked_mode_client_ssl, idle_probe_allows_request_and_response)
|
||||
{
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
for (unsigned i = 0; i < 10; ++i)
|
||||
ASSERT_TRUE(client.is_connected());
|
||||
EXPECT_LT(std::chrono::steady_clock::now() - start, std::chrono::seconds{1});
|
||||
ASSERT_TRUE(client.send("x", std::chrono::seconds{5}));
|
||||
char request = 0;
|
||||
boost::asio::read(*peer, boost::asio::buffer(&request, 1));
|
||||
EXPECT_EQ('x', request);
|
||||
boost::asio::write(*peer, boost::asio::buffer("y", 1));
|
||||
std::string response;
|
||||
ASSERT_TRUE(client.recv(response, std::chrono::seconds{5}));
|
||||
EXPECT_EQ("y", response);
|
||||
EXPECT_EQ(1u, client.get_bytes_received());
|
||||
}
|
||||
|
||||
TEST_P(blocked_mode_client_ssl, reconnect_with_pending_probe)
|
||||
{
|
||||
ASSERT_TRUE(client.is_connected());
|
||||
ASSERT_TRUE(connect());
|
||||
boost::asio::write(*peer, boost::asio::buffer("y", 1));
|
||||
std::string response;
|
||||
ASSERT_TRUE(client.recv(response, std::chrono::seconds{5}));
|
||||
EXPECT_EQ("y", response);
|
||||
}
|
||||
|
||||
TEST_P(blocked_mode_client_ssl, shutdown_aborts_pending_probe)
|
||||
{
|
||||
ASSERT_TRUE(client.is_connected());
|
||||
std::thread stopper{[this] {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds{100});
|
||||
client.shutdown();
|
||||
}};
|
||||
std::string response;
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
EXPECT_FALSE(client.recv(response, std::chrono::seconds{30}));
|
||||
EXPECT_LT(std::chrono::steady_clock::now() - start, std::chrono::seconds{5});
|
||||
stopper.join();
|
||||
EXPECT_FALSE(client.is_connected());
|
||||
}
|
||||
|
||||
TEST_P(blocked_mode_client_ssl, data_before_close_notify_is_preserved)
|
||||
{
|
||||
boost::asio::write(*peer, boost::asio::buffer("xyz", 3));
|
||||
io.restart();
|
||||
peer->async_shutdown([](const boost::system::error_code&) {});
|
||||
io.poll();
|
||||
ASSERT_TRUE(client.is_connected());
|
||||
std::string response, received;
|
||||
while (received.size() < 3)
|
||||
{
|
||||
ASSERT_TRUE(client.recv(response, std::chrono::seconds{5}));
|
||||
ASSERT_FALSE(response.empty());
|
||||
received += response;
|
||||
}
|
||||
EXPECT_EQ("xyz", received);
|
||||
EXPECT_EQ(3u, client.get_bytes_received());
|
||||
EXPECT_TRUE(wait_for_disconnect());
|
||||
}
|
||||
|
||||
#if OPENSSL_VERSION_NUMBER >= 0x10101000L && !defined(LIBRESSL_VERSION_NUMBER)
|
||||
// This branch's Google Test predates GTEST_SKIP: only instantiate this test for TLS 1.3.
|
||||
class blocked_mode_client_ssl_tls13 : public blocked_mode_client_ssl {};
|
||||
|
||||
TEST_P(blocked_mode_client_ssl_tls13, key_update_allows_request_and_response)
|
||||
{
|
||||
ASSERT_EQ(1, SSL_key_update(peer->native_handle(), SSL_KEY_UPDATE_REQUESTED));
|
||||
boost::asio::write(*peer, boost::asio::buffer("x", 1));
|
||||
ASSERT_TRUE(client.is_connected());
|
||||
std::string response;
|
||||
ASSERT_TRUE(client.recv(response, std::chrono::seconds{5}));
|
||||
EXPECT_EQ("x", response);
|
||||
ASSERT_TRUE(client.send("y", std::chrono::seconds{5}));
|
||||
char request = 0;
|
||||
boost::asio::read(*peer, boost::asio::buffer(&request, 1));
|
||||
EXPECT_EQ('y', request);
|
||||
}
|
||||
INSTANTIATE_TEST_CASE_P(tls13, blocked_mode_client_ssl_tls13, testing::Values(TLS1_3_VERSION));
|
||||
#endif
|
||||
|
||||
TEST_P(blocked_mode_client_ssl, recv_times_out_with_pending_probe)
|
||||
{
|
||||
ASSERT_TRUE(client.is_connected());
|
||||
std::string response;
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
EXPECT_FALSE(client.recv(response, std::chrono::milliseconds{100}));
|
||||
EXPECT_LT(std::chrono::steady_clock::now() - start, std::chrono::seconds{5});
|
||||
EXPECT_FALSE(client.is_connected());
|
||||
}
|
||||
|
||||
TEST_P(blocked_mode_client_ssl, disconnect_with_pending_probe_is_graceful)
|
||||
{
|
||||
ASSERT_TRUE(client.is_connected());
|
||||
boost::system::error_code peer_error = boost::asio::error::would_block;
|
||||
char byte;
|
||||
io.restart();
|
||||
boost::asio::steady_timer deadline{io, std::chrono::seconds{5}};
|
||||
deadline.async_wait([this](const boost::system::error_code& error) {
|
||||
if (!error)
|
||||
{
|
||||
boost::system::error_code ignored;
|
||||
peer->next_layer().close(ignored);
|
||||
}
|
||||
});
|
||||
peer->async_read_some(boost::asio::buffer(&byte, 1),
|
||||
[&](const boost::system::error_code& error, size_t) {
|
||||
EXPECT_EQ(boost::asio::error::eof, error);
|
||||
if (error != boost::asio::error::eof)
|
||||
{
|
||||
peer_error = boost::asio::error::fault;
|
||||
deadline.cancel();
|
||||
}
|
||||
else
|
||||
peer->async_shutdown([&](const boost::system::error_code& error) {
|
||||
peer_error = error;
|
||||
deadline.cancel();
|
||||
});
|
||||
});
|
||||
std::thread server{[this] { io.run(); }};
|
||||
EXPECT_TRUE(client.disconnect());
|
||||
server.join();
|
||||
EXPECT_FALSE(peer_error);
|
||||
EXPECT_FALSE(client.socket_is_open());
|
||||
// Repeated disconnects must not attempt another TLS shutdown, even after the peer closes.
|
||||
peer->next_layer().close();
|
||||
EXPECT_TRUE(client.disconnect());
|
||||
EXPECT_TRUE(client.disconnect());
|
||||
EXPECT_FALSE(client.is_connected());
|
||||
ASSERT_TRUE(connect());
|
||||
EXPECT_TRUE(client.is_connected());
|
||||
}
|
||||
|
||||
TEST_P(blocked_mode_client_ssl, disconnect_after_peer_eof_is_graceful)
|
||||
{
|
||||
boost::system::error_code peer_error = boost::asio::error::would_block;
|
||||
io.restart();
|
||||
boost::asio::steady_timer deadline{io, std::chrono::seconds{5}};
|
||||
deadline.async_wait([this](const boost::system::error_code& error) {
|
||||
if (!error)
|
||||
{
|
||||
boost::system::error_code ignored;
|
||||
peer->next_layer().close(ignored);
|
||||
}
|
||||
});
|
||||
peer->async_shutdown([&](const boost::system::error_code& error) {
|
||||
peer_error = error;
|
||||
deadline.cancel();
|
||||
});
|
||||
std::thread server{[this] { io.run(); }};
|
||||
std::string response;
|
||||
EXPECT_TRUE(client.recv(response, std::chrono::seconds{5}));
|
||||
EXPECT_TRUE(response.empty());
|
||||
EXPECT_FALSE(client.is_connected());
|
||||
EXPECT_TRUE(client.disconnect());
|
||||
server.join();
|
||||
EXPECT_FALSE(peer_error);
|
||||
EXPECT_FALSE(client.socket_is_open());
|
||||
peer->next_layer().close();
|
||||
EXPECT_TRUE(client.disconnect());
|
||||
EXPECT_TRUE(client.disconnect());
|
||||
}
|
||||
|
||||
TEST_P(blocked_mode_client_ssl, disconnect_after_abrupt_eof_is_idempotent)
|
||||
{
|
||||
peer->next_layer().shutdown(tcp::socket::shutdown_both);
|
||||
peer->next_layer().close();
|
||||
ASSERT_TRUE(wait_for_disconnect());
|
||||
for (unsigned i = 0; i < 3; ++i)
|
||||
{
|
||||
EXPECT_TRUE(client.disconnect());
|
||||
EXPECT_FALSE(client.socket_is_open());
|
||||
EXPECT_FALSE(client.is_connected());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(blocked_mode_client_ssl, disconnect_after_shutdown_does_not_wait_for_peer)
|
||||
{
|
||||
ASSERT_TRUE(client.is_connected());
|
||||
ASSERT_TRUE(client.shutdown());
|
||||
// permanent shutdown must not wait for the peer's TLS close alert
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
EXPECT_TRUE(client.disconnect());
|
||||
EXPECT_LT(std::chrono::steady_clock::now() - start, std::chrono::seconds{1});
|
||||
EXPECT_FALSE(client.is_connected());
|
||||
}
|
||||
|
||||
TEST_P(blocked_mode_client_ssl, recv_n_preserves_probe_byte_and_count)
|
||||
{
|
||||
ASSERT_TRUE(client.is_connected());
|
||||
std::string response;
|
||||
ASSERT_TRUE(client.recv_n(response, 0, std::chrono::seconds{5}));
|
||||
EXPECT_TRUE(response.empty());
|
||||
const std::string expected(100000, 'x');
|
||||
boost::asio::write(*peer, boost::asio::buffer(expected));
|
||||
ASSERT_TRUE(client.recv_n(response, expected.size(), std::chrono::seconds{5}));
|
||||
EXPECT_EQ(expected, response);
|
||||
EXPECT_EQ(expected.size(), client.get_bytes_received());
|
||||
}
|
||||
|
||||
TEST_P(blocked_mode_client_ssl, recv_n_returns_false_on_eof_after_probe_byte)
|
||||
{
|
||||
// Unlike master, this branch still needs the multi-byte async_read path for recv_n.
|
||||
ASSERT_TRUE(client.is_connected());
|
||||
boost::asio::write(*peer, boost::asio::buffer("x", 1));
|
||||
io.restart();
|
||||
peer->async_shutdown([](const boost::system::error_code&) {});
|
||||
io.poll();
|
||||
std::string response;
|
||||
EXPECT_FALSE(client.recv_n(response, 2, std::chrono::seconds{5}));
|
||||
EXPECT_FALSE(client.is_connected());
|
||||
ASSERT_EQ(2u, response.size());
|
||||
EXPECT_EQ('x', response[0]);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(tls, blocked_mode_client_ssl, testing::Values(
|
||||
TLS1_2_VERSION
|
||||
#if OPENSSL_VERSION_NUMBER >= 0x10101000L && !defined(LIBRESSL_VERSION_NUMBER)
|
||||
, TLS1_3_VERSION
|
||||
#endif
|
||||
));
|
||||
|
||||
TEST(tor_address, constants)
|
||||
{
|
||||
static_assert(!net::tor_address::is_local(), "bad is_local() response");
|
||||
|
||||
Reference in New Issue
Block a user