From 4ea05fc14f33ddda5cd85ed74891308a71797c97 Mon Sep 17 00:00:00 2001 From: DuProcess <273172371+DuProcess@users.noreply.github.com> Date: Sat, 11 Jul 2026 17:44:43 -0400 Subject: [PATCH] Clarify proxied SSH UDP requirements --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 4 +++ src/bin/dosh-client.rs | 76 +++++++++++++++++++++++++++++++++++++----- 4 files changed, 74 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3f416fc..c420660 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -436,7 +436,7 @@ dependencies = [ [[package]] name = "dosh" -version = "1.0.0-rc24" +version = "1.0.0-rc25" dependencies = [ "anyhow", "base64", diff --git a/Cargo.toml b/Cargo.toml index 8665076..225a906 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dosh" -version = "1.0.0-rc24" +version = "1.0.0-rc25" edition = "2024" license = "MIT" diff --git a/README.md b/README.md index bffa271..8e54494 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,10 @@ dosh_host = "prod.example.com" port = 50000 ``` +`ProxyJump` and `ProxyCommand` can be used for SSH setup, but the Dosh UDP +endpoint still has to be reachable directly from the client. Set `dosh_host` to +the reachable UDP hostname or IP when it differs from the SSH alias target. + ## Rust Library Dosh exposes a Rust transport for encrypted, reconnecting application streams. diff --git a/src/bin/dosh-client.rs b/src/bin/dosh-client.rs index 80e76b6..011a7ef 100644 --- a/src/bin/dosh-client.rs +++ b/src/bin/dosh-client.rs @@ -960,6 +960,14 @@ async fn run_doctor_for_host( } }; println!("[info] native endpoint: {udp_host}:{dosh_port}"); + if let Some(warning) = native_proxy_udp_warning( + &parsed_ssh_config, + requested_udp_host.as_deref(), + &udp_host, + dosh_port, + ) { + println!("[warn] native endpoint: {warning}"); + } match resolve_addr(&udp_host, dosh_port) { Ok(addr) => println!("[ok] udp resolve: {addr}"), Err(err) => println!("[fail] udp resolve: {err:#}"), @@ -3532,6 +3540,25 @@ fn selected_udp_host( } } +fn ssh_config_uses_proxy(ssh_config: &SshConfig) -> bool { + ssh_config.proxy_jump.is_some() || ssh_config.proxy_command.is_some() +} + +fn native_proxy_udp_warning( + ssh_config: &SshConfig, + requested_udp_host: Option<&str>, + udp_host: &str, + dosh_port: u16, +) -> Option { + if ssh_config_uses_proxy(ssh_config) && requested_udp_host.is_none() { + Some(format!( + "SSH uses ProxyJump/ProxyCommand, but Dosh UDP still needs a directly reachable host; set dosh_host if {udp_host}:{dosh_port} is not reachable from this client" + )) + } else { + None + } +} + fn ssh_username(server: &str) -> Option { let (user, _) = server.rsplit_once('@')?; if user.is_empty() { @@ -7756,20 +7783,21 @@ mod tests { cleanup_stream_state, 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, - load_first_native_identity_with_prompt, parse_dynamic_forward, parse_escape_key, - parse_local_forward, parse_remote_forward, parse_ssh_config, parse_update_options, - post_submit_hold_duration, queue_pending_user_input, raw_contains_host_table, - recv_response_until, refresh_live_addr, release_tag_download_url, + load_first_native_identity_with_prompt, native_proxy_udp_warning, parse_dynamic_forward, + parse_escape_key, parse_local_forward, parse_remote_forward, parse_ssh_config, + parse_update_options, 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, retire_stream_state, 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, should_repaint_idle_alternate_screen, split_after_command_submit, ssh_command_target, - ssh_destination_host, ssh_username, ssh_with_user, startup_command, status_ssh_target, - strip_stale_mouse_reports, strip_terminal_focus_reports, terminal_private_mode_transition, - toml_bare_key_or_quoted, unix_update_script, update_installer_url, update_version_status, - upsert_managed_block, valid_forward_host, vscode_safe_alias, windows_update_script, + ssh_config_uses_proxy, ssh_destination_host, ssh_username, ssh_with_user, startup_command, + status_ssh_target, strip_stale_mouse_reports, strip_terminal_focus_reports, + terminal_private_mode_transition, toml_bare_key_or_quoted, unix_update_script, + update_installer_url, update_version_status, upsert_managed_block, valid_forward_host, + vscode_safe_alias, windows_update_script, }; use dosh::config::{ClientConfig, CommandExtension, HostConfig}; use dosh::native::EnvVar; @@ -8141,6 +8169,38 @@ mod tests { assert!(selected_udp_host(Some("any"), "alias", &ssh).is_err()); } + #[test] + fn ssh_config_proxy_detection_covers_jump_and_command() { + assert!(!ssh_config_uses_proxy(&SshConfig::default())); + assert!(ssh_config_uses_proxy(&SshConfig { + proxy_jump: Some("bastion".to_string()), + ..SshConfig::default() + })); + assert!(ssh_config_uses_proxy(&SshConfig { + proxy_command: Some("ssh bastion -W %h:%p".to_string()), + ..SshConfig::default() + })); + } + + #[test] + fn native_proxy_udp_warning_requires_missing_explicit_dosh_host() { + let proxied = SshConfig { + proxy_jump: Some("bastion".to_string()), + ..SshConfig::default() + }; + let warning = native_proxy_udp_warning(&proxied, None, "internal.lan", 50000) + .expect("proxied SSH without dosh_host should warn"); + assert!(warning.contains("ProxyJump/ProxyCommand")); + assert!(warning.contains("internal.lan:50000")); + assert!( + native_proxy_udp_warning(&proxied, Some("public.example.com"), "internal.lan", 50000) + .is_none() + ); + assert!( + native_proxy_udp_warning(&SshConfig::default(), None, "internal.lan", 50000).is_none() + ); + } + #[test] fn parses_user_from_ssh_destination() { assert_eq!(ssh_username("alice@example.com"), Some("alice".to_string()));