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
+99 -5
View File
@@ -32,6 +32,7 @@
//#include <Winsock2.h>
//#include <Ws2tcpip.h>
#include <atomic>
#include <memory>
#include <string>
#include <boost/version.hpp>
#include <boost/asio/io_context.hpp>
@@ -280,6 +281,7 @@ namespace net_utils
try
{
m_ssl_socket->next_layer().close();
m_pending_read.reset();
// Set SSL options
// disable sslv2
@@ -323,14 +325,18 @@ namespace net_utils
inline
bool disconnect()
{
m_connected = false;
try
{
if(m_connected)
if(!shutdown_requested() && m_ssl_socket->next_layer().is_open())
{
m_connected = false;
if(m_ssl_options)
shutdown_ssl();
m_ssl_socket->next_layer().shutdown(boost::asio::ip::tcp::socket::shutdown_both);
// The peer or the TLS shutdown deadline may already have closed the connection.
boost::system::error_code ignored_ec;
m_ssl_socket->next_layer().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ignored_ec);
// shutdown() alone leaves is_open() true and repeats TLS shutdown on later calls.
m_ssl_socket->next_layer().close();
}
}
catch(const boost::system::system_error& /*er*/)
@@ -475,9 +481,61 @@ namespace net_utils
{
if (!m_connected || !m_ssl_socket->next_layer().is_open())
return false;
boost::system::error_code ec;
if (m_ssl_options)
{
// let Asio process TLS alerts and retain a byte of application data for recv
if (!m_pending_read)
{
const auto pending = std::make_shared<pending_read>();
// reconnect may replace the stream before this handler completes
const auto socket = m_ssl_socket;
socket->async_read_some(boost::asio::buffer(&pending->byte, 1),
[socket, pending](const boost::system::error_code& error, size_t) {
pending->error = error;
if (pending->handler)
{
auto handler = std::move(pending->handler);
handler(error, pending->byte);
}
});
m_pending_read = pending;
}
m_io_service.restart();
m_io_service.poll();
ec = m_pending_read->error;
}
else
{
// peek for EOF without consuming application data or changing the socket mode
auto& socket = m_ssl_socket->next_layer();
const bool non_blocking = socket.non_blocking();
socket.non_blocking(true, ec);
if (!ec)
{
char byte;
socket.receive(boost::asio::buffer(&byte, 1), boost::asio::ip::tcp::socket::message_peek, ec);
boost::system::error_code restore_error;
socket.non_blocking(non_blocking, restore_error);
if (restore_error)
{
m_connected = false;
return false;
}
}
}
if (ec && ec != boost::asio::error::would_block &&
ec != boost::asio::error::try_again && ec != boost::asio::error::interrupted)
{
MDEBUG("Peer closed idle connection, marking disconnected: " << ec.message());
m_connected = false;
return false;
}
if (ssl)
*ssl = m_ssl_options.support != ssl_support_t::e_ssl_support_disabled;
return true;
return m_connected;
}
inline
@@ -534,6 +592,7 @@ namespace net_utils
if (ec)
{
m_connected = false;
MTRACE("READ ENDS: Connection err_code " << ec.value());
if(ec == boost::asio::error::eof)
{
@@ -544,7 +603,6 @@ namespace net_utils
}
MDEBUG("Problems at read: " << ec.message());
m_connected = false;
return false;
}else
{
@@ -705,6 +763,13 @@ namespace net_utils
private:
struct pending_read
{
char byte = 0;
boost::system::error_code error = boost::asio::error::would_block;
std::function<void(const boost::system::error_code&, char)> handler;
};
bool shutdown_requested() const
{
return m_aborted.load();
@@ -785,6 +850,34 @@ namespace net_utils
void async_read(char* buff, size_t sz, boost::asio::detail::transfer_at_least_t transfer_at_least, handler_obj& hndlr)
{
// finish the probe before starting another TLS read
if (sz && m_pending_read)
{
const auto pending = m_pending_read;
auto on_read = [socket = m_ssl_socket, buff, sz, transfer_at_least, hndlr]
(const boost::system::error_code& error, char byte) mutable {
if (error)
{
hndlr(error, 0);
return;
}
buff[0] = byte;
if (!transfer_at_least(error, 1) || sz == 1)
hndlr(error, 1);
else
boost::asio::async_read(*socket, boost::asio::buffer(buff + 1, sz - 1),
[transfer_at_least](const boost::system::error_code& ec, size_t bytes) mutable {
return transfer_at_least(ec, bytes + 1);
},
[hndlr](const boost::system::error_code& ec, size_t bytes) mutable { hndlr(ec, bytes + 1); });
};
if (pending->error == boost::asio::error::would_block)
pending->handler = std::move(on_read);
m_pending_read.reset();
if (pending->error != boost::asio::error::would_block)
on_read(pending->error, pending->byte);
return;
}
if(m_ssl_options.support == ssl_support_t::e_ssl_support_disabled)
boost::asio::async_read(m_ssl_socket->next_layer(), boost::asio::buffer(buff, sz), transfer_at_least, hndlr);
else
@@ -796,6 +889,7 @@ namespace net_utils
boost::asio::io_context m_io_service;
boost::asio::ssl::context m_ctx;
std::shared_ptr<boost::asio::ssl::stream<boost::asio::ip::tcp::socket>> m_ssl_socket;
std::shared_ptr<pending_read> m_pending_read;
std::function<connect_func> m_connector;
ssl_options_t m_ssl_options;
bool m_initialized;