╭ changed ─────────────────╮ │src/serve_ssh_protocol.zig│ ╰──────────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────────────────────────────────────────────╮ │2 //! │ │3 //! handles, per connection: version exchange, KEXINIT, curve25519 key │ │4 //! exchange, NEWKEYS, key derivation, chacha20-poly1305 packet codec, │ │5 +//! strict kex (Terrapin mitigation), client-initiated rekeying, │ │6 //! ssh-userauth service request, publickey authentication (ed25519 only;│ │7 //! captures the SHA256 fingerprint of the verified key), channel-layer │ │8 //! requests (pty-req, env, window-change, shell, exec), CHANNEL_DATA │ ╰────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │27 //! MAC: none (implicit in AEAD) │ │28 //! compress: none │ │29 //! user auth: publickey + ssh-ed25519 only (RSA/ECDSA deferred) │ │29 -//! │ │30 -//! TODO: add SSH session idle deadlines. A reachable SSH listener can currently│ │31 -//! hold one task per idle connection while blocked in banner exchange, KEX, │ │32 -//! auth, channel setup, waiting for send WINDOW_ADJUST, reading git pack data, │ │33 -//! or waiting for peer CLOSE during teardown. The eventual fix should enforce a│ │34 -//! per-connection idle timeout that is refreshed by successful SSH packet I/O │ │35 -//! and closes/shuts down the underlying stream when no progress occurs. Prefer │ │36 -//! an I/O-layer deadline if std.Io exposes one; otherwise use a carefully-owned│ │37 -//! watchdog that cannot outlive the connection state it observes. │ │30 │ │31 const std = @import("std"); │ │32 const builtin = @import("builtin"); │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────╮ │43 const max_name_list_len: u32 = 4096; │ │44 const max_auth_attempts: u32 = 20; │ │45 const max_exit_drain_packets: u32 = 32; │ │46 +const max_banner_lines: u32 = 32; │ │47 │ │48 // SSH message type bytes (RFC 4250 §4.1)│ │49 pub const SSH_MSG_DISCONNECT: u8 = 1; │ ╰─────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │ 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. │ │ 98 +pub const IdleState = struct { │ │ 99 + activity: std.atomic.Value(u64) = .init(0), │ │100 + exempt: std.atomic.Value(bool) = .init(false), │ │101 +}; │ │102 + │ │103 // ---------------------------------------------------------------------------│ │104 // host key │ │105 // ---------------------------------------------------------------------------│ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │127 } │ │128 │ │129 const keypair = Ed25519.KeyPair.generate(io); │ │128 - const file = try cwd.createFile(io, path, .{}); │ │130 + // owner-only from the start — never a window where the key is readable │ │131 + const permissions: std.Io.File.Permissions = if (builtin.os.tag == .windows) .default_file else @enumFromInt(0o600);│ │132 + const file = try cwd.createFile(io, path, .{ .permissions = permissions }); │ │133 defer file.close(io); │ │134 try file.writeStreamingAll(io, &keypair.secret_key.bytes); │ │131 - if (builtin.os.tag != .windows) { │ │132 - try file.setPermissions(io, @enumFromInt(0o600)); │ │133 - } │ │135 return .{ .keypair = keypair }; │ │136 } │ │137 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │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; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯