╭ changed ─────────────────╮ │src/serve_ssh_protocol.zig│ ╰──────────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │40 pub const host_key_file_name = "ssh_host_ed25519_key"; │ │41 │ │42 const max_packet_len: u32 = 35000; // RFC 4253 §6.1 — minimum implementations must support│ │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; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────╮ │47 const max_banner_lines: u32 = 32; │ │48 +// terminal grids can have several live copies during layout and rendering│ │49 +const max_pty_width: u32 = 512; │ │50 +const max_pty_height: u32 = 128; │ │51 │ │52 // SSH message type bytes (RFC 4250 §4.1) │ │53 pub const SSH_MSG_DISCONNECT: u8 = 1; │ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │189 sc_cipher: Cipher, │ │190 rekey: RekeyState, │ │191 idle: ?*IdleState = null, │ │192 + // set only while the TUI waits to resolve a lone escape key │ │193 + read_timeout: std.Io.Timeout = .none, │ │194 + │ │195 + fn readPacket(self: *Conn) ![]u8 { │ │196 + if (self.read_timeout != .none) { │ │197 + // peek keeps partial packets in the reader if the timer wins. │ │198 + // join both tasks before consuming bytes or changing cipher state. │ │199 + const Result = union(enum) { ready: anyerror!void, timeout: std.Io.Cancelable!void }; │ │200 + var results: [2]Result = undefined; │ │201 + var select = std.Io.Select(Result).init(self.io, &results); │ │202 + defer select.cancelDiscard(); │ │203 + try select.concurrent(.ready, Cipher.peekPacket, .{ &self.cs_cipher, self.reader }); │ │204 + try select.concurrent(.timeout, std.Io.Timeout.sleep, .{ self.read_timeout, self.io });│ │205 + switch (try select.await()) { │ │206 + .ready => |result| try result, │ │207 + .timeout => |result| { │ │208 + try result; │ │209 + return error.Timeout; │ │210 + }, │ │211 + } │ │212 + } │ │213 + return self.cs_cipher.readPacket(self.allocator, self.reader); │ │214 + } │ │215 }; │ │216 │ │217 /// session bridge handed to the consumer's handleSession callback. exposes a │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────╮ │294 if (self.incomingBytes().len > 0) { │ │295 // hand the buffered bytes to the caller (caller owns) │ │296 const payload = try self.conn.allocator.dupe(u8, self.incomingBytes());│ │297 + errdefer self.conn.allocator.free(payload); │ │298 self.consumeIncoming(payload.len); │ │299 + try self.maybeRefillRecvWindowForBufferedInput(); │ │300 return .{ .data = payload }; │ │301 } │ │302 if (self.remote_closed) return .close; │ ╰────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │330 const conn = self.conn; │ │331 var rest = bytes; │ │332 while (rest.len > 0) { │ │333 + if (self.closed or self.remote_closed) return error.RemoteClosed;│ │334 while (self.channel.remote_window == 0) { │ │335 if (self.remote_closed) return error.RemoteClosed; │ │336 try self.processOneBackgroundPacket(); │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │412 try parseChannelId(packet, self.channel.local_id); │ │413 self.incoming_eof = true; │ │414 self.remote_closed = true; │ │415 + try sendChannelClose(conn, self.channel); │ │416 }, │ │417 SSH_MSG_GLOBAL_REQUEST => try handleGlobalRequest(conn, packet),│ │418 else => try sendUnimplemented(conn), │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │420 } │ │421 │ │422 fn maybeRefillRecvWindowForBufferedInput(self: *SessionCtx) !void { │ │423 + if (self.remote_closed or self.closed) return; │ │424 // credit only what the incoming buffer can still take │ │425 const buffered: u32 = @intCast(self.incomingBytes().len); │ │426 try maybeRefillRecvWindow(self.conn, self.channel, max_incoming_buffered - buffered);│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │433 const conn = self.conn; │ │434 if (conn.idle) |idle| idle.closing.store(true, .release); │ │435 │ │404 - // once the peer has sent CHANNEL_CLOSE the channel is gone on its │ │405 - // side, and these would be addressed to an id it no longer knows. │ │436 + // a peer CLOSE is acknowledged by the packet pump. don't send data,│ │437 + // exit-status, or EOF after that acknowledgement. │ │438 if (!self.remote_closed) { │ │439 // exit-status request (informational; client uses it as the │ │440 // command's exit code) │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮ │457 try writeU32(&eof, conn.allocator, self.channel.remote_id); │ │458 try conn.sc_cipher.writePacket(conn.io, conn.writer, eof.items); │ │459 } │ │428 - { │ │429 - var close: std.ArrayList(u8) = .empty; │ │430 - defer close.deinit(conn.allocator); │ │431 - try close.append(conn.allocator, SSH_MSG_CHANNEL_CLOSE); │ │432 - try writeU32(&close, conn.allocator, self.channel.remote_id); │ │433 - try conn.sc_cipher.writePacket(conn.io, conn.writer, close.items); │ │434 - } │ │460 + try sendChannelClose(conn, self.channel); │ │461 } │ │462 + drainConnection(conn); │ │463 + } │ │464 +}; │ │465 │ │437 - // read and discard until the peer closes (TCP FIN) before letting the │ │438 - // caller close the socket. a git client, after our CHANNEL_CLOSE, still │ │439 - // sends its own CHANNEL_CLOSE and SSH_MSG_DISCONNECT before closing; if │ │440 - // we close with those unread, Linux sends RST instead of FIN, which the │ │441 - // client reports as "Connection reset by peer" and treats as a failed │ │442 - // push even though the ref updated. draining to EOF leaves nothing │ │443 - // unread, so we send a clean FIN. we're tearing down, so the packet │ │444 - // contents don't matter — just get them off the socket. │ │445 - var drain_packets: u32 = 0; │ │446 - while (drain_packets < max_exit_drain_packets) : (drain_packets += 1) { │ │447 - const packet = conn.cs_cipher.readPacket(conn.allocator, conn.reader) catch break;│ │448 - conn.allocator.free(packet); │ │449 - } │ │466 +fn drainConnection(conn: *Conn) void { │ │467 + if (conn.idle) |idle| idle.closing.store(true, .release); │ │468 + // read and discard until the peer closes (TCP FIN) before letting the │ │469 + // caller close the socket. a git client, after our CHANNEL_CLOSE, still │ │470 + // sends its own CHANNEL_CLOSE and SSH_MSG_DISCONNECT before closing; if │ │471 + // we close with those unread, Linux sends RST instead of FIN, which the │ │472 + // client reports as "Connection reset by peer" and treats as a failed │ │473 + // push even though the ref updated. draining to EOF leaves nothing │ │474 + // unread, so we send a clean FIN. we're tearing down, so the packet │ │475 + // contents don't matter — just get them off the socket. │ │476 + var drain_packets: u32 = 0; │ │477 + while (drain_packets < max_exit_drain_packets) : (drain_packets += 1) { │ │478 + const packet = conn.cs_cipher.readPacket(conn.allocator, conn.reader) catch break; │ │479 + conn.allocator.free(packet); │ │480 } │ │451 -}; │ │481 +} │ │482 │ │483 /// std.Io.Writer adapter that ships bytes to a SessionCtx as CHANNEL_DATA. │ │484 /// useful for plugging the session into anything that wants a *std.Io.Writer │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────╮ │792 // skipping ignorable messages and servicing client-initiated rekeys. │ │793 pub fn readSessionPacket(conn: *Conn) ![]u8 { │ │794 while (true) { │ │765 - const packet = try conn.cs_cipher.readPacket(conn.allocator, conn.reader);│ │795 + const packet = try conn.readPacket(); │ │796 if (packet.len >= 1 and isIgnorableMsg(packet[0])) { │ │797 conn.allocator.free(packet); │ │798 continue; │ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯