Skip to content

Commit abc65c5

Browse files
authored
fix: Add password fallback and improve SSH debugging for compatibility (#80)
* fix: Add password fallback and improve SSH debugging for compatibility This commit addresses SSH connection compatibility issues where bssh fails to connect to servers that work with OpenSSH. Changes: - Add password fallback when key authentication fails (matches OpenSSH behavior) - Support RUST_LOG environment variable for debugging - Include russh library logs with -vv flag for SSH troubleshooting - Auto-detect SSH agent when available (without --use-agent flag) - Show module targets in debug logs for better diagnostics The password fallback is automatically triggered in interactive mode when publickey authentication is rejected by the server, prompting for password input just like OpenSSH does. Closes #79 * refactor: Improve log levels and error message consistency - Change SSH agent auto-detection log from info to debug level - Change password fallback attempt log from info to debug level - Unify error messages for connection failures (consistent format)
1 parent 868c9be commit abc65c5

File tree

3 files changed

+88
-13
lines changed

3 files changed

+88
-13
lines changed

src/commands/interactive/connection.rs

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,31 @@ use russh::client::Msg;
2020
use russh::Channel;
2121
use std::io::{self, Write};
2222
use tokio::time::{timeout, Duration};
23+
use zeroize::Zeroizing;
2324

2425
use crate::jump::{parse_jump_hosts, JumpHostChain};
2526
use crate::node::Node;
2627
use crate::ssh::{
2728
known_hosts::get_check_method,
28-
tokio_client::{AuthMethod, Client, ServerCheckMethod},
29+
tokio_client::{AuthMethod, Client, Error as SshError, ServerCheckMethod},
2930
};
3031

3132
use super::types::{InteractiveCommand, NodeSession};
3233

3334
impl InteractiveCommand {
3435
/// Helper function to establish SSH connection with proper error handling and rate limiting
3536
/// This eliminates code duplication across different connection paths and prevents brute-force attacks
37+
///
38+
/// If `allow_password_fallback` is true and key authentication fails, it will prompt for password
39+
/// and retry with password authentication (matching OpenSSH behavior).
3640
async fn establish_connection(
3741
addr: (&str, u16),
3842
username: &str,
3943
auth_method: AuthMethod,
4044
check_method: ServerCheckMethod,
4145
host: &str,
4246
port: u16,
47+
allow_password_fallback: bool,
4348
) -> Result<Client> {
4449
const SSH_CONNECT_TIMEOUT_SECS: u64 = 30;
4550
let connect_timeout = Duration::from_secs(SSH_CONNECT_TIMEOUT_SECS);
@@ -56,15 +61,47 @@ impl InteractiveCommand {
5661

5762
let result = timeout(
5863
connect_timeout,
59-
Client::connect(addr, username, auth_method, check_method),
64+
Client::connect(addr, username, auth_method, check_method.clone()),
6065
)
6166
.await
6267
.with_context(|| {
6368
format!(
6469
"Connection timeout: Failed to connect to {host}:{port} after {SSH_CONNECT_TIMEOUT_SECS} seconds"
6570
)
66-
})?
67-
.with_context(|| format!("SSH connection failed to {host}:{port}"));
71+
})?;
72+
73+
// Check if key authentication failed and password fallback is allowed
74+
let result = match result {
75+
Err(SshError::KeyAuthFailed)
76+
if allow_password_fallback && atty::is(atty::Stream::Stdin) =>
77+
{
78+
tracing::debug!(
79+
"SSH key authentication failed for {username}@{host}:{port}, attempting password fallback"
80+
);
81+
82+
// Prompt for password (matching OpenSSH behavior)
83+
let password = Self::prompt_password(username, host).await?;
84+
85+
// Retry with password authentication
86+
let password_auth = AuthMethod::with_password(&password);
87+
88+
// Small delay before retry to prevent rapid attempts
89+
tokio::time::sleep(Duration::from_millis(500)).await;
90+
91+
timeout(
92+
connect_timeout,
93+
Client::connect(addr, username, password_auth, check_method),
94+
)
95+
.await
96+
.with_context(|| {
97+
format!(
98+
"Connection timeout: Failed to connect to {host}:{port} after {SSH_CONNECT_TIMEOUT_SECS} seconds"
99+
)
100+
})?
101+
.with_context(|| format!("SSH connection failed to {host}:{port}"))
102+
}
103+
other => other.with_context(|| format!("SSH connection failed to {host}:{port}")),
104+
};
68105

69106
// SECURITY: Normalize timing to prevent timing attacks
70107
// Ensure all authentication attempts take at least 500ms to complete
@@ -79,6 +116,22 @@ impl InteractiveCommand {
79116
result
80117
}
81118

119+
/// Prompt for password with secure handling
120+
async fn prompt_password(username: &str, host: &str) -> Result<Zeroizing<String>> {
121+
let username = username.to_string();
122+
let host = host.to_string();
123+
124+
tokio::task::spawn_blocking(move || {
125+
let password = Zeroizing::new(
126+
rpassword::prompt_password(format!("{username}@{host}'s password: "))
127+
.with_context(|| "Failed to read password")?,
128+
);
129+
Ok(password)
130+
})
131+
.await
132+
.with_context(|| "Password prompt task failed")?
133+
}
134+
82135
/// Determine authentication method based on node and config (same logic as exec mode)
83136
pub(super) async fn determine_auth_method(&self, node: &Node) -> Result<AuthMethod> {
84137
// Use centralized authentication logic from auth module
@@ -164,13 +217,15 @@ impl InteractiveCommand {
164217
tracing::debug!("No valid jump hosts found, using direct connection");
165218

166219
// Use the helper function to establish connection
220+
// Enable password fallback for interactive mode (matches OpenSSH behavior)
167221
Self::establish_connection(
168222
addr,
169223
&node.username,
170224
auth_method.clone(),
171225
check_method.clone(),
172226
&node.host,
173227
node.port,
228+
!self.use_password, // Allow fallback unless explicit password mode
174229
)
175230
.await?
176231
} else {
@@ -239,13 +294,15 @@ impl InteractiveCommand {
239294
tracing::debug!("Using direct connection (no jump hosts)");
240295

241296
// Use the helper function to establish connection
297+
// Enable password fallback for interactive mode (matches OpenSSH behavior)
242298
Self::establish_connection(
243299
addr,
244300
&node.username,
245301
auth_method,
246302
check_method,
247303
&node.host,
248304
node.port,
305+
!self.use_password, // Allow fallback unless explicit password mode
249306
)
250307
.await?
251308
};
@@ -300,13 +357,15 @@ impl InteractiveCommand {
300357
tracing::debug!("No valid jump hosts found, using direct connection for PTY");
301358

302359
// Use the helper function to establish connection
360+
// Enable password fallback for interactive mode (matches OpenSSH behavior)
303361
Self::establish_connection(
304362
addr,
305363
&node.username,
306364
auth_method.clone(),
307365
check_method.clone(),
308366
&node.host,
309367
node.port,
368+
!self.use_password, // Allow fallback unless explicit password mode
310369
)
311370
.await?
312371
} else {
@@ -375,13 +434,15 @@ impl InteractiveCommand {
375434
tracing::debug!("Using direct connection for PTY (no jump hosts)");
376435

377436
// Use the helper function to establish connection
437+
// Enable password fallback for interactive mode (matches OpenSSH behavior)
378438
Self::establish_connection(
379439
addr,
380440
&node.username,
381441
auth_method,
382442
check_method,
383443
&node.host,
384444
node.port,
445+
!self.use_password, // Allow fallback unless explicit password mode
385446
)
386447
.await?
387448
};

src/ssh/auth.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,15 +218,20 @@ impl AuthContext {
218218
}
219219
}
220220

221-
// Priority 3: Key file authentication
221+
// Priority 3: Key file authentication (explicit -i flag)
222222
if let Some(ref key_path) = self.key_path {
223223
return self.key_file_auth(key_path).await;
224224
}
225225

226-
// Priority 4: SSH agent auto-detection (if use_agent is true)
226+
// Priority 4: SSH agent auto-detection (like OpenSSH behavior)
227+
// OpenSSH tries SSH agent first when available, as it can try all registered keys
227228
#[cfg(not(target_os = "windows"))]
228-
if self.use_agent {
229+
if !self.use_agent {
230+
// Auto-detect SSH agent even without --use-agent flag
229231
if let Some(auth) = self.agent_auth()? {
232+
tracing::debug!(
233+
"Using SSH agent (auto-detected) - agent will try all registered keys"
234+
);
230235
return Ok(auth);
231236
}
232237
}

src/utils/logging.rs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,24 @@
1515
use tracing_subscriber::EnvFilter;
1616

1717
pub fn init_logging(verbosity: u8) {
18-
let filter = match verbosity {
19-
0 => EnvFilter::new("bssh=warn"),
20-
1 => EnvFilter::new("bssh=info"),
21-
2 => EnvFilter::new("bssh=debug"),
22-
_ => EnvFilter::new("bssh=trace"),
18+
// Priority: RUST_LOG environment variable > verbosity flag
19+
let filter = if std::env::var("RUST_LOG").is_ok() {
20+
// Use RUST_LOG if set (allows debugging russh and other dependencies)
21+
EnvFilter::from_default_env()
22+
} else {
23+
// Fall back to verbosity-based filter
24+
match verbosity {
25+
0 => EnvFilter::new("bssh=warn"),
26+
1 => EnvFilter::new("bssh=info"),
27+
// -vv: Include russh debug logs for SSH troubleshooting
28+
2 => EnvFilter::new("bssh=debug,russh=debug"),
29+
// -vvv: Full trace including all dependencies
30+
_ => EnvFilter::new("bssh=trace,russh=trace,russh_sftp=debug"),
31+
}
2332
};
2433

2534
tracing_subscriber::fmt()
2635
.with_env_filter(filter)
27-
.with_target(false)
36+
.with_target(true) // Show module targets for better debugging
2837
.init();
2938
}

0 commit comments

Comments
 (0)