Compare commits
4
Commits
5c54601cdf
...
v1.0.0-rc2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
689d20f7e7 | ||
|
|
252f081a61 | ||
|
|
b1e8326b0a | ||
|
|
3b4931aa8f |
Generated
+1
-1
@@ -436,7 +436,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dosh"
|
||||
version = "1.0.0-rc1"
|
||||
version = "1.0.0-rc2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "dosh"
|
||||
version = "1.0.0-rc1"
|
||||
version = "1.0.0-rc2"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ cleanup() {
|
||||
kill "$server_pid" 2>/dev/null || true
|
||||
wait "$server_pid" 2>/dev/null || true
|
||||
fi
|
||||
pkill -f "$server_bin hold --runtime-dir $home/sessions/run" 2>/dev/null || true
|
||||
rm -rf "$home"
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
@@ -86,6 +87,7 @@ scrollback = 5000
|
||||
auth_ttl_secs = 30
|
||||
attach_ticket_ttl_secs = 3600
|
||||
allow_attach_tickets = true
|
||||
persist_sessions = false
|
||||
client_timeout_secs = 60
|
||||
retransmit_window = 256
|
||||
default_input_mode = "read-write"
|
||||
|
||||
+71
-2
@@ -63,6 +63,8 @@ const STREAM_INITIAL_WINDOW: usize = 1024 * 1024;
|
||||
const MAX_PENDING_USER_INPUT_BYTES: usize = 1024 * 1024;
|
||||
const STARTUP_INPUT_HOLD: Duration = Duration::from_millis(750);
|
||||
const POST_SUBMIT_ALL_INPUT_HOLD: Duration = Duration::from_millis(120);
|
||||
const STALE_TERMINAL_INPUT_AFTER: Duration = Duration::from_secs(2);
|
||||
const POST_RECONNECT_STALE_INPUT_GRACE: Duration = Duration::from_secs(2);
|
||||
|
||||
/// Sentinel `target_host` the server uses on a server-initiated `StreamOpen` that
|
||||
/// represents an SSH-agent connection (rather than a TCP target). The client
|
||||
@@ -4376,6 +4378,7 @@ async fn run_terminal(
|
||||
let mut pending_user_input_bytes = 0usize;
|
||||
let mut startup_input_hold_until: Option<Instant> = None;
|
||||
let mut startup_gate_mode = StartupGateMode::HoldControl;
|
||||
let mut stale_terminal_input_suppress_until: Option<Instant> = None;
|
||||
if let Some(frame) = first_frame {
|
||||
if !forward_only {
|
||||
render_frame(&frame)?;
|
||||
@@ -4433,10 +4436,19 @@ async fn run_terminal(
|
||||
biased;
|
||||
stdin_msg = stdin_rx.recv() => {
|
||||
match stdin_msg {
|
||||
Some(bytes) => {
|
||||
Some(mut bytes) => {
|
||||
if input_matches_escape(&bytes, escape_key.as_deref()) {
|
||||
break;
|
||||
}
|
||||
if should_strip_stale_terminal_reports(
|
||||
last_packet_at.elapsed(),
|
||||
stale_terminal_input_suppress_until,
|
||||
) {
|
||||
bytes = strip_stale_mouse_reports(&bytes);
|
||||
if bytes.is_empty() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if cred.mode != "view-only" && cred.mode != "forward-only" {
|
||||
if let Some(deadline) = startup_input_hold_until {
|
||||
if !predictor.alternate_screen
|
||||
@@ -4535,6 +4547,9 @@ async fn run_terminal(
|
||||
.await?
|
||||
{
|
||||
refresh_live_addr(&mut addr, &cred)?;
|
||||
arm_stale_terminal_input_suppression(
|
||||
&mut stale_terminal_input_suppress_until,
|
||||
);
|
||||
if !forward_only {
|
||||
render_frame(&frame)?;
|
||||
predictor.observe_output(&frame.bytes);
|
||||
@@ -4594,6 +4609,9 @@ async fn run_terminal(
|
||||
.await?
|
||||
{
|
||||
refresh_live_addr(&mut addr, &cred)?;
|
||||
arm_stale_terminal_input_suppression(
|
||||
&mut stale_terminal_input_suppress_until,
|
||||
);
|
||||
if !forward_only {
|
||||
render_frame(&frame)?;
|
||||
predictor.observe_output(&frame.bytes);
|
||||
@@ -4644,6 +4662,9 @@ async fn run_terminal(
|
||||
.await?
|
||||
{
|
||||
refresh_live_addr(&mut addr, &cred)?;
|
||||
arm_stale_terminal_input_suppression(
|
||||
&mut stale_terminal_input_suppress_until,
|
||||
);
|
||||
if !forward_only {
|
||||
render_frame(&frame)?;
|
||||
predictor.observe_output(&frame.bytes);
|
||||
@@ -4765,6 +4786,9 @@ async fn run_terminal(
|
||||
.await?
|
||||
{
|
||||
refresh_live_addr(&mut addr, &cred)?;
|
||||
arm_stale_terminal_input_suppression(
|
||||
&mut stale_terminal_input_suppress_until,
|
||||
);
|
||||
if !forward_only {
|
||||
render_frame(&frame)?;
|
||||
predictor.observe_output(&frame.bytes);
|
||||
@@ -5157,6 +5181,9 @@ async fn run_terminal(
|
||||
.await?
|
||||
{
|
||||
refresh_live_addr(&mut addr, &cred)?;
|
||||
arm_stale_terminal_input_suppression(
|
||||
&mut stale_terminal_input_suppress_until,
|
||||
);
|
||||
if !forward_only {
|
||||
render_frame(&frame)?;
|
||||
predictor.observe_output(&frame.bytes);
|
||||
@@ -5562,6 +5589,18 @@ fn should_flush_terminal_input_after_contact(
|
||||
!forward_only && !pending_empty && mode != "view-only" && mode != "forward-only"
|
||||
}
|
||||
|
||||
fn should_strip_stale_terminal_reports(
|
||||
silent_for: Duration,
|
||||
suppress_until: Option<Instant>,
|
||||
) -> bool {
|
||||
silent_for >= STALE_TERMINAL_INPUT_AFTER
|
||||
|| suppress_until.is_some_and(|deadline| Instant::now() < deadline)
|
||||
}
|
||||
|
||||
fn arm_stale_terminal_input_suppression(suppress_until: &mut Option<Instant>) {
|
||||
*suppress_until = Some(Instant::now() + POST_RECONNECT_STALE_INPUT_GRACE);
|
||||
}
|
||||
|
||||
async fn flush_pending_user_input(
|
||||
socket: &UdpSocket,
|
||||
addr: SocketAddr,
|
||||
@@ -5623,6 +5662,7 @@ fn stale_mouse_report_len(bytes: &[u8], offset: usize) -> Option<usize> {
|
||||
match bytes.get(offset).copied()? {
|
||||
0x1b if bytes.get(offset + 1) == Some(&b'[') => match bytes.get(offset + 2).copied() {
|
||||
Some(b'M') if offset + 6 <= bytes.len() => Some(6),
|
||||
Some(b'I' | b'O') => Some(3),
|
||||
Some(b'<') => mouse_report_params_len(bytes, offset + 3, 2).map(|len| len + 3),
|
||||
Some(byte) if byte.is_ascii_digit() => {
|
||||
mouse_report_params_len(bytes, offset + 2, 2).map(|len| len + 2)
|
||||
@@ -5631,13 +5671,15 @@ fn stale_mouse_report_len(bytes: &[u8], offset: usize) -> Option<usize> {
|
||||
},
|
||||
0x9b => match bytes.get(offset + 1).copied() {
|
||||
Some(b'M') if offset + 5 <= bytes.len() => Some(5),
|
||||
Some(b'I' | b'O') => Some(2),
|
||||
Some(b'<') => mouse_report_params_len(bytes, offset + 2, 2).map(|len| len + 2),
|
||||
Some(byte) if byte.is_ascii_digit() => {
|
||||
mouse_report_params_len(bytes, offset + 1, 2).map(|len| len + 1)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
byte if byte.is_ascii_digit() => mouse_report_params_len(bytes, offset, 1),
|
||||
byte if byte.is_ascii_digit() => mouse_report_params_len(bytes, offset, 1)
|
||||
.or_else(|| orphan_mouse_suffix_len(bytes, offset)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -5659,6 +5701,21 @@ fn mouse_report_params_len(bytes: &[u8], offset: usize, min_semicolons: usize) -
|
||||
None
|
||||
}
|
||||
|
||||
fn orphan_mouse_suffix_len(bytes: &[u8], offset: usize) -> Option<usize> {
|
||||
if offset != 0 {
|
||||
return None;
|
||||
}
|
||||
let mut cursor = offset;
|
||||
while let Some(byte) = bytes.get(cursor).copied() {
|
||||
match byte {
|
||||
b'0'..=b'9' => cursor += 1,
|
||||
b'M' | b'm' if cursor > offset => return Some(cursor + 1 - offset),
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn stale_mouse_report_maybe_incomplete(bytes: &[u8], offset: usize) -> bool {
|
||||
let Some(rest) = bytes.get(offset..) else {
|
||||
return false;
|
||||
@@ -8495,6 +8552,18 @@ mod tests {
|
||||
assert_eq!(strip_stale_mouse_reports(b"152;1Mls\r"), b"ls\r");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_stale_mouse_suffixes_are_stripped_from_pending_input() {
|
||||
assert_eq!(strip_stale_mouse_reports(b"1M35;149;1Mls\r"), b"ls\r");
|
||||
assert_eq!(strip_stale_mouse_reports(b"10m"), b"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
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"\x9bIhello\x9bO"), b"hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_mouse_escape_input_survives_stale_filter() {
|
||||
assert_eq!(strip_stale_mouse_reports(b"\x1b[A"), b"\x1b[A");
|
||||
|
||||
+91
-16
@@ -980,7 +980,7 @@ async fn handle_native_client_hello(
|
||||
};
|
||||
let body = protocol::to_body(&NativeServerHelloBody { hello })?;
|
||||
let out = protocol::encode_plain(PacketKind::NativeServerHello, pending_id, 1, 0, &body)?;
|
||||
socket.send_to(&out, peer).await?;
|
||||
let _ = send_udp(socket, &out, peer).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1058,7 +1058,7 @@ async fn handle_native_user_auth(
|
||||
SERVER_TO_CLIENT,
|
||||
&body,
|
||||
)?;
|
||||
socket.send_to(&out, peer).await?;
|
||||
let _ = send_udp(socket, &out, peer).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -1251,7 +1251,7 @@ async fn handle_native_user_auth(
|
||||
SERVER_TO_CLIENT,
|
||||
&body,
|
||||
)?;
|
||||
socket.send_to(&out, peer).await?;
|
||||
let _ = send_udp(socket, &out, peer).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1359,7 +1359,7 @@ async fn handle_bootstrap_attach(
|
||||
SERVER_TO_CLIENT,
|
||||
&body,
|
||||
)?;
|
||||
socket.send_to(&out, peer).await?;
|
||||
let _ = send_udp(socket, &out, peer).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1494,7 +1494,7 @@ async fn handle_ticket_attach(
|
||||
};
|
||||
let body = protocol::to_body(&envelope)?;
|
||||
let out = protocol::encode_plain(PacketKind::AttachOk, client_id, 1, 0, &body)?;
|
||||
socket.send_to(&out, peer).await?;
|
||||
let _ = send_udp(socket, &out, peer).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1512,7 +1512,7 @@ async fn send_reject_to_client(
|
||||
reason: reason.to_string(),
|
||||
})?;
|
||||
let out = protocol::encode_plain(PacketKind::AttachReject, client_id, 0, 0, &body)?;
|
||||
socket.send_to(&out, peer).await?;
|
||||
let _ = send_udp(socket, &out, peer).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1578,7 +1578,7 @@ async fn handle_resume(
|
||||
SERVER_TO_CLIENT,
|
||||
&body,
|
||||
)?;
|
||||
socket.send_to(&out, peer).await?;
|
||||
let _ = send_udp(socket, &out, peer).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1690,7 +1690,7 @@ async fn handle_ping(
|
||||
SERVER_TO_CLIENT,
|
||||
b"",
|
||||
)?;
|
||||
socket.send_to(&out, peer).await?;
|
||||
let _ = send_udp(socket, &out, peer).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1705,6 +1705,60 @@ fn remove_client(state: &Arc<Mutex<ServerState>>, client_id: [u8; 16]) {
|
||||
locked.remove_client_everywhere(&client_id);
|
||||
}
|
||||
|
||||
async fn send_udp(socket: &UdpSocket, packet: &[u8], peer: SocketAddr) -> Result<bool> {
|
||||
match socket.send_to(packet, peer).await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(err) if is_transient_udp_send_error(&err) => Ok(false),
|
||||
Err(err) => Err(err).with_context(|| format!("send UDP packet to {peer}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_transient_udp_send_error(err: &std::io::Error) -> bool {
|
||||
matches!(
|
||||
err.kind(),
|
||||
std::io::ErrorKind::Interrupted
|
||||
| std::io::ErrorKind::TimedOut
|
||||
| std::io::ErrorKind::WouldBlock
|
||||
) || err.raw_os_error().is_some_and(is_transient_udp_os_error)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn is_transient_udp_os_error(code: i32) -> bool {
|
||||
matches!(
|
||||
code,
|
||||
libc::EADDRNOTAVAIL
|
||||
| libc::ECONNREFUSED
|
||||
| libc::ECONNRESET
|
||||
| libc::EHOSTDOWN
|
||||
| libc::EHOSTUNREACH
|
||||
| libc::ENETDOWN
|
||||
| libc::ENETRESET
|
||||
| libc::ENETUNREACH
|
||||
| libc::ETIMEDOUT
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn is_transient_udp_os_error(code: i32) -> bool {
|
||||
matches!(
|
||||
code,
|
||||
10049 // WSAEADDRNOTAVAIL
|
||||
| 10050 // WSAENETDOWN
|
||||
| 10051 // WSAENETUNREACH
|
||||
| 10052 // WSAENETRESET
|
||||
| 10054 // WSAECONNRESET
|
||||
| 10060 // WSAETIMEDOUT
|
||||
| 10061 // WSAECONNREFUSED
|
||||
| 10064 // WSAEHOSTDOWN
|
||||
| 10065 // WSAEHOSTUNREACH
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
fn is_transient_udp_os_error(_code: i32) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn handle_ack(
|
||||
state: &Arc<Mutex<ServerState>>,
|
||||
peer: SocketAddr,
|
||||
@@ -3020,7 +3074,7 @@ async fn send_stream_open_to_client(
|
||||
found
|
||||
};
|
||||
if let Some((endpoint, packet)) = send {
|
||||
socket.send_to(&packet, endpoint).await?;
|
||||
let _ = send_udp(socket, &packet, endpoint).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -3114,7 +3168,7 @@ async fn queue_or_send_stream_data_to_client(
|
||||
send
|
||||
};
|
||||
if let Some((endpoint, packet)) = send {
|
||||
socket.send_to(&packet, endpoint).await?;
|
||||
let _ = send_udp(socket, &packet, endpoint).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -3189,7 +3243,7 @@ async fn flush_stream_pending_data_to_client(
|
||||
let Some((endpoint, packet)) = send else {
|
||||
return Ok(());
|
||||
};
|
||||
socket.send_to(&packet, endpoint).await?;
|
||||
let _ = send_udp(socket, &packet, endpoint).await?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3333,7 +3387,7 @@ async fn send_stream_packet_to_client(
|
||||
found
|
||||
};
|
||||
if let Some((endpoint, packet)) = send {
|
||||
socket.send_to(&packet, endpoint).await?;
|
||||
let _ = send_udp(socket, &packet, endpoint).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -3435,7 +3489,7 @@ async fn broadcast_output(
|
||||
}
|
||||
}
|
||||
for (endpoint, packet) in sends {
|
||||
socket.send_to(&packet, endpoint).await?;
|
||||
let _ = send_udp(socket, &packet, endpoint).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -3488,7 +3542,7 @@ async fn broadcast_session_exit(
|
||||
persist::remove_runtime_dir(&sessions_dir, &session_name);
|
||||
}
|
||||
for (endpoint, packet) in sends {
|
||||
socket.send_to(&packet, endpoint).await?;
|
||||
let _ = send_udp(socket, &packet, endpoint).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -3595,7 +3649,7 @@ async fn retransmit_pending(
|
||||
sends
|
||||
};
|
||||
for (endpoint, packet) in sends {
|
||||
socket.send_to(&packet, endpoint).await?;
|
||||
let _ = send_udp(socket, &packet, endpoint).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -3691,7 +3745,7 @@ async fn maybe_rekey_clients(
|
||||
sends
|
||||
};
|
||||
for (endpoint, packet) in sends {
|
||||
socket.send_to(&packet, endpoint).await?;
|
||||
let _ = send_udp(socket, &packet, endpoint).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -4536,4 +4590,25 @@ mod tests {
|
||||
};
|
||||
remote_bind_allowed(&config, "0.0.0.0").unwrap();
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn server_udp_send_classifier_treats_network_roaming_errors_as_transient() {
|
||||
for code in [
|
||||
libc::EADDRNOTAVAIL,
|
||||
libc::ECONNREFUSED,
|
||||
libc::ECONNRESET,
|
||||
libc::EHOSTDOWN,
|
||||
libc::EHOSTUNREACH,
|
||||
libc::ENETDOWN,
|
||||
libc::ENETRESET,
|
||||
libc::ENETUNREACH,
|
||||
libc::ETIMEDOUT,
|
||||
] {
|
||||
assert!(
|
||||
is_transient_udp_send_error(&std::io::Error::from_raw_os_error(code)),
|
||||
"expected errno {code} to be transient"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user