Skip to main content

rustsmb/
dispatch.rs

1//! Per-connection request context and async frame loop.
2//!
3//! [`IoContext`] bundles the refcounted server metadata a handler needs —
4//! server config plus connection bookkeeping — together with the SMB command
5//! currently being served. Command routing lives in [`crate::cmds`].
6
7use std::sync::Arc;
8
9use smb_server_proto::types::Status;
10use smb_server_proto_smb1::consts;
11use smb_server_proto_smb1::header::{Header, RespBody, build_response, parse_header};
12
13use smb_server_proto_smb1::consts::flags2;
14use smb_server_proto_smb2::{PROTO_ID_COMPRESSED, PROTO_ID_ENCRYPTED, PROTO_ID_SMB2};
15use smb_server_transport::{FrameSink, Transport};
16
17use metrics::{counter, histogram};
18use tokio::sync::mpsc;
19
20use crate::state::{ConnState, ServerShared};
21
22/// Depth of the per-connection outbound frame queue.
23const OUTBOUND_QUEUE_DEPTH: usize = 64;
24
25/// Short alias used by command handlers.
26pub type IoCtx<'a> = IoContext<'a>;
27
28/// Bundled per-request context handed to every command handler.
29pub struct IoContext<'a> {
30    /// Refcounted global configuration and user database.
31    pub server: Arc<ServerShared>,
32    /// Connection bookkeeping (challenge, session, trees, handles…).
33    pub conn: &'a mut ConnState,
34}
35
36/// A parsed view over one request body inside the received frame.
37#[derive(Debug)]
38pub struct ReqView<'a> {
39    /// Request header (echoed into the response).
40    pub hdr: Header,
41    /// WordCount value.
42    pub wct: usize,
43    /// Parameter words as raw bytes.
44    pub words: &'a [u8],
45    /// ByteCount data section.
46    pub data: &'a [u8],
47    /// Absolute frame offset of ByteCount.
48    pub bc_off_abs: usize,
49    /// The complete received frame (needed by commands whose payloads are
50    /// addressed with absolute offsets, e.g. WRITE_ANDX).
51    pub frame: &'a [u8],
52}
53
54impl<'a> ReqView<'a> {
55    /// True when FLAGS2_UNICODE is set on the request header.
56    pub fn unicode(&self) -> bool {
57        self.hdr.flags2 & flags2::UNICODE != 0
58    }
59
60    /// AndX successor command byte when chaining continues past this request.
61    pub fn andx_command(&self) -> Option<u8> {
62        if self.wct < consts::andx::MIN_WCT {
63            return None;
64        }
65        self.words
66            .first()
67            .copied()
68            .filter(|c| *c != consts::ANDX_NONE)
69    }
70
71    /// AndXOffset of the successor body relative to the SMB header start.
72    pub fn andx_offset(&self) -> Option<usize> {
73        if self.wct < consts::andx::MIN_WCT_OFFSET || self.andx_command().is_none() {
74            return None;
75        }
76        Some(u16::from_le_bytes([
77            *self.words.get(consts::andx::OFFSET_POS)?,
78            *self.words.get(consts::andx::OFFSET_POS + 1)?,
79        ]) as usize)
80    }
81}
82
83/// Drain the outbound frame queue into the connection's write half. Runs as a
84/// dedicated task so background work can emit unsolicited frames (async
85/// STATUS_PENDING completions, oplock/lease breaks, CHANGE_NOTIFY) while the
86/// reader blocks on `recv`. Ends when every [`mpsc::Sender`] clone is dropped.
87pub(crate) async fn writer_loop(
88    mut writer: Box<dyn FrameSink>,
89    mut out_rx: mpsc::Receiver<Vec<u8>>,
90) {
91    while let Some(frame) = out_rx.recv().await {
92        if writer.send(&frame).await.is_err() {
93            break;
94        }
95    }
96}
97
98/// Serve one client connection until EOF or transport error: read NBSS frames,
99/// route SMB1 and SMB2/3 (including encrypted/compressed transforms) to their
100/// processors, and release the session's locks and durable handles on close.
101pub async fn serve_client(server: Arc<crate::state::ServerShared>, transport: Box<dyn Transport>) {
102    let (mut reader, writer) = transport.split();
103    let (out_tx, out_rx) = mpsc::channel::<Vec<u8>>(OUTBOUND_QUEUE_DEPTH);
104    let writer_task = tokio_uring::spawn(writer_loop(writer, out_rx));
105
106    {
107        let challenge = rand_challenge();
108        let mut conn = ConnState::new(challenge);
109        let mut smb2_conn: Option<crate::smb2::Smb2Conn> = None;
110
111        loop {
112            let Some(frame) = (match reader.recv().await {
113                Ok(Some(f)) => Some(f),
114                Ok(None) | Err(_) => break,
115            }) else {
116                break;
117            };
118            if frame.0.len() < smb_server_proto_smb2::SMB2_MAGIC.len() {
119                continue;
120            }
121
122            counter!("smb_nbss_frames_total").increment(1);
123
124            // SMB2/3 frames (\xFESMB magic), encrypted transform frames
125            // (\xFD"SMB" — [MS-SMB2] §2.2.41) and compressed transform frames
126            // (\xFC"SMB" — §2.2.42).
127            if frame.0[0] == PROTO_ID_SMB2
128                || frame.0[0] == PROTO_ID_ENCRYPTED
129                || frame.0[0] == PROTO_ID_COMPRESSED
130                || conn.upgraded_smb2
131            {
132                // Once a dialect has been negotiated, a stray SMB1 frame (a
133                // second multi-protocol NEGOTIATE) is invalid: the server MUST
134                // disconnect and not reply ([MS-SMB2] §3.3.5.4).
135                if conn.upgraded_smb2
136                    && frame.0[0] != PROTO_ID_SMB2
137                    && frame.0[0] != PROTO_ID_ENCRYPTED
138                    && frame.0[0] != PROTO_ID_COMPRESSED
139                {
140                    break;
141                }
142                if smb2_conn.is_none() {
143                    smb2_conn = Some(crate::smb2::Smb2Conn::new(rand_challenge(), out_tx.clone()));
144                }
145                let c2 = smb2_conn.as_mut().unwrap();
146                // Decompress an SMB3 compressed transform before dispatch. A
147                // frame that fails to decompress (bad length or unsupported
148                // algorithm) or whose decompressed payload is not a valid SMB2
149                // message is fatal: the server MUST disconnect the connection
150                // ([MS-SMB2] §3.3.5.2.13).
151                let decompressed;
152                let msg: &[u8] = if frame.0[0] == PROTO_ID_COMPRESSED {
153                    match smb_server_proto_smb2::compress::decompress_message(&frame.0) {
154                        Some(d) if d.first() == Some(&PROTO_ID_SMB2) => {
155                            decompressed = d;
156                            &decompressed
157                        }
158                        _ => break,
159                    }
160                } else {
161                    &frame.0
162                };
163                let start = std::time::Instant::now();
164                if let Some(mut resp) = crate::smb2::process_frame(&server, c2, msg).await {
165                    // Compress a plaintext READ response only when the client
166                    // asked for it (SMB2_READFLAG_REQUEST_COMPRESSED); Windows
167                    // does not compress responses opportunistically, and doing
168                    // so breaks peers that reject a chained payload filling the
169                    // buffer. compress_message* wraps only when it shrinks.
170                    if let Some(algo) = c2.compress_algo
171                        && c2.compress_response
172                            && resp.first() != Some(&PROTO_ID_ENCRYPTED)
173                        {
174                            let packed = if c2.compress_chained {
175                                smb_server_proto_smb2::compress::compress_message_chained(&resp, algo)
176                            } else {
177                                smb_server_proto_smb2::compress::compress_message(&resp, algo)
178                            };
179                            if let Some(packed) = packed {
180                                resp = packed;
181                            }
182                        }
183                    histogram!("smb_frame_duration_us").record(start.elapsed().as_micros() as f64);
184                    counter!("smb_responses_total").increment(1);
185                    if out_tx.send(resp).await.is_err() {
186                        break;
187                    }
188                }
189                // A handler may ask to terminate the connection (e.g. a failed
190                // FSCTL_VALIDATE_NEGOTIATE_INFO downgrade check, [MS-SMB2]
191                // §3.3.5.15.12).
192                if c2.disconnect {
193                    break;
194                }
195                continue;
196            }
197
198            // SMB1 frames.
199            if let Some(resp) = process_frame(&server, &mut conn, &frame.0).await
200                && out_tx.send(resp).await.is_err() {
201                    break;
202                }
203        }
204
205        // Connection closing: drop any byte-range locks this session held.
206        if let Some(c2) = &mut smb2_conn {
207            if c2.session_id != 0 {
208                server.locks.release_session(c2.session_id);
209                server.share_modes.close_session(c2.session_id);
210                server.oplocks.release_session(c2.session_id);
211                server.app_instances.close_session(c2.session_id);
212                server.leases.release_session(c2.session_id);
213            }
214            // Preserve durable handles so a later connection can reclaim them
215            // ([MS-SMB2] §3.3.7.1).
216            let now_ms = crate::state::now_ms();
217            for (_fid, entry) in c2.durable.drain() {
218                let _ = server.durables.put(entry.into_record(now_ms)).await;
219            }
220        }
221    }
222
223    // The read loop has ended: drop our sender so the writer task sees every
224    // clone gone and shuts down, then join it.
225    drop(out_tx);
226    let _ = writer_task.await;
227}
228
229/// Cryptographically random per-connection NTLM challenge.
230pub fn rand_challenge_pub() -> [u8; 8] {
231    rand_challenge()
232}
233
234/// Cryptographically random bytes (urandom with time fallback).
235pub fn rand_bytes(n: usize) -> Vec<u8> {
236    use std::io::Read;
237    let mut buf = vec![0u8; n];
238    if let Ok(mut f) = std::fs::File::open("/dev/urandom")
239        && f.read_exact(&mut buf).is_ok() {
240            return buf;
241        }
242    let nanos = std::time::SystemTime::now()
243        .duration_since(std::time::UNIX_EPOCH)
244        .unwrap_or_default()
245        .as_nanos()
246        .to_le_bytes();
247    for (i, b) in buf.iter_mut().enumerate() {
248        *b = nanos[i % nanos.len()];
249    }
250    buf
251}
252
253/// Cryptographically random per-connection NTLM challenge.
254fn rand_challenge() -> [u8; 8] {
255    use std::io::Read;
256    let mut b = [0u8; 8];
257    if let Ok(mut f) = std::fs::File::open("/dev/urandom")
258        && f.read_exact(&mut b).is_ok() {
259            return b;
260        }
261    let nanos = std::time::SystemTime::now()
262        .duration_since(std::time::UNIX_EPOCH)
263        .unwrap_or_default()
264        .as_nanos()
265        .to_le_bytes();
266    let n = b.len();
267    b.copy_from_slice(&nanos[..n]);
268    b
269}
270
271/// Execute one frame and build its response (`None` to stay silent).
272pub(crate) async fn process_frame(
273    server: &Arc<crate::state::ServerShared>,
274    conn: &mut ConnState,
275    buf: &[u8],
276) -> Option<Vec<u8>> {
277    let (hdr, wc_off) = parse_header(buf)?;
278
279    // NT_CANCEL never produces a normal body exchange.
280    if hdr.command == consts::COM_NT_CANCEL {
281        return Some(build_response(&hdr, Status::CANCELLED, Vec::new()));
282    }
283
284    // Multi-protocol upgrade ([MS-SMB2] §3.2.4.2): a client may send an SMB1
285    // NEGOTIATE whose dialect list carries the "SMB 2.002"/"SMB 2.???" strings
286    // (Windows clients do this), or embed the \xFESMB id. Either way, answer
287    // with an SMB2 NEGOTIATE and route later frames through the SMB2 processor.
288    if hdr.command == consts::COM_NEGOTIATE
289        && !conn.upgraded_smb2
290        && (buf
291            .windows(smb_server_proto_smb2::SMB2_MAGIC.len())
292            .any(|w| w == smb_server_proto_smb2::SMB2_MAGIC)
293            || buf.windows(b"SMB 2.".len()).any(|w| w == b"SMB 2."))
294        && let Some(resp) = crate::smb2::handle_multiprotocol_negotiate(buf, &server.guid) {
295            conn.upgraded_smb2 = true;
296            tracing::info!("client upgraded to SMB2 via multi-protocol negotiate");
297            return Some(resp);
298        }
299
300    let wct = buf[wc_off] as usize;
301    let _bc_off_abs = wc_off + 1 + wct * consts::WORD_LEN;
302
303    // Build the AndX chain view over this single frame buffer.
304    let mut views: Vec<ReqView> = Vec::new();
305    let mut off = wc_off;
306    while off < buf.len() {
307        let cwct = buf[off] as usize;
308        let cbc = off + 1 + cwct * consts::WORD_LEN;
309        if cbc + consts::BYTE_COUNT_LEN > buf.len() {
310            break;
311        }
312        let words = &buf[off + 1..off + 1 + cwct * consts::WORD_LEN];
313        views.push(ReqView {
314            hdr: hdr.clone(),
315            wct: cwct,
316            words,
317            data: &buf[cbc + consts::BYTE_COUNT_LEN..],
318            bc_off_abs: cbc,
319            frame: buf,
320        });
321        let next_cmd = words.first().copied().unwrap_or(consts::ANDX_NONE);
322        let next_off = u16::from_le_bytes([
323            *words.get(consts::andx::OFFSET_POS).unwrap_or(&0),
324            *words.get(consts::andx::OFFSET_POS + 1).unwrap_or(&0),
325        ]) as usize;
326        off = if next_cmd != consts::ANDX_NONE && next_off > off && next_off < buf.len() {
327            next_off
328        } else {
329            break;
330        };
331    }
332
333    let mut io = IoContext {
334        server: server.clone(),
335        conn,
336    };
337    let mut bodies: Vec<RespBody> = Vec::new();
338    let mut last_status = Status::SUCCESS;
339    let mut final_hdr = hdr.clone();
340
341    for req in &views {
342        match crate::cmds::dispatch_one(&mut io, req, &mut bodies).await {
343            Ok(st) => last_status = st,
344            Err(status) => {
345                tracing::warn!(
346                    cmd = format!("{:#06x}", req.hdr.command),
347                    tid = format!("{:#06x}", req.hdr.tid),
348                    uid = format!("{:#06x}", req.hdr.uid),
349                    status = format!("{:08x}", status.raw()),
350                    "handler error"
351                );
352                counter!("smb_handler_errors_total").increment(1);
353                final_hdr = req.hdr.clone();
354                last_status = status;
355                bodies.clear();
356                break;
357            }
358        }
359    }
360
361    Some(build_response(&final_hdr, last_status, bodies))
362}