SMB Protocol Internals
This document is aimed at contributors to the DittoFS SMB implementation. It covers wire formats, message flows, key derivation, lease and durable-handle internals, cross-protocol coordination, credit flow control, and code structure.
Operators and end-users: see ../guide/smb.md for mount instructions, configuration, and client-facing behaviour.
Table of Contents
Section titled “Table of Contents”- SMB vs NFS: Conceptual Mapping
- Message Format and Wire Layout
- Connection Lifecycle
- SMB2 Request Processing
- Code Structure
- Two-Phase Write Pattern
- Block Store Integration
- Error Mapping
- Credit Flow Control
- SMB3 Dialect Negotiation Details
- Encryption Internals
- Signing Internals
- Key Derivation (SP800-108)
- Lease State Machine and Epoch Internals
- Durable Handle Validation and Wire Format
- Kerberos and SPNEGO Flow
- Cross-Protocol Behaviour
- Byte-Range Locking
- Opportunistic Locks
- Change Notifications
SMB vs NFS: Conceptual Mapping
Section titled “SMB vs NFS: Conceptual Mapping”| Aspect | NFS (v3/v4) | SMB2 (2.0.2) | SMB3 (3.0-3.1.1) |
|---|---|---|---|
| Origin | Unix (Sun Microsystems, 1984) | Windows (IBM/Microsoft, 1983) | Windows (Microsoft, 2012) |
| Design | v3: Stateless / v4: Stateful | Stateful, session-based | Stateful, session-based |
| Identity | UID/GID (Unix) | SID (Windows Security ID) | SID + Kerberos principal |
| Permissions | Unix mode bits / NFSv4 ACLs | ACLs (Access Control Lists) | ACLs |
| Transport | TCP (port 2049) | TCP (port 445) | TCP (port 445) |
| Framing | RPC record marking | NetBIOS session header | NetBIOS + Transform header |
| Encoding | XDR (big-endian) | Custom (little-endian) | Custom (little-endian) |
| Header | Variable (RPC) | Fixed 64 bytes | Fixed 64 bytes (+ 52-byte transform) |
| Strings | UTF-8 | UTF-16LE | UTF-16LE |
| Flow control | None (relies on TCP) | Credit-based | Credit-based |
| Encryption | krb5p (RPCSEC_GSS) | None | AES-GCM / AES-CCM (transform header) |
| Signing | krb5i (RPCSEC_GSS) | HMAC-SHA256 | AES-CMAC / AES-GMAC |
| Client caching | Delegations | Oplocks | Leases V2 (file + directory) |
| Handle resilience | Volatile | Volatile | Durable / Persistent handles |
NFS-to-SMB concept mapping:
| NFS Concept | SMB Equivalent | Notes |
|---|---|---|
| Export | Share | Network-accessible directory |
| Mount | Tree Connect | Establishing access to a share |
| File Handle | FileID | Opaque identifier for open file |
| UID/GID | SID | User/group identity |
| Mode bits | Security Descriptor | Permission model |
| LOOKUP | Part of CREATE | SMB combines lookup and open |
| GETATTR | QUERY_INFO | Get file metadata |
| SETATTR | SET_INFO | Set file metadata |
| READDIR | QUERY_DIRECTORY | List directory contents |
| COMMIT | FLUSH | Sync to disk |
| Delegation | Lease V2 | Client caching grant |
| CB_RECALL | Lease Break Notification | Cache invalidation |
| CB_NOTIFY | CHANGE_NOTIFY | Directory change events |
Message Format and Wire Layout
Section titled “Message Format and Wire Layout”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.
Connection Lifecycle
Section titled “Connection Lifecycle”SMB connections follow a multi-phase setup before file operations can begin:
- NEGOTIATE — Client and server agree on protocol dialect, capabilities, and security parameters (cipher suites, signing algorithms, preauth integrity)
- SESSION_SETUP — Client authenticates (NTLM or Kerberos via SPNEGO), receives a SessionID; session keys are derived and encryption/signing activated
- TREE_CONNECT — Client connects to a specific share, receives a TreeID; per-share encryption may be enforced
- File Operations — CREATE opens a file (returns FileID), then READ/WRITE/CLOSE use that FileID
- 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.
SMB2 Request Processing
Section titled “SMB2 Request Processing”Message Flow
Section titled “Message Flow”- TCP connection accepted
- NetBIOS session header parsed
- SMB2 message decoded (decrypted if transform header present)
- Session/tree context validated
- Command handler dispatched
- Handler calls metadata/block stores
- Response encoded (encrypted if session requires it) and sent
Request Dispatch
Section titled “Request Dispatch”// Per-connection parallel request handlingfor { msg := readSMB2Message(conn) go handleRequest(msg) // Concurrent handling}Critical Commands
Section titled “Critical Commands”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 derivationTREE_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 contextsREAD: Read file content (with cache support)WRITE: Write file content (with cache support)CLOSE: Close file handle and cleanupFLUSH: Flush cached data to block storeQUERY_INFO: Get file/directory attributesSET_INFO: Modify attributes, rename, deleteQUERY_DIRECTORY: List directory contentsLOCK: Acquire/release byte-range locksIOCTL: VALIDATE_NEGOTIATE_INFO, server-side copy
Code Structure
Section titled “Code Structure”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 ...Two-Phase Write Pattern
Section titled “Two-Phase Write Pattern”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 datablockStore, _ := rt.GetBlockStoreForHandle(ctx, handle)blockStore.WriteAt(ctx, writeOp.ContentID, data, offset)
// 3. Commit write (update metadata: size, timestamps)metadataStore.CommitWrite(authCtx, writeOp)Block Store Integration
Section titled “Block Store Integration”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 handleblockStore, 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
seam that plumbs []BlockRef into the engine — handler code does not change.
READ Response Buffer Pool
Section titled “READ Response Buffer Pool”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.
Error Mapping
Section titled “Error Mapping”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:
ErrNotFound→STATUS_OBJECT_NAME_NOT_FOUNDErrAlreadyExists→STATUS_OBJECT_NAME_COLLISIONErrAccessDenied/ErrPermissionDenied/ErrAuthRequired→STATUS_ACCESS_DENIED(SMB has no EPERM distinction per MS-ERREF 2.3)ErrIsDirectory→STATUS_FILE_IS_A_DIRECTORYErrStaleHandle→STATUS_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.
Wrapped Error Unwrapping
Section titled “Wrapped Error Unwrapping”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.
Credit Flow Control
Section titled “Credit Flow Control”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.
Defaults
Section titled “Defaults”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:
availablemirrors the client’scur_credits. EveryGrant(N)incrementsavailableby the amount the server actually extended the window; everyConsume(msgId, charge)decrementsavailablebycharge. The server never grants more thanMaxSessionCredits - available, so the client’s counter can never overflow.lowadvances lazily in 64-bit blocks.advanceLowreclaims bitmap words once an entire 64-sequence run has been consumed. Theavailablecounter is the authoritative credit tally; the bitmap span (high - low) can briefly exceedavailablewhen the oldest unconsumed bit is still in place, but stays bounded becauseavailablegates new grants.- Credit-exempt commands still consume sequence numbers. MS-SMB2 exempts
NEGOTIATE,CANCEL, and the firstSESSION_SETUP(SessionID=0) from credit validation, but the client still advances its msgId and decrementscur_creditsfor them. The server therefore MUST callConsumeon those messages too — otherwiseavailabledrifts up by one per credit-exempt request, saturates atMaxSessionCredits, and future responses carrycredits=0until the client runs out of credits (observed in issue #378).
Reclaim: Compound Response Zeroing
Section titled “Reclaim: Compound Response Zeroing”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.
Grant Path: Atomic, Pre-Write
Section titled “Grant Path: Atomic, Pre-Write”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.
Strategies
Section titled “Strategies”- Echo (default): grant what the client requests, bounded by
[MinGrant, MaxGrant]andRemaining(). Matches Samba’ssmb2_set_operation_credit:grant = credit_charge + (requested - 1). - Fixed: always grant
InitialGrant. - Adaptive:
InitialGrantscaled by live load and client-outstanding factors. More aggressive than Echo, primarily useful when throughput matters more than strict Samba interop.
Interoperability Notes
Section titled “Interoperability Notes”- Samba client hard-caps
cur_creditsatuint16max (65535) and rejects any response that would overflow. Prior to #378 we advertised ~384 credits per response (InitialGrant=256 x adaptive 1.5x boost), which saturated the client after ~85 SESSION_SETUP iterations and triggeredNT_STATUS_INVALID_NETWORK_RESPONSE. The fix lowered defaults to Samba-compatible values and enforcedRemaining()clamping at every response build site. - Windows client is more tolerant but grants are capped by the negotiated
Connection.MaxCredits; settingMaxSessionCredits > 8192gains nothing because Windows caps at 8192 by default too. - Multi-credit operations (large READ/WRITE) consume
CreditChargesequence numbers per request; the window handles charge > 1 natively.
References:
- MS-SMB2 3.3.1.2 (Server Credit Tracking)
- Samba
source3/smbd/smb2_server.csmb2_set_operation_creditand surrounding bitmap bookkeeping - Samba client check:
libcli/smb/smbXcli_base.c:4295-4298
SMB3 Dialect Negotiation Details
Section titled “SMB3 Dialect Negotiation Details”Dialect Selection
Section titled “Dialect Selection”The NEGOTIATE request contains a list of dialect revisions supported by the client. The server selects the highest dialect both sides support:
| Priority | Dialect | Hex | Key Capability |
|---|---|---|---|
| 1 (highest) | SMB 3.1.1 | 0x0311 | Preauth integrity, negotiate contexts |
| 2 | SMB 3.0.2 | 0x0302 | VALIDATE_NEGOTIATE_INFO |
| 3 | SMB 3.0 | 0x0300 | Encryption (AES-CCM), CMAC signing |
| 4 (lowest) | SMB 2.0.2 | 0x0202 | Basic SMB2 operations |
Negotiate Contexts (SMB 3.1.1)
Section titled “Negotiate Contexts (SMB 3.1.1)”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
Preauth Integrity Hash Chain
Section titled “Preauth Integrity Hash Chain”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.
Server Cipher and Signing Preference
Section titled “Server Cipher and Signing Preference”DittoFS uses the following default preference order:
Cipher preference (configurable):
- AES-128-GCM (0x0002) — fastest on modern hardware with AES-NI
- AES-128-CCM (0x0001) — fallback for 3.0/3.0.2
- AES-256-GCM (0x0004) — higher security, slightly slower
- AES-256-CCM (0x0003) — highest security AES-CCM variant
Signing preference (configurable):
- AES-128-GMAC (0x0002) — fastest for 3.1.1
- AES-128-CMAC (0x0001) — required for 3.0+
- 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.
Wire Format: Negotiate Request
Section titled “Wire Format: Negotiate Request”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)Dialect Configuration
Section titled “Dialect Configuration”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 negotiateEncryption Internals
Section titled “Encryption Internals”Transform Header
Section titled “Transform Header”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.
Cipher Suites
Section titled “Cipher Suites”| Cipher | ID | Default For | Key Size | Nonce Size | Tag Size |
|---|---|---|---|---|---|
| AES-128-CCM | 0x0001 | SMB 3.0, 3.0.2 | 128-bit | 11 bytes | 16 bytes |
| AES-128-GCM | 0x0002 | SMB 3.1.1 | 128-bit | 12 bytes | 16 bytes |
| AES-256-CCM | 0x0003 | — | 256-bit | 11 bytes | 16 bytes |
| AES-256-GCM | 0x0004 | — | 256-bit | 12 bytes | 16 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.
Encryption Enforcement Details
Section titled “Encryption Enforcement Details”Per-session encryption (Session.EncryptData): When mode is required, sessions
negotiating SMB 3.x have SMB2_SESSION_FLAG_ENCRYPT_DATA set in the SESSION_SETUP response
and all subsequent messages on the session are encrypted. When mode is preferred, AEAD keys
are still derived for SMB 3.x sessions (so per-share encryption can use them), but the session
flag is not set — whole-session message encryption is only enforced in required mode or
for shares with encrypt_data=true.
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.
Signing Internals
Section titled “Signing Internals”Signing Algorithms by Dialect
Section titled “Signing Algorithms by Dialect”| Dialect | Algorithm | Key Derivation |
|---|---|---|
| SMB 2.0.2 | HMAC-SHA256 | Direct from session key |
| SMB 3.0 | AES-128-CMAC | SP800-108 KDF |
| SMB 3.0.2 | AES-128-CMAC | SP800-108 KDF |
| SMB 3.1.1 | AES-128-GMAC (preferred) or AES-128-CMAC | SP800-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.
Signing Algorithm Selection
Section titled “Signing Algorithm Selection”The signing algorithm is determined by the negotiated dialect and negotiate contexts:
- SMB 2.0.2: Always HMAC-SHA256 (no negotiation)
- SMB 3.0/3.0.2: Always AES-128-CMAC (no negotiation)
- SMB 3.1.1 with SIGNING_CAPABILITIES: First mutually supported algorithm from server preference list
- 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)When Signing Is Required vs Optional
Section titled “When Signing Is Required vs Optional”- 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 behaviour
Key Derivation (SP800-108)
Section titled “Key Derivation (SP800-108)”Algorithm
Section titled “Algorithm”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.
Key Purposes
Section titled “Key Purposes”Four keys are derived per session:
| Key | Label (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 |
Context by Dialect
Section titled “Context by Dialect”| Dialect | KDF Context |
|---|---|
| SMB 3.0 | "SmbSign\0" / "ServerIn \0" / "ServerOut\0" (fixed strings) |
| SMB 3.0.2 | Same as 3.0 |
| SMB 3.1.1 | Preauth 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.
Key Length
Section titled “Key Length”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).
Lease State Machine and Epoch Internals
Section titled “Lease State Machine and Epoch Internals”Lease V2 vs V1
Section titled “Lease V2 vs V1”| Feature | Lease V1 (SMB 2.1) | Lease V2 (SMB 3.0+) |
|---|---|---|
| ParentLeaseKey | Not available | Links child to parent directory lease |
| Epoch | Not available | Monotonic counter for stale break detection |
| Directory Leases | Not supported | Read-caching on directories |
| Create Context | SMB2_CREATE_REQUEST_LEASE | SMB2_CREATE_REQUEST_LEASE_V2 |
Lease State Machine
Section titled “Lease State Machine”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.
Epoch-Based Stale Break Prevention
Section titled “Epoch-Based Stale Break Prevention”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.
ParentLeaseKey
Section titled “ParentLeaseKey”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.
Directory Lease Internals
Section titled “Directory Lease Internals”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.
Durable Handle Validation and Wire Format
Section titled “Durable Handle Validation and Wire Format”Durable Handle V1 (DHnQ/DHnC)
Section titled “Durable Handle V1 (DHnQ/DHnC)”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)
Durable Handle V2 (DH2Q/DH2C)
Section titled “Durable Handle V2 (DH2Q/DH2C)”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
CreateGuidfor matching - No oplock requirement: V2 handles do not require batch oplock
- Idempotent: Multiple reconnect attempts with the same
CreateGuidsucceed - Precedence: When both V1 and V2 create contexts are present, V2 takes precedence per MS-SMB2
Reconnect Validation
Section titled “Reconnect Validation”V2 reconnect performs 14+ validation checks per MS-SMB2 specification:
- Look up handle by
CreateGuid - Verify handle is in disconnected/durable state
- Verify requesting user matches original creator
- Verify file name matches
- Verify session key hash matches (SHA-256 of signing key)
- Verify share name matches
- Verify handle has not timed out
- Verify no conflicting opens exist
- … (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.
Handle Timeout and Scavenger
Section titled “Handle Timeout and Scavenger”- 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
Servecontext — stops automatically on adapter shutdown
App Instance ID
Section titled “App Instance ID”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.
Wire Format: Create Context
Section titled “Wire Format: Create Context”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: uint32Kerberos and SPNEGO Flow
Section titled “Kerberos and SPNEGO Flow”SPNEGO Negotiation Flow
Section titled “SPNEGO Negotiation Flow”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.
Kerberos Session Setup
Section titled “Kerberos Session Setup”- Client obtains TGT from KDC, then requests service ticket for
cifs/server.example.com@REALM - Client sends AP-REQ inside SPNEGO InitToken in SESSION_SETUP
- Server validates AP-REQ against keytab, extracts session key
- Server sends AP-REP (mutual authentication) inside SPNEGO Response
- Session key from Kerberos is used as input to SP800-108 KDF for signing/encryption keys
Session Key Extraction for KDF
Section titled “Session Key Extraction for KDF”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)
NTLM Fallback
Section titled “NTLM Fallback”When Kerberos is not available (no keytab configured, client has no valid TGT, or DNS resolution fails), the server falls back to NTLM authentication:
- Client sends NTLM Negotiate message
- Server responds with NTLM Challenge
- Client sends NTLM Authenticate with NTProofStr
- Server validates credentials: local users are checked against the stored NT hash; AD domain users are validated by forwarding the challenge/response to the Domain Controller over a sealed NETLOGON secure channel (requires a configured machine account)
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.
Guest Sessions
Section titled “Guest Sessions”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
Cross-Protocol Behaviour
Section titled “Cross-Protocol Behaviour”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/.
Cross-Protocol Behaviour Matrix
Section titled “Cross-Protocol Behaviour Matrix”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 Operation | SMB Read Lease (R) | SMB Write Lease (RW/RWH) | SMB Dir Lease |
|---|---|---|---|
| READ | Coexists | Break to None, wait for ack | — |
| WRITE | Break to None | Break to None, wait for ack | — |
| CREATE | — | — | Break directory lease |
| REMOVE | Break to None | Break to None, wait for ack | Break directory lease |
| RENAME | Break to None (src + dst) | Break to None, wait for ack | Break both src and dst dir leases |
| LINK | — | — | Break target directory lease |
| SETATTR (file) | — | Break to None | — |
| OPEN (delegation grant) | Check coexistence | Conflict: break lease first | — |
SMB operation encountering NFS state:
| SMB Operation | NFS Read Deleg | NFS Write Deleg | NFS Dir Deleg |
|---|---|---|---|
| CREATE (read) | Coexists | CB_RECALL, wait | — |
| CREATE (write) | CB_RECALL, wait | CB_RECALL, wait | — |
| WRITE | CB_RECALL, wait | CB_RECALL, wait | — |
| DELETE | CB_RECALL, wait | CB_RECALL, wait | — |
| RENAME | CB_RECALL (src + dst) | CB_RECALL, wait | CB_RECALL both dirs |
| CREATE (in dir) | — | — | CB_RECALL + CB_NOTIFY |
| DELETE (in dir) | — | — | CB_RECALL + CB_NOTIFY |
| QUERY_DIR (lease req) | — | — | Check coexistence |
Coexistence Rules
Section titled “Coexistence Rules”| NFS State | SMB State | Result | Rationale |
|---|---|---|---|
| Read delegation | Read lease (R) | Coexist | Both are read-only caching; no data conflict |
| Read delegation | Write lease (RW/RWH) | Conflict | Write lease allows cached writes that read delegation won’t see |
| Write delegation | Any lease | Conflict | Write delegation implies exclusive write caching |
| Any delegation | Write lease | Conflict | Write lease implies exclusive write caching |
| Dir delegation | Dir lease | Coexist | Both 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 ------------| |Directory Change Coordination
Section titled “Directory Change Coordination”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.
Anti-Storm Mechanism
Section titled “Anti-Storm Mechanism”To prevent rapid grant-break-grant-break cycles (lease/delegation storms), the LockManager
maintains a unified recentlyBrokenCache with a configurable TTL (default 30 seconds):
- When a lease or delegation is broken, the file handle is marked in the cache
- Subsequent lease/delegation grant requests check the cache
- If the handle was recently broken, the grant is denied (client retries later)
- The TTL applies cross-protocol: an NFS delegation broken due to SMB activity prevents NFS re-grant for the TTL duration, and vice versa
Notification Queue
Section titled “Notification Queue”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
Section titled “Hidden Files”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 withFILE_ATTRIBUTE_HIDDENin SMB listings - The
Hiddenattribute can also be explicitly set via SMBSET_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 behaviour matches commercial NAS devices (Synology, QNAP) which typically do not expose special files via SMB.
Symlinks
Section titled “Symlinks”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
Byte-Range Locking
Section titled “Byte-Range Locking”DittoFS implements SMB2 byte-range locking per [MS-SMB2] 2.2.26/2.2.27.
Lock Types
Section titled “Lock Types”- 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 Behaviour
Section titled “Lock Behaviour”// Lock request processingfor 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) }}Lock Enforcement
Section titled “Lock Enforcement”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.
Lock Lifetime
Section titled “Lock Lifetime”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)
Atomicity Limitations
Section titled “Atomicity Limitations”Per SMB2 specification ([MS-SMB2] 2.2.26):
-
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.
-
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.
Opportunistic Locks
Section titled “Opportunistic Locks”DittoFS implements SMB2 opportunistic locks per [MS-SMB2] 2.2.14, 2.2.23, 2.2.24.
Oplock Levels
Section titled “Oplock Levels”- 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
How Oplocks Work
Section titled “How Oplocks Work”- Grant: Client requests oplock level in CREATE request
- Cache: Client caches file data according to granted level
- Break: When another client opens the file, server sends OPLOCK_BREAK notification
- Acknowledge: Original client flushes cached data and acknowledges break
Oplock Behaviour
Section titled “Oplock Behaviour”// Level II allows multiple readers (first holder tracked)clientA opens file -> granted Level IIclientB opens file (Level II) -> granted Level II (coexistence)
// Exclusive/Batch requires break on conflictclientA opens file -> granted ExclusiveclientB 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.
Current Limitations
Section titled “Current Limitations”- 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)
Change Notifications
Section titled “Change Notifications”DittoFS implements CHANGE_NOTIFY support per [MS-SMB2] 2.2.35/2.2.36, with directory change events delivered through the unified notification queue.
Current Status
Section titled “Current Status”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
How It Works
Section titled “How It Works”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_INFORMATIONCompletion Filter Support
Section titled “Completion Filter Support”The following filters are recognized:
| Filter | Value | Description |
|---|---|---|
| FILE_NOTIFY_CHANGE_FILE_NAME | 0x0001 | File create/delete/rename |
| FILE_NOTIFY_CHANGE_DIR_NAME | 0x0002 | Directory create/delete/rename |
| FILE_NOTIFY_CHANGE_ATTRIBUTES | 0x0004 | Attribute changes |
| FILE_NOTIFY_CHANGE_SIZE | 0x0008 | File size changes |
| FILE_NOTIFY_CHANGE_LAST_WRITE | 0x0010 | Last write time changes |
Future Work
Section titled “Future Work”Full async notification delivery requires:
- Connection-level async response infrastructure
- Message ID tracking for pending requests
- Proper SMB2 async response framing