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:
Guoqiang Liu
2026-10-04 18:50:24 -04:00
committed by such-gitea
parent a53871e588
commit 6d9aca92f1
5 changed files with 717 additions and 5 deletions
+14
View File
@@ -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'
```
+30
View File
@@ -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-----
+52
View File
@@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
MEPbK7BIR9js26unXYswf1Cy8rfO
-----END PRIVATE KEY-----
+522
View File
@@ -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");