From 6580e75fbfaf9c1f2c944b4227a713f68339a513 Mon Sep 17 00:00:00 2001 From: DuProcess <273172371+DuProcess@users.noreply.github.com> Date: Thu, 11 Jun 2026 22:09:55 -0400 Subject: [PATCH] Resolve SSH aliases for Dosh UDP host --- src/bin/dosh-client.rs | 95 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 84 insertions(+), 11 deletions(-) diff --git a/src/bin/dosh-client.rs b/src/bin/dosh-client.rs index 4eca73b..46e40c9 100644 --- a/src/bin/dosh-client.rs +++ b/src/bin/dosh-client.rs @@ -98,17 +98,21 @@ async fn main() -> Result<()> { let (cols, rows) = size().unwrap_or((80, 24)); let started = Instant::now(); - let target_udp_host = args - .dosh_host - .clone() - .or_else(|| config.dosh_host.clone()) - .unwrap_or_else(|| { - if args.local_auth { - "127.0.0.1".to_string() - } else { + let target_udp_host = + if let Some(host) = args.dosh_host.clone().or_else(|| config.dosh_host.clone()) { + host + } else if args.local_auth { + "127.0.0.1".to_string() + } else { + ssh_config_hostname(&server, ssh_port).unwrap_or_else(|err| { + log_debug( + args.verbose, + 2, + &format!("dosh could not resolve SSH config hostname, using {server}: {err:#}"), + ); ssh_destination_host(&server) - } - }); + }) + }; let credential = if !args.no_cache { load_cache(&cache_path).ok() @@ -378,6 +382,40 @@ fn ssh_destination_host(server: &str) -> String { .to_string() } +fn ssh_config_hostname(server: &str, ssh_port: Option) -> Result { + let mut command = Command::new("ssh"); + command.arg("-G"); + if let Some(ssh_port) = ssh_port { + command.arg("-p").arg(ssh_port.to_string()); + } + let output = command + .arg(server) + .output() + .with_context(|| format!("run ssh -G {server}"))?; + if !output.status.success() { + return Err(anyhow!( + "ssh -G failed: {}", + String::from_utf8_lossy(&output.stderr) + )); + } + let raw = String::from_utf8(output.stdout)?; + parse_ssh_config_hostname(&raw).ok_or_else(|| anyhow!("ssh -G did not print hostname")) +} + +fn parse_ssh_config_hostname(raw: &str) -> Option { + raw.lines().find_map(|line| { + let line = line.trim(); + let (key, value) = line.split_once(char::is_whitespace)?; + if key.eq_ignore_ascii_case("hostname") { + let value = value.trim(); + if !value.is_empty() { + return Some(value.to_string()); + } + } + None + }) +} + fn resolve_addr(host: &str, port: u16) -> Result { (host, port) .to_socket_addrs() @@ -407,6 +445,10 @@ async fn bootstrap_attach( let (n, _) = tokio::time::timeout(Duration::from_secs(5), socket.recv_from(&mut buf)).await??; let packet = protocol::decode(&buf[..n])?; if packet.header.kind != PacketKind::AttachOk { + if packet.header.kind == PacketKind::AttachReject { + let reject: AttachReject = protocol::from_body(&packet.body)?; + return Err(anyhow!("attach rejected: {}", reject.reason)); + } return Err(anyhow!("attach rejected or unexpected response")); } let plain = protocol::decrypt_body(&packet, &bootstrap.session_key, SERVER_TO_CLIENT)?; @@ -471,6 +513,10 @@ async fn try_ticket_attach( tokio::time::timeout(Duration::from_millis(700), socket.recv_from(&mut buf)).await??; let packet = protocol::decode(&buf[..n])?; if packet.header.kind != PacketKind::AttachOk { + if packet.header.kind == PacketKind::AttachReject { + let reject: AttachReject = protocol::from_body(&packet.body)?; + return Err(anyhow!("ticket attach rejected: {}", reject.reason)); + } return Err(anyhow!("ticket attach rejected")); } let envelope: TicketAttachOkEnvelope = protocol::from_body(&packet.body)?; @@ -533,6 +579,10 @@ async fn try_resume( tokio::time::timeout(Duration::from_millis(700), socket.recv_from(&mut buf)).await??; let packet = protocol::decode(&buf[..n])?; if packet.header.kind != PacketKind::ResumeOk { + if packet.header.kind == PacketKind::AttachReject { + let reject: AttachReject = protocol::from_body(&packet.body)?; + return Err(anyhow!("resume rejected: {}", reject.reason)); + } return Err(anyhow!("resume rejected")); } let plain = protocol::decrypt_body(&packet, &cached.session_key, SERVER_TO_CLIENT)?; @@ -866,7 +916,7 @@ const TERMINAL_CLEANUP: &[u8] = concat!( #[cfg(test)] mod tests { - use super::InputNormalizer; + use super::{InputNormalizer, parse_ssh_config_hostname, ssh_destination_host}; #[test] fn normalizes_ss3_cursor_keys_to_csi() { @@ -890,4 +940,27 @@ mod tests { let mut normalizer = InputNormalizer::default(); assert_eq!(normalizer.normalize(b"\x1bOP"), b"\x1bOP"); } + + #[test] + fn parses_ssh_config_hostname() { + let raw = "user palav\nhostname palav.dev\nport 22\n"; + assert_eq!( + parse_ssh_config_hostname(raw), + Some("palav.dev".to_string()) + ); + } + + #[test] + fn parses_ssh_config_hostname_case_insensitively() { + let raw = "HostName 192.0.2.10\n"; + assert_eq!( + parse_ssh_config_hostname(raw), + Some("192.0.2.10".to_string()) + ); + } + + #[test] + fn strips_user_from_fallback_udp_host() { + assert_eq!(ssh_destination_host("palav@example.com"), "example.com"); + } }