╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────────────────╮ │src/serve_ssh_protocol.zig│ ╰──────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │178 writer: *std.Io.Writer, │ │179 cs_cipher: *Cipher, │ │180 sc_cipher: *Cipher, │ │181 + rekey: *const RekeyState, │ │182 channel: *Channel, │ │183 /// SHA256 fingerprint of the pubkey this session authenticated with, │ │184 /// formatted as "SHA256:<base64-no-padding>" — same value the openssh│ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │199 self.incoming_buffer.deinit(self.allocator); │ │200 } │ │201 │ │202 + /// interactive sessions idle legitimately; tell the host's idle │ │203 + /// watchdog to stand down for the rest of this connection. │ │204 + pub fn exemptFromIdleTimeout(self: *SessionCtx) void { │ │205 + if (self.cs_cipher.idle) |idle| idle.exempt.store(true, .monotonic); │ │206 + } │ │207 + │ │208 /// pump SSH packets until something interesting (data / resize / close) │ │209 /// arrives. background traffic (window adjusts, env requests, etc.) is │ │210 /// handled silently. used by pumped-event consumers (e.g. the TUI path);│ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │215 return .{ .resize = sz }; │ │216 } │ │217 while (true) { │ │210 - const packet = self.cs_cipher.readPacket(self.allocator, self.reader) catch |err| switch (err) { │ │218 + const packet = readSessionPacket(self.io, self.allocator, self.reader, self.writer, self.cs_cipher, self.sc_cipher, self.rekey) catch |err| switch (err) {│ │219 error.EndOfStream => { │ │220 self.closed = true; │ │221 return .close; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │240 try discardChannelData(self.io, self.allocator, self.sc_cipher, self.writer, self.channel, packet, true);│ │241 }, │ │242 SSH_MSG_CHANNEL_WINDOW_ADJUST => try handleWindowAdjust(self.channel, packet), │ │235 - SSH_MSG_KEXINIT => { │ │236 - try sendDisconnect(self.io, self.allocator, self.sc_cipher, self.writer, "rekey is not supported"); │ │237 - return error.RekeyUnsupported; │ │238 - }, │ │243 SSH_MSG_CHANNEL_REQUEST => { │ │244 var r = std.Io.Reader.fixed(packet[1..]); │ │245 const recipient_channel = try r.takeInt(u32, .big); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │316 /// SessionReader (waiting for inbound bytes) and writeBytes (waiting │ │317 /// for send-window). │ │318 fn processOneBackgroundPacket(self: *SessionCtx) !void { │ │315 - const packet = self.cs_cipher.readPacket(self.allocator, self.reader) catch |err| switch (err) { │ │319 + const packet = readSessionPacket(self.io, self.allocator, self.reader, self.writer, self.cs_cipher, self.sc_cipher, self.rekey) catch |err| switch (err) {│ │320 error.EndOfStream => { │ │321 self.incoming_eof = true; │ │322 return; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │342 try discardChannelData(self.io, self.allocator, self.sc_cipher, self.writer, self.channel, packet, true);│ │343 }, │ │344 SSH_MSG_CHANNEL_WINDOW_ADJUST => try handleWindowAdjust(self.channel, packet), │ │341 - SSH_MSG_KEXINIT => { │ │342 - try sendDisconnect(self.io, self.allocator, self.sc_cipher, self.writer, "rekey is not supported"); │ │343 - return error.RekeyUnsupported; │ │344 - }, │ │345 SSH_MSG_CHANNEL_REQUEST => { │ │346 var r = std.Io.Reader.fixed(packet[1..]); │ │347 const recipient_channel = try r.takeInt(u32, .big); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │562 /// pub fn handleSession(self, sess: *SessionCtx, request: Request) anyerror!void│ │563 /// invoked once the channel is open and a shell/exec request has arrived. │ │564 /// │ │565 -/// `reader` and `writer` are the bidirectional byte stream │ │565 +/// `reader` and `writer` are the bidirectional byte stream. `idle`, if │ │566 +/// given, is bumped on packet reads for the host's idle watchdog. │ │567 pub fn handleConnection( │ │568 io: std.Io, │ │569 allocator: std.mem.Allocator, │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │570 reader: *std.Io.Reader, │ │571 writer: *std.Io.Writer, │ │572 host_key: *const HostKey, │ │573 + idle: ?*IdleState, │ │574 handler: anytype, │ │575 ) !void { │ │576 const client_version = try exchangeVersions(allocator, reader, writer);│ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │578 │ │579 const kex = try runKex(io, allocator, reader, writer, host_key, client_version); │ │580 │ │579 - // post-KEX everything is encrypted with chacha20-poly1305@openssh.com. │ │580 - // both sides started counting packet seqnos at 0. we sent exactly │ │581 - // KEXINIT, KEX_ECDH_REPLY and NEWKEYS, so our first encrypted packet │ │582 - // uses seqno 3; the client's count includes any skipped IGNORE/DEBUG. │ │581 + // post-KEX everything is encrypted with chacha20-poly1305@openssh.com │ │582 var cs_cipher = Cipher.init(&kex.cs_key, kex.next_cs_seq); │ │584 - var sc_cipher = Cipher.init(&kex.sc_key, 3); │ │583 + var sc_cipher = Cipher.init(&kex.sc_key, kex.next_sc_seq); │ │584 + cs_cipher.idle = idle; │ │585 + │ │586 + const rekey = RekeyState{ │ │587 + .host_key = host_key, │ │588 + .client_version = client_version, │ │589 + .session_id = kex.session_id, │ │590 + .strict = kex.strict, │ │591 + }; │ │592 │ │586 - const fingerprint = try runAuth(io, allocator, reader, writer, &cs_cipher, &sc_cipher, &kex.session_id); │ │593 + const fingerprint = try runAuth(io, allocator, reader, writer, &cs_cipher, &sc_cipher, &rekey); │ │594 │ │588 - try runChannelLayer(io, allocator, reader, writer, &cs_cipher, &sc_cipher, &fingerprint, handler); │ │595 + try runChannelLayer(io, allocator, reader, writer, &cs_cipher, &sc_cipher, &rekey, &fingerprint, handler);│ │596 } │ │597 │ │598 // --------------------------------------------------------------------------- │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │607 try writer.writeAll(server_version ++ "\r\n"); │ │608 try writer.flush(); │ │609 │ │603 - // the spec allows the client to send arbitrary comment lines before its │ │604 - // banner; skip anything that isn't an "SSH-2.0-…" / "SSH-1.99-…" line. │ │605 - while (true) { │ │610 + // the spec allows the client to send comment lines before its banner; │ │611 + // skip anything that isn't an "SSH-2.0-…" / "SSH-1.99-…" line, up to a cap │ │612 + // so a garbage-spewing peer can't hold the connection open forever. │ │613 + var lines: u32 = 0; │ │614 + while (lines < max_banner_lines) : (lines += 1) { │ │615 const line = (try reader.takeDelimiter('\n')) orelse return error.UnexpectedEof; │ │616 const trimmed = if (line.len > 0 and line[line.len - 1] == '\r') line[0 .. line.len - 1] else line;│ │617 if (std.mem.startsWith(u8, trimmed, "SSH-2.0-") or std.mem.startsWith(u8, trimmed, "SSH-1.99-")) { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯