Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ SSH server implementation using the russh library for accepting incoming connect
- `session.rs` - Session state management (`SessionManager`, `SessionInfo`, `ChannelState`)
- `exec.rs` - Command execution for SSH exec requests
- `sftp.rs` - SFTP subsystem handler with path traversal prevention
- `scp.rs` - SCP protocol handler with sink/source modes
- `auth/` - Authentication provider infrastructure
- `audit/` - Audit logging infrastructure (event types, exporters, manager)

Expand Down Expand Up @@ -499,6 +500,22 @@ SSH server implementation using the russh library for accepting incoming connect
- Handle limit enforcement to prevent resource exhaustion
- Read size capping to prevent memory exhaustion

- **ScpHandler**: SCP protocol handler (`src/server/scp.rs`)
- Implements SCP server protocol for file transfers via the `scp` command
- Sink mode (`-t` flag): receives files from client (upload)
- Source mode (`-f` flag): sends files to client (download)
- Recursive transfer support (`-r` flag) for directories
- Time preservation (`-p` flag) for file modification times
- Security features:
- Path traversal prevention with normalized path resolution
- Symlink escape prevention via canonicalization
- Filename validation (rejects `/`, `..`, `.`)
- File size limit (10 GB maximum)
- Mode permission masking (strips setuid/setgid/sticky bits)
- Line length limits to prevent DoS via buffer exhaustion
- Automatic SCP command detection in exec_request handler
- Configurable via `scp_enabled` setting

### Server Authentication Module

The authentication subsystem (`src/server/auth/`) provides extensible authentication for the SSH server:
Expand Down
3 changes: 2 additions & 1 deletion docs/architecture/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ bssh is a high-performance parallel SSH command execution tool with SSH-compatib

### Server Components

- **[Server Configuration](./server-configuration.md)** - YAML-based server configuration, environment overrides, validation
- **[Server Configuration](./server-configuration.md)** - YAML-based server configuration, environment overrides, validation, SCP protocol handler
- **Server CLI (`bssh-server`)** - Server management commands including host key generation, password hashing, config validation (see main ARCHITECTURE.md)
- **SSH Server Module** - SSH server implementation using russh (see main ARCHITECTURE.md)
- **Server Authentication** - Authentication providers including public key verification (see main ARCHITECTURE.md)
- **SFTP Handler** - SFTP subsystem with path traversal prevention and chroot-like isolation (see main ARCHITECTURE.md)
- **SCP Handler** - SCP protocol with sink/source modes and security controls (see main ARCHITECTURE.md)
- **Audit Logging** - Audit event types, exporters, and async event processing (see main ARCHITECTURE.md)

## Navigation
Expand Down
114 changes: 114 additions & 0 deletions docs/architecture/server-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,120 @@ SSH Client Request Flow:
└───────────────────┘ └─────────────────┘ └──────────────┘
```

## SCP Protocol Handler

The bssh-server supports file transfers via the SCP (Secure Copy Protocol) command. Unlike SFTP which uses a dedicated subsystem, SCP operates through SSH exec requests.

### Protocol Overview

SCP is not a standalone protocol but a command-line tool that communicates over SSH. When a client runs `scp file user@host:path`:
1. The SSH client establishes a connection to the server
2. The server receives an exec request for `scp -t path` (upload) or `scp -f path` (download)
3. The server spawns the SCP handler to manage the file transfer

### Operation Modes

**Sink Mode (`-t` flag)**: Server receives files from client (upload)
```bash
# Client uploads file.txt to server's /tmp directory
scp file.txt user@server:/tmp/
```

**Source Mode (`-f` flag)**: Server sends files to client (download)
```bash
# Client downloads file.txt from server
scp user@server:/home/user/file.txt ./
```

### SCP Command Flags

| Flag | Description |
|------|-------------|
| `-t` | Sink mode (target/upload) |
| `-f` | Source mode (from/download) |
| `-r` | Recursive transfer for directories |
| `-p` | Preserve file modification times |
| `-d` | Target is expected to be a directory |
| `-v` | Verbose mode |

### Security Features

The SCP handler implements multiple security measures:

**Path Traversal Prevention:**
- All paths are normalized before processing
- `..` components are resolved without escaping the root directory
- Absolute paths are stripped and joined with the user's root directory

**Symlink Escape Prevention:**
- Existing paths are canonicalized to resolve symlinks
- If the canonical path is outside the root directory, access is denied
- Symlinks in recursive transfers are skipped for security

**Input Validation:**
- Filenames cannot contain `/`, `..`, or `.`
- File size is limited to 10 GB maximum
- Permission mode bits are masked to strip setuid/setgid/sticky bits (only 0o777 allowed)
- Protocol line length is limited to prevent DoS via buffer exhaustion

### Configuration

SCP is enabled by default. To disable it:

**YAML Configuration:**
```yaml
scp:
enabled: false
```

**Builder API:**
```rust
let config = ServerConfig::builder()
.scp_enabled(false)
.build();
```

### Handler Architecture

```
SCP Request Flow:
┌───────────────┐ ┌──────────────────┐ ┌────────────────┐
│ exec_request │ --> │ Parse SCP cmd │ --> │ Create Handler │
│ "scp -t /tmp"│ │ mode, path, flags│ │ with root_dir │
└───────────────┘ └──────────────────┘ └────────────────┘
v
┌───────────────┐ ┌──────────────────┐ ┌────────────────┐
│ Spawn task │ --> │ SCP I/O loop │ --> │ File transfer │
│ (async) │ │ protocol messages│ │ operations │
└───────────────┘ └──────────────────┘ └────────────────┘
v
┌───────────────┐ ┌──────────────────┐ ┌────────────────┐
│ Send status │ --> │ EOF & close │ --> │ Channel done │
│ exit code │ │ channel │ │ │
└───────────────┘ └──────────────────┘ └────────────────┘
```

### Usage Examples

```bash
# Upload a single file
scp local_file.txt user@bssh-server:/home/user/

# Download a file
scp user@bssh-server:/home/user/file.txt ./

# Recursive directory upload
scp -r ./project/ user@bssh-server:/home/user/projects/

# Preserve timestamps
scp -p important.doc user@bssh-server:/backup/

# Recursive with timestamps
scp -rp ./data/ user@bssh-server:/storage/backup/
```

---

**Related Documentation:**
Expand Down
13 changes: 13 additions & 0 deletions src/server/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ pub struct ServerConfig {
#[serde(default)]
pub exec: ExecConfig,

/// Enable SCP protocol support.
#[serde(default = "default_true")]
pub scp_enabled: bool,

/// Time window for counting authentication attempts in seconds.
///
/// Default: 300 (5 minutes)
Expand Down Expand Up @@ -271,6 +275,7 @@ impl Default for ServerConfig {
publickey_auth: PublicKeyAuthConfigSerde::default(),
password_auth: PasswordAuthConfigSerde::default(),
exec: ExecConfig::default(),
scp_enabled: true,
auth_window_secs: default_auth_window_secs(),
ban_time_secs: default_ban_time_secs(),
whitelist_ips: Vec::new(),
Expand Down Expand Up @@ -506,6 +511,12 @@ impl ServerConfigBuilder {
self
}

/// Enable or disable SCP protocol support.
pub fn scp_enabled(mut self, enabled: bool) -> Self {
self.config.scp_enabled = enabled;
self
}

/// Build the ServerConfig.
pub fn build(self) -> ServerConfig {
self.config
Expand Down Expand Up @@ -564,6 +575,7 @@ impl ServerFileConfig {
allowed_commands: None,
blocked_commands: Vec::new(),
},
scp_enabled: self.scp.enabled,
auth_window_secs: self.security.auth_window,
ban_time_secs: self.security.ban_time,
whitelist_ips: self.security.whitelist_ips,
Expand All @@ -586,6 +598,7 @@ mod tests {
assert_eq!(config.max_auth_attempts, 5);
assert!(!config.allow_password_auth);
assert!(config.allow_publickey_auth);
assert!(config.scp_enabled);
}

#[test]
Expand Down
75 changes: 75 additions & 0 deletions src/server/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use super::auth::AuthProvider;
use super::config::ServerConfig;
use super::exec::CommandExecutor;
use super::pty::PtyConfig as PtyMasterConfig;
use super::scp::{ScpCommand, ScpHandler};
use super::security::AuthRateLimiter;
use super::session::{ChannelState, PtyConfig, SessionId, SessionInfo, SessionManager};
use super::sftp::SftpHandler;
Expand Down Expand Up @@ -764,6 +765,9 @@ impl russh::server::Handler for SshHandler {
/// Executes the requested command and streams output back to the client.
/// The command is executed via the configured shell with proper environment
/// setup based on the authenticated user.
///
/// Special handling is provided for SCP commands, which require bidirectional
/// communication with the client.
fn exec_request(
&mut self,
channel_id: ChannelId,
Expand Down Expand Up @@ -811,9 +815,29 @@ impl russh::server::Handler for SshHandler {
// Clone what we need for the async block
let auth_provider = Arc::clone(&self.auth_provider);
let exec_config = self.config.exec.clone();
let scp_enabled = self.config.scp_enabled;
let handle = session.handle();
let peer_addr = self.peer_addr;

// Check if this is an SCP command
let scp_command = if scp_enabled && ScpCommand::is_scp_command(&command) {
ScpCommand::parse(&command).ok()
} else {
None
};

// If SCP, we need to set up data forwarding
let scp_data_rx = if scp_command.is_some() {
let (tx, rx) = tokio::sync::mpsc::channel::<Vec<u8>>(1024);
// Store the sender in channel state for data forwarding
if let Some(state) = self.channels.get_mut(&channel_id) {
state.shell_data_tx = Some(tx);
}
Some(rx)
} else {
None
};

// Signal channel success before executing
let _ = session.channel_success(channel_id);

Expand Down Expand Up @@ -844,6 +868,57 @@ impl russh::server::Handler for SshHandler {
}
};

// Handle SCP commands specially
if let Some(scp_cmd) = scp_command {
tracing::info!(
user = %username,
peer = ?peer_addr,
mode = %scp_cmd.mode,
path = %scp_cmd.path.display(),
recursive = %scp_cmd.recursive,
"Executing SCP command"
);

// Get the receiver that was set up earlier
let data_rx = match scp_data_rx {
Some(rx) => rx,
None => {
tracing::error!("SCP data receiver not set up");
let _ = handle.exit_status_request(channel_id, 1).await;
let _ = handle.eof(channel_id).await;
let _ = handle.close(channel_id).await;
return Ok(());
}
};

let handle_clone = handle.clone();

// Create SCP handler with user's home directory as root
let scp_handler = ScpHandler::from_command(
&scp_cmd,
user_info.clone(),
Some(user_info.home_dir.clone()),
);

// Run SCP in a spawned task so the session loop can process incoming data
// The data() handler will forward data to shell_data_tx which the SCP handler
// will receive via data_rx
tokio::spawn(async move {
let exit_code = scp_handler
.run(channel_id, handle_clone.clone(), data_rx)
.await;

// Send exit status, EOF, and close channel
let _ = handle_clone
.exit_status_request(channel_id, exit_code as u32)
.await;
let _ = handle_clone.eof(channel_id).await;
let _ = handle_clone.close(channel_id).await;
});

return Ok(());
}

tracing::info!(
user = %username,
peer = ?peer_addr,
Expand Down
1 change: 1 addition & 0 deletions src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub mod config;
pub mod exec;
pub mod handler;
pub mod pty;
pub mod scp;
pub mod security;
pub mod session;
pub mod sftp;
Expand Down
Loading
Loading