Human-readable Peer Close Classification

This commit is contained in:
Alexey
2026-04-21 15:46:18 +03:00
parent db8d333ed6
commit 8684378030
2 changed files with 162 additions and 55 deletions

View File

@@ -231,7 +231,10 @@ fn print_help() {
#[cfg(test)] #[cfg(test)]
mod tests { 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}; use crate::error::{ProxyError, StreamError};
#[test] #[test]
@@ -314,6 +317,67 @@ mod tests {
))); )));
assert!(is_expected_handshake_eof(&err)); 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) { 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 { pub(crate) fn is_expected_handshake_eof(err: &crate::error::ProxyError) -> bool {
matches!( expected_handshake_close_description(err).is_some()
err, }
crate::error::ProxyError::Io(ioe)
if matches!( pub(crate) fn peer_close_description(err: &crate::error::ProxyError) -> Option<&'static str> {
ioe.kind(), fn from_kind(kind: std::io::ErrorKind) -> Option<&'static str> {
std::io::ErrorKind::UnexpectedEof match kind {
| std::io::ErrorKind::ConnectionReset std::io::ErrorKind::ConnectionReset => Some("Peer reset TCP connection (RST)"),
| std::io::ErrorKind::ConnectionAborted std::io::ErrorKind::ConnectionAborted => {
| std::io::ErrorKind::BrokenPipe Some("Peer aborted TCP connection during transport")
| std::io::ErrorKind::NotConnected }
) std::io::ErrorKind::BrokenPipe => Some("Peer closed write side (broken pipe)"),
) || matches!(err, crate::error::ProxyError::Stream(crate::error::StreamError::UnexpectedEof)) std::io::ErrorKind::NotConnected => Some("Socket was already closed by peer"),
|| matches!( _ => None,
err, }
crate::error::ProxyError::Stream(crate::error::StreamError::Io(ioe)) }
if matches!(
ioe.kind(), match err {
std::io::ErrorKind::UnexpectedEof crate::error::ProxyError::Io(ioe) => from_kind(ioe.kind()),
| std::io::ErrorKind::ConnectionReset crate::error::ProxyError::Stream(crate::error::StreamError::Io(ioe)) => {
| std::io::ErrorKind::ConnectionAborted from_kind(ioe.kind())
| std::io::ErrorKind::BrokenPipe }
| std::io::ErrorKind::NotConnected _ => 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( pub(crate) async fn load_startup_proxy_config_snapshot(

View File

@@ -24,7 +24,10 @@ use crate::transport::middle_proxy::MePool;
use crate::transport::socket::set_linger_zero; use crate::transport::socket::set_linger_zero;
use crate::transport::{ListenOptions, UpstreamManager, create_listener, find_listener_processes}; 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) struct BoundListeners {
pub(crate) listeners: Vec<(TcpListener, bool)>, pub(crate) listeners: Vec<(TcpListener, bool)>,
@@ -485,29 +488,9 @@ pub(crate) fn spawn_tcp_accept_loops(
Ok(guard) => *guard, Ok(guard) => *guard,
Err(_) => None, Err(_) => None,
}; };
let peer_closed = matches!( let peer_close_reason = peer_close_description(&e);
&e, let handshake_close_reason =
crate::error::ProxyError::Io(ioe) expected_handshake_close_description(&e);
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 me_closed = matches!( let me_closed = matches!(
&e, &e,
@@ -518,12 +501,23 @@ pub(crate) fn spawn_tcp_accept_loops(
crate::error::ProxyError::Proxy(msg) if msg == ROUTE_SWITCH_ERROR_MSG crate::error::ProxyError::Proxy(msg) if msg == ROUTE_SWITCH_ERROR_MSG
); );
match (peer_closed, me_closed) { match (peer_close_reason, me_closed) {
(true, _) => { (Some(reason), _) => {
if let Some(real_peer) = real_peer { 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 { } 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) => { (_, true) => {
@@ -541,10 +535,24 @@ pub(crate) fn spawn_tcp_accept_loops(
} }
} }
_ if is_expected_handshake_eof(&e) => { _ 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 { 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 { } 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"
);
} }
} }
_ => { _ => {