# 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, cert: Certificate }, FuturePublicKey { key: PublicKey, hash_alg: Option }, FutureCertificate { cert: Certificate, hash_alg: Option }, 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?; ```