14 KiB
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 + AsyncWritestream, 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(request–response) and a high-levelSftpSession(std::fs-like API withAsyncRead/AsyncWritefile I/O) - Custom serde wire format — implements its own
Serializer/Deserializeroverbytes::BytesMutfor SFTP binary encoding, not using serde's typical self-describing formats - Concurrent writes — the high-level
Filetype 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