╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │194 // runTui owns the terminal, whose deinit restores the client's screen and │ │195 // runs as the function unwinds — so any error it returns leaves the TUI torn │ │196 // down and we can surface the failure on the restored screen before exiting. │ │197 - runTui(handler, sess, pty) catch |err| { │ │197 + runTui(handler, sess, pty) catch |tui_err| { │ │198 + const err = sess.underlyingError(tui_err); │ │199 serve_common.logError(sess.conn.io, handler.err, "ssh tui session failed: {s}\n", .{@errorName(err)}); │ │200 var buf: [256]u8 = undefined; │ │201 const msg = std.fmt.bufPrint(&buf, "haxy ssh: {s}\r\n", .{@errorName(err)}) catch "haxy ssh: internal error\r\n";│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────────────╮ │src/serve_ssh_protocol.zig│ ╰──────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────╮ │43 pub const packet_buffer_size = 4 + max_packet_len + Poly1305.mac_length; │ │44 const max_name_list_len: u32 = 4096; │ │45 const max_auth_attempts: u32 = 20; │ │46 -const max_exit_drain_packets: u32 = 32; │ │46 const max_banner_lines: u32 = 32; │ │47 // terminal grids can have several live copies during layout and rendering│ │48 const max_pty_width: u32 = 512; │ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────╮ │79 pub const SSH_MSG_CHANNEL_FAILURE: u8 = 100; │ │80 │ │81 pub const SSH_DISCONNECT_PROTOCOL_ERROR: u32 = 2; │ │82 +pub const SSH_DISCONNECT_MAC_ERROR: u32 = 5; │ │83 +pub const SSH_DISCONNECT_BY_APPLICATION: u32 = 11; │ │84 │ │85 // SSH_OPEN_* reason codes for CHANNEL_OPEN_FAILURE│ │86 pub const SSH_OPEN_RESOURCE_SHORTAGE: u32 = 4; │ ╰───────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────╮ │194 idle: ?*IdleState = null, │ │195 // set only while the TUI waits to resolve a lone escape key │ │196 read_timeout: std.Io.Timeout = .none, │ │197 + // teardown ran to the peer's EOF, so nothing more can be said │ │198 + drained: bool = false, │ │199 │ │200 fn setWaiting(self: *Conn, waiting: bool) void { │ │201 if (self.idle) |idle| idle.waiting.store(waiting, .release);│ ╰─────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │249 /// can use it as a stable per-key identity. │ │250 fingerprint: [fingerprint_len]u8, │ │251 closed: bool = false, │ │252 + // what a SessionReader or SessionWriter actually hit. kept apart so a│ │253 + // cleanup write can't replace the cause of a failed read. │ │254 + read_err: ?anyerror = null, │ │255 + write_err: ?anyerror = null, │ │256 │ │257 // shared state for the packet pump. incoming CHANNEL_DATA bytes │ │258 // accumulate in incoming_buffer (already-consumed prefix tracked by │ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │271 │ │272 pub fn deinit(self: *SessionCtx) void { │ │273 self.incoming_buffer.deinit(self.conn.allocator); │ │274 + } │ │275 + │ │276 + /// the std.Io adapters can only report ReadFailed or WriteFailed. recover│ │277 + /// what the session hit. │ │278 + pub fn underlyingError(self: *const SessionCtx, err: anyerror) anyerror { │ │279 + return switch (err) { │ │280 + error.ReadFailed => self.read_err orelse err, │ │281 + error.WriteFailed => self.write_err orelse err, │ │282 + else => err, │ │283 + }; │ │284 } │ │285 │ │286 + fn failRead(self: *SessionCtx, err: anyerror) error{ReadFailed} { │ │287 + self.read_err = err; │ │288 + return error.ReadFailed; │ │289 + } │ │290 + │ │291 + fn failWrite(self: *SessionCtx, err: anyerror) error{WriteFailed} { │ │292 + self.write_err = err; │ │293 + return error.WriteFailed; │ │294 + } │ │295 + │ │296 fn incomingBytes(self: *const SessionCtx) []const u8 { │ │297 return self.incoming_buffer.items[self.incoming_start..]; │ │298 } │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │513 // we close with those unread, Linux sends RST instead of FIN, which the │ │514 // client reports as "Connection reset by peer" and treats as a failed │ │515 // push even though the ref updated. draining to EOF leaves nothing │ │489 - // unread, so we send a clean FIN. we're tearing down, so the packet │ │490 - // contents don't matter — just get them off the socket. │ │491 - var drain_packets: u32 = 0; │ │492 - while (drain_packets < max_exit_drain_packets) : (drain_packets += 1) { │ │493 - const packet = conn.cs_cipher.readPacket(conn.allocator, conn.reader) catch break;│ │494 - conn.allocator.free(packet); │ │495 - } │ │516 + // unread, so we send a clean FIN. the bytes don't matter, and the host's │ │517 + // watchdog bounds how long a peer can keep this going. │ │518 + _ = conn.reader.discardRemaining() catch {}; │ │519 + conn.drained = true; │ │520 } │ │521 │ │522 /// std.Io.Writer adapter that ships bytes to a SessionCtx as CHANNEL_DATA. │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────╮ │542 // first ship anything already accumulated in the Writer's buffer │ │543 const buffered = w.buffered(); │ │544 if (buffered.len > 0) { │ │521 - self.sess.writeBytes(buffered) catch return error.WriteFailed; │ │545 + self.sess.writeBytes(buffered) catch |err| return self.sess.failWrite(err);│ │546 } │ │547 │ │548 // then ship the bytes passed in directly. data[0..len-1] are written │ ╰────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────╮ │552 if (data.len > 1) { │ │553 for (data[0 .. data.len - 1]) |chunk| { │ │554 if (chunk.len == 0) continue; │ │531 - self.sess.writeBytes(chunk) catch return error.WriteFailed; │ │555 + self.sess.writeBytes(chunk) catch |err| return self.sess.failWrite(err);│ │556 extra += chunk.len; │ │557 } │ │558 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮ │561 if (pattern.len > 0) { │ │562 var i: usize = 0; │ │563 while (i < splat) : (i += 1) { │ │540 - self.sess.writeBytes(pattern) catch return error.WriteFailed; │ │564 + self.sess.writeBytes(pattern) catch |err| return self.sess.failWrite(err);│ │565 extra += pattern.len; │ │566 } │ │567 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯