From db8d333ed6325831f2bffefddd82ff83d62697ab Mon Sep 17 00:00:00 2001 From: Alexey <247128645+axkurcom@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:35:11 +0300 Subject: [PATCH] Noisy-network peer Close Errors Classification --- src/maestro/helpers.rs | 42 ++++++++++++++++++++++-- src/proxy/client.rs | 23 ++++++++++++- src/proxy/tests/client_security_tests.rs | 40 ++++++++++++++++++++++ 3 files changed, 102 insertions(+), 3 deletions(-) diff --git a/src/maestro/helpers.rs b/src/maestro/helpers.rs index 49c5347..32e6a54 100644 --- a/src/maestro/helpers.rs +++ b/src/maestro/helpers.rs @@ -231,7 +231,8 @@ fn print_help() { #[cfg(test)] mod tests { - use super::resolve_runtime_config_path; + use super::{is_expected_handshake_eof, resolve_runtime_config_path}; + use crate::error::{ProxyError, StreamError}; #[test] fn resolve_runtime_config_path_anchors_relative_to_startup_cwd() { @@ -299,6 +300,20 @@ mod tests { let _ = std::fs::remove_dir(&startup_cwd); } + + #[test] + fn expected_handshake_eof_matches_connection_reset() { + let err = ProxyError::Io(std::io::Error::from(std::io::ErrorKind::ConnectionReset)); + assert!(is_expected_handshake_eof(&err)); + } + + #[test] + fn expected_handshake_eof_matches_stream_io_unexpected_eof() { + let err = ProxyError::Stream(StreamError::Io(std::io::Error::from( + std::io::ErrorKind::UnexpectedEof, + ))); + assert!(is_expected_handshake_eof(&err)); + } } pub(crate) fn print_proxy_links(host: &str, port: u16, config: &ProxyConfig) { @@ -428,7 +443,30 @@ 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 { - err.to_string().contains("expected 64 bytes, got 0") + 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 + ) + ) } pub(crate) async fn load_startup_proxy_config_snapshot( diff --git a/src/proxy/client.rs b/src/proxy/client.rs index 0937a8f..67fea54 100644 --- a/src/proxy/client.rs +++ b/src/proxy/client.rs @@ -331,10 +331,31 @@ fn record_handshake_failure_class( error: &ProxyError, ) { let class = match error { - ProxyError::Io(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => { + ProxyError::Io(err) + if matches!( + err.kind(), + std::io::ErrorKind::UnexpectedEof + | std::io::ErrorKind::ConnectionReset + | std::io::ErrorKind::ConnectionAborted + | std::io::ErrorKind::BrokenPipe + | std::io::ErrorKind::NotConnected + ) => + { "expected_64_got_0" } ProxyError::Stream(StreamError::UnexpectedEof) => "expected_64_got_0", + ProxyError::Stream(StreamError::Io(err)) + if matches!( + err.kind(), + std::io::ErrorKind::UnexpectedEof + | std::io::ErrorKind::ConnectionReset + | std::io::ErrorKind::ConnectionAborted + | std::io::ErrorKind::BrokenPipe + | std::io::ErrorKind::NotConnected + ) => + { + "expected_64_got_0" + } _ => "other", }; record_beobachten_class(beobachten, config, peer_ip, class); diff --git a/src/proxy/tests/client_security_tests.rs b/src/proxy/tests/client_security_tests.rs index 85af766..480b33d 100644 --- a/src/proxy/tests/client_security_tests.rs +++ b/src/proxy/tests/client_security_tests.rs @@ -2493,6 +2493,46 @@ fn unexpected_eof_is_classified_without_string_matching() { ); } +#[test] +fn connection_reset_is_classified_as_expected_handshake_close() { + let beobachten = BeobachtenStore::new(); + let mut config = ProxyConfig::default(); + config.general.beobachten = true; + config.general.beobachten_minutes = 1; + + let reset = ProxyError::Io(std::io::Error::from(std::io::ErrorKind::ConnectionReset)); + let peer_ip: IpAddr = "198.51.100.202".parse().unwrap(); + + record_handshake_failure_class(&beobachten, &config, peer_ip, &reset); + + let snapshot = beobachten.snapshot_text(Duration::from_secs(60)); + assert!( + snapshot.contains("[expected_64_got_0]"), + "ConnectionReset must be classified as expected handshake close" + ); +} + +#[test] +fn stream_io_unexpected_eof_is_classified_without_string_matching() { + let beobachten = BeobachtenStore::new(); + let mut config = ProxyConfig::default(); + config.general.beobachten = true; + config.general.beobachten_minutes = 1; + + let eof = ProxyError::Stream(StreamError::Io(std::io::Error::from( + std::io::ErrorKind::UnexpectedEof, + ))); + let peer_ip: IpAddr = "198.51.100.203".parse().unwrap(); + + record_handshake_failure_class(&beobachten, &config, peer_ip, &eof); + + let snapshot = beobachten.snapshot_text(Duration::from_secs(60)); + assert!( + snapshot.contains("[expected_64_got_0]"), + "StreamError::Io(UnexpectedEof) must be classified as expected handshake close" + ); +} + #[test] fn non_eof_error_is_classified_as_other() { let beobachten = BeobachtenStore::new();