docs(research): add russh and sftp-rs deep-dive references

This commit is contained in:
2026-06-10 13:41:17 +00:00
parent 5bb5e1064c
commit f2a25f5bc1
15 changed files with 3908 additions and 0 deletions

View File

@@ -0,0 +1,153 @@
# Russh: Overview & Architecture
**Version**: 0.60.2
**Repository**: https://github.com/warp-tech/russh
**License**: Apache-2.0
**Rust Edition**: 2024
**MSRV**: 1.85
**Origin**: Fork of [Thrussh](https://nest.pijul.com/pijul/thrussh) by Pierre-Étienne Meunier
## What is Russh?
Russh is a **low-level, asynchronous SSH2 client and server implementation** for Rust, built on Tokio and Futures. It provides a complete SSH-2 protocol stack — key exchange, authentication, channel multiplexing, port forwarding, and subsystems — exposed through handler traits that users implement for their specific needs.
Core characteristics:
- **Async-native** — built entirely on Tokio and Futures; no blocking I/O
- **Handler-driven** — both client and server are used by implementing trait handlers (`client::Handler`, `server::Handler`)
- **Both client and server** — a single crate supports both sides of the SSH connection
- **Streaming I/O** — channels implement `AsyncRead`/`AsyncWrite` for ergonomic integration
- **Safety-conscious** — `deny(clippy::unwrap_used)`, `deny(clippy::expect_used)`, `deny(clippy::panic)`, `deny(clippy::indexing_slicing)` by default; sensitive data uses `CryptoVec` with `mlock`
- **Two crypto backends** — `aws-lc-rs` (default) or `ring`, at least one required
## Workspace Structure
```
russh/ # Main SSH library crate
├── russh-util/ # Runtime abstraction utilities (WASM support, time, spawn)
├── russh-config/ # SSH config file parser + ProxyCommand support
├── cryptovec/ # Zeroing-on-drop vector with mlock (CryptoVec)
└── pageant/ # Windows Pageant SSH agent transport client
```
### Dependency Graph
```
russh depends on:
├── russh-cryptovec (CryptoVec: zeroing vector with mlock, ssh-encoding support)
├── russh-util (runtime abstraction: tokio spawn, time, WASM compat)
├── ssh-key (internal-russh-forked-ssh-key: key types, parsing, certificates)
├── ssh-encoding (SSH wire format encode/decode)
├── tokio (async runtime, IO, sync, time)
├── futures (future combinators)
├── curve25519-dalek (Curve25519 DH)
├── ed25519-dalek (Ed25519 signing)
├── p256/p384/p521 (NIST ECDSA curves + ECDH)
├── aes (AES cipher implementations)
├── flate2 (zlib compression, optional)
├── rsa (RSA signing, optional feature)
└── aws-lc-rs / ring (crypto backend for GCM etc.)
russh-config depends on:
├── tokio (async IO, process for ProxyCommand)
├── futures (future utilities)
└── globset / whoami (config matching, username detection)
cryptovec depends on:
├── nix (Unix) (mlock/munlock via mmap)
├── windows-sys (Win) (VirtualLock/VirtualUnlock)
└── ssh-encoding (optional, for Encode support)
```
## Architecture Overview
Russh implements the SSH-2 protocol as a **state machine** driven by an event loop. The core design separates protocol handling from application logic via handler traits.
```
┌─────────────────────────────────────────────────────────┐
│ Application Layer │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ client::Handler │ │ server::Handler │ │
│ │ (user implements) │ │ (user implements) │ │
│ └────────┬─────────┘ └────────┬─────────┘ │
├───────────┼──────────────────────────┼───────────────────┤
│ │ Russh Library │ │
│ ┌────────▼─────────────────────────▼─────────┐ │
│ │ Event Loop (tokio::select!) │ │
│ │ ┌──────────┐ ┌─────────┐ ┌──────────────┐ │ │
│ │ │ Reading │ │ Writing │ │ Handler │ │ │
│ │ │ packets │ │ packets │ │ dispatch │ │ │
│ │ └────┬─────┘ └────┬────┘ └──────┬───────┘ │ │
│ │ │ │ │ │ │
│ │ ┌────▼────────────▼─────────────▼───────┐ │ │
│ │ │ Session State Machine │ │ │
│ │ │ KEX → Auth → Channels → Forwarding │ │ │
│ │ └──────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────┤
│ Transport Layer │
│ ┌────────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Cipher (enc) │ │ MAC (auth) │ │ Compression │ │
│ └────────────────┘ └────────────┘ └────────────┘ │
│ ┌────────────────┐ ┌────────────────────────────┐ │
│ │ PacketWriter │ │ SSHBuffer / SshRead │ │
│ └────────────────┘ └────────────────────────────┘ │
├──────────────────────────────────────────────────────────┤
│ TCP Stream (tokio::net) │
└──────────────────────────────────────────────────────────┘
```
### Key Design Principle: Buffered Writes
From the library documentation:
> It might seem a little odd that the read/write methods for server or client sessions often return neither `Result` nor `Future`. This is because the data sent to the remote side is **buffered**, because it needs to be encrypted first, and encryption works on buffers, and for many algorithms, not in place.
The event loop works as follows:
1. Wait for incoming packets
2. React by calling the provided `Handler`, which fills some buffers
3. If buffers are non-empty, send them to the socket, flush, empty the buffers
4. For servers, unsolicited messages sent through a `server::Handle` are processed when there is no incoming packet
## Module Map
| Module | Purpose |
|--------|---------|
| `client/` | Client-side SSH: `Handler` trait, `Session`, `Handle`, `Config`, kex, encrypted state |
| `server/` | Server-side SSH: `Handler` trait, `Server` trait, `Session`, `Handle`, `Config` |
| `kex/` | Key exchange algorithms: Curve25519, DH groups, ECDH-NIST, ML-KEM hybrid |
| `cipher/` | Encryption: Chacha20-Poly1305, AES-GCM, AES-CTR, AES-CBC, 3DES-CBC |
| `mac/` | Message authentication: HMAC-SHA2, ETM variants |
| `keys/` | Key loading, parsing, SSH agent protocol, known hosts |
| `channels/` | Channel abstraction, `Channel`/`ChannelMsg`, `AsyncRead`/`AsyncWrite` streams |
| `negotiation` | Algorithm negotiation: `Preferred`, `Names`, `write_kex`, `read_kex` |
| `compression` | zlib and zlib@openssh.com compression |
| `session` | `CommonSession`, `Encrypted` state, `NewKeys`, `Exchange` |
| `sshbuffer` | `SSHBuffer`, `PacketWriter`, `SshId` |
| `auth` | Auth methods: `Method`, `MethodSet`, `Signer`, `AuthResult` |
| `cert` | OpenSSH certificate handling |
| `pty` | PTY terminal modes |
| `msg` | SSH message type constants (RFC 4250/4253/4254) |
| `parsing` | Wire format parsing helpers |
## Crypto Backend Selection
At least one crypto backend feature must be enabled, or compilation fails:
```rust
#[cfg(not(any(feature = "ring", feature = "aws-lc-rs")))]
compile_error!(
"`russh` requires enabling either the `ring` or `aws-lc-rs` feature as a crypto backend."
);
```
- **`aws-lc-rs`** (default): Used for AES-GCM via the `aws-lc-rs` crate's AEAD interface
- **`ring`**: Alternative backend for AES-GCM via the `ring` crate's AEAD interface
Other features:
- **`rsa`** (default): Enables RSA key support
- **`des`**: Enables insecure 3DES-CBC cipher
- **`dsa`**: Enables insecure DSA key support
- **`flate2`** (default): Enables zlib compression
- **`async-trait`**: Enables `#[async_trait]` attribute on handler traits
- **`legacy-ed25519-pkcs8-parser`**: Enables ASN1-based Ed25519 PKCS#8 parsing
- **`serde`**: Enables serde support for ssh-key types

View File

@@ -0,0 +1,410 @@
# Russh: Key Types and Traits
## Client API
### `client::Handler` Trait
The primary interface for implementing SSH clients. All methods have default implementations (mostly no-ops returning `Ok(())`), except `check_server_key` which defaults to rejecting all keys.
```rust
pub trait Handler: Sized + Send {
type Error: From<crate::Error> + Send + core::fmt::Debug;
// --- Must implement (security critical) ---
fn check_server_key(
&mut self,
server_public_key: &ssh_key::PublicKey,
) -> impl Future<Output = Result<bool, Self::Error>> + Send;
// Default: async { Ok(false) } — REJECTS ALL KEYS
// --- Optional callbacks ---
fn auth_banner(&mut self, banner: &str, session: &mut Session) -> ...;
fn kex_done(&mut self, shared_secret: Option<&[u8]>, names: &Names, session: &mut Session) -> ...;
fn channel_open_confirmation(&mut self, id: ChannelId, max_packet_size: u32, window_size: u32, session: &mut Session) -> ...;
fn channel_success(&mut self, channel: ChannelId, session: &mut Session) -> ...;
fn channel_failure(&mut self, channel: ChannelId, session: &mut Session) -> ...;
fn channel_close(&mut self, channel: ChannelId, session: &mut Session) -> ...;
fn channel_eof(&mut self, channel: ChannelId, session: &mut Session) -> ...;
fn channel_open_failure(&mut self, channel: ChannelId, reason: ChannelOpenFailure, description: &str, language: &str, session: &mut Session) -> ...;
fn data(&mut self, channel: ChannelId, data: &[u8], session: &mut Session) -> ...;
fn extended_data(&mut self, channel: ChannelId, ext: u32, data: &[u8], session: &mut Session) -> ...;
fn exit_status(&mut self, channel: ChannelId, exit_status: u32, session: &mut Session) -> ...;
fn exit_signal(&mut self, channel: ChannelId, signal_name: Sig, core_dumped: bool, error_message: &str, lang_tag: &str, session: &mut Session) -> ...;
fn window_adjusted(&mut self, channel: ChannelId, new_size: u32, session: &mut Session) -> ...;
fn adjust_window(&mut self, channel: ChannelId, window: u32) -> u32;
// Server-initiated channels (port forwarding, agent, X11, etc.)
fn server_channel_open_forwarded_tcpip(&mut self, channel: Channel<Msg>, ...) -> ...;
fn server_channel_open_forwarded_streamlocal(&mut self, channel: Channel<Msg>, ...) -> ...;
fn server_channel_open_agent_forward(&mut self, channel: Channel<Msg>, ...) -> ...;
fn server_channel_open_session(&mut self, channel: Channel<Msg>, ...) -> ...;
fn server_channel_open_x11(&mut self, channel: Channel<Msg>, ...) -> ...;
fn server_channel_open_direct_tcpip(&mut self, channel: Channel<Msg>, ...) -> ...;
fn server_channel_open_direct_streamlocal(&mut self, channel: Channel<Msg>, ...) -> ...;
// OpenSSH extensions
fn openssh_ext_host_keys_announced(&mut self, keys: Vec<PublicKey>, session: &mut Session) -> ...;
fn disconnected(&mut self, reason: DisconnectReason<Self::Error>) -> ...;
}
```
### `client::Config`
```rust
pub struct Config {
pub client_id: SshId, // SSH version string, default: "SSH-2.0-russh_0.60.2"
pub limits: Limits, // Rekey limits (1GB read/write, 1hr time)
pub window_size: u32, // Initial channel window (default: 2097152 = 2MB)
pub maximum_packet_size: u32, // Max single packet (default: 32768)
pub channel_buffer_size: usize, // Channel message buffer (default: 100)
pub preferred: Preferred, // Algorithm preferences
pub inactivity_timeout: Option<Duration>, // Connection timeout (default: None)
pub keepalive_interval: Option<Duration>, // Keepalive frequency (default: None)
pub keepalive_max: usize, // Max missed keepalives (default: 3)
pub anonymous: bool, // Skip authentication (default: false)
pub gex: GexParams, // DH-GEX parameters (default: 3072-8192 bits)
pub nodelay: bool, // TCP_NODELAY (default: false)
}
```
### `client::Handle<H>`
The handle returned after connecting. Used to send commands, open channels, authenticate, etc.
```rust
impl<H: Handler> Handle<H> {
// Authentication
pub async fn authenticate_none(&mut self, user: U) -> Result<AuthResult, Error>;
pub async fn authenticate_password(&mut self, user: U, password: P) -> Result<AuthResult, Error>;
pub async fn authenticate_publickey(&mut self, user: U, key: PrivateKeyWithHashAlg) -> Result<AuthResult, Error>;
pub async fn authenticate_openssh_cert(&mut self, user: U, key: Arc<PrivateKey>, cert: Certificate) -> Result<AuthResult, Error>;
pub async fn authenticate_publickey_with<U, S: Signer>(&mut self, user: U, key: PublicKey, hash_alg: Option<HashAlg>, signer: &mut S) -> Result<AuthResult, S::Error>;
pub async fn authenticate_keyboard_interactive_start<U, S>(&mut self, user: U, submethods: S) -> Result<KeyboardInteractiveAuthResponse, Error>;
pub async fn authenticate_keyboard_interactive_respond(&mut self, responses: Vec<String>) -> Result<KeyboardInteractiveAuthResponse, Error>;
// Channels
pub async fn channel_open_session(&self) -> Result<Channel<Msg>, Error>;
pub async fn channel_open_x11(&self, originator_address: A, originator_port: u32) -> Result<Channel<Msg>, Error>;
pub async fn channel_open_direct_tcpip(&self, host_to_connect: A, port_to_connect: u32, originator_address: B, originator_port: u32) -> Result<Channel<Msg>, Error>;
pub async fn channel_open_direct_streamlocal(&self, socket_path: S) -> Result<Channel<Msg>, Error>;
// Port forwarding
pub async fn tcpip_forward(&self, address: A, port: u32) -> Result<u32, Error>;
pub async fn cancel_tcpip_forward(&self, address: A, port: u32) -> Result<(), Error>;
pub async fn streamlocal_forward(&self, socket_path: A) -> Result<(), Error>;
pub async fn cancel_streamlocal_forward(&self, socket_path: A) -> Result<(), Error>;
// Connection management
pub async fn disconnect(&self, reason: Disconnect, description: &str, language_tag: &str) -> Result<(), Error>;
pub async fn rekey_soon(&self) -> Result<(), Error>;
pub async fn send_keepalive(&self, want_reply: bool) -> Result<(), Error>;
pub async fn send_ping(&self) -> Result<(), Error>;
pub async fn no_more_sessions(&self, want_reply: bool) -> Result<(), Error>;
pub async fn best_supported_rsa_hash(&self) -> Result<Option<Option<HashAlg>>, Error>;
pub fn is_closed(&self) -> bool;
}
```
`Handle<H>` also implements `Future<Output = Result<(), H::Error>>`, so you can `.await` it to wait for the session to end.
### `client::connect` and `client::connect_stream`
```rust
// Connect via TCP
pub async fn connect<H: Handler + Send + 'static, A: ToSocketAddrs>(
config: Arc<Config>, addrs: A, handler: H,
) -> Result<Handle<H>, H::Error>;
// Connect via any AsyncRead+AsyncWrite stream
pub async fn connect_stream<H, R>(
config: Arc<Config>, stream: R, handler: H,
) -> Result<Handle<H>, H::Error>
where
H: Handler + Send + 'static,
R: AsyncRead + AsyncWrite + Unpin + Send + 'static;
```
---
## Server API
### `server::Server` Trait
Factory trait that creates a new `Handler` for each client connection:
```rust
pub trait Server {
type Handler: Handler + Send + 'static;
fn new_client(&mut self, peer_addr: Option<SocketAddr>) -> Self::Handler;
fn handle_session_error(&mut self, _error: <Self::Handler as Handler>::Error) {}
// Run on a pre-bound TcpListener
fn run_on_socket(&mut self, config: Arc<Config>, socket: &TcpListener) -> RunningServer<...>;
// Bind and run on an address
fn run_on_address<A: ToSocketAddrs + Send>(&mut self, config: Arc<Config>, addrs: A) -> impl Future<...>;
}
```
### `server::Handler` Trait
Per-client handler, similar to `client::Handler` but with different callback signatures (receives `&mut Session` for sending responses):
```rust
pub trait Handler: Sized {
type Error: From<crate::Error> + Send;
// Authentication callbacks
fn auth_none(&mut self, user: &str) -> impl Future<Output = Result<Auth, Self::Error>> + Send;
fn auth_password(&mut self, user: &str, password: &str) -> impl Future<Output = Result<Auth, Self::Error>> + Send;
fn auth_publickey_offered(&mut self, user: &str, public_key: &PublicKey) -> impl Future<...> + Send;
fn auth_publickey(&mut self, user: &str, public_key: &PublicKey) -> impl Future<...> + Send;
fn auth_openssh_certificate(&mut self, user: &str, certificate: &Certificate) -> impl Future<...> + Send;
fn auth_keyboard_interactive<'a>(&'a mut self, user: &str, submethods: &str, response: Option<Response<'a>>) -> impl Future<...> + Send;
fn auth_succeeded(&mut self, session: &mut Session) -> impl Future<...> + Send;
fn authentication_banner(&mut self) -> impl Future<Output = Result<Option<String>, Self::Error>> + Send;
// Channel callbacks (return bool = whether to grant the channel)
fn channel_open_session(&mut self, channel: Channel<Msg>, session: &mut Session) -> impl Future<Output = Result<bool, Self::Error>> + Send;
fn channel_open_x11(&mut self, channel: Channel<Msg>, originator_address: &str, originator_port: u32, session: &mut Session) -> impl Future<...> + Send;
fn channel_open_direct_tcpip(&mut self, channel: Channel<Msg>, host_to_connect: &str, port_to_connect: u32, originator_address: &str, originator_port: u32, session: &mut Session) -> impl Future<...> + Send;
fn channel_open_forwarded_tcpip(&mut self, channel: Channel<Msg>, ...) -> impl Future<...> + Send;
fn channel_open_direct_streamlocal(&mut self, channel: Channel<Msg>, socket_path: &str, session: &mut Session) -> impl Future<...> + Send;
// Channel events
fn data(&mut self, channel: ChannelId, data: &[u8], session: &mut Session) -> impl Future<...> + Send;
fn extended_data(&mut self, channel: ChannelId, code: u32, data: &[u8], session: &mut Session) -> impl Future<...> + Send;
fn channel_close(&mut self, channel: ChannelId, session: &mut Session) -> impl Future<...> + Send;
fn channel_eof(&mut self, channel: ChannelId, session: &mut Session) -> impl Future<...> + Send;
fn window_adjusted(&mut self, channel: ChannelId, new_size: u32, session: &mut Session) -> impl Future<...> + Send;
fn adjust_window(&mut self, channel: ChannelId, current: u32) -> u32;
// Channel requests (use session.channel_success/failure to respond)
fn pty_request(&mut self, channel: ChannelId, term: &str, col_width: u32, row_height: u32, pix_width: u32, pix_height: u32, modes: &[(Pty, u32)], session: &mut Session) -> impl Future<...> + Send;
fn x11_request(&mut self, channel: ChannelId, ...) -> impl Future<...> + Send;
fn env_request(&mut self, channel: ChannelId, variable_name: &str, variable_value: &str, session: &mut Session) -> impl Future<...> + Send;
fn shell_request(&mut self, channel: ChannelId, session: &mut Session) -> impl Future<...> + Send;
fn exec_request(&mut self, channel: ChannelId, data: &[u8], session: &mut Session) -> impl Future<...> + Send;
fn subsystem_request(&mut self, channel: ChannelId, name: &str, session: &mut Session) -> impl Future<...> + Send;
fn window_change_request(&mut self, channel: ChannelId, ...) -> impl Future<...> + Send;
fn agent_request(&mut self, channel: ChannelId, session: &mut Session) -> impl Future<...> + Send;
fn signal(&mut self, channel: ChannelId, signal: Sig, session: &mut Session) -> impl Future<...> + Send;
// Port forwarding
fn tcpip_forward(&mut self, address: &str, port: &mut u32, session: &mut Session) -> impl Future<...> + Send;
fn cancel_tcpip_forward(&mut self, address: &str, port: u32, session: &mut Session) -> impl Future<...> + Send;
fn streamlocal_forward(&mut self, socket_path: &str, session: &mut Session) -> impl Future<...> + Send;
fn cancel_streamlocal_forward(&mut self, socket_path: &str, session: &mut Session) -> impl Future<...> + Send;
// DH-GEX group lookup
fn lookup_dh_gex_group(&mut self, gex_params: &GexParams) -> impl Future<Output = Result<Option<DhGroup>, Self::Error>> + Send;
}
```
### `server::Auth` Enum
```rust
pub enum Auth {
Reject { proceed_with_methods: Option<MethodSet>, partial_success: bool },
Accept,
UnsupportedMethod,
Partial { name: Cow<'static, str>, instructions: Cow<'static, str>, prompts: Cow<'static, [(Cow<'static, str>, bool)]> },
}
```
### `server::Config`
```rust
pub struct Config {
pub server_id: SshId, // "SSH-2.0-russh_0.60.2"
pub methods: auth::MethodSet, // All methods by default
pub auth_rejection_time: Duration, // Constant-time rejection (default: 1s)
pub auth_rejection_time_initial: Option<Duration>, // For "none" probe (default: None)
pub keys: Vec<PrivateKey>, // Server host keys
pub limits: Limits,
pub window_size: u32, // Default: 2097152
pub maximum_packet_size: u32, // Default: 32768
pub channel_buffer_size: usize, // Default: 100
pub event_buffer_size: usize, // Default: 10
pub preferred: Preferred,
pub max_auth_attempts: usize, // Default: 10
pub inactivity_timeout: Option<Duration>, // Default: 600s
pub keepalive_interval: Option<Duration>, // Default: None
pub keepalive_max: usize, // Default: 3
pub nodelay: bool, // Default: false
}
```
### `server::Handle`
Server-side handle for sending unsolicited messages to a client:
```rust
impl Handle {
pub async fn data(&self, id: ChannelId, data: impl Into<Bytes>) -> Result<(), Bytes>;
pub async fn extended_data(&self, id: ChannelId, ext: u32, data: impl Into<Bytes>) -> Result<(), Bytes>;
pub async fn eof(&self, id: ChannelId) -> Result<(), ()>;
pub async fn channel_success(&self, id: ChannelId) -> Result<(), ()>;
pub async fn channel_failure(&self, id: ChannelId) -> Result<(), ()>;
pub async fn close(&self, id: ChannelId) -> Result<(), ()>;
pub async fn xon_xoff_request(&self, id: ChannelId, client_can_do: bool) -> Result<(), ()>;
pub async fn exit_status_request(&self, id: ChannelId, exit_status: u32) -> Result<(), ()>;
pub async fn forward_tcpip(&self, address: String, port: u32) -> Result<u32, ()>;
pub async fn cancel_tcpip_forward(&self, address: String, port: u32) -> Result<(), ()>;
// ... etc.
}
```
---
## Channel Types
### `Channel<Send>`
A bidirectional handle to an SSH channel. `Send` is the message type (`client::Msg` or `server::Msg`).
```rust
pub struct Channel<Send: From<(ChannelId, ChannelMsg)>> {
pub read_half: ChannelReadHalf,
pub write_half: ChannelWriteHalf<Send>,
}
impl<S: From<(ChannelId, ChannelMsg)> + Send + Sync + 'static> Channel<S> {
pub fn id(&self) -> ChannelId;
pub async fn writable_packet_size(&self) -> usize;
pub fn split(self) -> (ChannelReadHalf, ChannelWriteHalf<S>);
pub async fn wait(&mut self) -> Option<ChannelMsg>;
// Client-side operations
pub async fn request_pty(&self, ...) -> Result<(), Error>;
pub async fn request_shell(&self, want_reply: bool) -> Result<(), Error>;
pub async fn exec(&self, want_reply: bool, command: A) -> Result<(), Error>;
pub async fn signal(&self, signal: Sig) -> Result<(), Error>;
pub async fn request_subsystem(&self, want_reply: bool, name: A) -> Result<(), Error>;
pub async fn request_x11(&self, ...) -> Result<(), Error>;
pub async fn set_env(&self, ...) -> Result<(), Error>;
pub async fn window_change(&self, ...) -> Result<(), Error>;
pub async fn agent_forward(&self, want_reply: bool) -> Result<(), Error>;
pub async fn data<R: AsyncRead + Unpin>(&self, data: R) -> Result<(), Error>;
pub async fn extended_data<R: AsyncRead + Unpin>(&self, ext: u32, data: R) -> Result<(), Error>;
pub async fn eof(&self) -> Result<(), Error>;
pub async fn exit_status(&self, exit_status: u32) -> Result<(), Error>;
pub async fn close(&self) -> Result<(), Error>;
// Streaming
pub fn into_stream(self) -> ChannelStream<S>;
pub fn make_reader(&mut self) -> impl AsyncRead + '_;
pub fn make_reader_ext(&mut self, ext: Option<u32>) -> impl AsyncRead + '_;
pub fn make_writer(&self) -> impl AsyncWrite + 'static;
pub fn make_writer_ext(&self, ext: Option<u32>) -> impl AsyncWrite + 'static;
}
```
### `ChannelMsg` Enum
All possible messages receivable on a channel:
```rust
pub enum ChannelMsg {
Open { id: ChannelId, max_packet_size: u32, window_size: u32 },
Data { data: Bytes },
ExtendedData { data: Bytes, ext: u32 },
Eof,
Close,
RequestPty { want_reply: bool, term: String, col_width: u32, row_height: u32, pix_width: u32, pix_height: u32, terminal_modes: Vec<(Pty, u32)> },
RequestShell { want_reply: bool },
Exec { want_reply: bool, command: Vec<u8> },
Signal { signal: Sig },
RequestSubsystem { want_reply: bool, name: String },
RequestX11 { want_reply: bool, single_connection: bool, x11_authentication_protocol: String, x11_authentication_cookie: String, x11_screen_number: u32 },
SetEnv { want_reply: bool, variable_name: String, variable_value: String },
WindowChange { col_width: u32, row_height: u32, pix_width: u32, pix_height: u32 },
AgentForward { want_reply: bool },
XonXoff { client_can_do: bool },
ExitStatus { exit_status: u32 },
ExitSignal { signal_name: Sig, core_dumped: bool, error_message: String, lang_tag: String },
WindowAdjusted { new_size: u32 },
Success,
Failure,
OpenFailure(ChannelOpenFailure),
}
```
### `ChannelId`
A `u32` wrapper identifying a channel within a session:
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub struct ChannelId(u32);
```
---
## Shared Types
### `Limits`
Rekey thresholds (following RFC 4253 Section 9):
```rust
pub struct Limits {
pub rekey_write_limit: usize, // Default: 1 << 30 (1 GB)
pub rekey_read_limit: usize, // Default: 1 << 30 (1 GB)
pub rekey_time_limit: Duration, // Default: 3600s (1 hour)
}
```
### `Preferred`
Algorithm preference lists for negotiation:
```rust
pub struct Preferred {
pub kex: Cow<'static, [kex::Name]>,
pub key: Cow<'static, [Algorithm]>,
pub cipher: Cow<'static, [cipher::Name]>,
pub mac: Cow<'static, [mac::Name]>,
pub compression: Cow<'static, [compression::Name]>,
}
```
Default order prioritizes modern algorithms:
- **KEX**: ML-KEM-768-X25519 → Curve25519 → DH-GEX-SHA256 → DH-G18/17/16/15/14
- **Key**: Ed25519 → ECDSA-P256/P384/P521 → RSA-SHA512/256
- **Cipher**: Chacha20-Poly1305 → AES-256-GCM → AES-256/192/128-CTR
- **MAC**: HMAC-SHA512-ETM → HMAC-SHA256-ETM → HMAC-SHA512/256
### `Disconnect` Enum
RFC 4253 Section 11.1 disconnect reason codes:
```rust
pub enum Disconnect {
HostNotAllowedToConnect = 1,
ProtocolError = 2,
KeyExchangeFailed = 3,
MACError = 5,
CompressionError = 6,
ServiceNotAvailable = 7,
ProtocolVersionNotSupported = 8,
HostKeyNotVerifiable = 9,
ConnectionLost = 10,
ByApplication = 11,
TooManyConnections = 12,
AuthCancelledByUser = 13,
NoMoreAuthMethodsAvailable = 14,
IllegalUserName = 15,
}
```
### `CryptoVec`
A vector that zeroes its memory on clears and reallocations, using `mlock` on Unix and `VirtualLock` on Windows. Used for all sensitive key material.
```rust
// From russh-cryptovec
pub struct CryptoVec { /* ... */ }
// Implements mlock/munlock for sensitive data
// Zeroes memory on drop/resize
```

View File

@@ -0,0 +1,298 @@
# Russh: SSH Protocol Implementation
This document covers how russh implements the SSH-2 protocol — key exchange, authentication, channel multiplexing, port forwarding, and extensions.
## Protocol Message Constants (`msg` module)
Russh defines SSH protocol message type codes as constants:
| Constant | Value | RFC Reference |
|----------|-------|---------------|
| `DISCONNECT` | 1 | RFC 4253 §11.1 |
| `IGNORE` | 2 | RFC 4253 §11.2 |
| `UNIMPLEMENTED` | 3 | RFC 4253 §11.3 |
| `DEBUG` | 4 | RFC 4253 §11.4 |
| `SERVICE_REQUEST` | 5 | RFC 4253 §7 |
| `SERVICE_ACCEPT` | 6 | RFC 4253 §7 |
| `EXT_INFO` | 7 | RFC 8308 |
| `KEXINIT` | 20 | RFC 4253 §7.1 |
| `NEWKEYS` | 21 | RFC 4253 §7.3 |
| `KEX_ECDH_INIT` | 30 | RFC 5656 §4 |
| `KEX_ECDH_REPLY` | 31 | RFC 5656 §4 |
| `KEX_DH_GEX_REQUEST` | 34 | RFC 4419 §3 |
| `KEX_DH_GEX_GROUP` | 31 | RFC 4419 §3 |
| `KEX_DH_GEX_INIT` | 32 | RFC 4419 §3 |
| `KEX_DH_GEX_REPLY` | 33 | RFC 4419 §3 |
| `USERAUTH_REQUEST` | 50 | RFC 4252 §5 |
| `USERAUTH_FAILURE` | 51 | RFC 4252 §5 |
| `USERAUTH_SUCCESS` | 52 | RFC 4252 §5 |
| `USERAUTH_BANNER` | 53 | RFC 4252 §5.4 |
| `USERAUTH_INFO_REQUEST` | 60 | RFC 4256 §5 |
| `USERAUTH_INFO_RESPONSE` | 61 | RFC 4256 §5 |
| `GLOBAL_REQUEST` | 80 | RFC 4254 §4 |
| `REQUEST_SUCCESS` | 81 | RFC 4254 §4 |
| `REQUEST_FAILURE` | 82 | RFC 4254 §4 |
| `CHANNEL_OPEN` | 90 | RFC 4254 §5.1 |
| `CHANNEL_OPEN_CONFIRMATION` | 91 | RFC 4254 §5.1 |
| `CHANNEL_OPEN_FAILURE` | 92 | RFC 4254 §5.1 |
| `CHANNEL_WINDOW_ADJUST` | 93 | RFC 4254 §5.2 |
| `CHANNEL_DATA` | 94 | RFC 4254 §5.2 |
| `CHANNEL_EXTENDED_DATA` | 95 | RFC 4254 §5.2 |
| `CHANNEL_EOF` | 96 | RFC 4254 §5.3 |
| `CHANNEL_CLOSE` | 97 | RFC 4254 §5.3 |
| `CHANNEL_REQUEST` | 98 | RFC 4254 §5.4 |
| `CHANNEL_SUCCESS` | 99 | RFC 4254 §5.4 |
| `CHANNEL_FAILURE` | 100 | RFC 4254 §5.4 |
---
## Connection Lifecycle
### 1. Version Exchange
Both sides send their SSH identification string:
```
SSH-2.0-russh_0.60.2\r\n
```
The `SshId` type handles this:
```rust
pub enum SshId {
Standard(Cow<'static, str>), // Appends \r\n
Raw(Cow<'static, str>), // Sent as-is
}
```
Client sends first, then reads server's ID. Server reads client's ID first, then sends its own.
### 2. Key Exchange (KEXINIT)
The `negotiation` module handles algorithm negotiation. Each side sends a `KEXINIT` packet containing lists of supported algorithms. The first algorithm from the client's list that also appears in the server's list is chosen (for client), or vice versa (for server).
**Algorithm selection order** (per `Preferred::DEFAULT`):
- **KEX**: `mlkem768x25519-sha256``curve25519-sha256``curve25519-sha256@libssh.org``diffie-hellman-group-exchange-sha256` → group18/17/16/15/14
- **Host Key**: `ssh-ed25519``ecdsa-sha2-nistp256/384/521``rsa-sha2-512/256``ssh-rsa`
- **Cipher**: `chacha20-poly1305@openssh.com``aes256-gcm@openssh.com``aes256-ctr``aes192-ctr``aes128-ctr`
- **MAC**: `hmac-sha2-512-etm@openssh.com``hmac-sha2-256-etm@openssh.com``hmac-sha2-512``hmac-sha2-256`
**Negotiation flow** (`write_kex` / `read_kex`):
1. Both sides send KEXINIT with their algorithm lists
2. Each side parses the other's KEXINIT using `Client::read_kex()` or `Server::read_kex()`
3. The `Select` trait implements the first-match algorithm
4. `Names` struct is produced with the negotiated algorithms
5. Extension kex names (like `ext-info-c`, `kex-strict-c-v00@openssh.com`) are handled specially — filtered from negotiation but used for capability detection
### 3. Diffie-Hellman Key Exchange
After KEXINIT, the actual key exchange runs. This is managed by `ClientKex` and `ServerKex` state machines.
**Client KEX state machine** (`ClientKex`):
```
Created → (receive KEXINIT) → WaitingForDhReply or WaitingForGexReply → WaitingForNewKeys → Done
```
Steps for a typical Curve25519 exchange:
1. **Client sends KEX_ECDH_INIT**: Contains client's ephemeral public key
2. **Server sends KEX_ECDH_REPLY**: Contains server host key, server's ephemeral public key, and a signature
3. **Client verifies**:
- Parses the server host key
- Computes the shared secret from the ephemeral keys
- Computes the exchange hash (H) from: `V_C || V_S || I_C || I_S || K_S || e || f || K`
- Verifies the server's signature on H using the server host key
- Calls `handler.check_server_key()` for application-level trust verification
4. **Both sides send NEWKEYS**: Activate the newly computed encryption keys
**Server KEX** mirrors this, responding to client's init and sending the reply.
**DH-GEX (Group Exchange) flow** has an extra step:
1. Client sends `KEX_DH_GEX_REQUEST` with min/preferred/max group sizes
2. Server sends `KEX_DH_GEX_GROUP` with the prime and generator
3. Client validates group size and sends `KEX_DH_GEX_INIT`
4. Server sends `KEX_DH_GEX_REPLY`
5. Both send `NEWKEYS`
### 4. Key Derivation (`compute_keys`)
After the shared secret and exchange hash are computed, keys are derived per RFC 4253 §7.2:
```
Key = HASH(K || H || X || session_id)
```
Where `X` is a single character:
- `A` = client-to-server IV (nonce)
- `B` = server-to-client IV (nonce)
- `C` = client-to-server encryption key
- `D` = server-to-client encryption key
- `E` = client-to-server MAC key
- `F` = server-to-client MAC key
If the derived key is shorter than needed, it's extended by hashing `K || H || Key[0..n]` repeatedly.
The `session_id` is set to the exchange hash from the first key exchange and remains constant across rekeys.
### 5. Strict Key Exchange
Russh supports the OpenSSH strict key exchange extension (`kex-strict-c-v00@openssh.com` / `kex-strict-s-v00@openssh.com`), which:
- Resets sequence numbers after NEWKEYS
- Enforces that only KEXINIT, DH init, and NEWKEYS messages appear during initial kex
- Validated by `validate_client_msg_strict_kex` / `validate_server_msg_strict_kex`
- Prevents the Terrapin attack (CVE-2023-48799)
### 6. Authentication
After NEWKEYS, the encrypted state machine transitions through:
```rust
pub enum EncryptedState {
WaitingAuthServiceRequest { sent: bool, accepted: bool },
WaitingAuthRequest(auth::AuthRequest),
InitCompression,
Authenticated,
}
```
Flow:
1. Client sends `SERVICE_REQUEST` for "ssh-connection"
2. Server responds with `SERVICE_ACCEPT`
3. Client sends authentication requests (password, publickey, keyboard-interactive, none)
4. Server responds with `USERAUTH_SUCCESS`, `USERAUTH_FAILURE`, or `USERAUTH_INFO_REQUEST`
5. Upon success, state transitions to `InitCompression``Authenticated`
**Authentication methods** (`auth::Method`):
```rust
pub enum Method {
None,
Password { password: String },
PublicKey { key: PrivateKeyWithHashAlg },
OpenSshCertificate { key: Arc<PrivateKey>, cert: Certificate },
FuturePublicKey { key: PublicKey, hash_alg: Option<HashAlg> },
FutureCertificate { cert: Certificate, hash_alg: Option<HashAlg> },
KeyboardInteractive { submethods: String },
}
```
The `FuturePublicKey` / `FutureCertificate` variants are used with the `Signer` trait (e.g., SSH agent), where signing is delegated to an external component.
### 7. Rekeying
Rekeying is triggered automatically when:
- Bytes written exceed `Limits::rekey_write_limit` (default 1 GB)
- Bytes read exceed `Limits::rekey_read_limit` (default 1 GB)
- Time since last rekey exceeds `Limits::rekey_time_limit` (default 1 hour)
- Explicitly via `Handle::rekey_soon()`
During rekey:
- New `KEXINIT` packets are exchanged
- Pending channel data is buffered until rekey completes
- Sequence numbers are reset if strict kex is negotiated
---
## Channel Multiplexing
SSH channels are multiplexed over a single connection. Each channel has:
- A `ChannelId` (local identifier)
- A `recipient_channel` (remote identifier)
- Window size for flow control
- Maximum packet size
### Channel Types
| Type | Open Method | Description |
|------|-------------|-------------|
| `session` | `channel_open_session()` | Standard shell/exec channel |
| `x11` | `channel_open_x11()` | X11 forwarding |
| `direct-tcpip` | `channel_open_direct_tcpip()` | Local TCP port forwarding |
| `forwarded-tcpip` | Server-initiated | Remote TCP port forwarding |
| `direct-streamlocal` | `channel_open_direct_streamlocal()` | Local UNIX socket forwarding |
| `forwarded-streamlocal` | Server-initiated | Remote UNIX socket forwarding |
| `auth-agent@openssh.com` | Server-initiated | Agent forwarding |
### Flow Control
SSH uses a window-based flow control mechanism:
- Each side maintains a `sender_window_size` and `recipient_window_size`
- Data cannot be sent if the recipient's window is exhausted
- `CHANNEL_WINDOW_ADJUST` messages increase the window
- `adjust_window()` is called when data is received to determine the next target window
- Data is queued in `pending_data` when the window is full, and flushed when the window is adjusted
### Channel Request/Response Pattern
Many channel operations follow a request-response pattern:
1. Client sends `CHANNEL_REQUEST` with `want_reply = true`
2. Server processes the request via `Handler` callback
3. Server calls `session.channel_success(channel)` or `session.channel_failure(channel)`
4. Client receives `CHANNEL_SUCCESS` or `CHANNEL_FAILURE`
---
## Port Forwarding
### Local TCP Forwarding (direct-tcpip)
Client opens a channel to forward a TCP connection through the server:
```rust
// Client side
let channel = handle.channel_open_direct_tcpip(
"target-host", 8080, // host:port to connect to
"origin-host", 12345, // originator info
).await?;
```
### Remote TCP Forwarding (forward-tcpip)
**Step 1**: Client requests the server to listen on a port:
```rust
let allocated_port = handle.tcpip_forward("0.0.0.0", 0).await?;
```
**Step 2**: When a connection arrives, the server calls `Handler::channel_open_forwarded_tcpip()`.
**Step 3**: Client receives the channel via `Handler::server_channel_open_forwarded_tcpip()`.
**Step 4**: To stop:
```rust
handle.cancel_tcpip_forward("0.0.0.0", allocated_port).await?;
```
### UNIX Socket Forwarding (streamlocal)
Similar to TCP forwarding but for UNIX domain sockets:
```rust
// Local
handle.channel_open_direct_streamlocal("/tmp/socket").await?;
// Remote
handle.streamlocal_forward("/tmp/socket").await?;
handle.cancel_streamlocal_forward("/tmp/socket").await?;
```
---
## Extensions
### `ext-info-c` / `ext-info-s` (RFC 8308)
Extension info is sent after NEWKEYS. Russh supports:
- **`server-sig-algs`**: Server tells client which signature algorithms it accepts for public key authentication. Used by `Handle::best_supported_rsa_hash()`.
### `kex-strict-c-v00@openssh.com` / `kex-strict-s-v00@openssh.com`
OpenSSH strict key exchange (Terrapin attack mitigation). Negotiated during KEXINIT and enforced during the initial key exchange.
### OpenSSH Agent Forwarding
The server can open an `auth-agent@openssh.com` channel to forward the client's SSH agent. The client handles this via `Handler::server_channel_open_agent_forward()`.
### OpenSSH `no-more-sessions@openssh.com`
Requests that the other side not open any more sessions:
```rust
handle.no_more_sessions(true).await?;
```

View File

@@ -0,0 +1,299 @@
# Russh: Cryptographic Primitives
This document covers the cryptographic implementations in russh — key exchange algorithms, ciphers, MACs, and key handling.
## Key Exchange Algorithms (`kex` module)
All KEX algorithms implement the `KexAlgorithmImplementor` trait:
```rust
pub(crate) trait KexAlgorithmImplementor {
fn skip_exchange(&self) -> bool;
fn is_dh_gex(&self) -> bool;
fn client_dh_gex_init(&mut self, gex: &GexParams, writer: &mut impl Writer) -> Result<(), Error>;
fn dh_gex_set_group(&mut self, group: DhGroup) -> Result<(), Error>;
fn server_dh(&mut self, exchange: &mut Exchange, payload: &[u8]) -> Result<(), Error>;
fn client_dh(&mut self, client_ephemeral: &mut Vec<u8>, writer: &mut impl Writer) -> Result<(), Error>;
fn compute_shared_secret(&mut self, remote_pubkey: &[u8]) -> Result<(), Error>;
fn shared_secret_bytes(&self) -> Option<&[u8]>;
fn compute_exchange_hash(&self, key: &[u8], exchange: &Exchange, buffer: &mut CryptoVec) -> Result<Vec<u8>, Error>;
fn compute_keys(&self, session_id: &[u8], exchange_hash: &[u8], cipher: cipher::Name, remote_to_local_mac: mac::Name, local_to_remote_mac: mac::Name, is_server: bool) -> Result<CipherPair, Error>;
}
```
The `KexAlgorithm` enum dispatches via `enum_dispatch`:
```rust
pub(crate) enum KexAlgorithm {
DhGroupKexSha1(DhGroupKex<Sha1>),
DhGroupKexSha256(DhGroupKex<Sha256>),
DhGroupKexSha512(DhGroupKex<Sha512>),
Curve25519Kex(Curve25519Kex),
EcdhNistP256Kex(EcdhNistPKex<NistP256, Sha256>),
EcdhNistP384Kex(EcdhNistPKex<NistP384, Sha384>),
EcdhNistP521Kex(EcdhNistPKex<NistP521, Sha512>),
MlKem768X25519Kex(MlKem768X25519Kex),
NoneKexAlgorithm(NoneKexAlgorithm),
}
```
### Supported KEX Algorithms
| Algorithm Name | Constant | Type | Hash | Notes |
|----------------|----------|------|------|-------|
| `mlkem768x25519-sha256` | `MLKEM768X25519_SHA256` | PQ/T Hybrid (ML-KEM + X25519) | SHA-256 | **Default first choice**, post-quantum |
| `curve25519-sha256` | `CURVE25519` | Curve25519 ECDH | SHA-256 | Recommended |
| `curve25519-sha256@libssh.org` | `CURVE25519_PRE_RFC_8731` | Curve25519 ECDH | SHA-256 | Pre-RFC name, same impl |
| `ecdh-sha2-nistp256` | `ECDH_SHA2_NISTP256` | NIST P-256 ECDH | SHA-256 | |
| `ecdh-sha2-nistp384` | `ECDH_SHA2_NISTP384` | NIST P-384 ECDH | SHA-384 | |
| `ecdh-sha2-nistp521` | `ECDH_SHA2_NISTP521` | NIST P-521 ECDH | SHA-512 | |
| `diffie-hellman-group-exchange-sha256` | `DH_GEX_SHA256` | DH-GEX | SHA-256 | Dynamic group |
| `diffie-hellman-group-exchange-sha1` | `DH_GEX_SHA1` | DH-GEX | SHA-1 | Legacy |
| `diffie-hellman-group14-sha256` | `DH_G14_SHA256` | DH Group 14 | SHA-256 | 2048-bit |
| `diffie-hellman-group14-sha1` | `DH_G14_SHA1` | DH Group 14 | SHA-1 | Legacy |
| `diffie-hellman-group15-sha512` | `DH_G15_SHA512` | DH Group 15 | SHA-512 | 3072-bit |
| `diffie-hellman-group16-sha512` | `DH_G16_SHA512` | DH Group 16 | SHA-512 | 4096-bit |
| `diffie-hellman-group17-sha512` | `DH_G17_SHA512` | DH Group 17 | SHA-512 | 6144-bit |
| `diffie-hellman-group18-sha512` | `DH_G18_SHA512` | DH Group 18 | SHA-512 | 8192-bit |
| `diffie-hellman-group1-sha1` | `DH_G1_SHA1` | DH Group 1 | SHA-1 | **Insecure**, 1024-bit |
| `none` | `NONE` | No exchange | N/A | Testing only |
### Curve25519 Implementation
Located in `kex/curve25519.rs`. Uses `curve25519-dalek` for the Diffie-Hellman computation:
- Generates an ephemeral keypair
- Sends the public key as the DH init
- Computes the shared secret from the remote public key
- The shared secret is encoded as a raw 32-byte string (not mpint) in the exchange hash
### NIST ECDH Implementation
Located in `kex/ecdh_nistp.rs`. Generic over the curve (`NistP256`, `NistP384`, `NistP521`):
- Uses `p256`/`p384`/`p521` crates for ECDH
- Points are encoded in the SSH EC point format (32/48/66 bytes for uncompressed)
### DH Group Exchange Implementation
Located in `kex/dh/`. Two variants: `DhGroupKex<D>` (GEX) and fixed-group variants.
- GEX uses `num-bigint` for modular exponentiation with safe primes
- Fixed groups use pre-defined safe primes from RFC 3526
- `DhGroup` struct: `{ prime: CryptoVec, generator: CryptoVec }`
- Server provides groups via `Handler::lookup_dh_gex_group()` — default uses `BUILTIN_SAFE_DH_GROUPS`
- `GexParams` controls min/preferred/max group sizes (default: 3072/8192/8192 bits)
### ML-KEM Hybrid Implementation
Located in `kex/hybrid_mlkem.rs`. Implements the `mlkem768x25519-sha256` hybrid key exchange:
- Combines ML-KEM-768 (post-quantum) with X25519 (classical)
- Both shared secrets are concatenated before hashing
- Provides quantum-resistant key exchange while maintaining classical security
### Exchange Hash Computation
The exchange hash `H` is computed from the following data (per RFC 4253 §8):
```
H = HASH(V_C || V_S || I_C || I_S || K_S || e || f || K)
```
Where:
- `V_C` = client version string
- `V_S` = server version string
- `I_C` = client's KEXINIT payload
- `I_S` = server's KEXINIT payload
- `K_S` = server host key
- `e` = client ephemeral public key
- `f` = server ephemeral public key
- `K` = shared secret
For DH-GEX, additional fields are included (p, g, and the GEX parameters).
---
## Ciphers (`cipher` module)
The `Cipher` trait defines how to construct opening and sealing keys:
```rust
pub(crate) trait Cipher {
fn needs_mac(&self) -> bool; // Whether a separate MAC is needed
fn key_len(&self) -> usize;
fn nonce_len(&self) -> usize;
fn make_opening_key(&self, key, nonce, mac_key, mac) -> Box<dyn OpeningKey + Send>;
fn make_sealing_key(&self, key, nonce, mac_key, mac) -> Box<dyn SealingKey + Send>;
}
```
### Supported Ciphers
| Algorithm Name | Constant | Type | Key Size | MAC Required | AEAD |
|----------------|----------|------|----------|-------------|------|
| `chacha20-poly1305@openssh.com` | `CHACHA20_POLY1305` | Stream cipher + Poly1305 | 256-bit + 256-bit | No | Yes |
| `aes256-gcm@openssh.com` | `AES_256_GCM` | AES-GCM | 256-bit | No | Yes |
| `aes128-gcm@openssh.com` | `AES_128_GCM` | AES-GCM | 128-bit | No | Yes |
| `aes256-ctr` | `AES_256_CTR` | AES-CTR | 256-bit | Yes | No |
| `aes192-ctr` | `AES_192_CTR` | AES-CTR | 192-bit | Yes | No |
| `aes128-ctr` | `AES_128_CTR` | AES-CTR | 128-bit | Yes | No |
| `aes256-cbc` | `AES_256_CBC` | AES-CBC | 256-bit | Yes | No |
| `aes192-cbc` | `AES_192_CBC` | AES-CBC | 192-bit | Yes | No |
| `aes128-cbc` | `AES_128_CBC` | AES-CBC | 128-bit | Yes | No |
| `3des-cbc` | `TRIPLE_DES_CBC` | 3DES-CBC | 168-bit | Yes | No | (feature `des`) |
### AEAD Ciphers (Chacha20-Poly1305, AES-GCM)
AEAD ciphers do not need a separate MAC. They handle both encryption and authentication:
- **Chacha20-Poly1305**: Uses the OpenSSH construction where the sequence number is used as the Chacha20 counter. The Poly1305 key is derived from a separate Chacha20 stream.
- **AES-GCM**: Uses `aws-lc-rs` or `ring` as the backend depending on feature flags. The nonce is constructed from the sequence number.
### Block Ciphers (AES-CTR, AES-CBC, 3DES-CBC)
Block ciphers require a separate MAC. They implement `SshBlockCipher<C>`:
- **CTR mode**: Uses `ctr::Ctr128BE` from the `ctr` crate
- **CBC mode**: Uses `cbc::Encryptor`/`cbc::Decryptor` from the `cbc` crate with PKCS#7 padding
- The `needs_mac()` method returns `true`
### `OpeningKey` and `SealingKey` Traits
```rust
pub(crate) trait OpeningKey {
fn packet_length_to_read_for_block_length(&self) -> usize { 4 }
fn decrypt_packet_length(&self, seqn: u32, encrypted_packet_length: &[u8]) -> [u8; 4];
fn tag_len(&self) -> usize;
fn open<'a>(&mut self, seqn: u32, ciphertext_and_tag: &'a mut [u8]) -> Result<&'a [u8], Error>;
}
pub(crate) trait SealingKey {
fn padding_length(&self, plaintext: &[u8]) -> usize;
fn fill_padding(&self, padding_out: &mut [u8]);
fn tag_len(&self) -> usize;
fn seal(&mut self, seqn: u32, plaintext_in_ciphertext_out: &mut [u8], tag_out: &mut [u8]);
fn write(&mut self, payload: &[u8], buffer: &mut SSHBuffer);
}
```
The `write()` method on `SealingKey` handles the full packet construction:
1. Compute padding length (minimum 4 bytes, block-aligned)
2. Write packet length (4 bytes) + padding length (1 byte) + payload + padding
3. Encrypt the packet
4. Append the authentication tag
5. Increment the sequence number
---
## MACs (`mac` module)
MAC algorithms are split into two categories: regular and Encrypt-Then-MAC (ETM).
### Supported MACs
| Algorithm Name | Constant | Hash | Key Length | ETM |
|----------------|----------|------|-----------|-----|
| `hmac-sha2-512-etm@openssh.com` | `HMAC_SHA512_ETM` | SHA-512 | 64 bytes | Yes |
| `hmac-sha2-256-etm@openssh.com` | `HMAC_SHA256_ETM` | SHA-256 | 32 bytes | Yes |
| `hmac-sha2-512` | `HMAC_SHA512` | SHA-512 | 64 bytes | No |
| `hmac-sha2-256` | `HMAC_SHA256` | SHA-256 | 32 bytes | No |
| `hmac-sha1-etm@openssh.com` | `HMAC_SHA1_ETM` | SHA-1 | 20 bytes | Yes |
| `hmac-sha1` | `HMAC_SHA1` | SHA-1 | 20 bytes | No |
| `none` | `NONE` | — | 0 | — |
### ETM (Encrypt-Then-MAC) Mode
With ETM MACs:
- The packet length field is sent unencrypted (read first)
- The rest of the packet is encrypted
- The MAC is computed over the unencrypted length + encrypted payload
- This prevents padding oracle attacks
### Regular MAC Mode
With regular MACs:
- The entire packet (including length) is encrypted
- The MAC is computed over the sequence number + unencrypted packet
- This is the traditional SSH MAC construction
---
## Key Handling (`keys` module)
### Key Formats Supported
- **OpenSSH format** (private keys): `-----BEGIN OPENSSH PRIVATE KEY-----` with bcrypt-pbkdf encrypted keys
- **PKCS#8** (unencrypted and encrypted): `-----BEGIN PRIVATE KEY-----` / `-----BEGIN ENCRYPTED PRIVATE KEY-----`
- **PKCS#1** (RSA): `-----BEGIN RSA PRIVATE KEY-----`
- **SEC1** (EC): `-----BEGIN EC PRIVATE KEY-----`
- **OpenSSH public keys**: `ssh-ed25519 AAAAC3N...`
- **PPK format**: PuTTY private key files
- **OpenSSH certificates**: `-----BEGIN OPENSSH SSH2 CERTIFICATE-----`
### Key Algorithms
| Algorithm | Key Type | Signing |
|-----------|----------|---------|
| `ssh-ed25519` | Ed25519 | Ed25519 |
| `ecdsa-sha2-nistp256` | ECDSA P-256 | SHA-256 |
| `ecdsa-sha2-nistp384` | ECDSA P-384 | SHA-384 |
| `ecdsa-sha2-nistp521` | ECDSA P-521 | SHA-512 |
| `rsa-sha2-512` | RSA | SHA-512 |
| `rsa-sha2-256` | RSA | SHA-256 |
| `ssh-rsa` | RSA | SHA-1 (legacy) |
### `PrivateKeyWithHashAlg`
Wrapper that pairs a private key with a specific hash algorithm (needed for RSA key disambiguation):
```rust
pub struct PrivateKeyWithHashAlg {
key: Arc<PrivateKey>,
hash: Option<HashAlg>,
}
```
This is required because RSA keys can sign with different hash algorithms (`rsa-sha2-256`, `rsa-sha2-512`, `ssh-rsa`), and the server needs to know which one the client intends.
### SSH Agent Protocol (`keys::agent`)
Russh implements both the client and server sides of the SSH agent protocol:
**Client** (`agent::client::AgentClient`):
```rust
impl<R: AsyncRead + AsyncWrite + Unpin + Send + 'static> AgentClient<R> {
pub async fn request_identities(&mut self) -> Result<Vec<AgentIdentity>, Error>;
pub async fn sign_request(&mut self, key: &AgentIdentity, hash_alg: Option<HashAlg>, data: Vec<u8>) -> Result<Vec<u8>, Error>;
pub async fn add_identity(&mut self, key: &PrivateKey, constraints: &[Constraint]) -> Result<(), Error>;
pub async fn remove_identity(&mut self, key: &PublicKey) -> Result<(), Error>;
pub async fn remove_all_identities(&mut self) -> Result<(), Error>;
pub async fn lock(&mut self, passphrase: &str) -> Result<(), Error>;
pub async fn unlock(&mut self, passphrase: &str) -> Result<(), Error>;
// ... ping, add smartcard, etc.
}
```
**Server** (`agent::server::Agent`):
```rust
pub trait Agent: Clone + Send + 'static {
fn confirm(self, key: Arc<PrivateKey>) -> Box<dyn Future<Output = (Self, bool)> + Send + Unpin>;
}
```
**AgentIdentity** distinguishes between plain public keys and certificates:
```rust
pub enum AgentIdentity {
PublicKey { pubkey: PublicKey, comment: String },
Certificate { certificate: Certificate, comment: String },
}
```
### Known Hosts (`keys::known_hosts`)
```rust
pub fn check_known_hosts(host: &str, port: u16, key: &PublicKey) -> Result<(), Error>;
pub fn check_known_hosts_path<P: AsRef<Path>>(path: P, host: &str, port: u16, key: &PublicKey) -> Result<(), Error>;
```
These functions read `~/.ssh/known_hosts` and verify the server's public key matches. Returns `Error::KeyChanged` on mismatch.
### Windows Pageant Support
On Windows, russh uses the `pageant` crate to communicate with the Pageant SSH agent via either:
- WM_MESSAGE-based protocol (legacy)
- Named pipes protocol (since PuTTY 0.75)

View File

@@ -0,0 +1,430 @@
# Russh: Internal Architecture & Data Flow
This document covers the internal mechanics — session state machines, the event loop, packet handling, buffering, and the interaction between components.
## Session State
### `CommonSession<C>`
The shared session state used by both client and server:
```rust
pub(crate) struct CommonSession<C> {
pub auth_user: String,
pub remote_sshid: Vec<u8>,
pub config: C, // Arc<client::Config> or Arc<server::Config>
pub encrypted: Option<Encrypted>,
pub auth_method: Option<auth::Method>, // Client only
pub auth_attempts: usize,
pub packet_writer: PacketWriter,
pub remote_to_local: Box<dyn OpeningKey + Send>,
pub wants_reply: bool,
pub disconnected: bool,
pub buffer: Vec<u8>, // Incoming packet scratch buffer
pub strict_kex: bool,
pub alive_timeouts: usize,
pub received_data: bool,
}
```
### `Encrypted`
The state after encryption keys are established:
```rust
pub(crate) struct Encrypted {
pub state: EncryptedState,
pub exchange: Option<Exchange>,
pub kex: KexAlgorithm,
pub key: usize,
pub client_mac: mac::Name,
pub server_mac: mac::Name,
pub session_id: CryptoVec, // Constant across rekeys
pub channels: HashMap<ChannelId, ChannelParams>,
pub last_channel_id: Wrapping<u32>,
pub write: Vec<u8>, // Outgoing packet assembly buffer
pub write_cursor: usize, // Current position in write buffer
pub last_rekey: Instant,
pub server_compression: Compression,
pub client_compression: Compression,
pub decompress: Decompress,
pub rekey_wanted: bool,
pub received_extensions: Vec<String>,
pub extension_info_awaiters: HashMap<String, Vec<oneshot::Sender<()>>>,
}
```
### `EncryptedState`
```rust
pub enum EncryptedState {
WaitingAuthServiceRequest { sent: bool, accepted: bool },
WaitingAuthRequest(auth::AuthRequest),
InitCompression,
Authenticated,
}
```
### `Exchange`
Protocol values collected during key exchange (all non-secret, visible on wire):
```rust
pub struct Exchange {
pub client_id: Vec<u8>,
pub server_id: Vec<u8>,
pub client_kex_init: Vec<u8>,
pub server_kex_init: Vec<u8>,
pub client_ephemeral: Vec<u8>,
pub server_ephemeral: Vec<u8>,
pub gex: Option<(GexParams, DhGroup)>,
}
```
### `NewKeys`
Produced when key exchange completes, contains everything needed to activate encryption:
```rust
pub(crate) struct NewKeys {
pub exchange: Exchange,
pub names: negotiation::Names,
pub kex: KexAlgorithm,
pub key: usize,
pub cipher: CipherPair, // { local_to_remote, remote_to_local }
pub session_id: CryptoVec,
}
```
### `ChannelParams`
Internal channel state (not exposed to users):
```rust
pub(crate) struct ChannelParams {
pub recipient_channel: u32, // Remote channel ID
pub sender_channel: ChannelId, // Local channel ID
pub recipient_window_size: u32,
pub sender_window_size: u32,
pub recipient_maximum_packet_size: u32,
pub sender_maximum_packet_size: u32,
pub confirmed: bool, // Whether server confirmed the channel
pub wants_reply: bool,
pub pending_data: VecDeque<(Bytes, Option<u32>, usize)>, // (data, ext_code, offset)
pub pending_eof: bool,
pub pending_close: bool,
}
```
---
## Client Event Loop
The client event loop is the core of `client::Session::run_inner()`:
```rust
async fn run_inner<H, R>(&mut self, stream_read, stream_write, handler, kex_done_signal)
-> Result<RemoteDisconnectInfo, H::Error>
{
// Initial setup
self.flush()?;
self.common.packet_writer.flush_into(stream_write).await?;
// Set up timers
let keepalive_timer = ...;
let inactivity_timer = ...;
let reading = start_reading(stream_read, buffer, opening_cipher);
while !self.common.disconnected {
tokio::select! {
// 1. Incoming SSH packet
r = &mut reading => {
// Decrypt and decompress packet
// Process DISCONNECT or pass to reply()
// Restart reading
}
// 2. Keepalive timer
() = &mut keepalive_timer => {
// Send keepalive if authenticated
// Track timeout count
}
// 3. Inactivity timer
() = &mut inactivity_timer => {
// Return InactivityTimeout error
}
// 4. Outgoing messages from Handle
msg = self.receiver.recv(), if !self.kex.active() => {
// Process message (auth, channel open, data, etc.)
// Batch all pending outgoing messages
}
// 5. Inbound channel messages
msg = self.inbound_channel_receiver.recv(), if !self.kex.active() => {
// Process channel data/eof/close
// Batch all pending messages
}
};
// Flush all pending writes
self.flush()?;
self.common.packet_writer.flush_into(stream_write).await?;
// Handle deferred compression after authentication
if EncryptedState::InitCompression { ... } {
// Init client compression if deferred (zlib@openssh.com)
// Transition to Authenticated
}
// Reset timers if data received or keepalive sent
}
}
```
### Key Event Loop Behaviors
1. **Kex blocking**: When `self.kex.active()` is true, outgoing messages from `Handle` are NOT processed. This prevents sending data during key exchange.
2. **Batching**: After receiving one message from `receiver`, all pending messages are drained with `try_recv()` to batch writes.
3. **Keepalive management**: Keepalive timer resets when data is received. `alive_timeouts` tracks consecutive unanswered keepalives.
4. **Compression activation**: `zlib@openssh.com` compression is deferred until after authentication succeeds (handled by `InitCompression` state).
---
## Server Event Loop
Similar to the client, but the server accepts connections via `run_on_socket()` or `run_stream()`:
```rust
// Server::run_on_socket
loop {
tokio::select! {
_ = shutdown_rx.recv() => { /* Graceful shutdown */ },
accept_result = socket.accept() => {
// For each connection:
// 1. Create a new Handler via Server::new_client()
// 2. Call run_stream() in a spawned task
// 3. Wait for session or shutdown
},
error = error_rx.recv() => {
// Report session errors
}
}
}
```
### `run_stream`
```rust
pub async fn run_stream<H, R>(config, stream, handler) -> Result<RunningSession<H>, H::Error>
{
// 1. Write server SSH ID
// 2. Read client SSH ID
// 3. Create Session with CommonSession state
// 4. Begin initial rekey (sends KEXINIT)
// 5. Spawn session.run() in a task
// 6. Return RunningSession (implements Future)
}
```
### `RunningServer` and `RunningServerHandle`
The server returns a `RunningServer` that implements `Future` and a `RunningServerHandle` for graceful shutdown:
```rust
pub struct RunningServer<F: Future<Output = io::Result<()>> + Unpin + Send> {
inner: F,
shutdown_tx: broadcast::Sender<String>,
}
impl RunningServerHandle {
pub fn shutdown(&self, reason: String) {
let _ = self.shutdown_tx.send(reason);
}
}
```
---
## Packet Handling Pipeline
### Reading Packets (`cipher::read`)
```
1. Read 4 bytes (or more for block ciphers) → encrypted packet length
2. Decrypt packet length
3. Parse length, check against MAXIMUM_PACKET_LEN (262159 bytes)
4. Read remaining bytes (length + tag_len - already_read)
5. Decrypt the ciphertext (including tag verification for AEAD)
6. Remove padding
7. Increment sequence number
```
### Writing Packets (`SealingKey::write`)
```
1. Compute padding length (block-aligned, min 4 bytes)
2. Write: [packet_length (4B)] [padding_length (1B)] [payload] [padding] [tag]
3. Encrypt the packet
4. Compute and append MAC/tag
5. Increment sequence number
6. Add payload bytes to rekey counter
```
### Packet Assembly (`PacketWriter`)
```rust
pub(crate) struct PacketWriter {
cipher: Box<dyn SealingKey + Send>,
compress: Compress,
compress_buffer: Vec<u8>,
write_buffer: SSHBuffer,
}
```
Methods:
- `packet_raw(buf)`: Compress and encrypt a raw packet
- `packet(f)`: Build a packet via closure, compress, encrypt, return the plaintext
- `flush_into(w)`: Write all buffered data to the async writer
- `set_cipher(c)`: Swap the cipher (for rekeying)
- `reset_seqn()`: Reset sequence number (for strict kex)
### `push_packet!` Macro
Used throughout for building packets:
```rust
macro_rules! push_packet {
( $buffer:expr, $x:expr ) => {{
let i0 = $buffer.len();
$buffer.extend(b"\0\0\0\0"); // Placeholder for length
let x = $x; // Build the packet body
let i1 = $buffer.len();
BigEndian::write_u32(&mut buf[i0..], (i1 - i0 - 4) as u32);
x
}};
}
```
---
## Channel Data Flow
### Client-side Channel Creation
```
1. Handle::channel_open_session()
→ Creates ChannelRef with mpsc::channel
→ Sends Msg::ChannelOpenSession to session
2. Session::handle_msg(Msg::ChannelOpenSession)
→ Calls self.channel_open_session()
→ Sends CHANNEL_OPEN packet with channel type "session"
→ Stores ChannelRef in self.channels
3. Server responds with CHANNEL_OPEN_CONFIRMATION
→ Session updates ChannelParams (recipient_channel, window sizes)
→ Sends ChannelMsg::Open through ChannelRef's sender
4. wait_channel_confirmation() receives the Open message
→ Creates Channel { read_half, write_half }
→ Returns the Channel to the caller
```
### Data Transmission (Client → Server)
```
1. channel.data(reader)
→ Uses ChannelTx (AsyncWrite) that reads from the reader
→ Sends ChannelMsg::Data through the channel sender
→ Waits for window availability via WindowSizeRef
2. Session receives ChannelMsg::Data
→ Calls Encrypted::data(channel, bytes, is_rekeying)
→ If pending data exists or rekeying: queues in pending_data
→ Otherwise: writes CHANNEL_DATA packet immediately
→ If window exhausted: queues remaining data
3. PacketWriter encrypts and buffers the packet
4. Event loop flushes the buffer to the TCP stream
```
### Data Reception (Server → Client)
```
1. Encrypted packet arrives, decrypted, decompressed
2. Packet type = CHANNEL_DATA
3. Encrypted::adjust_window_size() checks if window needs adjustment
4. ChannelMsg::Data sent through ChannelRef's sender
5. Channel::wait() returns the ChannelMsg::Data
6. ChannelRx (AsyncRead) reads from the channel receiver
```
---
## Rekeying Flow
```
1. Trigger:
- Bytes written/read exceed limits
- Time since last rekey exceeds limit
- Explicit: Handle::rekey_soon()
- Server-initiated: receiving KEXINIT when idle
2. Session::begin_rekey():
- Creates new ClientKex/ServerKex
- Sends new KEXINIT
- Sets kex state to InProgress
3. During rekey:
- Outgoing messages from Handle are blocked (!kex.active())
- Incoming non-kex packets are buffered in pending_reads
- Kex state machine processes kex packets (KEXINIT, DH init/reply, NEWKEYS)
4. On completion:
- Flush all pending channel data
- Process buffered pending_reads
- Call CommonSession::newkeys() to swap ciphers
- Reset byte counters
- Set kex state to Idle
- Resume processing outgoing messages
```
---
## Sub-Crates
### `russh-cryptovec` (cryptovec/)
A `Vec<u8>` alternative that:
- Zeroes memory on drop and reallocation (via `memset` / `ExplicitZero`)
- Locks memory pages with `mlock` (Unix) / `VirtualLock` (Windows) to prevent swapping
- Uses `unsafe` for performance-critical operations (copying, initialization)
- Integrates with `ssh-encoding` for Encode support
Used for all sensitive data: session keys, shared secrets, MAC keys, exchange hashes.
### `russh-util` (russh-util/)
Runtime abstraction layer:
- `russh_util::runtime::spawn()` — spawns a task (tokio or wasm)
- `russh_util::runtime::JoinHandle` — task join handle
- `russh_util::time::Instant` — time source (tokio or chrono for WASM)
- WASM compatibility: uses `wasm-bindgen-futures` and `chrono` when target is `wasm32`
### `russh-config` (russh-config/)
SSH config file parser:
- Parses `~/.ssh/config` format
- Supports `Host` matching with `globset`
- Provides `Stream::tcp_connect()` and `Stream::proxy_command()` for establishing connections based on config
### `pageant` (pageant/)
Windows Pageant SSH agent transport:
- `wmmessage` feature: Classic Pageant protocol via Windows messages
- `namedpipes` feature: Modern Pageant protocol via named pipes (PuTTY ≥ 0.75)

View File

@@ -0,0 +1,413 @@
# Russh: Usage Patterns & Examples
This document provides practical usage patterns for both client and server sides of russh.
## Minimal Client
The simplest client connects, authenticates, opens a session channel, and runs a command:
```rust
use std::sync::Arc;
use russh::*;
use russh::keys::*;
struct Client;
impl client::Handler for Client {
type Error = russh::Error;
async fn check_server_key(&mut self, key: &ssh_key::PublicKey) -> Result<bool, Self::Error> {
// In production: verify against known_hosts
Ok(true)
}
}
async fn run_command(host: &str, command: &str) -> Result<u32, Box<dyn std::error::Error>> {
let config = Arc::new(client::Config::default());
let mut session = client::connect(config, (host, 22), Client).await?;
let auth = session.authenticate_publickey(
"user",
PrivateKeyWithHashAlg::new(
Arc::new(keys::load_secret_key("/path/to/key", None)?),
session.best_supported_rsa_hash().await?.flatten(),
),
).await?;
if !auth.success() {
return Err("Auth failed".into());
}
let mut channel = session.channel_open_session().await?;
channel.exec(true, command).await?;
let mut exit_code = 0;
let mut stdout = tokio::io::stdout();
loop {
let Some(msg) = channel.wait().await else { break };
match msg {
ChannelMsg::Data { data } => { stdout.write_all(&data).await?; }
ChannelMsg::ExtendedData { data, ext } => {
// ext == 1 is stderr
eprint!("{}", String::from_utf8_lossy(&data));
}
ChannelMsg::ExitStatus { exit_status } => { exit_code = exit_status; }
_ => {}
}
}
session.disconnect(Disconnect::ByApplication, "", "en").await?;
Ok(exit_code)
}
```
## Interactive PTY Client
For interactive sessions (shell access), request a PTY and handle window changes:
```rust
async fn interactive_session(session: &client::Handle<Client>) -> Result<Channel<client::Msg>, russh::Error> {
let mut channel = session.channel_open_session().await?;
// Request PTY
channel.request_pty(
false, // want_reply
"xterm-256bit", // term type
80, 24, // cols, rows
0, 0, // pixel width/height (0 = not specified)
&[], // terminal modes
).await?;
// Request shell
channel.request_shell(true).await?;
Ok(channel)
}
```
## Server Implementation
A basic server that accepts all public keys and echoes input:
```rust
use std::sync::Arc;
use russh::server::{self, Server as _, Session};
use russh::*;
#[derive(Clone)]
struct App;
impl server::Server for App {
type Handler = ClientHandler;
fn new_client(&mut self, _: Option<std::net::SocketAddr>) -> ClientHandler {
ClientHandler
}
}
struct ClientHandler;
impl server::Handler for ClientHandler {
type Error = russh::Error;
async fn auth_publickey(&mut self, _: &str, _: &ssh_key::PublicKey) -> Result<server::Auth, Self::Error> {
Ok(server::Auth::Accept)
}
async fn channel_open_session(
&mut self,
channel: Channel<server::Msg>,
session: &mut Session,
) -> Result<bool, Self::Error> {
Ok(true)
}
async fn data(
&mut self,
channel: ChannelId,
data: &[u8],
session: &mut Session,
) -> Result<(), Self::Error> {
// Echo back
session.data(channel, data.to_vec().into())?;
Ok(())
}
async fn exec_request(
&mut self,
channel: ChannelId,
data: &[u8],
session: &mut Session,
) -> Result<(), Self::Error> {
// Handle exec request
session.channel_success(channel);
// Process the command...
session.channel_failure(channel);
Ok(())
}
}
async fn run_server() {
let config = Arc::new(server::Config {
keys: vec![russh::keys::PrivateKey::random(&mut rand::rng(), russh::keys::Algorithm::Ed25519).unwrap()],
..Default::default()
});
let mut app = App;
let socket = tokio::net::TcpListener::bind(("0.0.0.0", 22)).await.unwrap();
app.run_on_socket(config, &socket).await.unwrap();
}
```
## Authentication with SSH Agent
```rust
use russh::keys::agent::client::AgentClient;
async fn auth_with_agent(
session: &mut client::Handle<Client>,
user: &str,
) -> Result<AuthResult, Box<dyn std::error::Error>> {
// Connect to SSH agent (Unix socket or Windows Pageant)
let stream = tokio::net::UnixStream::connect(std::env::var("SSH_AUTH_SOCK")?).await?;
let mut agent = AgentClient::connect(stream);
// List available identities
let identities = agent.request_identities().await?;
// Try each identity
for identity in &identities {
let result = session.authenticate_publickey_with(
user,
identity.public_key(),
None, // hash_alg (None for Ed25519)
&mut agent,
).await?;
if result.success() {
return Ok(result);
}
}
Err("No matching key found in agent".into())
}
```
## Port Forwarding
### Local TCP Forwarding
```rust
async fn local_forward(
session: &client::Handle<Client>,
target_host: &str,
target_port: u16,
) -> Result<Channel<client::Msg>, russh::Error> {
let channel = session.channel_open_direct_tcpip(
target_host,
target_port as u32,
"127.0.0.1", // originator
0, // originator port
).await?;
Ok(channel)
}
```
### Remote TCP Forwarding
```rust
async fn remote_forward(
session: &client::Handle<Client>,
listen_addr: &str,
listen_port: u32,
) -> Result<u32, russh::Error> {
// Request server to listen
let assigned_port = session.tcpip_forward(listen_addr, listen_port).await?;
Ok(assigned_port)
}
```
## Channel as AsyncRead/AsyncWrite
Channels can be converted to `AsyncRead` + `AsyncWrite` streams:
```rust
async fn use_channel_as_stream(channel: Channel<client::Msg>) {
// Convert to bidirectional stream
let stream = channel.into_stream();
// Or split into read/write halves
let (read_half, write_half) = channel.split();
let mut reader = read_half.make_reader();
let writer = write_half.make_writer();
// Use with any tokio IO utility
let mut buf = vec![0u8; 1024];
use tokio::io::AsyncReadExt;
let n = reader.read(&mut buf).await.unwrap();
// Read stderr separately
let stderr_reader = read_half.make_reader_ext(Some(1)); // ext code 1 = stderr
}
```
## Custom Algorithm Preferences
```rust
use std::borrow::Cow;
let config = client::Config {
preferred: Preferred {
kex: Cow::Owned(vec![
russh::kex::CURVE25519,
russh::kex::EXTENSION_SUPPORT_AS_CLIENT,
]),
cipher: Cow::Owned(vec![
russh::cipher::CHACHA20_POLY1305,
russh::cipher::AES_256_GCM,
]),
mac: Cow::Owned(vec![
russh::mac::HMAC_SHA256_ETM,
]),
..Default::default()
},
..Default::default()
};
```
## Rekeying
```rust
// Automatic rekeying happens based on Limits (default 1GB / 1 hour)
// Explicit rekeying:
session.rekey_soon().await?;
// Access shared secret after kex (for custom key derivation):
struct MyHandler;
impl client::Handler for MyHandler {
type Error = russh::Error;
async fn kex_done(
&mut self,
shared_secret: Option<&[u8]>,
names: &Names,
session: &mut client::Session,
) -> Result<(), Self::Error> {
// shared_secret is the raw DH shared secret
// names contains all negotiated algorithms
Ok(())
}
async fn check_server_key(&mut self, key: &ssh_key::PublicKey) -> Result<bool, Self::Error> {
Ok(true)
}
}
```
## Keyboard-Interactive Authentication
```rust
async fn keyboard_interactive_auth(
session: &mut client::Handle<Client>,
user: &str,
) -> Result<AuthResult, russh::Error> {
let response = session.authenticate_keyboard_interactive_start(user, None).await?;
match response {
KeyboardInteractiveAuthResponse::Success => Ok(AuthResult::Success),
KeyboardInteractiveAuthResponse::Failure { .. } => Ok(AuthResult::Failure { remaining_methods: MethodSet::empty(), partial_success: false }),
KeyboardInteractiveAuthResponse::InfoRequest { prompts, .. } => {
// Respond to each prompt
let responses: Vec<String> = prompts.iter()
.map(|p| /* get user input for p.prompt */ String::new())
.collect();
let response = session.authenticate_keyboard_interactive_respond(responses).await?;
// May need to loop if server sends more challenges
// ...
todo!()
}
}
}
```
## Server: Handling Channel Requests
Server-side channel requests (shell, exec, pty, etc.) must be explicitly accepted or rejected:
```rust
impl server::Handler for MyHandler {
type Error = russh::Error;
async fn shell_request(
&mut self,
channel: ChannelId,
session: &mut Session,
) -> Result<(), Self::Error> {
// Must call success or failure!
session.channel_success(channel);
Ok(())
}
async fn exec_request(
&mut self,
channel: ChannelId,
data: &[u8],
session: &mut Session,
) -> Result<(), Self::Error> {
let command = String::from_utf8_lossy(data);
if is_allowed(&command) {
session.channel_success(channel);
// Process command, send data back...
session.data(channel, output.into())?;
session.eof(channel)?;
} else {
session.channel_failure(channel);
}
Ok(())
}
async fn pty_request(
&mut self,
channel: ChannelId,
term: &str,
col_width: u32,
row_height: u32,
pix_width: u32,
pix_height: u32,
modes: &[(Pty, u32)],
session: &mut Session,
) -> Result<(), Self::Error> {
session.channel_success(channel);
Ok(())
}
}
```
## Known Hosts Verification
```rust
use russh::keys::check_known_hosts;
impl client::Handler for SecureClient {
type Error = russh::Error;
async fn check_server_key(&mut self, key: &ssh_key::PublicKey) -> Result<bool, Self::Error> {
match check_known_hosts("example.com", 22, key) {
Ok(()) => Ok(true),
Err(e) => {
if matches!(e, russh::keys::Error::KeyChanged { .. }) {
// Possible MITM attack!
eprintln!("WARNING: Host key changed! Possible MITM attack.");
Ok(false)
} else {
// Unknown host - prompt user to verify fingerprint
eprintln!("Unknown host key. Fingerprint: {}", key.fingerprint(ssh_key::HashAlg::Sha256));
// In a real app, ask the user
Ok(false)
}
}
}
}
}
```