From d74457d198d91e44e19beb863b5f56d3a40a0c5c Mon Sep 17 00:00:00 2001 From: DuProcess <273172371+DuProcess@users.noreply.github.com> Date: Sat, 11 Jul 2026 11:47:45 -0400 Subject: [PATCH] Preserve OpenSSH alias config for hosts --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 10 +++++++ src/bin/dosh-client.rs | 67 ++++++++++++++++++++++++------------------ src/config.rs | 6 ++++ 5 files changed, 57 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c56d64c..7cfdbeb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -436,7 +436,7 @@ dependencies = [ [[package]] name = "dosh" -version = "1.0.0-rc19" +version = "1.0.0-rc20" dependencies = [ "anyhow", "base64", diff --git a/Cargo.toml b/Cargo.toml index a334351..5102f2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dosh" -version = "1.0.0-rc19" +version = "1.0.0-rc20" edition = "2024" license = "MIT" diff --git a/README.md b/README.md index 0d61415..bffa271 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,16 @@ cache_attach_tickets = true disconnect_status = true ``` +Example host entry: + +```toml +[prod] +ssh = "prod" +ssh_config = "prod" # OpenSSH alias for IdentityFile, ProxyJump, SendEnv, etc. +dosh_host = "prod.example.com" +port = 50000 +``` + ## 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 e51a768..8653391 100644 --- a/src/bin/dosh-client.rs +++ b/src/bin/dosh-client.rs @@ -302,8 +302,7 @@ async fn main() -> Result<()> { .get(&requested_server) .cloned() .unwrap_or_default(); - let raw_server = host.ssh.clone().unwrap_or_else(|| requested_server.clone()); - let server = ssh_with_user(&raw_server, host.user.as_deref()); + let server = ssh_command_target(&requested_server, &host); let startup_command = resolved_startup_command(&args.command, &config, &host); let local_forwards = parse_local_forwards(&args.local_forward)?; let remote_forwards = parse_remote_forwards(&args.remote_forward)?; @@ -655,8 +654,7 @@ fn run_trust_command(config: &dosh::config::ClientConfig, args: &Args) -> Result let hosts = load_hosts_config(None).unwrap_or_default(); let host_config = hosts.hosts.get(host).cloned().unwrap_or_default(); - let raw_server = host_config.ssh.clone().unwrap_or_else(|| host.to_string()); - let server = ssh_with_user(&raw_server, host_config.user.as_deref()); + let server = ssh_command_target(host, &host_config); let ssh_port = args.ssh_port.or(host_config.ssh_port).or(config.ssh_port); let ssh_auth_command = args .ssh_auth_command @@ -701,8 +699,8 @@ async fn run_setup_command(config: &dosh::config::ClientConfig, args: &Args) -> for entry in imported { match entry.action { ImportAction::Added => println!( - "[ok] host config: [{}] ssh={} dosh_host={} port={}", - entry.alias, entry.ssh, entry.dosh_host, entry.port + "[ok] host config: [{}] ssh={} ssh_config={} dosh_host={} port={}", + entry.alias, entry.ssh, entry.ssh_config, entry.dosh_host, entry.port ), ImportAction::Skipped => println!("[ok] host config: [{}] already exists", entry.alias), } @@ -710,8 +708,7 @@ async fn run_setup_command(config: &dosh::config::ClientConfig, args: &Args) -> let hosts = load_hosts_config(None).unwrap_or_default(); let host_config = hosts.hosts.get(&host).cloned().unwrap_or_default(); - let raw_server = host_config.ssh.clone().unwrap_or_else(|| host.clone()); - let server = ssh_with_user(&raw_server, host_config.user.as_deref()); + let server = ssh_command_target(&host, &host_config); let ssh_port = args.ssh_port.or(host_config.ssh_port).or(config.ssh_port); let ssh_auth_command = args .ssh_auth_command @@ -843,9 +840,15 @@ fn run_restart_command(config: &dosh::config::ClientConfig, args: &Args) -> Resu } fn status_ssh_target(requested: &str, host_config: &dosh::config::HostConfig) -> String { + ssh_command_target(requested, host_config) +} + +fn ssh_command_target(requested: &str, host_config: &dosh::config::HostConfig) -> String { let raw_server = host_config - .ssh - .clone() + .ssh_config + .as_ref() + .or(host_config.ssh.as_ref()) + .cloned() .unwrap_or_else(|| requested.to_string()); ssh_with_user(&raw_server, host_config.user.as_deref()) } @@ -882,11 +885,7 @@ async fn run_doctor_for_host( ) -> Result<()> { let hosts = load_hosts_config(None).unwrap_or_default(); let host_config = hosts.hosts.get(requested).cloned().unwrap_or_default(); - let raw_server = host_config - .ssh - .clone() - .unwrap_or_else(|| requested.to_string()); - let server = ssh_with_user(&raw_server, host_config.user.as_deref()); + let server = ssh_command_target(requested, &host_config); let ssh_port = args.ssh_port.or(host_config.ssh_port).or(config.ssh_port); let dosh_port = args .dosh_port @@ -1393,8 +1392,7 @@ async fn run_proxy_stdio_command(config: &dosh::config::ClientConfig, args: &Arg .get(&requested_server) .cloned() .unwrap_or_default(); - let raw_server = host.ssh.clone().unwrap_or_else(|| requested_server.clone()); - let server = ssh_with_user(&raw_server, host.user.as_deref()); + let server = ssh_command_target(&requested_server, &host); let ssh_port = args.ssh_port.or(host.ssh_port).or(config.ssh_port); let resolved_ssh_config = ssh_config(&server, ssh_port).unwrap_or_default(); let requested_udp_host = args @@ -1561,11 +1559,7 @@ fn ensure_vscode_ssh_host( ) -> Result { let hosts = load_hosts_config(None).unwrap_or_default(); let host_config = hosts.hosts.get(requested).cloned().unwrap_or_default(); - let raw_server = host_config - .ssh - .clone() - .unwrap_or_else(|| requested.to_string()); - let server = ssh_with_user(&raw_server, host_config.user.as_deref()); + let server = ssh_command_target(requested, &host_config); let ssh_port = args.ssh_port.or(host_config.ssh_port).or(config.ssh_port); let ssh_config = ssh_config(&server, ssh_port).unwrap_or_default(); let target_ssh_port = ssh_config.port.or(ssh_port).unwrap_or(22); @@ -3247,6 +3241,7 @@ enum ImportAction { struct ImportedHost { alias: String, ssh: String, + ssh_config: String, dosh_host: String, port: u16, action: ImportAction, @@ -3260,8 +3255,8 @@ fn run_import_ssh(config: &dosh::config::ClientConfig, aliases: &[String]) -> Re for entry in imported { match entry.action { ImportAction::Added => eprintln!( - "dosh import-ssh: added [{}] ssh={} dosh_host={}", - entry.alias, entry.ssh, entry.dosh_host + "dosh import-ssh: added [{}] ssh={} ssh_config={} dosh_host={}", + entry.alias, entry.ssh, entry.ssh_config, entry.dosh_host ), ImportAction::Skipped => { eprintln!( @@ -3303,6 +3298,7 @@ fn import_ssh_aliases( imported.push(ImportedHost { alias: alias.clone(), ssh: alias.clone(), + ssh_config: alias.clone(), dosh_host: udp_host, port: config.dosh_port, action: ImportAction::Skipped, @@ -3312,6 +3308,7 @@ fn import_ssh_aliases( raw.push('\n'); raw.push_str(&format!("[{}]\n", toml_bare_key_or_quoted(alias))); raw.push_str(&format!("ssh = {}\n", toml_string(alias))); + raw.push_str(&format!("ssh_config = {}\n", toml_string(alias))); raw.push_str(&format!("dosh_host = {}\n", toml_string(&udp_host))); raw.push_str(&format!("port = {}\n", config.dosh_port)); if let Some(user) = &ssh_config.user { @@ -3326,6 +3323,7 @@ fn import_ssh_aliases( imported.push(ImportedHost { alias: alias.clone(), ssh: alias.clone(), + ssh_config: alias.clone(), dosh_host: udp_host, port: config.dosh_port, action: ImportAction::Added, @@ -7773,10 +7771,10 @@ mod tests { 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_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_username, ssh_with_user, ssh_command_target, 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; @@ -9017,6 +9015,19 @@ mod tests { ); } + #[test] + fn ssh_command_target_prefers_explicit_ssh_config_alias() { + let host = HostConfig { + ssh: Some("server.example.com".to_string()), + ssh_config: Some("work-alias".to_string()), + user: Some("alice".to_string()), + ..HostConfig::default() + }; + + assert_eq!(ssh_command_target("work", &host), "alice@work-alias"); + assert_eq!(status_ssh_target("work", &host), "alice@work-alias"); + } + #[test] fn status_detects_local_targets() { assert!(is_local_status_target("localhost")); diff --git a/src/config.rs b/src/config.rs index 7dbc998..c6df5e9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -150,6 +150,12 @@ pub struct ClientConfig { #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct HostConfig { pub ssh: Option, + /// Optional OpenSSH config host name to use for SSH-backed bootstrap, + /// trust/setup/doctor checks, and native-auth identity selection. This lets + /// a Dosh host keep a separate UDP endpoint while still inheriting + /// alias-scoped OpenSSH settings like IdentityFile, ProxyJump, and SendEnv. + #[serde(default)] + pub ssh_config: Option, pub user: Option, pub dosh_host: Option, pub port: Option,