# 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 { // In production: verify against known_hosts Ok(true) } } async fn run_command(host: &str, command: &str) -> Result> { 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) -> Result, 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) -> ClientHandler { ClientHandler } } struct ClientHandler; impl server::Handler for ClientHandler { type Error = russh::Error; async fn auth_publickey(&mut self, _: &str, _: &ssh_key::PublicKey) -> Result { Ok(server::Auth::Accept) } async fn channel_open_session( &mut self, channel: Channel, session: &mut Session, ) -> Result { 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, user: &str, ) -> Result> { // 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, target_host: &str, target_port: u16, ) -> Result, 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, listen_addr: &str, listen_port: u32, ) -> Result { // 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) { // 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 { Ok(true) } } ``` ## Keyboard-Interactive Authentication ```rust async fn keyboard_interactive_auth( session: &mut client::Handle, user: &str, ) -> Result { 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 = 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 { 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) } } } } } ```