╭ 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. │ │18 +const max_connections: u32 = 4096; │ │19 +const idle_interval = std.Io.Duration.fromSeconds(120); │ │20 + │ │21 +var active_connections: std.atomic.Value(u32) = .init(0); │ │22 + │ │23 pub const SessionHandler = struct { │ │24 admin_repo_path: []const u8, │ │25 repo_root_path: []const u8, │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────╮ │38 try sess.exit(1); │ │39 return; │ │40 }; │ │41 + sess.exemptFromIdleTimeout(); │ │42 try runTuiSession(self, sess, pty);│ │43 }, │ │44 .exec => |command| { │ ╰───────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │68 const handle = struct { │ │69 fn h(ctx: Context, stream: std.Io.net.Stream) void { │ │70 defer stream.close(ctx.io); │ │71 + │ │72 + const prev_count = active_connections.fetchAdd(1, .monotonic); │ │73 + defer _ = active_connections.fetchSub(1, .monotonic); │ │74 + if (prev_count >= max_connections) { │ │75 + serve_common.logError(ctx.err, "ssh: connection limit reached, dropping\n", .{}); │ │76 + return; │ │77 + } │ │78 + │ │79 + var idle_state = ssh.IdleState{}; │ │80 + var watchdog_future = std.Io.concurrent(ctx.io, watchdog, .{ ctx.io, &stream, &idle_state }) catch |spawn_err| {│ │81 + serve_common.logError(ctx.err, "ssh: watchdog spawn failed: {s}\n", .{@errorName(spawn_err)}); │ │82 + return; │ │83 + }; │ │84 + defer watchdog_future.cancel(ctx.io); │ │85 + │ │86 var recv_buf: [4096]u8 = undefined; │ │87 var send_buf: [4096]u8 = undefined; │ │88 var stream_reader = stream.reader(ctx.io, &recv_buf); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │93 &stream_reader.interface, │ │94 &stream_writer.interface, │ │95 ctx.host_key, │ │96 + &idle_state, │ │97 ctx.session_handler, │ │98 ) catch |session_err| { │ │99 serve_common.logError(ctx.err, "ssh session failed: {s}\n", .{@errorName(session_err)});│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮ │110 }, handle); │ │111 } │ │112 │ │113 +// shut down connections that made no SSH packet progress for a whole │ │114 +// interval. exempt connections (interactive TUIs) end enforcement instead. │ │115 +fn watchdog(io: std.Io, stream: *const std.Io.net.Stream, idle_state: *ssh.IdleState) void { │ │116 + var last_seen: u64 = 0; │ │117 + while (true) { │ │118 + io.sleep(idle_interval, .awake) catch return; // canceled — connection is done │ │119 + if (idle_state.exempt.load(.monotonic)) return; │ │120 + const seen = idle_state.activity.load(.monotonic); │ │121 + if (seen == last_seen) { │ │122 + stream.shutdown(io, .both) catch {}; │ │123 + return; │ │124 + } │ │125 + last_seen = seen; │ │126 + } │ │127 +} │ │128 + │ │129 fn runTuiSession(handler: *const SessionHandler, sess: *ssh.SessionCtx, pty: ssh.PtySize) !void {│ │130 // runTui owns the terminal, whose deinit restores the client's screen and │ │131 // runs as the function unwinds — so any error it returns leaves the TUI torn │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯