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
This commit is contained in:
2026-05-22 08:56:39 +02:00
parent 5f567b4320
commit 5a65b62a3d
+33 -1
View File
@@ -35,6 +35,8 @@ const TAP_INTERFACE_METRIC: u32 = 9_000;
const RELAY_ROUTE_VERIFY_INTERVAL: Duration = Duration::from_secs(5); const RELAY_ROUTE_VERIFY_INTERVAL: Duration = Duration::from_secs(5);
#[cfg(windows)] #[cfg(windows)]
const CLIENT_DIAGNOSTICS_INTERVAL: Duration = Duration::from_secs(10); 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)] #[derive(Debug, Parser)]
#[command( #[command(
@@ -221,10 +223,28 @@ async fn main() -> Result<()> {
} }
fn client_shutdown_message(run_result: &Result<()>) -> String { fn client_shutdown_message(run_result: &Result<()>) -> String {
match run_result { let message = match run_result {
Ok(()) => "client shutting down".to_owned(), Ok(()) => "client shutting down".to_owned(),
Err(error) => format!("client shutting down after error: {error:#}"), 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)] #[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] #[test]
fn formats_client_diagnostics_status_line() { fn formats_client_diagnostics_status_line() {
let diagnostics = ClientDiagnostics::new( let diagnostics = ClientDiagnostics::new(