Add socket-level websocket connection health checking

Implements proper socket-level health checking for gateway websocket connections:

- Add is_connection_alive() method to GatewayTransceiver trait
- Implement socket-level health check using TcpStream::peek() on Unix systems
- Add is_gateway_connection_alive() method to MixnetClient for easy access
- Update network monitor to use connection health checks before sending
- Fix axum router state configuration in network monitor

The health check performs actual OS-level socket validation rather than just
checking file descriptor existence, providing reliable connection status.
This commit is contained in:
durch
2025-06-06 12:15:13 +02:00
parent cab072f2d0
commit b45bb9e7e9
6 changed files with 154 additions and 5 deletions
@@ -36,6 +36,9 @@ pub trait GatewayTransceiver: GatewaySender + GatewayReceiver {
&mut self,
message: ClientRequest,
) -> Result<(), GatewayClientError>;
/// Check if the websocket connection to the gateway is alive
fn is_connection_alive(&self) -> bool;
}
/// This trait defines the functionality of sending `MixPacket` into the mixnet,
@@ -90,6 +93,11 @@ impl<G: GatewayTransceiver + ?Sized + Send> GatewayTransceiver for Box<G> {
log::debug!("Sent client request: {:?}", message);
Ok(())
}
#[inline]
fn is_connection_alive(&self) -> bool {
(**self).is_connection_alive()
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -147,6 +155,10 @@ where
) -> Result<(), GatewayClientError> {
self.gateway_client.send_client_request(message).await
}
fn is_connection_alive(&self) -> bool {
self.gateway_client.is_connection_alive()
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -234,6 +246,11 @@ mod nonwasm_sealed {
) -> Result<(), GatewayClientError> {
Ok(())
}
fn is_connection_alive(&self) -> bool {
// LocalGateway is always "connected" since it's in-process
true
}
}
#[async_trait]
@@ -316,4 +333,9 @@ impl GatewayTransceiver for MockGateway {
) -> Result<(), GatewayClientError> {
Ok(())
}
fn is_connection_alive(&self) -> bool {
// MockGateway is always "connected" for testing purposes
true
}
}
@@ -164,6 +164,24 @@ impl<C, St> GatewayClient<C, St> {
pub fn remaining_bandwidth(&self) -> i64 {
self.bandwidth.remaining()
}
pub fn is_connection_established(&self) -> bool {
self.connection.is_established()
}
/// Check if the websocket connection is actually alive at the socket level
pub fn is_connection_alive(&self) -> bool {
// First check if we have an established connection
if !self.connection.is_established() {
return false;
}
// Get the file descriptor and check if the socket is alive
match self.ws_fd() {
Some(fd) => socket_is_alive(fd),
None => false,
}
}
#[cfg(not(target_arch = "wasm32"))]
async fn _close_connection(&mut self) -> Result<(), GatewayClientError> {
@@ -1128,3 +1146,49 @@ impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
}
}
}
/// Check if a socket file descriptor is alive and responsive
///
/// This function performs socket-level checks to determine if the connection is actually alive.
/// It's cross-platform compatible and works on both Unix and non-Unix systems.
fn socket_is_alive(fd: RawFd) -> bool {
#[cfg(unix)]
{
use std::os::unix::io::FromRawFd;
use std::net::TcpStream;
use std::io::ErrorKind;
unsafe {
// Create a TcpStream from the raw fd to perform socket operations
let stream = TcpStream::from_raw_fd(fd);
// Try to peek at the socket to see if it's still connected
// We peek with a zero-length buffer to avoid consuming data
let mut buf = [0u8; 0];
let result = match stream.peek(&mut buf) {
Ok(_) => true, // Socket is alive and readable
Err(e) => match e.kind() {
ErrorKind::WouldBlock => true, // Socket is alive but no data available
ErrorKind::ConnectionReset
| ErrorKind::ConnectionAborted
| ErrorKind::BrokenPipe
| ErrorKind::NotConnected => false, // Socket is clearly dead
_ => true, // Other errors might be temporary, assume alive
}
};
// Prevent the TcpStream from closing the fd when it's dropped
// since we don't own the fd
std::mem::forget(stream);
result
}
}
#[cfg(not(unix))]
{
// On non-Unix systems, we can't easily check socket state
// Fall back to assuming the connection is alive if we have an fd
fd != 0
}
}
+1 -1
View File
@@ -12,7 +12,7 @@ license.workspace = true
[dependencies]
anyhow = { workspace = true }
axum = { workspace = true, features = ["json"] }
axum = { workspace = true, features = ["json", "macros"] }
clap = { workspace = true, features = ["derive", "env"] }
dashmap = { workspace = true }
futures = { workspace = true }
+7 -2
View File
@@ -15,13 +15,13 @@ use std::{
sync::Arc,
time::Duration,
};
use tokio::{sync::RwLock, time::timeout};
use tokio::time::timeout;
use utoipa::ToSchema;
use crate::{
accounting::{all_node_stats, NetworkAccount, NetworkAccountStats, NodeStats},
http::AppState,
make_client, MIXNET_TIMEOUT, TOPOLOGY,
MIXNET_TIMEOUT,
};
#[derive(ToSchema, Serialize)]
@@ -207,6 +207,11 @@ async fn send_receive_mixnet(state: AppState) -> Result<String, StatusCode> {
}
};
if !client.read().await.is_gateway_connection_alive() {
warn!("Client is not connected, waiting for it to connect, trying another one");
return Err(StatusCode::SERVICE_UNAVAILABLE);
}
let recv = Arc::clone(&client);
let sender = Arc::clone(&client);
+3 -2
View File
@@ -67,7 +67,7 @@ impl HttpServer {
let n_clients = clients.read().await.len();
let state = AppState { clients };
let app = Router::new()
.route("/v1/send", post(send_handler).with_state(state))
.route("/v1/send", post(send_handler))
.merge(SwaggerUi::new("/v1/ui").url("/v1/docs/openapi.json", ApiDoc::openapi()))
.route("/v1/accounting", get(accounting_handler))
.route("/v1/sent", get(sent_handler))
@@ -77,7 +77,8 @@ impl HttpServer {
.route("/v1/stats", get(stats_handler))
.route("/v1/node_stats/:mix_id", get(node_stats_handler))
.route("/v1/node_stats", get(all_nodes_stats_handler))
.route("/v1/received", get(recv_handler));
.route("/v1/received", get(recv_handler))
.with_state(state);
let listener = tokio::net::TcpListener::bind(self.listener).await?;
let server_future =
@@ -148,6 +148,17 @@ impl MixnetClient {
pub fn gateway_connection(&self) -> GatewayConnection {
self.client_state.gateway_connection
}
/// Check if the websocket connection to the gateway is alive at the socket level
///
/// This performs a real socket-level health check to determine if the connection
/// is actually alive and responsive, not just whether we have a file descriptor.
pub fn is_gateway_connection_alive(&self) -> bool {
match self.client_state.gateway_connection.gateway_ws_fd {
Some(fd) => socket_is_alive(fd),
None => false,
}
}
/// Get a shallow clone of [`MixnetClientSender`]. Useful if you want split the send and
/// receive logic in different locations.
@@ -340,3 +351,49 @@ impl MixnetMessageSender for MixnetClientSender {
.map_err(|_| Error::MessageSendingFailure)
}
}
/// Check if a socket file descriptor is alive and responsive
///
/// This function performs socket-level checks to determine if the connection is actually alive.
/// It's cross-platform compatible and works on both Unix and non-Unix systems.
fn socket_is_alive(fd: std::os::raw::c_int) -> bool {
#[cfg(unix)]
{
use std::os::unix::io::FromRawFd;
use std::net::TcpStream;
use std::io::ErrorKind;
unsafe {
// Create a TcpStream from the raw fd to perform socket operations
let stream = TcpStream::from_raw_fd(fd);
// Try to peek at the socket to see if it's still connected
// We peek with a zero-length buffer to avoid consuming data
let mut buf = [0u8; 0];
let result = match stream.peek(&mut buf) {
Ok(_) => true, // Socket is alive and readable
Err(e) => match e.kind() {
ErrorKind::WouldBlock => true, // Socket is alive but no data available
ErrorKind::ConnectionReset
| ErrorKind::ConnectionAborted
| ErrorKind::BrokenPipe
| ErrorKind::NotConnected => false, // Socket is clearly dead
_ => true, // Other errors might be temporary, assume alive
}
};
// Prevent the TcpStream from closing the fd when it's dropped
// since we don't own the fd
std::mem::forget(stream);
result
}
}
#[cfg(not(unix))]
{
// On non-Unix systems, we can't easily check socket state
// Fall back to assuming the connection is alive if we have an fd
fd != 0
}
}