Files
alknet/docs/research/references/ssh/russh-sftp/01-overview-and-architecture.md

14 KiB
Raw Blame History

russh-sftp: Overview and Architecture

Version: 2.3.0
Repository: https://github.com/AspectUnk/russh-sftp
License: Apache-2.0
Rust Edition: 2021
Protocol: SFTP v3 (draft-ietf-secsh-filexfer-02)

What It Is

russh-sftp is a Rust crate providing both SFTP client and server implementations. It operates on any transport that provides AsyncRead + AsyncWrite byte streams — not just russh. The crate targets SFTP protocol version 3, the most widely deployed version.

Core design decisions:

  • Transport-agnostic — works with any AsyncRead + AsyncWrite stream, not tied to a specific SSH library
  • Both client and server — provides handler traits and run functions for both sides
  • Two client tiers — a low-level RawSftpSession (requestresponse) and a high-level SftpSession (std::fs-like API with AsyncRead/AsyncWrite file I/O)
  • Custom serde wire format — implements its own Serializer/Deserializer over bytes::BytesMut for SFTP binary encoding, not using serde's typical self-describing formats
  • Concurrent writes — the high-level File type supports pipelined writes with configurable concurrency
  • WASM support — the server module is gated behind not(target_arch = "wasm32"); client runtime abstracts over tokio and wasm-bindgen-futures

Source Layout

russh-sftp/src/
├── lib.rs              # Crate root: module declarations, macro imports
├── buf.rs              # TryBuf trait: try_get_bytes/try_get_string on Buf
├── de.rs               # Custom serde Deserializer (binary SFTP wire format)
├── ser.rs              # Custom serde Serializer (binary SFTP wire format)
├── error.rs            # Top-level Error enum
├── extensions.rs       # OpenSSH extension types: limits, hardlink, fsync, statvfs
├── utils.rs            # unix() time helper, read_packet() async wire reader
├── protocol/           # SFTP v3 message types and Packet enum
│   ├── mod.rs          # Packet enum, type constants, TryFrom<Bytes>/Into<Bytes>
│   ├── init.rs         # SSH_FXP_INIT
│   ├── version.rs      # SSH_FXP_VERSION
│   ├── open.rs         # SSH_FXP_OPEN + OpenFlags bitflags
│   ├── close.rs        # SSH_FXP_CLOSE
│   ├── read.rs         # SSH_FXP_READ
│   ├── write.rs        # SSH_FXP_WRITE
│   ├── lstat.rs        # SSH_FXP_LSTAT
│   ├── stat.rs         # SSH_FXP_STAT
│   ├── fstat.rs        # SSH_FXP_FSTAT
│   ├── setstat.rs      # SSH_FXP_SETSTAT
│   ├── fsetstat.rs     # SSH_FXP_FSETSTAT
│   ├── opendir.rs      # SSH_FXP_OPENDIR
│   ├── readdir.rs      # SSH_FXP_READDIR
│   ├── remove.rs       # SSH_FXP_REMOVE
│   ├── mkdir.rs        # SSH_FXP_MKDIR
│   ├── rmdir.rs        # SSH_FXP_RMDIR
│   ├── realpath.rs     # SSH_FXP_REALPATH
│   ├── rename.rs       # SSH_FXP_RENAME
│   ├── readlink.rs     # SSH_FXP_READLINK
│   ├── symlink.rs      # SSH_FXP_SYMLINK
│   ├── status.rs       # SSH_FXP_STATUS + StatusCode enum
│   ├── handle.rs       # SSH_FXP_HANDLE
│   ├── data.rs         # SSH_FXP_DATA
│   ├── name.rs         # SSH_FXP_NAME
│   ├── attrs.rs        # SSH_FXP_ATTRS
│   ├── extended.rs     # SSH_FXP_EXTENDED / SSH_FXP_EXTENDED_REPLY
│   ├── file.rs         # File struct (filename + longname + attrs)
│   └── file_attrs.rs   # FileAttributes, FileAttr flags, FileMode, FileType, FilePermissions
├── client/             # Client-side implementation
│   ├── mod.rs          # Config, run(), execute_handler()
│   ├── handler.rs      # Client Handler trait
│   ├── rawsession.rs   # RawSftpSession: request-response SFTP client
│   ├── session.rs      # SftpSession: high-level std::fs-like client
│   ├── error.rs        # Client-specific Error enum
│   ├── runtime.rs      # Runtime abstraction (tokio native vs WASM)
│   └── fs/
│       ├── mod.rs      # Re-exports: File, DirEntry, ReadDir, Metadata
│       ├── file.rs     # File: AsyncRead + AsyncWrite + AsyncSeek
│       └── dir.rs      # DirEntry, ReadDir iterator
└── server/             # Server-side implementation
    ├── mod.rs          # Config, run(), run_with_config(), process_request()
    ├── handler.rs     # Server Handler trait
    └── reply.rs        # StatusReply type for error responses

Key Dependencies

Dependency Version Purpose
tokio 1 Async runtime: io-util, rt, sync, time, macros
tokio-util 0.7.18 Runtime utilities
serde 1.0 Derive macros for protocol types
serde_bytes 0.11 Efficient byte array serialization
bitflags 2.11 Bitflag types: OpenFlags, FileAttr, FileMode, FilePermissionFlags
bytes 1.11 BytesMut/Bytes for zero-copy wire I/O
dashmap 6.1 Concurrent HashMap for request/response tracking
chrono 0.4 DateTime for File::longname formatting
thiserror 2.0 Error derive macros
log 0.4 Logging facade

Dev dependencies: russh 0.61.0 (for examples), criterion 0.8.2 (benchmarks).

Feature Flags

Feature Default Description
async-trait Enables #[async_trait] attribute on Handler traits

Architecture Diagram

┌──────────────────────────────────────────────────────────────────────┐
│                         Application Layer                            │
│                                                                      │
│  ┌───────────────────┐  ┌──────────────────────┐  ┌──────────────┐ │
│  │   SftpSession      │  │   RawSftpSession      │  │  server::    │ │
│  │   (high-level)     │  │   (low-level)          │  │  Handler     │ │
│  │                    │  │                        │  │  (user impl) │ │
│  │  • open/create     │  │  • init/open/close     │  │              │ │
│  │  • read/write      │  │  • read/write          │  │  init, open, │ │
│  │  • metadata        │  │  • stat/lstat/fstat    │  │  read, write │ │
│  │  • read_dir        │  │  • opendir/readdir     │  │  close, ...  │ │
│  │  • canonicalize    │  │  • mkdir/rmdir/remove  │  └──────┬───────┘ │
│  │  • hardlink/fsync  │  │  • symlink/readlink    │         │         │
│  │  • fs_info (statvfs)│  │  • extended            │         │         │
│  └────────┬──────────┘  └──────────┬────────────┘         │         │
│           │                         │                       │         │
│           │    File (AsyncIO)       │                       │         │
│           │  ┌─────────────────┐   │                       │         │
│           │  │ AsyncRead/Write  │   │                       │         │
│           │  │ AsyncSeek        │   │                       │         │
│           │  │ • pipelined writes│  │                       │         │
│           │  │ • handle tracking │  │                       │         │
│           │  └────────┬─────────┘   │                       │         │
│           └───────────┼─────────────┘                       │         │
│                       │                                     │         │
├───────────────────────┼─────────────────────────────────────┼─────────┤
│                       │    Protocol Layer                    │         │
│  ┌────────────────────┼─────────────────────────────────────┼───┐    │
│  │                   ▼                                     ▼   │    │
│  │   ┌─────────────────────────────────────────────────────┐  │    │
│  │   │                    Packet enum                       │  │    │
│  │   │  Init, Version, Open, Close, Read, Write,           │  │    │
│  │   │  Lstat, Fstat, SetStat, FSetStat, OpenDir,         │  │    │
│  │   │  ReadDir, Remove, MkDir, RmDir, RealPath,          │  │    │
│  │   │  Stat, Rename, ReadLink, Symlink, Status,           │  │    │
│  │   │  Handle, Data, Name, Attrs, Extended, ExtendedReply │  │    │
│  │   └─────────────────────────────────────────────────────┘  │    │
│  │                           │                                │    │
│  │              ┌────────────┴────────────┐                  │    │
│  │              │    ser.rs / de.rs        │                  │    │
│  │              │  Custom serde for binary │                  │    │
│  │              │  SFTP wire format         │                  │    │
│  │              └─────────────────────────┘                  │    │
│  └────────────────────────────────────────────────────────────┘    │
│                                  │                                  │
├──────────────────────────────────┼──────────────────────────────────┤
│                                  ▼                                  │
│                    ┌──────────────────────────┐                      │
│                    │  utils::read_packet()    │                      │
│                    │  buf::TryBuf             │                      │
│                    │  Wire I/O (length-prefixed)                    │
│                    └────────────┬─────────────┘                      │
│                                 │                                   │
├─────────────────────────────────┼───────────────────────────────────┤
│                                 ▼                                   │
│              ┌──────────────────────────────────────┐               │
│              │  Transport (AsyncRead + AsyncWrite)   │               │
│              │  e.g., russh Channel::into_stream()   │               │
│              └──────────────────────────────────────┘               │
└──────────────────────────────────────────────────────────────────────┘

How russh Integration Works

The crate does not depend on russh at runtime — it only appears as a dev-dependency for examples. Integration is by the caller providing a stream:

// From examples/client.rs — typical russh integration
let channel = session.channel_open_session().await.unwrap();
channel.request_subsystem(true, "sftp").await.unwrap();
let sftp = SftpSession::new(channel.into_stream()).await.unwrap();
// From examples/server.rs — typical russh server integration
async fn subsystem_request(&mut self, channel_id: ChannelId, name: &str, session: &mut Session)
    -> Result<(), Self::Error>
{
    if name == "sftp" {
        let channel = self.get_channel(channel_id).await;
        let sftp = SftpSession::default();
        session.channel_success(channel_id)?;
        russh_sftp::server::run(channel.into_stream(), sftp).await;
    }
    Ok(())
}

The into_stream() method on russh's Channel produces a type implementing AsyncRead + AsyncWrite + Unpin + Send + 'static, which is exactly what russh-sftp's run() and SftpSession::new() accept.

Re-exports from lib.rs

pub mod client;
pub mod de;
pub mod extensions;
pub mod protocol;
pub mod ser;
#[cfg(not(target_arch = "wasm32"))]
pub mod server;

// Key re-exports:
pub use client::Handler;        // client::Handler
pub use client::RawSftpSession; // low-level client
pub use client::SftpSession;    // high-level client
pub use server::Handler;        // server::Handler
pub use server::StatusReply;    // server error reply type