Skip to content

SSH ProxyJump (-J) does not work for file transfers and interactive mode times out #38

@inureyes

Description

@inureyes

Problem Summary

There are two critical issues with the SSH jump host functionality:

  1. ProxyJump (-J/--jump-host) only works for command execution, not for file transfers
  2. Interactive mode times out when using jump hosts

These issues make jump host functionality unusable for many common SSH operations.


Issue 1: File Transfer Commands Don't Support Jump Hosts

Detailed Analysis

Location: src/executor.rs

The upload_to_node() (line 627-664) and download_from_node() (line 666-693) functions create SshClient instances but do not pass jump_hosts parameters:

// Line 627-664: upload_to_node
async fn upload_to_node(
    node: Node,
    local_path: &Path,
    remote_path: &str,
    key_path: Option<&str>,
    strict_mode: StrictHostKeyChecking,
    use_agent: bool,
    use_password: bool,
) -> Result<()> {
    let mut client = SshClient::new(node.host.clone(), node.port, node.username.clone());
    let key_path = key_path.map(Path::new);
    
    // ❌ NO jump_hosts parameter passed!
    client
        .upload_file(local_path, remote_path, key_path, Some(strict_mode), use_agent, use_password)
        .await
}

In contrast, execute_on_node_with_jump_hosts() (line 604-625) properly supports jump hosts:

async fn execute_on_node_with_jump_hosts(
    node: Node,
    command: &str,
    config: &ExecutionConfig<'_>,
) -> Result<CommandResult> {
    let connection_config = ConnectionConfig {
        jump_hosts_spec: config.jump_hosts,  // ✓ Jump hosts properly passed
        // ...
    };
    
    client
        .connect_and_execute_with_jump_hosts(command, &connection_config)
        .await
}

SshClient File Transfer Methods Lack Jump Host Support

Location: src/ssh/client.rs

  • upload_file() (line 44-173)
  • download_file() (line 175-304)
  • upload_dir() (line 306-423)
  • download_dir() (line 425-540)

These methods only call connect_direct(), not connect_via_jump_hosts().

Expected Behavior

# Upload with jump host
bssh -J bastion.example.com -H target.internal upload local.txt /tmp/

# Download with jump host
bssh -J user@bastion:2222 -c production download /etc/config ./backups/

# Multi-hop jump hosts
bssh -J jump1,jump2,jump3 user@target upload app.tar.gz /opt/

Current Behavior

  • ✓ Command execution with -J works correctly
  • ✗ File upload with -J fails (attempts direct connection)
  • ✗ File download with -J fails (attempts direct connection)

Issue 2: Interactive Mode Times Out with Jump Hosts

Problem Description

When using interactive mode with jump hosts, the connection times out and fails to establish. This affects both single-node SSH mode and multiplexed interactive sessions.

Location: src/main.rs (lines 417-479)

Reproduction Steps

# SSH mode interactive (like: ssh -J bastion user@target)
bssh -J bastion.example.com user@target.internal

# Interactive subcommand with jump host
bssh -J bastion.example.com -H target1,target2 interactive

# Multi-hop interactive
bssh -J jump1,jump2 user@target

Expected Behavior

  1. Connection should be established through the jump host chain
  2. Interactive shell should start immediately
  3. User input should be properly forwarded
  4. PTY allocation should work through jump hosts
  5. Session should remain stable without timeouts

Current Behavior

  1. Connection attempt starts
  2. Timeout occurs (likely at 30s connection timeout)
  3. No interactive shell is established
  4. Error message about connection timeout

Root Cause Analysis

Location: src/main.rs (lines 417-479)

The interactive mode initialization doesn't properly integrate jump hosts:

// Line 417-479: SSH mode interactive session
if cli.is_ssh_mode() && command.is_empty() {
    // Interactive mode started, but jump_hosts not properly passed
    let interactive_cmd = InteractiveCommand {
        single_node: true,
        multiplex: false,
        // ...
        key_path,
        use_agent: cli.use_agent,
        use_password: cli.password,
        strict_mode,
        pty_config,
        use_pty,
    };
    // ❌ Missing: jump_hosts parameter in InteractiveCommand
}

Location: src/commands/interactive.rs (check if InteractiveCommand struct has jump_hosts field)

The InteractiveCommand struct likely doesn't have:

  1. jump_hosts field
  2. Logic to pass jump_hosts to SSH connection establishment
  3. Proper timeout handling for multi-hop connections

Potential Issues

  1. Missing jump_hosts field in InteractiveCommand

    • The struct doesn't store jump host specification
    • Can't pass it to SSH client during connection
  2. Connection timeout too short for multi-hop

    • Default 30s timeout may be insufficient for 2+ hops
    • Each hop needs time for: connection + auth + channel setup
    • Suggested: 30s base + 15s per additional hop
  3. PTY allocation through jump hosts

    • PTY must be properly allocated through the final connection
    • Channel forwarding needs to support interactive I/O
  4. Error handling insufficient

    • Timeout errors don't indicate which hop failed
    • No retry logic for transient network issues

Implementation Analysis

Location: src/jump/chain.rs

The JumpHostChain::connect() method (line 229-278) exists and works for command execution. It should be used for interactive mode as well:

pub async fn connect(
    &self,
    destination_host: &str,
    destination_port: u16,
    destination_user: &str,
    dest_auth_method: AuthMethod,
    // ...
) -> Result<JumpConnection>

This method returns a JumpConnection with a working Client that can be used for both command execution and interactive sessions.


Proposed Solution

Phase 1: File Transfer Support (Priority: High)

  1. Modify ExecutionConfig in src/executor.rs

    • Already has jump_hosts: Option<&'a str>
  2. Update upload_to_node() and download_from_node()

    async fn upload_to_node(
        node: Node,
        local_path: &Path,
        remote_path: &str,
        key_path: Option<&str>,
        strict_mode: StrictHostKeyChecking,
        use_agent: bool,
        use_password: bool,
        jump_hosts: Option<&str>,  // ✓ Add this parameter
    ) -> Result<()>
  3. Update SshClient file transfer methods

    • Add jump_hosts_spec to ConnectionConfig
    • Use existing connect_via_jump_hosts() infrastructure

Phase 2: Interactive Mode Support (Priority: High)

  1. Update InteractiveCommand struct

    pub struct InteractiveCommand {
        // existing fields...
        pub jump_hosts: Option<String>,  // ✓ Add this field
    }
  2. Pass jump_hosts to SSH connection in interactive mode

    • Use JumpHostChain::connect() when jump hosts are specified
    • Handle PTY allocation through final connection
    • Properly forward stdin/stdout through jump chain
  3. Improve timeout handling

    // Dynamic timeout based on hop count
    let base_timeout = Duration::from_secs(30);
    let per_hop_timeout = Duration::from_secs(15);
    let total_timeout = base_timeout + (per_hop_timeout * jump_host_count);
  4. Better error messages

    // Instead of: "Connection timeout"
    // Show: "Connection timeout at hop 2 of 3 (bastion2.example.com)"

Phase 3: SSH Config Integration (Priority: Medium)

  1. Parse ProxyJump from SSH config

    • Read ProxyJump directive in SshConfig
    • Merge with CLI -J option (CLI takes precedence)
  2. Auto-detect jump hosts from config

    # ~/.ssh/config
    Host production-*
        ProxyJump bastion.company.com
    
    # This should work automatically:
    bssh production-web1 interactive

Testing Requirements

Unit Tests

  1. Jump host parameter propagation in file transfer
  2. InteractiveCommand with jump_hosts field
  3. Timeout calculation based on hop count
  4. Error message formatting with hop information

Integration Tests

  1. File Transfer Tests:

    • Upload single file through 1 jump host
    • Download through 2 jump hosts
    • Recursive upload/download through jump chain
  2. Interactive Mode Tests:

    • SSH mode interactive through 1 jump host
    • Multi-node interactive through jump chain
    • PTY allocation and terminal size detection
    • Signal handling (Ctrl+C, Ctrl+D)
  3. Timeout Tests:

    • Verify connection succeeds within calculated timeout
    • Verify timeout triggers at correct time
    • Test error messages indicate which hop failed

Manual Testing Scenarios

# Scenario 1: File transfer through bastion
bssh -J bastion upload local.txt target:/tmp/
bssh -J bastion download target:/tmp/remote.txt ./

# Scenario 2: Interactive through multi-hop
bssh -J bastion1,bastion2 user@target

# Scenario 3: Combined operations
bssh -J bastion -H web1,web2 upload app.tar.gz /opt/
bssh -J bastion -H web1,web2 interactive

# Scenario 4: SSH config integration
# (with ProxyJump in ~/.ssh/config)
bssh production-server interactive

Security Considerations

  1. Authentication per hop: Each jump host requires proper authentication
  2. Host key verification: All hosts in chain must pass verification
  3. Connection cleanup: Ensure all hops properly closed on error
  4. Timeout limits: Prevent indefinite hanging connections
  5. Error messages: Don't leak internal network topology in errors

Implementation Architecture

The codebase already has complete jump host infrastructure:

  • src/jump/parser.rs: ProxyJump syntax parsing (OpenSSH compatible)
  • src/jump/chain.rs: Multi-hop connection via JumpHostChain
  • src/jump/connection.rs: Connection lifecycle management
  • src/ssh/client.rs: connect_via_jump_hosts() method exists

The infrastructure is complete, but integration is incomplete.


Priority

Critical Priority - Jump hosts are essential for:

  • Enterprise environments with bastion hosts (security requirement)
  • Compliance requirements (SOC2, PCI-DSS, HIPAA)
  • Network-isolated environments
  • Multi-region deployments

Interactive mode is a core SSH functionality that users expect to work seamlessly with jump hosts.


Related Files

  • src/executor.rs (lines 604-693) - File transfer functions
  • src/ssh/client.rs (lines 44-540, 256-297) - SSH client methods
  • src/main.rs (lines 183-207, 417-523) - Interactive mode initialization
  • src/commands/interactive.rs - InteractiveCommand implementation
  • src/jump/chain.rs - Jump host connection chain (complete)
  • src/jump/parser.rs - Jump host parsing (complete)
  • src/commands/exec.rs (line 197) - Working example

Additional Context

The working command execution implementation in execute_on_node_with_jump_hosts() provides a clear template for fixing both issues. The main work is:

  1. Propagating jump_hosts parameter through function calls
  2. Adding jump_hosts field to InteractiveCommand
  3. Adjusting timeouts for multi-hop scenarios
  4. Testing and error handling improvements

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions