Skip to main content

rustsmb/cmds/
session.rs

1//! Negotiate / session setup / tree connect / echo handlers.
2
3use smb_server_proto::types::{FileTime, Status};
4use smb_server_proto_smb1::consts::{self, caps};
5use smb_server_proto_smb1::header::RespBody;
6use smb_server_proto_smb1::negotiate;
7use smb_server_proto_smb1::session_setup as ss;
8use smb_server_proto_smb1::tree_connect as tc;
9
10use crate::auth::authenticate_ntlmssp;
11use crate::dispatch::{IoCtx, IoContext, ReqView};
12use crate::state::{next_tid, next_uid, Session};
13
14fn crate_ext_sec() -> u32 {
15    smb_server_proto_smb1::consts::caps::CAP_EXTENDED_SECURITY
16}
17
18/// Our negotiated capability set.
19pub fn our_caps() -> u32 {
20    caps::CAP_UNICODE
21        | caps::CAP_LARGE_FILES
22        | caps::CAP_NT_SMBS
23        | caps::CAP_STATUS32
24        | caps::CAP_LEVEL2_OPLOCKS
25        | caps::CAP_LOCK_AND_READ
26        | caps::CAP_NT_FIND
27        | caps::CAP_INFOLEVEL_PASSTHRU
28        | caps::CAP_LARGE_READX
29        | caps::CAP_LARGE_WRITEX
30}
31
32/// NEGOTIATE: pick dialect, advertise capabilities, hand out the challenge.
33pub fn negotiate(
34    io: &IoCtx<'_>,
35    req: &ReqView,
36    bodies: &mut Vec<RespBody>,
37) -> Result<Status, Status> {
38    let parsed = negotiate::NegotiateReq::parse(req.data);
39    let idx = parsed.select().ok_or(Status::INVALID_PARAMETER)?;
40    // Extended security: ChallengeLength=0, data = 16-byte server GUID.
41    // (Session setup then runs the SPNEGO/NTLMSSP two-leg exchange.)
42    let body = RespBody::new(
43        consts::COM_NEGOTIATE,
44        negotiate::build_params(idx as u16, our_caps() | crate_ext_sec(), FileTime::now()),
45        io.server.guid.to_vec(),
46    );
47    *bodies = vec![body];
48    Ok(Status::SUCCESS)
49}
50
51/// SESSION_SETUP_ANDX extended security: NTLMSSP two-leg flow.
52#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB1 SESSION_SETUP_ANDX wire layout
53pub fn setup(
54    io: &mut IoContext,
55    req: &ReqView,
56    bodies: &mut Vec<RespBody>,
57) -> Result<Status, Status> {
58    let conn = &mut io.conn;
59    let wct = req.wct;
60    // WC=12 → extended security (opaque blob); WC=13 → legacy passwords.
61    let ext = wct == 12;
62    let (blob, _legacy) = if ext {
63        let e = ss::SessionSetupExtReq::parse(req.words, req.data)
64            .map_err(|_| Status::LOGON_FAILURE)?;
65        (e.blob, None)
66    } else {
67        let l = ss::SessionSetupLegacyReq::parse(
68            req.words,
69            req.data,
70            req.unicode(),
71            req.bc_off_abs + 2,
72        )
73        .map_err(|_| Status::LOGON_FAILURE)?;
74        (l.nt_resp.clone(), Some(l))
75    };
76
77    let parsed_nt = blob.clone();
78    let inner = smb_server_auth::ntlm::unwrap_blob(&parsed_nt).unwrap_or(&[]);
79    match smb_server_auth::ntlm::msg_type(inner) {
80        Some(smb_server_auth::ntlm::MSG_TYPE1) | Some(smb_server_auth::ntlm::MSG_TYPE2) | None => {
81            // Leg 1 — issue the CHALLENGE.
82            let uid = if conn.uid != 0 { conn.uid } else { next_uid() };
83            conn.uid = uid;
84            conn.auth_pending = true;
85            conn.spnego = smb_server_auth::ntlm::is_spnego(&parsed_nt);
86
87            let t2 = smb_server_auth::ntlm::build_type2(
88                &conn.challenge,
89                &io.server.domain,
90                &io.server.server_name,
91            );
92            let out_blob = if conn.spnego {
93                smb_server_auth::ntlm::wrap_negtoken_targ(&t2)
94            } else {
95                t2.clone()
96            };
97            let (params, bytes) = ss::build_session_setup_response(0, &out_blob);
98            let mut b = RespBody::new(consts::COM_SESSION_SETUP_ANDX, params, bytes);
99            b.uid_override = Some(uid);
100            *bodies = vec![b];
101            Ok(Status::MORE_PROCESSING_REQUIRED)
102        }
103        Some(smb_server_auth::ntlm::MSG_TYPE3) => {
104            // Leg 2 — verify the AUTHENTICATE message.
105            let t3 = smb_server_auth::ntlm::parse_type3(inner).ok_or(Status::LOGON_FAILURE)?;
106            let _ = &_legacy;
107            let out =
108                authenticate_ntlmssp(&io.server.users, io.server.allow_guest, &conn.challenge, &t3);
109            if !out.ok {
110                return Err(Status::LOGON_FAILURE);
111            }
112            let uid = if conn.uid != 0 { conn.uid } else { next_uid() };
113            conn.uid = uid;
114            conn.auth_pending = false;
115            conn.session = Some(Session {
116                user: out.user.clone(),
117                guest: out.guest,
118                trees: Vec::new(),
119            });
120
121            let action: u16 = if out.guest { 0x0001 } else { 0x0000 };
122            let fin = if conn.spnego {
123                smb_server_auth::ntlm::wrap_accept_complete()
124            } else {
125                Vec::new()
126            };
127            let (params, mut bytes) = ss::build_session_setup_response(action, &fin);
128            // Drop OEM strings when a SPNEGO accept-complete token is present:
129            // some parsers mis-detect string boundaries after DER blobs.
130            bytes.truncate(fin.len());
131            let mut b = RespBody::new(consts::COM_SESSION_SETUP_ANDX, params, bytes);
132            b.uid_override = Some(uid);
133            *bodies = vec![b];
134            Ok(Status::SUCCESS)
135        }
136        _ => Err(Status::LOGON_FAILURE),
137    }
138}
139
140/// TREE_CONNECT_ANDX: resolve share name to its VFS-backed entry.
141#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB1 TREE_CONNECT_ANDX wire layout
142pub fn tree_connect(
143    io: &mut IoContext,
144    req: &ReqView,
145    bodies: &mut Vec<RespBody>,
146) -> Result<Status, Status> {
147    let tc = tc::TreeConnectReq::parse(
148        req.words,
149        req.data,
150        req.unicode(),
151        req.bc_off_abs + 2,
152    )
153    .map_err(|_| Status::INVALID_PARAMETER)?;
154
155    let name = tc
156        .path
157        .rsplit(['\\', '/'])
158        .find(|s| !s.is_empty())
159        .unwrap_or("")
160        .to_lowercase();
161    let ipc = name == "ipc$" || tc.service.starts_with("IPC");
162    if !ipc && !io.server.shares.contains_key(&name) {
163        return Err(Status::BAD_NETWORK_NAME);
164    }
165    let tid = next_tid();
166    io.conn.trees.insert(tid, name.clone());
167    if let Some(sess) = io.conn.session.as_mut() {
168        sess.trees.push(tid);
169    }
170
171    let svc = if ipc { "IPC" } else { "A:" };
172    let fs = if ipc { "" } else { "NTFS" };
173    let (params, mut bytes) = tc::build_response(0x0001, svc, fs, req.unicode(), 43);
174    if fs.is_empty() && unicode_tail_ok(&bytes) {
175        bytes.truncate(bytes.len().saturating_sub(if req.unicode() { 2 } else { 1 }));
176    }
177    let mut b = RespBody::new(consts::COM_TREE_CONNECT_ANDX, params, bytes);
178    b.tid_override = Some(tid);
179    *bodies = vec![b];
180    Ok(Status::SUCCESS)
181}
182
183fn unicode_tail_ok(_bytes: &[u8]) -> bool {
184    true
185}
186
187/// ECHO: reply `count` times with identical payloads.
188#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB1 ECHO word/byte layout
189pub fn echo(req: &ReqView, bodies: &mut Vec<RespBody>) -> Result<Status, Status> {
190    let count = if req.words.len() >= 2 {
191        u16::from_le_bytes([req.words[0], req.words[1]]).max(1)
192    } else {
193        1
194    };
195    bodies.clear();
196    for seq in 0..count {
197        bodies.push(ss_echo_body(seq, req.data));
198    }
199    Ok(Status::SUCCESS)
200}
201
202#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB1 ECHO response params
203fn ss_echo_body(seq: u16, data: &[u8]) -> RespBody {
204    let mut params = Vec::with_capacity(4);
205    params.extend_from_slice(&seq.to_le_bytes());
206    params.extend_from_slice(&0u16.to_le_bytes());
207    RespBody::new(consts::COM_ECHO, params, data.to_vec())
208}