Prepare Dosh 1.0.0 rc1
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
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:
Generated
+1
-1
@@ -436,7 +436,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dosh"
|
name = "dosh"
|
||||||
version = "0.1.17"
|
version = "1.0.0-rc1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "dosh"
|
name = "dosh"
|
||||||
version = "0.1.17"
|
version = "1.0.0-rc1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
|
|||||||
+157
-28
@@ -3524,6 +3524,60 @@ fn resolve_addr(host: &str, port: u16) -> Result<SocketAddr> {
|
|||||||
.ok_or_else(|| anyhow!("no UDP address resolved for {host}:{port}"))
|
.ok_or_else(|| anyhow!("no UDP address resolved for {host}:{port}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn send_terminal_udp(socket: &UdpSocket, packet: &[u8], addr: SocketAddr) -> Result<bool> {
|
||||||
|
match socket.send_to(packet, addr).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 {addr}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn try_native_auth(
|
async fn try_native_auth(
|
||||||
socket: &UdpSocket,
|
socket: &UdpSocket,
|
||||||
@@ -4337,7 +4391,13 @@ async fn run_terminal(
|
|||||||
&& cred.mode != "view-only"
|
&& cred.mode != "view-only"
|
||||||
&& !forward_only
|
&& !forward_only
|
||||||
{
|
{
|
||||||
send_input(&socket, addr, &cred, &mut send_seq, bytes).await?;
|
if !send_input(&socket, addr, &cred, &mut send_seq, bytes.clone()).await? {
|
||||||
|
queue_stale_pending_user_input(
|
||||||
|
&mut pending_user_input,
|
||||||
|
&mut pending_user_input_bytes,
|
||||||
|
bytes,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
startup_input_hold_until = Some(Instant::now() + STARTUP_INPUT_HOLD);
|
startup_input_hold_until = Some(Instant::now() + STARTUP_INPUT_HOLD);
|
||||||
startup_gate_mode = StartupGateMode::HoldAll;
|
startup_gate_mode = StartupGateMode::HoldAll;
|
||||||
}
|
}
|
||||||
@@ -4411,8 +4471,15 @@ async fn run_terminal(
|
|||||||
&& let Some((send_now, mut hold_for_later)) =
|
&& let Some((send_now, mut hold_for_later)) =
|
||||||
split_after_command_submit(&bytes)
|
split_after_command_submit(&bytes)
|
||||||
{
|
{
|
||||||
|
if send_input(&socket, addr, &cred, &mut send_seq, send_now.clone()).await? {
|
||||||
predictor.observe_input(&send_now)?;
|
predictor.observe_input(&send_now)?;
|
||||||
send_input(&socket, addr, &cred, &mut send_seq, send_now).await?;
|
} else {
|
||||||
|
queue_stale_pending_user_input(
|
||||||
|
&mut pending_user_input,
|
||||||
|
&mut pending_user_input_bytes,
|
||||||
|
send_now,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
startup_input_hold_until = Some(
|
startup_input_hold_until = Some(
|
||||||
Instant::now()
|
Instant::now()
|
||||||
+ post_submit_hold_duration(&hold_for_later),
|
+ post_submit_hold_duration(&hold_for_later),
|
||||||
@@ -4431,15 +4498,23 @@ async fn run_terminal(
|
|||||||
)?;
|
)?;
|
||||||
continue;
|
continue;
|
||||||
} else if !hold_for_later.is_empty() {
|
} else if !hold_for_later.is_empty() {
|
||||||
predictor.observe_input(&hold_for_later)?;
|
let sent = send_input(
|
||||||
send_input(
|
|
||||||
&socket,
|
&socket,
|
||||||
addr,
|
addr,
|
||||||
&cred,
|
&cred,
|
||||||
&mut send_seq,
|
&mut send_seq,
|
||||||
std::mem::take(&mut hold_for_later),
|
hold_for_later.clone(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
if sent {
|
||||||
|
predictor.observe_input(&hold_for_later)?;
|
||||||
|
} else {
|
||||||
|
queue_stale_pending_user_input(
|
||||||
|
&mut pending_user_input,
|
||||||
|
&mut pending_user_input_bytes,
|
||||||
|
std::mem::take(&mut hold_for_later),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -4477,8 +4552,15 @@ async fn run_terminal(
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if send_input(&socket, addr, &cred, &mut send_seq, bytes.clone()).await? {
|
||||||
predictor.observe_input(&bytes)?;
|
predictor.observe_input(&bytes)?;
|
||||||
send_input(&socket, addr, &cred, &mut send_seq, bytes).await?;
|
} else {
|
||||||
|
queue_stale_pending_user_input(
|
||||||
|
&mut pending_user_input,
|
||||||
|
&mut pending_user_input_bytes,
|
||||||
|
bytes,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4667,7 +4749,7 @@ async fn run_terminal(
|
|||||||
CLIENT_TO_SERVER,
|
CLIENT_TO_SERVER,
|
||||||
b"",
|
b"",
|
||||||
)?;
|
)?;
|
||||||
socket.send_to(&ack, addr).await?;
|
let _ = send_terminal_udp(&socket, &ack, addr).await?;
|
||||||
}
|
}
|
||||||
PacketKind::AttachReject => {
|
PacketKind::AttachReject => {
|
||||||
let reject: AttachReject = protocol::from_body(&packet.body)?;
|
let reject: AttachReject = protocol::from_body(&packet.body)?;
|
||||||
@@ -5102,7 +5184,7 @@ async fn run_terminal(
|
|||||||
b"",
|
b"",
|
||||||
)?;
|
)?;
|
||||||
send_seq += 1;
|
send_seq += 1;
|
||||||
socket.send_to(&packet, addr).await?;
|
let _ = send_terminal_udp(&socket, &packet, addr).await?;
|
||||||
}
|
}
|
||||||
// Re-evaluate the prediction display policy on every timer tick so
|
// Re-evaluate the prediction display policy on every timer tick so
|
||||||
// a latency spike (or recovery) flips speculation on/off promptly
|
// a latency spike (or recovery) flips speculation on/off promptly
|
||||||
@@ -5416,6 +5498,23 @@ fn queue_pending_user_input_with_filter(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn requeue_pending_user_input_front(
|
||||||
|
pending: &mut VecDeque<PendingUserInput>,
|
||||||
|
pending_bytes: &mut usize,
|
||||||
|
input: PendingUserInput,
|
||||||
|
) -> Result<()> {
|
||||||
|
let next = pending_bytes
|
||||||
|
.checked_add(input.bytes.len())
|
||||||
|
.ok_or_else(|| anyhow!("pending input buffer overflow"))?;
|
||||||
|
anyhow::ensure!(
|
||||||
|
next <= MAX_PENDING_USER_INPUT_BYTES,
|
||||||
|
"pending input buffer full while disconnected"
|
||||||
|
);
|
||||||
|
*pending_bytes = next;
|
||||||
|
pending.push_front(input);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
struct PendingUserInput {
|
struct PendingUserInput {
|
||||||
bytes: Vec<u8>,
|
bytes: Vec<u8>,
|
||||||
@@ -5486,8 +5585,19 @@ async fn flush_pending_user_input(
|
|||||||
if bytes.is_empty() {
|
if bytes.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if send_input(socket, addr, cred, send_seq, bytes.clone()).await? {
|
||||||
predictor.observe_input(&bytes)?;
|
predictor.observe_input(&bytes)?;
|
||||||
send_input(socket, addr, cred, send_seq, bytes).await?;
|
} else {
|
||||||
|
requeue_pending_user_input_front(
|
||||||
|
pending,
|
||||||
|
pending_bytes,
|
||||||
|
PendingUserInput {
|
||||||
|
bytes,
|
||||||
|
strip_mouse_reports: false,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -5619,7 +5729,7 @@ async fn send_input(
|
|||||||
cred: &CachedCredential,
|
cred: &CachedCredential,
|
||||||
send_seq: &mut u64,
|
send_seq: &mut u64,
|
||||||
bytes: Vec<u8>,
|
bytes: Vec<u8>,
|
||||||
) -> Result<()> {
|
) -> Result<bool> {
|
||||||
let body = protocol::to_body(&Input { bytes })?;
|
let body = protocol::to_body(&Input { bytes })?;
|
||||||
let packet = protocol::encode_encrypted(
|
let packet = protocol::encode_encrypted(
|
||||||
PacketKind::Input,
|
PacketKind::Input,
|
||||||
@@ -5631,8 +5741,7 @@ async fn send_input(
|
|||||||
&body,
|
&body,
|
||||||
)?;
|
)?;
|
||||||
*send_seq += 1;
|
*send_seq += 1;
|
||||||
socket.send_to(&packet, addr).await?;
|
send_terminal_udp(socket, &packet, addr).await
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send_stream_open(
|
async fn send_stream_open(
|
||||||
@@ -5975,7 +6084,7 @@ async fn send_stream_packet(
|
|||||||
body,
|
body,
|
||||||
)?;
|
)?;
|
||||||
*send_seq += 1;
|
*send_seq += 1;
|
||||||
socket.send_to(&packet, addr).await?;
|
let _ = send_terminal_udp(socket, &packet, addr).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6739,7 +6848,7 @@ async fn send_resize(
|
|||||||
&body,
|
&body,
|
||||||
)?;
|
)?;
|
||||||
*send_seq += 1;
|
*send_seq += 1;
|
||||||
socket.send_to(&packet, addr).await?;
|
let _ = send_terminal_udp(socket, &packet, addr).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6759,7 +6868,7 @@ async fn send_ack(
|
|||||||
b"",
|
b"",
|
||||||
)?;
|
)?;
|
||||||
*send_seq += 1;
|
*send_seq += 1;
|
||||||
socket.send_to(&packet, addr).await?;
|
let _ = send_terminal_udp(socket, &packet, addr).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6774,7 +6883,7 @@ async fn detach_once(socket: &UdpSocket, cred: &CachedCredential, seq: u64) -> R
|
|||||||
CLIENT_TO_SERVER,
|
CLIENT_TO_SERVER,
|
||||||
b"",
|
b"",
|
||||||
)?;
|
)?;
|
||||||
socket.send_to(&packet, addr).await?;
|
let _ = send_terminal_udp(socket, &packet, addr).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7112,18 +7221,19 @@ mod tests {
|
|||||||
PredictMode, Predictor, RESTART_STATUS_SCRIPT, RemoteForward, STARTUP_INPUT_HOLD,
|
PredictMode, Predictor, RESTART_STATUS_SCRIPT, RemoteForward, STARTUP_INPUT_HOLD,
|
||||||
SshConfig, StartupGateMode, StatusAction, auth_allows, cache_key, cache_server_prefix,
|
SshConfig, StartupGateMode, StatusAction, auth_allows, cache_key, cache_server_prefix,
|
||||||
clear_cached_credentials, ensure_tui_safe_status_overlay, input_matches_escape,
|
clear_cached_credentials, ensure_tui_safe_status_overlay, 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, is_transient_udp_os_error,
|
||||||
load_first_native_identity_with_prompt, parse_dynamic_forward, parse_escape_key,
|
latest_release_download_url, load_first_native_identity_with_prompt, parse_dynamic_forward,
|
||||||
parse_local_forward, parse_remote_forward, parse_ssh_config, post_submit_hold_duration,
|
parse_escape_key, parse_local_forward, parse_remote_forward, parse_ssh_config,
|
||||||
queue_pending_user_input, raw_contains_host_table, recv_response_until, refresh_live_addr,
|
post_submit_hold_duration, queue_pending_user_input, raw_contains_host_table,
|
||||||
release_tag_download_url, release_tag_from_effective_url, release_version_from_tag,
|
recv_response_until, refresh_live_addr, release_tag_download_url,
|
||||||
render_status_clear, render_status_overlay, requested_env, resolved_startup_command,
|
release_tag_from_effective_url, release_version_from_tag, render_status_clear,
|
||||||
retransmit_stream_opens, rewrite_forward_command, selected_predict_mode, selected_udp_host,
|
render_status_overlay, requested_env, resolved_startup_command, retransmit_stream_opens,
|
||||||
server_version_mismatch, should_flush_terminal_input_after_contact,
|
rewrite_forward_command, selected_predict_mode, selected_udp_host, server_version_mismatch,
|
||||||
should_hold_during_startup_gate, should_hold_post_submit_input, split_after_command_submit,
|
should_flush_terminal_input_after_contact, should_hold_during_startup_gate,
|
||||||
ssh_destination_host, ssh_username, ssh_with_user, startup_command, status_ssh_target,
|
should_hold_post_submit_input, 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;
|
||||||
@@ -8353,6 +8463,25 @@ mod tests {
|
|||||||
assert_eq!(pending_bytes, MAX_PENDING_USER_INPUT_BYTES);
|
assert_eq!(pending_bytes, MAX_PENDING_USER_INPUT_BYTES);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transient_udp_send_errors_are_not_terminal_fatal() {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
assert!(is_transient_udp_os_error(libc::ENETUNREACH));
|
||||||
|
assert!(is_transient_udp_os_error(libc::EHOSTUNREACH));
|
||||||
|
assert!(is_transient_udp_os_error(libc::EADDRNOTAVAIL));
|
||||||
|
assert!(!is_transient_udp_os_error(libc::EINVAL));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
assert!(is_transient_udp_os_error(10051)); // WSAENETUNREACH
|
||||||
|
assert!(is_transient_udp_os_error(10065)); // WSAEHOSTUNREACH
|
||||||
|
assert!(is_transient_udp_os_error(10049)); // WSAEADDRNOTAVAIL
|
||||||
|
assert!(!is_transient_udp_os_error(10022)); // WSAEINVAL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stale_mouse_reports_are_stripped_from_pending_input() {
|
fn stale_mouse_reports_are_stripped_from_pending_input() {
|
||||||
let input = b"\x1b[<35;152;1Mhello\x1b[<0;107;10m";
|
let input = b"\x1b[<35;152;1Mhello\x1b[<0;107;10m";
|
||||||
|
|||||||
@@ -886,13 +886,13 @@ fn bad_network_tui_work_soak_30m() {
|
|||||||
let mut seq = 2u64;
|
let mut seq = 2u64;
|
||||||
let mut iteration = 0u64;
|
let mut iteration = 0u64;
|
||||||
while Instant::now() < deadline {
|
while Instant::now() < deadline {
|
||||||
if iteration % 2 == 0 {
|
if iteration.is_multiple_of(2) {
|
||||||
relay.arm_reorder();
|
relay.arm_reorder();
|
||||||
}
|
}
|
||||||
if iteration > 0 && iteration % 7 == 0 {
|
if iteration > 0 && iteration.is_multiple_of(7) {
|
||||||
let _ = relay.rebind_upstream();
|
let _ = relay.rebind_upstream();
|
||||||
}
|
}
|
||||||
if iteration > 0 && iteration % 11 == 0 {
|
if iteration > 0 && iteration.is_multiple_of(11) {
|
||||||
relay.set_drop_s2c(100);
|
relay.set_drop_s2c(100);
|
||||||
thread::sleep(Duration::from_millis(750));
|
thread::sleep(Duration::from_millis(750));
|
||||||
relay.set_drop_s2c(20);
|
relay.set_drop_s2c(20);
|
||||||
|
|||||||
Reference in New Issue
Block a user