╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │10 const evt = @import("./event.zig"); │ │11 const serve_common = @import("./serve_common.zig"); │ │12 │ │13 -// listener resource limits. idle enforcement is coarse: the watchdog wakes │ │14 -// once per interval and shuts the connection down if no SSH packet arrived │ │15 -// during the entire previous interval. each connection holds two OS threads │ │16 -// under std.Io.Threaded (session + watchdog), so the cap is sized to keep │ │17 -// the worst-case thread count in comfortable territory, not by RAM. │ │13 +// listener resource limits. the watchdog gives unauthenticated peers a hard │ │14 +// deadline, detects idle non-interactive sessions from protocol progress, and│ │15 +// bounds the post-CLOSE drain. each connection holds two OS threads under │ │16 +// std.Io.Threaded (session + watchdog), so the cap is sized by thread count. │ │17 const max_connections: u32 = 4096; │ │19 -const idle_interval = std.Io.Duration.fromSeconds(120); │ │18 +const watchdog_interval = std.Io.Duration.fromSeconds(5); │ │19 +const preauth_timeout_ticks: u32 = 6; // 30 seconds │ │20 +const idle_timeout_ticks: u32 = 24; // 120 seconds │ │21 +const close_drain_timeout_ticks: u32 = 2; // 5-10 seconds │ │22 │ │23 var active_connections: std.atomic.Value(u32) = .init(0); │ │24 │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │112 }, handle); │ │113 } │ │114 │ │113 -// shut down connections that made no SSH packet progress for a whole │ │114 -// interval. exempt connections (interactive TUIs) end enforcement instead. │ │115 +// enforce a hard pre-auth deadline, an idle timeout for non-interactive │ │116 +// sessions, and a bounded close drain. interactive sessions are exempt only │ │117 +// from the idle timeout; the watchdog remains available to break a stalled │ │118 +// teardown. │ │119 fn watchdog(io: std.Io, stream: *const std.Io.net.Stream, idle_state: *ssh.IdleState) void {│ │120 var last_seen: u64 = 0; │ │121 + var preauth_ticks: u32 = 0; │ │122 + var idle_ticks: u32 = 0; │ │123 + var closing_ticks: u32 = 0; │ │124 while (true) { │ │118 - io.sleep(idle_interval, .awake) catch return; // canceled — connection is done │ │119 - if (idle_state.exempt.load(.monotonic)) return; │ │125 + io.sleep(watchdog_interval, .awake) catch return; // canceled — connection is done │ │126 + │ │127 + if (idle_state.closing.load(.acquire)) { │ │128 + closing_ticks += 1; │ │129 + if (closing_ticks >= close_drain_timeout_ticks) { │ │130 + stream.shutdown(io, .both) catch {}; │ │131 + return; │ │132 + } │ │133 + continue; │ │134 + } │ │135 + │ │136 + if (!idle_state.authenticated.load(.acquire)) { │ │137 + preauth_ticks += 1; │ │138 + if (preauth_ticks >= preauth_timeout_ticks) { │ │139 + stream.shutdown(io, .both) catch {}; │ │140 + return; │ │141 + } │ │142 + continue; │ │143 + } │ │144 + │ │145 + if (idle_state.exempt.load(.monotonic)) continue; │ │146 const seen = idle_state.activity.load(.monotonic); │ │147 if (seen == last_seen) { │ │122 - stream.shutdown(io, .both) catch {}; │ │123 - return; │ │148 + idle_ticks += 1; │ │149 + if (idle_ticks >= idle_timeout_ticks) { │ │150 + stream.shutdown(io, .both) catch {}; │ │151 + return; │ │152 + } │ │153 + } else { │ │154 + idle_ticks = 0; │ │155 } │ │156 last_seen = seen; │ │157 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯