Harden embedded transport under UDP churn
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-07-05 09:37:44 -04:00
parent 689d20f7e7
commit 4b2e9a62d5
5 changed files with 178 additions and 119 deletions
+38 -67
View File
@@ -36,6 +36,7 @@ use dosh::transport::{
DoshTransport, SessionEvent, SessionRole, SessionTransportConfig, TransportConfig,
TransportEvent,
};
use dosh::udp::is_transient_udp_send_error;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::BTreeMap;
@@ -3534,52 +3535,6 @@ async fn send_terminal_udp(socket: &UdpSocket, packet: &[u8], addr: SocketAddr)
}
}
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)]
async fn try_native_auth(
socket: &UdpSocket,
@@ -7278,23 +7233,23 @@ mod tests {
PredictMode, Predictor, RESTART_STATUS_SCRIPT, RemoteForward, STARTUP_INPUT_HOLD,
SshConfig, StartupGateMode, StatusAction, auth_allows, cache_key, cache_server_prefix,
clear_cached_credentials, ensure_tui_safe_status_overlay, input_matches_escape,
is_local_status_target, is_resume_response_for_client, is_transient_udp_os_error,
latest_release_download_url, 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, queue_pending_user_input, raw_contains_host_table,
recv_response_until, refresh_live_addr, release_tag_download_url,
release_tag_from_effective_url, release_version_from_tag, render_status_clear,
render_status_overlay, requested_env, resolved_startup_command, retransmit_stream_opens,
rewrite_forward_command, selected_predict_mode, selected_udp_host, server_version_mismatch,
should_flush_terminal_input_after_contact, should_hold_during_startup_gate,
should_hold_post_submit_input, split_after_command_submit, ssh_destination_host,
ssh_username, ssh_with_user, startup_command, status_ssh_target, strip_stale_mouse_reports,
toml_bare_key_or_quoted, update_check_requested, update_version_status,
upsert_managed_block, valid_forward_host, vscode_safe_alias,
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,
parse_local_forward, parse_remote_forward, parse_ssh_config, post_submit_hold_duration,
queue_pending_user_input, raw_contains_host_table, recv_response_until, refresh_live_addr,
release_tag_download_url, release_tag_from_effective_url, release_version_from_tag,
render_status_clear, render_status_overlay, requested_env, resolved_startup_command,
retransmit_stream_opens, rewrite_forward_command, selected_predict_mode, selected_udp_host,
server_version_mismatch, should_flush_terminal_input_after_contact,
should_hold_during_startup_gate, should_hold_post_submit_input, split_after_command_submit,
ssh_destination_host, ssh_username, ssh_with_user, startup_command, status_ssh_target,
strip_stale_mouse_reports, 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::native::EnvVar;
use dosh::protocol::{self, Frame, PacketKind};
use dosh::udp::is_transient_udp_send_error;
use ed25519_dalek::{SigningKey, VerifyingKey};
use std::collections::{HashMap, VecDeque};
use std::fs;
@@ -8524,18 +8479,34 @@ mod tests {
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));
assert!(is_transient_udp_send_error(
&std::io::Error::from_raw_os_error(libc::ENETUNREACH)
));
assert!(is_transient_udp_send_error(
&std::io::Error::from_raw_os_error(libc::EHOSTUNREACH)
));
assert!(is_transient_udp_send_error(
&std::io::Error::from_raw_os_error(libc::EADDRNOTAVAIL)
));
assert!(!is_transient_udp_send_error(
&std::io::Error::from_raw_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
assert!(is_transient_udp_send_error(
&std::io::Error::from_raw_os_error(10051)
)); // WSAENETUNREACH
assert!(is_transient_udp_send_error(
&std::io::Error::from_raw_os_error(10065)
)); // WSAEHOSTUNREACH
assert!(is_transient_udp_send_error(
&std::io::Error::from_raw_os_error(10049)
)); // WSAEADDRNOTAVAIL
assert!(!is_transient_udp_send_error(
&std::io::Error::from_raw_os_error(10022)
)); // WSAEINVAL
}
}
+1 -46
View File
@@ -28,6 +28,7 @@ use dosh::protocol::{
TicketAttachBody, TicketAttachEnvelope, TicketAttachOkEnvelope,
};
use dosh::pty::{PtyHandle, PtyOutput, adopt_pty_from_fd, spawn_pty_session};
use dosh::udp::is_transient_udp_send_error;
use sha2::{Digest, Sha256};
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
use std::fs;
@@ -1713,52 +1714,6 @@ async fn send_udp(socket: &UdpSocket, packet: &[u8], peer: SocketAddr) -> Result
}
}
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,