╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────────────────╮ │src/serve_ssh_protocol.zig│ ╰──────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │386 fn maybeRefillRecvWindowForBufferedInput(self: *SessionCtx) !void { │ │373 - if (self.incomingBytes().len >= incoming_refill_threshold) return; │ │374 - try maybeRefillRecvWindow(self.conn, self.channel); │ │387 + // credit only what the incoming buffer can still take │ │388 + const buffered: u32 = @intCast(self.incomingBytes().len); │ │389 + try maybeRefillRecvWindow(self.conn, self.channel, max_incoming_buffered - buffered);│ │390 } │ │391 │ │392 /// signal the consumer's exit status and tear the channel down. │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────╮ │396 const conn = self.conn; │ │397 if (conn.cs_cipher.idle) |idle| idle.closing.store(true, .release); │ │398 │ │384 - // exit-status request (informational; client uses it as the │ │385 - // command's exit code) │ │386 - { │ │387 - var req: std.ArrayList(u8) = .empty; │ │388 - defer req.deinit(conn.allocator); │ │389 - try req.append(conn.allocator, SSH_MSG_CHANNEL_REQUEST); │ │390 - try writeU32(&req, conn.allocator, self.channel.remote_id); │ │391 - try writeStringField(&req, conn.allocator, "exit-status"); │ │392 - try req.append(conn.allocator, 0); // want_reply MUST be false │ │393 - try writeU32(&req, conn.allocator, status); │ │394 - try conn.sc_cipher.writePacket(conn.io, conn.writer, req.items); │ │399 + // once the peer has sent CHANNEL_CLOSE the channel is gone on its │ │400 + // side, and these would be addressed to an id it no longer knows. │ │401 + if (!self.remote_closed) { │ │402 + // exit-status request (informational; client uses it as the │ │403 + // command's exit code) │ │404 + { │ │405 + var req: std.ArrayList(u8) = .empty; │ │406 + defer req.deinit(conn.allocator); │ │407 + try req.append(conn.allocator, SSH_MSG_CHANNEL_REQUEST); │ │408 + try writeU32(&req, conn.allocator, self.channel.remote_id); │ │409 + try writeStringField(&req, conn.allocator, "exit-status"); │ │410 + try req.append(conn.allocator, 0); // want_reply MUST be false │ │411 + try writeU32(&req, conn.allocator, status); │ │412 + try conn.sc_cipher.writePacket(conn.io, conn.writer, req.items); │ │413 + } │ │414 + │ │415 + // EOF then CLOSE │ │416 + { │ │417 + var eof: std.ArrayList(u8) = .empty; │ │418 + defer eof.deinit(conn.allocator); │ │419 + try eof.append(conn.allocator, SSH_MSG_CHANNEL_EOF); │ │420 + try writeU32(&eof, conn.allocator, self.channel.remote_id); │ │421 + try conn.sc_cipher.writePacket(conn.io, conn.writer, eof.items); │ │422 + } │ │423 + { │ │424 + var close: std.ArrayList(u8) = .empty; │ │425 + defer close.deinit(conn.allocator); │ │426 + try close.append(conn.allocator, SSH_MSG_CHANNEL_CLOSE); │ │427 + try writeU32(&close, conn.allocator, self.channel.remote_id); │ │428 + try conn.sc_cipher.writePacket(conn.io, conn.writer, close.items);│ │429 + } │ │430 } │ │431 │ │397 - // EOF then CLOSE │ │398 - var eof: std.ArrayList(u8) = .empty; │ │399 - defer eof.deinit(conn.allocator); │ │400 - try eof.append(conn.allocator, SSH_MSG_CHANNEL_EOF); │ │401 - try writeU32(&eof, conn.allocator, self.channel.remote_id); │ │402 - try conn.sc_cipher.writePacket(conn.io, conn.writer, eof.items); │ │403 - │ │404 - var close: std.ArrayList(u8) = .empty; │ │405 - defer close.deinit(conn.allocator); │ │406 - try close.append(conn.allocator, SSH_MSG_CHANNEL_CLOSE); │ │407 - try writeU32(&close, conn.allocator, self.channel.remote_id); │ │408 - try conn.sc_cipher.writePacket(conn.io, conn.writer, close.items); │ │409 - │ │432 // read and discard until the peer closes (TCP FIN) before letting the │ │433 // caller close the socket. a git client, after our CHANNEL_CLOSE, still │ │434 // sends its own CHANNEL_CLOSE and SSH_MSG_DISCONNECT before closing; if │ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │617 }; │ │618 conn.cs_cipher.idle = idle; │ │619 │ │598 - const fingerprint = try runAuth(&conn); │ │620 + const fingerprint = runAuth(&conn) catch |err| { │ │621 + disconnectOnError(&conn, err); │ │622 + return err; │ │623 + }; │ │624 if (idle) |state| state.authenticated.store(true, .release); │ │625 │ │601 - try runChannelLayer(&conn, &fingerprint, handler); │ │626 + runChannelLayer(&conn, &fingerprint, handler) catch |err| { │ │627 + disconnectOnError(&conn, err); │ │628 + return err; │ │629 + }; │ │630 +} │ │631 + │ │632 +/// tell the peer why we're hanging up (RFC 4253 §11.1). best effort — a peer │ │633 +/// that already went away has nothing left to read it. │ │634 +fn disconnectOnError(conn: *Conn, err: anyerror) void { │ │635 + if (err == error.EndOfStream) return; // peer hung up first │ │636 + disconnect(conn, @errorName(err)) catch {}; │ │637 +} │ │638 + │ │639 +fn disconnect(conn: *Conn, description: []const u8) !void { │ │640 + var buf: std.ArrayList(u8) = .empty; │ │641 + defer buf.deinit(conn.allocator); │ │642 + try buf.append(conn.allocator, SSH_MSG_DISCONNECT); │ │643 + try writeU32(&buf, conn.allocator, SSH_DISCONNECT_PROTOCOL_ERROR); │ │644 + try writeStringField(&buf, conn.allocator, description); │ │645 + try writeStringField(&buf, conn.allocator, ""); // language tag │ │646 + try conn.sc_cipher.writePacket(conn.io, conn.writer, buf.items); │ │647 } │ │648 │ │649 // ---------------------------------------------------------------------------│ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │755 conn.allocator.free(packet); │ │756 continue; │ │757 } │ │758 + // the peer is hanging up (RFC 4253 §11.1). same end of the packet │ │759 + // stream as a transport close, so callers handle both the same way.│ │760 + if (packet.len >= 1 and packet[0] == SSH_MSG_DISCONNECT) { │ │761 + conn.allocator.free(packet); │ │762 + return error.EndOfStream; │ │763 + } │ │764 if (packet.len >= 1 and packet[0] == SSH_MSG_KEXINIT) { │ │765 defer conn.allocator.free(packet); │ │766 recordActivity(conn); │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │772 } │ │773 } │ │774 │ │775 +/// RFC 4253 §11.4 — an unrecognized message must be answered with │ │776 +/// SSH_MSG_UNIMPLEMENTED carrying the offending packet's sequence number. │ │777 +fn sendUnimplemented(conn: *Conn) !void { │ │778 + var buf: std.ArrayList(u8) = .empty; │ │779 + defer buf.deinit(conn.allocator); │ │780 + try buf.append(conn.allocator, SSH_MSG_UNIMPLEMENTED); │ │781 + // the read already advanced past the packet we're rejecting │ │782 + try writeU32(&buf, conn.allocator, @truncate(conn.cs_cipher.seq - 1)); │ │783 + try conn.sc_cipher.writePacket(conn.io, conn.writer, buf.items); │ │784 +} │ │785 + │ │786 fn recordActivity(conn: *Conn) void { │ │787 if (conn.cs_cipher.idle) |idle| _ = idle.activity.fetchAdd(1, .monotonic);│ │788 } │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────╮ │913 host_key_algos: []u8, │ │914 cs_cipher: []u8, │ │915 sc_cipher: []u8, │ │916 + first_kex_packet_follows: bool, │ │917 │ │918 fn deinit(self: ParsedKexInit, allocator: std.mem.Allocator) void {│ │919 allocator.free(self.kex_algos); │ ╰────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────╮ │944 if (len > max_name_list_len) return error.FieldTooLarge; │ │945 try reader.discardAll(len); │ │946 } │ │884 - _ = try reader.takeByte(); // first_kex_packet_follows │ │947 + const first_kex_packet_follows = (try reader.takeByte()) != 0;│ │948 _ = try reader.takeInt(u32, .big); // reserved │ │949 │ │950 return .{ │ ╰───────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────╮ │952 .host_key_algos = host_key_algos, │ │953 .cs_cipher = cs_cipher, │ │954 .sc_cipher = sc_cipher, │ │955 + .first_kex_packet_follows = first_kex_packet_follows,│ │956 }; │ │957 } │ │958 │ ╰──────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────╮ │1101 if (!nameListContainsAny(parsed.cs_cipher, &our_ciphers)) return error.NoCommonCipher;│ │1102 if (!nameListContainsAny(parsed.sc_cipher, &our_ciphers)) return error.NoCommonCipher;│ │1103 const hybrid = std.mem.eql(u8, kex_algo, "mlkem768x25519-sha256"); │ │1104 + │ │1105 + // a client may follow its KEXINIT with a kex packet for its guess. the │ │1106 + // guess is right only when both sides' first kex and host key algorithms │ │1107 + // match (RFC 4253 §7.1) — not when the client's first choice merely │ │1108 + // happens to be kex_algo, so this compares against our list. a client │ │1109 + // whose guess was wrong re-sends an init for the negotiated method. │ │1110 + if (parsed.first_kex_packet_follows) { │ │1111 + var kex_iter = std.mem.splitScalar(u8, parsed.kex_algos, ','); │ │1112 + var host_key_iter = std.mem.splitScalar(u8, parsed.host_key_algos, ','); │ │1113 + const guessed_right = std.mem.eql(u8, kex_iter.first(), our_kex_algos[0]) and │ │1114 + std.mem.eql(u8, host_key_iter.first(), our_host_key_algos[0]); │ │1115 + if (!guessed_right) { │ │1116 + const wrong_guess = try transport.readPacket(allocator, reader, strict); │ │1117 + allocator.free(wrong_guess); │ │1118 + } │ │1119 + } │ │1120 │ │1121 // receive KEX_ECDH_INIT (mlkem768x25519 reuses message code 30 — same │ │1122 // wire shape, only the string size differs). │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────╮ │1769 return; │ │1770 }, │ │1771 │ │1692 - else => {}, // ignore unknown messages│ │1772 + else => try sendUnimplemented(conn), │ │1773 } │ │1774 } │ │1775 } │ ╰────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯