Fix native-v1 connection break, server panic, and resource leaks
The native-v1 work changed the UDP wire format (HEADER_LEN 42->58) without a
version bump, so a current client silently timed out against the older
still-deployed server. The server just needed redeploying; alongside that,
fix the robustness bugs surfaced while restoring the connection:
- Server: clamp terminal size to >=1 at every PTY/parser resize site. A client
reporting a 0x0 terminal made vt100::Parser::set_size panic ("attempt to
subtract with overflow") and took down the whole single-threaded daemon.
- Client: treat size() == Ok((0,0)) like a missing size and fall back to 80x24
instead of sending 0x0 to the server.
- Reap abandoned sessions and their shells. PtyHandle now retains the child and
kills it on drop, and the cleanup task removes clientless, non-prewarmed
sessions after the grace period. Previously the shell leaked forever (one
zsh per abandoned session); prewarmed sessions still stay hot.
- Evict half-finished native handshakes (pending_native) after 30s so an
unauthenticated ClientHello flood can't grow the map without bound.
- Drop the per-client, per-packet vt100 Screen clone on the output hot path and
remove the write-only last_screen field.
- Guard load_or_create_server_secret against a <32-byte secret file (was an
index-out-of-range panic at startup).
- Reconcile stream flow-control credit (client and server) when StreamData hits
a missing writer, so a closed/unknown stream can't wedge the peer's window.
Adds a unit test for session reaping. cargo fmt + cargo test green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+29
-10
@@ -44,6 +44,20 @@ use tokio::sync::mpsc;
|
||||
|
||||
const STREAM_INITIAL_WINDOW: usize = 1024 * 1024;
|
||||
|
||||
/// Current terminal size, with a sane fallback.
|
||||
///
|
||||
/// `crossterm::size()` returns `Err` when stdout is not a TTY (piped), but it
|
||||
/// can also return `Ok((0, 0))` for a real PTY that has not been sized yet
|
||||
/// (e.g. launched under `script` with no controlling window). Sending `0` to
|
||||
/// the server is meaningless and used to crash older servers, so treat any
|
||||
/// zero dimension the same as a missing size and fall back to 80x24.
|
||||
fn terminal_size() -> (u16, u16) {
|
||||
match size() {
|
||||
Ok((cols, rows)) if cols > 0 && rows > 0 => (cols, rows),
|
||||
_ => (80, 24),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "dosh-client")]
|
||||
struct Args {
|
||||
@@ -218,7 +232,7 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
let dosh_port = args.dosh_port.or(host.port).unwrap_or(config.dosh_port);
|
||||
let cache_path = cache_path(&config.credential_cache, &requested_server, &session, &mode);
|
||||
let (cols, rows) = size().unwrap_or((80, 24));
|
||||
let (cols, rows) = terminal_size();
|
||||
let auth_preference = args
|
||||
.auth
|
||||
.clone()
|
||||
@@ -2072,7 +2086,7 @@ async fn run_terminal(
|
||||
let mut last_packet_at = Instant::now();
|
||||
let mut status_tick = tokio::time::interval(Duration::from_secs(1));
|
||||
let mut resize_tick = tokio::time::interval(Duration::from_millis(250));
|
||||
let mut last_size = size().unwrap_or((80, 24));
|
||||
let mut last_size = terminal_size();
|
||||
let mut frame_buffer = FrameBuffer::default();
|
||||
let mut predictor = Predictor::new(predict && cred.mode != "view-only" && !forward_only);
|
||||
let (forward_tx, mut forward_rx) = mpsc::channel::<ForwardEvent>(1024);
|
||||
@@ -2151,6 +2165,8 @@ async fn run_terminal(
|
||||
}
|
||||
_ = resize_tick.tick() => {
|
||||
if let Ok((cols, rows)) = size()
|
||||
&& cols > 0
|
||||
&& rows > 0
|
||||
&& (cols, rows) != last_size
|
||||
{
|
||||
last_size = (cols, rows);
|
||||
@@ -2337,17 +2353,20 @@ async fn run_terminal(
|
||||
continue;
|
||||
};
|
||||
last_packet_at = Instant::now();
|
||||
let len = data.bytes.len();
|
||||
if let Some(writer) = stream_writers.get_mut(&data.stream_id) {
|
||||
let _ = writer.write_all(&data.bytes).await;
|
||||
send_stream_window_adjust(
|
||||
&socket,
|
||||
addr,
|
||||
&cred,
|
||||
&mut send_seq,
|
||||
data.stream_id,
|
||||
data.bytes.len(),
|
||||
).await?;
|
||||
}
|
||||
// Always return flow-control credit so a stream whose local
|
||||
// writer is already gone cannot wedge the server's send window.
|
||||
send_stream_window_adjust(
|
||||
&socket,
|
||||
addr,
|
||||
&cred,
|
||||
&mut send_seq,
|
||||
data.stream_id,
|
||||
len,
|
||||
).await?;
|
||||
}
|
||||
PacketKind::StreamWindowAdjust => {
|
||||
let Ok(plain) = protocol::decrypt_body(&packet, &cred.session_key, SERVER_TO_CLIENT) else {
|
||||
|
||||
Reference in New Issue
Block a user