Skip to content

SMB

This document details DittoFS’s SMB implementation, protocol status, client usage, and protocol internals for maintainers and users. DittoFS supports SMB2 dialect 0x0202 through SMB 3.1.1, including encryption, signing, leases V2, directory leasing, durable handles, and Kerberos authentication.

SMB (Server Message Block) is a network file sharing protocol originally developed by IBM in 1983 and later extended by Microsoft. It is the native file sharing protocol for Windows and is also known as CIFS (Common Internet File System).

DittoFS implements multiple SMB dialects for broad compatibility:

DialectVersionKey Features
0x0202SMB 2.0.2Basic file operations, credits, HMAC-SHA256 signing
0x0300SMB 3.0Encryption (AES-128-CCM), AES-128-CMAC signing, secure dialect negotiation
0x0302SMB 3.0.2VALIDATE_NEGOTIATE_INFO for downgrade protection
0x0311SMB 3.1.1Preauth integrity (SHA-512), AES-128-GCM encryption, GMAC signing, negotiate contexts

The server negotiates the highest mutually supported dialect with each client. SMB 3.1.1 is preferred for its stronger security guarantees.

AspectNFS (v3/v4)SMB2 (2.0.2)SMB3 (3.0-3.1.1)
OriginUnix (Sun Microsystems, 1984)Windows (IBM/Microsoft, 1983)Windows (Microsoft, 2012)
Designv3: Stateless / v4: StatefulStateful, session-basedStateful, session-based
IdentityUID/GID (Unix)SID (Windows Security ID)SID + Kerberos principal
PermissionsUnix mode bits / NFSv4 ACLsACLs (Access Control Lists)ACLs
TransportTCP (port 2049)TCP (port 445)TCP (port 445)
FramingRPC record markingNetBIOS session headerNetBIOS + Transform header
EncodingXDR (big-endian)Custom (little-endian)Custom (little-endian)
HeaderVariable (RPC)Fixed 64 bytesFixed 64 bytes (+ 52-byte transform)
StringsUTF-8UTF-16LEUTF-16LE
Flow controlNone (relies on TCP)Credit-basedCredit-based
Encryptionkrb5p (RPCSEC_GSS)NoneAES-GCM / AES-CCM (transform header)
Signingkrb5i (RPCSEC_GSS)HMAC-SHA256AES-CMAC / AES-GMAC
Client cachingDelegationsOplocksLeases V2 (file + directory)
Handle resilienceVolatileVolatileDurable / Persistent handles
NFS ConceptSMB EquivalentNotes
ExportShareNetwork-accessible directory
MountTree ConnectEstablishing access to a share
File HandleFileIDOpaque identifier for open file
UID/GIDSIDUser/group identity
Mode bitsSecurity DescriptorPermission model
LOOKUPPart of CREATESMB combines lookup and open
GETATTRQUERY_INFOGet file metadata
SETATTRSET_INFOSet file metadata
READDIRQUERY_DIRECTORYList directory contents
COMMITFLUSHSync to disk
DelegationLease V2Client caching grant
CB_RECALLLease Break NotificationCache invalidation
CB_NOTIFYCHANGE_NOTIFYDirectory change events

Every SMB2 message follows this structure:

+------------------------------------------------------------+
| NetBIOS Session Header |
| (4 bytes) |
+------------------------------------------------------------+
| SMB2 Header |
| (64 bytes) |
+------------------------------------------------------------+
| Command Body |
| (variable) |
+------------------------------------------------------------+

For SMB3 encrypted messages, a Transform Header wraps the entire message:

+------------------------------------------------------------+
| NetBIOS Session Header |
| (4 bytes) |
+------------------------------------------------------------+
| Transform Header (0xFD534D42) |
| (52 bytes) |
| Signature (16) | Nonce (16) | OrigMsgSize (4) |
| Reserved (2) | Flags (2) | SessionID (8) |
+------------------------------------------------------------+
| Encrypted Payload |
| (SMB2 Header + Command Body, encrypted) |
+------------------------------------------------------------+

The NetBIOS session header contains a type byte (0x00 for session messages) and a 24-bit big-endian length. The SMB2 header is always 64 bytes and includes the protocol magic (0xFE 'S' 'M' 'B'), command code, credit charge/grant, session ID, tree ID, message ID, flags, and signature. The Transform header uses magic 0xFD 'S' 'M' 'B' and carries the AEAD nonce and authentication tag.

SMB connections follow a multi-phase setup before file operations can begin:

  1. NEGOTIATE — Client and server agree on protocol dialect, capabilities, and security parameters (cipher suites, signing algorithms, preauth integrity)
  2. SESSION_SETUP — Client authenticates (NTLM or Kerberos via SPNEGO), receives a SessionID; session keys are derived and encryption/signing activated
  3. TREE_CONNECT — Client connects to a specific share, receives a TreeID; per-share encryption may be enforced
  4. File Operations — CREATE opens a file (returns FileID), then READ/WRITE/CLOSE use that FileID
  5. Cleanup — CLOSE releases file handles, TREE_DISCONNECT leaves the share, LOGOFF ends the session

This is fundamentally different from NFS, where each request is independent and carries its own auth context.

DittoFS uses a configurable port (default 12445) and supports NTLM and Kerberos authentication.

The dfsctl share mount command handles platform-specific mount options automatically:

Terminal window
# macOS - Mount to user directory (recommended, no sudo needed)
mkdir -p ~/mnt/dittofs
dfsctl share mount --protocol smb /export ~/mnt/dittofs
# macOS - Mount to system directory (requires sudo)
sudo dfsctl share mount --protocol smb /export /mnt/smb
# Linux - Mount with sudo (owner set to your user automatically)
sudo dfsctl share mount --protocol smb /export /mnt/smb
# Unmount
sudo umount /mnt/smb # or: diskutil unmount ~/mnt/dittofs (macOS)

macOS has a security restriction where only the mount owner can access files, regardless of Unix permissions. Even with 0777, non-owner users get “Permission denied”. Apple confirmed this is “works as intended”.

How dfsctl handles this: When you run sudo dfsctl share mount, it automatically uses sudo -u $SUDO_USER to mount as your user (not root):

Terminal window
# Works correctly - mount owned by your user
sudo dfsctl share mount --protocol smb /export /mnt/share

Alternative - mount without sudo (to user directory):

Terminal window
mkdir -p ~/mnt/share
dfsctl share mount --protocol smb /export ~/mnt/share

Linux CIFS mount fully supports uid= and gid= options. When using sudo with dfsctl:

  • The SUDO_UID and SUDO_GID environment variables are automatically detected
  • Mount options include uid=<your-uid>,gid=<your-gid>
  • Files appear owned by your user, not root
  • Default permissions are 0755 (standard Unix)
Terminal window
# Files will be owned by your user, not root
sudo dfsctl share mount --protocol smb /export /mnt/smb
ls -la /mnt/smb
# drwxr-xr-x youruser yourgroup ... .

If you prefer to use native mount commands directly:

Terminal window
# Using mount_smbfs (built-in)
# Note: -f sets file mode, -d sets directory mode (required for write access with sudo)
sudo mount_smbfs -f 0777 -d 0777 //username:password@localhost:12445/export /mnt/smb
# Mount to home directory (no sudo, user-owned)
mount_smbfs //username:password@localhost:12445/export ~/mnt/smb
# Using open (opens in Finder)
open smb://username:password@localhost:12445/export
# Unmount
sudo umount /mnt/smb
# or
diskutil unmount /mnt/smb
Terminal window
# Using mount.cifs (requires cifs-utils)
# uid/gid options set the owner of mounted files
sudo mount -t cifs //localhost/export /mnt/smb \
-o port=12445,username=testuser,vers=2.0,uid=$(id -u),gid=$(id -g)
# Password will be prompted interactively
# Mount with SMB3 encryption
sudo mount -t cifs //localhost/export /mnt/smb \
-o port=12445,username=testuser,vers=3.1.1,seal,uid=$(id -u),gid=$(id -g)
Terminal window
# Interactive client
smbclient //localhost/export -p 12445 -U testuser
# List shares
smbclient -L localhost -p 12445 -U testuser
# One-liner file operations
smbclient //localhost/export -p 12445 -U testuser -c "ls"
smbclient //localhost/export -p 12445 -U testuser -c "get file.txt"
smbclient //localhost/export -p 12445 -U testuser -c "put localfile.txt"
CommandStatusNotes
NEGOTIATEImplementedMulti-dialect (2.0.2 through 3.1.1), negotiate contexts
SESSION_SETUPImplementedNTLM and Kerberos via SPNEGO, key derivation
LOGOFFImplemented
TREE_CONNECTImplementedShare-level permissions, per-share encryption
TREE_DISCONNECTImplemented
CommandStatusNotes
CREATEImplementedFiles and directories, lease V2 request/grant, durable handle create contexts
CLOSEImplemented
FLUSHImplementedFlushes data to block store
READImplementedWith cache support
WRITEImplementedWith cache support
QUERY_INFOImplementedMultiple info classes
SET_INFOImplementedAttributes, timestamps, rename, delete
QUERY_DIRECTORYImplementedWith pagination
CHANGE_NOTIFYPartialAccepts watches, async delivery via notification queue
IOCTLImplementedVALIDATE_NEGOTIATE_INFO, FSCTL_PIPE_WAIT
LOCKImplementedShared and exclusive byte-range locks
FeatureStatusNotes
Multi-Dialect NegotiationImplemented2.0.2, 3.0, 3.0.2, 3.1.1
Negotiate ContextsImplementedPREAUTH_INTEGRITY, ENCRYPTION, SIGNING
Preauth Integrity HashImplementedSHA-512 chain over raw wire bytes
AES-GCM EncryptionImplementedDefault for 3.1.1
AES-CCM EncryptionImplementedDefault for 3.0/3.0.2
AES-256-GCM/CCMImplemented256-bit variants
AES-CMAC SigningImplementedDefault for 3.0+
AES-GMAC SigningImplementedPreferred for 3.1.1
SP800-108 KDFImplementedKey derivation for signing/encryption
VALIDATE_NEGOTIATE_INFOImplementedDowngrade protection for 3.0/3.0.2
Leases V2ImplementedParentLeaseKey, epoch tracking
Directory LeasesImplementedRead-caching for directory listings
Durable Handles V1ImplementedDHnQ/DHnC with batch oplock
Durable Handles V2ImplementedDH2Q/DH2C with CreateGuid
Durable Handle ScavengerImplementedTimeout-based cleanup
Kerberos via SPNEGOImplementedShared keytab with NFS adapter
Compound RequestsImplementedCREATE+QUERY_INFO+CLOSE
Credit ManagementImplementedAdaptive flow control
Parallel RequestsImplementedPer-connection concurrency
Byte-Range LockingImplementedShared/exclusive locks
OplocksImplementedLevel II, Exclusive, Batch
Cross-Protocol CoordinationImplementedBidirectional lease/delegation breaks
FeatureNotes
SMB1Legacy protocol, security risk
CompressionSMB 3.1.1 compression contexts not implemented
MultichannelMultiple TCP connections per session
Persistent HandlesCluster-aware handles (requires shared state)
RDMARemote Direct Memory Access transport
QUICUDP-based transport (SMB over QUIC)
Security DescriptorsWindows ACLs not supported
DFSDistributed File System referrals
  1. TCP connection accepted
  2. NetBIOS session header parsed
  3. SMB2 message decoded (decrypted if transform header present)
  4. Session/tree context validated
  5. Command handler dispatched
  6. Handler calls metadata/block stores
  7. Response encoded (encrypted if session requires it) and sent
// Per-connection parallel request handling
for {
msg := readSMB2Message(conn)
go handleRequest(msg) // Concurrent handling
}

Session Management (internal/adapter/smb/handlers/)

  • NEGOTIATE: Multi-dialect negotiation with negotiate contexts (cipher, signing, preauth)
  • SESSION_SETUP: NTLM or Kerberos authentication via SPNEGO, key derivation
  • TREE_CONNECT: Share access with permission validation, per-share encryption enforcement

File Operations (internal/adapter/smb/handlers/)

  • CREATE: Create/open files and directories, lease V2 grants, durable handle create contexts
  • READ: Read file content (with cache support)
  • WRITE: Write file content (with cache support)
  • CLOSE: Close file handle and cleanup
  • FLUSH: Flush cached data to block store
  • QUERY_INFO: Get file/directory attributes
  • SET_INFO: Modify attributes, rename, delete
  • QUERY_DIRECTORY: List directory contents
  • LOCK: Acquire/release byte-range locks
  • IOCTL: VALIDATE_NEGOTIATE_INFO, server-side copy
NFS Implementation: SMB Implementation:
internal/adapter/nfs/ internal/adapter/smb/
+-- dispatch.go +-- dispatch.go
+-- rpc/ +-- header/
| +-- message.go | +-- header.go
| +-- reply.go | +-- parser.go
+-- xdr/ | +-- encoder.go
| +-- reader.go +-- auth/
| +-- writer.go | +-- ntlm/
+-- types/ | +-- spnego/
| +-- constants.go +-- smbenc/
+-- mount/handlers/ | +-- encrypt.go
| +-- mnt.go | +-- decrypt.go
| +-- export.go +-- signing/
+-- v3/handlers/ | +-- hmac.go
| +-- lookup.go | +-- cmac.go
| +-- read.go | +-- gmac.go
| +-- write.go +-- kdf/
+-- v4/handlers/ | +-- sp800_108.go
| +-- compound.go +-- lease/
| +-- delegation.go | +-- manager.go
| +-- state/ | +-- notifier.go
+-- types/
| +-- constants.go
| +-- status.go
| +-- filetime.go
+-- v2/handlers/
+-- handler.go
+-- negotiate.go
+-- session_setup.go
+-- tree_connect.go
+-- create.go
+-- read.go
+-- write.go
+-- ioctl.go
+-- durable.go
...

WRITE operations use a two-phase commit pattern:

// 1. Prepare write (validate permissions, get ContentID)
writeOp, err := metadataStore.PrepareWrite(authCtx, handle, newSize)
// 2. Resolve per-share block store and write data
blockStore, _ := rt.GetBlockStoreForHandle(ctx, handle)
blockStore.WriteAt(ctx, writeOp.ContentID, data, offset)
// 3. Commit write (update metadata: size, timestamps)
metadataStore.CommitWrite(authCtx, writeOp)

SMB handlers use the same per-share block store as NFS, routed through the shared internal/adapter/common/ helpers so NFS and SMB share one code path for block-store resolution and pooled READ:

// Resolve per-share block store from file handle
blockStore, err := common.ResolveForRead(ctx.Context, h.Registry, handle)
// Read path (pooled buffer; release fires after wire write completes)
readResult, err := common.ReadFromBlockStore(ctx.Context, blockStore,
payloadID, offset, count)
// Response hands readResult.Release to the encoder via SMBResponseBase.ReleaseData
// Write path (data is caller-owned, no Release closure)
err := common.WriteToBlockStore(ctx.Context, blockStore, payloadID, data, offset)
// Commit path (flush + discard *FlushResult)
err := common.CommitBlockStore(ctx.Context, blockStore, payloadID)

These three helpers (ReadFromBlockStore, WriteToBlockStore, CommitBlockStore) are the Phase-12 seam where v0.15.0 A3 (META-01 + API-01) will plumb []BlockRef into the engine — handler code does not change.

As of v0.15.0 (Phase 09 ADAPT-03), every metadata.ErrorCode is translated to an NTSTATUS via internal/adapter/common.MapToSMB, which consumes the same shared table as NFSv3 / NFSv4 (internal/adapter/common/errmap.go). Examples:

  • ErrNotFoundSTATUS_OBJECT_NAME_NOT_FOUND
  • ErrAlreadyExistsSTATUS_OBJECT_NAME_COLLISION
  • ErrAccessDenied / ErrPermissionDenied / ErrAuthRequiredSTATUS_ACCESS_DENIED (SMB has no EPERM distinction per MS-ERREF 2.3)
  • ErrIsDirectorySTATUS_FILE_IS_A_DIRECTORY
  • ErrStaleHandleSTATUS_FILE_CLOSED

Lock-context vs general-context divergence

Section titled “Lock-context vs general-context divergence”

Lock-operation errors (SMB2 LOCK requests) use a separate accessor common.MapLockToSMB backed by internal/adapter/common/lock_errmap.go. The divergence matters: ErrLocked in lock context → STATUS_LOCK_NOT_GRANTED; ErrLocked in general READ/WRITE I/O context → STATUS_FILE_LOCK_CONFLICT. Clients react differently to the two codes (retry-later vs. hard-fail-with-indication), so the distinction is wire- visible.

See internal/adapter/common/lock_errmap.go for the full lock-context override table.

common.MapToSMB uses errors.As, so wrapped StoreError values (fmt.Errorf("context: %w", storeErr)) unwrap correctly. Prior to v0.15.0 the SMB handler used an unwrapped type assertion that failed on wrapped errors and fell through to STATUS_INTERNAL_ERROR — the consolidation fixed that latent bug.

SMB2 READ responses for regular files allocate the data buffer through internal/adapter/pool (4 KB / 64 KB / 1 MB tiered sync.Pool, with a direct-alloc fallback for sizes above LargeSize). The pooled buffer is handed to the response encoder via SMBResponseBase.ReleaseData (a func() field); the encoder invokes the closure after WriteNetBIOSFrame returns, safe across plain, encrypted, and compound- response paths. Non-pooled responses leave ReleaseData nil and the encoder null-checks before invoking.

Pipe and symlink READ variants deliberately stay on heap allocations — memcpy overhead with no reuse benefit for the small buffer sizes involved, and pipes have an ownership model that conflicts with a pool-managed return buffer. Regression tests (TestRead_PipeRead_LeavesReleaseDataNil / TestRead_SymlinkRead_...) guard the non-pool decision.

SMB2 uses credits (MS-SMB2 3.3.1.2) as the protocol-level flow-control mechanism. Each request consumes credits equal to its CreditCharge; each response grants credits via the CreditResponse header field. The client tracks a per-connection running balance (cur_credits) and will refuse to send a request once its balance would go negative, or reject a response whose grant would overflow its 16-bit counter. Both outcomes look like NT_STATUS_INTERNAL_ERROR or NT_STATUS_INVALID_NETWORK_RESPONSE on the wire, so credit accounting must be byte-for-byte consistent between the server’s window and the client’s counter.

type CreditConfig struct {
MinGrant uint16 // Minimum credits per response (1)
MaxGrant uint16 // Maximum credits per response (8192)
InitialGrant uint16 // Floor when client requests 0 (1)
MaxSessionCredits uint32 // Per-connection window cap (8192)
}

The defaults match Samba’s server (smb2 max credits = 8192, initial grant = 1 in source3/smbd/smb2_server.c) and Windows Server 2008R2+. These are the protocol-level invariants clients expect; tuning them higher can break interoperability.

Server data structure — CommandSequenceWindow

Section titled “Server data structure — CommandSequenceWindow”

One per connection. Tracks granted message IDs as a sliding bitmap (internal/adapter/smb/session/sequence_window.go):

low high
│ span=high-low │
▼ ▼
[0111100011001110000...] bit i = sequence (low+i) is granted-and-unconsumed
set by Grant, cleared by Consume
available = the server's view of the client's cur_credits
(initially equal to popcount(bitmap); decoupled by Reclaim)

Three invariants drive correctness:

  1. available mirrors the client’s cur_credits. Every Grant(N) increments available by the amount the server actually extended the window; every Consume(msgId, charge) decrements available by charge. The server never grants more than MaxSessionCredits - available, so the client’s counter can never overflow.
  2. low advances lazily in 64-bit blocks. advanceLow reclaims bitmap words once an entire 64-sequence run has been consumed. The available counter is the authoritative credit tally; the bitmap span (high - low) can briefly exceed available when the oldest unconsumed bit is still in place, but stays bounded because available gates new grants.
  3. Credit-exempt commands still consume sequence numbers. MS-SMB2 exempts NEGOTIATE, CANCEL, and the first SESSION_SETUP (SessionID=0) from credit validation, but the client still advances its msgId and decrements cur_credits for them. The server therefore MUST call Consume on those messages too — otherwise available drifts up by one per credit-exempt request, saturates at MaxSessionCredits, and future responses carry credits=0 until the client runs out of credits (observed in issue #378).

MS-SMB2 3.2.4.1.4 requires middle responses in a compound to advertise Credits=0. Our response builder grants credits atomically before the write (see below), so after zeroing the middle headers the window would be over-extended relative to what the client was told. Reclaim(n) decrements available by n without touching the bitmap — the reclaimed message IDs remain valid on the server (a misbehaving client that sent one would still pass Consume), but the client was never told about them and will not use them under normal operation. Consume saturates available at zero rather than underflowing if a reclaimed message ID is used anyway.

GrantCredits (per-session policy) → credits (requested grant)
└─ strategy-dependent (echo/fixed/adaptive)
CommandSequenceWindow.Grant(credits) → credits' (may be less; ≤ MaxSessionCredits - available)
└─ extends the window and updates `available` atomically under w.mu
respHeader.Credits = credits'
...send response...

The grant is recorded against the window before the response is written, and the grant function returns the actual amount extended, so the value advertised in hdr.Credits is always exactly what the window was extended by. This closes the TOCTOU gap that a “read Remaining(), clamp, write, then Grant()” pattern would leave open when pipelined responses run on the same connection. All response build sites funnel through grantConnectionCredits in internal/adapter/smb/response.go.

  • Echo (default): grant what the client requests, bounded by [MinGrant, MaxGrant] and Remaining(). Matches Samba’s smb2_set_operation_credit: grant = credit_charge + (requested − 1).
  • Fixed: always grant InitialGrant.
  • Adaptive: InitialGrant scaled by live load and client-outstanding factors. More aggressive than Echo, primarily useful when throughput matters more than strict Samba interop.
  • Samba client hard-caps cur_credits at uint16 max (65535) and rejects any response that would overflow. Prior to #378 we advertised ~384 credits per response (InitialGrant=256 × adaptive 1.5× boost), which saturated the client after ~85 SESSION_SETUP iterations and triggered NT_STATUS_INVALID_NETWORK_RESPONSE. The fix lowered defaults to Samba-compatible values and enforced Remaining() clamping at every response build site.
  • Windows client is more tolerant but grants are capped by the negotiated Connection.MaxCredits; setting MaxSessionCredits > 8192 gains nothing because Windows caps at 8192 by default too.
  • Multi-credit operations (large READ/WRITE) consume CreditCharge sequence numbers per request; the window handles charge > 1 natively.

Reference:

  • MS-SMB2 3.3.1.2 (Server Credit Tracking)
  • Samba source3/smbd/smb2_server.c smb2_set_operation_credit and surrounding bitmap bookkeeping
  • Samba client check: libcli/smb/smbXcli_base.c:4295-4298

DittoFS implements NTLMv2 authentication with SPNEGO negotiation:

  1. Client sends NEGOTIATE with SPNEGO token
  2. Server responds with NTLM challenge
  3. Client sends SESSION_SETUP with NTLM response
  4. Server validates credentials and creates session

DittoFS supports Kerberos authentication via SPNEGO alongside NTLM. When a client presents a Kerberos AP-REQ token in the SPNEGO negotiation, the server validates the ticket using the configured service keytab and maps the Kerberos principal to a control plane user.

Key details:

  • Single round-trip: Unlike NTLM’s multi-step handshake, Kerberos authentication completes in one exchange (AP-REQ/AP-REP)
  • Shared keytab: The SMB adapter shares the Kerberos keytab with the NFS adapter; the server automatically derives the cifs/ service principal from the configured nfs/ principal
  • Principal-to-user mapping: The client principal name (without realm) is looked up in the control plane user store
  • SPNEGO negotiation: The server advertises both NTLM and Kerberos OIDs; clients choose based on their configuration

See test/e2e/smb_kerberos_test.go for end-to-end Kerberos authentication tests.

config.yaml
users:
- username: alice
password_hash: "$2a$10$..." # bcrypt hash
uid: 1001
gid: 1000
share_permissions:
/export: read-write
groups:
- name: editors
gid: 1000
share_permissions:
/export: read-write
guest:
enabled: false # Disable guest access
  • none: No access
  • read: Read-only access
  • read-write: Full read/write access
  • admin: Full access (future)

Resolution order: User explicit -> Group permissions -> Share default

SMB3 dialect negotiation determines the protocol version, cipher suite, signing algorithm, and preauth integrity mechanism used for the session. The server selects the highest mutually supported dialect and communicates security capabilities via negotiate contexts.

The NEGOTIATE request contains a list of dialect revisions supported by the client. The server selects the highest dialect both sides support:

PriorityDialectHexKey Capability
1 (highest)SMB 3.1.10x0311Preauth integrity, negotiate contexts
2SMB 3.0.20x0302VALIDATE_NEGOTIATE_INFO
3SMB 3.00x0300Encryption (AES-CCM), CMAC signing
4 (lowest)SMB 2.0.20x0202Basic SMB2 operations

When the negotiated dialect is 3.1.1, both client and server exchange negotiate contexts that specify security parameters:

SMB2_PREAUTH_INTEGRITY_CAPABILITIES:

  • Hash algorithm: SHA-512 (mandatory)
  • Salt: random 32-byte value per side
  • Purpose: preauth integrity hash chain for downgrade protection

SMB2_ENCRYPTION_CAPABILITIES:

  • Supported ciphers in preference order
  • Server selects the first mutually supported cipher

SMB2_SIGNING_CAPABILITIES:

  • Supported signing algorithms in preference order
  • Server selects the first mutually supported algorithm

For SMB 3.1.1, a running SHA-512 hash is computed over the raw NEGOTIATE and SESSION_SETUP request/response bytes:

PreauthHash[0] = SHA-512(Salt || NEGOTIATE_REQUEST_bytes)
PreauthHash[1] = SHA-512(PreauthHash[0] || NEGOTIATE_RESPONSE_bytes)
PreauthHash[2] = SHA-512(PreauthHash[1] || SESSION_SETUP_REQUEST_bytes)
...

This hash chain serves as the KDF context for key derivation (see Key Derivation), binding the session keys to the exact negotiate exchange. Any man-in-the-middle modification of the negotiate messages produces different keys, causing authentication to fail.

DittoFS uses the following default preference order:

Cipher preference (configurable):

  1. AES-128-GCM (0x0002) — fastest on modern hardware with AES-NI
  2. AES-128-CCM (0x0001) — fallback for 3.0/3.0.2
  3. AES-256-GCM (0x0004) — higher security, slightly slower
  4. AES-256-CCM (0x0003) — highest security AES-CCM variant

Signing preference (configurable):

  1. AES-128-GMAC (0x0002) — fastest for 3.1.1
  2. AES-128-CMAC (0x0001) — required for 3.0+
  3. HMAC-SHA256 — legacy for 2.x clients

FSCTL_VALIDATE_NEGOTIATE_INFO (Downgrade Protection)

Section titled “FSCTL_VALIDATE_NEGOTIATE_INFO (Downgrade Protection)”

For SMB 3.0 and 3.0.2 (which lack the preauth integrity hash chain), the client sends an FSCTL_VALIDATE_NEGOTIATE_INFO IOCTL after tree connect. The server validates that the negotiate parameters match what was originally negotiated:

  • Client sends: Capabilities, GUID, SecurityMode, requested Dialects
  • Server compares against stored negotiate state
  • If any field mismatches: connection is dropped (potential MITM downgrade)
  • For SMB 3.1.1: this IOCTL is not needed (preauth hash provides stronger protection). DittoFS drops the TCP connection if a 3.1.1 client sends it, per MS-SMB2 Section 3.3.5.15.12.
NEGOTIATE Request (variable):
StructureSize: 36
DialectCount: N (number of dialects)
SecurityMode: flags (SIGNING_ENABLED, SIGNING_REQUIRED)
Reserved: 0
Capabilities: flags
ClientGuid: 16 bytes
NegContextOffset: offset to negotiate contexts (3.1.1 only)
NegContextCount: number of contexts (3.1.1 only)
Dialects[]: array of uint16 dialect revisions
NegContextList[]: padded negotiate context structures (3.1.1 only)
adapters:
smb:
# Dialect selection (optional, default: all supported)
min_dialect: "3.0" # Reject clients below this dialect
max_dialect: "3.1.1" # Maximum dialect to negotiate

SMB3 encryption provides confidentiality and integrity for all messages on an encrypted session using AEAD (Authenticated Encryption with Associated Data) ciphers. Encryption wraps the entire SMB2 message (header + body) in a Transform Header.

CipherIDDefault ForKey SizeNonce SizeTag Size
AES-128-CCM0x0001SMB 3.0, 3.0.2128-bit11 bytes16 bytes
AES-128-GCM0x0002SMB 3.1.1128-bit12 bytes16 bytes
AES-256-CCM0x0003256-bit11 bytes16 bytes
AES-256-GCM0x0004256-bit12 bytes16 bytes

AES-GCM is preferred for SMB 3.1.1 due to hardware acceleration (AES-NI + CLMUL) on modern CPUs. AES-CCM is the mandatory cipher for SMB 3.0 and 3.0.2 compatibility.

Encrypted messages use the 0xFD 'S' 'M' 'B' magic (vs 0xFE 'S' 'M' 'B' for unencrypted):

Transform Header (52 bytes):
ProtocolID: 0xFD534D42 (4 bytes)
Signature: AES-GCM/CCM authentication tag (16 bytes)
Nonce: AES-GCM/CCM nonce (16 bytes, left-padded with zeros)
OriginalMessageSize: uint32 (4 bytes)
Reserved: uint16 (2 bytes)
Flags: uint16 (2 bytes) -- 0x0001 = encrypted
SessionId: uint64 (8 bytes)

The AAD (Additional Authenticated Data) for the AEAD cipher is the 20 bytes of the transform header starting from the Nonce field through SessionId (bytes 20-51). This ensures the session binding and message size cannot be tampered with.

DittoFS supports three encryption modes:

ModeBehavior
disabledNo encryption for any session
preferredEncrypt SMB 3.x sessions that support it; allow unencrypted 2.x
requiredReject SMB 2.x clients; encrypt all SMB 3.x sessions

Per-session encryption (Session.EncryptData): When mode is preferred or required, sessions negotiating SMB 3.x have SMB2_SESSION_FLAG_ENCRYPT_DATA set in SESSION_SETUP response. All subsequent messages on the session are encrypted.

Per-share encryption (Share.EncryptData): Individual shares can require encryption via the encrypt_data flag in share configuration. When set, SMB2_SHAREFLAG_ENCRYPT_DATA is returned in TREE_CONNECT response.

Guest sessions: Never encrypted because guest sessions have no session key for key derivation.

adapters:
smb:
encryption:
encryption_mode: preferred # disabled | preferred | required
allowed_ciphers: [] # Empty = all in default order
# Custom cipher preference: [AES-128-GCM, AES-128-CCM]

See docs/CONFIGURATION.md for complete encryption configuration options. See docs/SECURITY.md for security implications and recommendations.

SMB message signing provides integrity protection against man-in-the-middle attacks and message tampering. The signature is computed over the SMB2 header and body, placed in the 16-byte Signature field of the SMB2 header.

DialectAlgorithmKey Derivation
SMB 2.0.2HMAC-SHA256Direct from session key
SMB 3.0AES-128-CMACSP800-108 KDF
SMB 3.0.2AES-128-CMACSP800-108 KDF
SMB 3.1.1AES-128-GMAC (preferred) or AES-128-CMACSP800-108 KDF with preauth hash

AES-128-GMAC is the preferred signing algorithm for SMB 3.1.1 because it leverages the same GCM hardware acceleration as encryption. If a 3.1.1 client omits the SIGNING_CAPABILITIES negotiate context, the server defaults to AES-128-CMAC per specification.

The signing algorithm is determined by the negotiated dialect and negotiate contexts:

  1. SMB 2.0.2: Always HMAC-SHA256 (no negotiation)
  2. SMB 3.0/3.0.2: Always AES-128-CMAC (no negotiation)
  3. SMB 3.1.1 with SIGNING_CAPABILITIES: First mutually supported algorithm from server preference list
  4. SMB 3.1.1 without SIGNING_CAPABILITIES: Default to AES-128-CMAC

SP800-108 Counter Mode KDF for Signing Keys

Section titled “SP800-108 Counter Mode KDF for Signing Keys”

For SMB 3.0+, the signing key is derived from the session key using NIST SP800-108 in Counter Mode with HMAC-SHA256 as the PRF:

SigningKey = KDF(SessionKey, Label, Context)
Where:
PRF = HMAC-SHA256
Key = SessionKey (from authentication)
Label = "SMBSigningKey\0" (null-terminated)
Context = varies by dialect (see Key Derivation section)
  • NEGOTIATE: Never signed (no session key yet)
  • SESSION_SETUP: Final response can be signed (to prove server identity)
  • After SESSION_SETUP: All messages signed when signing is enabled for the session
  • Encrypted messages: Signing is redundant when encryption is active (AEAD provides integrity), but DittoFS still signs to match Windows Server behavior
adapters:
smb:
signing:
enabled: true # Advertise signing capability
required: false # Require all clients to sign
# Signing algorithm preference (for 3.1.1 negotiate context)
# Default: [AES-128-GMAC, AES-128-CMAC]
preferred_algorithms: []

SMB3 uses NIST SP800-108 Counter Mode KDF with HMAC-SHA256 as the PRF to derive per-purpose cryptographic keys from the session key obtained during authentication.

KDF-HMAC-SHA256(Key, Label, Context):
i = 1
L = keyLength * 8 (in bits)
result = PRF(Key, i || Label || 0x00 || Context || L)
return result[0:keyLength]

Where || denotes concatenation and PRF is HMAC-SHA256.

Four keys are derived per session:

KeyLabel (null-terminated)Usage
SigningKey"SMBSigningKey\0"Message signing (HMAC/CMAC/GMAC)
EncryptionKey"SMBS2CCipherKey\0" (3.0) / "SMBServerEncryptionKey\0" (3.1.1)Server-to-client encryption
DecryptionKey"SMBC2SCipherKey\0" (3.0) / "SMBClientEncryptionKey\0" (3.1.1)Client-to-server decryption
ApplicationKey"SMBAppKey\0"Application-level use
DialectKDF Context
SMB 3.0"SmbSign\0" / "ServerIn \0" / "ServerOut\0" (fixed strings)
SMB 3.0.2Same as 3.0
SMB 3.1.1Preauth integrity hash value (SHA-512 hash chain output)

The use of the preauth integrity hash as KDF context in 3.1.1 is critical for security: it cryptographically binds the derived keys to the exact negotiate exchange, preventing downgrade attacks where a MITM strips security capabilities.

For 128-bit ciphers (AES-128-GCM, AES-128-CCM, AES-128-CMAC, AES-128-GMAC), the derived key is 16 bytes. For 256-bit ciphers (AES-256-GCM, AES-256-CCM), the derived key is 32 bytes; the session key is required to be at least 32 bytes (achieved by hashing with SHA-256 if needed).

Leases V2 extend SMB2.1 lease functionality with ParentLeaseKey tracking and epoch-based stale break prevention. Directory leasing adds Read-caching for directory listings, reducing QUERY_DIRECTORY round trips.

FeatureLease V1 (SMB 2.1)Lease V2 (SMB 3.0+)
ParentLeaseKeyNot availableLinks child to parent directory lease
EpochNot availableMonotonic counter for stale break detection
Directory LeasesNot supportedRead-caching on directories
Create ContextSMB2_CREATE_REQUEST_LEASESMB2_CREATE_REQUEST_LEASE_V2

Leases use a combination of three caching flags:

FlagAbbreviationDescription
ReadRClient may cache read data without revalidating
WriteWClient may cache writes and defer flushing to server
HandleHClient may cache the file handle and defer CLOSE

Common state combinations:

StateFlagsTypical Use
NoneNo caching
ReadRShared read caching (multiple clients)
Read-HandleRHRead caching with handle caching
Read-WriteRWExclusive read/write caching
Read-Write-HandleRWHFull exclusive caching (most aggressive)
Grant: None -> R (shared read)
None -> RWH (exclusive, single opener)
Break: RWH -> RH (another client opens for read)
RWH -> None (another client opens for write)
RH -> R (handle caching revoked)
R -> None (all caching revoked)

Break is initiated by the server when a conflicting open arrives. The original client must acknowledge the break and flush cached data before the new open proceeds.

Directory leases grant Read-caching on a directory, allowing the client to cache directory listings locally:

  • Granted: When a client opens a directory with a lease V2 create context
  • Cached data: QUERY_DIRECTORY results are cached client-side
  • Break trigger: Any modification to the directory’s contents (create, delete, rename)
  • Break target: Always breaks to None (directory leases only support Read state)

Directory lease breaks are triggered by the MetadataService when CreateFile, RemoveFile, or Rename modifies a directory. The break flows through the LockManager.CheckAndBreakDirectoryCaching() method.

Each lease V2 has a monotonic epoch counter that increments on every state change. When a lease break notification is sent, it includes the current epoch. If the client sends a break acknowledgment with a stale epoch (lower than current), the server knows the client missed an intermediate break and can take corrective action.

Lease V2 includes a ParentLeaseKey that associates the file’s lease with its parent directory’s lease. When a file operation triggers a directory lease break, the server can identify which parent directory leases need to be broken by matching ParentLeaseKey values.

adapters:
smb:
leases:
enabled: true # Enable lease support
directory_leases: true # Enable directory leasing
lease_break_timeout: 35s # Time to wait for break acknowledgment

Durable handles allow SMB clients to reconnect and resume file operations after a network disconnection without losing cached state. DittoFS implements both V1 and V2 durable handles per the MS-SMB2 specification.

V1 durable handles (SMB 2.0.2+) require a batch oplock:

  • DHnQ (Durable Handle Request): Client requests a durable handle in CREATE
  • DHnC (Durable Handle Reconnect): Client reconnects to a preserved handle
  • Requirement: The file must have been opened with a batch oplock grant
  • Limitation: No idempotent reconnection (duplicate reconnects may fail)

V2 durable handles (SMB 3.0+) add CreateGuid for idempotent reconnection:

  • DH2Q (Durable Handle V2 Request): Client provides a CreateGuid (16-byte GUID)
  • DH2C (Durable Handle V2 Reconnect): Client provides CreateGuid for matching
  • No oplock requirement: V2 handles do not require batch oplock
  • Idempotent: Multiple reconnect attempts with the same CreateGuid succeed
  • Precedence: When both V1 and V2 create contexts are present, V2 takes precedence per MS-SMB2

V2 reconnect performs 14+ validation checks per MS-SMB2 specification:

  1. Look up handle by CreateGuid
  2. Verify handle is in disconnected/durable state
  3. Verify requesting user matches original creator
  4. Verify file name matches
  5. Verify session key hash matches (SHA-256 of signing key)
  6. Verify share name matches
  7. Verify handle has not timed out
  8. Verify no conflicting opens exist
  9. … (additional checks per spec)

If all checks pass, the handle is restored to the new session. The IsDurable flag is NOT set on the restored handle — the client must re-request durability after reconnect.

  • Default timeout: 60 seconds (configurable)
  • Scavenger interval: Periodic background goroutine scans for expired handles
  • Cleanup: Expired handles are cleaned up (pending I/O cancelled, locks released, handle removed from store)
  • Scavenger lifecycle: Tied to Serve context — stops automatically on adapter shutdown

V2 durable handles support an optional App Instance ID (16-byte GUID) for cluster failover scenarios. When a client reconnects from a different cluster node with the same App Instance ID, the server can close the old handle and transfer state to the new session.

DH2Q Create Context (Durable Handle V2 Request):
Timeout: uint32 (requested timeout in milliseconds)
Flags: uint32 (PERSISTENT flag for persistent handles)
Reserved: 8 bytes
CreateGuid: 16 bytes (client-generated GUID)
DH2C Create Context (Durable Handle V2 Reconnect):
FileId: 16 bytes (persistent + volatile)
CreateGuid: 16 bytes (must match original DH2Q)
Flags: uint32
adapters:
smb:
durable_handles:
enabled: true # Enable durable handle support
default_timeout: 60s # Default handle preservation timeout
scavenger_interval: 30s # How often to scan for expired handles
max_handles_per_session: 1000 # Limit per session

DittoFS supports Kerberos authentication for SMB clients through the SPNEGO (Simple and Protected GSSAPI Negotiation Mechanism) protocol during SESSION_SETUP. The Kerberos provider is shared between NFS (RPCSEC_GSS) and SMB (SPNEGO) adapters.

Client Server
| |
|--- NEGOTIATE (SecurityBuffer) ---->|
|<-- NEGOTIATE Response (mechTypes) -|
| |
|--- SESSION_SETUP (SPNEGO Init) --->|
| Contains: mechToken (AP-REQ) |
| or NTLM Negotiate |
| |
|<-- SESSION_SETUP Response ---------|
| Contains: mechToken (AP-REP) |
| or NTLM Challenge |
| Status: MORE_PROCESSING (NTLM) |
| or SUCCESS (Kerberos) |
| |
[NTLM only: additional round-trip] |
|--- SESSION_SETUP (NTLM Auth) ----->|
|<-- SESSION_SETUP (SUCCESS) --------|

The SPNEGO wrapper advertises both Kerberos and NTLM mechanism OIDs. Clients with valid Kerberos tickets choose Kerberos for single round-trip authentication.

  1. Client obtains TGT from KDC, then requests service ticket for cifs/server.example.com@REALM
  2. Client sends AP-REQ inside SPNEGO InitToken in SESSION_SETUP
  3. Server validates AP-REQ against keytab, extracts session key
  4. Server sends AP-REP (mutual authentication) inside SPNEGO Response
  5. Session key from Kerberos is used as input to SP800-108 KDF for signing/encryption keys

The Kerberos session key (from AP-REQ validation) becomes the base session key for the SP800-108 KDF. This key is then used to derive:

  • SigningKey (for AES-CMAC/GMAC message signing)
  • EncryptionKey (for AES-GCM/CCM encryption)
  • DecryptionKey (for AES-GCM/CCM decryption)

When Kerberos is not available (no keytab configured, client has no valid TGT, or DNS resolution fails), the server falls back to NTLM authentication:

  1. Client sends NTLM Negotiate message
  2. Server responds with NTLM Challenge
  3. Client sends NTLM Authenticate with NTProofStr
  4. Server validates against stored password hash

NTLM provides weaker security than Kerberos: no mutual authentication, vulnerable to relay attacks, and the session key is derived from the password hash rather than a fresh Kerberos session key.

When authentication fails and guest access is enabled:

  • Session is created with guest privileges
  • No signing: Guest sessions cannot sign messages (no session key)
  • No encryption: Guest sessions cannot be encrypted (no key for KDF)
  • Security implications: guest access should be limited to read-only public shares

DittoFS uses a shared Kerberos keytab for both NFS and SMB:

kerberos:
enabled: true
keytab_path: /etc/dittofs/dittofs.keytab
service_principal: nfs/server.example.com@EXAMPLE.COM

The server automatically derives the cifs/ service principal from the configured nfs/ principal for SMB authentication. The keytab supports hot-reload: when the file is replaced on disk, the server detects the change and loads the new key without restart.

See docs/SECURITY.md for detailed Kerberos security considerations.

DittoFS supports simultaneous NFS and SMB access to the same files and directories. This section documents how the protocols interact through the unified LockManager in pkg/metadata/lock/.

The following table shows what happens when an operation from one protocol encounters active caching state from the other protocol:

NFS operation encountering SMB state:

NFS OperationSMB Read Lease (R)SMB Write Lease (RW/RWH)SMB Dir Lease
READCoexistsBreak to None, wait for ack
WRITEBreak to NoneBreak to None, wait for ack
CREATEBreak directory lease
REMOVEBreak to NoneBreak to None, wait for ackBreak directory lease
RENAMEBreak to None (src + dst)Break to None, wait for ackBreak both src and dst dir leases
LINKBreak target directory lease
SETATTR (file)Break to None
OPEN (delegation grant)Check coexistenceConflict: break lease first

SMB operation encountering NFS state:

SMB OperationNFS Read DelegNFS Write DelegNFS Dir Deleg
CREATE (read)CoexistsCB_RECALL, wait
CREATE (write)CB_RECALL, waitCB_RECALL, wait
WRITECB_RECALL, waitCB_RECALL, wait
DELETECB_RECALL, waitCB_RECALL, wait
RENAMECB_RECALL (src + dst)CB_RECALL, waitCB_RECALL both dirs
CREATE (in dir)CB_RECALL + CB_NOTIFY
DELETE (in dir)CB_RECALL + CB_NOTIFY
QUERY_DIR (lease req)Check coexistence
NFS StateSMB StateResultRationale
Read delegationRead lease (R)CoexistBoth are read-only caching; no data conflict
Read delegationWrite lease (RW/RWH)ConflictWrite lease allows cached writes that read delegation won’t see
Write delegationAny leaseConflictWrite delegation implies exclusive write caching
Any delegationWrite leaseConflictWrite lease implies exclusive write caching
Dir delegationDir leaseCoexistBoth are read-only directory caching

Break Flow: SMB Write Triggers NFS Delegation Recall

Section titled “Break Flow: SMB Write Triggers NFS Delegation Recall”
SMB Client LockManager NFS Client
| | |
|-- CREATE (write) ---------->| |
| |-- CheckAndBreakCachingForWrite |
| | find NFS read delegation |
| | mark delegation.Breaking |
| |-- OnDelegationRecall -------->|
| | (via NFSBreakHandler) |
| | CB_RECALL --->|
| | |
| |<---- DELEGRETURN -------------|
| | delegation removed |
|<-- CREATE response ----------| |

Break Flow: NFS Open Triggers SMB Lease Break

Section titled “Break Flow: NFS Open Triggers SMB Lease Break”
NFS Client LockManager SMB Client
| | |
|-- OPEN (write) ------------>| |
| |-- CheckAndBreakCachingForWrite |
| | find SMB RWH lease |
| | mark lease.Breaking |
| |-- OnOpLockBreak ------------->|
| | (via SMBBreakHandler) |
| | LEASE_BREAK ----->|
| | |
| |<---- LEASE_BREAK_ACK ---------|
| | lease downgraded/removed |
|<-- OPEN response ------------| |

When a file is created, deleted, or renamed, the MetadataService triggers directory caching breaks for the parent directory:

Any Client MetadataService LockManager
| | |
|-- CREATE file in /dir ------>| |
| |-- notifyDirChange("/dir") -->|
| | |
| | CheckAndBreakDirectoryCaching:
| | 1. Break SMB dir leases |
| | 2. Break NFS dir delegations
| | 3. Queue DirNotification |
| | (type=Add, name=file) |
| | |
| | Consumers: |
| | - SMB: CHANGE_NOTIFY |
| | - NFS: CB_NOTIFY |

RENAME across directories breaks both source and target directory leases and delegations.

To prevent rapid grant-break-grant-break cycles (lease/delegation storms), the LockManager maintains a unified recentlyBrokenCache with a configurable TTL (default 30 seconds):

  1. When a lease or delegation is broken, the file handle is marked in the cache
  2. Subsequent lease/delegation grant requests check the cache
  3. If the handle was recently broken, the grant is denied (client retries later)
  4. The TTL applies cross-protocol: an NFS delegation broken due to SMB activity prevents NFS re-grant for the TTL duration, and vice versa

Directory change notifications are queued in a bounded notification queue owned by the LockManager:

  • Capacity: 1024 events per directory (configurable)
  • Overflow: Collapses to a single “full rescan needed” event
  • Flush: Triggered by size threshold (100 events) or time threshold (500ms)
  • Consumers: NFS adapter drains into CB_NOTIFY; SMB adapter drains into CHANGE_NOTIFY
  • Event types: Add, Remove, Rename, Modify (with entry name and old/new names for rename)

Hidden files are handled differently between Unix and Windows:

  • Unix convention: Files starting with . are hidden
  • Windows convention: Files with the Hidden attribute flag are hidden

DittoFS bridges both conventions:

  • Dot-prefix files (.gitignore, .DS_Store) appear with FILE_ATTRIBUTE_HIDDEN in SMB listings
  • The Hidden attribute can also be explicitly set via SMB SET_INFO (FileBasicInformation)
  • Both conventions are persisted: dot-prefix detection is automatic, explicit Hidden flag is stored in metadata

Special Files (FIFO, Socket, Device Nodes)

Section titled “Special Files (FIFO, Socket, Device Nodes)”

Unix special files (FIFO, socket, block device, character device) have no meaningful representation in SMB:

  • Via NFS: Full support — MKNOD creates, GETATTR returns correct type
  • Via SMB: Hidden from directory listings entirely

This behavior matches commercial NAS devices (Synology, QNAP) which typically do not expose special files via SMB.

Symlinks are handled transparently via MFsymlink format:

  • NFS-created symlinks: Appear as MFsymlink files (1067 bytes) when read via SMB
  • SMB-created symlinks: MFsymlink files are automatically converted to real symlinks on CLOSE
  • Both NFS and SMB clients can follow symlinks correctly

DittoFS implements SMB2 byte-range locking per [MS-SMB2] 2.2.26/2.2.27.

  • Shared (Read) Locks: Multiple clients can hold shared locks on overlapping ranges
  • Exclusive (Write) Locks: Only one client can hold an exclusive lock on a range
// Lock request processing
for each lockElement in request.Locks {
if lockElement.Flags & UNLOCK {
// Release lock - NOT rolled back on batch failure
store.UnlockFile(handle, sessionID, offset, length)
} else {
// Acquire lock - rolled back if later operation fails
store.LockFile(handle, lock)
acquiredLocks = append(acquiredLocks, lockElement)
}
}

Locks are enforced on READ/WRITE operations:

  • READ: Blocked by another session’s exclusive lock on overlapping range
  • WRITE: Blocked by any other session’s lock (shared or exclusive) on overlapping range

Same-session locks never block the owning session’s I/O operations.

Locks are ephemeral (in-memory only) and persist until:

  • Explicitly released via LOCK with SMB2_LOCKFLAG_UNLOCK
  • File handle is closed (CLOSE command)
  • Session disconnects (LOGOFF or connection drop)
  • Server restarts (all locks lost)

Per SMB2 specification ([MS-SMB2] 2.2.26):

  1. Unlock operations are NOT rolled back: If a batch LOCK request includes unlocks and a later lock acquisition fails, the successful unlocks remain in effect.

  2. Lock type changes: When re-locking an existing range with a different type (shared to exclusive), rollback removes the lock entirely rather than reverting to the original type.

Locking is automatically enabled with no additional configuration required. Locks are stored in-memory per metadata store instance.

DittoFS implements SMB2 opportunistic locks per [MS-SMB2] 2.2.14, 2.2.23, 2.2.24.

  • None (0x00): No caching allowed
  • Level II (0x01): Shared read caching — multiple clients can cache read data
  • Exclusive (0x08): Exclusive read/write caching — single client can cache reads and writes
  • Batch (0x09): Like Exclusive with handle caching — client can delay close operations
  1. Grant: Client requests oplock level in CREATE request
  2. Cache: Client caches file data according to granted level
  3. Break: When another client opens the file, server sends OPLOCK_BREAK notification
  4. Acknowledge: Original client flushes cached data and acknowledges break
// Level II allows multiple readers (first holder tracked)
clientA opens file -> granted Level II
clientB opens file (Level II) -> granted Level II (coexistence)
// Exclusive/Batch requires break on conflict
clientA opens file -> granted Exclusive
clientB opens file -> server initiates break to Level II
-> clientB gets None (must retry after break)

When an oplock break is initiated, the conflicting client is not granted an oplock immediately. It must retry after the break acknowledgment.

  • Reduced network traffic: Clients cache reads locally
  • Better write performance: Exclusive oplock allows write caching
  • Handle caching: Batch oplock reduces CREATE/CLOSE round trips
  • Leases preferred: SMB3 clients should use Lease V2 instead of traditional oplocks
  • In-memory tracking: Oplock state is lost on server restart
  • Single holder tracking: Only tracks one Level II holder (others coexist but are not tracked)

DittoFS implements CHANGE_NOTIFY support per [MS-SMB2] 2.2.35/2.2.36, with directory change events delivered through the unified notification queue.

The implementation accepts CHANGE_NOTIFY requests and delivers change events through the LockManager’s notification queue:

  • Watch Registration: Clients can register directory watches with completion filters
  • Change Detection: CREATE, CLOSE (delete-on-close), SET_INFO (rename), and cross-protocol operations trigger change events
  • Notification Queue: Events are queued and delivered to registered watchers via the LockManager
Client registers CHANGE_NOTIFY -> STATUS_PENDING
|
MetadataService detects change -> LockManager.notifyDirChange()
|
LockManager queues DirNotification -> flush to registered consumers
|
SMB adapter delivers CHANGE_NOTIFY response with FILE_NOTIFY_INFORMATION

The following filters are recognized:

FilterValueDescription
FILE_NOTIFY_CHANGE_FILE_NAME0x0001File create/delete/rename
FILE_NOTIFY_CHANGE_DIR_NAME0x0002Directory create/delete/rename
FILE_NOTIFY_CHANGE_ATTRIBUTES0x0004Attribute changes
FILE_NOTIFY_CHANGE_SIZE0x0008File size changes
FILE_NOTIFY_CHANGE_LAST_WRITE0x0010Last write time changes

Full async notification delivery requires:

  1. Connection-level async response infrastructure
  2. Message ID tracking for pending requests
  3. Proper SMB2 async response framing
Terminal window
# Start server with debug logging
DITTOFS_LOGGING_LEVEL=DEBUG ./dfs start
# Mount and test (macOS)
sudo mount_smbfs //testuser:testpass@localhost:12445/export /mnt/smb
cd /mnt/smb
# Test operations
ls -la # QUERY_DIRECTORY
cat readme.txt # READ
echo "test" > new # CREATE + WRITE
mkdir foo # CREATE (directory)
rm new # SET_INFO (delete)
rmdir foo # SET_INFO (delete)
mv file1 file2 # SET_INFO (rename)
Terminal window
# Interactive mode
smbclient //localhost/export -p 12445 -U testuser%testpass
smb: \> ls
smb: \> get file.txt
smb: \> put local.txt
smb: \> mkdir newdir
smb: \> rm file.txt
smb: \> rmdir newdir
smb: \> exit
Terminal window
# Run SMB E2E tests
sudo go test -tags=e2e -v ./test/e2e/ -run TestSMB
# Run interoperability tests (NFS <-> SMB)
sudo go test -tags=e2e -v ./test/e2e/ -run TestInterop
# Run specific test
sudo go test -tags=e2e -v ./test/e2e/ -run TestSMBCreateFileWithContent
# Run SMB Kerberos authentication tests
sudo go test -tags=e2e -v ./test/e2e/ -run TestSMBKerberos
# Run cross-protocol lease/delegation tests
sudo go test -tags=e2e -v ./test/e2e/ -run TestCrossProtocol
  1. Verify server is running: netstat -an | grep 12445
  2. Check firewall rules
  3. Try explicit port: port=12445 in mount options
  1. Verify user exists in config
  2. Check password hash is valid bcrypt
  3. Enable debug logging to see authentication flow
  4. Ensure user has share permissions
  5. For Kerberos: verify the keytab contains the cifs/ service principal and the KDC is reachable
  1. Increase timeout in SMB config
  2. Check block store connectivity (S3, filesystem)
  3. Enable debug logging for detailed timing
Terminal window
# Clear SMB credential cache
security delete-internet-password -s localhost
# Check for stale mounts
mount | grep smb
# Force unmount
sudo umount -f /mnt/smb
Terminal window
# Install cifs-utils if missing
sudo apt-get install cifs-utils # Debian/Ubuntu
sudo yum install cifs-utils # RHEL/CentOS
# Check kernel module
lsmod | grep cifs

See docs/TROUBLESHOOTING.md for cross-protocol troubleshooting, including:

  • File locked by another protocol
  • Delegation recall timeouts
  • Lease break storms
  • Stale data after cross-protocol writes
  1. No SMB1 support: Legacy protocol, not implemented for security reasons
  2. No compression: SMB 3.1.1 compression contexts are not implemented
  3. No multichannel: Multiple TCP connections per session not supported
  4. No persistent handles: Cluster-aware handles require shared state infrastructure
  5. No RDMA transport: Remote Direct Memory Access not supported
  6. No QUIC transport: SMB over QUIC (UDP) not supported
  7. No security descriptors: Windows ACLs not supported (uses POSIX permissions)
  8. No DFS referrals: Distributed File System not supported
  1. Ephemeral locks and oplocks: Both byte-range locks and oplocks are in-memory only, lost on server restart
  2. No blocking locks: Lock requests fail immediately if conflicting lock exists
  3. Single-node only: No clustering or high availability for SMB state
  4. Durable handle state is in-memory: Durable handles survive disconnection but not server restart (BadgerDB/PostgreSQL stores persist handle metadata but in-memory state is lost)
  1. No extended attributes (xattrs): EA support not implemented
  2. No server-side copy offload: FSCTL_SRV_COPYCHUNK not implemented
  3. No per-file encryption: Encryption is per-session or per-share only
TermDefinition
AEADAuthenticated Encryption with Associated Data — encryption providing both confidentiality and integrity (AES-GCM, AES-CCM)
ACLAccess Control List — Windows permission model
AES-CCMAES in Counter with CBC-MAC mode — AEAD cipher for SMB 3.0/3.0.2
AES-CMACAES-based Cipher-based Message Authentication Code — signing algorithm for SMB 3.0+
AES-GCMAES in Galois/Counter Mode — AEAD cipher preferred for SMB 3.1.1
AES-GMACAES-GCM used for authentication only (no encryption) — signing algorithm for SMB 3.1.1
AP-REQKerberos Application Request — contains client’s service ticket
AP-REPKerberos Application Reply — provides mutual authentication
CIFSCommon Internet File System — older name for SMB
CreateGuid16-byte GUID used for idempotent durable handle V2 reconnection
CreditFlow control unit in SMB2
DH2Q/DH2CDurable Handle V2 Request/Reconnect create contexts
DHnQ/DHnCDurable Handle V1 Request/Reconnect create contexts
DialectSMB protocol version (e.g., 0x0311 = SMB 3.1.1)
EpochMonotonic counter on lease V2 for stale break detection
FileID16-byte handle for open file (8 persistent + 8 volatile)
GUID16-byte globally unique identifier
KDFKey Derivation Function — derives session-specific keys from base key
Lease V2Enhanced lease with ParentLeaseKey and epoch tracking (SMB 3.0+)
NetBIOSNetwork Basic Input/Output System — legacy session layer
NT_STATUSWindows error code format
OplockOpportunistic lock — client caching hint
ParentLeaseKeyLease V2 field linking file lease to parent directory lease
Preauth IntegritySHA-512 hash chain over negotiate/session-setup messages for downgrade protection
SessionID64-bit identifier for authenticated session
ShareNetwork-accessible folder (like NFS export)
SIDSecurity Identifier — Windows user/group identity
SP800-108NIST key derivation specification using Counter Mode with HMAC-SHA256
SPNEGOSimple and Protected GSSAPI Negotiation Mechanism — wraps NTLM/Kerberos tokens
Transform Header52-byte header wrapping encrypted SMB3 messages (magic 0xFD)
TreeID32-bit identifier for share connection
UTF-16LE16-bit Unicode, little-endian byte order
  • MS-SMB2 - SMB2/3 Protocol Specification
  • MS-NLMP - NTLM Authentication Protocol
  • MS-FSCC - File System Control Codes
  • MS-ERREF - Windows Error Codes
  • RFC 4178 - SPNEGO Protocol
  • RFC 1813 - NFS Version 3 Protocol Specification
  • RFC 7530 - NFS Version 4.0 Protocol Specification
  • RFC 8881 - NFS Version 4.1 Protocol Specification
  • NIST SP800-108 - Key Derivation Using Pseudorandom Functions
  • go-smb2 - SMB2 client in Go
  • Samba - SMB/CIFS implementation for Unix