╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────────────────╮ │src/serve_ssh_protocol.zig│ ╰──────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │306 │ │307 /// process exactly one SSH packet for side effects only. CHANNEL_DATA │ │308 /// payloads accumulate in incoming_buffer; CHANNEL_EOF/CLOSE flips │ │317 - /// incoming_eof; WINDOW_ADJUST updates the channel state; channel/global │ │318 - /// requests get a polite failure. used both by SessionReader (waiting │ │319 - /// for inbound bytes) and writeBytes (waiting for send-window). │ │309 + /// incoming_eof; WINDOW_ADJUST updates the channel state; window-change │ │310 + /// is stashed in pending_resize for the next nextEvent call; other │ │311 + /// channel/global requests get a polite failure. used both by │ │312 + /// SessionReader (waiting for inbound bytes) and writeBytes (waiting │ │313 + /// for send-window). │ │314 fn processOneBackgroundPacket(self: *SessionCtx) !void { │ │315 const packet = self.cs_cipher.readPacket(self.allocator, self.reader) catch |err| switch (err) {│ │316 error.EndOfStream => { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │349 const req_type = try takeStringField(self.allocator, &r, 64); │ │350 defer self.allocator.free(req_type); │ │351 const want_reply = (try r.takeByte()) != 0; │ │358 - // mid-session channel requests aren't honored — fail them │ │359 - try replyChannelRequest(self.io, self.allocator, self.sc_cipher, self.writer, self.channel, want_reply, false); │ │352 + if (std.mem.eql(u8, req_type, "window-change")) { │ │353 + const sz = try takePtySize(&r); │ │354 + if (self.channel.pty != null) self.channel.pty = sz; │ │355 + self.pending_resize = sz; │ │356 + try replyChannelRequest(self.io, self.allocator, self.sc_cipher, self.writer, self.channel, want_reply, true); │ │357 + } else { │ │358 + // other mid-session channel requests aren't honored — fail them │ │359 + try replyChannelRequest(self.io, self.allocator, self.sc_cipher, self.writer, self.channel, want_reply, false);│ │360 + } │ │361 }, │ │362 SSH_MSG_CHANNEL_EOF, SSH_MSG_CHANNEL_CLOSE => { │ │363 try parseChannelId(packet, self.channel.local_id); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │577 const kex = try runKex(io, allocator, reader, writer, host_key, client_version); │ │578 │ │579 // post-KEX everything is encrypted with chacha20-poly1305@openssh.com. │ │579 - // both sides started counting packet seqnos at 0; KEXINIT, KEX_ECDH and │ │580 - // NEWKEYS used seqno 0/1/2, so the first encrypted packet uses seqno 3. │ │581 - var cs_cipher = Cipher.init(&kex.cs_key, 3); │ │580 + // both sides started counting packet seqnos at 0. we sent exactly │ │581 + // KEXINIT, KEX_ECDH_REPLY and NEWKEYS, so our first encrypted packet │ │582 + // uses seqno 3; the client's count includes any skipped IGNORE/DEBUG. │ │583 + var cs_cipher = Cipher.init(&kex.cs_key, kex.next_cs_seq); │ │584 var sc_cipher = Cipher.init(&kex.sc_key, 3); │ │585 │ │586 const fingerprint = try runAuth(io, allocator, reader, writer, &cs_cipher, &sc_cipher, &kex.session_id);│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │642 errdefer allocator.free(payload); │ │643 try reader.discardAll(padding_len); │ │644 return payload; │ │645 +} │ │646 + │ │647 +// IGNORE/DEBUG/UNIMPLEMENTED may arrive at any time (RFC 4253 §11); skip │ │648 +// them wherever a specific message is expected │ │649 +fn isIgnorableMsg(msg_type: u8) bool { │ │650 + return msg_type == SSH_MSG_IGNORE or msg_type == SSH_MSG_UNIMPLEMENTED or msg_type == SSH_MSG_DEBUG;│ │651 } │ │652 │ │653 +// read a plaintext packet, skipping ignorable messages. counts every packet │ │654 +// consumed in `seq` so the caller knows the peer's next sequence number. │ │655 +fn readPlainKexPacket(allocator: std.mem.Allocator, reader: *std.Io.Reader, seq: *u64) ![]u8 { │ │656 + while (true) { │ │657 + const packet = try readPlainPacket(allocator, reader); │ │658 + seq.* += 1; │ │659 + if (packet.len >= 1 and isIgnorableMsg(packet[0])) { │ │660 + allocator.free(packet); │ │661 + continue; │ │662 + } │ │663 + return packet; │ │664 + } │ │665 +} │ │666 + │ │667 +// read an encrypted packet, skipping ignorable messages │ │668 +fn readNonIgnoredPacket(cipher: *Cipher, allocator: std.mem.Allocator, reader: *std.Io.Reader) ![]u8 { │ │669 + while (true) { │ │670 + const packet = try cipher.readPacket(allocator, reader); │ │671 + if (packet.len >= 1 and isIgnorableMsg(packet[0])) { │ │672 + allocator.free(packet); │ │673 + continue; │ │674 + } │ │675 + return packet; │ │676 + } │ │677 +} │ │678 + │ │679 // --------------------------------------------------------------------------- │ │680 // SSH field encoders / decoders (RFC 4251 §5) │ │681 // --------------------------------------------------------------------------- │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │844 cs_key: [64]u8, // client → server cipher key material (K_2 || K_1) │ │845 sc_key: [64]u8, // server → client cipher key material │ │846 session_id: [Sha256.digest_length]u8, │ │847 + next_cs_seq: u64, // packets the client has sent, including skipped ones│ │848 }; │ │849 │ │850 fn runKex( │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────╮ │855 host_key: *const HostKey, │ │856 client_version: []const u8, │ │857 ) !KexResult { │ │858 + var client_seq: u64 = 0; │ │859 + │ │860 // exchange KEXINITs │ │861 const server_kex_init = try buildServerKexInit(io, allocator);│ │862 defer allocator.free(server_kex_init); │ ╰───────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │863 try writePlainPacket(io, writer, server_kex_init); │ │864 │ │828 - const client_kex_init = try readPlainPacket(allocator, reader); │ │865 + const client_kex_init = try readPlainKexPacket(allocator, reader, &client_seq);│ │866 defer allocator.free(client_kex_init); │ │867 │ │868 var parsed = try parseClientKexInit(allocator, client_kex_init); │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │886 │ │887 // receive KEX_ECDH_INIT (mlkem768x25519 reuses message code 30 — same │ │888 // wire shape, only the string size differs). │ │852 - const ecdh_init = try readPlainPacket(allocator, reader); │ │889 + const ecdh_init = try readPlainKexPacket(allocator, reader, &client_seq); │ │890 defer allocator.free(ecdh_init); │ │891 if (ecdh_init.len < 1 or ecdh_init[0] != SSH_MSG_KEX_ECDH_INIT) return error.UnexpectedMessage;│ │892 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │968 // the peer's NEWKEYS is received. │ │969 try writePlainPacket(io, writer, &[_]u8{SSH_MSG_NEWKEYS}); │ │970 │ │934 - const peer_newkeys = try readPlainPacket(allocator, reader); │ │971 + const peer_newkeys = try readPlainKexPacket(allocator, reader, &client_seq); │ │972 defer allocator.free(peer_newkeys); │ │973 if (peer_newkeys.len < 1 or peer_newkeys[0] != SSH_MSG_NEWKEYS) return error.UnexpectedMessage;│ │974 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────╮ │982 .cs_key = keys.cs_enc, │ │983 .sc_key = keys.sc_enc, │ │984 .session_id = exchange_hash,│ │985 + .next_cs_seq = client_seq, │ │986 }; │ │987 } │ │988 │ ╰─────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯