From 8684378030202cef96eaedf9604e65a90cbf9eed Mon Sep 17 00:00:00 2001 From: Alexey <247128645+axkurcom@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:46:18 +0300 Subject: [PATCH] Human-readable Peer Close Classification --- src/maestro/helpers.rs | 149 ++++++++++++++++++++++++++++++++------- src/maestro/listeners.rs | 68 ++++++++++-------- 2 files changed, 162 insertions(+), 55 deletions(-) diff --git a/src/maestro/helpers.rs b/src/maestro/helpers.rs index 32e6a54..37b6563 100644 --- a/src/maestro/helpers.rs +++ b/src/maestro/helpers.rs @@ -231,7 +231,10 @@ fn print_help() { #[cfg(test)] mod tests { - use super::{is_expected_handshake_eof, resolve_runtime_config_path}; + use super::{ + expected_handshake_close_description, is_expected_handshake_eof, peer_close_description, + resolve_runtime_config_path, + }; use crate::error::{ProxyError, StreamError}; #[test] @@ -314,6 +317,67 @@ mod tests { ))); assert!(is_expected_handshake_eof(&err)); } + + #[test] + fn peer_close_description_is_human_readable_for_all_peer_close_kinds() { + let cases = [ + ( + std::io::ErrorKind::ConnectionReset, + "Peer reset TCP connection (RST)", + ), + ( + std::io::ErrorKind::ConnectionAborted, + "Peer aborted TCP connection during transport", + ), + ( + std::io::ErrorKind::BrokenPipe, + "Peer closed write side (broken pipe)", + ), + ( + std::io::ErrorKind::NotConnected, + "Socket was already closed by peer", + ), + ]; + + for (kind, expected) in cases { + let err = ProxyError::Io(std::io::Error::from(kind)); + assert_eq!(peer_close_description(&err), Some(expected)); + } + } + + #[test] + fn handshake_close_description_is_human_readable_for_all_expected_kinds() { + let cases = [ + ( + ProxyError::Io(std::io::Error::from(std::io::ErrorKind::UnexpectedEof)), + "Peer closed before sending full 64-byte MTProto handshake", + ), + ( + ProxyError::Io(std::io::Error::from(std::io::ErrorKind::ConnectionReset)), + "Peer reset TCP connection during initial MTProto handshake", + ), + ( + ProxyError::Io(std::io::Error::from(std::io::ErrorKind::ConnectionAborted)), + "Peer aborted TCP connection during initial MTProto handshake", + ), + ( + ProxyError::Io(std::io::Error::from(std::io::ErrorKind::BrokenPipe)), + "Peer closed write side before MTProto handshake completed", + ), + ( + ProxyError::Io(std::io::Error::from(std::io::ErrorKind::NotConnected)), + "Handshake socket was already closed by peer", + ), + ( + ProxyError::Stream(StreamError::UnexpectedEof), + "Peer closed before sending full 64-byte MTProto handshake", + ), + ]; + + for (err, expected) in cases { + assert_eq!(expected_handshake_close_description(&err), Some(expected)); + } + } } pub(crate) fn print_proxy_links(host: &str, port: u16, config: &ProxyConfig) { @@ -443,30 +507,65 @@ pub(crate) async fn wait_until_admission_open(admission_rx: &mut watch::Receiver } pub(crate) fn is_expected_handshake_eof(err: &crate::error::ProxyError) -> bool { - matches!( - err, - crate::error::ProxyError::Io(ioe) - if matches!( - ioe.kind(), - std::io::ErrorKind::UnexpectedEof - | std::io::ErrorKind::ConnectionReset - | std::io::ErrorKind::ConnectionAborted - | std::io::ErrorKind::BrokenPipe - | std::io::ErrorKind::NotConnected - ) - ) || matches!(err, crate::error::ProxyError::Stream(crate::error::StreamError::UnexpectedEof)) - || matches!( - err, - crate::error::ProxyError::Stream(crate::error::StreamError::Io(ioe)) - if matches!( - ioe.kind(), - std::io::ErrorKind::UnexpectedEof - | std::io::ErrorKind::ConnectionReset - | std::io::ErrorKind::ConnectionAborted - | std::io::ErrorKind::BrokenPipe - | std::io::ErrorKind::NotConnected - ) - ) + expected_handshake_close_description(err).is_some() +} + +pub(crate) fn peer_close_description(err: &crate::error::ProxyError) -> Option<&'static str> { + fn from_kind(kind: std::io::ErrorKind) -> Option<&'static str> { + match kind { + std::io::ErrorKind::ConnectionReset => Some("Peer reset TCP connection (RST)"), + std::io::ErrorKind::ConnectionAborted => { + Some("Peer aborted TCP connection during transport") + } + std::io::ErrorKind::BrokenPipe => Some("Peer closed write side (broken pipe)"), + std::io::ErrorKind::NotConnected => Some("Socket was already closed by peer"), + _ => None, + } + } + + match err { + crate::error::ProxyError::Io(ioe) => from_kind(ioe.kind()), + crate::error::ProxyError::Stream(crate::error::StreamError::Io(ioe)) => { + from_kind(ioe.kind()) + } + _ => None, + } +} + +pub(crate) fn expected_handshake_close_description( + err: &crate::error::ProxyError, +) -> Option<&'static str> { + fn from_kind(kind: std::io::ErrorKind) -> Option<&'static str> { + match kind { + std::io::ErrorKind::UnexpectedEof => { + Some("Peer closed before sending full 64-byte MTProto handshake") + } + std::io::ErrorKind::ConnectionReset => { + Some("Peer reset TCP connection during initial MTProto handshake") + } + std::io::ErrorKind::ConnectionAborted => { + Some("Peer aborted TCP connection during initial MTProto handshake") + } + std::io::ErrorKind::BrokenPipe => { + Some("Peer closed write side before MTProto handshake completed") + } + std::io::ErrorKind::NotConnected => { + Some("Handshake socket was already closed by peer") + } + _ => None, + } + } + + match err { + crate::error::ProxyError::Io(ioe) => from_kind(ioe.kind()), + crate::error::ProxyError::Stream(crate::error::StreamError::UnexpectedEof) => { + Some("Peer closed before sending full 64-byte MTProto handshake") + } + crate::error::ProxyError::Stream(crate::error::StreamError::Io(ioe)) => { + from_kind(ioe.kind()) + } + _ => None, + } } pub(crate) async fn load_startup_proxy_config_snapshot( diff --git a/src/maestro/listeners.rs b/src/maestro/listeners.rs index 796eb9e..b393f3f 100644 --- a/src/maestro/listeners.rs +++ b/src/maestro/listeners.rs @@ -24,7 +24,10 @@ use crate::transport::middle_proxy::MePool; use crate::transport::socket::set_linger_zero; use crate::transport::{ListenOptions, UpstreamManager, create_listener, find_listener_processes}; -use super::helpers::{is_expected_handshake_eof, print_proxy_links}; +use super::helpers::{ + expected_handshake_close_description, is_expected_handshake_eof, peer_close_description, + print_proxy_links, +}; pub(crate) struct BoundListeners { pub(crate) listeners: Vec<(TcpListener, bool)>, @@ -485,29 +488,9 @@ pub(crate) fn spawn_tcp_accept_loops( Ok(guard) => *guard, Err(_) => None, }; - let peer_closed = matches!( - &e, - crate::error::ProxyError::Io(ioe) - if matches!( - ioe.kind(), - std::io::ErrorKind::ConnectionReset - | std::io::ErrorKind::ConnectionAborted - | std::io::ErrorKind::BrokenPipe - | std::io::ErrorKind::NotConnected - ) - ) || matches!( - &e, - crate::error::ProxyError::Stream( - crate::error::StreamError::Io(ioe) - ) - if matches!( - ioe.kind(), - std::io::ErrorKind::ConnectionReset - | std::io::ErrorKind::ConnectionAborted - | std::io::ErrorKind::BrokenPipe - | std::io::ErrorKind::NotConnected - ) - ); + let peer_close_reason = peer_close_description(&e); + let handshake_close_reason = + expected_handshake_close_description(&e); let me_closed = matches!( &e, @@ -518,12 +501,23 @@ pub(crate) fn spawn_tcp_accept_loops( crate::error::ProxyError::Proxy(msg) if msg == ROUTE_SWITCH_ERROR_MSG ); - match (peer_closed, me_closed) { - (true, _) => { + match (peer_close_reason, me_closed) { + (Some(reason), _) => { if let Some(real_peer) = real_peer { - debug!(peer = %peer_addr, real_peer = %real_peer, error = %e, "Connection closed by client"); + debug!( + peer = %peer_addr, + real_peer = %real_peer, + error = %e, + close_reason = reason, + "Connection closed by peer" + ); } else { - debug!(peer = %peer_addr, error = %e, "Connection closed by client"); + debug!( + peer = %peer_addr, + error = %e, + close_reason = reason, + "Connection closed by peer" + ); } } (_, true) => { @@ -541,10 +535,24 @@ pub(crate) fn spawn_tcp_accept_loops( } } _ if is_expected_handshake_eof(&e) => { + let reason = handshake_close_reason.unwrap_or( + "Peer closed during initial handshake", + ); if let Some(real_peer) = real_peer { - info!(peer = %peer_addr, real_peer = %real_peer, error = %e, "Connection closed during initial handshake"); + info!( + peer = %peer_addr, + real_peer = %real_peer, + error = %e, + close_reason = reason, + "Connection closed during initial handshake" + ); } else { - info!(peer = %peer_addr, error = %e, "Connection closed during initial handshake"); + info!( + peer = %peer_addr, + error = %e, + close_reason = reason, + "Connection closed during initial handshake" + ); } } _ => {