Fix reconnect stale keys and TUI status rendering
ci / test (push) Has been cancelled
ci / fuzz-smoke (push) Has been cancelled
ci / windows-client (push) Has been cancelled
ci / package-release (linux-x86_64, ubuntu-latest) (push) Has been cancelled
ci / package-release (macos-aarch64, macos-14) (push) Has been cancelled
ci / package-release (macos-x86_64, macos-13) (push) Has been cancelled
ci / package-release (windows-x86_64, windows-latest) (push) Has been cancelled
ci / remote-bench (push) Has been cancelled

This commit is contained in:
DuProcess
2026-06-27 11:46:26 -04:00
parent f71cd975bc
commit 9d0396dbf9
3 changed files with 100 additions and 25 deletions
+10 -17
View File
@@ -674,8 +674,9 @@ impl ServerState {
/// previous epoch's key must still decrypt instead of being dropped as fatal
/// (spec: "stale encrypted packets ... ignored, not fatal"). We pick whichever
/// of the current or retained-previous key matches the packet's
/// `session_key_id`, falling back to the current key when neither matches (so
/// a genuinely stale/forged packet still fails the AEAD check, not silently).
/// `session_key_id`. If neither key id matches, the client is on a stale
/// epoch and should be forced through the normal reconnect path instead of
/// waiting for a silent timeout.
fn lookup_client_key_for(
&self,
client_id: &[u8; 16],
@@ -692,7 +693,7 @@ impl ServerState {
{
return Some((previous, session_name.clone()));
}
Some((client.session_key, session_name.clone()))
None
}
/// O(1) mutable access to a live client via the conn_id index.
@@ -836,7 +837,7 @@ async fn handle_packet(
| PacketKind::StreamEof
| PacketKind::StreamWindowAdjust
| PacketKind::RekeyAck
if find_client_key(state, &packet.header.conn_id).is_err() =>
if find_client_decrypt_key(state, &packet.header).is_err() =>
{
send_reject_to_client(socket, peer, packet.header.conn_id, "unknown client").await
}
@@ -2822,6 +2823,7 @@ fn screen_snapshot(screen: &vt100::Screen) -> Vec<u8> {
let mut bytes = Vec::new();
if screen.alternate_screen() {
bytes.extend_from_slice(b"\x1b[?1049h");
bytes.extend_from_slice(b"\x1b[H\x1b[2J");
} else {
bytes.extend_from_slice(b"\x1b[?1049l");
}
@@ -3172,19 +3174,10 @@ fn cleanup_disconnected_clients(state: &Arc<Mutex<ServerState>>) {
}
}
fn find_client_key(
state: &Arc<Mutex<ServerState>>,
client_id: &[u8; 16],
) -> Result<([u8; 32], String)> {
let locked = state.lock().expect("server state poisoned");
locked
.lookup_client(client_id)
.ok_or_else(|| anyhow!("unknown client"))
}
/// Like [`find_client_key`] but selects the key (current or retained previous
/// epoch) matching the packet header's `session_key_id`, so a packet sealed
/// under the pre-rekey key during the grace window still decrypts.
/// Select the client key (current or retained previous epoch) matching the
/// packet header's `session_key_id`, so a packet sealed under the pre-rekey key
/// during the grace window still decrypts while an expired/stale epoch is
/// treated as an unknown client and triggers a client reconnect.
fn find_client_decrypt_key(
state: &Arc<Mutex<ServerState>>,
header: &protocol::Header,