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

7.1 KiB

sftp-rs: Overview and Architecture

Version: 0.3.0
Repository: https://github.com/jelmer/sftp-rs
License: Apache-2.0
Rust Edition: 2021

What It Is

sftp-rs is a Rust crate implementing an SFTP client — the SSH File Transfer Protocol as defined in draft-ietf-secsh-filexfer-02. It provides a pure wire-protocol codec plus both synchronous and asynchronous client implementations that layer on top of any transport providing Read + Write (sync) or AsyncRead + AsyncWrite (async) byte streams.

Core design decisions:

  • Transport-agnostic — does not include an SSH implementation itself; operates on top of an already-established SSH channel or any byte stream
  • Protocol v3 — targets SFTP protocol version 3, following the published draft but deviating where other servers and clients ignore the RFC
  • Pure codec — the protocol module contains zero I/O; it builds and parses raw bytes, shared by both sync and async clients
  • Two concurrency models — a synchronous SftpClient<C> and an async AsyncSftpClient<W> with a background reader task for concurrent pipelining

Source Layout

sftp-rs/src/
├── lib.rs          # Crate root, re-exports, feature gating
├── protocol.rs     # Pure wire-protocol codec: types, builders, parsers
├── sync.rs         # Synchronous SftpClient<C: Read + Write>
├── async.rs        # AsyncSftpClient<W: AsyncWrite + Unpin> with background reader
├── russh.rs        # russh transport glue (optional, feature-gated)
└── bin/
    └── sftp.rs     # CLI interactive sftp client binary

Feature Flags

Feature Default Dependencies Description
default bin Includes the CLI binary
bin (via default) rustyline, shell-words Interactive CLI binary
ssh2 ssh2 Integration with the ssh2 crate (libssh2 bindings)
async tokio Async client (AsyncSftpClient)
russh russh, async, tokio russh transport integration

The russh feature implies async (it requires tokio and the async client).

Key Dependencies

Dependency Version Purpose
byteorder 1 Big-endian binary read/write for wire protocol
russh 0.61 (optional) Pure-Rust SSH implementation, provides channel + stream
ssh2 0.9 (optional) libssh2 bindings, provides Channel
tokio 1 (optional) Async runtime, AsyncRead/AsyncWrite, io::split
rustyline 18 (optional) Readline library for CLI binary
shell-words 1 (optional) Shell-style token parsing for CLI

Architecture Diagram

┌───────────────────────────────────────────────────────────────┐
│                        Application Layer                       │
│                                                                │
│  ┌─────────────────┐  ┌──────────────────┐  ┌──────────────┐ │
│  │   CLI Binary     │  │  SftpClient<C>   │  │ AsyncSftp-  │ │
│  │   (bin/sftp)     │  │   (sync.rs)      │  │ Client<W>   │ │
│  │                  │  │                  │  │ (async.rs)   │ │
│  │  SshChannel →    │  │  C: Read+Write   │  │  W: AsyncW   │ │
│  │  SftpClient       │  │                  │  │             │ │
│  └────────┬─────────┘  └────────┬─────────┘  └──────┬──────┘ │
│           │                      │                    │        │
│           └──────────────────────┴────────────────────┘        │
│                                  │                              │
│                    ┌─────────────┴─────────────┐               │
│                    │    protocol.rs (codec)     │               │
│                    │                            │               │
│                    │  • Error, Result           │               │
│                    │  • Attributes (serde)      │               │
│                    │  • OpenOptions              │               │
│                    │  • build_*() builders      │               │
│                    │  • parse_*() / expect_*()  │               │
│                    │  • read/write_raw_packet    │               │
│                    │  • with/split_request_id    │               │
│                    └────────────────────────────┘               │
│                                  │                              │
└──────────────────────────────────┼──────────────────────────────┘
                                   │
              ┌────────────────────┼────────────────────┐
              │                    │                     │
    ┌─────────┴──────┐  ┌─────────┴───────┐  ┌─────────┴──────┐
    │  ssh subprocess│  │  russh Channel  │  │  ssh2 Channel  │
    │  (stdin/stdout)│  │  (via Channel-  │  │  (libssh2)     │
    │                │  │   Stream)       │  │                │
    └────────────────┘  └────────────────┘  └────────────────┘

Protocol Version Negotiation

Both clients perform the same handshake on construction:

  1. Client sends SSH_FXP_INIT with version 3
  2. Server responds SSH_FXP_VERSION with its version and optional extensions
  3. If server version ≠ 3, the constructor returns an error
// From protocol.rs
pub fn build_init() -> Vec<u8> {
    let mut buf = Vec::with_capacity(4);
    buf.write_u32::<BigEndian>(3).unwrap();  // version = 3
    buf
}

The handshake is the only unnumbered (no request-id) exchange. All subsequent requests include a 4-byte request-id used for demultiplexing responses.

Re-exports from lib.rs

The crate root re-exports the most commonly used types:

pub use protocol::{
    Attributes, Directory, Error, File, Kind, OpenOptions, Result, TextHint,
    SSH_FILEXFER_ATTR_ACCESSTIME, SSH_FILEXFER_ATTR_ACL, /* ... all flag constants */
};
pub use sync::SftpClient;

#[cfg(feature = "async")]
pub use r#async::AsyncSftpClient;