Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37ec9fc520 | ||
|
|
f3c7ec225d | ||
|
|
d3c40a06ac | ||
|
|
6b12b3dfe0 |
Generated
+1
-1
@@ -436,7 +436,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dosh"
|
name = "dosh"
|
||||||
version = "1.0.0-rc4"
|
version = "1.0.0-rc6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "dosh"
|
name = "dosh"
|
||||||
version = "1.0.0-rc4"
|
version = "1.0.0-rc6"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
|
|||||||
+160
-15
@@ -66,6 +66,8 @@ const STARTUP_INPUT_HOLD: Duration = Duration::from_millis(750);
|
|||||||
const POST_SUBMIT_ALL_INPUT_HOLD: Duration = Duration::from_millis(120);
|
const POST_SUBMIT_ALL_INPUT_HOLD: Duration = Duration::from_millis(120);
|
||||||
const STALE_TERMINAL_INPUT_AFTER: Duration = Duration::from_secs(2);
|
const STALE_TERMINAL_INPUT_AFTER: Duration = Duration::from_secs(2);
|
||||||
const POST_RECONNECT_STALE_INPUT_GRACE: Duration = Duration::from_secs(2);
|
const POST_RECONNECT_STALE_INPUT_GRACE: Duration = Duration::from_secs(2);
|
||||||
|
const FOCUS_REPAINT_COOLDOWN: Duration = Duration::from_secs(1);
|
||||||
|
const ALT_SCREEN_IDLE_REPAINT_AFTER: Duration = Duration::from_secs(15);
|
||||||
|
|
||||||
/// Sentinel `target_host` the server uses on a server-initiated `StreamOpen` that
|
/// Sentinel `target_host` the server uses on a server-initiated `StreamOpen` that
|
||||||
/// represents an SSH-agent connection (rather than a TCP target). The client
|
/// represents an SSH-agent connection (rather than a TCP target). The client
|
||||||
@@ -4334,10 +4336,14 @@ async fn run_terminal(
|
|||||||
let mut startup_input_hold_until: Option<Instant> = None;
|
let mut startup_input_hold_until: Option<Instant> = None;
|
||||||
let mut startup_gate_mode = StartupGateMode::HoldControl;
|
let mut startup_gate_mode = StartupGateMode::HoldControl;
|
||||||
let mut stale_terminal_input_suppress_until: Option<Instant> = None;
|
let mut stale_terminal_input_suppress_until: Option<Instant> = None;
|
||||||
|
let mut last_terminal_frame_at = Instant::now();
|
||||||
|
let mut last_focus_repaint_at = Instant::now() - FOCUS_REPAINT_COOLDOWN;
|
||||||
|
let mut last_idle_repaint_attempt_at = Instant::now() - ALT_SCREEN_IDLE_REPAINT_AFTER;
|
||||||
if let Some(frame) = first_frame {
|
if let Some(frame) = first_frame {
|
||||||
if !forward_only {
|
if !forward_only {
|
||||||
render_frame(&frame)?;
|
render_frame(&frame)?;
|
||||||
predictor.observe_output(&frame.bytes);
|
predictor.observe_output(&frame.bytes);
|
||||||
|
last_terminal_frame_at = Instant::now();
|
||||||
}
|
}
|
||||||
if frame.closed {
|
if frame.closed {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -4395,6 +4401,41 @@ async fn run_terminal(
|
|||||||
if input_matches_escape(&bytes, escape_key.as_deref()) {
|
if input_matches_escape(&bytes, escape_key.as_deref()) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if !forward_only
|
||||||
|
&& input_contains_focus_in(&bytes)
|
||||||
|
&& last_focus_repaint_at.elapsed() >= FOCUS_REPAINT_COOLDOWN
|
||||||
|
{
|
||||||
|
last_focus_repaint_at = Instant::now();
|
||||||
|
if let Some(frame) = reconnect(
|
||||||
|
&socket,
|
||||||
|
&mut cred,
|
||||||
|
&mut send_seq,
|
||||||
|
last_size,
|
||||||
|
&mut frame_buffer,
|
||||||
|
&mut predictor,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
refresh_live_addr(&mut addr, &cred)?;
|
||||||
|
arm_stale_terminal_input_suppression(
|
||||||
|
&mut stale_terminal_input_suppress_until,
|
||||||
|
);
|
||||||
|
render_frame(&frame)?;
|
||||||
|
predictor.observe_output(&frame.bytes);
|
||||||
|
last_terminal_frame_at = Instant::now();
|
||||||
|
last_packet_at = Instant::now();
|
||||||
|
flush_pending_user_input(
|
||||||
|
&socket,
|
||||||
|
addr,
|
||||||
|
&cred,
|
||||||
|
&mut send_seq,
|
||||||
|
&mut predictor,
|
||||||
|
&mut pending_user_input,
|
||||||
|
&mut pending_user_input_bytes,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
if should_strip_stale_terminal_reports(
|
if should_strip_stale_terminal_reports(
|
||||||
last_packet_at.elapsed(),
|
last_packet_at.elapsed(),
|
||||||
stale_terminal_input_suppress_until,
|
stale_terminal_input_suppress_until,
|
||||||
@@ -4508,6 +4549,7 @@ async fn run_terminal(
|
|||||||
if !forward_only {
|
if !forward_only {
|
||||||
render_frame(&frame)?;
|
render_frame(&frame)?;
|
||||||
predictor.observe_output(&frame.bytes);
|
predictor.observe_output(&frame.bytes);
|
||||||
|
last_terminal_frame_at = Instant::now();
|
||||||
}
|
}
|
||||||
last_packet_at = Instant::now();
|
last_packet_at = Instant::now();
|
||||||
flush_pending_user_input(
|
flush_pending_user_input(
|
||||||
@@ -4570,6 +4612,7 @@ async fn run_terminal(
|
|||||||
if !forward_only {
|
if !forward_only {
|
||||||
render_frame(&frame)?;
|
render_frame(&frame)?;
|
||||||
predictor.observe_output(&frame.bytes);
|
predictor.observe_output(&frame.bytes);
|
||||||
|
last_terminal_frame_at = Instant::now();
|
||||||
}
|
}
|
||||||
last_packet_at = Instant::now();
|
last_packet_at = Instant::now();
|
||||||
flush_pending_user_input(
|
flush_pending_user_input(
|
||||||
@@ -4623,6 +4666,7 @@ async fn run_terminal(
|
|||||||
if !forward_only {
|
if !forward_only {
|
||||||
render_frame(&frame)?;
|
render_frame(&frame)?;
|
||||||
predictor.observe_output(&frame.bytes);
|
predictor.observe_output(&frame.bytes);
|
||||||
|
last_terminal_frame_at = Instant::now();
|
||||||
}
|
}
|
||||||
last_packet_at = Instant::now();
|
last_packet_at = Instant::now();
|
||||||
flush_pending_user_input(
|
flush_pending_user_input(
|
||||||
@@ -4648,6 +4692,7 @@ async fn run_terminal(
|
|||||||
if !forward_only {
|
if !forward_only {
|
||||||
render_frame(&frame)?;
|
render_frame(&frame)?;
|
||||||
predictor.observe_output(&frame.bytes);
|
predictor.observe_output(&frame.bytes);
|
||||||
|
last_terminal_frame_at = Instant::now();
|
||||||
flush_startup_input_if_ready(
|
flush_startup_input_if_ready(
|
||||||
&socket,
|
&socket,
|
||||||
addr,
|
addr,
|
||||||
@@ -4747,6 +4792,7 @@ async fn run_terminal(
|
|||||||
if !forward_only {
|
if !forward_only {
|
||||||
render_frame(&frame)?;
|
render_frame(&frame)?;
|
||||||
predictor.observe_output(&frame.bytes);
|
predictor.observe_output(&frame.bytes);
|
||||||
|
last_terminal_frame_at = Instant::now();
|
||||||
}
|
}
|
||||||
last_packet_at = Instant::now();
|
last_packet_at = Instant::now();
|
||||||
flush_pending_user_input(
|
flush_pending_user_input(
|
||||||
@@ -5142,6 +5188,7 @@ async fn run_terminal(
|
|||||||
if !forward_only {
|
if !forward_only {
|
||||||
render_frame(&frame)?;
|
render_frame(&frame)?;
|
||||||
predictor.observe_output(&frame.bytes);
|
predictor.observe_output(&frame.bytes);
|
||||||
|
last_terminal_frame_at = Instant::now();
|
||||||
}
|
}
|
||||||
last_packet_at = Instant::now();
|
last_packet_at = Instant::now();
|
||||||
flush_pending_user_input(
|
flush_pending_user_input(
|
||||||
@@ -5183,6 +5230,44 @@ async fn run_terminal(
|
|||||||
(&mut startup_input_hold_until, &mut startup_gate_mode),
|
(&mut startup_input_hold_until, &mut startup_gate_mode),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
let now = Instant::now();
|
||||||
|
if should_repaint_idle_alternate_screen(
|
||||||
|
predictor.alternate_screen,
|
||||||
|
last_terminal_frame_at,
|
||||||
|
last_idle_repaint_attempt_at,
|
||||||
|
now,
|
||||||
|
) {
|
||||||
|
last_idle_repaint_attempt_at = now;
|
||||||
|
if let Some(frame) = reconnect(
|
||||||
|
&socket,
|
||||||
|
&mut cred,
|
||||||
|
&mut send_seq,
|
||||||
|
last_size,
|
||||||
|
&mut frame_buffer,
|
||||||
|
&mut predictor,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
refresh_live_addr(&mut addr, &cred)?;
|
||||||
|
arm_stale_terminal_input_suppression(
|
||||||
|
&mut stale_terminal_input_suppress_until,
|
||||||
|
);
|
||||||
|
render_frame(&frame)?;
|
||||||
|
predictor.observe_output(&frame.bytes);
|
||||||
|
last_terminal_frame_at = Instant::now();
|
||||||
|
last_packet_at = Instant::now();
|
||||||
|
flush_pending_user_input(
|
||||||
|
&socket,
|
||||||
|
addr,
|
||||||
|
&cred,
|
||||||
|
&mut send_seq,
|
||||||
|
&mut predictor,
|
||||||
|
&mut pending_user_input,
|
||||||
|
&mut pending_user_input_bytes,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Refresh / clear the non-destructive disconnect status line based
|
// Refresh / clear the non-destructive disconnect status line based
|
||||||
// on how long the link has been silent (recomputed after any
|
// on how long the link has been silent (recomputed after any
|
||||||
@@ -5552,6 +5637,21 @@ fn should_strip_stale_terminal_reports(
|
|||||||
|| suppress_until.is_some_and(|deadline| Instant::now() < deadline)
|
|| suppress_until.is_some_and(|deadline| Instant::now() < deadline)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn input_contains_focus_in(bytes: &[u8]) -> bool {
|
||||||
|
contains_bytes(bytes, b"\x1b[I") || contains_bytes(bytes, b"\x9bI")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_repaint_idle_alternate_screen(
|
||||||
|
alternate_screen: bool,
|
||||||
|
last_terminal_frame_at: Instant,
|
||||||
|
last_attempt_at: Instant,
|
||||||
|
now: Instant,
|
||||||
|
) -> bool {
|
||||||
|
alternate_screen
|
||||||
|
&& now.duration_since(last_terminal_frame_at) >= ALT_SCREEN_IDLE_REPAINT_AFTER
|
||||||
|
&& now.duration_since(last_attempt_at) >= ALT_SCREEN_IDLE_REPAINT_AFTER
|
||||||
|
}
|
||||||
|
|
||||||
fn arm_stale_terminal_input_suppression(suppress_until: &mut Option<Instant>) {
|
fn arm_stale_terminal_input_suppression(suppress_until: &mut Option<Instant>) {
|
||||||
*suppress_until = Some(Instant::now() + POST_RECONNECT_STALE_INPUT_GRACE);
|
*suppress_until = Some(Instant::now() + POST_RECONNECT_STALE_INPUT_GRACE);
|
||||||
}
|
}
|
||||||
@@ -5679,19 +5779,21 @@ fn stale_mouse_report_maybe_incomplete(bytes: &[u8], offset: usize) -> bool {
|
|||||||
[0x1b] | [0x1b, b'['] | [0x1b, b'[', b'<'] => true,
|
[0x1b] | [0x1b, b'['] | [0x1b, b'[', b'<'] => true,
|
||||||
[0x1b, b'[', b'M', ..] if rest.len() < 6 => true,
|
[0x1b, b'[', b'M', ..] if rest.len() < 6 => true,
|
||||||
[0x1b, b'[', b'<', params @ ..] | [0x1b, b'[', params @ ..]
|
[0x1b, b'[', b'<', params @ ..] | [0x1b, b'[', params @ ..]
|
||||||
if params_are_mouse_prefix(params) =>
|
if params_are_mouse_prefix(params, 2) =>
|
||||||
{
|
{
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
[0x9b] | [0x9b, b'<'] => true,
|
[0x9b] | [0x9b, b'<'] => true,
|
||||||
[0x9b, b'M', ..] if rest.len() < 5 => true,
|
[0x9b, b'M', ..] if rest.len() < 5 => true,
|
||||||
[0x9b, b'<', params @ ..] | [0x9b, params @ ..] if params_are_mouse_prefix(params) => true,
|
[0x9b, b'<', params @ ..] | [0x9b, params @ ..] if params_are_mouse_prefix(params, 2) => {
|
||||||
params if params_are_mouse_prefix(params) => true,
|
true
|
||||||
|
}
|
||||||
|
params if params_are_mouse_prefix(params, 2) => true,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn params_are_mouse_prefix(params: &[u8]) -> bool {
|
fn params_are_mouse_prefix(params: &[u8], min_semicolons: usize) -> bool {
|
||||||
let mut semicolons = 0usize;
|
let mut semicolons = 0usize;
|
||||||
let mut saw_digit = false;
|
let mut saw_digit = false;
|
||||||
for byte in params {
|
for byte in params {
|
||||||
@@ -5701,7 +5803,7 @@ fn params_are_mouse_prefix(params: &[u8]) -> bool {
|
|||||||
_ => return false,
|
_ => return false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
saw_digit && semicolons >= 1
|
saw_digit && semicolons >= min_semicolons
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn flush_startup_input_if_ready(
|
async fn flush_startup_input_if_ready(
|
||||||
@@ -7228,11 +7330,12 @@ const TERMINAL_CLEANUP: &[u8] = concat!(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
CachedCredential, DisconnectStatus, DynamicForward, FRAME_GAP_RESYNC_AFTER_MS, FrameBuffer,
|
ALT_SCREEN_IDLE_REPAINT_AFTER, CachedCredential, DisconnectStatus, DynamicForward,
|
||||||
LocalForward, MAX_PENDING_USER_INPUT_BYTES, POST_SUBMIT_ALL_INPUT_HOLD, PendingStreamOpen,
|
FRAME_GAP_RESYNC_AFTER_MS, FrameBuffer, LocalForward, MAX_PENDING_USER_INPUT_BYTES,
|
||||||
PredictMode, Predictor, RESTART_STATUS_SCRIPT, RemoteForward, STARTUP_INPUT_HOLD,
|
POST_SUBMIT_ALL_INPUT_HOLD, PendingStreamOpen, PredictMode, Predictor,
|
||||||
SshConfig, StartupGateMode, StatusAction, auth_allows, cache_key, cache_server_prefix,
|
RESTART_STATUS_SCRIPT, RemoteForward, STARTUP_INPUT_HOLD, SshConfig, StartupGateMode,
|
||||||
clear_cached_credentials, ensure_tui_safe_status_overlay, input_matches_escape,
|
StatusAction, auth_allows, cache_key, cache_server_prefix, clear_cached_credentials,
|
||||||
|
ensure_tui_safe_status_overlay, input_contains_focus_in, input_matches_escape,
|
||||||
is_local_status_target, is_resume_response_for_client, latest_release_download_url,
|
is_local_status_target, is_resume_response_for_client, latest_release_download_url,
|
||||||
load_first_native_identity_with_prompt, parse_dynamic_forward, parse_escape_key,
|
load_first_native_identity_with_prompt, parse_dynamic_forward, parse_escape_key,
|
||||||
parse_local_forward, parse_remote_forward, parse_ssh_config, post_submit_hold_duration,
|
parse_local_forward, parse_remote_forward, parse_ssh_config, post_submit_hold_duration,
|
||||||
@@ -7241,10 +7344,11 @@ mod tests {
|
|||||||
render_status_clear, render_status_overlay, requested_env, resolved_startup_command,
|
render_status_clear, render_status_overlay, requested_env, resolved_startup_command,
|
||||||
retransmit_stream_opens, rewrite_forward_command, selected_predict_mode, selected_udp_host,
|
retransmit_stream_opens, rewrite_forward_command, selected_predict_mode, selected_udp_host,
|
||||||
server_version_mismatch, should_flush_terminal_input_after_contact,
|
server_version_mismatch, should_flush_terminal_input_after_contact,
|
||||||
should_hold_during_startup_gate, should_hold_post_submit_input, split_after_command_submit,
|
should_hold_during_startup_gate, should_hold_post_submit_input,
|
||||||
ssh_destination_host, ssh_username, ssh_with_user, startup_command, status_ssh_target,
|
should_repaint_idle_alternate_screen, split_after_command_submit, ssh_destination_host,
|
||||||
strip_stale_mouse_reports, toml_bare_key_or_quoted, update_check_requested,
|
ssh_username, ssh_with_user, startup_command, status_ssh_target, strip_stale_mouse_reports,
|
||||||
update_version_status, upsert_managed_block, valid_forward_host, vscode_safe_alias,
|
toml_bare_key_or_quoted, update_check_requested, update_version_status,
|
||||||
|
upsert_managed_block, valid_forward_host, vscode_safe_alias,
|
||||||
};
|
};
|
||||||
use dosh::config::{ClientConfig, CommandExtension, HostConfig};
|
use dosh::config::{ClientConfig, CommandExtension, HostConfig};
|
||||||
use dosh::native::EnvVar;
|
use dosh::native::EnvVar;
|
||||||
@@ -7253,7 +7357,7 @@ mod tests {
|
|||||||
use ed25519_dalek::{SigningKey, VerifyingKey};
|
use ed25519_dalek::{SigningKey, VerifyingKey};
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::time::Duration;
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
fn test_args(server: &str, command: &[&str]) -> super::Args {
|
fn test_args(server: &str, command: &[&str]) -> super::Args {
|
||||||
super::Args {
|
super::Args {
|
||||||
@@ -8529,12 +8633,53 @@ mod tests {
|
|||||||
assert_eq!(strip_stale_mouse_reports(b"10m"), b"");
|
assert_eq!(strip_stale_mouse_reports(b"10m"), b"");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn numeric_semicolon_input_survives_stale_filter() {
|
||||||
|
assert_eq!(strip_stale_mouse_reports(b"123;"), b"123;");
|
||||||
|
assert_eq!(strip_stale_mouse_reports(b"12;34"), b"12;34");
|
||||||
|
assert_eq!(strip_stale_mouse_reports(b"echo 1;"), b"echo 1;");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn incomplete_three_field_mouse_prefix_is_suppressed() {
|
||||||
|
assert_eq!(strip_stale_mouse_reports(b"35;152;"), b"");
|
||||||
|
assert_eq!(strip_stale_mouse_reports(b"\x1b[<35;152;"), b"");
|
||||||
|
assert_eq!(strip_stale_mouse_reports(b"\x9b35;152;"), b"");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stale_focus_reports_are_stripped_from_pending_input() {
|
fn stale_focus_reports_are_stripped_from_pending_input() {
|
||||||
assert_eq!(strip_stale_mouse_reports(b"\x1b[Ihello\x1b[O"), b"hello");
|
assert_eq!(strip_stale_mouse_reports(b"\x1b[Ihello\x1b[O"), b"hello");
|
||||||
assert_eq!(strip_stale_mouse_reports(b"\x9bIhello\x9bO"), b"hello");
|
assert_eq!(strip_stale_mouse_reports(b"\x9bIhello\x9bO"), b"hello");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn focus_in_reports_trigger_local_repaint_detection() {
|
||||||
|
assert!(input_contains_focus_in(b"\x1b[I"));
|
||||||
|
assert!(input_contains_focus_in(b"before\x9bIafter"));
|
||||||
|
assert!(!input_contains_focus_in(b"\x1b[O"));
|
||||||
|
assert!(!input_contains_focus_in(b"\x1b[A"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn idle_repaint_only_runs_for_stale_alternate_screen() {
|
||||||
|
let now = Instant::now();
|
||||||
|
let stale = now - ALT_SCREEN_IDLE_REPAINT_AFTER - Duration::from_secs(1);
|
||||||
|
let recent = now - Duration::from_secs(1);
|
||||||
|
assert!(should_repaint_idle_alternate_screen(
|
||||||
|
true, stale, stale, now
|
||||||
|
));
|
||||||
|
assert!(!should_repaint_idle_alternate_screen(
|
||||||
|
false, stale, stale, now
|
||||||
|
));
|
||||||
|
assert!(!should_repaint_idle_alternate_screen(
|
||||||
|
true, recent, stale, now
|
||||||
|
));
|
||||||
|
assert!(!should_repaint_idle_alternate_screen(
|
||||||
|
true, stale, recent, now
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn non_mouse_escape_input_survives_stale_filter() {
|
fn non_mouse_escape_input_survives_stale_filter() {
|
||||||
assert_eq!(strip_stale_mouse_reports(b"\x1b[A"), b"\x1b[A");
|
assert_eq!(strip_stale_mouse_reports(b"\x1b[A"), b"\x1b[A");
|
||||||
|
|||||||
@@ -865,6 +865,7 @@ async fn handle_packet(
|
|||||||
| PacketKind::StreamEof
|
| PacketKind::StreamEof
|
||||||
| PacketKind::StreamWindowAdjust
|
| PacketKind::StreamWindowAdjust
|
||||||
| PacketKind::RekeyAck
|
| PacketKind::RekeyAck
|
||||||
|
| PacketKind::Detach
|
||||||
if find_client_decrypt_key(state, &packet.header).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
|
send_reject_to_client(socket, peer, packet.header.conn_id, "unknown client").await
|
||||||
@@ -1734,7 +1735,14 @@ async fn handle_ping(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_detach(state: &Arc<Mutex<ServerState>>, packet: &protocol::Packet) -> Result<()> {
|
async fn handle_detach(state: &Arc<Mutex<ServerState>>, packet: &protocol::Packet) -> Result<()> {
|
||||||
|
let (key, _) = find_client_decrypt_key(state, &packet.header)?;
|
||||||
|
let _ = protocol::decrypt_body(packet, &key, CLIENT_TO_SERVER)?;
|
||||||
let mut locked = state.lock().expect("server state poisoned");
|
let mut locked = state.lock().expect("server state poisoned");
|
||||||
|
if let Some(client) = locked.client_mut(&packet.header.conn_id) {
|
||||||
|
if !client.replay.accept(packet.header.seq) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
locked.remove_client_everywhere(&packet.header.conn_id);
|
locked.remove_client_everywhere(&packet.header.conn_id);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -4105,6 +4113,63 @@ mod tests {
|
|||||||
assert!(locked.lookup_client(&client_id).is_none());
|
assert!(locked.lookup_client(&client_id).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn forged_plaintext_detach_does_not_remove_client() {
|
||||||
|
let (pty_tx, _pty_rx) = mpsc::unbounded_channel();
|
||||||
|
let mut state = ServerState::new(ServerConfig::default(), [0u8; 32], pty_tx);
|
||||||
|
state
|
||||||
|
.ensure_session("work", 80, 24, "forward-only", &[])
|
||||||
|
.unwrap();
|
||||||
|
let client_id = [21u8; 16];
|
||||||
|
let session_key = [22u8; 32];
|
||||||
|
state.insert_client("work", client_id, test_client_state(session_key));
|
||||||
|
let state = Arc::new(Mutex::new(state));
|
||||||
|
let mut packet = protocol::decode(
|
||||||
|
&protocol::encode_plain(PacketKind::Detach, client_id, 1, 0, b"").unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
packet.header.session_key_id = protocol::session_key_id(&session_key);
|
||||||
|
|
||||||
|
assert!(handle_detach(&state, &packet).await.is_err());
|
||||||
|
|
||||||
|
let locked = state.lock().unwrap();
|
||||||
|
assert!(
|
||||||
|
locked.lookup_client(&client_id).is_some(),
|
||||||
|
"plaintext detach with a copied key id must not remove a live client"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn authenticated_detach_removes_client() {
|
||||||
|
let (pty_tx, _pty_rx) = mpsc::unbounded_channel();
|
||||||
|
let mut state = ServerState::new(ServerConfig::default(), [0u8; 32], pty_tx);
|
||||||
|
state
|
||||||
|
.ensure_session("work", 80, 24, "forward-only", &[])
|
||||||
|
.unwrap();
|
||||||
|
let client_id = [23u8; 16];
|
||||||
|
let session_key = [24u8; 32];
|
||||||
|
state.insert_client("work", client_id, test_client_state(session_key));
|
||||||
|
let state = Arc::new(Mutex::new(state));
|
||||||
|
let packet = protocol::decode(
|
||||||
|
&protocol::encode_encrypted(
|
||||||
|
PacketKind::Detach,
|
||||||
|
client_id,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
&session_key,
|
||||||
|
CLIENT_TO_SERVER,
|
||||||
|
b"",
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
handle_detach(&state, &packet).await.unwrap();
|
||||||
|
|
||||||
|
let locked = state.lock().unwrap();
|
||||||
|
assert!(locked.lookup_client(&client_id).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn duplicate_stream_open_for_open_stream_resends_ok() {
|
async fn duplicate_stream_open_for_open_stream_resends_ok() {
|
||||||
let (pty_tx, _pty_rx) = mpsc::unbounded_channel();
|
let (pty_tx, _pty_rx) = mpsc::unbounded_channel();
|
||||||
|
|||||||
+1
-1
@@ -280,7 +280,7 @@ pub fn decode(input: &[u8]) -> Result<Packet> {
|
|||||||
|
|
||||||
pub fn decrypt_body(packet: &Packet, key: &[u8; 32], direction: u32) -> Result<Vec<u8>> {
|
pub fn decrypt_body(packet: &Packet, key: &[u8; 32], direction: u32) -> Result<Vec<u8>> {
|
||||||
if packet.header.flags & 1 == 0 {
|
if packet.header.flags & 1 == 0 {
|
||||||
return Ok(packet.body.clone());
|
bail!("packet body is not encrypted");
|
||||||
}
|
}
|
||||||
let nonce = crypto::nonce_from(direction, packet.header.seq);
|
let nonce = crypto::nonce_from(direction, packet.header.seq);
|
||||||
let aad = packet.header.aad();
|
let aad = packet.header.aad();
|
||||||
|
|||||||
@@ -58,6 +58,19 @@ fn encrypted_packet_round_trips() {
|
|||||||
assert_eq!(plain, b"hello");
|
assert_eq!(plain, b"hello");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plaintext_packet_never_decrypts_as_authenticated_body() {
|
||||||
|
let key = crypto::random_32();
|
||||||
|
let mut decoded = protocol::decode(
|
||||||
|
&protocol::encode_plain(PacketKind::Input, crypto::random_16(), 1, 0, b"hello").unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
decoded.header.session_key_id = protocol::session_key_id(&key);
|
||||||
|
|
||||||
|
let err = protocol::decrypt_body(&decoded, &key, CLIENT_TO_SERVER).unwrap_err();
|
||||||
|
assert!(err.to_string().contains("not encrypted"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn encrypted_packet_rejects_wrong_session_key_id_before_decrypt() {
|
fn encrypted_packet_rejects_wrong_session_key_id_before_decrypt() {
|
||||||
let key = crypto::random_32();
|
let key = crypto::random_32();
|
||||||
|
|||||||
Reference in New Issue
Block a user