Try all resolved UDP peers for native auth
ci / test (push) Canceled after 0s
ci / fuzz-smoke (push) Canceled after 0s
ci / macos-client (macos-aarch64, macos-14) (push) Canceled after 0s
ci / macos-client (macos-x86_64, macos-13) (push) Canceled after 0s
ci / windows-client (push) Canceled after 0s
ci / package-release (linux-x86_64, ubuntu-latest, , , ) (push) Canceled after 0s
ci / package-release (macos-aarch64, macos-14, , , ) (push) Canceled after 0s
ci / package-release (macos-x86_64, macos-13, , , ) (push) Canceled after 0s
ci / package-release (windows-aarch64, windows-latest, aarch64, windows, aarch64-pc-windows-msvc) (push) Canceled after 0s
ci / package-release (windows-x86_64, windows-latest, , , ) (push) Canceled after 0s
ci / remote-bench (push) Canceled after 0s
ci / publish-gitea-release (push) Canceled after 0s
ci / test (push) Canceled after 0s
ci / fuzz-smoke (push) Canceled after 0s
ci / macos-client (macos-aarch64, macos-14) (push) Canceled after 0s
ci / macos-client (macos-x86_64, macos-13) (push) Canceled after 0s
ci / windows-client (push) Canceled after 0s
ci / package-release (linux-x86_64, ubuntu-latest, , , ) (push) Canceled after 0s
ci / package-release (macos-aarch64, macos-14, , , ) (push) Canceled after 0s
ci / package-release (macos-x86_64, macos-13, , , ) (push) Canceled after 0s
ci / package-release (windows-aarch64, windows-latest, aarch64, windows, aarch64-pc-windows-msvc) (push) Canceled after 0s
ci / package-release (windows-x86_64, windows-latest, , , ) (push) Canceled after 0s
ci / remote-bench (push) Canceled after 0s
ci / publish-gitea-release (push) Canceled after 0s
This commit is contained in:
+197
-30
@@ -90,6 +90,13 @@ const AGENT_STREAM_SENTINEL: &str = "@dosh-agent";
|
|||||||
const WINDOWS_OPENSSH_AGENT_PIPE: &str = r"\\.\pipe\openssh-ssh-agent";
|
const WINDOWS_OPENSSH_AGENT_PIPE: &str = r"\\.\pipe\openssh-ssh-agent";
|
||||||
type AgentWriter = Box<dyn AsyncWrite + Unpin + Send>;
|
type AgentWriter = Box<dyn AsyncWrite + Unpin + Send>;
|
||||||
|
|
||||||
|
struct NativeAuthAttempt {
|
||||||
|
socket: UdpSocket,
|
||||||
|
addr: SocketAddr,
|
||||||
|
frame: Frame,
|
||||||
|
cred: CachedCredential,
|
||||||
|
}
|
||||||
|
|
||||||
/// Current terminal size, with a sane fallback.
|
/// Current terminal size, with a sane fallback.
|
||||||
///
|
///
|
||||||
/// `crossterm::size()` returns `Err` when stdout is not a TTY (piped), but it
|
/// `crossterm::size()` returns `Err` when stdout is not a TTY (piped), but it
|
||||||
@@ -451,7 +458,8 @@ async fn main() -> Result<()> {
|
|||||||
};
|
};
|
||||||
let cache_requested_env = requested_env(&config, &host, resolved_ssh_config.as_ref());
|
let cache_requested_env = requested_env(&config, &host, resolved_ssh_config.as_ref());
|
||||||
|
|
||||||
let target_udp_addr = resolve_addr(&target_udp_host, dosh_port)?;
|
let target_udp_addrs = resolve_addrs(&target_udp_host, dosh_port)?;
|
||||||
|
let target_udp_addr = first_resolved_addr(&target_udp_addrs, &target_udp_host, dosh_port)?;
|
||||||
let socket = bind_udp_for_peer(target_udp_addr).await?;
|
let socket = bind_udp_for_peer(target_udp_addr).await?;
|
||||||
|
|
||||||
if let Some(mut cached) = credential.clone() {
|
if let Some(mut cached) = credential.clone() {
|
||||||
@@ -472,6 +480,7 @@ async fn main() -> Result<()> {
|
|||||||
return run_terminal(
|
return run_terminal(
|
||||||
socket,
|
socket,
|
||||||
cred,
|
cred,
|
||||||
|
Some(target_udp_addr),
|
||||||
Some(frame),
|
Some(frame),
|
||||||
predict,
|
predict,
|
||||||
predict_mode,
|
predict_mode,
|
||||||
@@ -510,6 +519,7 @@ async fn main() -> Result<()> {
|
|||||||
return run_terminal(
|
return run_terminal(
|
||||||
socket,
|
socket,
|
||||||
cred,
|
cred,
|
||||||
|
Some(target_udp_addr),
|
||||||
Some(frame),
|
Some(frame),
|
||||||
predict,
|
predict,
|
||||||
predict_mode,
|
predict_mode,
|
||||||
@@ -540,9 +550,14 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
let cold_requested_env = requested_env(&config, &host, resolved_ssh_config.as_ref());
|
let cold_requested_env = requested_env(&config, &host, resolved_ssh_config.as_ref());
|
||||||
let native_start = Instant::now();
|
let native_start = Instant::now();
|
||||||
match try_native_auth(
|
let requested_forwardings = forwarding_requests(
|
||||||
&socket,
|
&local_forwards,
|
||||||
target_udp_addr,
|
&remote_forwards,
|
||||||
|
&dynamic_forwards,
|
||||||
|
forward_agent,
|
||||||
|
);
|
||||||
|
match try_native_auth_any_addr(
|
||||||
|
&target_udp_addrs,
|
||||||
&config,
|
&config,
|
||||||
&requested_server,
|
&requested_server,
|
||||||
&server,
|
&server,
|
||||||
@@ -554,19 +569,19 @@ async fn main() -> Result<()> {
|
|||||||
&mode,
|
&mode,
|
||||||
cols,
|
cols,
|
||||||
rows,
|
rows,
|
||||||
forwarding_requests(
|
requested_forwardings,
|
||||||
&local_forwards,
|
|
||||||
&remote_forwards,
|
|
||||||
&dynamic_forwards,
|
|
||||||
forward_agent,
|
|
||||||
),
|
|
||||||
resolved_ssh_config.as_ref(),
|
resolved_ssh_config.as_ref(),
|
||||||
cold_requested_env,
|
cold_requested_env,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok((frame, cred)) => {
|
Ok(NativeAuthAttempt {
|
||||||
|
socket,
|
||||||
|
addr,
|
||||||
|
frame,
|
||||||
|
cred,
|
||||||
|
}) => {
|
||||||
log_timing(args.verbose, "native_auth", native_start.elapsed());
|
log_timing(args.verbose, "native_auth", native_start.elapsed());
|
||||||
if allow_cache {
|
if allow_cache {
|
||||||
save_cache(&cache_path, &cred)?;
|
save_cache(&cache_path, &cred)?;
|
||||||
@@ -580,6 +595,7 @@ async fn main() -> Result<()> {
|
|||||||
return run_terminal(
|
return run_terminal(
|
||||||
socket,
|
socket,
|
||||||
cred,
|
cred,
|
||||||
|
Some(addr),
|
||||||
Some(frame),
|
Some(frame),
|
||||||
predict,
|
predict,
|
||||||
predict_mode,
|
predict_mode,
|
||||||
@@ -658,6 +674,7 @@ async fn main() -> Result<()> {
|
|||||||
run_terminal(
|
run_terminal(
|
||||||
socket,
|
socket,
|
||||||
cred,
|
cred,
|
||||||
|
Some(target_udp_addr),
|
||||||
Some(first),
|
Some(first),
|
||||||
predict,
|
predict,
|
||||||
predict_mode,
|
predict_mode,
|
||||||
@@ -1949,10 +1966,17 @@ async fn run_doctor_for_host(
|
|||||||
) {
|
) {
|
||||||
println!("[warn] native endpoint: {warning}");
|
println!("[warn] native endpoint: {warning}");
|
||||||
}
|
}
|
||||||
let udp_addr = match resolve_addr(&udp_host, dosh_port) {
|
let udp_addrs = match resolve_addrs(&udp_host, dosh_port) {
|
||||||
Ok(addr) => {
|
Ok(addrs) => {
|
||||||
println!("[ok] udp resolve: {addr}");
|
println!(
|
||||||
addr
|
"[ok] udp resolve: {}",
|
||||||
|
addrs
|
||||||
|
.iter()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
);
|
||||||
|
addrs
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
println!("[fail] udp resolve: {err:#}");
|
println!("[fail] udp resolve: {err:#}");
|
||||||
@@ -1963,10 +1987,8 @@ async fn run_doctor_for_host(
|
|||||||
"[info] forwarding requested by config: agent={} default_session={}",
|
"[info] forwarding requested by config: agent={} default_session={}",
|
||||||
config.forward_agent, config.default_session
|
config.forward_agent, config.default_session
|
||||||
);
|
);
|
||||||
let socket = bind_udp_for_peer(udp_addr).await?;
|
match try_native_auth_check_any_addr(
|
||||||
match try_native_auth_check(
|
&udp_addrs,
|
||||||
&socket,
|
|
||||||
udp_addr,
|
|
||||||
config,
|
config,
|
||||||
requested,
|
requested,
|
||||||
&server,
|
&server,
|
||||||
@@ -2400,8 +2422,7 @@ async fn run_proxy_stdio_command(config: &dosh::config::ClientConfig, args: &Arg
|
|||||||
let target_udp_host =
|
let target_udp_host =
|
||||||
selected_udp_host(requested_udp_host.as_deref(), &server, &resolved_ssh_config)?;
|
selected_udp_host(requested_udp_host.as_deref(), &server, &resolved_ssh_config)?;
|
||||||
let dosh_port = args.dosh_port.or(host.port).unwrap_or(config.dosh_port);
|
let dosh_port = args.dosh_port.or(host.port).unwrap_or(config.dosh_port);
|
||||||
let target_udp_addr = resolve_addr(&target_udp_host, dosh_port)?;
|
let target_udp_addrs = resolve_addrs(&target_udp_host, dosh_port)?;
|
||||||
let socket = bind_udp_for_peer(target_udp_addr).await?;
|
|
||||||
let session = args
|
let session = args
|
||||||
.session
|
.session
|
||||||
.clone()
|
.clone()
|
||||||
@@ -2413,9 +2434,13 @@ async fn run_proxy_stdio_command(config: &dosh::config::ClientConfig, args: &Arg
|
|||||||
target_host: Some(target_host.clone()),
|
target_host: Some(target_host.clone()),
|
||||||
target_port: Some(target_port),
|
target_port: Some(target_port),
|
||||||
}];
|
}];
|
||||||
let (_frame, cred) = try_native_auth(
|
let NativeAuthAttempt {
|
||||||
&socket,
|
socket,
|
||||||
target_udp_addr,
|
addr,
|
||||||
|
frame: _frame,
|
||||||
|
cred,
|
||||||
|
} = try_native_auth_any_addr(
|
||||||
|
&target_udp_addrs,
|
||||||
config,
|
config,
|
||||||
&requested_server,
|
&requested_server,
|
||||||
&server,
|
&server,
|
||||||
@@ -2434,14 +2459,13 @@ async fn run_proxy_stdio_command(config: &dosh::config::ClientConfig, args: &Arg
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let peer_addr = resolve_addr(&cred.udp_host, cred.udp_port)?;
|
|
||||||
let mut transport = DoshTransport::new_owned(
|
let mut transport = DoshTransport::new_owned(
|
||||||
socket,
|
socket,
|
||||||
SessionTransportConfig {
|
SessionTransportConfig {
|
||||||
role: SessionRole::Client,
|
role: SessionRole::Client,
|
||||||
conn_id: cred.client_id,
|
conn_id: cred.client_id,
|
||||||
session_key: cred.session_key,
|
session_key: cred.session_key,
|
||||||
peer_addr,
|
peer_addr: addr,
|
||||||
initial_send_seq: 2,
|
initial_send_seq: 2,
|
||||||
initial_ack: cred.last_rendered_seq,
|
initial_ack: cred.last_rendered_seq,
|
||||||
stream: TransportConfig::default(),
|
stream: TransportConfig::default(),
|
||||||
@@ -5310,10 +5334,25 @@ fn glob_matches(pattern: &str, value: &str) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_addr(host: &str, port: u16) -> Result<SocketAddr> {
|
fn resolve_addr(host: &str, port: u16) -> Result<SocketAddr> {
|
||||||
(host, port)
|
let addrs = resolve_addrs(host, port)?;
|
||||||
|
first_resolved_addr(&addrs, host, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_addrs(host: &str, port: u16) -> Result<Vec<SocketAddr>> {
|
||||||
|
let addrs = (host, port)
|
||||||
.to_socket_addrs()
|
.to_socket_addrs()
|
||||||
.with_context(|| format!("resolve UDP target {host}:{port}"))?
|
.with_context(|| format!("resolve UDP target {host}:{port}"))?
|
||||||
.next()
|
.collect::<Vec<_>>();
|
||||||
|
if addrs.is_empty() {
|
||||||
|
return Err(anyhow!("no UDP address resolved for {host}:{port}"));
|
||||||
|
}
|
||||||
|
Ok(addrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn first_resolved_addr(addrs: &[SocketAddr], host: &str, port: u16) -> Result<SocketAddr> {
|
||||||
|
addrs
|
||||||
|
.first()
|
||||||
|
.copied()
|
||||||
.ok_or_else(|| anyhow!("no UDP address resolved for {host}:{port}"))
|
.ok_or_else(|| anyhow!("no UDP address resolved for {host}:{port}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5373,6 +5412,72 @@ async fn send_terminal_udp(socket: &UdpSocket, packet: &[u8], addr: SocketAddr)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
async fn try_native_auth_any_addr(
|
||||||
|
addrs: &[SocketAddr],
|
||||||
|
config: &dosh::config::ClientConfig,
|
||||||
|
requested_server: &str,
|
||||||
|
server: &str,
|
||||||
|
ssh_port: Option<u16>,
|
||||||
|
udp_host: &str,
|
||||||
|
udp_port: u16,
|
||||||
|
cli_identity: Option<&Path>,
|
||||||
|
session: &str,
|
||||||
|
mode: &str,
|
||||||
|
cols: u16,
|
||||||
|
rows: u16,
|
||||||
|
requested_forwardings: Vec<ForwardingRequest>,
|
||||||
|
ssh_config_hint: Option<&SshConfig>,
|
||||||
|
requested_env: Vec<EnvVar>,
|
||||||
|
allow_passphrase_prompt: bool,
|
||||||
|
) -> Result<NativeAuthAttempt> {
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
for addr in addrs {
|
||||||
|
let socket = match bind_udp_for_peer(*addr).await {
|
||||||
|
Ok(socket) => socket,
|
||||||
|
Err(err) => {
|
||||||
|
errors.push(format!("{addr}: bind UDP socket: {err:#}"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match try_native_auth(
|
||||||
|
&socket,
|
||||||
|
*addr,
|
||||||
|
config,
|
||||||
|
requested_server,
|
||||||
|
server,
|
||||||
|
ssh_port,
|
||||||
|
udp_host,
|
||||||
|
udp_port,
|
||||||
|
cli_identity,
|
||||||
|
session,
|
||||||
|
mode,
|
||||||
|
cols,
|
||||||
|
rows,
|
||||||
|
requested_forwardings.clone(),
|
||||||
|
ssh_config_hint,
|
||||||
|
requested_env.clone(),
|
||||||
|
allow_passphrase_prompt,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok((frame, cred)) => {
|
||||||
|
return Ok(NativeAuthAttempt {
|
||||||
|
socket,
|
||||||
|
addr: *addr,
|
||||||
|
frame,
|
||||||
|
cred,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(err) => errors.push(format!("{addr}: {err:#}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(anyhow!(
|
||||||
|
"native auth failed for all resolved UDP addresses: {}",
|
||||||
|
errors.join("; ")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn try_native_auth(
|
async fn try_native_auth(
|
||||||
socket: &UdpSocket,
|
socket: &UdpSocket,
|
||||||
@@ -5658,6 +5763,46 @@ async fn try_native_auth_check(
|
|||||||
protocol::from_body(&plain)
|
protocol::from_body(&plain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn try_native_auth_check_any_addr(
|
||||||
|
addrs: &[SocketAddr],
|
||||||
|
config: &dosh::config::ClientConfig,
|
||||||
|
requested_server: &str,
|
||||||
|
server: &str,
|
||||||
|
ssh_port: Option<u16>,
|
||||||
|
cli_identity: Option<&Path>,
|
||||||
|
ssh_config_hint: Option<&SshConfig>,
|
||||||
|
) -> Result<NativeAuthCheckOkBody> {
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
for addr in addrs {
|
||||||
|
let socket = match bind_udp_for_peer(*addr).await {
|
||||||
|
Ok(socket) => socket,
|
||||||
|
Err(err) => {
|
||||||
|
errors.push(format!("{addr}: bind UDP socket: {err:#}"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match try_native_auth_check(
|
||||||
|
&socket,
|
||||||
|
*addr,
|
||||||
|
config,
|
||||||
|
requested_server,
|
||||||
|
server,
|
||||||
|
ssh_port,
|
||||||
|
cli_identity,
|
||||||
|
ssh_config_hint,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(check) => return Ok(check),
|
||||||
|
Err(err) => errors.push(format!("{addr}: {err:#}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(anyhow!(
|
||||||
|
"native auth check failed for all resolved UDP addresses: {}",
|
||||||
|
errors.join("; ")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
fn sign_native_user_auth(
|
fn sign_native_user_auth(
|
||||||
config: &dosh::config::ClientConfig,
|
config: &dosh::config::ClientConfig,
|
||||||
cli_identity: Option<&Path>,
|
cli_identity: Option<&Path>,
|
||||||
@@ -6207,6 +6352,7 @@ where
|
|||||||
async fn run_terminal(
|
async fn run_terminal(
|
||||||
socket: UdpSocket,
|
socket: UdpSocket,
|
||||||
mut cred: CachedCredential,
|
mut cred: CachedCredential,
|
||||||
|
initial_addr: Option<SocketAddr>,
|
||||||
first_frame: Option<Frame>,
|
first_frame: Option<Frame>,
|
||||||
predict: bool,
|
predict: bool,
|
||||||
predict_mode: PredictMode,
|
predict_mode: PredictMode,
|
||||||
@@ -6223,7 +6369,10 @@ async fn run_terminal(
|
|||||||
} else {
|
} else {
|
||||||
Some(RawMode::enter()?)
|
Some(RawMode::enter()?)
|
||||||
};
|
};
|
||||||
let mut addr = resolve_addr(&cred.udp_host, cred.udp_port)?;
|
let mut addr = match initial_addr {
|
||||||
|
Some(addr) => addr,
|
||||||
|
None => resolve_addr(&cred.udp_host, cred.udp_port)?,
|
||||||
|
};
|
||||||
let mut send_seq = 2u64;
|
let mut send_seq = 2u64;
|
||||||
let mut last_packet_at = Instant::now();
|
let mut last_packet_at = Instant::now();
|
||||||
let mut status_tick = tokio::time::interval(Duration::from_secs(1));
|
let mut status_tick = tokio::time::interval(Duration::from_secs(1));
|
||||||
@@ -10411,7 +10560,7 @@ mod tests {
|
|||||||
SshPathTokenContext, StartupGateMode, StatusAction, TERMINAL_SNAPSHOT_RESET, UpdateOptions,
|
SshPathTokenContext, StartupGateMode, StatusAction, TERMINAL_SNAPSHOT_RESET, UpdateOptions,
|
||||||
UpdateRole, auth_allows, cache_key, cache_server_prefix, cleanup_stream_state,
|
UpdateRole, auth_allows, cache_key, cache_server_prefix, cleanup_stream_state,
|
||||||
clear_cached_credentials, effective_update_artifact_tag, ensure_tui_safe_status_overlay,
|
clear_cached_credentials, effective_update_artifact_tag, ensure_tui_safe_status_overlay,
|
||||||
expand_ssh_path_tokens, input_contains_focus_in, input_matches_escape,
|
expand_ssh_path_tokens, first_resolved_addr, input_contains_focus_in, input_matches_escape,
|
||||||
is_local_status_target, is_resume_response_for_client, latest_release_download_url,
|
is_local_status_target, is_resume_response_for_client, latest_release_download_url,
|
||||||
load_first_native_identity_with_prompt, local_symlink_target_is_dir,
|
load_first_native_identity_with_prompt, local_symlink_target_is_dir,
|
||||||
local_username_from_env, native_proxy_udp_warning, newest_client_trace_path_from,
|
local_username_from_env, native_proxy_udp_warning, newest_client_trace_path_from,
|
||||||
@@ -10937,6 +11086,24 @@ mod tests {
|
|||||||
assert_eq!(ssh_destination_host("alice@example.com"), "example.com");
|
assert_eq!(ssh_destination_host("alice@example.com"), "example.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_resolved_addr_uses_dns_order_and_reports_empty_results() {
|
||||||
|
let addrs = [
|
||||||
|
"127.0.0.1:50000".parse().unwrap(),
|
||||||
|
"[::1]:50000".parse().unwrap(),
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
first_resolved_addr(&addrs, "example.test", 50000).unwrap(),
|
||||||
|
addrs[0]
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
first_resolved_addr(&[], "example.test", 50000)
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string()
|
||||||
|
.contains("no UDP address resolved for example.test:50000")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn selected_udp_host_supports_ssh_auto_and_localhost() {
|
fn selected_udp_host_supports_ssh_auto_and_localhost() {
|
||||||
let ssh = SshConfig {
|
let ssh = SshConfig {
|
||||||
|
|||||||
Reference in New Issue
Block a user