docs(research): add russh-sftp deep-dive reference

This commit is contained in:
2026-06-10 14:45:08 +00:00
parent f2a25f5bc1
commit f10dc23d13
7 changed files with 1927 additions and 0 deletions

View File

@@ -0,0 +1,212 @@
# 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](https://datatracker.ietf.org/doc/html/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:
```rust
// 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();
```
```rust
// 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`
```rust
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
```