╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────────────────╮ │src/serve_ssh_protocol.zig│ ╰──────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │895 // the peer is hanging up (RFC 4253 §11.1). same end of the packet │ │896 // stream as a transport close, so callers handle both the same way.│ │985 - if (packet.len >= 1 and packet[0] == SSH_MSG_DISCONNECT) { │ │897 + if (packet[0] == SSH_MSG_DISCONNECT) { │ │898 conn.allocator.free(packet); │ │899 return error.EndOfStream; │ │900 } │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────╮ │989 - if (packet.len >= 1 and packet[0] == SSH_MSG_KEXINIT) {│ │901 + if (packet[0] == SSH_MSG_KEXINIT) { │ │902 defer conn.allocator.free(packet); │ │991 - recordActivity(conn); │ │903 + _ = conn.idle.activity.fetchAdd(1, .acq_rel); │ │904 try runRekey(conn, packet); │ │905 continue; │ │906 } │ ╰────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────╮ │995 - recordActivity(conn); │ │907 + _ = conn.idle.activity.fetchAdd(1, .acq_rel);│ │908 return packet; │ │909 } │ │910 } │ ╰──────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────╮ │ 912 /// RFC 4253 §11.4 — an unrecognized message must be answered with │ │ 913 /// SSH_MSG_UNIMPLEMENTED carrying the offending packet's sequence number. │ │ 914 fn sendUnimplemented(conn: *Conn) !void { │ │1003 - var buf: std.ArrayList(u8) = .empty; │ │1004 - defer buf.deinit(conn.allocator); │ │1005 - try buf.append(conn.allocator, SSH_MSG_UNIMPLEMENTED); │ │ 915 + var packet: [5]u8 = undefined; │ │ 916 + packet[0] = SSH_MSG_UNIMPLEMENTED; │ │ 917 // the read already advanced past the packet we're rejecting │ │1007 - try writeU32(&buf, conn.allocator, @truncate(conn.cs_cipher.seq - 1)); │ │1008 - try conn.writePacket(&.{buf.items}); │ │ 918 + std.mem.writeInt(u32, packet[1..5], @truncate(conn.cs_cipher.seq - 1), .big);│ │ 919 + try conn.writePacket(&.{&packet}); │ │ 920 } │ │ 921 │ │1011 -fn recordActivity(conn: *Conn) void { │ │1012 - if (conn.idle) |idle| _ = idle.activity.fetchAdd(1, .acq_rel); │ │1013 -} │ │1014 - │ │ 922 // --------------------------------------------------------------------------- │ │ 923 // SSH field encoders / decoders (RFC 4251 §5) │ │ 924 // --------------------------------------------------------------------------- │ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────╮ │ 973 return reader.take(len); │ │ 974 } │ │ 975 │ │1069 -fn nameListContainsAny(haystack: []const u8, our_options: []const []const u8) bool { │ │1070 - var iter = std.mem.splitScalar(u8, haystack, ','); │ │ 976 +// the first name in the peer's preference list that we also support (RFC │ │ 977 +// 4253 §7.1) │ │ 978 +fn firstCommonName(peer_names: []const u8, our_names: []const []const u8) ?[]const u8 {│ │ 979 + var iter = std.mem.splitScalar(u8, peer_names, ','); │ │ 980 while (iter.next()) |name| { │ │1072 - for (our_options) |opt| { │ │1073 - if (std.mem.eql(u8, name, opt)) return true; │ │ 981 + for (our_names) |ours| { │ │ 982 + if (std.mem.eql(u8, name, ours)) return ours; │ │ 983 } │ │ 984 } │ │1076 - return false; │ │ 985 + return null; │ │ 986 } │ │ 987 │ │ 988 // --------------------------------------------------------------------------- │ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │1080 // ---------------------------------------------------------------------------│ │1081 // KEX orchestration: KEXINIT → ECDH → NEWKEYS → key derivation │ │1082 // ---------------------------------------------------------------------------│ │1174 - │ │1175 -const KexResult = struct { │ │1176 - cs_key: [64]u8, // client → server cipher key material (K_2 || K_1) │ │1177 - sc_key: [64]u8, // server → client cipher key material │ │1178 - session_id: [Sha256.digest_length]u8, │ │1179 - next_cs_seq: u64, │ │1180 - next_sc_seq: u64, │ │1181 - strict: bool, │ │1182 -}; │ │1083 │ │1084 fn runKex( │ │1085 io: std.Io, │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────╮ │1088 writer: *std.Io.Writer, │ │1089 host_key: *const HostKey, │ │1090 client_version: []const u8, │ │1191 -) !KexResult { │ │1091 + idle: *IdleState, │ │1092 +) !Conn { │ │1093 var seqs: PlainSeqs = .{}; │ │1094 const transport = KexTransport{ .plain = &seqs };│ │1095 │ ╰───────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────╮ │1106 // strict kex: when the client advertises it, its KEXINIT must be the │ │1107 // first packet, ignorable messages are banned until NEWKEYS, and both │ │1108 // seqnos reset to 0 after every NEWKEYS. │ │1208 - const strict = nameListContainsAny(parsed.kex_algos, &.{kex_strict_client}); │ │1109 + const strict = firstCommonName(parsed.kex_algos, &.{kex_strict_client}) != null;│ │1110 if (strict and seqs.read != 1) return error.StrictKexViolation; │ │1111 │ │1211 - var negotiated = try exchangeKeys( │ │1112 + var keys: SessionKeys = undefined; │ │1113 + defer std.crypto.secureZero(u8, std.mem.asBytes(&keys)); │ │1114 + const session_id = try exchangeKeys( │ │1115 io, │ │1116 allocator, │ │1117 reader, │ ╰──────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │1124 server_kex_init, │ │1125 &parsed, │ │1126 null, │ │1127 + &keys, │ │1128 ); │ │1225 - defer std.crypto.secureZero(u8, std.mem.asBytes(&negotiated.keys)); │ │1129 │ │1130 + // post-KEX everything is encrypted with chacha20-poly1305@openssh.com.│ │1131 + // each cipher continues its own direction's count; the read side │ │1132 + // includes any skipped IGNORE/DEBUG packets. │ │1133 return .{ │ │1228 - .cs_key = negotiated.keys.cs_enc, │ │1229 - .sc_key = negotiated.keys.sc_enc, │ │1230 - .session_id = negotiated.exchange_hash, │ │1231 - // each cipher continues its own direction's count; the read side │ │1232 - // includes any skipped IGNORE/DEBUG packets. │ │1233 - .next_cs_seq = if (strict) 0 else seqs.read, │ │1234 - .next_sc_seq = if (strict) 0 else seqs.written, │ │1235 - .strict = strict, │ │1134 + .io = io, │ │1135 + .allocator = allocator, │ │1136 + .reader = reader, │ │1137 + .writer = writer, │ │1138 + .cs_cipher = .init(&keys.cs_enc, if (strict) 0 else seqs.read), │ │1139 + .sc_cipher = .init(&keys.sc_enc, if (strict) 0 else seqs.written), │ │1140 + .rekey = .{ │ │1141 + .host_key = host_key, │ │1142 + .client_version = client_version, │ │1143 + .session_id = session_id, │ │1144 + .strict = strict, │ │1145 + }, │ │1146 + .idle = idle, │ │1147 }; │ │1148 } │ │1149 │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │1162 const parsed = try parseClientKexInit(client_kex_init); │ │1163 │ │1164 // the exchange reads and writes through the ciphers directly │ │1254 - conn.setWaiting(true); │ │1255 - defer conn.setWaiting(false); │ │1165 + conn.idle.waiting.store(true, .release); │ │1166 + defer conn.idle.waiting.store(false, .release); │ │1167 │ │1168 const server_kex_init = try buildServerKexInit(conn.io, conn.allocator);│ │1169 defer conn.allocator.free(server_kex_init); │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯