From 48e3de29225754f89b0d8578d874dab000c78993 Mon Sep 17 00:00:00 2001 From: DuProcess <273172371+DuProcess@users.noreply.github.com> Date: Sat, 27 Jun 2026 20:11:41 -0400 Subject: [PATCH] Refresh reconnect send address and guard prebuilts --- Cargo.lock | 2 +- Cargo.toml | 2 +- install.ps1 | 26 +++++++++++++++++++ install.sh | 35 +++++++++++++++++++++++++ src/bin/dosh-client.rs | 59 ++++++++++++++++++++++++++++++++---------- 5 files changed, 108 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2ebc74c..01beca4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -436,7 +436,7 @@ dependencies = [ [[package]] name = "dosh" -version = "0.1.2" +version = "0.1.3" dependencies = [ "anyhow", "base64", diff --git a/Cargo.toml b/Cargo.toml index 9f5e165..08aa427 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dosh" -version = "0.1.2" +version = "0.1.3" edition = "2024" license = "MIT" diff --git a/install.ps1 b/install.ps1 index 8ad9853..e675c40 100644 --- a/install.ps1 +++ b/install.ps1 @@ -116,6 +116,31 @@ function Verify-ArchiveChecksum($Url, $Archive) { } } +function Expected-ArchiveVersion($Url) { + if ($Url -match "/releases/download/v([^/]+)/") { + return $Matches[1] + } + if (-not $BinaryUrl -and -not $BinaryBase -and $BinaryVersion -ne "latest") { + return $BinaryVersion.TrimStart("v") + } + return $null +} + +function Verify-ArchiveVersion($ExtractDir, $Url) { + $expected = Expected-ArchiveVersion $Url + if (-not $expected) { + return + } + $versionFile = Get-ChildItem -Path $ExtractDir -Recurse -File -Filter VERSION | Select-Object -First 1 + if (-not $versionFile) { + throw "prebuilt archive missing VERSION for $Url" + } + $actual = (Get-Content $versionFile.FullName -Raw).Trim() + if ($actual -ne $expected) { + throw "prebuilt archive version mismatch for ${Url}: expected $expected, got $actual" + } +} + $bindir = Join-Path $Prefix "bin" $configDir = Join-Path $HOME ".config\dosh" New-Item -ItemType Directory -Force -Path $bindir, $configDir | Out-Null @@ -137,6 +162,7 @@ function Install-Prebuilt { Invoke-WebRequest -UseBasicParsing -Uri $url -OutFile $zip Verify-ArchiveChecksum $url $zip Expand-Archive -Force -Path $zip -DestinationPath $extract + Verify-ArchiveVersion $extract $url foreach ($bin in @("dosh-client.exe", "dosh-bench.exe")) { $found = Get-ChildItem -Path $extract -Recurse -File -Filter $bin | Select-Object -First 1 if (-not $found) { diff --git a/install.sh b/install.sh index bad878d..a901666 100755 --- a/install.sh +++ b/install.sh @@ -268,6 +268,40 @@ verify_archive_checksum() { fi } +expected_archive_version() { + url="$1" + case "$url" in + */releases/download/v*/*) + tag="${url#*/releases/download/v}" + tag="${tag%%/*}" + printf '%s\n' "$tag" + return 0 + ;; + esac + if [ -z "$binary_url" ] && [ -z "$binary_base" ] && [ "$binary_version" != "latest" ]; then + printf '%s\n' "${binary_version#v}" + return 0 + fi + return 1 +} + +verify_archive_version() { + extract_dir="$1" + url="$2" + expected="$(expected_archive_version "$url" || true)" + [ -n "$expected" ] || return 0 + version_file="$(find "$extract_dir" -type f -name VERSION 2>/dev/null | sed -n '1p')" + if [ -z "$version_file" ]; then + echo "prebuilt archive missing VERSION for $url" >&2 + return 1 + fi + actual="$(tr -d '\r\n' <"$version_file")" + if [ "$actual" != "$expected" ]; then + echo "prebuilt archive version mismatch for $url: expected $expected, got $actual" >&2 + return 1 + fi +} + try_install_prebuilt() { download_url="$(release_latest_tag_download_url || release_download_url)" || return 1 tmpdir="$(mktemp -d)" @@ -291,6 +325,7 @@ try_install_prebuilt() { echo "prebuilt archive could not be extracted: $download_url" >&2 return 1 fi + verify_archive_version "$tmpdir/extract" "$download_url" || return 1 install_extracted_binary "$tmpdir/extract" dosh-client "$bindir/dosh-client" || return 1 ln -sf dosh-client "$bindir/dosh" if [ "$role" = "server" ] || [ "$role" = "both" ]; then diff --git a/src/bin/dosh-client.rs b/src/bin/dosh-client.rs index 66f4e34..408e4c4 100644 --- a/src/bin/dosh-client.rs +++ b/src/bin/dosh-client.rs @@ -2797,7 +2797,7 @@ async fn run_terminal( } else { Some(RawMode::enter()?) }; - let addr = resolve_addr(&cred.udp_host, cred.udp_port)?; + let mut addr = resolve_addr(&cred.udp_host, cred.udp_port)?; let mut send_seq = 2u64; let mut last_packet_at = Instant::now(); let mut status_tick = tokio::time::interval(Duration::from_secs(1)); @@ -2989,6 +2989,7 @@ async fn run_terminal( ) .await? { + refresh_live_addr(&mut addr, &cred)?; if !forward_only { render_frame(&frame)?; predictor.observe_output(&frame.bytes); @@ -3060,6 +3061,7 @@ async fn run_terminal( ) .await? { + refresh_live_addr(&mut addr, &cred)?; if !forward_only { render_frame(&frame)?; predictor.observe_output(&frame.bytes); @@ -3164,6 +3166,7 @@ async fn run_terminal( ) .await? { + refresh_live_addr(&mut addr, &cred)?; if !forward_only { render_frame(&frame)?; predictor.observe_output(&frame.bytes); @@ -3534,6 +3537,7 @@ async fn run_terminal( ) .await? { + refresh_live_addr(&mut addr, &cred)?; if !forward_only { render_frame(&frame)?; predictor.observe_output(&frame.bytes); @@ -3826,6 +3830,11 @@ async fn reconnect( Ok(Some(frame)) } +fn refresh_live_addr(addr: &mut SocketAddr, cred: &CachedCredential) -> Result<()> { + *addr = resolve_addr(&cred.udp_host, cred.udp_port)?; + Ok(()) +} + fn queue_pending_user_input( pending: &mut VecDeque>, pending_bytes: &mut usize, @@ -5337,19 +5346,20 @@ const TERMINAL_CLEANUP: &[u8] = concat!( #[cfg(test)] mod tests { use super::{ - DisconnectStatus, DynamicForward, FrameBuffer, LocalForward, MAX_PENDING_USER_INPUT_BYTES, - POST_SUBMIT_ALL_INPUT_HOLD, 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, 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, - render_status_clear, render_status_overlay, requested_env, resolved_startup_command, - 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, - ssh_destination_host, ssh_username, ssh_with_user, startup_command, status_ssh_target, - toml_bare_key_or_quoted, update_check_requested, valid_forward_host, + CachedCredential, DisconnectStatus, DynamicForward, FrameBuffer, LocalForward, + MAX_PENDING_USER_INPUT_BYTES, POST_SUBMIT_ALL_INPUT_HOLD, 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, + 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, render_status_clear, render_status_overlay, + requested_env, resolved_startup_command, 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, ssh_destination_host, + ssh_username, ssh_with_user, startup_command, status_ssh_target, toml_bare_key_or_quoted, + update_check_requested, valid_forward_host, }; use dosh::config::{ClientConfig, CommandExtension, HostConfig}; use dosh::native::EnvVar; @@ -6052,6 +6062,27 @@ mod tests { assert_eq!(post_submit_hold_duration(b"x"), POST_SUBMIT_ALL_INPUT_HOLD); } + #[test] + fn reconnect_refreshes_live_send_address_from_credentials() { + let mut addr = "127.0.0.1:50000".parse().unwrap(); + let cred = CachedCredential { + server: "host".to_string(), + session: "term".to_string(), + mode: "normal".to_string(), + udp_host: "127.0.0.1".to_string(), + udp_port: 50001, + client_id: [1; 16], + session_key: [2; 32], + session_key_id: [3; 16], + attach_ticket: Vec::new(), + attach_ticket_psk: [4; 32], + last_rendered_seq: 0, + }; + + refresh_live_addr(&mut addr, &cred).unwrap(); + assert_eq!(addr.port(), 50001); + } + #[test] fn update_check_accepts_only_check_flag() { assert!(!update_check_requested(&[]).unwrap());