SSH Client Module Structure :
client/core.rs- Client struct and core functionality (44 lines)client/connection.rs- Connection establishment and management (308 lines)client/command.rs- Command execution logic (155 lines)client/file_transfer.rs- SFTP operations (691 lines)client/config.rs- Configuration types (27 lines)client/result.rs- Result types and implementations (86 lines)
Tokio Client Module Structure :
tokio_client/connection.rs- Connection management (293 lines)tokio_client/authentication.rs- Authentication methods (378 lines)tokio_client/channel_manager.rs- Channel operations (230 lines)tokio_client/file_transfer.rs- SFTP file operations (285 lines)
Library Choice: russh and russh-sftp
- Native Rust SSH implementation with full async support
- SFTP support via russh-sftp for file operations
- Custom tokio_client wrapper providing high-level API
- Better control over SSH protocol implementation
Implementation Details:
- Custom tokio_client wrapper for simplified API
- Support for SSH agent, key-based, and password authentication
- Configurable timeouts and retry logic
- Full SFTP support for file transfers
- SSH keepalive support via
SshConnectionConfig:keepalive_interval: Interval between keepalive packets (default: 60s, 0 to disable)keepalive_max: Maximum unanswered keepalive packets before disconnect (default: 3)- Equivalent to OpenSSH
ServerAliveIntervalandServerAliveCountMax
Security Implementation:
- Host key verification with three modes:
StrictHostKeyChecking::Yes- Strict verification using known_hostsStrictHostKeyChecking::No- Skip all verificationStrictHostKeyChecking::AcceptNew- TOFU mode- CLI flag
--strict-host-key-checkingwith default "accept-new" - Uses system known_hosts file (~/.ssh/known_hosts)
- SSH agent authentication with auto-detection
Status: Implemented as part of Phase 1
Design Motivation: Real-time command output streaming enables future UI features such as live progress bars, per-node output display, and streaming aggregation. The infrastructure provides the foundation for responsive UIs while maintaining full backward compatibility with existing synchronous APIs.
Architecture:
The streaming infrastructure consists of three key components:
- CommandOutput Enum (
tokio_client/channel_manager.rs)
pub enum CommandOutput {
StdOut(CryptoVec),
StdErr(CryptoVec),
}- Represents streaming output events
- Separates stdout and stderr streams
- Uses russh's
CryptoVecfor zero-copy efficiency
- CommandOutputBuffer (
tokio_client/channel_manager.rs)
pub(crate) struct CommandOutputBuffer {
sender: Sender<CommandOutput>,
receiver_task: JoinHandle<(Vec<u8>, Vec<u8>)>,
}- Internal buffer for collecting streaming output
- Background task aggregates stdout and stderr
- Channel capacity: 100 events (tunable)
- Used by synchronous
executefor backward compatibility
- Streaming API Methods
Client::execute_streaming(command, sender)- Low-level streaming APISshClient::connect_and_execute_with_output_streaming- High-level streaming API- Both respect timeout settings and handle errors consistently
Implementation Pattern:
// Streaming execution (new in Phase 1)
let (sender, receiver_task) = build_output_buffer;
let exit_status = client.execute_streaming("command", sender).await?;
let (stdout, stderr) = receiver_task.await?;
// Backward-compatible execution (refactored to use streaming)
let result = client.execute("command").await?;
// Internally uses execute_streaming + CommandOutputBufferBackward Compatibility:
The existing execute method was refactored to use execute_streaming internally:
- Same function signature
- Same return type (
CommandExecutedResult) - Same error handling behavior
- Same timeout behavior
- Zero breaking changes to existing code
Performance Characteristics:
- Channel-based architecture with bounded buffer (100 events)
- Zero-copy transfer of SSH channel data via
CryptoVec - Background task for output aggregation (non-blocking)
- Memory overhead: ~16KB per streaming command (8KB stdout + 1KB stderr + buffer)
- Latency: Real-time streaming with minimal buffering delay
Error Handling:
- New
JoinErrorvariant intokio_client::Error - Handles task join failures gracefully
- Timeout handling preserved from original implementation
- Channel send errors handled silently (receiver may be dropped)
Testing:
- Integration tests cover streaming with stdout/stderr separation
- Backward compatibility test ensures no behavioral changes
- Tests use localhost SSH for reproducible validation
- All existing tests pass with zero modifications
Future Phases :
Phase 2: Executor integration for parallel streaming✓ Completed (2025-10-29)- Phase 3: UI components (progress bars, live updates)
- Phase 4: Advanced features (filtering, aggregation)
Status: Implemented as part of Phase 2
Design Motivation: Building on Phase 1's streaming infrastructure, Phase 2 adds independent stream management for multiple nodes and flexible output modes. This enables real-time monitoring of parallel command execution across clusters while maintaining full backward compatibility.
Architecture:
The Phase 2 implementation consists of four key components:
- NodeStream (
executor/stream_manager.rs)
pub struct NodeStream {
pub node: Node,
receiver: mpsc::Receiver<CommandOutput>,
stdout_buffer: Vec<u8>,
stderr_buffer: Vec<u8>,
status: ExecutionStatus,
exit_code: Option<u32>,
closed: bool,
}- Independent output stream for each node
- Non-blocking polling of command output
- Separate buffers for stdout and stderr
- Tracks execution status and exit codes
- Can consume buffers incrementally for streaming
- MultiNodeStreamManager (
executor/stream_manager.rs)
pub struct MultiNodeStreamManager {
streams: Vec<NodeStream>,
}- Coordinates multiple node streams
- Non-blocking poll of all streams
- Tracks completion status
- Provides access to all stream states
- OutputMode (
executor/output_mode.rs)
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum OutputMode {
#[default]
Normal, // Traditional batch mode
Stream, // Real-time with [node] prefixes
File(PathBuf), // Save to per-node files
}- Three distinct output modes
- TTY detection for automatic mode selection
- Priority:
--output-dir>--stream> default
- CLI Integration (
cli.rs)
--streamflag: Enable real-time streaming output--output-dir <DIR>: Save per-node output to files- Auto-detection of non-TTY environments (pipes, CI)
Implementation Details:
Streaming Execution Flow:
// In ParallelExecutor::execute_with_streaming
1. Create MultiNodeStreamManager
2. Spawn task per node with streaming sender
3. Poll all streams in loop:
- Extract new output from each stream
- Process based on output mode:
* Stream: Print with [node] prefix
* File: Buffer until completion
* Normal: Use traditional execute
4. Wait for all tasks to complete
5. Collect and return ExecutionResultsStream Mode Output:
[host1] Starting process...
[host2] Starting process...
[host1] Processing data...
[host2] Processing data...
[host1] Complete
[host2] Complete
File Mode Output:
Output directory: ./results/
host1_20251029_143022.stdout
host1_20251029_143022.stderr
host2_20251029_143022.stdout
host2_20251029_143022.stderr
Backward Compatibility:
Phase 2 maintains full backward compatibility:
- Without
--streamor--output-dir, uses traditionalexecutemethod - Existing CLI behavior unchanged
- All 396 existing tests pass without modification
- Exit code strategy and error handling preserved
Performance Characteristics:
-
Stream Mode:
-
50ms polling interval for smooth output
-
Minimal memory: only buffered lines in flight
-
Real-time latency: <100ms from node to display
-
File Mode:
-
Buffers entire output in memory
-
Async file writes (non-blocking)
-
Timestamped filenames prevent collisions
TTY Detection:
- Auto-detects piped output (
stdout.is_terminal) - Checks CI environment variables (CI, GITHUB_ACTIONS, etc.)
- Respects NO_COLOR convention
- Falls back gracefully when colors unavailable
Error Handling:
- Per-node failure tracking with ExecutionStatus
- Failed nodes still report in stream/file modes
- Exit code calculation respects user-specified strategy
- Graceful handling of channel closures
Testing:
- 10 unit tests for stream management
- 3 unit tests for output mode selection
- TTY detection tests
- All existing integration tests pass
- Total test coverage: 396 tests passing
Code Organization:
src/executor/
├── stream_manager.rs # NodeStream, MultiNodeStreamManager (252 lines)
├── output_mode.rs # OutputMode enum, TTY detection (171 lines)
├── parallel.rs # Updated with streaming methods (+264 lines)
└── mod.rs # Exports for new types
Usage Examples:
Stream Mode:
# Real-time streaming output
bssh -C production --stream "tail -f /var/log/app.log"
# With filtering
bssh -H "web*" --stream "systemctl status nginx"File Mode:
# Save outputs to directory
bssh -C cluster --output-dir ./results "ps aux"
# Each node gets separate files with timestamps
ls ./results/
# web1_20251029_143022.stdout
# web2_20251029_143022.stdoutFuture Enhancements:
Phase 3: UI components (progress bars, spinners)✅ Implemented (see Phase 3 below)- Phase 4: Advanced filtering and aggregation
- Potential: Colored output per node
- Potential: Interactive stream control (pause/resume)
Status: Implemented as part of Phase 3
Design Motivation: Phase 3 builds on the streaming infrastructure from Phase 1 and multi-node management from Phase 2 to provide a rich interactive Terminal User Interface (TUI) for monitoring parallel SSH command execution. The TUI automatically activates in interactive terminals and provides multiple view modes optimized for different monitoring needs.
Architecture:
The Phase 3 implementation introduces a complete TUI system built with ratatui and crossterm:
src/ui/tui/
├── mod.rs # TUI entry point, event loop, terminal management
├── app.rs # TuiApp state management
├── event.rs # Keyboard event handling
├── progress.rs # Progress parsing utilities
├── terminal_guard.rs # RAII terminal cleanup guards
└── views/
├── mod.rs
├── summary.rs # Summary view (all nodes)
├── detail.rs # Detail view (single node with scrolling)
├── split.rs # Split view (2-4 nodes simultaneously)
└── diff.rs # Diff view (compare two nodes)
- TuiApp State (
app.rs)
pub struct TuiApp {
pub view_mode: ViewMode,
pub scroll_positions: HashMap<usize, usize>, // Per-node scroll
pub follow_mode: bool, // Auto-scroll
pub should_quit: bool,
pub show_help: bool,
needs_redraw: bool, // Conditional rendering
last_data_sizes: Vec<usize>, // Change detection
}
pub enum ViewMode {
Summary, // All nodes status
Detail(usize), // Single node full output
Split(Vec<usize>), // 2-4 nodes side-by-side
Diff(usize, usize), // Compare two nodes
}- Manages current view mode and transitions
- Tracks per-node scroll positions (preserved across view switches)
- Auto-scroll (follow mode) with manual override detection
- Conditional rendering to reduce CPU usage (80-90% reduction)
- View Modes:
Summary View:
- Displays all nodes with status icons (⊙ pending, ⟳ running, ✓ completed, ✗ failed)
- Real-time progress bars extracted from command output
- Quick navigation keys (1-9, s, d, q, ?)
- Compact representation for up to hundreds of nodes
Detail View:
- Full output from a single node
- Scrolling support: ↑/↓, PgUp/PgDn, Home/End
- Auto-scroll mode (f key) with manual override
- Separate stderr display in red color
- Node switching with ←/→ or number keys
- Scroll position preserved when switching nodes
Split View:
- Monitor 2-4 nodes simultaneously in grid layout
- Automatic layout adjustment (1x2 or 2x2)
- Color-coded borders by node status
- Last N lines displayed per pane
- Focus switching between panes
Diff View:
- Side-by-side comparison of two nodes
- Highlights output differences
- Useful for debugging inconsistencies across nodes
- Progress Parsing (
progress.rs)
lazy_static! {
static ref PERCENT_PATTERN: Regex = Regex::new(r"(\d+)%").unwrap;
static ref FRACTION_PATTERN: Regex = Regex::new(r"(\d+)/(\d+)").unwrap;
}
pub fn parse_progress(text: &str) -> Option<f32>- Detects percentage patterns: "78%", "Progress: 78%"
- Detects fraction patterns: "45/100", "23 of 100"
- Special handling for apt/dpkg output
- Input length limits to prevent regex DoS (max 1000 chars)
- Returns progress as 0.0-100.0 float
- Terminal Safety (
terminal_guard.rs)
pub struct RawModeGuard { enabled: bool }
pub struct AlternateScreenGuard { /* ... */ }- RAII-style guards ensure terminal cleanup on panic
- Automatic restoration of terminal state on exit
- Prevents terminal corruption from crashes
- Guaranteed cleanup via Drop trait implementation
- Event Loop (
mod.rs)
pub async fn run(
manager: &mut MultiNodeStreamManager,
cluster_name: &str,
command: &str) -> Result<Vec<ExecutionResult>>- 50ms polling interval for responsive UI
- Non-blocking SSH execution continues independently
- Conditional rendering (only when data changes)
- Keyboard event handling with crossterm
- Proper cleanup on exit or Ctrl+C
Event Loop Flow:
loop {
// 1. Poll all node streams (non-blocking)
manager.poll_all.await;
// 2. Detect changes
if data_changed || user_input {
app.needs_redraw = true;
}
// 3. Render UI (conditional)
if app.needs_redraw {
terminal.draw(|f| {
match app.view_mode {
ViewMode::Summary => render_summary(f, manager),
ViewMode::Detail(idx) => render_detail(f, &manager.streams[idx]),
ViewMode::Split(indices) => render_split(f, manager, &indices),
ViewMode::Diff(a, b) => render_diff(f, &streams[a], &streams[b]),
}
})?;
app.needs_redraw = false;
}
// 4. Handle keyboard input (50ms poll)
if event::poll(Duration::from_millis(50))? {
if let Event::Key(key) = event::read? {
app.handle_key_event(key, num_nodes);
}
}
// 5. Check exit conditions
if app.should_quit || all_completed(manager) {
break;
}
}Auto-Detection Logic:
let output_mode = OutputMode::from_cli_and_env(
cli.stream,
cli.output_dir.clone,
is_tty);
// Priority: --output-dir > --stream > TUI (if TTY) > Normal
match output_mode {
OutputMode::Tui => ui::tui::run(manager, cluster, cmd).await?,
OutputMode::Stream => handle_stream_mode(manager, cmd).await?,
OutputMode::File(dir) => handle_file_mode(manager, cmd, dir).await?,
OutputMode::Normal => execute_normal(nodes, cmd).await?,
}Security Features:
- Terminal Corruption Prevention:
- RAII guards guarantee terminal restoration
- Panic detection with extra recovery attempts
- Force terminal reset sequence on panic
- Scroll Boundary Validation:
- Comprehensive bounds checking prevents crashes
- Safe handling of empty output
- Terminal resize resilience
- Memory Protection:
- HashMap size limits (100 entries max for scroll_positions)
- Automatic eviction of oldest entries
- Uses Phase 2's RollingBuffer (10MB per node)
- Regex DoS Protection:
- Input length limits (1000 chars max)
- Simple, non-backtracking regex patterns
- No user-controlled regex patterns
Performance Characteristics:
- CPU Usage: <10% during idle (reduced by 80-90% via conditional rendering)
- Memory: ~16KB per node + UI overhead (~1MB)
- Latency: <100ms from output to display
- Rendering: Only when data changes or user input
- Terminal Size: Minimum 40x10, validated at startup
Keyboard Controls:
| Key | Action |
|---|---|
1-9 |
Jump to node detail view |
s |
Enter split view mode |
d |
Enter diff view mode |
f |
Toggle auto-scroll (follow mode) |
? |
Show help overlay |
Esc |
Return to previous view |
q |
Quit |
↑/↓ |
Scroll up/down in detail view |
←/→ |
Switch between nodes |
PgUp/PgDn |
Page scroll |
Home/End |
Jump to top/bottom |
Integration with Executor:
// In ParallelExecutor::handle_tui_mode
1. Create MultiNodeStreamManager
2. Spawn streaming task per node
3. Launch TUI with manager
4. TUI polls streams in event loop
5. Return ExecutionResults after TUI exitsBackward Compatibility:
- TUI only activates in interactive terminals (TTY detected)
- Automatically disabled in pipes, redirects, CI environments
- Existing flags (
--stream,--output-dir) disable TUI - All previous modes work identically
Testing:
- 20 unit tests added (app state, event handling, progress parsing)
- Terminal cleanup tested with panic scenarios
- Scroll boundary validation tests
- Memory limit enforcement tests
- All 417 tests passing (397 existing + 20 new)
Dependencies Added:
ratatui = "0.29" # Terminal UI framework
regex = "1" # Progress parsing
lazy_static = "1.5" # Regex compilation optimizationFuture Enhancements:
- Configuration file for custom keybindings
- Output filtering/search within TUI
- Mouse support for clickable UI
- Session recording and replay
- Color themes and customization
Status: Implemented as part of code deduplication refactoring
Design Motivation:
Authentication logic was previously duplicated across multiple modules (ssh/client.rs and commands/interactive.rs) with ~90% code duplication. This created maintenance burden and potential for bugs when fixing authentication issues in one location but not the other.
Refactoring Goals:
- Eliminate ~15% code duplication across codebase
- Provide single source of truth for authentication
- Maintain consistent authentication behavior across all commands
- Improve testability with centralized tests
- Reduce maintenance cost for authentication logic
Implementation:
The AuthContext struct encapsulates all authentication parameters and provides a single determine_method function that implements the standard authentication priority:
pub struct AuthContext {
pub key_path: Option<PathBuf>,
pub use_agent: bool,
pub use_password: bool,
pub username: String,
pub host: String,
}
impl AuthContext {
pub fn determine_method(&self) -> Result<AuthMethod> {
// Priority 1: Password authentication (if explicitly requested)
// Priority 2: SSH agent (if explicitly requested and available)
// Priority 3: Specified key file (if provided)
// Priority 4: SSH agent auto-detection (if use_agent is true)
// Priority 5: Default key locations (~/.ssh/id_ed25519, ~/.ssh/id_rsa, etc.)
}
}Builder Pattern Integration: The context uses a fluent builder pattern for ergonomic configuration:
let auth_ctx = AuthContext::new(username, host)
.with_key_path(key_path.map(|p| p.to_path_buf))
.with_agent(use_agent)
.with_password(use_password);
let auth_method = auth_ctx.determine_method?;Security Features:
- Uses
zeroizecrate to clear passwords and passphrases from memory - Secure passphrase prompts via
rpasswordcrate - No credential caching or storage
- Platform-specific handling (SSH agent not supported on Windows)
Code Reduction:
- Eliminated ~130 lines of duplicated authentication logic
- Reduced from 2 implementations to 1 canonical implementation
- Client modules reduced from ~140 lines to ~10 lines for authentication
Testing: Comprehensive test coverage including:
- Key file authentication
- SSH agent authentication (Unix only)
- Password authentication (manual test only)
- Default key location fallback
- Error conditions and edge cases
Usage in Codebase:
ssh/client.rs: UsesAuthContextfor all SSH operations
fn determine_auth_method(&self, ...) -> Result<AuthMethod> {
let auth_ctx = super::auth::AuthContext::new(...)
.with_key_path(...)
.with_agent(...)
.with_password(...);
auth_ctx.determine_method
}commands/interactive.rs: UsesAuthContextfor interactive sessions
fn determine_auth_method(&self, node: &Node) -> Result<AuthMethod> {
let auth_ctx = crate::ssh::AuthContext::new(...)
.with_key_path(...)
.with_agent(...)
.with_password(...);
auth_ctx.determine_method
}Benefits Realized:
- Single source of truth for authentication logic
- Easier to add new authentication methods
- Consistent behavior across all bssh commands
Status: Implemented
Overview:
The sudo password module provides secure handling of sudo authentication for commands that require elevated privileges. When enabled with the -S flag, bssh automatically detects sudo password prompts in command output and injects the password without user intervention.
Architecture Components:
- SudoPassword Struct (
security/sudo.rs)
- Wraps password string with automatic memory clearing via
zeroizecrate - Uses
Arcfor safe sharing across async tasks - Debug output redacts password content
#[derive(Clone, ZeroizeOnDrop)]
pub struct SudoPassword {
inner: Arc<SudoPasswordInner>,
}- Prompt Detection Patterns
- Case-insensitive matching against common sudo prompts
- Supports various Linux distributions:
[sudo] password for <user>:Password:<user>'s password:- Also detects failure patterns like "Sorry, try again"
- Password Injection Flow
Command Execution
|
+--> PTY Channel Opened (required for sudo interaction)
| |
| Output Monitoring
| |
| [Sudo Prompt Detected?] -- No --> Continue
| |Yes
| Send Password + Newline
| |
+--- Continue Monitoring
Implementation Details:
// Prompt detection patterns
pub const SUDO_PROMPT_PATTERNS: &[&str] = &[
"[sudo] password for ",
"password for ",
"password:",
"'s password:",
"sudo password",
"enter password",
"[sudo]",
];
// Failure detection patterns
pub const SUDO_FAILURE_PATTERNS: &[&str] = &[
"sorry, try again",
"incorrect password",
"authentication failure",
"permission denied",
];SSH Channel Integration (tokio_client/channel_manager.rs):
- Executes command with PTY allocation (required for sudo to send prompts)
- Monitors both stdout and stderr for sudo prompts
- Uses
channel.datato write password to stdin when prompt detected - Password sent only once per execution to prevent retry loops
pub async fn execute_with_sudo(
&self,
command: &str,
sender: Sender<CommandOutput>,
sudo_password: &SudoPassword) -> Result<u32, Error>Security Considerations:
- Password stored using
zeroizecrate for automatic memory clearing - Password never logged or printed in any output
- PTY required for proper sudo interaction (prevents stdin echo issues)
- Environment variable option (
BSSH_SUDO_PASSWORD) with security warnings
Execution Path Integration:
- CLI flag
-S/--sudo-passwordtriggers password prompt - Password wrapped in
Arc<SudoPassword>for sharing across nodes ExecutionConfigcarries optionalsudo_passwordfield- Both streaming and non-streaming execution paths support sudo
- Per-node execution uses
execute_with_sudowhen password present
Usage Patterns:
# Basic usage - prompts for password before execution
bssh -S -C production "sudo apt update"
# Combined with SSH agent authentication
bssh -A -S -C production "sudo systemctl restart nginx"
# Environment variable (not recommended)
export BSSH_SUDO_PASSWORD="password"
bssh -S -C production "sudo apt update"Limitations:
- Single password for all nodes (cannot handle different passwords per node)
- Assumes all nodes use the same sudo configuration
- Password cached for session duration (cleared on command completion)
Future Enhancements:
- Support for additional authentication methods (hardware tokens, certificates)
- Credential caching with secure storage integration
- Multi-factor authentication support
- Per-host authentication preferences
Current Status: Placeholder implementation
Design Decision: After thorough analysis, connection pooling was determined to be not beneficial for bssh's current usage pattern. The implementation exists as a placeholder for future features.
Analysis Results:
- Current Usage Pattern: Each CLI invocation executes exactly one operation per host then terminates
- No Reuse Scenarios: There are no cases where connections would be reused within a single bssh execution
- Library Limitation: russh sessions are not reusable across operations
- Performance Impact: Zero benefit for current one-shot command execution model
When Pooling Would Be Beneficial:
- Interactive mode with persistent shell sessions
- Watch mode for periodic command execution
- Server mode providing an HTTP API
- Batch command execution from files
- Command pipelining on the same hosts
Implementation:
pub struct ConnectionPool {
_connections: Arc<RwLock<Vec<ConnectionKey>>>, // Placeholder
ttl: Duration,
enabled: bool,
max_connections: usize,
}Current Behavior:
- Always creates new connections regardless of
enabledflag - Provides API surface for future pooling implementation
- No performance overhead when disabled (default)
Recommendation: Focus on more impactful optimizations like:
- Connection timeout tuning
- SSH compression for large outputs
- Buffered I/O optimizations
- Early termination on critical failures
- Parallel DNS resolution
Related Documentation: