Skip to main content

rustsmb/
smb2.rs

1//! SMB2 protocol loop ([MS-SMB2]).
2//!
3//! Implements the full file-serving command surface on top of the shared
4//! [`Vfs`](smb_server_vfs::Vfs) abstraction: NEGOTIATE, SESSION_SETUP (both NTLMSSP
5//! legs), TREE_CONNECT/DISCONNECT, LOGOFF, CREATE, READ, WRITE, CLOSE, FLUSH,
6//! LOCK, QUERY_DIRECTORY, QUERY_INFO and SET_INFO. Frames whose magic is
7//! `\xFESMB` route here from [`crate::dispatch`].
8
9use std::collections::{HashMap, VecDeque};
10use std::sync::Arc;
11use std::sync::atomic::{AtomicU64, Ordering};
12
13use metrics::{counter, gauge};
14use tokio::sync::{mpsc, oneshot};
15
16use smb_server_proto::types::Status;
17use smb_server_proto_smb2::commands as c;
18use smb_server_proto_smb2::consts::{aead, hdr, hdr_flags};
19use smb_server_proto_smb2::info;
20use smb_server_proto_smb2::session_setup as ss;
21use smb_server_transport::Transport;
22
23use crate::state::ServerShared;
24
25/// MaxTransactSize advertised in our NEGOTIATE response (1 MiB); CHANGE_NOTIFY
26/// rejects an OutputBufferLength larger than this ([MS-SMB2] §3.3.5.19).
27const MAX_TRANSACT_SIZE: u32 = 1024 * 1024;
28
29/// How long a conflicting open waits for a lease-break acknowledgment before
30/// proceeding ([MS-SMB2] §3.3.1.4). Kept short under test to keep unit tests
31/// fast (no real client acks there).
32#[cfg(not(test))]
33const LEASE_ACK_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
34#[cfg(test)]
35const LEASE_ACK_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(50);
36
37/// Per-connection SMB2 state.
38pub struct Smb2Conn {
39    /// Negotiated dialect revision (set after successful NEGOTIATE).
40    pub dialect: Option<u16>,
41    /// Current session id (0 = none).
42    pub session_id: u64,
43    /// True once credentials were accepted.
44    pub authenticated: bool,
45    /// NTLM challenge used during authentication.
46    pub challenge: [u8; 8],
47    /// Authenticated principal (`nobody` for guests).
48    pub user: String,
49    /// True when mapped to guest.
50    pub guest: bool,
51    /// Exported NTLM session key; enables SMB2 signing for this session
52    /// ([MS-SMB2] §3.2.5.3).
53    pub session_key: Option<[u8; 16]>,
54    /// Derived signing key (dialect-specific); `None` disables signing.
55    pub signing_key: Option<[u8; 16]>,
56    /// Negotiated 3.1.1 signing algorithm ([MS-SMB2] §2.2.3.1.7): HMAC-SHA256,
57    /// AES-128-CMAC, or AES-128-GMAC. Unused for 2.x (always HMAC-SHA256).
58    pub signing_algo: u16,
59    /// Capabilities word advertised in our NEGOTIATE response — echoed
60    /// verbatim in FSCTL_VALIDATE_NEGOTIATE_INFO ([MS-SMB2] §3.3.5.15).
61    pub advertised_caps: u32,
62    /// Credits the client currently holds (granted minus spent,
63    /// [MS-SMB2] §3.3.1.1).
64    pub client_credits: u32,
65    /// Pre-authentication integrity hash (3.1.1 only).
66    pub preauth_hash: [u8; 64],
67    /// Chosen encryption cipher (from negotiate ENCRYPTION_CAPABILITIES).
68    pub cipher: Option<u16>,
69    /// (client-to-server, server-to-client) cipher keys; `None` until the
70    /// session enables encryption ([MS-SMB2] §3.1.4.1 labels). AES-128 uses the
71    /// first 16 bytes; AES-256 uses all 32.
72    pub enc_keys: Option<([u8; 32], [u8; 32])>,
73    /// True once every subsequent message for this session must be wrapped
74    /// in a transform header (SMB2_SESSION_FLAG_ENCRYPT_DATA).
75    pub encrypt_data: bool,
76    /// True once the peer has sent at least one encrypted (transform) frame,
77    /// so async replies must be sealed even when the server does not force
78    /// encryption on the session.
79    pub peer_encrypts: bool,
80    /// SPNEGO exchange blobs: (init request, authenticate request).
81    pub ntlm_blobs: Option<(Vec<u8>, Vec<u8>)>,
82    /// Challenge token sent during leg 1 (for the mechListMIC input).
83    pub ntlm_targ: Option<Vec<u8>>,
84    /// True when the client's SESSION_SETUP carries bare NTLMSSP messages with
85    /// no SPNEGO envelope (leg 1's blob starts with the NTLMSSP signature
86    /// directly, not a negTokenInit/negTokenResp tag); the response must then
87    /// echo bare NTLMSSP too, with no mechListMIC (an SPNEGO-only concept).
88    pub raw_ntlm: bool,
89    /// Raw challenge token sent during leg 1 (for mechListMIC).
90    /// Lease key each open handle joined, so a write/open from a co-holder of
91    /// the same lease does not break it ([MS-SMB2] §3.3.4.7).
92    pub lease_keys: HashMap<[u8; 16], [u8; 16]>,
93    /// Virtual named pipes opened on IPC$ (srvsvc, …) keyed by file id.
94    pub pipes: HashMap<[u8; 16], crate::srvsvc::Pipe>,
95    /// Directory-enumeration continuation queues keyed by directory FileId.
96    pub searches: HashMap<[u8; 16], VecDeque<info::FindEntry>>,
97    /// Per-open integrity state (ChecksumAlgorithm, Flags) set/queried by
98    /// FSCTL_SET/GET_INTEGRITY_INFORMATION ([MS-FSCC] §2.3.55/57), keyed by FileId.
99    pub integrity: HashMap<[u8; 16], (u16, u32)>,
100    /// Outbound frame queue drained by the per-connection writer task. Async
101    /// handlers and background tasks (CHANGE_NOTIFY, oplock/lease breaks) clone
102    /// this to emit unsolicited frames without blocking the reader.
103    pub outbound: mpsc::Sender<Vec<u8>>,
104    /// Next async id to hand out for a STATUS_PENDING operation
105    /// ([MS-SMB2] §3.3.4.2).
106    pub next_async_id: u64,
107    /// In-flight async operations keyed by async id; sending on the channel
108    /// completes the background task with the given status (CANCELLED for a
109    /// CANCEL, NOTIFY_CLEANUP when the watched handle is closed).
110    pub async_cancels: HashMap<u64, oneshot::Sender<Status>>,
111    /// Maps a pending operation's MessageId to its async id, so a CANCEL that
112    /// correlates by MessageId (rather than AsyncId) can find it ([MS-SMB2]
113    /// §3.2.4.24).
114    pub async_msgids: HashMap<u64, u64>,
115    /// Pending CHANGE_NOTIFY async ids per watched FileId, so closing the
116    /// handle can complete them with STATUS_NOTIFY_CLEANUP ([MS-SMB2]
117    /// §3.3.5.10).
118    pub async_by_file: HashMap<[u8; 16], Vec<u64>>,
119    /// Server-side-copy resume keys → source FileId ([MS-SMB2] §2.2.32.3);
120    /// handed out by FSCTL_SRV_REQUEST_RESUME_KEY and consumed by COPYCHUNK.
121    pub resume_keys: HashMap<[u8; 24], [u8; 16]>,
122    /// Offload (ODX) tokens → the source bytes they represent ([MS-FSCC]
123    /// §2.3.77/79); issued by FSCTL_OFFLOAD_READ and consumed by
124    /// FSCTL_OFFLOAD_WRITE, keyed by the 16-byte id embedded in the token.
125    pub offload_tokens: HashMap<[u8; 16], Vec<u8>>,
126    /// Durable handles granted on this connection, keyed by FileId, preserved
127    /// into the server table when the connection drops ([MS-SMB2] §3.3.1.10).
128    pub durable: HashMap<[u8; 16], crate::state::DurableEntry>,
129    /// Negotiated outbound compression algorithm ([MS-SMB2] §2.2.42), if the
130    /// client and server share one; `None` disables compressed responses.
131    pub compress_algo: Option<u16>,
132    /// Set when the peer advertised SMB2_COMPRESSION_CAPABILITIES_FLAG_CHAINED
133    /// ([MS-SMB2] §2.2.3.1.3): compressed responses use the chained transform.
134    pub compress_chained: bool,
135    /// Set when the current request is a READ carrying
136    /// SMB2_READFLAG_REQUEST_COMPRESSED ([MS-SMB2] §2.2.19): the response MUST
137    /// be compressed if that shrinks it, regardless of size heuristics.
138    pub compress_response: bool,
139    /// Client's NEGOTIATE parameters, retained to validate a later
140    /// FSCTL_VALIDATE_NEGOTIATE_INFO request ([MS-SMB2] §3.3.5.15.12).
141    pub client_guid: [u8; 16],
142    /// Client SecurityMode from NEGOTIATE (signing enabled/required bits).
143    pub client_security_mode: u16,
144    /// Client Capabilities flags from NEGOTIATE.
145    pub client_capabilities: u32,
146    /// Connection.SupportsNotifications ([MS-SMB2] §3.3.5.4): true once the
147    /// server has echoed GLOBAL_CAP_NOTIFICATIONS in the NEGOTIATE response
148    /// (dialect 3.1.1 and the client requested it too).
149    pub supports_notifications: bool,
150    /// Dialects the client offered in NEGOTIATE.
151    pub client_dialects: Vec<u16>,
152    /// Set to terminate the transport connection after the current frame
153    /// (e.g. a failed VALIDATE_NEGOTIATE_INFO downgrade check).
154    pub disconnect: bool,
155    /// FileId generated by the most recent CREATE in the current compound
156    /// chain, used to resolve the wildcard FileId (0xFF..FF) that a related
157    /// follow-up request carries ([MS-SMB2] §3.3.5.2.7.2). Reset per frame.
158    pub chain_fid: Option<[u8; 16]>,
159    /// Symbolic Link Error Response body built by the last CREATE that stopped
160    /// on a symlink ([MS-SMB2] §2.2.2.2.1), consumed when framing the error.
161    pub symlink_error: Option<Vec<u8>>,
162    /// TreeId a successful TREE_CONNECT installs into its response header
163    /// ([MS-SMB2] §3.3.5.7), set by the handler and consumed once when the
164    /// reply is framed.
165    pub resp_tree_id: Option<u32>,
166    /// Force-seal the response to the current frame regardless of session
167    /// encryption — set when TREE_CONNECT resolves to an encrypted share so the
168    /// tree-connect reply itself is sealed ([MS-SMB2] §3.3.5.7). Reset per frame.
169    pub seal_current: bool,
170    /// True when the current request arrived inside a transform (encrypted).
171    /// Its AEAD tag is the integrity check, so the inner SMB2 signature is not
172    /// verified ([MS-SMB2] §3.3.5.2.4). Reset per frame.
173    pub req_encrypted: bool,
174    /// True while handling a channel-binding SESSION_SETUP ([MS-SMB2]
175    /// §3.3.5.5.3): the connection derives its own Channel.SigningKey but MUST
176    /// NOT overwrite the shared session's stored crypto material.
177    pub binding: bool,
178    /// Per-session shared state (Session.TreeConnectTable + Session.OpenTable)
179    /// this channel is bound to ([MS-SMB2] §3.3.5.5.3); `None` until a session
180    /// is established. Channels bound to one session share the same scope.
181    pub scope: Option<crate::session_scope::ScopeRef>,
182}
183
184impl Smb2Conn {
185    /// Create SMB2 connection state with the given NTLM challenge and the
186    /// outbound queue its writer task drains.
187    pub fn new(challenge: [u8; 8], outbound: mpsc::Sender<Vec<u8>>) -> Self {
188        Smb2Conn {
189            dialect: None,
190            session_id: 0,
191            authenticated: false,
192            challenge,
193            user: String::new(),
194            guest: false,
195            session_key: None,
196            signing_key: None,
197            signing_algo: smb_server_proto_smb2::negotiate::ctx_type::SIGNING_AES128_CMAC,
198            advertised_caps: 0,
199            client_credits: 0,
200            preauth_hash: [0u8; 64],
201            cipher: None,
202            enc_keys: None,
203            encrypt_data: false,
204            peer_encrypts: false,
205            ntlm_blobs: None,
206            ntlm_targ: None,
207            raw_ntlm: false,
208            lease_keys: HashMap::new(),
209            pipes: HashMap::new(),
210            searches: HashMap::new(),
211            integrity: HashMap::new(),
212            outbound,
213            next_async_id: 1,
214            async_cancels: HashMap::new(),
215            async_msgids: HashMap::new(),
216            async_by_file: HashMap::new(),
217            resume_keys: HashMap::new(),
218            offload_tokens: HashMap::new(),
219            durable: HashMap::new(),
220            compress_algo: None,
221            compress_chained: false,
222            compress_response: false,
223            client_guid: [0u8; 16],
224            client_security_mode: 0,
225            client_capabilities: 0,
226            supports_notifications: false,
227            client_dialects: Vec::new(),
228            disconnect: false,
229            chain_fid: None,
230            symlink_error: None,
231            resp_tree_id: None,
232            seal_current: false,
233            req_encrypted: false,
234            binding: false,
235            scope: Some(crate::session_scope::detached()),
236        }
237    }
238
239    /// Clone the outbound frame sender for a background/async task.
240    pub fn outbound(&self) -> mpsc::Sender<Vec<u8>> {
241        self.outbound.clone()
242    }
243
244    /// Share name bound to `tid` in the session's tree table, if any.
245    pub(crate) fn tree_name(&self, tid: u32) -> Option<String> {
246        self.scope.as_ref()?.borrow().trees.get(&tid).cloned()
247    }
248
249    /// True when `tid` names a tree in the session's tree table.
250    pub(crate) fn tree_exists(&self, tid: u32) -> bool {
251        self.scope
252            .as_ref()
253            .is_some_and(|s| s.borrow().trees.contains_key(&tid))
254    }
255
256    /// Bind `tid` to `name` in the session's tree table.
257    pub(crate) fn tree_insert(&self, tid: u32, name: String) {
258        if let Some(s) = self.scope.as_ref() {
259            s.borrow_mut().trees.insert(tid, name);
260        }
261    }
262
263    /// Remove `tid` from the session's tree table.
264    pub(crate) fn tree_remove(&self, tid: u32) {
265        if let Some(s) = self.scope.as_ref() {
266            s.borrow_mut().trees.remove(&tid);
267        }
268    }
269
270    /// True when `fid` names an open handle in the session's open table.
271    pub(crate) fn handle_exists(&self, fid: &[u8; 16]) -> bool {
272        self.scope
273            .as_ref()
274            .is_some_and(|s| s.borrow().handles.contains_key(fid))
275    }
276
277    /// Insert an open handle into the session's open table.
278    pub(crate) fn handle_insert(&self, fid: [u8; 16], open: Box<smb_server_vfs::OpenFile>) {
279        if let Some(s) = self.scope.as_ref() {
280            s.borrow_mut().handles.insert(fid, open);
281        }
282    }
283
284    /// Remove and return an open handle from the session's open table. Used both
285    /// to close a handle and to check one out for an async VFS call (reinsert
286    /// with [`Self::handle_insert`] afterwards) so no `RefCell` borrow is ever
287    /// held across an `.await`.
288    pub(crate) fn handle_take(&self, fid: &[u8; 16]) -> Option<Box<smb_server_vfs::OpenFile>> {
289        self.scope.as_ref()?.borrow_mut().handles.remove(fid)
290    }
291
292    /// Run `f` against a shared handle reference (no `.await` inside).
293    pub(crate) fn with_handle<R>(
294        &self,
295        fid: &[u8; 16],
296        f: impl FnOnce(&smb_server_vfs::OpenFile) -> R,
297    ) -> Option<R> {
298        Some(f(self.scope.as_ref()?.borrow().handles.get(fid)?))
299    }
300
301    /// Allocate a fresh per-connection async id for a STATUS_PENDING op.
302    pub fn alloc_async_id(&mut self) -> u64 {
303        let id = self.next_async_id;
304        self.next_async_id += 1;
305        id
306    }
307}
308
309fn next_session_id() -> u64 {
310    static S: AtomicU64 = AtomicU64::new(1);
311    S.fetch_add(1, Ordering::Relaxed)
312}
313
314/// Allocate a 16-byte SMB2 FileId (counter in the first quadword).
315#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB2 wire serialization
316fn next_file_id() -> [u8; 16] {
317    static F: AtomicU64 = AtomicU64::new(0x2000);
318    let mut fid = [0u8; 16];
319    fid[..8].copy_from_slice(&F.fetch_add(1, Ordering::Relaxed).to_le_bytes());
320    fid
321}
322
323/// Allocate a fresh TreeId.
324#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB2 wire serialization
325pub(crate) fn next_tree_id() -> u32 {
326    static T: AtomicU64 = AtomicU64::new(1);
327    (T.fetch_add(1, Ordering::Relaxed) & 0x7fff_ffff) as u32
328}
329
330/// Serve an SMB2 client connection until EOF/error (used when a connection
331/// starts life directly in SMB2 rather than upgrading through SMB1).
332#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB2 wire serialization
333pub async fn serve_client(
334    server: Arc<ServerShared>,
335    transport: Box<dyn Transport>,
336    mut pending: Option<Vec<u8>>,
337) {
338    let (mut reader, writer) = transport.split();
339    let (out_tx, out_rx) = mpsc::channel::<Vec<u8>>(64);
340    let writer_task = tokio_uring::spawn(crate::dispatch::writer_loop(writer, out_rx));
341
342    {
343        let mut conn = Smb2Conn::new(crate::dispatch::rand_challenge_pub(), out_tx.clone());
344        loop {
345            let frame = if let Some(f) = pending.take() {
346                f
347            } else {
348                match reader.recv().await {
349                    Ok(Some(f)) => f.0,
350                    _ => break,
351                }
352            };
353            if frame.len() < 64 || frame[0..4] != smb_server_proto_smb2::SMB2_MAGIC {
354                continue;
355            }
356            if let Some(resp) = process_frame(&server, &mut conn, &frame).await
357                && out_tx.send(resp).await.is_err() {
358                    break;
359                }
360            if conn.disconnect {
361                break;
362            }
363        }
364    }
365    drop(out_tx);
366    let _ = writer_task.await;
367}
368
369/// Respond to an SMB1 multi-protocol NEGOTIATE that carries the `\xFESMB`
370/// dialect marker: builds the SMB2 negotiate response so the client upgrades.
371#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB2 wire serialization
372pub fn handle_multiprotocol_negotiate(buf: &[u8], guid: &[u8; 16]) -> Option<Vec<u8>> {
373    let (hdr, _wc_off) = smb_server_proto_smb1::header::parse_header(buf)?;
374    let _ = hdr;
375    // The multi-protocol NEGOTIATE response is MessageId 0 ([MS-SMB2]
376    // §3.2.5.2). Offer the wildcard revision when the client listed
377    // "SMB 2.???" so it re-negotiates a concrete dialect (up to 3.1.1);
378    // otherwise answer the "SMB 2.002"-only case directly.
379    let dialect = if buf.windows(5).any(|w| w == b"2.???") {
380        smb_server_proto_smb2::negotiate::DIALECT_WILDCARD
381    } else {
382        smb_server_proto_smb2::negotiate::DIALECT_202
383    };
384    let h2 = smb_server_proto_smb2::Header2 {
385        credit_charge: 0,
386        status: 0,
387        command: 0,
388        credits: 1,
389        flags: 1,
390        next_command: 0,
391        message_id: 0,
392        tree_id: 0,
393        session_id: 0,
394        signature: [0u8; 16],
395    };
396    Some(response(
397        &h2,
398        Status::SUCCESS,
399        smb_server_proto_smb2::negotiate::build_response(
400            dialect,
401            guid,
402            smb_server_proto::types::FileTime::now().0,
403        ),
404        0,
405    ))
406}
407
408/// Execute one frame (possibly compound) and build its response (`None` to
409/// stay silent). Chained requests ([MS-SMB2] §3.3.5.2) are dispatched in
410/// order and their replies concatenated with 8-byte alignment.
411#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB2 wire serialization
412pub(crate) async fn process_frame(
413    server: &Arc<ServerShared>,
414    conn: &mut Smb2Conn,
415    buf: &[u8],
416) -> Option<Vec<u8>> {
417    // Incoming transform frames are decrypted first ([MS-SMB2] §3.3.5.16).
418    // A failed open means tampering or key desync — drop the connection.
419    let plaintext: Vec<u8>;
420    let mut buf = buf;
421    let mut request_encrypted = false;
422    if buf.len() >= 4 && buf[..4] == smb_server_proto_smb2::commands::TF_MAGIC {
423        let pt = decrypt_transform(conn, buf)?;
424        // A decrypted payload may itself be a compression transform
425        // ([MS-SMB2] §3.1.4.4: the client compresses, then encrypts); expand it.
426        plaintext = if pt.len() >= 4 && pt[..4] == smb_server_proto_smb2::compress::PROTOCOL_ID {
427            smb_server_proto_smb2::compress::decompress_message(&pt)?
428        } else {
429            pt
430        };
431        buf = &plaintext;
432        request_encrypted = true;
433        conn.peer_encrypts = true;
434    }
435    conn.seal_current = false;
436    conn.req_encrypted = request_encrypted;
437
438    let mut parts: Vec<(Vec<u8>, bool)> = Vec::new(); // (resp, may_wrap)
439    let mut related_flags: Vec<bool> = Vec::new(); // request carried FLAGS_RELATED_OPERATIONS
440    // Work on an owned copy so a related follow-up request's wildcard FileId
441    // can be patched in place with the chain's last-created handle.
442    let mut work: Vec<u8> = buf.to_vec();
443    conn.chain_fid = None;
444    let mut off = 0usize;
445    loop {
446        if off + hdr::LEN > work.len() {
447            break;
448        }
449        // A related request ([MS-SMB2] §3.3.5.2.7.2) that carries the wildcard
450        // FileId {0xFF..} refers to the FileId produced by the previous CREATE
451        // in this chain — substitute it before the handler parses the body.
452        let flags = g32(&work, off + hdr::FLAGS);
453        if flags & hdr_flags::RELATED_OPERATIONS != 0
454            && let Some(fid) = conn.chain_fid {
455                let cmd = g16(&work, off + hdr::COMMAND);
456                if let Some(foff) = file_id_body_offset(cmd) {
457                    let abs = off + hdr::LEN + foff;
458                    if abs + c::FileId::LEN <= work.len()
459                        && work[abs..abs + c::FileId::LEN] == c::FileId::WILDCARD
460                    {
461                        work[abs..abs + c::FileId::LEN].copy_from_slice(&fid);
462                    }
463                }
464            }
465        let rest = &work[off..];
466        #[cfg_attr(not(feature = "lib"), allow(unused_variables))]
467        let (single, may_wrap) = process_single(server, conn, rest).await?;
468        // Refund the Credits this response grants back to the client's
469        // balance ([MS-SMB2] §3.3.1.1) — the field was stamped by
470        // response() as max(CreditCharge, 1).
471        let granted = g16(&single, hdr::CREDIT) as u32;
472        conn.client_credits = conn.client_credits.saturating_add(granted);
473        #[cfg(not(feature = "lib"))]
474        let may_wrap = false; // sealing unsupported on this backend
475        parts.push((single, may_wrap));
476        related_flags.push(flags & hdr_flags::RELATED_OPERATIONS != 0);
477
478        let next = g32(&work, off + hdr::NEXT_COMMAND) as usize;
479        if next == 0 || next >= work.len() - off {
480            break;
481        }
482        off += next;
483    }
484    if parts.is_empty() {
485        return None;
486    }
487
488    // Assemble compound reply: every frame except the last carries
489    // NextCommand pointing at the following frame, 8-byte aligned.
490    let mut out: Vec<u8> =
491        Vec::with_capacity(parts.iter().map(|p| p.0.len() + (hdr::ALIGN - 1)).sum());
492    let mut starts = Vec::with_capacity(parts.len());
493    for (p, _) in &parts {
494        while !out.len().is_multiple_of(hdr::ALIGN) {
495            out.push(0);
496        }
497        starts.push(out.len());
498        out.extend_from_slice(p);
499    }
500    for w in 0..starts.len().saturating_sub(1) {
501        let next = (starts[w + 1] - starts[w]) as u32;
502        let bytes = next.to_le_bytes();
503        let pos = starts[w] + hdr::NEXT_COMMAND;
504        out[pos..pos + bytes.len()].copy_from_slice(&bytes);
505    }
506    // Echo SMB2_FLAGS_RELATED_OPERATIONS on every chained response whose
507    // request carried it, for all responses after the first ([MS-SMB2]
508    // §3.3.4.1: the flag marks a response as part of a compounded chain).
509    for w in 1..starts.len() {
510        if related_flags[w] {
511            out[starts[w] + hdr::FLAGS] |= hdr_flags::RELATED_OPERATIONS as u8;
512        }
513    }
514
515    // Seal the whole reply when the session requires encryption, or when the
516    // request itself arrived encrypted ([MS-SMB2] §3.3.5.16 — a sealed request
517    // is answered with a sealed response even if the server does not force it).
518    // The enabling SESSION_SETUP response itself always travels in the clear.
519    let first_tid = if work.len() >= hdr::LEN {
520        g32(&work, hdr::TREE_ID)
521    } else {
522        0
523    };
524    let tree_requires_seal = conn.seal_current
525        || conn
526            .tree_name(first_tid)
527            .and_then(|n| server.shares.get(&n))
528            .is_some_and(|s| s.encrypt);
529    if (conn.encrypt_data || request_encrypted || tree_requires_seal)
530        && parts.iter().all(|(_, w)| *w)
531    {
532        // The AEAD transform provides integrity, so each inner PDU travels
533        // unsigned with a zero Signature field ([MS-SMB2] §3.3.4.1.1).
534        for &s in &starts {
535            out[s + hdr::FLAGS] &= !(hdr_flags::SIGNED as u8);
536            out[s + hdr::SIGNATURE..s + hdr::LEN].fill(0);
537        }
538        // Compress before sealing when the peer negotiated compression and the
539        // READ asked for it ([MS-SMB2] §3.1.4.4: compress, then encrypt).
540        let mut payload = out;
541        if let Some(algo) = conn.compress_algo
542            && conn.compress_response {
543                let packed = if conn.compress_chained {
544                    smb_server_proto_smb2::compress::compress_message_chained(&payload, algo)
545                } else {
546                    smb_server_proto_smb2::compress::compress_message(&payload, algo)
547                };
548                if let Some(packed) = packed {
549                    payload = packed;
550                }
551            }
552        let sealed = encrypt_response(conn, &payload)?;
553        return Some(sealed);
554    }
555    Some(out)
556}
557
558/// Body-relative offset of the 16-byte FileId within a request that carries
559/// one, used to resolve the wildcard FileId in a related compound follow-up
560/// ([MS-SMB2] §3.3.5.2.7.2). `None` for commands with no FileId field.
561#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB2 wire serialization
562fn file_id_body_offset(command: u16) -> Option<usize> {
563    use smb_server_proto_smb2::session_setup::cmd as c;
564    match command {
565        c::CLOSE | c::FLUSH | c::LOCK | c::IOCTL | c::QUERY_DIRECTORY | c::CHANGE_NOTIFY => Some(8),
566        c::READ | c::WRITE | c::SET_INFO => Some(16),
567        c::QUERY_INFO => Some(24),
568        _ => None,
569    }
570}
571
572/// True for the GCM cipher variants ([MS-SMB2] §2.2.3.1.2).
573#[cfg(not(feature = "handrolled"))]
574fn cipher_is_gcm(cipher: u16) -> bool {
575    use smb_server_proto_smb2::negotiate::ctx_type::{AES128_GCM, AES256_GCM};
576    matches!(cipher, AES128_GCM | AES256_GCM)
577}
578
579/// True for the AES-256 cipher variants (32-byte keys).
580fn cipher_is_256(cipher: u16) -> bool {
581    use smb_server_proto_smb2::negotiate::ctx_type::{AES256_CCM, AES256_GCM};
582    matches!(cipher, AES256_GCM | AES256_CCM)
583}
584
585/// Key length in bytes for a negotiated cipher.
586fn cipher_key_len(cipher: u16) -> usize {
587    if cipher_is_256(cipher) { aead::AES256_KEY_LEN } else { aead::AES128_KEY_LEN }
588}
589
590/// Wrap an assembled response into a transform frame ([MS-SMB2] §2.2.41)
591/// using the S2C cipher key.
592#[cfg(not(feature = "lib"))]
593fn encrypt_response(conn: &Smb2Conn, msg: &[u8]) -> Option<Vec<u8>> {
594    let _ = (conn, msg);
595    None
596}
597#[cfg(not(feature = "lib"))]
598fn seal_pdu(
599    session_id: u64,
600    enc_keys: ([u8; 32], [u8; 32]),
601    cipher: u16,
602    msg: &[u8],
603) -> Option<Vec<u8>> {
604    let _ = (session_id, enc_keys, cipher, msg);
605    None
606}
607#[cfg(not(feature = "handrolled"))]
608fn encrypt_response(conn: &Smb2Conn, msg: &[u8]) -> Option<Vec<u8>> {
609    seal_pdu(conn.session_id, conn.enc_keys?, conn.cipher?, msg)
610}
611/// Seal `msg` into a transform frame with explicit key material so both the
612/// request path and background async tasks can encrypt without borrowing the
613/// whole connection.
614#[cfg(not(feature = "handrolled"))]
615fn seal_pdu(
616    session_id: u64,
617    enc_keys: ([u8; 32], [u8; 32]),
618    cipher: u16,
619    msg: &[u8],
620) -> Option<Vec<u8>> {
621    let (_c2s, s2c) = enc_keys;
622    let gcm = cipher_is_gcm(cipher);
623    let iv_size = if gcm {
624        aead::GCM_NONCE_LEN
625    } else {
626        aead::CCM_NONCE_LEN
627    };
628
629    let mut nonce_field = [0u8; 16];
630    nonce_field[..iv_size].copy_from_slice(&crate::dispatch::rand_bytes(iv_size));
631
632    let mut tf = smb_server_proto_smb2::commands::build_transform(session_id, &nonce_field, msg.len());
633    // AAD region: everything after the Nonce field ([MS-SMB2] §3.1.4.2).
634    let aad = &tf[smb_server_proto_smb2::commands::tf_off::NONCE..];
635    let sealed = match (gcm, cipher_is_256(cipher)) {
636        (true, true) => smb_server_auth::crypto::aes256gcm_seal(
637            &s2c,
638            nonce_field[..aead::GCM_NONCE_LEN].try_into().ok()?,
639            aad,
640            msg,
641        ),
642        (true, false) => smb_server_auth::crypto::aes128gcm_seal(
643            s2c[..aead::AES128_KEY_LEN].try_into().ok()?,
644            nonce_field[..aead::GCM_NONCE_LEN].try_into().ok()?,
645            aad,
646            msg,
647        ),
648        (false, true) => smb_server_auth::crypto::aes256ccm_seal(
649            &s2c,
650            nonce_field[..aead::CCM_NONCE_LEN].try_into().ok()?,
651            aad,
652            msg,
653        ),
654        (false, false) => smb_server_auth::crypto::aes128ccm_seal(
655            s2c[..aead::AES128_KEY_LEN].try_into().ok()?,
656            nonce_field[..aead::CCM_NONCE_LEN].try_into().ok()?,
657            aad,
658            msg,
659        ),
660    };
661    // Tag lands in the Signature field; ciphertext follows the header.
662    let tag_at = smb_server_proto_smb2::commands::tf_off::SIGNATURE;
663    tf[tag_at..tag_at + aead::TAG_LEN].copy_from_slice(&sealed[sealed.len() - aead::TAG_LEN..]);
664    let mut frame = tf;
665    frame.extend_from_slice(&sealed[..sealed.len() - aead::TAG_LEN]);
666    counter!("smb_encrypted_responses_total").increment(1);
667    counter!("smb_encrypted_bytes_total").increment(msg.len() as u64);
668    Some(frame)
669}
670
671/// Open an incoming transform frame using the C2S cipher key.
672#[cfg(not(feature = "lib"))]
673fn decrypt_transform(_conn: &mut Smb2Conn, _frame: &[u8]) -> Option<Vec<u8>> {
674    None
675}
676#[cfg(not(feature = "handrolled"))]
677fn decrypt_transform(conn: &mut Smb2Conn, frame: &[u8]) -> Option<Vec<u8>> {
678    let tf = match smb_server_proto_smb2::commands::TransformHdr::parse(frame) {
679        Some(t) => t,
680        None => {
681            tracing::warn!("transform parse failed");
682            return None;
683        }
684    };
685    if tf.session_id != conn.session_id {
686        tracing::warn!(
687            sid = format!("{:#x}", tf.session_id),
688            "transform sid mismatch"
689        );
690        return None;
691    }
692    let Some((c2s, _s2c)) = conn.enc_keys else {
693        tracing::warn!("no cipher keys derived");
694        return None;
695    };
696    let Some(cipher) = conn.cipher else {
697        tracing::warn!("no cipher negotiated");
698        return None;
699    };
700    let gcm = cipher_is_gcm(cipher);
701    let is256 = cipher_is_256(cipher);
702    let aad = frame
703        .get(smb_server_proto_smb2::commands::tf_off::NONCE..smb_server_proto_smb2::commands::tf_off::HDR_SIZE)?;
704    let payload =
705        frame.get(smb_server_proto_smb2::commands::tf_off::HDR_SIZE..tf_off_end(tf.original_len))?;
706    // The GCM/CCM tag travels in the TF Signature field ([MS-SMB2]
707    // §2.2.41), detached from the ciphertext; re-attach it for the AEAD
708    // open, which expects ct||tag.
709    let tag_at = smb_server_proto_smb2::commands::tf_off::SIGNATURE;
710    let mut sealed = payload.to_vec();
711    sealed.extend_from_slice(frame.get(tag_at..tag_at + aead::TAG_LEN)?);
712    let nonce = &frame[smb_server_proto_smb2::commands::tf_off::NONCE..];
713    let opened = match (gcm, is256) {
714        (true, true) => smb_server_auth::crypto::aes256gcm_open(
715            &c2s,
716            nonce.get(..aead::GCM_NONCE_LEN)?.try_into().ok()?,
717            aad,
718            &sealed,
719        ),
720        (true, false) => smb_server_auth::crypto::aes128gcm_open(
721            c2s[..aead::AES128_KEY_LEN].try_into().ok()?,
722            nonce.get(..aead::GCM_NONCE_LEN)?.try_into().ok()?,
723            aad,
724            &sealed,
725        ),
726        (false, true) => smb_server_auth::crypto::aes256ccm_open(
727            &c2s,
728            nonce.get(..aead::CCM_NONCE_LEN)?.try_into().ok()?,
729            aad,
730            &sealed,
731        ),
732        (false, false) => smb_server_auth::crypto::aes128ccm_open(
733            c2s[..aead::AES128_KEY_LEN].try_into().ok()?,
734            nonce.get(..aead::CCM_NONCE_LEN)?.try_into().ok()?,
735            aad,
736            &sealed,
737        ),
738    };
739    let pt = match opened {
740        Some(p) => p,
741        None => {
742            tracing::warn!(cipher = cipher, aad_len = aad.len(), "aead open failed");
743            return None;
744        }
745    };
746    counter!("smb_decrypted_msgs_total").increment(1);
747    counter!("smb_decrypted_bytes_total").increment(pt.len() as u64);
748    Some(pt)
749}
750
751/// End offset (relative to frame start) of transform-wrapped payload.
752#[cfg(not(feature = "handrolled"))]
753fn tf_off_end(original_len: usize) -> usize {
754    smb_server_proto_smb2::commands::tf_off::HDR_SIZE + original_len
755}
756
757/// Route a request through the typestate pipeline (io::dispatch) and return the
758/// `(status, body)` the legacy framing still wraps. Used by commands migrated
759/// onto `io::Command` (docs/typestate_plan.md Phases 2–3).
760/// How a typestate dispatch resolves for `process_single`: a body that still
761/// flows the common signing/sealing tail, a pre-framed async PDU to send
762/// verbatim (the STATUS_PENDING interim), or nothing at all.
763enum Routed {
764    Framed(Status, Vec<u8>),
765    Raw(Vec<u8>),
766    Silent,
767}
768
769async fn via_typestate(
770    hdr: &smb_server_proto_smb2::Header2,
771    conn: &mut Smb2Conn,
772    server: &Arc<ServerShared>,
773    buf: &[u8],
774) -> Routed {
775    let reply = crate::io::ReplyHeader {
776        command: hdr.command,
777        message_id: hdr.message_id,
778        session_id: conn.session_id,
779        tree_id: hdr.tree_id,
780        credits: 0,
781        seal: crate::io::SealIntent::default(),
782    };
783    let req = match crate::io::SmbRequest::parse(hdr.command, buf) {
784        Ok(req) => req,
785        Err(status) => return Routed::Framed(status, Vec::new()),
786    };
787    let ctx = crate::io::IoContext::accept(reply, req);
788    let outcome = {
789        let mut res = crate::io::Resources {
790            conn,
791            server,
792            frame: buf,
793        };
794        crate::io::dispatch(ctx, &mut res).await
795    };
796    match outcome {
797        crate::io::Outcome::Final(r) => Routed::Framed(r.status, r.body),
798        // Silent: the handler asked for no reply (e.g. a CANCEL, or a
799        // connection termination), so nothing is sent.
800        crate::io::Outcome::Silent => Routed::Silent,
801        // Interim: frame the STATUS_PENDING async reply now ([MS-SMB2]
802        // §3.3.4.2); the parked async worker sends the final reply later on
803        // the outbound queue.
804        crate::io::Outcome::Interim { parked, interim } => {
805            let mut frame = build_async_frame(
806                interim.reply.session_id,
807                interim.reply.message_id,
808                parked.async_id(),
809                interim.reply.command,
810                interim.status,
811                &interim.body,
812            );
813            if hdr.is_signed()
814                && let Some(key) = conn.signing_key.or(conn.session_key) {
815                    sign_pdu(&mut frame, &key, conn.dialect, conn.signing_algo);
816                }
817            Routed::Raw(frame)
818        }
819    }
820}
821
822/// Process exactly one SMB2 request starting at `buf` (its own header).
823#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB2 wire serialization
824async fn process_single(
825    server: &Arc<ServerShared>,
826    conn: &mut Smb2Conn,
827    buf: &[u8],
828) -> Option<(Vec<u8>, bool)> {
829    let hdr = match smb_server_proto_smb2::Header2::parse(buf) {
830        Some(h) => h,
831        None => return None,
832    };
833    tracing::debug!(
834        cmd = hdr.command,
835        mid = hdr.message_id,
836        sid = format!("{:#x}", hdr.session_id),
837        tid = format!("{:#x}", hdr.tree_id),
838        len = buf.len(),
839        "smb2 request"
840    );
841    // SMB2_READFLAG_REQUEST_COMPRESSED (0x02) in a READ Flags byte (body off 3)
842    // asks the server to compress the read response ([MS-SMB2] §2.2.19/§3.3.5.12).
843    conn.compress_response = hdr.command == ss::cmd::READ
844        && buf
845            .get(hdr::LEN + 3)
846            .map(|f| f & 0x02 != 0)
847            .unwrap_or(false);
848    // Request half of the 3.1.1 pre-auth integrity hash ([MS-SMB2] §3.3.4.1.1):
849    // every NEGOTIATE / SESSION_SETUP message updates it before processing.
850    let is_preauth_msg = matches!(hdr.command, ss::cmd::NEGOTIATE | ss::cmd::SESSION_SETUP);
851    let preauth_before = conn.preauth_hash;
852    if is_preauth_msg {
853        let mut joined = Vec::with_capacity(hdr::LEN + buf.len());
854        joined.extend_from_slice(&conn.preauth_hash);
855        joined.extend_from_slice(buf);
856        conn.preauth_hash = smb_server_auth::crypto::sha512(&joined);
857    }
858    let sid = hdr.session_id;
859
860    // Commands valid without an authenticated session.
861    let pre_session = matches!(
862        hdr.command,
863        ss::cmd::NEGOTIATE | ss::cmd::SESSION_SETUP | ss::cmd::ECHO
864    );
865    if !pre_session && (!conn.authenticated || sid != conn.session_id) {
866        return Some((response(&hdr, Status::ACCESS_DENIED, Vec::new(), 0), false));
867    }
868
869    // A tree-scoped command must carry a TreeId that names a live tree connect
870    // ([MS-SMB2] §3.3.5.2.11); a stale/unknown one is STATUS_NETWORK_NAME_DELETED.
871    // Related compound follow-ups inherit the chain's tree, so exempt them.
872    let needs_tree = matches!(
873        hdr.command,
874        ss::cmd::CREATE
875            | ss::cmd::CLOSE
876            | ss::cmd::FLUSH
877            | ss::cmd::READ
878            | ss::cmd::WRITE
879            | ss::cmd::LOCK
880            | ss::cmd::IOCTL
881            | ss::cmd::QUERY_DIRECTORY
882            | ss::cmd::CHANGE_NOTIFY
883            | ss::cmd::QUERY_INFO
884            | ss::cmd::SET_INFO
885    );
886    if needs_tree
887        && hdr.flags & hdr_flags::RELATED_OPERATIONS == 0
888        && !conn.tree_exists(hdr.tree_id)
889    {
890        return Some((
891            response(
892                &hdr,
893                Status::NETWORK_NAME_DELETED,
894                Vec::new(),
895                conn.session_id,
896            ),
897            true,
898        ));
899    }
900
901    // Channel-sequence replay verification ([MS-SMB2] §3.3.5.2.10): a WRITE,
902    // SET_INFO, or IOCTL whose header ChannelSequence has drifted more than
903    // 0x7FFF from the open's fails STATUS_FILE_NOT_AVAILABLE.
904    if matches!(hdr.command, ss::cmd::WRITE | ss::cmd::SET_INFO | ss::cmd::IOCTL)
905        && matches!(conn.dialect, Some(d) if d >= smb_server_proto_smb2::negotiate::DIALECT_300)
906        && let Some(foff) = file_id_body_offset(hdr.command) {
907            let abs = hdr::LEN + foff;
908            if let Some(fid) = buf
909                .get(abs..abs + 16)
910                .and_then(|s| <[u8; 16]>::try_from(s).ok())
911            {
912                let channel_seq = u16::from_le_bytes([buf[hdr::STATUS], buf[hdr::STATUS + 1]]);
913                let is_replay = hdr.flags & hdr_flags::REPLAY_OPERATION != 0;
914                if !verify_channel_sequence(conn, &fid, channel_seq, is_replay) {
915                    return Some((
916                        response(&hdr, Status::FILE_NOT_AVAILABLE, Vec::new(), conn.session_id),
917                        true,
918                    ));
919                }
920            }
921        }
922
923    // A handle force-closed by an application-instance failover ([MS-SMB2]
924    // §3.3.5.9.13): the owner's next operation on it fails STATUS_FILE_CLOSED.
925    if let Some(foff) = file_id_body_offset(hdr.command) {
926        let abs = hdr::LEN + foff;
927        if let Some(fid) = buf
928            .get(abs..abs + 16)
929            .and_then(|s| <[u8; 16]>::try_from(s).ok())
930            && server.app_instances.take_forced((conn.session_id, fid)) {
931                conn.handle_take(&fid);
932                return Some((
933                    response(&hdr, Status::FILE_CLOSED, Vec::new(), conn.session_id),
934                    true,
935                ));
936            }
937    }
938
939    // Credit accounting ([MS-SMB2] §3.3.1.1): every request spends its
940    // CreditCharge from the balance we have granted; the response's own
941    // Credits field tops the balance back up (done by the caller reading
942    // it out of the response header). An over-spend is a broken or hostile
943    // client — reject it.
944    // The initial NEGOTIATE arrives with an implicit credit already granted
945    // ([MS-SMB2] §3.3.5.2.4) — some clients set CreditCharge=1 on it.
946    if hdr.command == ss::cmd::NEGOTIATE {
947        conn.client_credits = conn.client_credits.max(1);
948    }
949    // Lenient, smbd-style policy: track the spend but never fail a request
950    // whose CreditCharge exceeds the tracked balance — some clients
951    // (impacket) set large charges without prior grant negotiation. The
952    // response refunds max(CreditCharge, 1) via the caller.
953    let charge = hdr.credit_charge as u32;
954    conn.client_credits = conn.client_credits.saturating_sub(charge);
955
956    // Signing policy ([MS-SMB2] §3.3.5.2.3). Two independent rules:
957    //  1. A request that carries a signature MUST verify against the
958    //     session's signing key, regardless of the server signing policy —
959    //     a bad signature is a tamper/forgery attempt. This holds for every
960    //     dialect: the 3.1.1 signing key binds to the pre-auth integrity hash,
961    //     which we now reproduce bit-for-bit (canonical negotiate-context
962    //     framing), so honest 3.1.1 traffic verifies and a forged signature
963    //     is rejected.
964    //  2. Unsigned traffic from real accounts is rejected only when
965    //     --require-signing is set. Sealed (encrypted) sessions carry their
966    //     own AEAD integrity guarantee, so signatures are not required there.
967    if hdr.command != ss::cmd::NEGOTIATE
968        && hdr.command != ss::cmd::SESSION_SETUP
969        && !conn.guest
970        && !conn.encrypt_data
971        && !conn.req_encrypted
972        && let Some(key) = conn.signing_key
973    {
974        if hdr.is_signed() {
975            if !verify_pdu_signature(buf, &key, conn.dialect, conn.signing_algo) {
976                counter!("smb_reject_bad_signature_total").increment(1);
977                return Some((
978                    response(&hdr, Status::ACCESS_DENIED, Vec::new(), conn.session_id),
979                    true,
980                ));
981            }
982        } else if server.require_signing {
983            counter!("smb_reject_unsigned_total").increment(1);
984            return Some((
985                response(&hdr, Status::ACCESS_DENIED, Vec::new(), conn.session_id),
986                true,
987            ));
988        }
989    }
990
991    // Encrypted-share enforcement ([MS-SMB2] §3.3.5.2.9): once a tree that
992    // requires encryption is connected, every request on it (other than the
993    // negotiate/session/tree-connect handshake) MUST be encrypted.
994    if !conn.req_encrypted
995        && hdr.command != ss::cmd::NEGOTIATE
996        && hdr.command != ss::cmd::SESSION_SETUP
997        && hdr.command != ss::cmd::TREE_CONNECT
998        && conn
999            .tree_name(hdr.tree_id)
1000            .and_then(|n| server.shares.get(&n))
1001            .map(|s| s.encrypt)
1002            .unwrap_or(false)
1003    {
1004        return Some((
1005            response(&hdr, Status::ACCESS_DENIED, Vec::new(), conn.session_id),
1006            true,
1007        ));
1008    }
1009
1010    // TREE_CONNECT installs a fresh TreeId into its reply via conn.resp_tree_id.
1011
1012    // Route a command through the typestate pipeline: a framed body flows the
1013    // common signing/sealing tail below, a pre-framed async interim is sent
1014    // verbatim, and a silent outcome sends nothing.
1015    macro_rules! route {
1016        () => {
1017            match via_typestate(&hdr, conn, server, buf).await {
1018                Routed::Framed(s, b) => (s, b),
1019                Routed::Raw(frame) => return Some((frame, true)),
1020                Routed::Silent => return None,
1021            }
1022        };
1023    }
1024
1025    let (status, body): (Status, Vec<u8>) = match hdr.command {
1026        ss::cmd::NEGOTIATE => route!(),
1027        ss::cmd::SESSION_SETUP => route!(),
1028        ss::cmd::TREE_CONNECT => route!(),
1029        ss::cmd::TREE_DISCONNECT => route!(),
1030        ss::cmd::LOGOFF => route!(),
1031        ss::cmd::CREATE => route!(),
1032        ss::cmd::READ => route!(),
1033        ss::cmd::WRITE => route!(),
1034        ss::cmd::CLOSE => route!(),
1035        ss::cmd::FLUSH => route!(),
1036        ss::cmd::IOCTL => route!(),
1037        ss::cmd::LOCK => route!(),
1038        ss::cmd::CHANGE_NOTIFY => route!(),
1039        ss::cmd::QUERY_DIRECTORY => route!(),
1040        ss::cmd::QUERY_INFO => route!(),
1041        ss::cmd::SET_INFO => route!(),
1042        ss::cmd::CANCEL => route!(),
1043        ss::cmd::ECHO => route!(),
1044        ss::cmd::OPLOCK_BREAK => route!(),
1045        _ => (Status::NOT_IMPLEMENTED, Vec::new()),
1046    };
1047
1048    // Derive the dialect-specific signing key once authentication completes;
1049    // the 3.1.1 key binds to the finished pre-auth integrity hash.
1050    if hdr.command == ss::cmd::SESSION_SETUP
1051        && conn.authenticated
1052        && let Some(key) = conn.session_key
1053        && conn.signing_key.is_none()
1054    {
1055        conn.signing_key = match conn.dialect {
1056            Some(
1057                smb_server_proto_smb2::negotiate::DIALECT_300 | smb_server_proto_smb2::negotiate::DIALECT_302,
1058            ) => {
1059                let k = smb_server_auth::crypto::kdf_counter_mode_hmac_sha256(
1060                    &key,
1061                    b"SMB2AESCMAC\0",
1062                    b"SmbSign\0",
1063                    16,
1064                );
1065                Some(k.try_into().unwrap())
1066            }
1067            Some(smb_server_proto_smb2::negotiate::DIALECT_311) => {
1068                let k = smb_server_auth::crypto::kdf_counter_mode_hmac_sha256(
1069                    &key,
1070                    b"SMBSigningKey\0",
1071                    &conn.preauth_hash,
1072                    16,
1073                );
1074                Some(k.try_into().unwrap())
1075            }
1076            _ => Some(key),
1077        };
1078    }
1079
1080    // Encryption enablement ([MS-SMB2] §3.3.5.5 / §3.3.5.16): when the
1081    // server is configured to seal and the client offered a cipher, derive
1082    // C2S/S2C keys and set SMB2_SESSION_FLAG_ENCRYPT_DATA on the final
1083    // SESSION_SETUP response (that response itself stays plaintext).
1084    let mut seal_session = false;
1085    // Sealing requires the AEAD primitives from the default CSP backend.
1086    #[cfg(feature = "lib")]
1087    let sealing_available = true;
1088    #[cfg(not(feature = "lib"))]
1089    let sealing_available = false;
1090    // Derive cipher keys whenever a cipher was negotiated — even when the
1091    // server does not *require* encryption — so we can decrypt client-initiated
1092    // sealed traffic ([MS-SMB2] §3.1.5.1: a client may encrypt once a cipher is
1093    // agreed). `--encrypt` only controls whether we force-seal the session.
1094    if hdr.command == ss::cmd::SESSION_SETUP
1095        && status == Status::SUCCESS
1096        && sealing_available
1097        && conn.authenticated
1098        && !conn.guest
1099        && conn.cipher.is_some()
1100        && conn.enc_keys.is_none()
1101        && let Some(key) = conn.session_key {
1102            let (c2s_label, s2c_label) = match conn.dialect {
1103                Some(smb_server_proto_smb2::negotiate::DIALECT_311) => (
1104                    b"SMBC2SCipherKey\0".as_slice(),
1105                    b"SMBS2CCipherKey\0".as_slice(),
1106                ),
1107                _ => (b"SMB2AESCCM\0".as_slice(), b"SMB2AESCCM\0".as_slice()),
1108            };
1109            let s2c_ctx: &[u8] = match conn.dialect {
1110                Some(smb_server_proto_smb2::negotiate::DIALECT_311) => &conn.preauth_hash,
1111                _ => b"ServerOut\0",
1112            };
1113            let c2s_ctx: &[u8] = match conn.dialect {
1114                Some(smb_server_proto_smb2::negotiate::DIALECT_311) => &conn.preauth_hash,
1115                _ => b"ServerIn \0", // trailing space per [MS-SMB2] §3.1.4.1
1116            };
1117            // AES-128 derives a 16-byte key; AES-256 derives 32 bytes (the KDF
1118            // encodes the requested length as L in bits per [SP800-108]).
1119            let klen = cipher_key_len(conn.cipher.unwrap());
1120            let kdf = |label: &[u8], context: &[u8]| -> [u8; 32] {
1121                let derived =
1122                    smb_server_auth::crypto::kdf_counter_mode_hmac_sha256(&key, label, context, klen);
1123                let mut k = [0u8; 32];
1124                k[..klen].copy_from_slice(&derived);
1125                k
1126            };
1127            conn.enc_keys = Some((kdf(c2s_label, c2s_ctx), kdf(s2c_label, s2c_ctx)));
1128            // Only *require* encryption (force-seal + set ENCRYPT_DATA on this
1129            // response) when the operator asked for it.
1130            if server.encrypt {
1131                conn.encrypt_data = true;
1132                seal_session = true;
1133            }
1134            tracing::info!(
1135                cipher = format!("{:#x}", conn.cipher.unwrap()),
1136                dialect = format!("{:#06x}", conn.dialect.unwrap_or(0)),
1137                required = server.encrypt,
1138                "cipher keys derived"
1139            );
1140        }
1141
1142    let mut resp = response(&hdr, status, body, conn.session_id);
1143    if let Some(new_tid) = conn.resp_tree_id.take() {
1144        resp[hdr::TREE_ID..hdr::SESSION_ID].copy_from_slice(&new_tid.to_le_bytes()); // TreeId
1145    }
1146
1147    // Register the established session so later channels can bind to it
1148    // ([MS-SMB2] §3.3.5.5.3 multichannel session binding). A binding setup
1149    // attaches a new channel to an existing session and MUST NOT overwrite the
1150    // stored session crypto with this channel's freshly derived keys.
1151    if hdr.command == ss::cmd::SESSION_SETUP
1152        && status == Status::SUCCESS
1153        && conn.authenticated
1154        && conn.session_id != 0
1155        && !conn.binding
1156    {
1157        server.sessions.insert(
1158            conn.session_id,
1159            crate::state::SessionEntry {
1160                session_key: conn.session_key,
1161                signing_key: conn.signing_key,
1162                dialect: conn.dialect,
1163                user: conn.user.clone(),
1164                guest: conn.guest,
1165                cipher: conn.cipher,
1166                enc_keys: conn.enc_keys,
1167                encrypt_data: conn.encrypt_data,
1168            },
1169        );
1170    }
1171
1172    // A session setup that fails (e.g. a malformed binding token) does not
1173    // contribute to the 3.1.1 pre-auth integrity hash ([MS-SMB2] §3.3.5.5): roll
1174    // back the request-side update so a later attempt derives the right keys.
1175    let ss_failed = hdr.command == ss::cmd::SESSION_SETUP
1176        && !matches!(status, Status::SUCCESS | Status::MORE_PROCESSING_REQUIRED);
1177    if ss_failed {
1178        conn.preauth_hash = preauth_before;
1179    } else if is_preauth_msg && conn.dialect == Some(smb_server_proto_smb2::negotiate::DIALECT_311) {
1180        let mut joined = Vec::with_capacity(hdr::LEN + resp.len());
1181        joined.extend_from_slice(&conn.preauth_hash);
1182        joined.extend_from_slice(&resp);
1183        conn.preauth_hash = smb_server_auth::crypto::sha512(&joined);
1184    }
1185
1186    // The enabling SESSION_SETUP response itself must travel in the clear;
1187    // every later message for this session gets wrapped.
1188    let allow_wrap = !seal_session;
1189
1190    // Mirror the client's signing flag ([MS-SMB2] §3.3.4.2): authenticated
1191    // sessions get signatures over the full PDU - HMAC-SHA256 for 2.x and
1192    // AES-CMAC for 3.x. The final SESSION_SETUP leg is signed even though
1193    // its request was not (clients verify it once their key is complete).
1194    let final_setup_leg =
1195        hdr.command == ss::cmd::SESSION_SETUP && status == Status::SUCCESS && conn.authenticated;
1196    // A SESSION_SETUP interim (MORE_PROCESSING) response is never signed, even
1197    // when its request was signed (as a reauthentication leg is): the signing
1198    // key for this exchange is only finalized on the last leg ([MS-SMB2]
1199    // §3.3.4.1.1). Only the final leg, and non-setup signed requests, are signed.
1200    let sign_setup = final_setup_leg;
1201    let sign_other = hdr.is_signed() && hdr.command != ss::cmd::SESSION_SETUP;
1202    if (sign_setup || sign_other)
1203        && let Some(key) = conn.signing_key.or(conn.session_key) {
1204            sign_pdu(&mut resp, &key, conn.dialect, conn.signing_algo);
1205        }
1206    Some((resp, allow_wrap))
1207}
1208
1209/// AES-GMAC nonce for signing ([MS-SMB2] §3.1.4.1): MessageId in the low 8
1210/// bytes; in the top 4 bytes bit 0 marks a server-sent message and bit 1 an
1211/// SMB2 CANCEL request.
1212#[cfg(feature = "lib")]
1213fn gmac_nonce(msg: &[u8], server_sender: bool) -> [u8; 12] {
1214    /// Bit 1 of the nonce's flags half marks an SMB2 CANCEL request
1215    /// ([MS-SMB2] §3.1.4.1).
1216    const CANCEL_FLAG: u32 = 0x2;
1217    let msg_id_len = size_of::<u64>();
1218    let mut n = [0u8; 12];
1219    n[..msg_id_len].copy_from_slice(&msg[hdr::MESSAGE_ID..hdr::MESSAGE_ID + msg_id_len]);
1220    let mut flags = u32::from(server_sender);
1221    if u16::from_le_bytes([msg[hdr::COMMAND], msg[hdr::COMMAND + 1]]) == ss::cmd::CANCEL {
1222        flags |= CANCEL_FLAG;
1223    }
1224    n[msg_id_len..].copy_from_slice(&flags.to_le_bytes());
1225    n
1226}
1227
1228#[cfg(feature = "lib")]
1229fn sig_gmac(key: &[u8; 16], msg: &[u8], server_sender: bool) -> [u8; 16] {
1230    smb_server_auth::crypto::aes128_gmac(key, &gmac_nonce(msg, server_sender), msg)
1231}
1232#[cfg(not(feature = "lib"))]
1233fn sig_gmac(key: &[u8; 16], msg: &[u8], _server_sender: bool) -> [u8; 16] {
1234    let t = smb_server_auth::crypto::aes128_cmac(key, msg);
1235    let mut s = [0u8; 16];
1236    s.copy_from_slice(&t);
1237    s
1238}
1239
1240/// The 16-byte SMB2 signature over `msg` (signature field pre-zeroed): the
1241/// negotiated 3.x algorithm (AES-CMAC / AES-GMAC / HMAC-SHA256), or HMAC-SHA256
1242/// for 2.x ([MS-SMB2] §3.1.4.1). `server_sender` picks the AES-GMAC nonce bit.
1243fn message_signature(
1244    msg: &[u8],
1245    key: &[u8; 16],
1246    dialect: Option<u16>,
1247    signing_algo: u16,
1248    server_sender: bool,
1249) -> [u8; 16] {
1250    use smb_server_proto_smb2::negotiate::ctx_type;
1251    let hmac16 = || {
1252        let t = smb_server_auth::crypto::hmac_sha256(key, msg);
1253        let mut s = [0u8; 16];
1254        s.copy_from_slice(&t[..hdr::SIGNATURE_LEN]);
1255        s
1256    };
1257    let is_3x = matches!(
1258        dialect,
1259        Some(
1260            smb_server_proto_smb2::negotiate::DIALECT_300
1261                | smb_server_proto_smb2::negotiate::DIALECT_302
1262                | smb_server_proto_smb2::negotiate::DIALECT_311
1263        )
1264    );
1265    if !is_3x {
1266        return hmac16();
1267    }
1268    match signing_algo {
1269        ctx_type::SIGNING_HMAC_SHA256 => hmac16(),
1270        ctx_type::SIGNING_AES128_GMAC => sig_gmac(key, msg, server_sender),
1271        _ => {
1272            let t = smb_server_auth::crypto::aes128_cmac(key, msg);
1273            let mut s = [0u8; 16];
1274            s.copy_from_slice(&t);
1275            s
1276        }
1277    }
1278}
1279
1280/// Stamp an SMB2 signature over `resp` in place ([MS-SMB2] §3.3.4.1.1). Sets the
1281/// SIGNED flag and fills the 16-byte Signature field (bytes 48..64).
1282fn sign_pdu(resp: &mut [u8], key: &[u8; 16], dialect: Option<u16>, signing_algo: u16) {
1283    resp[hdr::FLAGS] |= hdr_flags::SIGNED as u8;
1284    let mut msg = Vec::with_capacity(resp.len());
1285    msg.extend_from_slice(&resp[..hdr::SIGNATURE]);
1286    msg.extend_from_slice(&[0u8; 16]); // zeroed Signature field
1287    msg.extend_from_slice(&resp[hdr::LEN..]);
1288    let sig = message_signature(&msg, key, dialect, signing_algo, true);
1289    resp[hdr::SIGNATURE..hdr::LEN].copy_from_slice(&sig);
1290}
1291
1292/// Verify a request PDU's signature against `key` ([MS-SMB2] §3.3.5.2.4):
1293/// recompute the negotiated signature over the PDU with the signature field
1294/// zeroed and compare to the header's Signature.
1295fn verify_pdu_signature(buf: &[u8], key: &[u8; 16], dialect: Option<u16>, signing_algo: u16) -> bool {
1296    if buf.len() < hdr::LEN {
1297        return false;
1298    }
1299    let mut msg = Vec::with_capacity(buf.len());
1300    msg.extend_from_slice(&buf[..hdr::SIGNATURE]);
1301    msg.extend_from_slice(&[0u8; 16]); // zeroed Signature field
1302    msg.extend_from_slice(&buf[hdr::LEN..]);
1303    let expected = message_signature(&msg, key, dialect, signing_algo, false);
1304    expected == buf[hdr::SIGNATURE..hdr::LEN]
1305}
1306
1307/// Per-session crypto material an async completion needs to sign/seal a frame
1308/// off the request path (snapshotted at request time so the background task
1309/// never borrows the connection).
1310#[derive(Clone)]
1311struct AsyncCrypto {
1312    dialect: Option<u16>,
1313    signing_key: Option<[u8; 16]>,
1314    signing_algo: u16,
1315    enc_keys: Option<([u8; 32], [u8; 32])>,
1316    cipher: Option<u16>,
1317    session_id: u64,
1318    encrypt: bool,
1319    signed: bool,
1320}
1321
1322impl AsyncCrypto {
1323    fn snapshot(conn: &Smb2Conn, request_signed: bool) -> Self {
1324        Self {
1325            dialect: conn.dialect,
1326            signing_key: conn.signing_key.or(conn.session_key),
1327            signing_algo: conn.signing_algo,
1328            enc_keys: conn.enc_keys,
1329            cipher: conn.cipher,
1330            session_id: conn.session_id,
1331            encrypt: conn.encrypt_data || conn.peer_encrypts,
1332            signed: request_signed,
1333        }
1334    }
1335}
1336
1337/// Plumbing every async-deferred command reply (LOCK / CHANGE_NOTIFY) shares:
1338/// crypto material to sign/seal the final frame, the request's identifiers,
1339/// the outbound queue to send it on, and its cancellation signal.
1340struct AsyncReply {
1341    crypto: AsyncCrypto,
1342    message_id: u64,
1343    async_id: u64,
1344    outbound: mpsc::Sender<Vec<u8>>,
1345    cancel: oneshot::Receiver<Status>,
1346}
1347
1348/// Build an async-header frame ([MS-SMB2] §3.3.4.2 async mode):
1349/// SERVER_TO_REDIR|ASYNC_COMMAND flags with the AsyncId occupying the
1350/// Reserved+TreeId slot. Returned unsigned and unsealed.
1351fn build_async_frame(
1352    session_id: u64,
1353    message_id: u64,
1354    async_id: u64,
1355    command: u16,
1356    status: Status,
1357    body: &[u8],
1358) -> Vec<u8> {
1359    let mut f = Vec::with_capacity(hdr::LEN + body.len());
1360    f.extend_from_slice(&smb_server_proto_smb2::SMB2_MAGIC);
1361    f.extend_from_slice(&(hdr::LEN as u16).to_le_bytes()); // StructureSize
1362    f.extend_from_slice(&1u16.to_le_bytes()); // CreditCharge
1363    f.extend_from_slice(&status.raw().to_le_bytes());
1364    f.extend_from_slice(&command.to_le_bytes());
1365    f.extend_from_slice(&1u16.to_le_bytes()); // CreditResponse
1366    f.extend_from_slice(&(hdr_flags::SERVER_TO_REDIR | hdr_flags::ASYNC_COMMAND).to_le_bytes());
1367    f.extend_from_slice(&0u32.to_le_bytes()); // NextCommand
1368    f.extend_from_slice(&message_id.to_le_bytes());
1369    f.extend_from_slice(&async_id.to_le_bytes()); // AsyncId
1370    f.extend_from_slice(&session_id.to_le_bytes());
1371    f.extend_from_slice(&[0u8; 16]); // Signature
1372    f.extend_from_slice(body);
1373    f
1374}
1375
1376/// Sign then (optionally) seal a fully-built async frame per the session's
1377/// protection, mirroring the request path's sign-then-encrypt order.
1378fn finalize_async(crypto: &AsyncCrypto, mut frame: Vec<u8>) -> Vec<u8> {
1379    if crypto.signed
1380        && let Some(key) = crypto.signing_key {
1381            sign_pdu(&mut frame, &key, crypto.dialect, crypto.signing_algo);
1382        }
1383    if crypto.encrypt
1384        && let (Some(keys), Some(cipher)) = (crypto.enc_keys, crypto.cipher)
1385            && let Some(sealed) = seal_pdu(crypto.session_id, keys, cipher, &frame) {
1386                return sealed;
1387            }
1388    frame
1389}
1390
1391/// Block until a conflicting byte-range lock is released and the requested
1392/// ranges can be acquired, then emit the final LOCK response ([MS-SMB2]
1393/// §3.3.5.14). Cancellation yields STATUS_CANCELLED.
1394/// How an async-capable command (LOCK / CHANGE_NOTIFY) starts: either an
1395/// immediate reply, or a deferral parked under an async id whose final reply a
1396/// background worker sends later ([MS-SMB2] §3.3.4.2).
1397pub(crate) enum AsyncStart {
1398    Reply(Status, Vec<u8>),
1399    Pending(u64),
1400}
1401
1402/// Begin a LOCK ([MS-SMB2] §3.3.5.14): apply unlocks, try acquisitions, and
1403/// either answer immediately or (for a blocking lock) spawn a waiter and defer.
1404pub(crate) fn begin_lock(
1405    conn: &mut Smb2Conn,
1406    server: &Arc<ServerShared>,
1407    hdr: &smb_server_proto_smb2::Header2,
1408    buf: &[u8],
1409) -> AsyncStart {
1410    let Some(req) = c::LockReq::parse(buf) else {
1411        return AsyncStart::Reply(Status::INVALID_PARAMETER, Vec::new());
1412    };
1413    let Some(path) = conn.with_handle(&req.file_id.0, |h| h.path.clone()) else {
1414        return AsyncStart::Reply(Status::INVALID_HANDLE, Vec::new());
1415    };
1416    let owner = (conn.session_id, req.file_id.0);
1417
1418    // Unlocks first, then acquisitions ([MS-SMB2] §3.3.5.14).
1419    for l in req.locks.iter().filter(|l| l.unlock) {
1420        server.locks.release(&path, &[(l.offset, l.length)], owner);
1421    }
1422    let acquires: Vec<(u64, u64, bool)> = req
1423        .locks
1424        .iter()
1425        .filter(|l| !l.unlock)
1426        .map(|l| (l.offset, l.length, l.exclusive))
1427        .collect();
1428
1429    if acquires.is_empty() || server.locks.try_acquire(&path, &acquires, owner) {
1430        return AsyncStart::Reply(Status::SUCCESS, c::build_lock_resp());
1431    }
1432    if req.locks.iter().any(|l| l.fail_immediately) {
1433        return AsyncStart::Reply(Status::LOCK_NOT_GRANTED, Vec::new());
1434    }
1435    // Blocking lock: interim STATUS_PENDING, then retry as conflicting locks
1436    // are released.
1437    let async_id = conn.alloc_async_id();
1438    let crypto = AsyncCrypto::snapshot(conn, hdr.is_signed());
1439    let (cancel_tx, cancel_rx) = oneshot::channel();
1440    conn.async_cancels.insert(async_id, cancel_tx);
1441    conn.async_msgids.insert(hdr.message_id, async_id);
1442    gauge!("smb_async_pending").increment(1.0);
1443    tokio_uring::spawn(run_lock_wait(
1444        server.locks.clone(),
1445        path,
1446        acquires,
1447        owner,
1448        AsyncReply {
1449            crypto,
1450            message_id: hdr.message_id,
1451            async_id,
1452            outbound: conn.outbound.clone(),
1453            cancel: cancel_rx,
1454        },
1455    ));
1456    AsyncStart::Pending(async_id)
1457}
1458
1459/// Begin a CHANGE_NOTIFY ([MS-SMB2] §3.3.5.19): validate the directory open,
1460/// register the watch, and defer with an interim STATUS_PENDING.
1461pub(crate) fn begin_change_notify(
1462    conn: &mut Smb2Conn,
1463    hdr: &smb_server_proto_smb2::Header2,
1464    buf: &[u8],
1465) -> AsyncStart {
1466    let Some(req) = c::ChangeNotifyReq::parse(buf) else {
1467        return AsyncStart::Reply(Status::INVALID_PARAMETER, Vec::new());
1468    };
1469    let Some((is_dir, can_read, dir_path)) =
1470        conn.with_handle(&req.file_id.0, |o| (o.is_dir, o.can_read, o.path.clone()))
1471    else {
1472        return AsyncStart::Reply(Status::INVALID_HANDLE, Vec::new());
1473    };
1474    // Validate before registering the watch ([MS-SMB2] §3.3.5.19).
1475    if !is_dir {
1476        return AsyncStart::Reply(Status::INVALID_PARAMETER, Vec::new());
1477    }
1478    if !can_read {
1479        return AsyncStart::Reply(Status::ACCESS_DENIED, Vec::new());
1480    }
1481    if req.output_len > MAX_TRANSACT_SIZE {
1482        return AsyncStart::Reply(Status::INVALID_PARAMETER, Vec::new());
1483    }
1484
1485    let async_id = conn.alloc_async_id();
1486    let crypto = AsyncCrypto::snapshot(conn, hdr.is_signed());
1487    let (cancel_tx, cancel_rx) = oneshot::channel();
1488    conn.async_cancels.insert(async_id, cancel_tx);
1489    conn.async_msgids.insert(hdr.message_id, async_id);
1490    conn.async_by_file
1491        .entry(req.file_id.0)
1492        .or_default()
1493        .push(async_id);
1494    gauge!("smb_async_pending").increment(1.0);
1495    tokio_uring::spawn(run_change_notify(
1496        dir_path,
1497        req.watch_tree,
1498        req.filter,
1499        AsyncReply {
1500            crypto,
1501            message_id: hdr.message_id,
1502            async_id,
1503            outbound: conn.outbound.clone(),
1504            cancel: cancel_rx,
1505        },
1506    ));
1507    AsyncStart::Pending(async_id)
1508}
1509
1510/// Handle a CANCEL ([MS-SMB2] §3.3.5.16): find the pending op by AsyncId (async
1511/// CANCEL) or MessageId (sync CANCEL) and signal it to complete with
1512/// STATUS_CANCELLED. CANCEL itself carries no response.
1513pub(crate) fn cancel(conn: &mut Smb2Conn, hdr: &smb_server_proto_smb2::Header2, buf: &[u8]) {
1514    counter!("smb_cancels_total").increment(1);
1515    let async_id = if hdr.is_async() && buf.len() >= hdr::ASYNC_ID + size_of::<u64>() {
1516        Some(u64::from_le_bytes(
1517            buf[hdr::ASYNC_ID..hdr::SESSION_ID].try_into().unwrap(),
1518        ))
1519    } else {
1520        conn.async_msgids.get(&hdr.message_id).copied()
1521    };
1522    if let Some(aid) = async_id {
1523        conn.async_msgids.retain(|_, v| *v != aid);
1524        conn.async_by_file
1525            .values_mut()
1526            .for_each(|v| v.retain(|x| *x != aid));
1527        if let Some(tx) = conn.async_cancels.remove(&aid) {
1528            let _ = tx.send(Status::CANCELLED);
1529        }
1530    }
1531}
1532
1533async fn run_lock_wait(
1534    locks: Arc<crate::state::LockManager>,
1535    path: String,
1536    ranges: Vec<(u64, u64, bool)>,
1537    owner: crate::state::LockOwner,
1538    mut reply: AsyncReply,
1539) {
1540    let granted = loop {
1541        // Register for the wakeup before trying, so a release between the try
1542        // and the await is not lost.
1543        let notified = locks.released().notified();
1544        if locks.try_acquire(&path, &ranges, owner) {
1545            break true;
1546        }
1547        tokio::select! {
1548            _ = notified => {}
1549            _ = &mut reply.cancel => break false,
1550        }
1551    };
1552    let (status, body) = if granted {
1553        counter!("smb_locks_granted_total").increment(1);
1554        (Status::SUCCESS, c::build_lock_resp())
1555    } else {
1556        (Status::CANCELLED, Vec::new())
1557    };
1558    let frame = finalize_async(
1559        &reply.crypto,
1560        build_async_frame(
1561            reply.crypto.session_id,
1562            reply.message_id,
1563            reply.async_id,
1564            ss::cmd::LOCK,
1565            status,
1566            &body,
1567        ),
1568    );
1569    let _ = reply.outbound.send(frame).await;
1570    gauge!("smb_async_pending").decrement(1.0);
1571}
1572
1573/// Watch `dir_path` for one filesystem change (or cancellation) and emit the
1574/// final CHANGE_NOTIFY response ([MS-SMB2] §2.2.36) on the outbound queue.
1575async fn run_change_notify(dir_path: String, watch_tree: bool, filter: u32, mut reply: AsyncReply) {
1576    let frame = match watch_one_event(&dir_path, watch_tree, filter, &mut reply.cancel).await {
1577        Ok(entries) => {
1578            let pairs: Vec<(u32, &str)> = entries.iter().map(|(a, n)| (*a, n.as_str())).collect();
1579            let buf = c::build_file_notify_information(&pairs);
1580            let body = c::build_change_notify_resp(&buf);
1581            counter!("smb_notifies_sent").increment(1);
1582            build_async_frame(
1583                reply.crypto.session_id,
1584                reply.message_id,
1585                reply.async_id,
1586                ss::cmd::CHANGE_NOTIFY,
1587                Status::SUCCESS,
1588                &body,
1589            )
1590        }
1591        Err(status) => {
1592            // NOTIFY_CLEANUP is severity-success: clients parse it as a normal
1593            // (empty) CHANGE_NOTIFY response. A genuine CANCEL is an error and
1594            // must carry the SMB2 error-response body.
1595            let body = if status == Status::NOTIFY_CLEANUP {
1596                c::build_change_notify_resp(&[])
1597            } else {
1598                error_resp()
1599            };
1600            build_async_frame(
1601                reply.crypto.session_id,
1602                reply.message_id,
1603                reply.async_id,
1604                ss::cmd::CHANGE_NOTIFY,
1605                status,
1606                &body,
1607            )
1608        }
1609    };
1610    let _ = reply.outbound.send(finalize_async(&reply.crypto, frame)).await;
1611    gauge!("smb_async_pending").decrement(1.0);
1612}
1613
1614/// Await the first inotify event on `dir_path` (mapped from the SMB completion
1615/// filter) or a cancellation. When `watch_tree` is set, subdirectories are
1616/// watched too and the reported name is relative to `dir_path`. Returns the
1617/// `(action, name)` list on an event, or `Err(status)` carrying the completion
1618/// status on cancel/handle-close/setup error.
1619async fn watch_one_event(
1620    dir_path: &str,
1621    watch_tree: bool,
1622    filter: u32,
1623    cancel: &mut oneshot::Receiver<Status>,
1624) -> Result<Vec<(u32, String)>, Status> {
1625    use futures_util::StreamExt;
1626    use inotify::Inotify;
1627    use std::collections::HashMap;
1628
1629    let inotify = Inotify::init().map_err(|_| Status::CANCELLED)?;
1630    let mask = filter_to_mask(filter);
1631    // Map each watch descriptor to its path relative to the watched root so a
1632    // recursive event names the file as `subdir\name`.
1633    let mut wd_rel: HashMap<inotify::WatchDescriptor, String> = HashMap::new();
1634    let root_wd = inotify
1635        .watches()
1636        .add(dir_path, mask)
1637        .map_err(|_| Status::CANCELLED)?;
1638    wd_rel.insert(root_wd, String::new());
1639    if watch_tree {
1640        for (abs, rel) in collect_subdirs(std::path::Path::new(dir_path)) {
1641            if let Ok(wd) = inotify.watches().add(&abs, mask) {
1642                wd_rel.insert(wd, rel);
1643            }
1644        }
1645    }
1646    let mut buf = [0u8; 4096];
1647    let mut stream = inotify
1648        .into_event_stream(&mut buf)
1649        .map_err(|_| Status::CANCELLED)?;
1650
1651    tokio::select! {
1652        ev = stream.next() => {
1653            let ev = ev.ok_or(Status::CANCELLED)?.map_err(|_| Status::CANCELLED)?;
1654            let name = ev
1655                .name
1656                .as_ref()
1657                .map(|n| n.to_string_lossy().into_owned())
1658                .unwrap_or_default();
1659            let prefix = wd_rel.get(&ev.wd).cloned().unwrap_or_default();
1660            let full = if prefix.is_empty() { name } else { format!("{prefix}\\{name}") };
1661            Ok(vec![(mask_to_action(ev.mask), full)])
1662        }
1663        status = cancel => Err(status.unwrap_or(Status::CANCELLED)),
1664    }
1665}
1666
1667/// Collect subdirectories under `root` as `(absolute_path, backslash_relative)`
1668/// pairs for recursive CHANGE_NOTIFY, capped to bound very deep trees.
1669fn collect_subdirs(root: &std::path::Path) -> Vec<(std::path::PathBuf, String)> {
1670    const CAP: usize = 4096;
1671    let mut out = Vec::new();
1672    let mut stack = vec![(root.to_path_buf(), String::new())];
1673    while let Some((dir, rel)) = stack.pop() {
1674        let Ok(rd) = std::fs::read_dir(&dir) else {
1675            continue;
1676        };
1677        for e in rd.flatten() {
1678            if e.file_type().map(|t| t.is_dir()).unwrap_or(false) {
1679                let name = e.file_name().to_string_lossy().into_owned();
1680                let child = if rel.is_empty() {
1681                    name
1682                } else {
1683                    format!("{rel}\\{name}")
1684                };
1685                out.push((e.path(), child.clone()));
1686                stack.push((e.path(), child));
1687                if out.len() >= CAP {
1688                    return out;
1689                }
1690            }
1691        }
1692    }
1693    out
1694}
1695
1696/// Translate an SMB completion filter ([MS-SMB2] §2.2.35) into an inotify
1697/// watch mask.
1698fn filter_to_mask(filter: u32) -> inotify::WatchMask {
1699    use inotify::WatchMask as M;
1700    use smb_server_proto_smb2::commands::notify_filter as nf;
1701
1702    let mut m = M::empty();
1703    if filter & (nf::FILE_NAME | nf::DIR_NAME) != 0 {
1704        m |= M::CREATE | M::DELETE | M::MOVED_FROM | M::MOVED_TO;
1705    }
1706    // Attribute and timestamp changes are delivered as inotify IN_ATTRIB on
1707    // Linux (utimensat / chmod), so map every metadata filter there.
1708    if filter & (nf::ATTRIBUTES | nf::LAST_WRITE | nf::LAST_ACCESS | nf::CREATION | nf::EA) != 0 {
1709        m |= M::ATTRIB;
1710    }
1711    if filter & (nf::SIZE | nf::LAST_WRITE | nf::STREAM_SIZE | nf::STREAM_WRITE | nf::STREAM_NAME)
1712        != 0
1713    {
1714        m |= M::MODIFY | M::CLOSE_WRITE;
1715    }
1716    if m.is_empty() {
1717        m = M::CREATE | M::DELETE | M::MODIFY | M::MOVED_FROM | M::MOVED_TO;
1718    }
1719    m
1720}
1721
1722/// Map an inotify event mask to a FILE_NOTIFY_INFORMATION action
1723/// ([MS-FSCC] §2.7.1).
1724fn mask_to_action(mask: inotify::EventMask) -> u32 {
1725    use inotify::EventMask as E;
1726    use smb_server_proto_smb2::commands::notify_action as na;
1727
1728    if mask.contains(E::CREATE) {
1729        na::ADDED
1730    } else if mask.contains(E::DELETE) {
1731        na::REMOVED
1732    } else if mask.contains(E::MOVED_FROM) {
1733        na::RENAMED_OLD_NAME
1734    } else if mask.contains(E::MOVED_TO) {
1735        na::RENAMED_NEW_NAME
1736    } else {
1737        na::MODIFIED
1738    }
1739}
1740
1741/// Build the wildcard-dialect NEGOTIATE response used to answer the
1742/// invalid-status probe ([MS-SMB2] §2.2.3.1.1): DialectRevision 0x02FF with
1743/// all other fields zero.
1744#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB2 wire serialization
1745fn probe_negotiate_resp() -> Vec<u8> {
1746    let mut b = Vec::with_capacity(68);
1747    b.extend_from_slice(&65u16.to_le_bytes()); // StructureSize
1748    b.extend_from_slice(&0u16.to_le_bytes()); // SecurityMode
1749    b.extend_from_slice(&0x02FFu16.to_le_bytes()); // DialectRevision wildcard
1750    b.extend_from_slice(&0u16.to_le_bytes()); // Reserved/NegotiateContextCount
1751    b.extend_from_slice(&[0u8; 16]); // ServerGuid
1752    b.extend_from_slice(&0u32.to_le_bytes()); // Capabilities
1753    b.extend_from_slice(&0u32.to_le_bytes()); // MaxTransactionSize
1754    b.extend_from_slice(&0u32.to_le_bytes()); // MaxReadSize
1755    b.extend_from_slice(&0u32.to_le_bytes()); // MaxWriteSize
1756    b.extend_from_slice(&0u64.to_le_bytes()); // SystemTime
1757    b.extend_from_slice(&0u64.to_le_bytes()); // ServerStartTime
1758    b.extend_from_slice(&0u16.to_le_bytes()); // SecurityBufferOffset
1759    b.extend_from_slice(&0u16.to_le_bytes()); // SecurityBufferLength
1760    b.extend_from_slice(&0u32.to_le_bytes()); // NegotiateContextOffset
1761    b
1762}
1763
1764// ---------------- SESSION_SETUP ----------------
1765
1766/// Handle one leg of SPNEGO/NTLMSSP session establishment ([MS-SMB2]
1767/// §3.3.5.5).
1768/// How a NEGOTIATE resolves: an immediate reply, or silence because a repeat
1769/// NEGOTIATE on an already-negotiated connection terminates it ([MS-SMB2]
1770/// §3.3.5.4).
1771pub(crate) enum NegotiateReply {
1772    Reply(Status, Vec<u8>),
1773    Silent,
1774}
1775
1776/// Negotiate a dialect ([MS-SMB2] §3.3.5.3-4): answer probes with the wildcard
1777/// response, pick a common dialect, validate 3.1.1 pre-auth contexts, and choose
1778/// cipher/compression. The response is framed and pre-auth-hashed by the shared
1779/// tail in `process_single`.
1780#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB2 wire serialization
1781pub(crate) fn negotiate(
1782    conn: &mut Smb2Conn,
1783    server: &Arc<ServerShared>,
1784    hdr: &smb_server_proto_smb2::Header2,
1785    buf: &[u8],
1786) -> NegotiateReply {
1787    let body_start = hdr::LEN;
1788    // A NEGOTIATE received after the connection already negotiated a dialect is
1789    // invalid: the server MUST disconnect and not reply ([MS-SMB2] §3.3.5.4).
1790    if conn.dialect.is_some() {
1791        conn.disconnect = true;
1792        return NegotiateReply::Silent;
1793    }
1794    // Clients probing for SMB2 support send either a header-only NEGOTIATE or one
1795    // carrying Status = STATUS_INVALID_PARAMETER ([MS-SMB2] §3.3.5.3). Answer
1796    // both with the wildcard-dialect response so they retry a real negotiation.
1797    let parsed = smb_server_proto_smb2::negotiate::Request::parse(buf.get(body_start..).unwrap_or(&[]));
1798    tracing::debug!(
1799        status = hdr.status,
1800        parsed_ok = parsed.is_some(),
1801        len = buf.len(),
1802        "negotiate probe check"
1803    );
1804    if hdr.status == Status::INVALID_PARAMETER.raw() || parsed.is_none() {
1805        if parsed.is_none() {
1806            tracing::debug!(len = buf.len(), body = %hex_str(buf.get(body_start..).unwrap_or(&[])), "negotiate parse failed");
1807        }
1808        return NegotiateReply::Reply(Status::INVALID_PARAMETER, probe_negotiate_resp());
1809    }
1810    let req = parsed.expect("validated above");
1811    let Some(dialect) = smb_server_proto_smb2::negotiate::pick(&req.dialects) else {
1812        return NegotiateReply::Reply(Status::INVALID_PARAMETER, Vec::new());
1813    };
1814    conn.dialect = Some(dialect);
1815    // Retain the client's NEGOTIATE fields for a later
1816    // FSCTL_VALIDATE_NEGOTIATE_INFO downgrade check ([MS-SMB2] §3.3.5.15.12).
1817    conn.client_guid = req.client_guid;
1818    conn.client_dialects = req.dialects.clone();
1819    conn.client_security_mode = g16(buf, body_start + 4);
1820    conn.client_capabilities = buf
1821        .get(body_start + 8..body_start + 12)
1822        .map(|s| u32::from_le_bytes(s.try_into().unwrap()))
1823        .unwrap_or(0);
1824    // SMB 3.1.1 negotiate-context validation ([MS-SMB2] §3.3.5.4): exactly one
1825    // PREAUTH_INTEGRITY context is required, and it must offer a hash algorithm
1826    // the server supports (SHA-512).
1827    if dialect == smb_server_proto_smb2::negotiate::DIALECT_311 {
1828        let preauth: Vec<_> = req
1829            .contexts
1830            .iter()
1831            .filter(|c| c.kind == smb_server_proto_smb2::negotiate::ctx_type::PREAUTH_INTEGRITY)
1832            .collect();
1833        if preauth.len() != 1 {
1834            return NegotiateReply::Reply(Status::INVALID_PARAMETER, Vec::new());
1835        }
1836        // HashAlgorithmCount(2) SaltLength(2) HashAlgorithms[] ([MS-SMB2] §2.2.3.1.1).
1837        let pd = &preauth[0].data;
1838        let count = pd
1839            .get(0..2)
1840            .map(|s| u16::from_le_bytes([s[0], s[1]]) as usize)
1841            .unwrap_or(0);
1842        let algos: Vec<u16> = pd
1843            .get(4..)
1844            .map(|d| {
1845                d.chunks_exact(2)
1846                    .take(count)
1847                    .map(|w| u16::from_le_bytes([w[0], w[1]]))
1848                    .collect()
1849            })
1850            .unwrap_or_default();
1851        if !algos.contains(&smb_server_proto_smb2::negotiate::ctx_type::SHA512) {
1852            return NegotiateReply::Reply(
1853                Status::SMB_NO_PREAUTH_INTEGRITY_HASH_OVERLAP,
1854                Vec::new(),
1855            );
1856        }
1857    }
1858    // SMB2_COMPRESSION_CAPABILITIES validation ([MS-SMB2] §3.3.5.4): a present
1859    // context must be at least the fixed structure size (8 bytes) and advertise
1860    // a non-zero CompressionAlgorithmCount, else the negotiate fails with
1861    // STATUS_INVALID_PARAMETER.
1862    if dialect == smb_server_proto_smb2::negotiate::DIALECT_311
1863        && let Some(c) = req
1864            .contexts
1865            .iter()
1866            .find(|c| c.kind == smb_server_proto_smb2::negotiate::ctx_type::COMPRESSION)
1867        {
1868            let count = c
1869                .data
1870                .get(0..2)
1871                .map(|s| u16::from_le_bytes([s[0], s[1]]))
1872                .unwrap_or(0);
1873            if c.data.len() < 8 || count == 0 {
1874                return NegotiateReply::Reply(Status::INVALID_PARAMETER, Vec::new());
1875            }
1876        }
1877    // Must match the Capabilities word written by build_response_full below so
1878    // VALIDATE_NEGOTIATE_INFO echoes it.
1879    // Server-to-client notifications ([MS-SMB2] §3.3.5.4): the server declares
1880    // IsServerToClientNotificationsSupported = TRUE (it can format and send an
1881    // SMB2_SERVER_TO_CLIENT_NOTIFICATION), so the bit is echoed whenever the
1882    // client requests it on a 3.1.1 connection.
1883    let notifications = dialect == smb_server_proto_smb2::negotiate::DIALECT_311
1884        && conn.client_capabilities & smb_server_proto_smb2::negotiate::caps::NOTIFICATIONS != 0;
1885    conn.supports_notifications = notifications;
1886    conn.advertised_caps = if dialect >= smb_server_proto_smb2::negotiate::DIALECT_300 {
1887        smb_server_proto_smb2::negotiate::caps::LARGE_MTU
1888            | smb_server_proto_smb2::negotiate::caps::MULTI_CHANNEL
1889            | smb_server_proto_smb2::negotiate::caps::LEASING
1890            | smb_server_proto_smb2::negotiate::caps::DIRECTORY_LEASING
1891            | smb_server_proto_smb2::negotiate::caps::PERSISTENT_HANDLES
1892    } else if dialect >= smb_server_proto_smb2::negotiate::DIALECT_210 {
1893        smb_server_proto_smb2::negotiate::caps::LARGE_MTU | smb_server_proto_smb2::negotiate::caps::LEASING
1894    } else {
1895        0
1896    };
1897    conn.advertised_caps |= if notifications {
1898        smb_server_proto_smb2::negotiate::caps::NOTIFICATIONS
1899    } else {
1900        0
1901    };
1902    tracing::debug!(dialect = format!("{:#06x}", dialect), "negotiated");
1903    let mut salt = [0u8; 32];
1904    salt.copy_from_slice(&crate::dispatch::rand_bytes(32));
1905    // Pick one cipher the client offered (GCM preferred).
1906    let client_ciphers: Vec<u16> = req
1907        .contexts
1908        .iter()
1909        .find(|c| c.kind == smb_server_proto_smb2::negotiate::ctx_type::ENCRYPTION)
1910        .and_then(|c| {
1911            if c.data.len() >= 2 {
1912                let n = u16::from_le_bytes([c.data[0], c.data[1]]) as usize;
1913                Some(
1914                    c.data[2..]
1915                        .chunks_exact(2)
1916                        .take(n)
1917                        .map(|w| u16::from_le_bytes([w[0], w[1]]))
1918                        .collect::<Vec<_>>(),
1919                )
1920            } else {
1921                None
1922            }
1923        })
1924        .unwrap_or_default();
1925    let chosen = {
1926        // GCM preferred by default ([MS-SMB2] §3.1.4.1). RUSTSMB_CIPHER pins a
1927        // preference to exercise a specific cipher live: ccm, 256gcm, 256ccm.
1928        let pref = std::env::var("RUSTSMB_CIPHER")
1929            .unwrap_or_default()
1930            .to_lowercase();
1931        use smb_server_proto_smb2::negotiate::ctx_type::{AES128_CCM, AES128_GCM, AES256_CCM, AES256_GCM};
1932        let order = match pref.as_str() {
1933            "ccm" | "128ccm" => [AES128_CCM, AES256_CCM, AES128_GCM, AES256_GCM],
1934            "256" | "256gcm" => [AES256_GCM, AES128_GCM, AES256_CCM, AES128_CCM],
1935            "256ccm" => [AES256_CCM, AES128_CCM, AES256_GCM, AES128_GCM],
1936            _ => [AES128_GCM, AES256_GCM, AES128_CCM, AES256_CCM],
1937        };
1938        order.into_iter().find(|ours| client_ciphers.contains(ours))
1939    };
1940    // Dialects without negotiate contexts (or clients that did not offer
1941    // ENCRYPTION_CAPABILITIES) must leave conn.cipher unset — otherwise
1942    // --encrypt would seal sessions the peer cannot read.
1943    if dialect == smb_server_proto_smb2::negotiate::DIALECT_311 && !client_ciphers.is_empty() {
1944        conn.cipher = chosen;
1945    }
1946    // Compression: intersect the client's advertised algorithms with ours; a
1947    // common set enables compressed transforms both ways.
1948    let comp_ctx = req
1949        .contexts
1950        .iter()
1951        .find(|c| c.kind == smb_server_proto_smb2::negotiate::ctx_type::COMPRESSION);
1952    let comp_algos = comp_ctx
1953        .map(|c| {
1954            smb_server_proto_smb2::compress::negotiate_algos(
1955                &smb_server_proto_smb2::compress::parse_compression_caps(&c.data),
1956            )
1957        })
1958        .unwrap_or_default();
1959    if dialect == smb_server_proto_smb2::negotiate::DIALECT_311 {
1960        // Choose the outbound compression algorithm: the first negotiated
1961        // non-pattern algorithm (Pattern_V1 is a scan applied within chained
1962        // payloads, not a standalone transform codec).
1963        use smb_server_proto_smb2::compress::algo;
1964        conn.compress_algo = comp_algos
1965            .iter()
1966            .copied()
1967            .find(|a| matches!(*a, algo::LZNT1 | algo::LZ77));
1968        // Use the chained transform for responses when the peer advertised it
1969        // ([MS-SMB2] §2.2.3.1.3 SMB2_COMPRESSION_CAPABILITIES_FLAG_CHAINED).
1970        conn.compress_chained = conn.compress_algo.is_some()
1971            && comp_ctx.is_some_and(|c| {
1972                smb_server_proto_smb2::compress::parse_compression_flags(&c.data)
1973                    & smb_server_proto_smb2::compress::CAP_FLAG_CHAINED
1974                    != 0
1975            });
1976    }
1977    // Echo the CHAINED flag only when the client requested it ([MS-SMB2]
1978    // §3.3.5.4); advertising it otherwise would make peers chain every message.
1979    let compression_chained = conn.compress_chained;
1980    // Select the 3.1.1 signing algorithm from the client's SIGNING_CAPABILITIES
1981    // (the first the server supports, [MS-SMB2] §3.3.5.4) and echo it back.
1982    let signing_algo = req
1983        .contexts
1984        .iter()
1985        .find(|c| c.kind == smb_server_proto_smb2::negotiate::ctx_type::SIGNING)
1986        .and_then(|c| {
1987            smb_server_proto_smb2::negotiate::select_signing_algo(
1988                &smb_server_proto_smb2::negotiate::parse_signing_algos(&c.data),
1989            )
1990        });
1991    if dialect == smb_server_proto_smb2::negotiate::DIALECT_311
1992        && let Some(a) = signing_algo {
1993            conn.signing_algo = a;
1994        }
1995    NegotiateReply::Reply(
1996        Status::SUCCESS,
1997        smb_server_proto_smb2::negotiate::build_response_full(
1998            dialect,
1999            &server.guid,
2000            smb_server_proto::types::FileTime::now().0,
2001            &salt,
2002            // Echo ENCRYPTION/SIGNING only when the client offered them
2003            // ([MS-SMB2] §3.3.5.4). With no common cipher, echo ENCRYPTION_NONE
2004            // (0) so the client clears Connection.CipherId.
2005            (!client_ciphers.is_empty()).then(|| chosen.unwrap_or(0)),
2006            signing_algo,
2007            &comp_algos,
2008            compression_chained,
2009            server.require_signing,
2010            notifications,
2011        ),
2012    )
2013}
2014
2015#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB2 wire serialization
2016pub(crate) fn session_setup(
2017    server: &Arc<ServerShared>,
2018    conn: &mut Smb2Conn,
2019    buf: &[u8],
2020) -> Result<(Status, Vec<u8>), Status> {
2021    let req = ss::Request::parse(buf).ok_or(Status::INVALID_PARAMETER)?;
2022    // Channel binding ([MS-SMB2] §3.3.5.5.2): the setup carries the binding
2023    // flag and the header names an existing session to attach this new
2024    // connection (channel) to.
2025    let hdr_session = buf
2026        .get(hdr::SESSION_ID..hdr::SIGNATURE)
2027        .and_then(|s| s.try_into().ok())
2028        .map(u64::from_le_bytes)
2029        .unwrap_or(0);
2030    let binding = req.flags & ss::FLAG_BINDING != 0
2031        && hdr_session != 0
2032        && server.sessions.get(hdr_session).is_some();
2033    // Channel binding requires the SMB 3.x dialect family; a 2.0.2/2.1
2034    // connection that sets the binding flag is rejected ([MS-SMB2] §3.3.5.5).
2035    if req.flags & ss::FLAG_BINDING != 0
2036        && hdr_session != 0
2037        && !matches!(conn.dialect, Some(d) if d >= smb_server_proto_smb2::negotiate::DIALECT_300)
2038    {
2039        return Err(Status::REQUEST_NOT_ACCEPTED);
2040    }
2041    conn.binding = binding;
2042    // Reauthentication ([MS-SMB2] §3.3.5.5.2): a SESSION_SETUP that names an
2043    // already-established session without the binding flag re-runs the NTLM
2044    // handshake on that same session id (the client refreshes its credentials).
2045    let reauth = !binding && hdr_session != 0 && server.sessions.get(hdr_session).is_some();
2046    let inner = smb_server_auth::ntlm::unwrap_blob(&req.blob).unwrap_or(&[]);
2047    // A binding channel does a full NTLM exchange; its GSS token must be a
2048    // well-formed SPNEGO token (negTokenInit 0x60 or negTokenResp 0xA1). A
2049    // malformed token is rejected with STATUS_INVALID_PARAMETER ([MS-SMB2]
2050    // §3.3.5.5) rather than treated as a fresh first leg.
2051    if binding && !matches!(req.blob.first(), Some(0x60) | Some(0xA1)) {
2052        return Err(Status::INVALID_PARAMETER);
2053    }
2054    tracing::trace!(
2055        blob_len = req.blob.len(),
2056        msg_type = ?smb_server_auth::ntlm::msg_type(inner),
2057        "session setup token"
2058    );
2059    match smb_server_auth::ntlm::msg_type(inner) {
2060        Some(smb_server_auth::ntlm::MSG_TYPE1) | None => {
2061            // A client using a raw NTLM security package (no SPNEGO) sends the
2062            // NTLMSSP signature directly with no negTokenInit/negTokenResp
2063            // envelope; the response must then also be bare NTLMSSP, or a
2064            // strict NTLM-only client fails to parse our SPNEGO framing as an
2065            // NTLM CHALLENGE_MESSAGE.
2066            conn.raw_ntlm = req.blob.first() != Some(&0x60) && req.blob.first() != Some(&0xA1);
2067            // Leg 1: issue CHALLENGE under MORE_PROCESSING_REQUIRED. A binding
2068            // channel and a reauthentication keep the existing session id
2069            // instead of allocating one ([MS-SMB2] §3.3.5.5.2/§3.3.5.5.3).
2070            conn.session_id = if binding || reauth {
2071                hdr_session
2072            } else {
2073                next_session_id()
2074            };
2075            let mut t2 =
2076                smb_server_auth::ntlm::build_type2(&conn.challenge, &server.domain, &server.server_name);
2077            // Grant SIGN|SEAL so clients may negotiate protected sessions
2078            // ([MS-NLMP] §3.2.5.1); sealing itself rides the SMB3 cipher.
2079            let extra = smb_server_auth::ntlm::NEGOTIATE_SIGN
2080                | smb_server_auth::ntlm::NEGOTIATE_SEAL
2081                | smb_server_auth::ntlm::NEGOTIATE_KEY_EXCH;
2082            let fl_off = 60 - 8; // flags live at type2 offset 52 within msg
2083            // build_type2 writes flags right after the fixed header; patch
2084            // via known offset instead of plumbing a parameter:
2085            let fpos = 12 + 4 + 4; // sig(8)+type(4) => 12; then +4 challenge? keep simple below
2086            let _ = (fl_off, fpos);
2087            {
2088                // Flags field sits at byte 20 of the type2 message
2089                // (sig 8 + type 4 + domlen 2 + dommax 2 + domoff 4).
2090                const FLAGS_OFF: usize = 20;
2091                let cur = u32::from_le_bytes(t2[FLAGS_OFF..FLAGS_OFF + 4].try_into().unwrap());
2092                t2[FLAGS_OFF..FLAGS_OFF + 4].copy_from_slice(&(cur | extra).to_le_bytes());
2093            }
2094            let wrapped = if conn.raw_ntlm {
2095                t2
2096            } else {
2097                smb_server_auth::ntlm::wrap_negtoken_targ(&t2)
2098            };
2099            conn.ntlm_targ = Some(wrapped.clone());
2100            conn.ntlm_blobs = Some((req.blob.clone(), Vec::new()));
2101            Ok((
2102                Status::MORE_PROCESSING_REQUIRED,
2103                ss::build_response(0, &wrapped),
2104            ))
2105        }
2106        Some(smb_server_auth::ntlm::MSG_TYPE3) => {
2107            if let Some(blobs) = &mut conn.ntlm_blobs {
2108                blobs.1 = req.blob.clone();
2109            }
2110            if smb_server_auth::ntlm::parse_type3(inner).is_none() {
2111                tracing::warn!("session setup: malformed NTLMSSP type3");
2112                return Err(Status::INVALID_PARAMETER);
2113            }
2114            let t3 = smb_server_auth::ntlm::parse_type3(inner).unwrap();
2115            let out = crate::auth::authenticate_ntlmssp(
2116                &server.users,
2117                server.allow_guest,
2118                &conn.challenge,
2119                &t3,
2120            );
2121            if !out.ok {
2122                counter!("smb_server_auth_total", "outcome" => "fail").increment(1);
2123                return Err(Status::LOGON_FAILURE);
2124            }
2125            counter!("smb_server_auth_total", "outcome" => "ok").increment(1);
2126            gauge!("smb_sessions_active").increment(1.0);
2127            tracing::info!(user = %out.user, guest = out.guest,
2128                signed = out.session_key.is_some(), "session established");
2129            conn.user = out.user.clone();
2130            conn.guest = out.guest;
2131            // out.session_key is already the EXPORTED session key
2132            // ([MS-NLMP] §3.2.5.1.2): derive_session_key in auth.rs RC4-
2133            // decrypts EncryptedRandomSessionKey under the key-exchange
2134            // key when KEY_EXCH was negotiated. mechListMIC, SMB2 signing
2135            // and SMB3 cipher keys all derive from this value.
2136            conn.session_key = out.session_key;
2137            // Dialect-aware signing key ([MS-SMB2] §3.2.5.3.1 / §3.3.5.2.1).
2138            conn.authenticated = true;
2139            // Reauthentication re-runs NTLM on a live session ([MS-SMB2]
2140            // §3.3.5.5.2): the new exchange yields a fresh session key, so the
2141            // old signing and encryption keys are cleared and re-derived below.
2142            if reauth {
2143                conn.signing_key = None;
2144                conn.enc_keys = None;
2145            }
2146            // Per-client dialect consistency ([MS-SMB2] §3.3.5.5.3): a client
2147            // that authenticates under a ClientGuid already seen with a
2148            // different negotiated dialect has its new session closed with
2149            // STATUS_USER_SESSION_DELETED. Skipped for SMB 2.0.2 (no ClientGuid
2150            // table) and guest/anonymous sessions.
2151            if let Some(dialect) = conn
2152                .dialect
2153                .filter(|&d| d != smb_server_proto_smb2::negotiate::DIALECT_202)
2154            {
2155                use std::sync::LazyLock;
2156                static CLIENT_DIALECTS: LazyLock<
2157                    std::sync::Mutex<std::collections::HashMap<[u8; 16], u16>>,
2158                > = LazyLock::new(|| std::sync::Mutex::new(std::collections::HashMap::new()));
2159                let mut table = CLIENT_DIALECTS.lock().unwrap();
2160                match table.get(&conn.client_guid) {
2161                    Some(&seen) if seen != dialect => return Err(Status::USER_SESSION_DELETED),
2162                    Some(_) => {}
2163                    None => {
2164                        table.insert(conn.client_guid, dialect);
2165                    }
2166                }
2167            }
2168            // A bound channel derives its own Channel.SigningKey from *this*
2169            // authentication ([MS-SMB2] §3.3.5.5.3 step 9) and the SPNEGO
2170            // mechListMIC is computed with this exchange's session key, so
2171            // conn.session_key/signing_key must stay the freshly authenticated
2172            // values. Only the session-wide encryption material (keyed by the
2173            // original Session.SessionKey) is inherited from the session.
2174            if binding
2175                && let Some(entry) = server.sessions.get(conn.session_id) {
2176                    conn.cipher = entry.cipher;
2177                    conn.enc_keys = entry.enc_keys;
2178                    conn.encrypt_data = entry.encrypt_data;
2179                    conn.dialect = entry.dialect.or(conn.dialect);
2180                }
2181            // Attach to the session's shared scope (Session.TreeConnectTable +
2182            // OpenTable). A binding channel reuses the scope the first channel
2183            // created; a new session creates it ([MS-SMB2] §3.3.5.5.3).
2184            conn.scope = Some(crate::session_scope::get_or_create(conn.session_id));
2185            let flags: u16 = if out.guest { 0x0001 } else { 0x0000 };
2186            // Close the SPNEGO exchange. When the AUTHENTICATE message
2187            // carried a mechListMIC (required by --client-protection=encrypt
2188            // clients), echo one: an NTLMSSP signature over the client's
2189            // MechTypes DER, computed with the server signing key and RC4
2190            // checksum-sealed when KEY_EXCH was negotiated ([MS-NLMP]
2191            // §3.4.6, samba ntlmssp_make_packet_signature RECEIVE path).
2192            // A raw-NTLM exchange (no SPNEGO) has no mechListMIC/accept-complete
2193            // concept at all: the AUTHENTICATE is the last client message, and
2194            // the server's SUCCESS response carries no security buffer.
2195            const MIC_OFFSET: usize = 60;
2196            const MIC_SIZE: usize = 16;
2197            let mic_field_present = req.blob.len() > MIC_OFFSET + MIC_SIZE;
2198            let client_mic_nonzero = mic_field_present
2199                && req.blob[MIC_OFFSET..MIC_OFFSET + MIC_SIZE]
2200                    .iter()
2201                    .any(|&b| b != 0);
2202            let wrapped = if conn.raw_ntlm {
2203                Vec::new()
2204            } else if client_mic_nonzero && conn.session_key.is_some() {
2205                // A guest/anonymous session has no session key, so it cannot
2206                // echo a mechListMIC; it completes with a plain accept-complete
2207                // instead of failing ([MS-SMB2] §3.3.5.5: anonymous/guest
2208                // sessions are unsigned).
2209                let key = conn.session_key.unwrap();
2210                let init = conn
2211                    .ntlm_blobs
2212                    .as_ref()
2213                    .map(|b| b.0.clone())
2214                    .unwrap_or_default();
2215                // Locate MechTypes: smbclient's token nests as
2216                //   60{ OID, A0{ 30{ A0{ 30<OIDs> }, A2{ntlm} } } }
2217                // and samba signs exactly the bare `30<OIDs>` SEQUENCE
2218                // (spnego_write_mech_types: ASN1_SEQUENCE(0) of OIDs).
2219                // Walk: outer A0 (negTokenInit) -> its 30 SEQUENCE ->
2220                // first inner A0 (mechTypes wrapper) -> that 30 SEQUENCE.
2221                #[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB2 wire serialization
2222                fn der_len(b: &[u8], i: usize) -> Option<(usize, usize)> {
2223                    let n = *b.get(i + 1)?;
2224                    Some(match n {
2225                        v @ 0..=0x7F => (v as usize, 2),
2226                        0x81 => (*b.get(i + 2)? as usize, 3),
2227                        _ => return None,
2228                    })
2229                }
2230                let mech_types: Vec<u8> = (|| {
2231                    for a in 0..init.len().saturating_sub(4) {
2232                        if init[a] != 0xA0 {
2233                            continue;
2234                        }
2235                        // outer negTokenInit: content must start with 0x30
2236                        let (_alen, ahdr) = match der_len(&init, a) {
2237                            Some(v) => v,
2238                            None => continue,
2239                        };
2240                        let s30 = a + ahdr;
2241                        if init.get(s30) != Some(&0x30) {
2242                            continue;
2243                        }
2244                        let (slen, _) = match der_len(&init, s30) {
2245                            Some(v) => v,
2246                            None => continue,
2247                        };
2248                        let send = s30 + slen;
2249                        // first child inside the SEQUENCE: mechTypes [0]
2250                        for m in s30 + 2..send.saturating_sub(4) {
2251                            if init[m] != 0xA0 {
2252                                break; // first field only
2253                            }
2254                            let (_, mhdr) = match der_len(&init, m) {
2255                                Some(v) => v,
2256                                None => continue,
2257                            };
2258                            let t30 = m + mhdr;
2259                            if init.get(t30) == Some(&0x30) {
2260                                // sign exactly the bare `30 <len> <OIDs>`
2261                                let (ilen, ihdr) = match der_len(&init, t30) {
2262                                    Some(v) => v,
2263                                    None => continue,
2264                                };
2265                                let end = (t30 + ihdr + ilen).min(init.len());
2266                                if end > t30 {
2267                                    return init[t30..end].to_vec();
2268                                }
2269                            }
2270                        }
2271                    }
2272                    Vec::new()
2273                })();
2274                let key_exch = t3.encrypted_session_key.len() == 16;
2275                #[cfg(feature = "lib")]
2276                // NTLMSSP sequence counters are independent of SMB2
2277                // signing; samba resets them to 0 at sign_reset right
2278                // after auth, and the accept-complete MIC is the first
2279                // SEND (ntlmssp_make_packet_signature: sending.seq_num++).
2280                let mic =
2281                    smb_server_auth::crypto::ntlm_mech_list_mic(&key, true, key_exch, 0, &mech_types);
2282                #[cfg(not(feature = "lib"))]
2283                let mic = [0u8; 16];
2284                tracing::debug!(
2285                    sk = %hex_str(&key),
2286                    init = %hex_str(&init),
2287                    mech = %hex_str(&mech_types),
2288                    key_exch,
2289                    mic = %hex_str(&mic),
2290                    "mechListMIC inputs"
2291                );
2292                smb_server_auth::ntlm::wrap_accept_complete_with_mic(&mic)
2293            } else {
2294                smb_server_auth::ntlm::wrap_accept_complete()
2295            };
2296            Ok((Status::SUCCESS, ss::build_response(flags, &wrapped)))
2297        }
2298        _ => Err(Status::LOGON_FAILURE),
2299    }
2300}
2301
2302// ---------------- TREE_CONNECT ----------------
2303
2304#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB2 wire serialization
2305pub(crate) fn tree_connect(
2306    server: &Arc<ServerShared>,
2307    _conn: &mut Smb2Conn,
2308    buf: &[u8],
2309) -> Result<(String, u8, bool, bool, bool), Status> {
2310    let path_len = g16(buf, c::tcon_off::PATH_LENGTH) as usize;
2311    let path_off = g16(buf, c::tcon_off::PATH_OFFSET) as usize;
2312    let raw = buf.get(path_off..path_off + path_len).unwrap_or(&[]);
2313    let path = String::from_utf16_lossy(
2314        &raw.chunks_exact(2)
2315            .map(|p| u16::from_le_bytes([p[0], p[1]]))
2316            .collect::<Vec<_>>(),
2317    )
2318    .trim_end_matches('\0')
2319    .to_string();
2320    let name = path
2321        .rsplit(['/', '\\'])
2322        .find(|s| !s.is_empty())
2323        .unwrap_or("")
2324        .to_lowercase();
2325    let share = server.shares.get(&name).ok_or(Status::BAD_NETWORK_NAME)?;
2326    let share_type = if share.is_ipc {
2327        c::share_type::PIPE
2328    } else {
2329        c::share_type::DISK
2330    };
2331    Ok((name, share_type, share.encrypt, share.compress, share.ca))
2332}
2333
2334// ---------------- CREATE ----------------
2335
2336#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB2 wire serialization
2337pub(crate) async fn create(
2338    conn: &mut Smb2Conn,
2339    vfs: Arc<dyn smb_server_vfs::Vfs>,
2340    server: &Arc<ServerShared>,
2341    req_signed: bool,
2342    is_ca: bool,
2343    buf: &[u8],
2344) -> Result<Vec<u8>, Status> {
2345    let req = c::CreateReq::parse(buf).ok_or(Status::INVALID_PARAMETER)?;
2346
2347    const OPT_DIRECTORY_FILE: u32 = 0x1;
2348    const OPT_NON_DIRECTORY_FILE: u32 = 0x40;
2349    const OPT_DELETE_ON_CLOSE: u32 = 0x1000;
2350
2351    // Durable reconnect ([MS-SMB2] §3.3.5.9.7/§3.3.5.9.12): reclaim a preserved
2352    // handle instead of opening fresh.
2353    if let Some((id, guid)) = durable_reconnect_ids(&req.durable) {
2354        return durable_reconnect(conn, vfs, server, &req, id, guid).await;
2355    }
2356
2357    // Reject a path that walks upward from a real component ([MS-SMB2]
2358    // §3.3.5.9 — e.g. "x\..\y.txt"); bare "." / leading ".." are normalized.
2359    {
2360        let mut seen_real = false;
2361        for c in req.name.split(['\\', '/']).filter(|c| !c.is_empty()) {
2362            if c == ".." && seen_real {
2363                return Err(Status::INVALID_PARAMETER);
2364            }
2365            if c != "." && c != ".." {
2366                seen_real = true;
2367            }
2368        }
2369    }
2370    // FILE_DELETE_ON_CLOSE requires DELETE or GENERIC_ALL access ([MS-SMB2]
2371    // §3.3.5.9).
2372    if req.options & OPT_DELETE_ON_CLOSE != 0
2373        && req.desired_access & (0x0001_0000 | 0x1000_0000) == 0
2374    {
2375        return Err(Status::ACCESS_DENIED);
2376    }
2377
2378    let rel = req.name.trim_start_matches(['\\', '/']).to_string();
2379    let want_dir = req.options & OPT_DIRECTORY_FILE != 0;
2380    // A fresh open of a file that still has a surviving persistent handle is
2381    // rejected while that handle is held ([MS-SMB2] §3.3.5.9: Open.IsPersistent
2382    // is TRUE and the oplock is not batch).
2383    if let Ok(list) = server.durables.list().await
2384        && list
2385            .iter()
2386            .any(|r| r.path == rel && r.flags & c::durable::FLAG_PERSISTENT != 0)
2387        {
2388            return Err(Status::FILE_NOT_AVAILABLE);
2389        }
2390
2391    let (mut open, meta, action) = match vfs
2392        .create(
2393            &rel,
2394            want_dir,
2395            req.desired_access,
2396            req.disposition,
2397            req.options,
2398            req.attrs,
2399        )
2400        .await
2401    {
2402        Ok(v) => v,
2403        // Traversal hit a symlink: build the Symbolic Link Error Response from
2404        // the target and unparsed path ([MS-SMB2] §2.2.2.2.1) for the caller to
2405        // frame with STATUS_STOPPED_ON_SYMLINK.
2406        Err(smb_server_vfs::VfsError::StoppedOnSymlink {
2407            target,
2408            unparsed_len,
2409            relative,
2410        }) => {
2411            conn.symlink_error = Some(c::build_symlink_error_response(
2412                &target,
2413                &target,
2414                unparsed_len,
2415                relative,
2416            ));
2417            return Err(Status::STOPPED_ON_SYMLINK);
2418        }
2419        Err(e) => return Err(vfs_err(e)),
2420    };
2421
2422    if req.options & OPT_NON_DIRECTORY_FILE != 0 && open.is_dir {
2423        return Err(Status::FILE_IS_A_DIRECTORY);
2424    }
2425    open.delete_on_close |= req.options & OPT_DELETE_ON_CLOSE != 0;
2426
2427    let fid_bytes = next_file_id();
2428    let path = open.path.clone();
2429    let is_dir = open.is_dir;
2430    // Application-instance failover ([MS-SMB2] §3.3.5.9.13): a CREATE carrying
2431    // the same AppInstanceId from a different client force-closes the prior
2432    // open (or is rejected when the existing open's version is newer/equal).
2433    // Durable reconnect is handled earlier and skips this path.
2434    if let Some(app_id) = req.app_instance_id {
2435        let is_311 = conn.dialect == Some(smb_server_proto_smb2::negotiate::DIALECT_311);
2436        match server.app_instances.resolve(
2437            app_id,
2438            &path,
2439            conn.client_guid,
2440            is_311,
2441            req.app_instance_version,
2442        ) {
2443            crate::state::AppInstanceMatch::Reject => {
2444                let _ = vfs.close(open).await;
2445                return Err(Status::FILE_FORCED_CLOSED);
2446            }
2447            crate::state::AppInstanceMatch::ForceClose { path: vpath, owner } => {
2448                server.share_modes.close(&vpath, owner);
2449                server.oplocks.take(&vpath);
2450                server.locks.release_owner(owner);
2451                server.leases.release(&vpath, owner);
2452            }
2453            crate::state::AppInstanceMatch::None => {}
2454        }
2455    }
2456    // Sharing-violation check ([MS-FSA] §2.1.5.1): reject an open whose access
2457    // or share flags conflict with an existing open on the same path. On a
2458    // directory, an incompatible open first revokes HANDLE caching on any
2459    // directory lease ([MS-SMB2] §3.3.1.4). Undo the just-opened handle on
2460    // rejection.
2461    if !server.share_modes.try_open(
2462        &path,
2463        req.desired_access,
2464        req.share_access,
2465        (conn.session_id, fid_bytes),
2466    ) {
2467        if is_dir {
2468            break_dir_lease_wait(server, conn, &path, fid_bytes, c::lease::HANDLE_CACHING).await;
2469        }
2470        let _ = vfs.close(open).await;
2471        counter!("smb_sharing_violations_total").increment(1);
2472        return Err(Status::SHARING_VIOLATION);
2473    }
2474
2475    let fid = c::FileId(fid_bytes);
2476    conn.searches.remove(&fid_bytes);
2477    conn.handle_insert(fid_bytes, open);
2478    // Seed per-open channel-sequence replay state ([MS-SMB2] §3.3.5.2.10).
2479    if let Some(scope) = conn.scope.as_ref() {
2480        let channel_sequence = u16::from_le_bytes([buf[hdr::STATUS], buf[hdr::STATUS + 1]]);
2481        scope.borrow_mut().channel_seq.insert(
2482            fid_bytes,
2483            crate::session_scope::ChannelSeq {
2484                channel_sequence,
2485                outstanding_request_count: 1,
2486                outstanding_pre_request_count: 0,
2487            },
2488        );
2489    }
2490    // Record this handle so a related follow-up in the compound chain that
2491    // carries the wildcard FileId resolves to it ([MS-SMB2] §3.3.5.2.7.2).
2492    conn.chain_fid = Some(fid_bytes);
2493    if let Some(app_id) = req.app_instance_id {
2494        server.app_instances.register(
2495            app_id,
2496            path.clone(),
2497            conn.client_guid,
2498            (conn.session_id, fid_bytes),
2499            req.app_instance_version,
2500        );
2501    }
2502
2503    // A non-lease open conflicting with another holder's lease breaks its
2504    // write caching (RWH -> RH) ([MS-SMB2] §3.3.4.7); lease-context opens are
2505    // handled by arbitrate_lease below.
2506    if req.lease.is_none() && !is_dir {
2507        for (k, old, new, ep, out, crypto) in server.leases.break_conflict(
2508            &path,
2509            (conn.session_id, fid_bytes),
2510            None,
2511            c::lease::WRITE_CACHING,
2512        ) {
2513            send_lease_break(&out, &crypto, k, old, new, ep);
2514        }
2515    }
2516
2517    // Adding a new file or directory revokes READ caching on a directory lease
2518    // held on the parent ([MS-SMB2] §3.3.1.4). A directory lease supports only
2519    // {None, Read, Read+Handle}, so losing READ also drops HANDLE (to NONE).
2520    const FILE_ACTION_CREATED: u32 = 2;
2521    if action == FILE_ACTION_CREATED {
2522        break_dir_lease(server, conn, parent_dir(&path), fid_bytes, c::lease::RH);
2523    }
2524
2525    // Oplock arbitration ([MS-SMB2] §2.2.23): grant an exclusive oplock only
2526    // to the sole opener of a file. A contending open first breaks the current
2527    // holder to NONE, then gets no oplock itself. A LEASE request without an
2528    // RqLs context is treated as an ordinary oplock request ([MS-SMB2]
2529    // §3.3.5.9 "Oplock Acquisition": any non-NONE level acquires an oplock).
2530    let lease_context = req.oplock_level == c::oplock::LEASE && req.lease.is_some();
2531    let wants_oplock = matches!(
2532        req.oplock_level,
2533        c::oplock::LEVEL_II | c::oplock::EXCLUSIVE | c::oplock::BATCH
2534    ) || (req.oplock_level == c::oplock::LEASE && req.lease.is_none());
2535    // A lease request (RequestedOplockLevel = LEASE + an RqLs context) uses the
2536    // lease path; leases and oplocks are arbitrated independently.
2537    let mut lease_grant: Option<c::LeaseResp> = None;
2538    // Directory leasing ([MS-SMB2] §3.3.1.4) is an SMB 3.x feature; a directory
2539    // lease supports only READ + HANDLE caching.
2540    let dir_leasing = is_dir
2541        && conn
2542            .dialect
2543            .map(|d| d >= smb_server_proto_smb2::negotiate::DIALECT_300)
2544            .unwrap_or(false);
2545    let granted = if lease_context && (!is_dir || dir_leasing) {
2546        if let Some(lr) = req.lease {
2547            let (resp, ack_waits) = arbitrate_lease(
2548                server, conn, &path, fid_bytes, &lr, req_signed, is_dir,
2549            );
2550            lease_grant = Some(resp);
2551            conn.lease_keys.insert(fid_bytes, lr.key);
2552            // Block this conflicting create until the prior holders acknowledge
2553            // the write-caching break ([MS-SMB2] §3.3.1.4), or a short timeout.
2554            for rx in ack_waits {
2555                let _ = tokio::time::timeout(LEASE_ACK_TIMEOUT, rx).await;
2556            }
2557            c::oplock::LEASE
2558        } else {
2559            c::oplock::NONE
2560        }
2561    } else if is_dir {
2562        c::oplock::NONE
2563    } else if server.share_modes.open_count(&path) > 1 {
2564        if let Some(holder) = server.oplocks.take(&path) {
2565            send_oplock_break(&holder, c::oplock::NONE);
2566        }
2567        c::oplock::NONE
2568    } else if wants_oplock {
2569        let holder = crate::state::OplockHolder {
2570            session_id: conn.session_id,
2571            file_id: fid_bytes,
2572            outbound: conn.outbound.clone(),
2573            crypto: break_crypto(conn, req_signed),
2574        };
2575        if server.oplocks.grant(&path, holder) {
2576            counter!("smb_oplocks_granted_total").increment(1);
2577            c::oplock::EXCLUSIVE
2578        } else {
2579            c::oplock::NONE
2580        }
2581    } else {
2582        c::oplock::NONE
2583    };
2584
2585    tracing::debug!(
2586        name = %rel, eof = meta.eof, dir = meta.is_dir, action,
2587        oplock = granted, lease = ?lease_grant.map(|l| l.state), "create"
2588    );
2589    counter!("smb_creates_total").increment(1);
2590
2591    // Grant a durable handle when the client requested one. Persistence is
2592    // granted only on a continuously-available share ([MS-SMB2] §3.3.5.9.11);
2593    // a lease present must include handle caching for durable.
2594    let durable_grant = grant_durable(
2595        conn,
2596        &req,
2597        fid_bytes,
2598        &rel,
2599        is_dir,
2600        lease_grant.as_ref().map(|l| l.state),
2601        is_ca,
2602    );
2603
2604    // Assemble create-context responses (lease grant, durable-handle grant).
2605    let mut ctx_entries: Vec<(&[u8], Vec<u8>)> = Vec::new();
2606    if let Some(l) = &lease_grant {
2607        ctx_entries.push((c::lease::CONTEXT_NAME, c::lease_context_data(l)));
2608    }
2609    if let Some(d) = &durable_grant {
2610        ctx_entries.push((d.0, d.1.clone()));
2611    }
2612    let contexts = c::encode_create_contexts(&ctx_entries);
2613    Ok(c::build_create_resp(
2614        fid,
2615        action,
2616        [
2617            meta.times[0].0,
2618            meta.times[1].0,
2619            meta.times[2].0,
2620            meta.times[3].0,
2621        ],
2622        meta.attrs.0,
2623        meta.alloc,
2624        meta.eof,
2625        meta.is_dir,
2626        granted,
2627        &contexts,
2628    ))
2629}
2630
2631/// Extract `(persistent_id, create_guid)` when a CREATE carries a durable
2632/// reconnect context.
2633fn durable_reconnect_ids(d: &Option<c::DurableReq>) -> Option<([u8; 16], Option<[u8; 16]>)> {
2634    match d {
2635        Some(c::DurableReq::ReconnectV2 {
2636            file_id,
2637            create_guid,
2638            ..
2639        }) => Some((*file_id, Some(*create_guid))),
2640        Some(c::DurableReq::ReconnectV1 { file_id }) => Some((*file_id, None)),
2641        _ => None,
2642    }
2643}
2644
2645/// Reclaim a preserved durable handle and re-open the file under its persistent
2646/// id (single-node durable model: the on-disk file, not a live fd, is durable).
2647#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB2 wire serialization
2648async fn durable_reconnect(
2649    conn: &mut Smb2Conn,
2650    vfs: Arc<dyn smb_server_vfs::Vfs>,
2651    server: &Arc<ServerShared>,
2652    req: &c::CreateReq,
2653    id: [u8; 16],
2654    guid: Option<[u8; 16]>,
2655) -> Result<Vec<u8>, Status> {
2656    // A reconnect must not be combined with a conflicting durable context
2657    // ([MS-SMB2] §3.3.5.9.7/§3.3.5.9.12). A v1 reconnect (DHnC) rejects any V2
2658    // durable context with STATUS_INVALID_PARAMETER; a v2 reconnect (DH2C)
2659    // rejects any other durable context with STATUS_OBJECT_NAME_NOT_FOUND.
2660    let tags = req.durable_ctx_tags;
2661    if guid.is_none() {
2662        if tags & (c::durable::tag::REQ_V2 | c::durable::tag::RECONNECT_V2) != 0 {
2663            return Err(Status::INVALID_PARAMETER);
2664        }
2665    } else if tags
2666        & (c::durable::tag::REQ_V1 | c::durable::tag::RECONNECT_V1 | c::durable::tag::REQ_V2)
2667        != 0
2668    {
2669        return Err(Status::OBJECT_NAME_NOT_FOUND);
2670    }
2671
2672    // Resolve the preserved handle: by its persistent FileId first, then — for
2673    // a v2 reconnect that requests persistence — by CreateGuid ([MS-SMB2]
2674    // §3.3.5.9.12 step 3).
2675    let reconnect_persistent = matches!(
2676        req.durable,
2677        Some(c::DurableReq::ReconnectV2 { flags, .. }) if flags & c::durable::FLAG_PERSISTENT != 0
2678    );
2679    let mut key = id;
2680    let mut via_create_guid = false;
2681    if server.durables.get(&key).await.ok().flatten().is_none()
2682        && reconnect_persistent
2683            && let Some(g) = guid
2684                && let Ok(list) = server.durables.list().await
2685                    && let Some(rec) = list.into_iter().find(|r| r.match_guid == Some(g)) {
2686                        key = rec.create_guid;
2687                        via_create_guid = true;
2688                    }
2689
2690    // Peek the preserved record to validate the reconnect against the original
2691    // open's lease and client before consuming it.
2692    let peeked = server
2693        .durables
2694        .get(&key)
2695        .await
2696        .ok()
2697        .flatten()
2698        .ok_or(Status::OBJECT_NAME_NOT_FOUND)?;
2699    if guid.is_some() && peeked.match_guid != guid {
2700        return Err(Status::OBJECT_NAME_NOT_FOUND);
2701    }
2702    // When resolved by FileId (§3.3.5.9.12 step 2), a persistent open reconnected
2703    // without the persistent flag is rejected, and the reconnect must present a
2704    // lease context when the original held a lease. The CreateGuid path (step 3)
2705    // skips that null-lease check per the section's note (the lease is recreated
2706    // on resume). A lease *key* mismatch is rejected on both paths.
2707    if !via_create_guid {
2708        if peeked.flags & c::durable::FLAG_PERSISTENT != 0 && !reconnect_persistent {
2709            return Err(Status::OBJECT_NAME_NOT_FOUND);
2710        }
2711        if let Some(_orig_key) = peeked.lease_key {
2712            if req.name.trim_start_matches(['\\', '/']) != peeked.path {
2713                return Err(if guid.is_some() {
2714                    Status::OBJECT_NAME_NOT_FOUND
2715                } else {
2716                    Status::INVALID_PARAMETER
2717                });
2718            }
2719            if peeked.client_guid != conn.client_guid {
2720                return Err(Status::OBJECT_NAME_NOT_FOUND);
2721            }
2722            if req.lease.is_none() {
2723                return Err(Status::OBJECT_NAME_NOT_FOUND);
2724            }
2725        }
2726    }
2727    // A reconnect lease context, when present, must carry the original lease key
2728    // ([MS-SMB2] §3.3.5.9.12 rules 10/11 and 3.2.4/3.2.5).
2729    if let (Some(l), Some(orig_key)) = (&req.lease, peeked.lease_key)
2730        && l.key != orig_key {
2731            return Err(Status::OBJECT_NAME_NOT_FOUND);
2732        }
2733
2734    // Durable-owner check ([MS-SMB2] §3.3.5.9.7 rule 17): a reconnect by a user
2735    // other than the one that created the handle fails with STATUS_ACCESS_DENIED
2736    // and leaves the preserved handle intact for its real owner.
2737    if !peeked.owner_user.eq_ignore_ascii_case(&conn.user) {
2738        return Err(Status::ACCESS_DENIED);
2739    }
2740
2741    let record = server
2742        .durables
2743        .take(&key, guid, crate::state::now_ms())
2744        .await
2745        .ok()
2746        .flatten()
2747        .ok_or(Status::OBJECT_NAME_NOT_FOUND)?;
2748    let persistent = record.flags & c::durable::FLAG_PERSISTENT != 0;
2749    let timeout = record.timeout_ms as u32;
2750    // FILE_OPEN (disposition 1): the file already exists.
2751    let (mut open, meta, _action) = vfs
2752        .create(
2753            &record.path,
2754            record.is_dir,
2755            record.access,
2756            1,
2757            record.create_options,
2758            0,
2759        )
2760        .await
2761        .map_err(vfs_err)?;
2762    open.delete_on_close = false;
2763    conn.handle_insert(id, open);
2764    conn.durable.insert(
2765        id,
2766        crate::state::DurableEntry {
2767            persistent_id: id,
2768            create_guid: record.match_guid.unwrap_or([0u8; 16]),
2769            rel: record.path.clone(),
2770            is_dir: record.is_dir,
2771            access: record.access,
2772            options: record.create_options,
2773            session_id: conn.session_id,
2774            owner_user: record.owner_user.clone(),
2775            client_guid: record.client_guid,
2776            lease_key: record.lease_key,
2777            lease_state: record.lease_state,
2778            persistent,
2779            timeout,
2780            deadline: std::time::Instant::now(),
2781        },
2782    );
2783    counter!("smb_durable_reconnects_total").increment(1);
2784
2785    let (name, data): (&[u8], Vec<u8>) = if guid.is_some() {
2786        let flags = if persistent {
2787            c::durable::FLAG_PERSISTENT
2788        } else {
2789            0
2790        };
2791        (c::durable::REQ_V2, c::durable_v2_resp_data(timeout, flags))
2792    } else {
2793        (c::durable::REQ_V1, c::durable_v1_resp_data())
2794    };
2795    let mut ctx_list: Vec<(&[u8], Vec<u8>)> = vec![(name, data)];
2796    // A persistent handle that held a lease recreates it on resume, returning
2797    // the lease create-context ([MS-SMB2] §3.3.5.9.12).
2798    if let Some(lease_key) = record.lease_key {
2799        let grant = c::LeaseResp {
2800            key: lease_key,
2801            state: record.lease_state,
2802            flags: 0,
2803            epoch: 0,
2804            v2: false,
2805        };
2806        ctx_list.push((c::lease::CONTEXT_NAME, c::lease_context_data(&grant)));
2807    }
2808    let contexts = c::encode_create_contexts(&ctx_list);
2809    Ok(c::build_create_resp(
2810        c::FileId(id),
2811        1, // FILE_OPENED
2812        [
2813            meta.times[0].0,
2814            meta.times[1].0,
2815            meta.times[2].0,
2816            meta.times[3].0,
2817        ],
2818        meta.attrs.0,
2819        meta.alloc,
2820        meta.eof,
2821        meta.is_dir,
2822        c::oplock::NONE,
2823        &contexts,
2824    ))
2825}
2826
2827/// Record a durable-handle grant on the connection and return the response
2828/// create-context to echo. Directories and one-shot deletes are ineligible.
2829/// A durable open also requires a batch oplock or a handle-caching lease, so
2830/// `lease_state` (the granted lease bits, if any) gates the grant; persistence
2831/// is only granted on a continuous-availability share (`is_ca`).
2832fn grant_durable(
2833    conn: &mut Smb2Conn,
2834    req: &c::CreateReq,
2835    fid: [u8; 16],
2836    rel: &str,
2837    is_dir: bool,
2838    lease_state: Option<u32>,
2839    is_ca: bool,
2840) -> Option<(&'static [u8], Vec<u8>)> {
2841    // A durable open requires a batch oplock or a handle-caching lease
2842    // ([MS-SMB2] §3.3.5.9.6/§3.3.5.9.10), except a persistent handle on a
2843    // continuous-availability share, which is granted regardless.
2844    let persistent_eligible = is_ca
2845        && matches!(req.durable, Some(c::DurableReq::RequestV2 { flags, .. }) if flags & c::durable::FLAG_PERSISTENT != 0);
2846    if !persistent_eligible {
2847        match lease_state {
2848            Some(state) if state & c::lease::HANDLE_CACHING != 0 => {}
2849            None if req.oplock_level == c::oplock::BATCH => {}
2850            _ => return None,
2851        }
2852    }
2853    /// Fallback handle timeout when the client requests 0 (60 s).
2854    const DEFAULT_TIMEOUT_MS: u32 = 60_000;
2855    let make = |create_guid: [u8; 16], persistent: bool, timeout: u32| crate::state::DurableEntry {
2856        persistent_id: fid,
2857        create_guid,
2858        rel: rel.to_string(),
2859        is_dir,
2860        access: req.desired_access,
2861        options: req.options,
2862        session_id: conn.session_id,
2863        owner_user: conn.user.clone(),
2864        client_guid: conn.client_guid,
2865        lease_key: req.lease.as_ref().map(|l| l.key),
2866        lease_state: lease_state.unwrap_or(0),
2867        persistent,
2868        timeout,
2869        deadline: std::time::Instant::now(),
2870    };
2871    match req.durable {
2872        Some(c::DurableReq::RequestV2 {
2873            timeout,
2874            flags,
2875            create_guid,
2876        }) => {
2877            // Persistence requires a CA share ([MS-SMB2] §3.3.5.9.11); otherwise
2878            // the persistent bit is ignored and a plain durable handle granted.
2879            let persistent = is_ca && flags & c::durable::FLAG_PERSISTENT != 0;
2880            let to = if timeout == 0 {
2881                DEFAULT_TIMEOUT_MS
2882            } else {
2883                timeout
2884            };
2885            conn.durable.insert(fid, make(create_guid, persistent, to));
2886            counter!("smb_durables_granted_total").increment(1);
2887            Some((
2888                c::durable::REQ_V2,
2889                c::durable_v2_resp_data(
2890                    to,
2891                    if persistent {
2892                        c::durable::FLAG_PERSISTENT
2893                    } else {
2894                        0
2895                    },
2896                ),
2897            ))
2898        }
2899        Some(c::DurableReq::RequestV1) => {
2900            conn.durable
2901                .insert(fid, make([0u8; 16], false, DEFAULT_TIMEOUT_MS));
2902            counter!("smb_durables_granted_total").increment(1);
2903            Some((c::durable::REQ_V1, c::durable_v1_resp_data()))
2904        }
2905        _ => None,
2906    }
2907}
2908
2909/// Arbitrate a caching lease for a lease-requesting CREATE ([MS-SMB2]
2910/// §2.2.13.2). A sole opener gets the requested state (capped at RWH); an open
2911/// reusing the same lease key shares the existing state; READ and HANDLE
2912/// caching are shareable, so directory holders with distinct keys coexist,
2913/// whereas a contending file open strips write caching from existing holders
2914/// (RWH -> RH) via unsolicited LEASE_BREAKs and is itself capped at RH.
2915fn arbitrate_lease(
2916    server: &Arc<ServerShared>,
2917    conn: &Smb2Conn,
2918    path: &str,
2919    fid: [u8; 16],
2920    lr: &c::LeaseReq,
2921    signed: bool,
2922    is_dir: bool,
2923) -> (c::LeaseResp, Vec<tokio::sync::oneshot::Receiver<()>>) {
2924    // A directory lease supports only READ + HANDLE caching; a file lease also
2925    // supports WRITE caching ([MS-SMB2] §3.3.1.4).
2926    let allowed = if is_dir { c::lease::RH } else { c::lease::RWH };
2927    let requested = lr.state & allowed;
2928    // A CREATE reusing an existing lease key shares that holder's caching state
2929    // ([MS-SMB2] §3.3.5.9.11).
2930    if let Some((state, epoch, v2)) = server.leases.peek_key(path, lr.key) {
2931        return (
2932            c::LeaseResp {
2933                key: lr.key,
2934                state,
2935                flags: 0,
2936                epoch,
2937                v2,
2938            },
2939            Vec::new(),
2940        );
2941    }
2942    // Other holders already cache this path. WRITE caching is exclusive, so a
2943    // contending file open strips it from every existing holder (RWH -> RH) and
2944    // blocks on their acknowledgments; READ/HANDLE caching is shareable, so
2945    // directory co-holders are simply added ([MS-SMB2] §3.3.1.4/§3.3.4.7).
2946    let contended = server.leases.has_holders(path);
2947    let mut ack_waits = Vec::new();
2948    if !is_dir {
2949        for (hkey, old, new, ep, out, crypto) in server.leases.break_conflict(
2950            path,
2951            (conn.session_id, fid),
2952            Some(lr.key),
2953            c::lease::WRITE_CACHING,
2954        ) {
2955            let rx = server.leases.register_ack_wait(hkey);
2956            send_lease_break(&out, &crypto, hkey, old, new, ep);
2957            ack_waits.push(rx);
2958        }
2959    }
2960    // A file lease contending with an existing holder is capped at READ+HANDLE
2961    // (WRITE is exclusive); a directory lease and a sole opener keep the full
2962    // requested caching.
2963    let grant_state = if contended && !is_dir {
2964        requested & c::lease::RH
2965    } else {
2966        requested
2967    };
2968    // A newly established lease initializes its epoch from the request and
2969    // increments it by one for a LeaseV2 grant ([MS-SMB2] §3.3.5.9.11); a file
2970    // co-holder capped to RH keeps the requested epoch (matching the write
2971    // holder it displaced); V1 leases have no epoch (reported as zero).
2972    let granted_epoch = if !lr.v2 {
2973        0
2974    } else if contended && !is_dir {
2975        lr.epoch
2976    } else {
2977        lr.epoch.wrapping_add(1)
2978    };
2979    let holder = crate::state::LeaseHolder {
2980        key: lr.key,
2981        state: grant_state,
2982        epoch: granted_epoch,
2983        v2: lr.v2,
2984        session_id: conn.session_id,
2985        file_id: fid,
2986        outbound: conn.outbound.clone(),
2987        crypto: break_crypto(conn, signed),
2988        client_guid: conn.client_guid,
2989        breaking: false,
2990        break_to: grant_state,
2991    };
2992    server.leases.grant(path, holder);
2993    counter!("smb_leases_granted_total").increment(1);
2994    (
2995        c::LeaseResp {
2996            key: lr.key,
2997            state: grant_state,
2998            flags: 0,
2999            epoch: granted_epoch,
3000            v2: lr.v2,
3001        },
3002        ack_waits,
3003    )
3004}
3005
3006/// Build a NETWORK_INTERFACE_INFO list ([MS-SMB2] §2.2.32.5) from the host's
3007/// interfaces so a multichannel client can discover additional server IPs.
3008fn network_interface_info() -> Vec<u8> {
3009    let mut entries: Vec<(u32, std::net::IpAddr)> = Vec::new();
3010    if let Ok(ifaces) = if_addrs::get_if_addrs() {
3011        for (i, iface) in ifaces.iter().enumerate() {
3012            entries.push((i as u32 + 1, iface.ip()));
3013        }
3014    }
3015    build_interface_list(&entries)
3016}
3017
3018/// Encode NETWORK_INTERFACE_INFO entries (RSS/RDMA both disabled, 10 Gbps),
3019/// chaining `Next` links; each entry is a fixed 152 bytes (8-byte aligned).
3020#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB2 wire serialization
3021fn build_interface_list(entries: &[(u32, std::net::IpAddr)]) -> Vec<u8> {
3022    const LINK_SPEED: u64 = 10_000_000_000;
3023    let mut out = Vec::new();
3024    for (idx, (ifindex, ip)) in entries.iter().enumerate() {
3025        let start = out.len();
3026        out.extend_from_slice(&0u32.to_le_bytes()); // Next (patched below)
3027        out.extend_from_slice(&ifindex.to_le_bytes()); // IfIndex
3028        out.extend_from_slice(&0u32.to_le_bytes()); // Capability (no RSS/RDMA)
3029        out.extend_from_slice(&0u32.to_le_bytes()); // Reserved
3030        out.extend_from_slice(&LINK_SPEED.to_le_bytes()); // LinkSpeed
3031        let mut sa = [0u8; 128]; // SOCKADDR_STORAGE
3032        match ip {
3033            std::net::IpAddr::V4(v4) => {
3034                sa[0..2].copy_from_slice(&2u16.to_le_bytes()); // AF_INET
3035                sa[4..8].copy_from_slice(&v4.octets()); // sin_addr
3036            }
3037            std::net::IpAddr::V6(v6) => {
3038                sa[0..2].copy_from_slice(&23u16.to_le_bytes()); // AF_INET6
3039                sa[8..24].copy_from_slice(&v6.octets()); // sin6_addr
3040            }
3041        }
3042        out.extend_from_slice(&sa);
3043        if idx + 1 < entries.len() {
3044            let next = (out.len() - start) as u32;
3045            out[start..start + 4].copy_from_slice(&next.to_le_bytes());
3046        }
3047    }
3048    out
3049}
3050
3051/// Build, protect and enqueue an unsolicited LEASE_BREAK notification
3052/// ([MS-SMB2] §2.2.23.2) asking a holder to drop from `current` to `new`.
3053fn send_lease_break(
3054    outbound: &mpsc::Sender<Vec<u8>>,
3055    crypto: &crate::state::BreakCrypto,
3056    key: [u8; 16],
3057    current: u32,
3058    new: u32,
3059    epoch: u16,
3060) {
3061    let body = c::build_lease_break(key, current, new, epoch, c::lease::BREAK_FLAG_ACK_REQUIRED);
3062    let frame = finalize_break(crypto, build_break_frame(&body));
3063    if outbound.try_send(frame).is_ok() {
3064        counter!("smb_lease_breaks_total").increment(1);
3065    }
3066}
3067
3068/// Revoke caching bits (`clear`) on a directory lease held on `dir` when its
3069/// contents or state change, unless the change originates from the lease holder
3070/// itself ([MS-SMB2] §3.3.1.4). `owner` is the file id of the open performing
3071/// the change, used to suppress a self-break.
3072pub(crate) fn break_dir_lease(
3073    server: &Arc<ServerShared>,
3074    conn: &Smb2Conn,
3075    dir: &str,
3076    owner: [u8; 16],
3077    clear: u32,
3078) {
3079    let req_key = conn.lease_keys.get(&owner).copied();
3080    for (key, old, new, epoch, outbound, crypto) in
3081        server
3082            .leases
3083            .break_conflict(dir, (conn.session_id, owner), req_key, clear)
3084    {
3085        send_lease_break(&outbound, &crypto, key, old, new, epoch);
3086    }
3087}
3088
3089/// Break a directory lease and wait (briefly) for the holder to acknowledge,
3090/// as a HANDLE-caching break requires before the conflicting operation may
3091/// proceed ([MS-SMB2] §3.3.1.4).
3092pub(crate) async fn break_dir_lease_wait(
3093    server: &Arc<ServerShared>,
3094    conn: &Smb2Conn,
3095    dir: &str,
3096    owner: [u8; 16],
3097    clear: u32,
3098) {
3099    let req_key = conn.lease_keys.get(&owner).copied();
3100    let mut waits = Vec::new();
3101    for (key, old, new, epoch, outbound, crypto) in
3102        server
3103            .leases
3104            .break_conflict(dir, (conn.session_id, owner), req_key, clear)
3105    {
3106        let rx = server.leases.register_ack_wait(key);
3107        send_lease_break(&outbound, &crypto, key, old, new, epoch);
3108        waits.push(rx);
3109    }
3110    for rx in waits {
3111        let _ = tokio::time::timeout(LEASE_ACK_TIMEOUT, rx).await;
3112    }
3113}
3114
3115/// Break HANDLE caching on every directory lease across the subtree rooted at
3116/// `dir` and wait (briefly) for each holder to acknowledge, as required before
3117/// a directory rename that invalidates the subtree's cached handles may be
3118/// evaluated ([MS-SMB2] §3.3.1.4). `owner` is the renaming open, suppressed
3119/// from the break.
3120pub(crate) async fn break_subtree_lease_wait(
3121    server: &Arc<ServerShared>,
3122    conn: &Smb2Conn,
3123    dir: &str,
3124    owner: [u8; 16],
3125    clear: u32,
3126) {
3127    let mut waits = Vec::new();
3128    for (key, old, new, epoch, outbound, crypto) in
3129        server
3130            .leases
3131            .break_subtree(dir, (conn.session_id, owner), clear)
3132    {
3133        let rx = server.leases.register_ack_wait(key);
3134        send_lease_break(&outbound, &crypto, key, old, new, epoch);
3135        waits.push(rx);
3136    }
3137    for rx in waits {
3138        let _ = tokio::time::timeout(LEASE_ACK_TIMEOUT, rx).await;
3139    }
3140}
3141
3142/// The parent directory portion of a normalized share-relative path, or the
3143/// share root (`""`) when the path has no parent component.
3144pub(crate) fn parent_dir(path: &str) -> &str {
3145    path.rsplit_once(['/', '\\']).map(|(p, _)| p).unwrap_or("")
3146}
3147
3148/// Snapshot the crypto material needed to protect an oplock break sent later
3149/// to this holder.
3150fn break_crypto(conn: &Smb2Conn, signed: bool) -> crate::state::BreakCrypto {
3151    crate::state::BreakCrypto {
3152        dialect: conn.dialect,
3153        signing_key: conn.signing_key.or(conn.session_key),
3154        enc_keys: conn.enc_keys,
3155        cipher: conn.cipher,
3156        session_id: conn.session_id,
3157        encrypt: conn.encrypt_data || conn.peer_encrypts,
3158        signed,
3159    }
3160}
3161
3162/// Build, protect and enqueue an unsolicited OPLOCK_BREAK notification
3163/// ([MS-SMB2] §2.2.23.1) telling `holder` to break down to `level`.
3164fn send_oplock_break(holder: &crate::state::OplockHolder, level: u8) {
3165    let body = c::build_oplock_break(c::FileId(holder.file_id), level);
3166    let frame = finalize_break(&holder.crypto, build_break_frame(&body));
3167    if holder.outbound.try_send(frame).is_ok() {
3168        counter!("smb_oplock_breaks_total").increment(1);
3169    }
3170}
3171
3172/// Build a server-initiated OPLOCK_BREAK frame (unsolicited: MessageId all-ones,
3173/// sync header). Returned unsigned and unsealed.
3174fn build_break_frame(body: &[u8]) -> Vec<u8> {
3175    let mut f = Vec::with_capacity(hdr::LEN + body.len());
3176    f.extend_from_slice(&smb_server_proto_smb2::SMB2_MAGIC);
3177    f.extend_from_slice(&(hdr::LEN as u16).to_le_bytes()); // StructureSize
3178    f.extend_from_slice(&0u16.to_le_bytes()); // CreditCharge
3179    f.extend_from_slice(&0u32.to_le_bytes()); // Status
3180    f.extend_from_slice(&ss::cmd::OPLOCK_BREAK.to_le_bytes());
3181    f.extend_from_slice(&0u16.to_le_bytes()); // CreditResponse
3182    f.extend_from_slice(&hdr_flags::SERVER_TO_REDIR.to_le_bytes()); // Flags
3183    f.extend_from_slice(&0u32.to_le_bytes()); // NextCommand
3184    f.extend_from_slice(&u64::MAX.to_le_bytes()); // MessageId (unsolicited)
3185    f.extend_from_slice(&0u32.to_le_bytes()); // Reserved
3186    f.extend_from_slice(&0u32.to_le_bytes()); // TreeId
3187    // Break notifications carry SessionId 0 ([MS-SMB2] §3.3.4.6/§3.3.4.7).
3188    f.extend_from_slice(&0u64.to_le_bytes());
3189    f.extend_from_slice(&[0u8; 16]); // Signature
3190    f.extend_from_slice(body);
3191    f
3192}
3193
3194/// Seal an oplock/lease break for its holder. Break notifications carry
3195/// SessionId 0 and are sent unsigned ([MS-SMB2] §3.3.4.6); encrypted sessions
3196/// still wrap them in a transform header.
3197fn finalize_break(crypto: &crate::state::BreakCrypto, frame: Vec<u8>) -> Vec<u8> {
3198    if crypto.encrypt
3199        && let (Some(keys), Some(cipher)) = (crypto.enc_keys, crypto.cipher)
3200            && let Some(sealed) = seal_pdu(crypto.session_id, keys, cipher, &frame) {
3201                return sealed;
3202            }
3203    frame
3204}
3205
3206// ---------------- READ / WRITE / CLOSE / FLUSH ----------------
3207
3208#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB2 wire serialization
3209pub(crate) async fn read(
3210    conn: &mut Smb2Conn,
3211    vfs: Arc<dyn smb_server_vfs::Vfs>,
3212    server: &Arc<ServerShared>,
3213    buf: &[u8],
3214) -> Result<Vec<u8>, Status> {
3215    let req = c::ReadReq::parse(buf).ok_or(Status::INVALID_PARAMETER)?;
3216    let len = (req.length as usize).min(1 << 20); // clamp to 1 MiB
3217    let (path, is_dir, can_read) = conn
3218        .with_handle(&req.file_id.0, |h| (h.path.clone(), h.is_dir, h.can_read))
3219        .ok_or(Status::FILE_CLOSED)?;
3220    if is_dir || !can_read {
3221        return Err(Status::ACCESS_DENIED);
3222    }
3223    // An exclusive byte-range lock from another open blocks reads ([MS-FSA]).
3224    if server.locks.read_conflict(
3225        &path,
3226        req.offset,
3227        len as u64,
3228        (conn.session_id, req.file_id.0),
3229    ) {
3230        return Err(Status::FILE_LOCK_CONFLICT);
3231    }
3232    // Check the open out of the shared table for the async read so no RefCell
3233    // borrow spans the await, then check it back in ([MS-SMB2] §3.3.5.5.3).
3234    let mut open = conn
3235        .handle_take(&req.file_id.0)
3236        .ok_or(Status::FILE_CLOSED)?;
3237    let result = vfs.read(&mut open, req.offset, len).await;
3238    conn.handle_insert(req.file_id.0, open);
3239    let data = result.map_err(vfs_err)?;
3240    counter!("smb_bytes_read_total").increment(data.len() as u64);
3241    tracing::trace!(offset = req.offset, got = data.len(), "read");
3242    Ok(c::build_read_resp(&data))
3243}
3244
3245pub(crate) async fn write(
3246    conn: &mut Smb2Conn,
3247    vfs: Arc<dyn smb_server_vfs::Vfs>,
3248    server: &Arc<ServerShared>,
3249    buf: &[u8],
3250) -> Result<Vec<u8>, Status> {
3251    let req = c::WriteReq::parse(buf).ok_or(Status::INVALID_PARAMETER)?;
3252    let (path, is_dir, can_write) = conn
3253        .with_handle(&req.file_id.0, |h| (h.path.clone(), h.is_dir, h.can_write))
3254        .ok_or(Status::FILE_CLOSED)?;
3255    if is_dir || !can_write {
3256        return Err(Status::ACCESS_DENIED);
3257    }
3258    // Byte-range lock enforcement ([MS-SMB2] §2.2.26 / [MS-FSA]).
3259    if server.locks.write_conflict(
3260        &path,
3261        req.offset,
3262        req.payload.len() as u64,
3263        (conn.session_id, req.file_id.0),
3264    ) {
3265        return Err(Status::FILE_LOCK_CONFLICT);
3266    }
3267    let mut open = conn
3268        .handle_take(&req.file_id.0)
3269        .ok_or(Status::FILE_CLOSED)?;
3270    let result = vfs.write(&mut open, req.offset, &req.payload, false).await;
3271    conn.handle_insert(req.file_id.0, open);
3272    let written = result.map_err(vfs_err)?;
3273    counter!("smb_bytes_written_total").increment(written);
3274    tracing::trace!(offset = req.offset, wrote = written, "write");
3275    // A write from a non-holder invalidates cached reads: break the lease to
3276    // NONE ([MS-SMB2] §3.3.4.7). A co-holder of the same lease key is exempt.
3277    let req_key = conn.lease_keys.get(&req.file_id.0).copied();
3278    for (k, old, new, ep, out, crypto) in server.leases.break_conflict(
3279        &path,
3280        (conn.session_id, req.file_id.0),
3281        req_key,
3282        c::lease::RWH,
3283    ) {
3284        send_lease_break(&out, &crypto, k, old, new, ep);
3285    }
3286    // Modifying a child updates directory metadata: revoke READ caching on any
3287    // directory lease held on the parent ([MS-SMB2] §3.3.1.4).
3288    break_dir_lease(server, conn, parent_dir(&path), req.file_id.0, c::lease::RH);
3289    Ok(c::build_write_resp(written as u32))
3290}
3291
3292/// The outcome of an IOCTL: either a reply body to encode, or silence because
3293/// the connection is being terminated ([MS-SMB2] §3.3.5.15.12).
3294pub(crate) enum IoctlReply {
3295    Reply(Status, Vec<u8>),
3296    Silent,
3297}
3298
3299/// IOCTL / FSCTL dispatch ([MS-SMB2] §3.3.5.15). Kept as a free function so the
3300/// typestate handler and its private FSCTL helpers all live in this module.
3301#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB2 wire serialization
3302pub(crate) async fn ioctl(
3303    conn: &mut Smb2Conn,
3304    server: &Arc<ServerShared>,
3305    tid: u32,
3306    buf: &[u8],
3307) -> IoctlReply {
3308    let Some(req) = c::IoctlReq::parse(buf) else {
3309        tracing::debug!("ioctl parse failed");
3310        return IoctlReply::Reply(Status::INVALID_PARAMETER, Vec::new());
3311    };
3312    tracing::debug!(ctl = format!("{:#010x}", req.ctl_code), "ioctl");
3313    let (status, body) = match req.ctl_code {
3314        // Echo the negotiated parameters back ([MS-SMB2] §3.3.5.15):
3315        // capabilities, server GUID, security mode and dialect.
3316        // Validate against the original NEGOTIATE and terminate the
3317        // connection on any mismatch — a downgrade attempt ([MS-SMB2]
3318        // §3.3.5.15.12). The request is invalid on 3.1.1, where
3319        // pre-auth integrity supersedes this exchange.
3320        c::fsctl::VALIDATE_NEGOTIATE_INFO => {
3321            let input = &req.input;
3322            let terminate = 'v: {
3323                if conn.dialect == Some(smb_server_proto_smb2::negotiate::DIALECT_311) {
3324                    break 'v true;
3325                }
3326                if input.len() < 24 || (req.max_output as usize) < 24 {
3327                    break 'v true;
3328                }
3329                let req_caps = u32::from_le_bytes(input[0..4].try_into().unwrap());
3330                if req_caps != conn.client_capabilities {
3331                    break 'v true;
3332                }
3333                if input[4..20] != conn.client_guid {
3334                    break 'v true;
3335                }
3336                let req_secmode = u16::from_le_bytes(input[20..22].try_into().unwrap());
3337                if req_secmode != conn.client_security_mode {
3338                    break 'v true;
3339                }
3340                let count = u16::from_le_bytes(input[22..24].try_into().unwrap()) as usize;
3341                let mut dialects = Vec::with_capacity(count);
3342                for i in 0..count {
3343                    match input.get(24 + i * 2..26 + i * 2) {
3344                        Some(s) => dialects.push(u16::from_le_bytes(s.try_into().unwrap())),
3345                        None => break 'v true,
3346                    }
3347                }
3348                const SUPPORTED: [u16; 5] = [0x0202, 0x0210, 0x0300, 0x0302, 0x0311];
3349                let gcd = dialects
3350                    .iter()
3351                    .copied()
3352                    .filter(|d| SUPPORTED.contains(d))
3353                    .max();
3354                gcd != conn.dialect
3355            };
3356            if terminate {
3357                conn.disconnect = true;
3358                return IoctlReply::Silent;
3359            }
3360            let mut out = Vec::with_capacity(24);
3361            // Echo the exact values from our NEGOTIATE response —
3362            // the client fails the exchange on any mismatch
3363            // ([MS-SMB2] §3.3.5.15).
3364            out.extend_from_slice(&conn.advertised_caps.to_le_bytes());
3365            out.extend_from_slice(&server.guid);
3366            let sec_mode = smb_server_proto_smb2::negotiate::SIGNING_ENABLED
3367                | if server.require_signing {
3368                    smb_server_proto_smb2::negotiate::SIGNING_REQUIRED
3369                } else {
3370                    0
3371                };
3372            out.extend_from_slice(&sec_mode.to_le_bytes()); // SecurityMode
3373            out.extend_from_slice(
3374                &conn
3375                    .dialect
3376                    .unwrap_or(smb_server_proto_smb2::negotiate::DIALECT_210)
3377                    .to_le_bytes(),
3378            );
3379            (
3380                Status::SUCCESS,
3381                c::build_ioctl_resp(req.file_id, req.ctl_code, &out),
3382            )
3383        }
3384        // Resiliency handshake carries no output data. Preserve the
3385        // handle like a durable open ([MS-SMB2] §3.3.5.15.9) so a later
3386        // SMB2_CREATE_DURABLE_HANDLE_RECONNECT can reclaim it.
3387        c::fsctl::LMR_REQUEST_RESILIENCY => {
3388            if let Some((can_write, is_dir, rel)) =
3389                conn.with_handle(&req.file_id.0, |o| (o.can_write, o.is_dir, o.rel.clone()))
3390            {
3391                let timeout = req
3392                    .input
3393                    .get(0..4)
3394                    .map(|b| u32::from_le_bytes(b.try_into().unwrap()))
3395                    .filter(|t| *t != 0)
3396                    .unwrap_or(60_000);
3397                let access = if can_write { 0x001F_01FF } else { 0x0012_0089 };
3398                let options = if is_dir { 0x1 } else { 0x40 };
3399                let entry = crate::state::DurableEntry {
3400                    persistent_id: req.file_id.0,
3401                    create_guid: [0u8; 16],
3402                    rel,
3403                    is_dir,
3404                    access,
3405                    options,
3406                    session_id: conn.session_id,
3407                    owner_user: conn.user.clone(),
3408                    client_guid: conn.client_guid,
3409                    lease_key: None,
3410                    lease_state: 0,
3411                    persistent: false,
3412                    timeout,
3413                    deadline: std::time::Instant::now(),
3414                };
3415                conn.durable.insert(req.file_id.0, entry);
3416            }
3417            (
3418                Status::SUCCESS,
3419                c::build_ioctl_resp(req.file_id, req.ctl_code, &[]),
3420            )
3421        }
3422        // FSCTL_PIPE_TRANSACT (0x0011C017): the DCERPC PDU arrives
3423        // in the input buffer and the reply leaves in the output
3424        // buffer — this is how SMB2 clients speak RPC over named
3425        // pipes.
3426        c::fsctl::PIPE_TRANSACT => {
3427            let Some(pipe) = conn.pipes.get_mut(&req.file_id.0) else {
3428                tracing::debug!(pipes = conn.pipes.len(), "transact on non-pipe fid");
3429                return IoctlReply::Reply(Status::NOT_IMPLEMENTED, Vec::new());
3430            };
3431            let shares = server.share_infos();
3432            let out = {
3433                pipe.on_write(&req.input, &shares);
3434                counter!("smb_pipe_writes_total").increment(1);
3435                pipe.take(req.max_output as usize)
3436            };
3437            counter!("smb_pipe_reads_total").increment(1);
3438            let out_hex = hex_str(&out);
3439            tracing::debug!(out_len = out.len(), %out_hex, "pipe transact");
3440            (
3441                Status::SUCCESS,
3442                c::build_ioctl_resp(req.file_id, req.ctl_code, &out),
3443            )
3444        }
3445        // Server-side copy: hand the client a resume key naming this
3446        // open so a later COPYCHUNK can use it as the source.
3447        c::fsctl::SRV_REQUEST_RESUME_KEY => {
3448            if !conn.handle_exists(&req.file_id.0) {
3449                return IoctlReply::Reply(Status::INVALID_HANDLE, Vec::new());
3450            }
3451            let mut key = [0u8; 24];
3452            key[..16].copy_from_slice(&req.file_id.0);
3453            key[16..24].copy_from_slice(&next_resume_nonce().to_le_bytes());
3454            conn.resume_keys.insert(key, req.file_id.0);
3455            (
3456                Status::SUCCESS,
3457                c::build_ioctl_resp(req.file_id, req.ctl_code, &c::build_resume_key_resp(&key)),
3458            )
3459        }
3460        // Server-side copy: read from the source open (named by the
3461        // resume key in the input) and write into this target handle.
3462        c::fsctl::SRV_COPYCHUNK | c::fsctl::SRV_COPYCHUNK_WRITE => {
3463            let vfs = match share_vfs(server, conn, tid) {
3464                Some(v) => v,
3465                None => return IoctlReply::Reply(Status::INVALID_HANDLE, Vec::new()),
3466            };
3467            match do_copychunk(conn, vfs, &req).await {
3468                Ok(out) => {
3469                    counter!("smb_copychunk_bytes_total").increment(copychunk_total(&out) as u64);
3470                    (
3471                        Status::SUCCESS,
3472                        c::build_ioctl_resp(req.file_id, req.ctl_code, &out),
3473                    )
3474                }
3475                Err((status, out)) => {
3476                    let body = c::build_ioctl_resp(req.file_id, req.ctl_code, &out);
3477                    return IoctlReply::Reply(status, body);
3478                }
3479            }
3480        }
3481        // Offload (ODX) read: capture the requested source range and hand the
3482        // client an opaque token that a later OFFLOAD_WRITE copies from
3483        // ([MS-FSCC] §2.3.77/78). Emulated by snapshotting the bytes.
3484        c::fsctl::OFFLOAD_READ => {
3485            let vfs = match share_vfs(server, conn, tid) {
3486                Some(v) => v,
3487                None => return IoctlReply::Reply(Status::INVALID_HANDLE, Vec::new()),
3488            };
3489            let Some((file_offset, copy_length)) = c::parse_offload_read(&req.input) else {
3490                return IoctlReply::Reply(Status::INVALID_PARAMETER, Vec::new());
3491            };
3492            let Some(mut open) = conn.handle_take(&req.file_id.0) else {
3493                return IoctlReply::Reply(Status::INVALID_HANDLE, Vec::new());
3494            };
3495            let read = vfs.read(&mut open, file_offset, copy_length as usize).await;
3496            conn.handle_insert(req.file_id.0, open);
3497            let data = match read {
3498                Ok(d) => d,
3499                Err(e) => return IoctlReply::Reply(vfs_err(e), Vec::new()),
3500            };
3501            let transfer_length = data.len() as u64;
3502            let id = next_offload_id();
3503            conn.offload_tokens.insert(id, data);
3504            (
3505                Status::SUCCESS,
3506                c::build_ioctl_resp(
3507                    req.file_id,
3508                    req.ctl_code,
3509                    &c::build_offload_read_resp(transfer_length, &id),
3510                ),
3511            )
3512        }
3513        // Offload (ODX) write: copy the range represented by the token into this
3514        // handle ([MS-FSCC] §2.3.79/80). Emulated from the snapshotted bytes.
3515        c::fsctl::OFFLOAD_WRITE => {
3516            let vfs = match share_vfs(server, conn, tid) {
3517                Some(v) => v,
3518                None => return IoctlReply::Reply(Status::INVALID_HANDLE, Vec::new()),
3519            };
3520            let Some((file_offset, copy_length, transfer_offset, token_id)) =
3521                c::parse_offload_write(&req.input)
3522            else {
3523                return IoctlReply::Reply(Status::INVALID_PARAMETER, Vec::new());
3524            };
3525            let chunk = match conn.offload_tokens.get(&token_id) {
3526                Some(data) => {
3527                    let start = transfer_offset as usize;
3528                    let end = match start.checked_add(copy_length as usize) {
3529                        Some(e) if e <= data.len() => e,
3530                        _ => return IoctlReply::Reply(Status::INVALID_PARAMETER, Vec::new()),
3531                    };
3532                    data[start..end].to_vec()
3533                }
3534                None => return IoctlReply::Reply(Status::INVALID_PARAMETER, Vec::new()),
3535            };
3536            let Some(mut open) = conn.handle_take(&req.file_id.0) else {
3537                return IoctlReply::Reply(Status::INVALID_HANDLE, Vec::new());
3538            };
3539            let written = vfs.write(&mut open, file_offset, &chunk, false).await;
3540            conn.handle_insert(req.file_id.0, open);
3541            match written {
3542                Ok(w) => {
3543                    counter!("smb_offload_bytes_total").increment(w);
3544                    (
3545                        Status::SUCCESS,
3546                        c::build_ioctl_resp(
3547                            req.file_id,
3548                            req.ctl_code,
3549                            &c::build_offload_write_resp(w),
3550                        ),
3551                    )
3552                }
3553                Err(e) => return IoctlReply::Reply(vfs_err(e), Vec::new()),
3554            }
3555        }
3556        // Linux files are implicitly sparse; accept the hint.
3557        c::fsctl::SET_SPARSE => (
3558            Status::SUCCESS,
3559            c::build_ioctl_resp(req.file_id, req.ctl_code, &[]),
3560        ),
3561        // FSCTL_FILE_LEVEL_TRIM ([MS-FSCC] §2.3.73): deallocate byte ranges. The
3562        // Key field MUST be zero; the trim is advisory, so every requested range
3563        // is reported as processed (§2.3.74).
3564        c::fsctl::FILE_LEVEL_TRIM => match c::parse_file_level_trim(&req.input) {
3565            Some((0, num_ranges)) => (
3566                Status::SUCCESS,
3567                c::build_ioctl_resp(
3568                    req.file_id,
3569                    req.ctl_code,
3570                    &c::build_file_level_trim_resp(num_ranges),
3571                ),
3572            ),
3573            _ => return IoctlReply::Reply(Status::INVALID_PARAMETER, Vec::new()),
3574        },
3575        // FSCTL_GET/SET_INTEGRITY_INFORMATION ([MS-FSCC] §2.3.55/57): the POSIX
3576        // backend has no real integrity streams, but the per-open checksum
3577        // state is tracked so a GET after a SET reflects it.
3578        c::fsctl::GET_INTEGRITY_INFORMATION => {
3579            if !conn.handle_exists(&req.file_id.0) {
3580                return IoctlReply::Reply(Status::INVALID_HANDLE, Vec::new());
3581            }
3582            let (algo, flags) = conn.integrity.get(&req.file_id.0).copied().unwrap_or((0, 0));
3583            (
3584                Status::SUCCESS,
3585                c::build_ioctl_resp(
3586                    req.file_id,
3587                    req.ctl_code,
3588                    &c::build_get_integrity_resp(algo, flags),
3589                ),
3590            )
3591        }
3592        c::fsctl::SET_INTEGRITY_INFORMATION => {
3593            if !conn.handle_exists(&req.file_id.0) {
3594                return IoctlReply::Reply(Status::INVALID_HANDLE, Vec::new());
3595            }
3596            let Some(algo) = c::parse_set_integrity(&req.input) else {
3597                return IoctlReply::Reply(Status::INVALID_PARAMETER, Vec::new());
3598            };
3599            let flags = req
3600                .input
3601                .get(4..8)
3602                .map(|s| u32::from_le_bytes(s.try_into().unwrap()))
3603                .unwrap_or(0);
3604            conn.integrity.insert(req.file_id.0, (algo, flags));
3605            (
3606                Status::SUCCESS,
3607                c::build_ioctl_resp(req.file_id, req.ctl_code, &[]),
3608            )
3609        }
3610        // FSCTL_PIPE_WAIT ([MS-SMB2] §2.2.31.2): our named pipes are
3611        // always instantiable, so the wait completes immediately.
3612        c::fsctl::PIPE_WAIT => (
3613            Status::SUCCESS,
3614            c::build_ioctl_resp(req.file_id, req.ctl_code, &[]),
3615        ),
3616        // DFS is not offered on this standalone server: a referral
3617        // query for any path is answered STATUS_NOT_FOUND so the client
3618        // treats the path as non-DFS ([MS-DFSC] §3.1.5.4.2).
3619        c::fsctl::DFS_GET_REFERRALS => {
3620            return IoctlReply::Reply(Status::NOT_FOUND, Vec::new());
3621        }
3622        // Multichannel interface discovery ([MS-SMB2] §3.3.5.15.4):
3623        // report the server's network interfaces so the client may open
3624        // additional channels for the session.
3625        c::fsctl::QUERY_NETWORK_INTERFACE_INFO => {
3626            let out = network_interface_info();
3627            (
3628                Status::SUCCESS,
3629                c::build_ioctl_resp(req.file_id, req.ctl_code, &out),
3630            )
3631        }
3632        // Zero a byte range in the target handle ([MS-FSCC] §2.3.79).
3633        c::fsctl::SET_ZERO_DATA => {
3634            let vfs = match share_vfs(server, conn, tid) {
3635                Some(v) => v,
3636                None => return IoctlReply::Reply(Status::INVALID_HANDLE, Vec::new()),
3637            };
3638            let Some((start, end)) = c::parse_zero_data(&req.input).filter(|(s, e)| e >= s) else {
3639                return IoctlReply::Reply(Status::INVALID_PARAMETER, Vec::new());
3640            };
3641            let Some(mut open) = conn.handle_take(&req.file_id.0) else {
3642                return IoctlReply::Reply(Status::INVALID_HANDLE, Vec::new());
3643            };
3644            let result = vfs.zero_range(&mut open, start, end - start).await;
3645            conn.handle_insert(req.file_id.0, open);
3646            match result {
3647                Ok(()) => {
3648                    counter!("smb_zeroed_bytes_total").increment(end - start);
3649                    (
3650                        Status::SUCCESS,
3651                        c::build_ioctl_resp(req.file_id, req.ctl_code, &[]),
3652                    )
3653                }
3654                Err(e) => (vfs_err(e), Vec::new()),
3655            }
3656        }
3657        _ => (Status::NOT_IMPLEMENTED, Vec::new()),
3658    };
3659    IoctlReply::Reply(status, body)
3660}
3661
3662#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB2 wire serialization
3663pub(crate) async fn close(
3664    conn: &mut Smb2Conn,
3665    vfs: Arc<dyn smb_server_vfs::Vfs>,
3666    buf: &[u8],
3667) -> Result<Vec<u8>, Status> {
3668    let req = c::CloseReq::parse(buf).ok_or(Status::INVALID_PARAMETER)?;
3669    let Some(h) = conn.handle_take(&req.file_id.0) else {
3670        return Err(Status::INVALID_HANDLE);
3671    };
3672    conn.searches.remove(&req.file_id.0);
3673    conn.lease_keys.remove(&req.file_id.0);
3674    conn.integrity.remove(&req.file_id.0);
3675    if let Some(s) = conn.scope.as_ref() {
3676        s.borrow_mut().channel_seq.remove(&req.file_id.0);
3677    }
3678    let meta = vfs.stat(&h.path).await.ok().unwrap_or_default();
3679    match vfs.close(h).await {
3680        Ok(()) => {}
3681        // A delete-on-close on a directory that is not empty at close time
3682        // abandons the delete and still completes the CLOSE successfully
3683        // ([MS-FSA] §2.1.5.4); the object simply remains.
3684        Err(smb_server_vfs::VfsError::DirectoryNotEmpty) => {}
3685        Err(e) => return Err(vfs_err(e)),
3686    }
3687    counter!("smb_closes_total").increment(1);
3688    Ok(c::build_close_resp(
3689        [
3690            meta.times[0].0,
3691            meta.times[1].0,
3692            meta.times[2].0,
3693            meta.times[3].0,
3694        ],
3695        meta.alloc,
3696        meta.eof,
3697        meta.attrs.0,
3698    ))
3699}
3700
3701pub(crate) fn close_all_handles(conn: &mut Smb2Conn) {
3702    if let Some(s) = conn.scope.as_ref() {
3703        let mut s = s.borrow_mut();
3704        s.handles.clear();
3705        s.channel_seq.clear();
3706    }
3707    conn.pipes.clear();
3708}
3709
3710/// Verify the request's ChannelSequence against the open's ([MS-SMB2]
3711/// §3.3.5.2.10), updating the open's counters. Returns `false` when the caller
3712/// must fail the WRITE/SET_INFO/IOCTL with STATUS_FILE_NOT_AVAILABLE. An open
3713/// with no tracked state always passes.
3714fn verify_channel_sequence(conn: &Smb2Conn, fid: &[u8; 16], channel_seq: u16, is_replay: bool) -> bool {
3715    let Some(scope) = conn.scope.as_ref() else {
3716        return true;
3717    };
3718    let mut scope = scope.borrow_mut();
3719    let Some(cs) = scope.channel_seq.get_mut(fid) else {
3720        return true;
3721    };
3722    // Unsigned 16-bit difference from Open.ChannelSequence.
3723    let diff = channel_seq.wrapping_sub(cs.channel_sequence);
3724    if !is_replay {
3725        if channel_seq == cs.channel_sequence {
3726            cs.outstanding_request_count += 1;
3727            true
3728        } else if diff <= smb_server_proto_smb2::consts::CHANNEL_SEQUENCE_WINDOW {
3729            cs.outstanding_pre_request_count += cs.outstanding_request_count;
3730            cs.outstanding_request_count = 1;
3731            cs.channel_sequence = channel_seq;
3732            true
3733        } else {
3734            false
3735        }
3736    } else if channel_seq == cs.channel_sequence {
3737        if cs.outstanding_pre_request_count == 0 {
3738            cs.outstanding_request_count += 1;
3739            true
3740        } else {
3741            false
3742        }
3743    } else if diff <= smb_server_proto_smb2::consts::CHANNEL_SEQUENCE_WINDOW {
3744        cs.outstanding_pre_request_count += cs.outstanding_request_count;
3745        cs.channel_sequence = channel_seq;
3746        if cs.outstanding_pre_request_count == 0 {
3747            cs.outstanding_request_count = 1;
3748            true
3749        } else {
3750            cs.outstanding_request_count = 0;
3751            false
3752        }
3753    } else {
3754        false
3755    }
3756}
3757
3758// ---------------- Named pipes (IPC$) ----------------
3759
3760/// True when the tree behind `tid` is the virtual IPC$ share.
3761pub(crate) fn share_is_ipc(server: &Arc<ServerShared>, conn: &Smb2Conn, tid: u32) -> bool {
3762    conn.tree_name(tid)
3763        .and_then(|n| server.shares.get(&n))
3764        .map(|s| s.is_ipc)
3765        .unwrap_or(false)
3766}
3767
3768/// True when the tree behind `tid` is a continuously-available share, which may
3769/// grant persistent handles ([MS-SMB2] §3.3.5.9.11).
3770pub(crate) fn share_is_ca(server: &Arc<ServerShared>, conn: &Smb2Conn, tid: u32) -> bool {
3771    conn.tree_name(tid)
3772        .and_then(|n| server.shares.get(&n))
3773        .map(|s| s.ca)
3774        .unwrap_or(false)
3775}
3776
3777/// Known pipe names served on IPC$.
3778const KNOWN_PIPES: &[&str] = &["srvsvc", "wkssvc", "lanman", "netlogon"];
3779
3780/// Open a virtual pipe; `Err(FILE_NOT_FOUND)` for unknown names.
3781#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB2 wire serialization
3782pub(crate) fn pipe_create(conn: &mut Smb2Conn, buf: &[u8]) -> Result<Vec<u8>, Status> {
3783    let req = c::CreateReq::parse(buf).ok_or(Status::INVALID_PARAMETER)?;
3784    let name = req.name.trim_start_matches(['\\', '/']).to_lowercase();
3785    if !KNOWN_PIPES.contains(&name.as_str()) {
3786        return Err(Status::OBJECT_PATH_NOT_FOUND);
3787    }
3788    let fid_bytes = next_file_id();
3789    let fid = c::FileId(fid_bytes);
3790    conn.pipes
3791        .insert(fid_bytes, crate::srvsvc::Pipe::new(&name));
3792    tracing::debug!(pipe = %name, "pipe opened");
3793    // Zeroed timestamps/attrs; a pipe is a stream device.
3794    Ok(c::build_create_resp(
3795        fid,
3796        1, // FILE_OPENED
3797        [0u64; 4],
3798        0, // no file attributes
3799        4096,
3800        0,
3801        false,
3802        c::oplock::NONE,
3803        &[],
3804    ))
3805}
3806
3807/// Drain up to the requested byte count from a pipe's outbound queue;
3808/// `None` when `file_id` is not a pipe.
3809pub(crate) fn pipe_read(conn: &mut Smb2Conn, buf: &[u8]) -> Option<Vec<u8>> {
3810    let Some(req) = c::ReadReq::parse(buf) else {
3811        tracing::debug!("pipe_read: parse failed");
3812        return None;
3813    };
3814    let Some(pipe) = conn.pipes.get_mut(&req.file_id.0) else {
3815        tracing::debug!(fid = ?req.file_id, "read on non-pipe");
3816        return None;
3817    };
3818    tracing::debug!(pending = pipe.pending(), max = req.length, "pipe read");
3819    let data = pipe.take(req.length as usize);
3820    counter!("smb_pipe_reads_total").increment(1);
3821    Some(c::build_read_resp(&data))
3822}
3823
3824/// Feed client bytes into a pipe RPC dispatcher; `None` when `file_id` is
3825/// not a pipe. Returns the WRITE response body.
3826pub(crate) fn pipe_write(
3827    server: &Arc<ServerShared>,
3828    conn: &mut Smb2Conn,
3829    buf: &[u8],
3830) -> Option<Vec<u8>> {
3831    let req = c::WriteReq::parse(buf);
3832    let Some(req) = req else {
3833        tracing::debug!("pipe_write: parse failed");
3834        return None;
3835    };
3836    if !conn.pipes.contains_key(&req.file_id.0) {
3837        return None;
3838    }
3839    let shares = server.share_infos();
3840    let written = req.payload.len() as u32;
3841    let pipe = conn.pipes.get_mut(&req.file_id.0)?;
3842    tracing::debug!(pipe = %pipe.name, len = req.payload.len(), payload = %hex_str(&req.payload), "pipe write");
3843    pipe.on_write(&req.payload, &shares);
3844    tracing::debug!(pending = pipe.pending(), "pipe read queued");
3845    counter!("smb_pipe_writes_total").increment(1);
3846    Some(c::build_write_resp(written))
3847}
3848
3849/// Remove a pipe handle; `true` when it was one.
3850pub(crate) fn pipe_close(conn: &mut Smb2Conn, buf: &[u8]) -> bool {
3851    c::CloseReq::parse(buf)
3852        .map(|r| conn.pipes.remove(&r.file_id.0).is_some())
3853        .unwrap_or(false)
3854}
3855
3856// ---------------- QUERY_DIRECTORY ----------------
3857
3858#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB2 wire serialization
3859pub(crate) async fn query_directory(
3860    conn: &mut Smb2Conn,
3861    vfs: Arc<dyn smb_server_vfs::Vfs>,
3862    buf: &[u8],
3863) -> Result<Option<Vec<u8>>, Status> {
3864    let req = c::QueryDirReq::parse(buf).ok_or(Status::INVALID_PARAMETER)?;
3865
3866    // Clients repeat the original pattern on every continuation call, so a
3867    // fresh enumeration starts only when no search state exists yet or the
3868    // client asks to restart/rescan ([MS-SMB2] §3.3.5.24).
3869    let has_state = conn.searches.contains_key(&req.file_id.0);
3870    let restart = !has_state
3871        || req.flags
3872            & (c::find_flags::RESTART_SCANS
3873                | c::find_flags::REOPEN
3874                | c::find_flags::SCAN
3875                | c::find_flags::INDEX_SPECIFIED)
3876            != 0;
3877
3878    if restart {
3879        let dir_rel = {
3880            let (is_dir, rel) = conn
3881                .with_handle(&req.file_id.0, |h| (h.is_dir, h.rel.clone()))
3882                .ok_or(Status::INVALID_HANDLE)?;
3883            if !is_dir {
3884                // QUERY_DIRECTORY on a non-directory open ([MS-SMB2] §3.3.5.18).
3885                return Err(Status::INVALID_PARAMETER);
3886            }
3887            // Enumeration runs inside the opened directory; the pattern may
3888            // still carry a subdirectory prefix relative to it.
3889            let pattern = req.pattern.trim_start_matches(['\\', '/']).to_string();
3890            let (dir_part, _) = crate::cmds::dir_cmds_split(&pattern);
3891            if dir_part.is_empty() {
3892                rel
3893            } else if rel.is_empty() {
3894                dir_part
3895            } else {
3896                format!("{}\\{}", rel.trim_end_matches(['\\', '/']), dir_part)
3897            }
3898        };
3899
3900        let (_, name_pat) =
3901            crate::cmds::dir_cmds_split(req.pattern.trim_start_matches(['\\', '/']));
3902        let entries = vfs.list(&dir_rel).await.map_err(vfs_err)?;
3903        // Windows precedes real children with "." (the directory itself) and
3904        // ".." (its parent) on a wildcard scan ([MS-FSCC] §2.4), so even an
3905        // empty directory yields these two entries. They pass through the same
3906        // pattern filter, so a non-matching pattern still excludes them.
3907        let dot_meta = vfs.stat(&dir_rel).await.map_err(vfs_err)?;
3908        let dots = [".", ".."].into_iter().map(|n| info::FindEntry {
3909            name: n.to_string(),
3910            meta: info::QueryMeta::from_vfs(&dot_meta),
3911        });
3912        let mut matched: Vec<info::FindEntry> = dots
3913            .chain(entries.into_iter().map(|e| info::FindEntry {
3914                name: e.name,
3915                meta: info::QueryMeta::from_vfs(&e.meta),
3916            }))
3917            .filter(|e| {
3918                name_pat.is_empty()
3919                    || crate::cmds::wildcard(&e.name.to_lowercase(), &name_pat.to_lowercase())
3920            })
3921            .collect();
3922        // Resume-by-index ([MS-SMB2] §2.2.33): skip to the requested ordinal.
3923        if req.flags & c::find_flags::INDEX_SPECIFIED != 0 && req.file_index > 0 {
3924            let skip = (req.file_index as usize).min(matched.len());
3925            matched.drain(..skip);
3926        }
3927        conn.searches.insert(req.file_id.0, matched.into());
3928    } else if !conn.searches.contains_key(&req.file_id.0) {
3929        return Err(Status::INVALID_PARAMETER);
3930    }
3931
3932    // Take up to one batch of entries (single entry when requested).
3933    let take = if req.flags & c::find_flags::RETURN_SINGLE_ENTRY != 0 {
3934        1
3935    } else {
3936        512
3937    };
3938    let Some(queue) = conn.searches.get_mut(&req.file_id.0) else {
3939        return Err(Status::INVALID_PARAMETER);
3940    };
3941    let out: Vec<info::FindEntry> = (0..take).filter_map(|_| queue.pop_front()).collect();
3942
3943    if out.is_empty() {
3944        return Ok(None); // STATUS_NO_MORE_FILES
3945    }
3946    Ok(Some(info::encode_find_entries(&out, req.class)))
3947}
3948
3949// ---------------- QUERY_INFO ----------------
3950
3951pub(crate) async fn query_info(
3952    conn: &mut Smb2Conn,
3953    vfs: Arc<dyn smb_server_vfs::Vfs>,
3954    buf: &[u8],
3955) -> Result<Option<Vec<u8>>, Status> {
3956    let req = c::QueryInfoReq::parse(buf).ok_or(Status::INVALID_PARAMETER)?;
3957    match req.info_type {
3958        c::info_type::FILE => {
3959            let (path, rel) = conn
3960                .with_handle(&req.file_id.0, |h| (h.path.clone(), h.rel.clone()))
3961                .ok_or(Status::INVALID_HANDLE)?;
3962            let m = vfs.stat(&path).await.map_err(vfs_err)?;
3963            if req.class == info::file_class::STREAM {
3964                // Default `::$DATA` data stream (files only) plus any ADS.
3965                let mut streams = Vec::new();
3966                if !m.is_dir {
3967                    streams.push(("::$DATA".to_string(), m.eof));
3968                }
3969                streams.extend(vfs.list_streams(&path).await.map_err(vfs_err)?);
3970                return Ok(Some(info::encode_stream_info(&streams)));
3971            }
3972            let qm = info::QueryMeta::from_vfs(&m);
3973            let name = if req.class == info::file_class::NORMALIZED_NAME {
3974                // Normalized name is the full share-relative path ([MS-FSCC]
3975                // §2.4.NormalizedName): no leading separator, backslash-joined.
3976                rel.trim_start_matches(['\\', '/']).replace('/', "\\")
3977            } else {
3978                rel.rsplit(['\\', '/']).next().unwrap_or("").to_string()
3979            };
3980            info::encode_file_info(req.class, &qm, &name)
3981                .map(Some)
3982                .ok_or(Status::NOT_IMPLEMENTED)
3983        }
3984        c::info_type::FS => info::encode_fs_info(req.class)
3985            .map(Some)
3986            .ok_or(Status::NOT_IMPLEMENTED),
3987        c::info_type::SECURITY => {
3988            let path = conn
3989                .with_handle(&req.file_id.0, |h| h.path.clone())
3990                .ok_or(Status::INVALID_HANDLE)?;
3991            let stored = vfs.get_security(&path).await.map_err(vfs_err)?;
3992            let additional = if req.additional == 0 {
3993                crate::security::sec_info::DEFAULT
3994            } else {
3995                req.additional
3996            };
3997            let sd = crate::security::query_security(stored.as_deref(), additional)
3998                .ok_or(Status::INVALID_PARAMETER)?;
3999            if (req.output_len as usize) < sd.len() {
4000                return Err(Status::BUFFER_TOO_SMALL);
4001            }
4002            Ok(Some(sd))
4003        }
4004        // Disk quotas are not tracked on this volume ([MS-FSCC] §2.4.33): report
4005        // an honestly empty FILE_QUOTA_INFORMATION list rather than fabricating
4006        // per-user entries.
4007        c::info_type::QUOTA => Ok(Some(Vec::new())),
4008        _ => Ok(None),
4009    }
4010}
4011
4012// ---------------- SET_INFO ----------------
4013
4014pub(crate) async fn set_info(
4015    conn: &mut Smb2Conn,
4016    vfs: Arc<dyn smb_server_vfs::Vfs>,
4017    buf: &[u8],
4018) -> Result<(), Status> {
4019    let req = c::SetInfoReq::parse(buf).ok_or(Status::INVALID_PARAMETER)?;
4020    match req.info_type {
4021        c::info_type::FILE => {
4022            let op = match info::decode_set_file_op(req.class, &req.buffer) {
4023                Ok(op) => op,
4024                Err(_) => return Err(Status::INVALID_PARAMETER),
4025            };
4026            let Some(op) = op else { return Ok(()) }; // advisory-only class
4027            let mut open = conn
4028                .handle_take(&req.file_id.0)
4029                .ok_or(Status::INVALID_HANDLE)?;
4030            let result = vfs.set_info_open(&mut open, &op).await;
4031            conn.handle_insert(req.file_id.0, open);
4032            result.map_err(vfs_err)
4033        }
4034        c::info_type::SECURITY => {
4035            let path = conn
4036                .with_handle(&req.file_id.0, |h| h.path.clone())
4037                .ok_or(Status::INVALID_HANDLE)?;
4038            vfs.set_security(&path, &req.buffer).await.map_err(vfs_err)
4039        }
4040        // Disk quotas are not tracked on this volume ([MS-FSCC] §2.4.33).
4041        c::info_type::QUOTA => Err(Status::INVALID_DEVICE_REQUEST),
4042        _ => Err(Status::NOT_IMPLEMENTED),
4043    }
4044}
4045
4046// ---------------- helpers ----------------
4047
4048fn hex_str(b: &[u8]) -> String {
4049    b.iter().map(|x| format!("{:02x}", x)).collect()
4050}
4051
4052fn g16(b: &[u8], o: usize) -> u16 {
4053    b.get(o..o + size_of::<u16>())
4054        .map(|s| u16::from_le_bytes([s[0], s[1]]))
4055        .unwrap_or(0)
4056}
4057
4058fn g32(b: &[u8], o: usize) -> u32 {
4059    b.get(o..o + size_of::<u32>())
4060        .and_then(|s| s.try_into().ok())
4061        .map(u32::from_le_bytes)
4062        .unwrap_or(0)
4063}
4064
4065/// Resolve the VFS backing `tid`, mirroring the SMB1 path (share by name,
4066/// unknown trees rejected before handlers run).
4067pub(crate) fn share_vfs(
4068    server: &Arc<ServerShared>,
4069    conn: &Smb2Conn,
4070    tid: u32,
4071) -> Option<Arc<dyn smb_server_vfs::Vfs>> {
4072    conn.tree_name(tid)
4073        .and_then(|n| server.shares.get(&n))
4074        .map(|s| s.vfs.clone())
4075}
4076
4077/// Server-side-copy nonce appended to a resume key so repeated keys on one
4078/// open stay distinct.
4079fn next_resume_nonce() -> u64 {
4080    static N: AtomicU64 = AtomicU64::new(0x5253_554d_4b45_5900);
4081    N.fetch_add(1, Ordering::Relaxed)
4082}
4083
4084/// A fresh 16-byte identifier embedded in an offload (ODX) token, keying the
4085/// source bytes captured by FSCTL_OFFLOAD_READ.
4086#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB2 wire serialization
4087fn next_offload_id() -> [u8; 16] {
4088    static N: AtomicU64 = AtomicU64::new(0x4F44_5849_4400_0000);
4089    let n = N.fetch_add(1, Ordering::Relaxed);
4090    let mut id = [0u8; 16];
4091    id[..8].copy_from_slice(&n.to_le_bytes());
4092    id[8..].copy_from_slice(&next_resume_nonce().to_le_bytes());
4093    id
4094}
4095
4096/// Read the TotalBytesWritten field out of a SRV_COPYCHUNK_RESPONSE body.
4097#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB2 wire serialization
4098fn copychunk_total(resp: &[u8]) -> u32 {
4099    resp.get(8..12)
4100        .map(|s| u32::from_le_bytes(s.try_into().unwrap()))
4101        .unwrap_or(0)
4102}
4103
4104/// Perform a server-side copy: for each chunk read from the source open named
4105/// by the resume key and write into the target handle. Returns the
4106/// SRV_COPYCHUNK_RESPONSE body, or `(status, body)` where the body carries the
4107/// server limits on a limits violation ([MS-SMB2] §3.3.5.15.6).
4108async fn do_copychunk(
4109    conn: &mut Smb2Conn,
4110    vfs: Arc<dyn smb_server_vfs::Vfs>,
4111    req: &c::IoctlReq,
4112) -> Result<Vec<u8>, (Status, Vec<u8>)> {
4113    use smb_server_proto_smb2::commands::copychunk_limits as lim;
4114    let cc = c::CopyChunkCopy::parse(&req.input).ok_or((Status::INVALID_PARAMETER, Vec::new()))?;
4115
4116    let total: u64 = cc.chunks.iter().map(|k| k.length as u64).sum();
4117    if cc.chunks.len() as u32 > lim::MAX_CHUNKS
4118        || cc.chunks.iter().any(|k| k.length > lim::MAX_CHUNK_SIZE)
4119        || total > lim::MAX_TOTAL_SIZE as u64
4120    {
4121        let limits =
4122            c::build_copychunk_resp(lim::MAX_CHUNKS, lim::MAX_CHUNK_SIZE, lim::MAX_TOTAL_SIZE);
4123        return Err((Status::INVALID_PARAMETER, limits));
4124    }
4125
4126    let src_fid = *conn
4127        .resume_keys
4128        .get(&cc.source_key)
4129        .ok_or((Status::OBJECT_NAME_NOT_FOUND, Vec::new()))?;
4130    let tgt_fid = req.file_id.0;
4131
4132    let mut chunks_written = 0u32;
4133    let mut total_written = 0u32;
4134    for k in &cc.chunks {
4135        let mut src = conn
4136            .handle_take(&src_fid)
4137            .ok_or((Status::INVALID_HANDLE, Vec::new()))?;
4138        let read = vfs.read(&mut src, k.source_offset, k.length as usize).await;
4139        conn.handle_insert(src_fid, src);
4140        let data = read.map_err(|e| (vfs_err(e), Vec::new()))?;
4141        let mut tgt = conn
4142            .handle_take(&tgt_fid)
4143            .ok_or((Status::INVALID_HANDLE, Vec::new()))?;
4144        let written = vfs.write(&mut tgt, k.target_offset, &data, false).await;
4145        conn.handle_insert(tgt_fid, tgt);
4146        let w = written.map_err(|e| (vfs_err(e), Vec::new()))?;
4147        total_written += w as u32;
4148        chunks_written += 1;
4149    }
4150    // On success ChunkBytesWritten is 0 ([MS-SMB2] §2.2.32.1).
4151    Ok(c::build_copychunk_resp(chunks_written, 0, total_written))
4152}
4153
4154pub(crate) fn vfs_err(e: smb_server_vfs::VfsError) -> Status {
4155    use smb_server_vfs::VfsError as E;
4156    match e {
4157        E::NotFound => Status::OBJECT_PATH_NOT_FOUND,
4158        E::AlreadyExists => Status::OBJECT_NAME_COLLISION,
4159        E::AccessDenied => Status::ACCESS_DENIED,
4160        E::DirectoryNotEmpty => Status::DIRECTORY_NOT_EMPTY,
4161        E::InvalidArgument => Status::INVALID_PARAMETER,
4162        E::NotSupported => Status::NOT_IMPLEMENTED,
4163        E::StoppedOnSymlink { .. } => Status::STOPPED_ON_SYMLINK,
4164        E::Io(_) => Status::UNSUCCESSFUL,
4165    }
4166}
4167
4168/// Build the generic ERROR response body ([MS-SMB2] §2.2.2): StructureSize
4169/// 9 with no error data. Every failed command carries this so clients can
4170/// parse the frame (a bare header breaks their compound parser). The body
4171/// is padded to the structure's declared 8-byte footprint.
4172#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB2 wire serialization
4173pub(crate) fn error_resp() -> Vec<u8> {
4174    let mut b = Vec::with_capacity(8);
4175    b.extend_from_slice(&9u16.to_le_bytes()); // StructureSize
4176    b.push(0); // Reserved
4177    b.push(0); // ByteCount
4178    b.resize(8, 0); // ErrorData area (empty, padded)
4179    b
4180}
4181
4182/// Build a 64-byte SMB2 response header + body, echoing MessageId/TreeId
4183/// and stamping the effective session id ([MS-SMB2] §3.3.4.1).
4184#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB2 wire serialization
4185pub(crate) fn response(
4186    req: &smb_server_proto_smb2::Header2,
4187    status: Status,
4188    body: Vec<u8>,
4189    session_id: u64,
4190) -> Vec<u8> {
4191    // Any failing/warning status without a handler-built body uses the
4192    // generic ERROR structure (clients cannot parse a bare header).
4193    let body = if status != Status::SUCCESS && body.is_empty() {
4194        error_resp()
4195    } else {
4196        body
4197    };
4198    let mut f = Vec::with_capacity(64 + body.len());
4199    f.extend_from_slice(&smb_server_proto_smb2::SMB2_MAGIC);
4200    f.extend_from_slice(&64u16.to_le_bytes()); // StructureSize
4201    f.extend_from_slice(&req.credit_charge.to_le_bytes()); // CreditCharge echo
4202    f.extend_from_slice(&status.raw().to_le_bytes());
4203    f.extend_from_slice(&req.command.to_le_bytes());
4204    // Honor the client's CreditRequest ([MS-SMB2] §3.3.1.2) so it can build a
4205    // credit window large enough for multi-credit (>64 KiB) reads/writes; the
4206    // grant is floored at the charge and capped to bound outstanding credits.
4207    let grant = req.credits.max(req.credit_charge).clamp(1, 512);
4208    f.extend_from_slice(&grant.to_le_bytes());
4209    counter!("smb_credits_granted_total").increment(grant as u64);
4210    f.extend_from_slice(&1u32.to_le_bytes()); // FLAGS_SERVER_TO_REDIR
4211    f.extend_from_slice(&0u32.to_le_bytes()); // NextCommand
4212    f.extend_from_slice(&req.message_id.to_le_bytes());
4213    f.extend_from_slice(&0u32.to_le_bytes()); // Reserved/ProcessId
4214    f.extend_from_slice(&req.tree_id.to_le_bytes());
4215    f.extend_from_slice(&session_id.to_le_bytes());
4216    f.extend_from_slice(&[0u8; 16]); // Signature
4217    debug_assert_eq!(f.len(), 64);
4218    f.extend_from_slice(&body);
4219    f
4220}
4221
4222#[cfg(test)]
4223mod async_notify_tests {
4224    use super::*;
4225
4226    #[test]
4227    fn async_frame_sets_async_flag_and_ids() {
4228        let f = build_async_frame(
4229            0xABCD,
4230            42,
4231            7,
4232            ss::cmd::CHANGE_NOTIFY,
4233            Status::PENDING,
4234            &[0u8; 8],
4235        );
4236        assert_eq!(&f[0..4], &smb_server_proto_smb2::SMB2_MAGIC);
4237        let flags = u32::from_le_bytes(f[16..20].try_into().unwrap());
4238        assert_eq!(flags & 0x2, 0x2, "ASYNC_COMMAND set");
4239        assert_eq!(flags & 0x1, 0x1, "SERVER_TO_REDIR set");
4240        assert_eq!(
4241            u64::from_le_bytes(f[24..32].try_into().unwrap()),
4242            42,
4243            "MessageId"
4244        );
4245        assert_eq!(
4246            u64::from_le_bytes(f[32..40].try_into().unwrap()),
4247            7,
4248            "AsyncId"
4249        );
4250        assert_eq!(
4251            u64::from_le_bytes(f[40..48].try_into().unwrap()),
4252            0xABCD,
4253            "SessionId"
4254        );
4255        assert_eq!(
4256            u32::from_le_bytes(f[8..12].try_into().unwrap()),
4257            Status::PENDING.raw()
4258        );
4259    }
4260
4261    /// End-to-end of the real inotify watcher: arm a watch on a temp dir, drop
4262    /// a file in, and confirm we surface a FILE_ACTION_ADDED for its name.
4263    #[test]
4264    fn watch_reports_created_file() {
4265        tokio_uring::start(async {
4266            let dir = std::env::temp_dir().join(format!("rustsmb_notify_{}", std::process::id()));
4267            std::fs::create_dir_all(&dir).unwrap();
4268            let path = dir.to_string_lossy().into_owned();
4269
4270            let (_tx, mut rx) = oneshot::channel();
4271            let watch = tokio_uring::spawn(async move {
4272                watch_one_event(
4273                    &path,
4274                    false,
4275                    smb_server_proto_smb2::commands::notify_filter::FILE_NAME,
4276                    &mut rx,
4277                )
4278                .await
4279            });
4280            tokio::time::sleep(std::time::Duration::from_millis(150)).await;
4281            std::fs::write(dir.join("created.txt"), b"hi").unwrap();
4282
4283            let events = watch.await.unwrap().expect("event fired");
4284            assert_eq!(events.len(), 1);
4285            assert_eq!(events[0].0, smb_server_proto_smb2::commands::notify_action::ADDED);
4286            assert_eq!(events[0].1, "created.txt");
4287            std::fs::remove_dir_all(&dir).ok();
4288        });
4289    }
4290
4291    /// A tree watch (`watch_tree`) fires on a file created in a subdirectory and
4292    /// names it relative to the watched root.
4293    #[test]
4294    fn recursive_watch_reports_subdir_file() {
4295        tokio_uring::start(async {
4296            let dir =
4297                std::env::temp_dir().join(format!("rustsmb_notify_rec_{}", std::process::id()));
4298            let sub = dir.join("nested");
4299            std::fs::create_dir_all(&sub).unwrap();
4300            let path = dir.to_string_lossy().into_owned();
4301
4302            let (_tx, mut rx) = oneshot::channel();
4303            let watch = tokio_uring::spawn(async move {
4304                watch_one_event(
4305                    &path,
4306                    true,
4307                    smb_server_proto_smb2::commands::notify_filter::FILE_NAME,
4308                    &mut rx,
4309                )
4310                .await
4311            });
4312            tokio::time::sleep(std::time::Duration::from_millis(150)).await;
4313            std::fs::write(sub.join("deep.txt"), b"hi").unwrap();
4314
4315            let events = watch.await.unwrap().expect("subdir event fired");
4316            assert_eq!(events[0].0, smb_server_proto_smb2::commands::notify_action::ADDED);
4317            assert_eq!(
4318                events[0].1, "nested\\deep.txt",
4319                "named relative to watch root"
4320            );
4321            std::fs::remove_dir_all(&dir).ok();
4322        });
4323    }
4324
4325    /// Cancelling the watch resolves the watcher with no events.
4326    #[test]
4327    fn cancel_stops_watch() {
4328        tokio_uring::start(async {
4329            let dir =
4330                std::env::temp_dir().join(format!("rustsmb_notify_cancel_{}", std::process::id()));
4331            std::fs::create_dir_all(&dir).unwrap();
4332            let path = dir.to_string_lossy().into_owned();
4333
4334            let (tx, mut rx) = oneshot::channel();
4335            let watch = tokio_uring::spawn(async move {
4336                watch_one_event(
4337                    &path,
4338                    false,
4339                    smb_server_proto_smb2::commands::notify_filter::FILE_NAME,
4340                    &mut rx,
4341                )
4342                .await
4343            });
4344            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
4345            tx.send(Status::CANCELLED).unwrap();
4346            assert!(
4347                watch.await.unwrap().is_err(),
4348                "cancelled watch yields no events"
4349            );
4350            std::fs::remove_dir_all(&dir).ok();
4351        });
4352    }
4353}
4354
4355#[cfg(test)]
4356mod fsctl_tests {
4357    use super::*;
4358
4359    fn conn_with_vfs(dir: &std::path::Path) -> (Smb2Conn, Arc<dyn smb_server_vfs::Vfs>) {
4360        let vfs: Arc<dyn smb_server_vfs::Vfs> = Arc::new(smb_server_backend_posix::PosixVfs::new(dir));
4361        let (tx, _rx) = mpsc::channel(8);
4362        (Smb2Conn::new([0u8; 8], tx), vfs)
4363    }
4364
4365    /// Server-side copy reads from the source open (named by a resume key) and
4366    /// writes into the target handle; the target file ends up with the bytes.
4367    #[test]
4368    fn copychunk_copies_between_handles() {
4369        tokio_uring::start(async {
4370            let dir = std::env::temp_dir().join(format!("rustsmb_cc_{}", std::process::id()));
4371            std::fs::create_dir_all(&dir).unwrap();
4372            std::fs::write(dir.join("src.txt"), b"hello copychunk world").unwrap();
4373            std::fs::write(dir.join("dst.txt"), b"").unwrap();
4374
4375            let (mut conn, vfs) = conn_with_vfs(&dir);
4376            let (src, _m, _a) = vfs
4377                .create("src.txt", false, 0x8000_0000, 1, 0, 0)
4378                .await
4379                .unwrap();
4380            let (dst, _m, _a) = vfs
4381                .create("dst.txt", false, 0x4000_0000, 1, 0, 0)
4382                .await
4383                .unwrap();
4384            let (src_fid, dst_fid) = ([1u8; 16], [2u8; 16]);
4385            conn.handle_insert(src_fid, src);
4386            conn.handle_insert(dst_fid, dst);
4387
4388            let mut key = [0u8; 24];
4389            key[..16].copy_from_slice(&src_fid);
4390            conn.resume_keys.insert(key, src_fid);
4391
4392            let mut input = Vec::new();
4393            input.extend_from_slice(&key);
4394            input.extend_from_slice(&1u32.to_le_bytes()); // ChunkCount
4395            input.extend_from_slice(&0u32.to_le_bytes()); // Reserved
4396            input.extend_from_slice(&0u64.to_le_bytes()); // SourceOffset
4397            input.extend_from_slice(&0u64.to_le_bytes()); // TargetOffset
4398            input.extend_from_slice(&21u32.to_le_bytes()); // Length
4399            input.extend_from_slice(&0u32.to_le_bytes()); // Reserved
4400
4401            let req = c::IoctlReq {
4402                ctl_code: c::fsctl::SRV_COPYCHUNK,
4403                file_id: c::FileId(dst_fid),
4404                input,
4405                max_output: 4096,
4406                is_fsctl: true,
4407            };
4408            let out = do_copychunk(&mut conn, vfs.clone(), &req)
4409                .await
4410                .expect("copychunk");
4411            assert_eq!(&out[0..4], &1u32.to_le_bytes(), "ChunksWritten");
4412            assert_eq!(&out[8..12], &21u32.to_le_bytes(), "TotalBytesWritten");
4413
4414            let dst = conn.handle_take(&dst_fid).unwrap();
4415            vfs.close(dst).await.unwrap();
4416            assert_eq!(
4417                std::fs::read(dir.join("dst.txt")).unwrap(),
4418                b"hello copychunk world"
4419            );
4420            std::fs::remove_dir_all(&dir).ok();
4421        });
4422    }
4423
4424    /// A request past the server limits is rejected with the limits echoed.
4425    #[test]
4426    fn copychunk_rejects_oversized_request() {
4427        tokio_uring::start(async {
4428            use smb_server_proto_smb2::commands::copychunk_limits as lim;
4429            let dir = std::env::temp_dir().join(format!("rustsmb_cc_lim_{}", std::process::id()));
4430            std::fs::create_dir_all(&dir).unwrap();
4431            std::fs::write(dir.join("f"), b"x").unwrap();
4432            let (mut conn, vfs) = conn_with_vfs(&dir);
4433            let (f, _m, _a) = vfs.create("f", false, 0x8000_0000, 1, 0, 0).await.unwrap();
4434            let fid = [3u8; 16];
4435            conn.handle_insert(fid, f);
4436            let mut key = [0u8; 24];
4437            key[..16].copy_from_slice(&fid);
4438            conn.resume_keys.insert(key, fid);
4439
4440            let mut input = Vec::new();
4441            input.extend_from_slice(&key);
4442            input.extend_from_slice(&1u32.to_le_bytes());
4443            input.extend_from_slice(&0u32.to_le_bytes());
4444            input.extend_from_slice(&0u64.to_le_bytes());
4445            input.extend_from_slice(&0u64.to_le_bytes());
4446            input.extend_from_slice(&(lim::MAX_CHUNK_SIZE + 1).to_le_bytes()); // over per-chunk cap
4447            input.extend_from_slice(&0u32.to_le_bytes());
4448
4449            let req = c::IoctlReq {
4450                ctl_code: c::fsctl::SRV_COPYCHUNK,
4451                file_id: c::FileId(fid),
4452                input,
4453                max_output: 4096,
4454                is_fsctl: true,
4455            };
4456            let err = do_copychunk(&mut conn, vfs, &req).await.unwrap_err();
4457            assert_eq!(err.0, Status::INVALID_PARAMETER);
4458            assert_eq!(
4459                &err.1[4..8],
4460                &lim::MAX_CHUNK_SIZE.to_le_bytes(),
4461                "limits echoed"
4462            );
4463            std::fs::remove_dir_all(&dir).ok();
4464        });
4465    }
4466
4467    /// FSCTL_SET_ZERO_DATA zeros the requested byte range.
4468    #[test]
4469    fn zero_range_zeros_bytes() {
4470        tokio_uring::start(async {
4471            let dir = std::env::temp_dir().join(format!("rustsmb_zd_{}", std::process::id()));
4472            std::fs::create_dir_all(&dir).unwrap();
4473            std::fs::write(dir.join("z.bin"), vec![0xFFu8; 16]).unwrap();
4474            let (conn, vfs) = conn_with_vfs(&dir);
4475            let (f, _m, _a) = vfs
4476                .create("z.bin", false, 0x4000_0000, 1, 0, 0)
4477                .await
4478                .unwrap();
4479            let fid = [4u8; 16];
4480            conn.handle_insert(fid, f);
4481
4482            let mut open = conn.handle_take(&fid).unwrap();
4483            vfs.zero_range(&mut open, 4, 8).await.expect("zero range");
4484            conn.handle_insert(fid, open);
4485            let f = conn.handle_take(&fid).unwrap();
4486            vfs.close(f).await.unwrap();
4487
4488            let mut expected = vec![0xFFu8; 16];
4489            for b in &mut expected[4..12] {
4490                *b = 0;
4491            }
4492            assert_eq!(std::fs::read(dir.join("z.bin")).unwrap(), expected);
4493            std::fs::remove_dir_all(&dir).ok();
4494        });
4495    }
4496}
4497
4498#[cfg(test)]
4499mod oplock_tests {
4500    use super::*;
4501
4502    fn server_with_share(dir: &std::path::Path) -> Arc<ServerShared> {
4503        use crate::state::*;
4504        let vfs: Arc<dyn smb_server_vfs::Vfs> = Arc::new(smb_server_backend_posix::PosixVfs::new(dir));
4505        let mut shares = HashMap::new();
4506        shares.insert(
4507            "public".to_string(),
4508            Share {
4509                name: "public".into(),
4510                root: dir.to_path_buf(),
4511                vfs,
4512                is_ipc: false,
4513                encrypt: false,
4514                compress: false,
4515                ca: false,
4516            },
4517        );
4518        Arc::new(ServerShared {
4519            shares,
4520            guid: [0; 16],
4521            domain: "W".into(),
4522            server_name: "R".into(),
4523            users: HashMap::new(),
4524            allow_guest: true,
4525            require_signing: false,
4526            encrypt: false,
4527            locks: Arc::new(LockManager::new()),
4528            share_modes: Arc::new(ShareModeTable::new()),
4529            oplocks: Arc::new(OplockTable::new()),
4530            leases: Arc::new(LeaseTable::new()),
4531            durables: Arc::new(smb_server_handle_store::MemStore::new()),
4532            sessions: Arc::new(SessionTable::new()),
4533            app_instances: Arc::new(AppInstanceTable::new()),
4534        })
4535    }
4536
4537    fn create_request(name: &str, oplock: u8, access: u32, share: u32) -> Vec<u8> {
4538        let name16: Vec<u8> = name.encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
4539        let name_off = 64 + 56; // header + fixed body
4540        let mut body = vec![0u8; 56];
4541        body[0..2].copy_from_slice(&57u16.to_le_bytes()); // StructureSize
4542        body[3] = oplock; // RequestedOplockLevel
4543        body[24..28].copy_from_slice(&access.to_le_bytes()); // DesiredAccess
4544        body[32..36].copy_from_slice(&share.to_le_bytes()); // ShareAccess
4545        body[36..40].copy_from_slice(&1u32.to_le_bytes()); // Disposition = OPEN
4546        body[44..46].copy_from_slice(&(name_off as u16).to_le_bytes()); // NameOffset
4547        body[46..48].copy_from_slice(&(name16.len() as u16).to_le_bytes()); // NameLength
4548        let mut f = vec![0u8; 64];
4549        f.extend_from_slice(&body);
4550        f.extend_from_slice(&name16);
4551        f
4552    }
4553
4554    /// The sole opener requesting an oplock gets EXCLUSIVE; a second open on the
4555    /// same file breaks that oplock (a break notification lands on the first
4556    /// connection's outbound queue) and itself gets no oplock.
4557    #[test]
4558    fn oplock_granted_then_broken_on_second_open() {
4559        tokio_uring::start(async {
4560            let dir = std::env::temp_dir().join(format!("rustsmb_op_{}", std::process::id()));
4561            std::fs::create_dir_all(&dir).unwrap();
4562            std::fs::write(dir.join("shared.bin"), b"data").unwrap();
4563            let server = server_with_share(&dir);
4564            let vfs = server.shares["public"].vfs.clone();
4565
4566            let access = 0x8000_0000 | 0x4000_0000; // GENERIC_READ | GENERIC_WRITE
4567            let share = 0x7; // READ|WRITE|DELETE
4568
4569            let (tx_a, mut rx_a) = mpsc::channel(8);
4570            let mut conn_a = Smb2Conn::new([0u8; 8], tx_a);
4571            conn_a.session_id = 1;
4572            let resp_a = create(
4573                &mut conn_a,
4574                vfs.clone(),
4575                &server,
4576                false,
4577                false,
4578                &create_request("shared.bin", c::oplock::BATCH, access, share),
4579            )
4580            .await
4581            .unwrap();
4582            assert_eq!(
4583                resp_a[2],
4584                c::oplock::EXCLUSIVE,
4585                "sole opener granted exclusive"
4586            );
4587
4588            let (tx_b, _rx_b) = mpsc::channel(8);
4589            let mut conn_b = Smb2Conn::new([0u8; 8], tx_b);
4590            conn_b.session_id = 2;
4591            let resp_b = create(
4592                &mut conn_b,
4593                vfs.clone(),
4594                &server,
4595                false,
4596                false,
4597                &create_request("shared.bin", c::oplock::NONE, access, share),
4598            )
4599            .await
4600            .unwrap();
4601            assert_eq!(resp_b[2], c::oplock::NONE, "contending open gets no oplock");
4602
4603            let brk = rx_a.try_recv().expect("break notification delivered to A");
4604            assert_eq!(
4605                u16::from_le_bytes([brk[12], brk[13]]),
4606                ss::cmd::OPLOCK_BREAK,
4607                "frame is an OPLOCK_BREAK",
4608            );
4609            assert_eq!(brk[64 + 2], c::oplock::NONE, "broken down to NONE");
4610
4611            std::fs::remove_dir_all(&dir).ok();
4612        });
4613    }
4614}
4615
4616#[cfg(test)]
4617mod lease_tests {
4618    use super::*;
4619    use crate::state::*;
4620    use std::collections::HashMap;
4621
4622    fn server_with_share(dir: &std::path::Path) -> Arc<ServerShared> {
4623        let vfs: Arc<dyn smb_server_vfs::Vfs> = Arc::new(smb_server_backend_posix::PosixVfs::new(dir));
4624        let mut shares = HashMap::new();
4625        shares.insert(
4626            "public".to_string(),
4627            Share {
4628                name: "public".into(),
4629                root: dir.to_path_buf(),
4630                vfs,
4631                is_ipc: false,
4632                encrypt: false,
4633                compress: false,
4634                ca: false,
4635            },
4636        );
4637        Arc::new(ServerShared {
4638            shares,
4639            guid: [0; 16],
4640            domain: "W".into(),
4641            server_name: "R".into(),
4642            users: HashMap::new(),
4643            allow_guest: true,
4644            require_signing: false,
4645            encrypt: false,
4646            locks: Arc::new(LockManager::new()),
4647            share_modes: Arc::new(ShareModeTable::new()),
4648            oplocks: Arc::new(OplockTable::new()),
4649            leases: Arc::new(LeaseTable::new()),
4650            durables: Arc::new(smb_server_handle_store::MemStore::new()),
4651            sessions: Arc::new(SessionTable::new()),
4652            app_instances: Arc::new(AppInstanceTable::new()),
4653        })
4654    }
4655
4656    /// Build a CREATE request that requests a v1 lease via an `RqLs` context.
4657    fn create_request_lease(
4658        name: &str,
4659        key: [u8; 16],
4660        state: u32,
4661        access: u32,
4662        share: u32,
4663    ) -> Vec<u8> {
4664        let name16: Vec<u8> = name.encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
4665        let name_off = 64 + 56; // header + fixed body
4666        let name_end = name_off + name16.len();
4667        let pad = (8 - (name_end % 8)) % 8; // 8-byte align the context
4668        let ctx_off = name_end + pad;
4669
4670        let mut ctx = Vec::new();
4671        ctx.extend_from_slice(&0u32.to_le_bytes()); // Next
4672        ctx.extend_from_slice(&16u16.to_le_bytes()); // NameOffset
4673        ctx.extend_from_slice(&4u16.to_le_bytes()); // NameLength
4674        ctx.extend_from_slice(&0u16.to_le_bytes()); // Reserved
4675        ctx.extend_from_slice(&24u16.to_le_bytes()); // DataOffset
4676        ctx.extend_from_slice(&32u32.to_le_bytes()); // DataLength (v1)
4677        ctx.extend_from_slice(c::lease::CONTEXT_NAME);
4678        ctx.extend_from_slice(&[0u8; 4]); // pad to 24
4679        ctx.extend_from_slice(&key);
4680        ctx.extend_from_slice(&state.to_le_bytes());
4681        ctx.extend_from_slice(&0u32.to_le_bytes()); // flags
4682        ctx.extend_from_slice(&0u64.to_le_bytes()); // duration
4683
4684        let mut body = vec![0u8; 56];
4685        body[0..2].copy_from_slice(&57u16.to_le_bytes()); // StructureSize
4686        body[3] = c::oplock::LEASE; // RequestedOplockLevel
4687        body[24..28].copy_from_slice(&access.to_le_bytes());
4688        body[32..36].copy_from_slice(&share.to_le_bytes());
4689        body[36..40].copy_from_slice(&1u32.to_le_bytes()); // Disposition = OPEN
4690        body[44..46].copy_from_slice(&(name_off as u16).to_le_bytes());
4691        body[46..48].copy_from_slice(&(name16.len() as u16).to_le_bytes());
4692        body[48..52].copy_from_slice(&(ctx_off as u32).to_le_bytes());
4693        body[52..56].copy_from_slice(&(ctx.len() as u32).to_le_bytes());
4694
4695        let mut f = vec![0u8; 64];
4696        f.extend_from_slice(&body);
4697        f.extend_from_slice(&name16);
4698        f.extend(std::iter::repeat_n(0u8, pad));
4699        f.extend_from_slice(&ctx);
4700        f
4701    }
4702
4703    /// Read the granted LeaseState out of a CREATE response body's `RqLs`
4704    /// context: fixed body 88 + ctx header 24 + LeaseKey 16 = offset 128.
4705    fn granted_state(resp: &[u8]) -> u32 {
4706        u32::from_le_bytes(resp[128..132].try_into().unwrap())
4707    }
4708
4709    /// The sole lease requester gets its full requested state (RWH); a second
4710    /// open with a *different* lease key breaks the holder's write caching down
4711    /// to RH (an unsolicited LEASE_BREAK lands on the first connection) and the
4712    /// contender is granted RH.
4713    #[test]
4714    fn lease_granted_then_broken_on_second_key() {
4715        tokio_uring::start(async {
4716            let dir = std::env::temp_dir().join(format!("rustsmb_ls_{}", std::process::id()));
4717            std::fs::create_dir_all(&dir).unwrap();
4718            std::fs::write(dir.join("leased.bin"), b"data").unwrap();
4719            let server = server_with_share(&dir);
4720            let vfs = server.shares["public"].vfs.clone();
4721
4722            let access = 0x8000_0000 | 0x4000_0000; // GENERIC_READ | GENERIC_WRITE
4723            let share = 0x7; // READ|WRITE|DELETE
4724            let k1 = [0x11u8; 16];
4725            let k2 = [0x22u8; 16];
4726
4727            let (tx_a, mut rx_a) = mpsc::channel(8);
4728            let mut conn_a = Smb2Conn::new([0u8; 8], tx_a);
4729            conn_a.session_id = 1;
4730            let resp_a = create(
4731                &mut conn_a,
4732                vfs.clone(),
4733                &server,
4734                false,
4735                false,
4736                &create_request_lease("leased.bin", k1, c::lease::RWH, access, share),
4737            )
4738            .await
4739            .unwrap();
4740            assert_eq!(resp_a[2], c::oplock::LEASE, "response is a lease grant");
4741            assert_eq!(
4742                granted_state(&resp_a),
4743                c::lease::RWH,
4744                "sole opener gets RWH"
4745            );
4746
4747            let (tx_b, _rx_b) = mpsc::channel(8);
4748            let mut conn_b = Smb2Conn::new([0u8; 8], tx_b);
4749            conn_b.session_id = 2;
4750            let resp_b = create(
4751                &mut conn_b,
4752                vfs.clone(),
4753                &server,
4754                false,
4755                false,
4756                &create_request_lease("leased.bin", k2, c::lease::RWH, access, share),
4757            )
4758            .await
4759            .unwrap();
4760            assert_eq!(
4761                granted_state(&resp_b),
4762                c::lease::RH,
4763                "contender gets read+handle"
4764            );
4765
4766            let brk = rx_a.try_recv().expect("lease break delivered to A");
4767            assert_eq!(
4768                u16::from_le_bytes([brk[12], brk[13]]),
4769                ss::cmd::OPLOCK_BREAK,
4770                "break rides the OPLOCK_BREAK command",
4771            );
4772            assert_eq!(
4773                u16::from_le_bytes([brk[64], brk[65]]),
4774                44,
4775                "lease-break StructureSize"
4776            );
4777            assert_eq!(&brk[72..88], &k1, "names holder A's lease key");
4778            assert_eq!(
4779                u32::from_le_bytes(brk[88..92].try_into().unwrap()),
4780                c::lease::RWH,
4781                "current"
4782            );
4783            assert_eq!(
4784                u32::from_le_bytes(brk[92..96].try_into().unwrap()),
4785                c::lease::RH,
4786                "new"
4787            );
4788
4789            std::fs::remove_dir_all(&dir).ok();
4790        });
4791    }
4792
4793    /// Reopening with the *same* lease key shares the existing state and emits
4794    /// no break notification.
4795    #[test]
4796    fn same_lease_key_reopen_does_not_break() {
4797        tokio_uring::start(async {
4798            let dir = std::env::temp_dir().join(format!("rustsmb_ls2_{}", std::process::id()));
4799            std::fs::create_dir_all(&dir).unwrap();
4800            std::fs::write(dir.join("f.bin"), b"data").unwrap();
4801            let server = server_with_share(&dir);
4802            let vfs = server.shares["public"].vfs.clone();
4803            let access = 0x8000_0000 | 0x4000_0000;
4804            let share = 0x7;
4805            let key = [0x55u8; 16];
4806
4807            let (tx_a, mut rx_a) = mpsc::channel(8);
4808            let mut conn_a = Smb2Conn::new([0u8; 8], tx_a);
4809            conn_a.session_id = 1;
4810            let _ = create(
4811                &mut conn_a,
4812                vfs.clone(),
4813                &server,
4814                false,
4815                false,
4816                &create_request_lease("f.bin", key, c::lease::RWH, access, share),
4817            )
4818            .await
4819            .unwrap();
4820
4821            let (tx_b, _rx_b) = mpsc::channel(8);
4822            let mut conn_b = Smb2Conn::new([0u8; 8], tx_b);
4823            conn_b.session_id = 2;
4824            let resp_b = create(
4825                &mut conn_b,
4826                vfs.clone(),
4827                &server,
4828                false,
4829                false,
4830                &create_request_lease("f.bin", key, c::lease::RWH, access, share),
4831            )
4832            .await
4833            .unwrap();
4834
4835            assert_eq!(granted_state(&resp_b), c::lease::RWH, "same key keeps RWH");
4836            assert!(rx_a.try_recv().is_err(), "no break for same lease key");
4837
4838            std::fs::remove_dir_all(&dir).ok();
4839        });
4840    }
4841}
4842
4843#[cfg(test)]
4844mod security_tests {
4845    use super::*;
4846    use win_sd::{AccessMask, SecurityDescriptor, SecurityDescriptorBuilder, Sid};
4847
4848    fn query_info_frame(
4849        info_type: u8,
4850        class: u8,
4851        output_len: u32,
4852        additional: u32,
4853        fid: [u8; 16],
4854    ) -> Vec<u8> {
4855        let mut b = vec![0u8; 40];
4856        b[0..2].copy_from_slice(&41u16.to_le_bytes()); // StructureSize
4857        b[2] = info_type;
4858        b[3] = class;
4859        b[4..8].copy_from_slice(&output_len.to_le_bytes());
4860        b[16..20].copy_from_slice(&additional.to_le_bytes()); // AdditionalInformation
4861        b[24..40].copy_from_slice(&fid); // FileId
4862        let mut f = vec![0u8; 64];
4863        f.extend_from_slice(&b);
4864        f
4865    }
4866
4867    fn set_info_frame(
4868        info_type: u8,
4869        class: u8,
4870        additional: u32,
4871        fid: [u8; 16],
4872        buffer: &[u8],
4873    ) -> Vec<u8> {
4874        let mut b = vec![0u8; 32];
4875        b[0..2].copy_from_slice(&33u16.to_le_bytes()); // StructureSize
4876        b[2] = info_type;
4877        b[3] = class;
4878        b[4..8].copy_from_slice(&(buffer.len() as u32).to_le_bytes()); // BufferLength
4879        b[8..10].copy_from_slice(&((64 + 32) as u16).to_le_bytes()); // BufferOffset (absolute)
4880        b[12..16].copy_from_slice(&additional.to_le_bytes()); // AdditionalInformation
4881        b[16..32].copy_from_slice(&fid); // FileId
4882        let mut f = vec![0u8; 64];
4883        f.extend_from_slice(&b);
4884        f.extend_from_slice(buffer);
4885        f
4886    }
4887
4888    /// A fresh file yields a synthesised default descriptor; setting a custom
4889    /// descriptor and querying it back round-trips through the backend store.
4890    #[test]
4891    fn security_query_set_round_trip() {
4892        tokio_uring::start(async {
4893            let dir = std::env::temp_dir().join(format!("rustsmb_sec_{}", std::process::id()));
4894            std::fs::create_dir_all(&dir).unwrap();
4895            std::fs::write(dir.join("sec.bin"), b"data").unwrap();
4896            let vfs: Arc<dyn smb_server_vfs::Vfs> = Arc::new(smb_server_backend_posix::PosixVfs::new(&dir));
4897            let (tx, _rx) = mpsc::channel(8);
4898            let mut conn = Smb2Conn::new([0u8; 8], tx);
4899            let (open, _m, _a) = vfs
4900                .create("sec.bin", false, 0x8000_0000 | 0x4000_0000, 1, 0, 0)
4901                .await
4902                .unwrap();
4903            let fid = [1u8; 16];
4904            conn.handle_insert(fid, open);
4905
4906            // Default query: parseable, DACL present.
4907            let q = query_info_frame(
4908                c::info_type::SECURITY,
4909                0,
4910                4096,
4911                crate::security::sec_info::DEFAULT,
4912                fid,
4913            );
4914            let out = query_info(&mut conn, vfs.clone(), &q)
4915                .await
4916                .unwrap()
4917                .unwrap();
4918            let sd = SecurityDescriptor::from_bytes(&out).expect("default parse");
4919            assert!(sd.dacl().is_some(), "default DACL present");
4920
4921            // Set a custom descriptor, then read the owner back.
4922            let custom = SecurityDescriptorBuilder::new()
4923                .owner(Sid::local_system())
4924                .allow(Sid::everyone(), AccessMask::FILE_GENERIC_READ)
4925                .build()
4926                .to_bytes()
4927                .unwrap();
4928            let s = set_info_frame(
4929                c::info_type::SECURITY,
4930                0,
4931                crate::security::sec_info::OWNER | crate::security::sec_info::DACL,
4932                fid,
4933                &custom,
4934            );
4935            set_info(&mut conn, vfs.clone(), &s)
4936                .await
4937                .expect("set security");
4938
4939            let q2 = query_info_frame(
4940                c::info_type::SECURITY,
4941                0,
4942                4096,
4943                crate::security::sec_info::OWNER,
4944                fid,
4945            );
4946            let out2 = query_info(&mut conn, vfs.clone(), &q2)
4947                .await
4948                .unwrap()
4949                .unwrap();
4950            let sd2 = SecurityDescriptor::from_bytes(&out2).expect("stored parse");
4951            assert_eq!(
4952                sd2.owner(),
4953                Some(&Sid::local_system()),
4954                "stored owner round-trips"
4955            );
4956
4957            std::fs::remove_dir_all(&dir).ok();
4958        });
4959    }
4960
4961    /// A too-small output buffer is rejected with STATUS_BUFFER_TOO_SMALL.
4962    #[test]
4963    fn security_query_rejects_small_buffer() {
4964        tokio_uring::start(async {
4965            let dir = std::env::temp_dir().join(format!("rustsmb_sec2_{}", std::process::id()));
4966            std::fs::create_dir_all(&dir).unwrap();
4967            std::fs::write(dir.join("s.bin"), b"x").unwrap();
4968            let vfs: Arc<dyn smb_server_vfs::Vfs> = Arc::new(smb_server_backend_posix::PosixVfs::new(&dir));
4969            let (tx, _rx) = mpsc::channel(8);
4970            let mut conn = Smb2Conn::new([0u8; 8], tx);
4971            let (open, _m, _a) = vfs
4972                .create("s.bin", false, 0x8000_0000, 1, 0, 0)
4973                .await
4974                .unwrap();
4975            let fid = [2u8; 16];
4976            conn.handle_insert(fid, open);
4977
4978            let q = query_info_frame(
4979                c::info_type::SECURITY,
4980                0,
4981                8,
4982                crate::security::sec_info::DEFAULT,
4983                fid,
4984            );
4985            let err = query_info(&mut conn, vfs.clone(), &q).await.unwrap_err();
4986            assert_eq!(err, Status::BUFFER_TOO_SMALL);
4987
4988            std::fs::remove_dir_all(&dir).ok();
4989        });
4990    }
4991}
4992
4993#[cfg(test)]
4994mod durable_tests {
4995    use super::*;
4996    use crate::state::*;
4997    use std::collections::HashMap;
4998
4999    fn server_with_share(dir: &std::path::Path) -> Arc<ServerShared> {
5000        let vfs: Arc<dyn smb_server_vfs::Vfs> = Arc::new(smb_server_backend_posix::PosixVfs::new(dir));
5001        let mut shares = HashMap::new();
5002        shares.insert(
5003            "public".to_string(),
5004            Share {
5005                name: "public".into(),
5006                root: dir.to_path_buf(),
5007                vfs,
5008                is_ipc: false,
5009                encrypt: false,
5010                compress: false,
5011                ca: false,
5012            },
5013        );
5014        Arc::new(ServerShared {
5015            shares,
5016            guid: [0; 16],
5017            domain: "W".into(),
5018            server_name: "R".into(),
5019            users: HashMap::new(),
5020            allow_guest: true,
5021            require_signing: false,
5022            encrypt: false,
5023            locks: Arc::new(LockManager::new()),
5024            share_modes: Arc::new(ShareModeTable::new()),
5025            oplocks: Arc::new(OplockTable::new()),
5026            leases: Arc::new(LeaseTable::new()),
5027            durables: Arc::new(smb_server_handle_store::MemStore::new()),
5028            sessions: Arc::new(SessionTable::new()),
5029            app_instances: Arc::new(AppInstanceTable::new()),
5030        })
5031    }
5032
5033    /// Build a CREATE frame carrying a single create-context (name, data).
5034    fn create_req_ctx(
5035        name: &str,
5036        access: u32,
5037        disp: u32,
5038        ctx_name: &[u8],
5039        ctx_data: &[u8],
5040    ) -> Vec<u8> {
5041        let name16: Vec<u8> = name.encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
5042        let name_off = 64 + 56;
5043        let name_end = name_off + name16.len();
5044        let pad = (8 - name_end % 8) % 8;
5045        let ctx_off = name_end + pad;
5046
5047        let data_off = (16 + ctx_name.len()).next_multiple_of(8);
5048        let mut ctx = Vec::new();
5049        ctx.extend_from_slice(&0u32.to_le_bytes());
5050        ctx.extend_from_slice(&16u16.to_le_bytes());
5051        ctx.extend_from_slice(&(ctx_name.len() as u16).to_le_bytes());
5052        ctx.extend_from_slice(&0u16.to_le_bytes());
5053        ctx.extend_from_slice(&(data_off as u16).to_le_bytes());
5054        ctx.extend_from_slice(&(ctx_data.len() as u32).to_le_bytes());
5055        ctx.extend_from_slice(ctx_name);
5056        while ctx.len() < data_off {
5057            ctx.push(0);
5058        }
5059        ctx.extend_from_slice(ctx_data);
5060
5061        let mut body = vec![0u8; 56];
5062        body[0..2].copy_from_slice(&57u16.to_le_bytes());
5063        body[24..28].copy_from_slice(&access.to_le_bytes());
5064        body[32..36].copy_from_slice(&0x7u32.to_le_bytes());
5065        body[36..40].copy_from_slice(&disp.to_le_bytes());
5066        body[44..46].copy_from_slice(&(name_off as u16).to_le_bytes());
5067        body[46..48].copy_from_slice(&(name16.len() as u16).to_le_bytes());
5068        body[48..52].copy_from_slice(&(ctx_off as u32).to_le_bytes());
5069        body[52..56].copy_from_slice(&(ctx.len() as u32).to_le_bytes());
5070        let mut f = vec![0u8; 64];
5071        f.extend_from_slice(&body);
5072        f.extend_from_slice(&name16);
5073        f.extend(std::iter::repeat_n(0u8, pad));
5074        f.extend_from_slice(&ctx);
5075        f
5076    }
5077
5078    /// A DH2Q durable request is granted (recorded + echoed); after the
5079    /// connection drops the handle is preserved and a DH2C reconnect on a fresh
5080    /// connection reclaims it under the same persistent id.
5081    #[test]
5082    fn durable_grant_then_reconnect() {
5083        tokio_uring::start(async {
5084            let dir = std::env::temp_dir().join(format!("rustsmb_dur_{}", std::process::id()));
5085            std::fs::create_dir_all(&dir).unwrap();
5086            std::fs::write(dir.join("dur.bin"), b"durable-data").unwrap();
5087            let server = server_with_share(&dir);
5088            let vfs = server.shares["public"].vfs.clone();
5089            let guid = [0x5Au8; 16];
5090            let access = 0x8000_0000 | 0x4000_0000;
5091
5092            let mut dq = Vec::new();
5093            dq.extend_from_slice(&10_000u32.to_le_bytes()); // Timeout
5094            dq.extend_from_slice(&0u32.to_le_bytes()); // Flags
5095            dq.extend_from_slice(&[0u8; 8]); // Reserved
5096            dq.extend_from_slice(&guid); // CreateGuid
5097            let frame = create_req_ctx("dur.bin", access, 1, c::durable::REQ_V2, &dq);
5098
5099            let (tx, _rx) = mpsc::channel(8);
5100            let mut conn = Smb2Conn::new([0u8; 8], tx);
5101            conn.session_id = 1;
5102            let mut frame = frame;
5103            frame[67] = c::oplock::BATCH; // durable requires a batch oplock
5104            let resp = create(&mut conn, vfs.clone(), &server, false, false, &frame)
5105                .await
5106                .unwrap();
5107            assert_ne!(
5108                u32::from_le_bytes(resp[80..84].try_into().unwrap()),
5109                0,
5110                "durable resp ctx"
5111            );
5112            assert_eq!(conn.durable.len(), 1, "durable handle recorded");
5113            let pid: [u8; 16] = resp[64..80].try_into().unwrap();
5114
5115            // Simulate the connection dropping: preserve into the server table.
5116            for (_f, e) in conn.durable.drain() {
5117                let _ = server
5118                    .durables
5119                    .put(e.into_record(crate::state::now_ms()))
5120                    .await;
5121            }
5122
5123            // Fresh connection reclaims the handle via DH2C reconnect.
5124            let mut dc = Vec::new();
5125            dc.extend_from_slice(&pid);
5126            dc.extend_from_slice(&guid);
5127            dc.extend_from_slice(&0u32.to_le_bytes()); // Flags
5128            let rframe = create_req_ctx("dur.bin", 0x8000_0000, 1, c::durable::RECONNECT_V2, &dc);
5129            let (tx2, _rx2) = mpsc::channel(8);
5130            let mut conn2 = Smb2Conn::new([0u8; 8], tx2);
5131            conn2.session_id = 2;
5132            let resp2 = create(&mut conn2, vfs.clone(), &server, false, false, &rframe)
5133                .await
5134                .expect("reconnect");
5135            let rid: [u8; 16] = resp2[64..80].try_into().unwrap();
5136            assert_eq!(rid, pid, "reconnect returns the persistent id");
5137            assert!(
5138                conn2.handle_exists(&pid),
5139                "handle reinstated on new connection"
5140            );
5141
5142            std::fs::remove_dir_all(&dir).ok();
5143        });
5144    }
5145
5146    /// A reconnect with an unknown persistent id is rejected.
5147    #[test]
5148    fn durable_reconnect_unknown_id_fails() {
5149        tokio_uring::start(async {
5150            let dir = std::env::temp_dir().join(format!("rustsmb_dur2_{}", std::process::id()));
5151            std::fs::create_dir_all(&dir).unwrap();
5152            std::fs::write(dir.join("x.bin"), b"data").unwrap();
5153            let server = server_with_share(&dir);
5154            let vfs = server.shares["public"].vfs.clone();
5155            let mut dc = Vec::new();
5156            dc.extend_from_slice(&[0xEE; 16]);
5157            dc.extend_from_slice(&[0xEE; 16]);
5158            dc.extend_from_slice(&0u32.to_le_bytes());
5159            let rframe = create_req_ctx("x.bin", 0x8000_0000, 1, c::durable::RECONNECT_V2, &dc);
5160            let (tx, _rx) = mpsc::channel(8);
5161            let mut conn = Smb2Conn::new([0u8; 8], tx);
5162            let err = create(&mut conn, vfs.clone(), &server, false, false, &rframe)
5163                .await
5164                .unwrap_err();
5165            assert_eq!(err, Status::OBJECT_NAME_NOT_FOUND);
5166            std::fs::remove_dir_all(&dir).ok();
5167        });
5168    }
5169}
5170
5171#[cfg(test)]
5172mod interface_tests {
5173    use super::*;
5174    use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
5175
5176    #[test]
5177    fn interface_list_chains_and_encodes_sockaddr() {
5178        let entries = [
5179            (1u32, IpAddr::V4(Ipv4Addr::new(10, 0, 0, 5))),
5180            (2u32, IpAddr::V6(Ipv6Addr::LOCALHOST)),
5181        ];
5182        let buf = build_interface_list(&entries);
5183        assert_eq!(buf.len(), 152 * 2, "two fixed-size entries");
5184        // First entry: Next links to the second (152), IfIndex 1, AF_INET.
5185        assert_eq!(u32::from_le_bytes(buf[0..4].try_into().unwrap()), 152);
5186        assert_eq!(u32::from_le_bytes(buf[4..8].try_into().unwrap()), 1);
5187        assert_eq!(
5188            u16::from_le_bytes(buf[24..26].try_into().unwrap()),
5189            2,
5190            "AF_INET"
5191        );
5192        assert_eq!(&buf[28..32], &[10, 0, 0, 5], "sin_addr");
5193        // Second entry terminates the chain and carries AF_INET6.
5194        assert_eq!(u32::from_le_bytes(buf[152..156].try_into().unwrap()), 0);
5195        assert_eq!(
5196            u16::from_le_bytes(buf[152 + 24..152 + 26].try_into().unwrap()),
5197            23,
5198            "AF_INET6"
5199        );
5200    }
5201}
5202
5203#[cfg(test)]
5204mod signing_tests {
5205    use super::*;
5206
5207    #[test]
5208    fn sign_then_verify_round_trips_and_detects_tamper() {
5209        let key = [0x42u8; 16];
5210        let dialect = Some(smb_server_proto_smb2::negotiate::DIALECT_311);
5211        let algo = smb_server_proto_smb2::negotiate::ctx_type::SIGNING_AES128_CMAC;
5212        let mut pdu = vec![0u8; 96];
5213        pdu[0..4].copy_from_slice(&smb_server_proto_smb2::SMB2_MAGIC);
5214        for (i, b) in pdu[64..].iter_mut().enumerate() {
5215            *b = i as u8; // arbitrary body bytes
5216        }
5217        sign_pdu(&mut pdu, &key, dialect, algo);
5218        assert!(
5219            verify_pdu_signature(&pdu, &key, dialect, algo),
5220            "valid signature verifies"
5221        );
5222        // Tamper with the body: verification must fail.
5223        pdu[70] ^= 0xFF;
5224        assert!(
5225            !verify_pdu_signature(&pdu, &key, dialect, algo),
5226            "tampered body rejected"
5227        );
5228        // Wrong key: fails.
5229        pdu[70] ^= 0xFF;
5230        assert!(
5231            !verify_pdu_signature(&pdu, &[0u8; 16], dialect, algo),
5232            "wrong key rejected"
5233        );
5234    }
5235}