Files
alknet/docs/research/references/ssh/russh/06-usage-patterns.md

11 KiB

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:

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:

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:

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

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

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

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:

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

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

// 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

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:

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

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)
                }
            }
        }
    }
}