╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────────────────╮ │src/serve_ssh_protocol.zig│ ╰──────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │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, │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │1385 const payload = try allocator.dupe(u8, enc_body[1 .. 1 + payload_len]);│ │1386 │ │1387 self.seq += 1; │ │1378 - if (self.idle) |idle| _ = idle.activity.fetchAdd(1, .monotonic); │ │1388 return payload; │ │1389 } │ │1390 }; │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────╮ │src/test/ssh.zig│ ╰────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 85 packet.items[8] = 2; │ │ 86 try std.testing.expectError(error.UnsupportedExtendedData, proto.parseChannelData(packet.items, true, 0));│ │ 87 } │ │ 88 + │ │ 89 +test "initial strict kex rejects ignored packets" { │ │ 90 + const io = std.testing.io; │ │ 91 + const allocator = std.testing.allocator; │ │ 92 + │ │ 93 + var encoded_buf: [128]u8 = undefined; │ │ 94 + var writer = std.Io.Writer.fixed(&encoded_buf); │ │ 95 + try proto.writePlainPacket(io, &writer, &.{proto.SSH_MSG_IGNORE}); │ │ 96 │ │ 97 + var reader = std.Io.Reader.fixed(writer.buffered()); │ │ 98 + var client_seq: u64 = 0; │ │ 99 + const transport = proto.KexTransport{ .plain = &client_seq }; │ │100 + try std.testing.expectError(error.StrictKexViolation, transport.readPacket(allocator, &reader, true)); │ │101 + try std.testing.expectEqual(@as(u64, 1), client_seq); │ │102 +} │ │103 + │ │104 +test "ignored packets do not count as connection activity" { │ │105 + const io = std.testing.io; │ │106 + const allocator = std.testing.allocator; │ │107 + const key_material = [_]u8{0x5A} ** 64; │ │108 + │ │109 + var sender = proto.Cipher.init(&key_material, 0); │ │110 + var encoded_buf: [128]u8 = undefined; │ │111 + var encoded_writer = std.Io.Writer.fixed(&encoded_buf); │ │112 + try sender.writePacket(io, &encoded_writer, &.{proto.SSH_MSG_IGNORE}); │ │113 + │ │114 + var idle = proto.IdleState{}; │ │115 + var receiver = proto.Cipher.init(&key_material, 0); │ │116 + receiver.idle = &idle; │ │117 + var reader = std.Io.Reader.fixed(encoded_writer.buffered()); │ │118 + var output_buf: [1]u8 = undefined; │ │119 + var output_writer = std.Io.Writer.fixed(&output_buf); │ │120 + var conn = proto.Conn{ │ │121 + .io = io, │ │122 + .allocator = allocator, │ │123 + .reader = &reader, │ │124 + .writer = &output_writer, │ │125 + .cs_cipher = receiver, │ │126 + .sc_cipher = sender, │ │127 + .rekey = undefined, │ │128 + }; │ │129 + │ │130 + try std.testing.expectError(error.EndOfStream, proto.readSessionPacket(&conn)); │ │131 + try std.testing.expectEqual(@as(u64, 0), idle.activity.load(.monotonic)); │ │132 +} │ │133 + │ │134 test "SSH-2 negotiation: walk through every step (banner -> KEX -> auth -> channel -> exec)" { │ │135 const io = std.testing.io; │ │136 const allocator = std.testing.allocator; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯