╭ changed ─────────────────╮ │src/serve_ssh_protocol.zig│ ╰──────────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │ 91 const max_incoming_buffered: usize = initial_recv_window; │ │ 92 const incoming_refill_threshold: usize = max_incoming_buffered / 2; │ │ 93 │ │ 94 -/// shared with the host's idle watchdog. `activity` is bumped on every │ │ 95 -/// successfully decrypted packet so the watchdog can tell an idle connection │ │ 96 -/// from a slow one; `exempt` is set once an interactive session starts, │ │ 97 -/// since users idle in a TUI legitimately. │ │ 94 +/// shared with the host's watchdog. `activity` is bumped only for non-ignored│ │ 95 +/// session packets, `authenticated` releases the hard pre-auth deadline, and │ │ 96 +/// `closing` starts the bounded close drain. `exempt` is set once an │ │ 97 +/// interactive session starts, since users idle in a TUI legitimately. │ │ 98 pub const IdleState = struct { │ │ 99 activity: std.atomic.Value(u64) = .init(0), │ │100 + authenticated: std.atomic.Value(bool) = .init(false), │ │101 exempt: std.atomic.Value(bool) = .init(false), │ │102 + closing: std.atomic.Value(bool) = .init(false), │ │103 }; │ │104 │ │105 // ---------------------------------------------------------------------------│ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │379 if (self.closed) return; │ │380 self.closed = true; │ │381 const conn = self.conn; │ │382 + if (conn.cs_cipher.idle) |idle| idle.closing.store(true, .release);│ │383 │ │384 // exit-status request (informational; client uses it as the │ │385 // command's exit code) │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────╮ │596 conn.cs_cipher.idle = idle; │ │597 │ │598 const fingerprint = try runAuth(&conn); │ │599 + if (idle) |state| state.authenticated.store(true, .release);│ │600 │ │601 try runChannelLayer(&conn, &fingerprint, handler); │ │602 } │ ╰─────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │668 │ │669 // transport for KEX packets: the initial exchange runs in plaintext, a rekey│ │670 // runs under the current session ciphers. │ │667 -const KexTransport = union(enum) { │ │671 +pub const KexTransport = union(enum) { │ │672 plain: *u64, // counts packets read so the caller can seed cipher seqnos │ │673 encrypted: struct { cs: *Cipher, sc: *Cipher }, │ │674 │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │675 // read one packet, skipping ignorable messages — unless strict kex is in │ │676 // force, which bans them mid-handshake │ │673 - fn readPacket(self: KexTransport, allocator: std.mem.Allocator, reader: *std.Io.Reader, strict: bool) ![]u8 { │ │677 + pub fn readPacket(self: KexTransport, allocator: std.mem.Allocator, reader: *std.Io.Reader, strict: bool) ![]u8 {│ │678 while (true) { │ │679 const packet = switch (self) { │ │680 .plain => |count| packet: { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────╮ │703 │ │704 // read one encrypted packet for the auth and channel layers, transparently │ │705 // skipping ignorable messages and servicing client-initiated rekeys. │ │702 -fn readSessionPacket(conn: *Conn) ![]u8 { │ │706 +pub fn readSessionPacket(conn: *Conn) ![]u8 { │ │707 while (true) { │ │708 const packet = try conn.cs_cipher.readPacket(conn.allocator, conn.reader);│ │709 if (packet.len >= 1 and isIgnorableMsg(packet[0])) { │ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────╮ │712 } │ │713 if (packet.len >= 1 and packet[0] == SSH_MSG_KEXINIT) {│ │714 defer conn.allocator.free(packet); │ │715 + recordActivity(conn); │ │716 try runRekey(conn, packet); │ │717 continue; │ │718 } │ ╰────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────╮ │719 + recordActivity(conn);│ │720 return packet; │ │721 } │ │722 } │ ╰──────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │723 │ │724 +fn recordActivity(conn: *Conn) void { │ │725 + if (conn.cs_cipher.idle) |idle| _ = idle.activity.fetchAdd(1, .monotonic);│ │726 +} │ │727 + │ │728 // ---------------------------------------------------------------------------│ │729 // SSH field encoders / decoders (RFC 4251 §5) │ │730 // ---------------------------------------------------------------------------│ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │982 conn.reader, │ │983 conn.writer, │ │984 .{ .encrypted = .{ .cs = &conn.cs_cipher, .sc = &conn.sc_cipher } }, │ │975 - false, │ │985 + false, // strict kex message rules apply to the initial exchange only│ │986 conn.rekey.host_key, │ │987 conn.rekey.client_version, │ │988 client_kex_init, │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯