DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
smb2.h
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file smb2.h
6 * @brief SMB2 client wire codec (MS-SMB2), DETWS_ENABLE_SMB - increment 1: the transport
7 * frame, the 64-byte sync packet header, and the NEGOTIATE exchange.
8 *
9 * Windows-share program storage is a common CNC file path (Fanuc / Haas / Mazak / Heidenhain
10 * expose one), so a device can read/write `.nc` files over SMB2. This is the pure wire layer:
11 * build the little-endian SMB2 messages and parse the responses; the TCP socket is the
12 * application's. All fields are little-endian (SMB2 is a little-endian protocol).
13 *
14 * A client speaks SMB2 over Direct TCP (port 445): each message is prefixed by a 4-byte transport
15 * header (`0x00` + a 24-bit big-endian length), then the 64-byte SMB2 sync header (MS-SMB2
16 * §2.2.1.2), then the per-command body. The exchange begins with NEGOTIATE (§2.2.3 request /
17 * §2.2.4 response): the client offers a dialect list, the server picks one and returns the SPNEGO
18 * security token that seeds authentication.
19 *
20 * Shipped: the NEGOTIATE exchange; the NTLM crypto (smb_md / ntlm / ntlmssp); the SPNEGO wrapping
21 * (spnego); the SESSION_SETUP request/response framing that carries those tokens; and the
22 * TREE_CONNECT / CREATE / CLOSE / READ / WRITE file commands - the full read/write-a-file-on-a-share
23 * client. Roadmap (later options): SMB 3.1.1 negotiate contexts + preauth integrity, SMB2 signing.
24 *
25 * @author Douglas Quigg (dstroy0)
26 * @date 2026
27 */
28
29#ifndef DETERMINISTICESPASYNCWEBSERVER_SMB2_H
30#define DETERMINISTICESPASYNCWEBSERVER_SMB2_H
31
32#include "ServerConfig.h"
33
34#if DETWS_ENABLE_SMB
35
36#include <stddef.h>
37#include <stdint.h>
38
39/** @brief SMB2 command codes (MS-SMB2 §2.2.1.2). */
40enum class Smb2Command : uint16_t
41{
42 SMB2_NEGOTIATE = 0x0000,
43 SMB2_SESSION_SETUP = 0x0001,
44 SMB2_LOGOFF = 0x0002,
45 SMB2_TREE_CONNECT = 0x0003,
46 SMB2_TREE_DISCONNECT = 0x0004,
47 SMB2_CREATE = 0x0005,
48 SMB2_CLOSE = 0x0006,
49 SMB2_READ = 0x0008,
50 SMB2_WRITE = 0x0009,
51};
52
53/** @brief SMB2 dialect revision numbers (MS-SMB2 §2.2.4). */
54enum class Smb2Dialect : uint16_t
55{
56 SMB2_DIALECT_0202 = 0x0202, ///< SMB 2.0.2
57 SMB2_DIALECT_0210 = 0x0210, ///< SMB 2.1
58 SMB2_DIALECT_0300 = 0x0300, ///< SMB 3.0
59 SMB2_DIALECT_0302 = 0x0302, ///< SMB 3.0.2
60 SMB2_DIALECT_0311 = 0x0311, ///< SMB 3.1.1
61};
62
63// These are SMB2 wire constants: flag words that get OR'd/AND'd and field values compared against the
64// uint8/16/32 wire fields of a parsed header. They live in namespacing structs of static constexpr
65// members - not enum class, which would force a cast at every bitwise op and every wire compare.
66
67/** @brief Fixed SMB2 sync header size (MS-SMB2 §2.2.1). */
68static constexpr uint32_t SMB2_HEADER_SIZE = 64;
69
70/** @brief NEGOTIATE / SESSION_SETUP SecurityMode flags (MS-SMB2 §2.2.3). */
71struct Smb2SecurityMode
72{
73 static constexpr uint16_t SMB2_NEGOTIATE_SIGNING_ENABLED = 0x0001;
74 static constexpr uint16_t SMB2_NEGOTIATE_SIGNING_REQUIRED = 0x0002;
75};
76
77/** @brief SMB2 header Flags field (MS-SMB2 §2.2.1.2). */
78struct Smb2HeaderFlags
79{
80 static constexpr uint32_t SMB2_FLAGS_SERVER_TO_REDIR = 0x00000001; ///< set on a response (server -> client)
81};
82
83/** @brief SESSION_SETUP response SessionFlags (MS-SMB2 §2.2.6). */
84struct Smb2SessionFlags
85{
86 static constexpr uint16_t SMB2_SESSION_FLAG_IS_GUEST = 0x0001;
87 static constexpr uint16_t SMB2_SESSION_FLAG_IS_NULL = 0x0002;
88 static constexpr uint16_t SMB2_SESSION_FLAG_ENCRYPT_DATA = 0x0004;
89};
90
91/** @brief NT status values seen in the SMB2 header during the SESSION_SETUP exchange. */
92struct Smb2Status
93{
94 static constexpr uint32_t SMB2_STATUS_SUCCESS = 0x00000000;
95 static constexpr uint32_t SMB2_STATUS_MORE_PROCESSING_REQUIRED = 0xC0000016; ///< server wants the next round
96 static constexpr uint32_t SMB2_STATUS_END_OF_FILE = 0xC0000011; ///< a READ at/past end of file
97};
98
99/** @brief TREE_CONNECT response ShareType (MS-SMB2 §2.2.10). */
100struct Smb2ShareType
101{
102 static constexpr uint8_t SMB2_SHARE_TYPE_DISK = 0x01;
103 static constexpr uint8_t SMB2_SHARE_TYPE_PIPE = 0x02;
104 static constexpr uint8_t SMB2_SHARE_TYPE_PRINT = 0x03;
105};
106
107/** @brief CREATE DesiredAccess masks (MS-DTYP ACCESS_MASK; the common file rights). */
108struct Smb2Access
109{
110 static constexpr uint32_t SMB2_FILE_READ_DATA = 0x00000001;
111 static constexpr uint32_t SMB2_FILE_WRITE_DATA = 0x00000002;
112 static constexpr uint32_t SMB2_FILE_APPEND_DATA = 0x00000004;
113 static constexpr uint32_t SMB2_FILE_READ_ATTRIBUTES = 0x00000080;
114 static constexpr uint32_t SMB2_FILE_GENERIC_READ = 0x00120089; ///< RC|SYNC|READ_ATTR|READ_EA|READ_DATA
115 static constexpr uint32_t SMB2_FILE_GENERIC_WRITE = 0x00120116; ///< RC|SYNC|WRITE_ATTR|WRITE_EA|APPEND|WRITE
116};
117
118/** @brief CREATE ShareAccess (MS-SMB2 §2.2.13). */
119struct Smb2ShareAccess
120{
121 static constexpr uint32_t SMB2_FILE_SHARE_READ = 0x01;
122 static constexpr uint32_t SMB2_FILE_SHARE_WRITE = 0x02;
123 static constexpr uint32_t SMB2_FILE_SHARE_DELETE = 0x04;
124};
125
126/** @brief CREATE CreateDisposition (MS-SMB2 §2.2.13). */
127struct Smb2Disposition
128{
129 static constexpr uint32_t SMB2_FILE_SUPERSEDE = 0;
130 static constexpr uint32_t SMB2_FILE_OPEN = 1; ///< open an existing file, fail if absent
131 static constexpr uint32_t SMB2_FILE_CREATE = 2; ///< create, fail if it exists
132 static constexpr uint32_t SMB2_FILE_OPEN_IF = 3; ///< open, create if absent
133 static constexpr uint32_t SMB2_FILE_OVERWRITE = 4; ///< open + truncate, fail if absent
134 static constexpr uint32_t SMB2_FILE_OVERWRITE_IF = 5;
135};
136
137/** @brief CREATE CreateOptions (MS-SMB2 §2.2.13; the two we set). */
138struct Smb2CreateOptions
139{
140 static constexpr uint32_t SMB2_FILE_DIRECTORY_FILE = 0x00000001;
141 static constexpr uint32_t SMB2_FILE_NON_DIRECTORY_FILE = 0x00000040;
142};
143
144/** @brief Parsed SMB2 sync header. */
145struct Smb2Header
146{
147 Smb2Command command;
148 uint32_t status; ///< NT status (response); 0 = STATUS_SUCCESS
149 uint32_t flags;
150 uint64_t message_id;
151 uint32_t tree_id;
152 uint64_t session_id;
153 uint16_t credit_response;
154};
155
156/** @brief Parsed NEGOTIATE response (MS-SMB2 §2.2.4). */
157struct Smb2NegotiateResp
158{
159 uint16_t security_mode;
160 uint16_t dialect; ///< the DialectRevision the server chose
161 uint8_t server_guid[16];
162 uint32_t capabilities;
163 uint32_t max_transact;
164 uint32_t max_read;
165 uint32_t max_write;
166 const uint8_t *sec_buf; ///< SPNEGO/NTLM security token (points into @p msg), or nullptr
167 uint16_t sec_buf_len;
168};
169
170/**
171 * @brief Prefix an SMB2 message with the 4-byte Direct-TCP transport header (`0x00` + a 24-bit
172 * big-endian length) into @p out.
173 * @return total bytes written (4 + @p msg_len), or 0 on overflow / a length that does not fit 24 bits.
174 */
175size_t smb2_transport_frame(uint8_t *out, size_t cap, const uint8_t *msg, size_t msg_len);
176
177/**
178 * @brief Read the Direct-TCP transport length prefix.
179 * @return the SMB2 message length that follows the 4-byte prefix, or 0 if @p len < 4 or the first
180 * byte is non-zero (an invalid Direct-TCP frame).
181 */
182uint32_t smb2_transport_len(const uint8_t *buf, size_t len);
183
184/**
185 * @brief Build a 64-byte SMB2 sync header into @p buf.
186 * @return SMB2_HEADER_SIZE, or 0 if @p cap < 64.
187 */
188size_t smb2_build_header(uint8_t *buf, size_t cap, Smb2Command command, uint16_t credit_request, uint64_t message_id,
189 uint32_t tree_id, uint64_t session_id);
190
191/**
192 * @brief Parse a 64-byte SMB2 sync header (validates ProtocolId + StructureSize).
193 * @return true on a valid header; false if @p len < 64, ProtocolId != `FE 53 4D 42`, or
194 * StructureSize != 64.
195 */
196bool smb2_parse_header(const uint8_t *buf, size_t len, Smb2Header *out);
197
198/**
199 * @brief Build a NEGOTIATE request (header + body) offering SMB 2.0.2 / 2.1 / 3.0 / 3.0.2.
200 * @param client_guid the 16-byte client GUID.
201 * @param security_mode SMB2_NEGOTIATE_SIGNING_ENABLED and/or _REQUIRED.
202 * @return total message bytes (no transport prefix), or 0 on overflow.
203 */
204size_t smb2_build_negotiate(uint8_t *buf, size_t cap, const uint8_t client_guid[16], uint16_t security_mode);
205
206/**
207 * @brief Parse a NEGOTIATE response message (the SMB2 header + §2.2.4 body).
208 *
209 * @param msg the SMB2 message (starting at the sync header, transport prefix already stripped).
210 * @return true on a well-formed response (header valid, command == NEGOTIATE, StructureSize == 65,
211 * and the security buffer within bounds); false otherwise. On success @p out->sec_buf points
212 * into @p msg (or is nullptr when SecurityBufferLength is 0).
213 */
214bool smb2_parse_negotiate_response(const uint8_t *msg, size_t len, Smb2NegotiateResp *out);
215
216/** @brief Parsed SESSION_SETUP response (MS-SMB2 §2.2.6). */
217struct Smb2SessionSetupResp
218{
219 uint16_t session_flags;
220 const uint8_t *sec_buf; ///< the server's SPNEGO/NTLM token (points into @p msg), or nullptr
221 uint16_t sec_buf_len;
222};
223
224/**
225 * @brief Build a SESSION_SETUP request (header + §2.2.5 body) carrying a security token.
226 *
227 * The token is the SPNEGO/NTLMSSP blob for this round of the handshake: the InitialContextToken on
228 * the first request, and (echoing the SessionId the server returned) the AUTHENTICATE NegTokenResp
229 * on the second. Capabilities / Channel / PreviousSessionId are 0 (a plain new session).
230 *
231 * @param message_id the SMB2 MessageId (increments across the exchange).
232 * @param session_id 0 on the first round; the server-assigned SessionId on the second.
233 * @param security_mode SMB2_NEGOTIATE_SIGNING_ENABLED and/or _REQUIRED (one byte on the wire).
234 * @return total message bytes (no transport prefix), or 0 on overflow / empty token.
235 */
236size_t smb2_build_session_setup(uint8_t *buf, size_t cap, uint64_t message_id, uint64_t session_id,
237 uint8_t security_mode, const uint8_t *sec_buf, size_t sec_len);
238
239/**
240 * @brief Parse a SESSION_SETUP response message (the SMB2 header + §2.2.6 body).
241 *
242 * The caller reads the SessionId (to echo on the next round) and the NT status
243 * (SMB2_STATUS_MORE_PROCESSING_REQUIRED vs SMB2_STATUS_SUCCESS) from smb2_parse_header on the same
244 * @p msg; this extracts the SessionFlags and the server security buffer.
245 *
246 * @param msg the SMB2 message (starting at the sync header, transport prefix already stripped).
247 * @return true on a well-formed response (header valid, command == SESSION_SETUP, StructureSize == 9,
248 * security buffer within bounds); false otherwise. On success @p out->sec_buf points into
249 * @p msg (or is nullptr when SecurityBufferLength is 0).
250 */
251bool smb2_parse_session_setup_response(const uint8_t *msg, size_t len, Smb2SessionSetupResp *out);
252
253/**
254 * @brief Build a TREE_CONNECT request (header + §2.2.9 body) for a share path.
255 * @param path_utf16 the UNC path `\\server\share` in UTF-16LE (no NUL); @p path_len its byte length.
256 * @return total message bytes (no transport prefix), or 0 on overflow / empty path.
257 */
258size_t smb2_build_tree_connect(uint8_t *buf, size_t cap, uint64_t message_id, uint64_t session_id,
259 const uint8_t *path_utf16, size_t path_len);
260
261/** @brief Parsed TREE_CONNECT response (MS-SMB2 §2.2.10). The TreeId is in the response header. */
262struct Smb2TreeConnectResp
263{
264 uint8_t share_type;
265 uint32_t share_flags;
266 uint32_t capabilities;
267 uint32_t maximal_access;
268};
269
270/**
271 * @brief Parse a TREE_CONNECT response message (validates command + StructureSize 16).
272 * @return true on a well-formed response; the caller reads the TreeId from smb2_parse_header.
273 */
274bool smb2_parse_tree_connect_response(const uint8_t *msg, size_t len, Smb2TreeConnectResp *out);
275
276/**
277 * @brief Build a CREATE request (header + §2.2.13 body) to open/create a file on the tree.
278 * @param name_utf16 the file name relative to the share root in UTF-16LE (no leading backslash, no
279 * NUL); @p name_len its byte length (must be > 0).
280 * @param desired_access e.g. SMB2_FILE_GENERIC_READ / _WRITE.
281 * @param share_access SMB2_FILE_SHARE_* bitmask.
282 * @param create_disposition SMB2_FILE_OPEN / _CREATE / _OPEN_IF / ...
283 * @param create_options SMB2_FILE_NON_DIRECTORY_FILE for a regular file.
284 * @return total message bytes (no transport prefix), or 0 on overflow.
285 */
286size_t smb2_build_create(uint8_t *buf, size_t cap, uint64_t message_id, uint64_t session_id, uint32_t tree_id,
287 uint32_t desired_access, uint32_t share_access, uint32_t create_disposition,
288 uint32_t create_options, const uint8_t *name_utf16, size_t name_len);
289
290/** @brief Parsed CREATE response (MS-SMB2 §2.2.14). */
291struct Smb2CreateResp
292{
293 uint8_t file_id[16]; ///< the open handle (persistent 8 + volatile 8), for READ/WRITE/CLOSE
294 uint64_t end_of_file;
295 uint32_t create_action;
296 uint32_t file_attributes;
297};
298
299/**
300 * @brief Parse a CREATE response message (validates command + StructureSize 89, FileId in bounds).
301 * @return true on a well-formed response.
302 */
303bool smb2_parse_create_response(const uint8_t *msg, size_t len, Smb2CreateResp *out);
304
305/**
306 * @brief Build a CLOSE request (header + §2.2.15 body) for an open FileId.
307 * @return total message bytes (no transport prefix), or 0 on overflow.
308 */
309size_t smb2_build_close(uint8_t *buf, size_t cap, uint64_t message_id, uint64_t session_id, uint32_t tree_id,
310 const uint8_t file_id[16]);
311
312/** @brief Parsed CLOSE response (MS-SMB2 §2.2.16). */
313struct Smb2CloseResp
314{
315 uint64_t end_of_file;
316 uint32_t file_attributes;
317};
318
319/**
320 * @brief Parse a CLOSE response message (validates command + StructureSize 60).
321 * @return true on a well-formed response.
322 */
323bool smb2_parse_close_response(const uint8_t *msg, size_t len, Smb2CloseResp *out);
324
325/**
326 * @brief Build a READ request (header + §2.2.19 body) for @p length bytes at @p offset of an open file.
327 * @return total message bytes (no transport prefix), or 0 on overflow.
328 */
329size_t smb2_build_read(uint8_t *buf, size_t cap, uint64_t message_id, uint64_t session_id, uint32_t tree_id,
330 const uint8_t file_id[16], uint32_t length, uint64_t offset);
331
332/** @brief Parsed READ response (MS-SMB2 §2.2.20). */
333struct Smb2ReadResp
334{
335 const uint8_t *data; ///< the file bytes read (points into @p msg), or nullptr when DataLength is 0
336 uint32_t data_len;
337};
338
339/**
340 * @brief Parse a READ response message (validates command + StructureSize 17, data within bounds).
341 * @return true on a well-formed response.
342 */
343bool smb2_parse_read_response(const uint8_t *msg, size_t len, Smb2ReadResp *out);
344
345/**
346 * @brief Build a WRITE request (header + §2.2.21 body) writing @p data at @p offset of an open file.
347 * @return total message bytes (no transport prefix), or 0 on overflow / empty data.
348 */
349size_t smb2_build_write(uint8_t *buf, size_t cap, uint64_t message_id, uint64_t session_id, uint32_t tree_id,
350 const uint8_t file_id[16], const uint8_t *data, size_t data_len, uint64_t offset);
351
352/** @brief Parsed WRITE response (MS-SMB2 §2.2.22). */
353struct Smb2WriteResp
354{
355 uint32_t count; ///< bytes actually written
356};
357
358/**
359 * @brief Parse a WRITE response message (validates command + StructureSize 17).
360 * @return true on a well-formed response.
361 */
362bool smb2_parse_write_response(const uint8_t *msg, size_t len, Smb2WriteResp *out);
363
364#endif // DETWS_ENABLE_SMB
365
366#endif // DETERMINISTICESPASYNCWEBSERVER_SMB2_H
User-facing configuration for DeterministicESPAsyncWebServer.