A pure-Rust SMB2/3 file server for container-native environments.
SMB 2.1 – 3.1.1 · AES-128-GCM encryption · NTLMv2 auth · ~8 MB static binary
RSSMB is a standalone SMB2/3 file server written entirely in Rust. No Samba,
no external C libraries — just Rust and the Linux kernel. It is designed for
cloud-native file serving: a single static binary that runs in a scratch
container, mounts config as a secret, and serves shares from a data volume.
The server negotiates SMB 2.1 through 3.1.1 with AES-128-GCM encryption,
NTLMv2 authentication via SPNEGO, and SrvSvc share enumeration. It has been
tested against the Linux kernel CIFS client, smbclient, GNOME Files
(Nautilus), and Samba's smbtorture conformance suite.
- Why RSSMB?
- Features
- Quick start
- Building from source
- Configuration
- Security
- Testing & conformance
- Architecture
- Roadmap
- License
| RSSMB | Samba | |
|---|---|---|
| Runtime dependencies | ✅ None (static binary) | ❌ glibc, libtalloc, libldb, libtdb, … |
| Container image | ✅ ~8 MB (FROM scratch) |
❌ ~100 MB+ (Debian/Ubuntu base) |
| Attack surface | ✅ Pure Rust, no C FFI | |
| Config | ✅ One TOML file, hot-reload via SIGHUP | smb.conf + pdbedit + net |
| Users | ✅ Config file (NT hashes) | |
| Scope | 🎯 File serving for containers | 🎯 General-purpose SMB/AD/DC |
RSSMB does not aim to replace Samba in Active Directory environments. It targets the common case: serve files from a volume to SMB clients with minimal overhead and maximal security.
- SMB 2.1 – 3.1.1 dialect negotiation
- AES-128-GCM encryption (SMB 3.x) with SHA-512 pre-auth integrity (3.1.1)
- HMAC-SHA256 message signing (required by default)
- NTLMv2 authentication via SPNEGO with AUTHENTICATE message MIC verification
KEY_EXCHsession key derivation (ExportedSessionKey)- Large MTU (1 MiB) with multi-credit flow control
- Durable handles v1/v2 (DHnQ/DHnC, DH2Q/DH2C) with reconnection —
smb2.durable-v2-open 14/15 (smbtorture 4.19.5; the one failure is
AppInstanceId, which Samba 4.19.5 itself does not implement — see
tests/conformance/results.md) - File leases v1/v2 (R, RH, RW, RWH) and traditional oplocks with break handling
- Byte-range locks with blocking, CANCEL, LockSequence replay detection — smb2.lock 22/22 (smbtorture 4.19.5, full parity with smbd)
- Multichannel (SMB 3.x): session binding across connections and
FSCTL_QUERY_NETWORK_INTERFACE_INFOinterface discovery - SMB over QUIC (SMB 3.1.1 over TLS 1.3, ALPN
smb, UDP/443) — traverse firewalls that block TCP/445 CHANGE_NOTIFY— inotify-backed directory change notifications- Mandatory byte-range locks (all-or-nothing batch)
- Case-insensitive path lookup (NFC-aware, multi-component)
- Security descriptors synthesized from POSIX uid/gid/mode
- Path confinement via
cap-std+openat2(RESOLVE_BENEATH)— TOCTOU-free - Per-share symlink policy (
Never/WithinShare) - NFC name normalization, control/reserved character rejection, length limits
- Per-IP authentication throttling (exponential backoff + hard cooldown)
- Global and per-IP connection limits
- MessageId sequence window (bounded state, overflow-safe)
- Per-share access lists:
valid_users,read_users,write_users deny_unknown_fieldsconfig validation with permission warningscatch_unwindin the read loop for malformed-frame resilience
- Graceful shutdown (SIGTERM / SIGINT with bounded drain)
- SIGHUP live-reload of users and shares without restart
- Prometheus metrics on
/metrics, health on/healthz - Structured JSON logging (
--log-format json) rssmb check-configvalidates configuration before startuprssmb hash-passwordgenerates NT hashes
The Docker image is a ~8 MB static binary in FROM scratch — no OS,
no shell, no dynamic libraries. Just the binary and the config/data volumes
you mount.
# 1. Create data directories owned by UID 1000 (the container's user)
mkdir -p data/public data/homes
sudo chown -R 1000:1000 data
# 2. Lock down the config file (it contains credentials)
chmod 600 config/rssmb.toml
# 3. Start the server
docker compose -f docker/compose.yml up -d --build
# 4. Verify it's healthy
curl -s localhost:8080/healthz
docker logs -f rssmb # Ctrl-C to stop following💡 Dev config:
config/rssmb.tomlships with a guest-accesspublicshare, ahomesshare, and test accountalice/TestPass123— not for production. For production, copyconfig/rssmb.example.tomltoconfig/rssmb.tomland edit it (signing required, guest disabled, real users with NT hashes).
📁 File permissions: The container runs as UID 1000 and reads
config/rssmb.tomlthrough a read-only bind mount. If your host user is not UID 1000, usesudo chown 1000 config/rssmb.toml(keeping mode600), orsudo chgrp 1000 config/rssmb.toml && chmod 640. Without this, the server logs a "config file is world-readable" warning.
🔌 Port 445: Must be free on the host (stop a local Samba first). If it isn't, change the mapping in
docker/compose.ymlto e.g."4450:445"and addport=4450to the mount options below.
sudo mount -t cifs //127.0.0.1/public /mnt/rssmb \
-o username=alice,password=TestPass123,vers=3.1.1,uid=$(id -u),gid=$(id -g)
ls -la /mnt/rssmb
echo hi > /mnt/rssmb/hello.txt
cat data/public/hello.txt
sudo umount /mnt/rssmbWithout a mount:
smbclient //127.0.0.1/public -U alice%TestPass123 -m SMB2 -c 'ls; get README.md'docker compose -f docker/compose.yml downEach GitHub release ships
static musl binaries for x86_64 and aarch64 Linux (with SHA256SUMS),
and a multi-arch image:
docker pull ghcr.io/treeol/rssmb:latest # or :0.1.0
docker run -d --name rssmb -p 445:445 \
-v ./config/rssmb.toml:/etc/rssmb/rssmb.toml:ro \
-v ./data:/data ghcr.io/treeol/rssmb:latestRequires Rust 1.85+ (stable).
cargo build --release
cargo test --workspaceRun the server:
cargo run --release --bin rssmb -- --config config/rssmb.tomlCLI subcommands:
rssmb check-config --config config/rssmb.toml # validate config
rssmb hash-password <password> # generate NT hashSee config/rssmb.example.toml for production-safe defaults. Key settings:
| Setting | Default | Description |
|---|---|---|
listen_addr / port |
0.0.0.0:445 |
SMB bind address |
min_dialect / max_dialect |
smb21 / smb311 |
SMB dialect range |
signing |
required |
required (reject unsigned + disable guest) · desired · disabled |
encryption |
desired |
required · desired · disabled (SMB 3.1.1 AES-128-GCM) |
guest_access |
false |
Allow anonymous guest logons (only guest_ok shares) |
max_connections_per_ip |
0 (100 in example config) |
Per-IP connection limit (0 = unlimited) |
max_frame_size |
1048576 |
Maximum SMB2 payload size = advertised MaxRead/MaxWrite/MaxTransactSize (capped at 1 MiB, min 65536); the wire frame limit adds a fixed 64 KiB header allowance |
max_credits |
1024 |
Maximum outstanding credits |
health_bind_addr / health_port |
0.0.0.0 / 8080 |
HTTP health/metrics endpoint |
shutdown_grace_secs |
10 |
Graceful drain timeout on SIGTERM/SIGINT |
leases |
true |
SMB 2.1+ file leases and traditional oplocks |
multichannel |
false |
SMB 3.x multichannel (advertised only when usable interfaces exist; disabled by default until binding hardening lands) |
ntlm_require_timestamp |
false |
Require MsvAvTimestamp in NTLMv2 AUTHENTICATE (anti-replay) |
ntlm_max_clock_skew_secs |
600 |
Max clock skew (seconds) for NTLMv2 timestamp freshness check; 0 = presence-only |
multichannel_interfaces |
[] |
Pin advertised addresses for NAT/Kubernetes (empty = enumerate host NICs) |
quic_enabled |
false |
SMB over QUIC (SMB 3.1.1 over TLS 1.3, UDP) |
quic_port |
443 |
UDP port for SMB over QUIC |
quic_cert_path / quic_key_path |
— | PEM TLS certificate chain / private key (required when quic_enabled) |
| Key | Description |
|---|---|
path |
Share root directory |
read_only |
Reject all mutating operations |
guest_ok |
Allow guest access to this share |
browseable |
Show in NetrShareEnum listing |
valid_users |
Restrict to named users (empty = all authenticated) |
read_users |
Restrict read access (empty = all valid_users) |
write_users |
Restrict write access (empty = all valid_users) |
publish_on_close |
Write new files under a hidden .rssmb-tmp.* name and atomically rename to the final name on the last close (default false). SMB clients always see the final name; host-side watchers/backups never observe a pre-sized file with no data. CHANGE_NOTIFY directory watches map temp events to the client-visible final name so no temp name ever surfaces. Multi-handle attaches publish on the last close; an attach that rolls back to zero publishes instead of orphaning the temp. Rename-with-replace does the move + collision handling under one table-level lock. Close-publish renames under the table write lock; attach is phantom-free (no mint-then-abort). |
Each user needs a username and either a password (plaintext) or
nt_hash (hex — preferred, generate with rssmb hash-password). Set
disabled = true to deactivate an account without removing it.
🔒 Security: The config file may contain password-equivalent secrets.
chmod 600it and mount it as a secret in Kubernetes (not a ConfigMap).
See SECURITY.md for the vulnerability reporting policy and
docs/threat-model.md for the full threat model.
Status: Pre-1.0. The MS-NLMP, MS-SMB2, and MS-SRVS protocol implementations have been hardened through multiple rounds of automated review (Mashūra) covering NTLM authentication, SPNEGO parsing, path confinement, credit tracking, connection lifecycle, delete-on-close semantics, and config validation.
cargo test --workspace # 616 unit + integration tests
tests/live/run.sh # impacket-based interop (needs Python)
tests/live/mount.sh <server-ip> # real kernel CIFS mount (needs Docker --privileged)
tests/conformance/run-smbtorture.sh # smbtorture baseline (needs samba-testsuite)tests/live/mount.sh is not run in CI: it mounts with the host kernel's
cifs module from a docker run --privileged container, which GitHub-hosted
runners don't offer reliably. Run it locally before releases. It is the only
test that drives a real kernel client, including sealed (seal) 1 MiB
O_DIRECT writes.
Every push and PR runs:
| Workflow | Checks |
|---|---|
| ci.yml | cargo fmt --check · cargo clippy -D warnings · cargo build --locked · cargo test --workspace · cargo deny check (advisories, licenses, bans, sources) · cargo audit |
| interop-ci.yml | impacket interop · smbclient interop · nightly smbtorture conformance · nightly fuzz campaigns (9 targets, 5 min each) |
9 targets covering the attack surface:
parse_payload · read_netbios_frame · extract_ntlm_token · extract_mech_types
proto_decode_header · proto_decode_plain_request · normalize_path
ntlm_authenticate · decrypt_transform
crates/
rssmb-proto/ SMB2/3 wire protocol types (pure-Rust, no smb_msg)
rssmb-wire-adapter/ Server-side wire codec over rssmb-proto
rssmb-auth/ NTLM acceptor (sspi), SPNEGO, credential store
rssmb-vfs/ VFS trait + LocalFs (cap-std) and MemoryFs backends
rssmb-rpc-server/ DCE/RPC server-side: SrvSvc share enumeration
rssmb-server/ Core server: connection engine, session, tree, opens
rssmb-cli/ Server binary: config, signals, health endpoint
rssmb-tests/ Integration test harness
docker/ Dockerfile + docker-compose
config/ Example configurations
fuzz/ cargo-fuzz targets
Each crate is independently testable. The protocol layer
(rssmb-proto / rssmb-wire-adapter) has zero server dependencies and can
be reused in other SMB tools.
Implemented (1.0 scope):
- ✅ SMB 2.1 – 3.1.1 with encryption, signing, and NTLMv2
- ✅ Durable handles v1/v2 (smb2.durable-v2-open 14/15), byte-range locks (smb2.lock 22/22), change notify
- ✅ SIGHUP hot-reload, Prometheus metrics, graceful shutdown
- ✅
smbtortureconformance, impacket/smbclient interop, fuzzing
Recently implemented:
- ✅ File leases v1/v2 (R, RH, RW, RWH) and traditional oplocks (Level II, Exclusive, Batch)
- ✅ SMB over QUIC transport (TLS 1.3, ALPN
smb, UDP/443) - ✅ Multichannel (SMB 3.x session binding, shared state across channels)
- ✅
FSCTL_QUERY_NETWORK_INTERFACE_INFO— NIC enumeration for multichannel interface discovery, withmultichannel_interfacesoverride for NAT/Kubernetes
Not yet implemented:
- Kerberos authentication
- Persistent handles (TLER resilient)
- Multichannel binding hardening (per-channel signing key derivation for SMB 3.1.1; binding works but is not yet hardened against a malicious second channel)
Apache-2.0 · Copyright 2026 Valon Mamudi