Skip to content

vdavid/smb2

smb2

Crates.io docs.rs CI License MSRV

A pure-Rust SMB2/3 client library with pipelined I/O. No C dependencies, no FFI. Faster than native macOS SMB in all operations: 1.3-5x faster on uploads, downloads, listings, and deletes.

I built this because I needed fast SMB access for Cmdr (my file manager), and the existing Rust SMB options weren't cutting it. The smb crate works fine for listing files but downloads are painfully slow because it sends one read at a time. Native OS SMB clients pipeline their reads, and so does this library.

Why this matters:

  • Cross-compile without system lib headaches (no libsmbclient, no -sys crates)
  • Pipelined I/O by default, not as an afterthought
  • Async and runtime-agnostic (uses futures traits)
  • Works anywhere Rust compiles

What it does

  • Connect to SMB2/3 shares using NTLM or Kerberos authentication
  • List directories, read files, write files, delete, rename, stat, create directories
  • Compound requests (CREATE+READ+CLOSE in 1 round-trip, 4-way write compounds, compound delete/rename/stat)
  • Batch operations (delete, rename, stat multiple files -- all requests sent before waiting for responses)
  • Pipelined I/O with sliding window for large file transfers
  • SMB 3.x signing (HMAC-SHA256, AES-CMAC, AES-GMAC) and encryption (AES-128/256-CCM/GCM)
  • LZ4 compression
  • Share enumeration (list shares on a server via IPC$ + srvsvc RPC)
  • Streaming downloads and uploads with progress reporting and cancellation
  • File watching (CHANGE_NOTIFY for live directory updates)
  • Disk space queries (total, free, used)
  • DFS path resolution (standalone DFS with transparent referral follow-through)
  • Reconnection after network failures
  • Auto-flush on writes (data safety for family photos and company docs)

Limitations

Not yet supported:

  • Domain-based DFS: standalone DFS links work; AD domain-based namespaces aren't supported yet
  • DFS target failback: uses the first reachable target; no automatic failback to preferred targets
  • Multi-channel: single TCP connection per client
  • QUIC/RDMA transport: TCP only (covers ~99% of use cases)

If you need multi-channel or QUIC, check the smb crate.

Not planned:

  • Server implementation (this is a client library)
  • SMB1 (deprecated, insecure)
  • LZNT1 compression (LZ4 is supported; LZNT1 is legacy)

Quick start

use smb2::{SmbClient, ClientConfig};

#[tokio::main]
async fn main() -> Result<(), smb2::Error> {
    let mut client = smb2::connect("192.168.1.100:445", "user", "pass").await?;

    // List shares
    let shares = client.list_shares().await?;
    for share in &shares {
        println!("{} - {}", share.name, share.comment);
    }

    // Connect to a share
    let mut share = client.connect_share("Documents").await?;

    // List files
    let entries = client.list_directory(&mut share, "projects/").await?;
    for entry in &entries {
        println!("{} ({} bytes)", entry.name, entry.size);
    }

    // Read a file
    let data = client.read_file(&mut share, "report.pdf").await?;
    std::fs::write("report.pdf", data)?;

    // Write a file
    let content = std::fs::read("local_file.txt")?;
    client.write_file(&mut share, "remote_file.txt", &content).await?;

    // Clean up
    client.disconnect_share(&share).await?;

    Ok(())
}

Pipeline API

The pipeline is the core feature. It lets you batch multiple operations and execute them together:

use smb2::{Pipeline, Op, OpResult};

# async fn example(client: &mut smb2::SmbClient, share: &mut smb2::Tree) -> Result<(), smb2::Error> {
    let mut pipeline = Pipeline::new(client.connection_mut(), &share);

    let results = pipeline.execute(vec![
        Op::ReadFile("a.txt".into()),
        Op::ReadFile("b.txt".into()),
        Op::ListDirectory("docs/".into()),
        Op::Delete("temp.txt".into()),
    ]).await;

    for result in results {
        match result {
            OpResult::FileData { path, data } => println!("{}: {} bytes", path, data.len()),
            OpResult::DirEntries { path, entries } => println!("{}: {} entries", path, entries.len()),
            OpResult::Deleted { path } => println!("deleted {}", path),
            OpResult::Error { path, error } => eprintln!("{}: {}", path, error),
            other => println!("{:?}", other),
        }
    }
    # Ok(())
    #
}

For large file I/O, use the pipelined variants which fill the credit window:

# async fn example(client: &mut smb2::SmbClient, share: &mut smb2::Tree) -> Result<(), smb2::Error> {
    // Pipelined I/O with sliding window for large files
    let data = client.read_file_pipelined(&mut share, "big_file.iso").await?;
    client.write_file_pipelined(&mut share, "copy.iso", &data).await?;
    # Ok(())
    #
}

Streaming I/O

For large files that don't fit in memory, use the streaming API. It downloads one chunk at a time and supports progress reporting and cancellation.

Streaming download

use tokio::io::AsyncWriteExt;

# async fn example(client: &mut smb2::SmbClient, share: &smb2::Tree) -> Result<(), smb2::Error> {
    let mut download = client.download(&share, "big_video.mp4").await?;
    println!("Downloading {} bytes...", download.size());

    let mut file = tokio::fs::File::create("big_video.mp4").await?;
    while let Some(chunk) = download.next_chunk().await {
        let bytes = chunk?;
        file.write_all(&bytes).await?;
        println!("{:.1}%", download.progress().percent());
    }
    # Ok(())
    #
}

Write with progress and cancellation

use std::ops::ControlFlow;

# async fn example(client: &mut smb2::SmbClient, share: &mut smb2::Tree) -> Result<(), smb2::Error> {
    let data = std::fs::read("big_file.bin")?;
    client.write_file_with_progress(&mut share, "remote.bin", &data, |progress| {
        println!("{:.1}%", progress.percent());
        ControlFlow::Continue(()) // return ControlFlow::Break(()) to cancel
    }).await?;
    # Ok(())
    #
}

All write methods (write_file, write_file_pipelined, write_file_with_progress) flush data to persistent storage before closing the file handle.

Installation

Add to your Cargo.toml:

[dependencies]
smb2 = "0.2"

You'll also need an async runtime. The library is runtime-agnostic, but tokio is the most common choice:

[dependencies]
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }

API overview

High-level API

For when you want to do one thing and get the result:

  • smb2::connect() -- connect and authenticate (shorthand)
  • SmbClient::connect() -- connect with full config
  • client.list_shares() -- list available shares
  • client.connect_share() -- connect to a share
  • client.list_directory(&mut share, path) -- list a directory
  • client.read_file(&mut share, path) -- download a file
  • client.write_file(&mut share, path, data) -- upload a file
  • client.delete_file(&mut share, path) -- delete a file
  • client.delete_files(&mut share, &paths) -- batch delete (all requests sent before waiting)
  • client.stat(&mut share, path) -- get file metadata
  • client.stat_files(&mut share, &paths) -- batch stat
  • client.rename(&mut share, from, to) -- rename a file
  • client.rename_files(&mut share, &renames) -- batch rename
  • client.create_directory(&mut share, path) -- create a directory
  • client.delete_directory(&mut share, path) -- remove a directory
  • client.download(&share, path) -- streaming download with progress (memory-efficient)
  • client.upload(&share, path, data) -- streaming upload with progress
  • client.watch(&share, path, recursive) -- watch for file changes (CHANGE_NOTIFY)
  • client.fs_info(&mut share) -- disk space (total, free, used)
  • client.reconnect() -- reconnect after network failure
  • client.credits() -- current available credits
  • client.estimated_rtt() -- round-trip time from negotiate
  • client.disconnect_share(&share) -- disconnect from a share

Pipeline API

For when you have many operations and want them fast:

  • Pipeline::new(conn, &share) -- create a pipeline
  • pipeline.execute(ops) -- run a batch of operations

Low-level API

For advanced use cases, the underlying types are available:

  • Connection -- message exchange, credit tracking
  • Session -- NTLM authentication, key derivation
  • Tree -- share-level file operations (take &mut Connection)
  • NegotiatedParams -- protocol parameters from negotiate

Performance

Benchmarked against native macOS SMB (with F_NOCACHE to disable kernel page cache) and the smb crate on a QNAP NAS over Gigabit LAN, SMB 3.1.1:

Small files (100 × 100 KB)

Operation native smb2 Speedup
Upload 3.69s 1.91s 1.9x faster
List 47ms 21ms 2.2x faster
Download 3.10s 617ms 5.0x faster
Delete 3.08s 1.03s 3.0x faster

Medium files (10 × 10 MB)

Operation native smb2 Speedup
Upload 1.66s 1.23s 1.3x faster
List 27ms 19ms 1.4x faster
Download 4.00s 2.93s 1.4x faster
Delete 301ms 128ms 2.4x faster

Large files (3 × 50 MB)

Operation native smb2 Speedup
Upload 1.69s 1.56s ~parity
List 27ms 18ms 1.5x faster
Download 5.62s 1.11s 5.1x faster
Delete 117ms 54ms 2.1x faster

Key optimizations: compound requests (CREATE+READ+CLOSE in 1 round-trip), pipelined I/O with sliding window and adaptive chunk sizing, and 256-credit request for wide pipeline windows.

Note: Native macOS download benchmarks use F_NOCACHE to bypass the kernel page cache. Without this, cached native reads appear ~20x faster because they skip the network entirely. F_NOCACHE gives a fair comparison of actual network I/O performance.

The benchmark tool is included at benchmarks/smb/. Run with cargo run -p smb-benchmark --release.

Comparison with existing libraries

vs smb crate

The smb crate supports multi-channel, QUIC, and RDMA transport. If you need those, use it. For everything else, smb2 is a better fit:

  • Compound + pipelined I/O: smb sends one request at a time; smb2 uses compound requests and pipelined reads with sliding window
  • Auto-reconnect with durable handles: survives Wi-Fi drops without restarting transfers
  • Comprehensive test suite: smb has almost no tests
  • MIT OR Apache-2.0: smb is MIT-only

I initially considered forking smb, but the architecture didn't support pipelining well, and adding it would have been a near-complete rewrite anyway.

vs pavao

pavao wraps libsmbclient via FFI. It works, but you need libsmbclient installed, which means system package management, cross-compilation headaches, and all the fun that comes with C dependencies.

smb2 is pure Rust. cargo build and done.

Implementation notes

  • I used Claude extensively for this implementation. The code has a comprehensive test suite (unit tests with mock transport, property tests with proptest, integration tests against Docker Samba), and it works for my use case in Cmdr. If you distrust AI-generated code, that's fair, but please check the tests and decide for yourself.
  • The protocol implementation is based on Microsoft's MS-SMB2 spec. I converted the relevant sections to Markdown so AI agents could work from them effectively. The spec files live in docs/specs/.

Contributing

See CONTRIBUTING.md for guidelines.

License

MIT OR Apache-2.0, at your option.

About

Pure-Rust SMB2/3 client. Faster than native macOS SMB on all operations. No C dependencies.

Topics

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages