From 5a65b62a3d48a4a62a07174de5f65d28533becef Mon Sep 17 00:00:00 2001 From: ddidderr Date: Fri, 22 May 2026 08:56:39 +0200 Subject: [PATCH] fix(client): bound shutdown diagnostic text The Windows client now reports runtime errors in its graceful disconnect message so relay logs can show why a client stopped. That message comes from an anyhow error chain, so keep it useful but bounded before sending it over the control stream and using it as the QUIC close reason. Truncate client shutdown messages to 1024 bytes, preserving UTF-8 boundaries and adding an ellipsis when truncation happens. Normal shutdown text and small error chains are unchanged. Test Plan: - cargo fmt --check - cargo test -p lanparty-client-win - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings Refs: MVP Windows client diagnostics hardening --- crates/lanparty-client-win/src/main.rs | 34 +++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/crates/lanparty-client-win/src/main.rs b/crates/lanparty-client-win/src/main.rs index 970f55b..b564c72 100644 --- a/crates/lanparty-client-win/src/main.rs +++ b/crates/lanparty-client-win/src/main.rs @@ -35,6 +35,8 @@ const TAP_INTERFACE_METRIC: u32 = 9_000; const RELAY_ROUTE_VERIFY_INTERVAL: Duration = Duration::from_secs(5); #[cfg(windows)] const CLIENT_DIAGNOSTICS_INTERVAL: Duration = Duration::from_secs(10); +const MAX_CLIENT_SHUTDOWN_MESSAGE_BYTES: usize = 1024; +const CLIENT_SHUTDOWN_TRUNCATION_SUFFIX: &str = "..."; #[derive(Debug, Parser)] #[command( @@ -221,10 +223,28 @@ async fn main() -> Result<()> { } fn client_shutdown_message(run_result: &Result<()>) -> String { - match run_result { + let message = match run_result { Ok(()) => "client shutting down".to_owned(), Err(error) => format!("client shutting down after error: {error:#}"), + }; + + truncate_client_shutdown_message(&message) +} + +fn truncate_client_shutdown_message(message: &str) -> String { + if message.len() <= MAX_CLIENT_SHUTDOWN_MESSAGE_BYTES { + return message.to_owned(); } + + let suffix_len = CLIENT_SHUTDOWN_TRUNCATION_SUFFIX.len(); + let mut end = MAX_CLIENT_SHUTDOWN_MESSAGE_BYTES - suffix_len; + while !message.is_char_boundary(end) { + end -= 1; + } + + let mut truncated = message[..end].to_owned(); + truncated.push_str(CLIENT_SHUTDOWN_TRUNCATION_SUFFIX); + truncated } #[cfg(windows)] @@ -1296,6 +1316,18 @@ mod tests { ); } + #[test] + fn bounds_runtime_errors_in_shutdown_message() { + let detail = "x".repeat(MAX_CLIENT_SHUTDOWN_MESSAGE_BYTES * 2); + let error = anyhow::anyhow!("{detail}").context("bridge stopped"); + + let message = client_shutdown_message(&Err(error)); + + assert_eq!(message.len(), MAX_CLIENT_SHUTDOWN_MESSAGE_BYTES); + assert!(message.starts_with("client shutting down after error: bridge stopped: ")); + assert!(message.ends_with(CLIENT_SHUTDOWN_TRUNCATION_SUFFIX)); + } + #[test] fn formats_client_diagnostics_status_line() { let diagnostics = ClientDiagnostics::new(