╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────────────────╮ │src/serve_ssh_protocol.zig│ ╰──────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │ 97 // the buffer will take. │ │ 98 const max_incoming_buffered: u32 = initial_recv_window; │ │ 99 │ │100 -/// shared with the host's watchdog. `activity` is bumped only for non-ignored │ │101 -/// session packets, `authenticated` releases the hard pre-auth deadline, and │ │102 -/// `closing` starts the bounded close drain. `exempt` is set once an │ │103 -/// interactive session starts, since users idle in a TUI legitimately. │ │100 +/// shared with the host's watchdog. `activity` is bumped for non-ignored │ │101 +/// session packets and for packets we send, `waiting` is set while we're │ │102 +/// blocked on the peer, `session_started` releases the hard pre-session │ │103 +/// deadline, and `closing` starts the bounded close drain. `exempt` is set │ │104 +/// once an interactive session starts, since users idle in a TUI legitimately.│ │105 pub const IdleState = struct { │ │106 activity: std.atomic.Value(u64) = .init(0), │ │106 - authenticated: std.atomic.Value(bool) = .init(false), │ │107 + waiting: std.atomic.Value(bool) = .init(false), │ │108 + session_started: std.atomic.Value(bool) = .init(false), │ │109 exempt: std.atomic.Value(bool) = .init(false), │ │110 closing: std.atomic.Value(bool) = .init(false), │ │111 }; │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │193 idle: ?*IdleState = null, │ │194 // set only while the TUI waits to resolve a lone escape key │ │195 read_timeout: std.Io.Timeout = .none, │ │196 + │ │197 + fn setWaiting(self: *Conn, waiting: bool) void { │ │198 + if (self.idle) |idle| idle.waiting.store(waiting, .release); │ │199 + } │ │200 + │ │201 + fn writePacket(self: *Conn, payload: []const u8) !void { │ │202 + self.setWaiting(true); │ │203 + defer self.setWaiting(false); │ │204 + try self.sc_cipher.writePacket(self.io, self.writer, payload); │ │205 + // a peer that keeps reading is alive │ │206 + recordActivity(self); │ │207 + } │ │208 │ │209 fn readPacket(self: *Conn) ![]u8 { │ │210 + self.setWaiting(true); │ │211 + defer self.setWaiting(false); │ │212 if (self.read_timeout != .none and !try self.cs_cipher.hasBufferedPacket(self.reader)) {│ │213 // peek keeps partial packets in the reader if the timer wins. │ │214 // join both tasks before consuming bytes or changing cipher state. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │294 /// interactive sessions idle legitimately; tell the host's idle │ │295 /// watchdog to stand down for the rest of this connection. │ │296 pub fn exemptFromIdleTimeout(self: *SessionCtx) void { │ │281 - if (self.conn.idle) |idle| idle.exempt.store(true, .monotonic); │ │297 + if (self.conn.idle) |idle| idle.exempt.store(true, .release); │ │298 } │ │299 │ │300 /// pump SSH packets until something interesting (data / resize / close)│ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │360 try writeU32(&self.write_scratch, conn.allocator, self.channel.remote_id); │ │361 if (extended) try writeU32(&self.write_scratch, conn.allocator, SSH_EXTENDED_DATA_STDERR);│ │362 try writeStringField(&self.write_scratch, conn.allocator, rest[0..chunk_len]); │ │347 - try conn.sc_cipher.writePacket(conn.io, conn.writer, self.write_scratch.items); │ │363 + try conn.writePacket(self.write_scratch.items); │ │364 │ │365 self.channel.remote_window -= chunk_len; │ │366 rest = rest[chunk_len..]; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │462 try writeStringField(&req, conn.allocator, "exit-status"); │ │463 try req.append(conn.allocator, 0); // want_reply MUST be false │ │464 try writeU32(&req, conn.allocator, status); │ │449 - try conn.sc_cipher.writePacket(conn.io, conn.writer, req.items);│ │465 + try conn.writePacket(req.items); │ │466 } │ │467 │ │468 // EOF then CLOSE │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │630 /// invoked once the channel is open and a shell/exec request has arrived.│ │631 /// │ │632 /// `reader` and `writer` are the bidirectional byte stream. `idle`, if │ │617 -/// given, is bumped on packet reads for the host's idle watchdog. │ │633 +/// given, is kept current for the host's watchdog. │ │634 pub fn handleConnection( │ │635 io: std.Io, │ │636 allocator: std.mem.Allocator, │ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────╮ │666 disconnectOnError(&conn, err); │ │667 return err; │ │668 }; │ │653 - if (idle) |state| state.authenticated.store(true, .release);│ │669 │ │670 runChannelLayer(&conn, &fingerprint, handler) catch |err| { │ │671 disconnectOnError(&conn, err); │ ╰─────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │687 try writeU32(&buf, conn.allocator, SSH_DISCONNECT_PROTOCOL_ERROR); │ │688 try writeStringField(&buf, conn.allocator, description); │ │689 try writeStringField(&buf, conn.allocator, ""); // language tag │ │675 - try conn.sc_cipher.writePacket(conn.io, conn.writer, buf.items); │ │690 + try conn.writePacket(buf.items); │ │691 } │ │692 │ │693 // ---------------------------------------------------------------------------│ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │831 try buf.append(conn.allocator, SSH_MSG_UNIMPLEMENTED); │ │832 // the read already advanced past the packet we're rejecting │ │833 try writeU32(&buf, conn.allocator, @truncate(conn.cs_cipher.seq - 1));│ │819 - try conn.sc_cipher.writePacket(conn.io, conn.writer, buf.items); │ │834 + try conn.writePacket(buf.items); │ │835 } │ │836 │ │837 fn recordActivity(conn: *Conn) void { │ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │1089 fn runRekey(conn: *Conn, client_kex_init: []const u8) !void { │ │1090 var parsed = try parseClientKexInit(conn.allocator, client_kex_init); │ │1091 defer parsed.deinit(conn.allocator); │ │1092 + │ │1093 + // the exchange reads and writes through the ciphers directly │ │1094 + conn.setWaiting(true); │ │1095 + defer conn.setWaiting(false); │ │1096 │ │1097 const server_kex_init = try buildServerKexInit(conn.io, conn.allocator);│ │1098 defer conn.allocator.free(server_kex_init); │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯