SMB Server Phase 2: VFS backend build fix + integration test
Some checks failed
Test / build (push) Has been cancelled
Test / test (push) Has been cancelled

- Add VfsFile: Send supertrait for Mutex compatibility
- Fix SmbServerCommand: struct → Subcommand enum with Start variant
- Fix tracing_subscriber::init() → try_init() to avoid panic when
  logger already initialized
- Fix CLI subcommand name: smb-server → smb-start (flatten naming)
- Add #[command(name = "smb-start")] for CLI disambiguation
- Fix unused variable warnings (smb_fs.rs, smb_server_backend.rs)
- Remove unused VfsFile imports (webdav.rs, scp_handler.rs)
- Integration test: Docker smbclient verified (list, upload, read)
This commit is contained in:
Warren
2026-06-20 19:42:29 +08:00
parent 45d050c0b3
commit 7eb528d35f
167 changed files with 59897 additions and 12 deletions

11
vendor/smb-server/src/proto/auth.rs vendored Normal file
View File

@@ -0,0 +1,11 @@
//! NTLMv2 server-side authentication and minimal SPNEGO outer envelope.
//!
//! See:
//! * MS-NLMP — NT LAN Manager (NTLM) Authentication Protocol
//! * MS-SPNG — Simple and Protected GSS-API Negotiation Mechanism
//!
//! v1 implements **only** the NTLM (NTLMSSP) mechanism inside SPNEGO.
//! Kerberos is out of scope (revisit in v0.2).
pub mod ntlm;
pub mod spnego;

1053
vendor/smb-server/src/proto/auth/ntlm.rs vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,524 @@
//! Minimal hand-rolled DER codec for SPNEGO (MS-SPNG / RFC 4178).
//!
//! v1 advertises **only** the NTLMSSP mechanism. We don't pull in a full
//! ASN.1 crate; this is a tiny subset of DER for the few SPNEGO tokens we
//! need to encode/decode during SESSION_SETUP.
//!
//! ASN.1 sketch:
//!
//! ```text
//! GSSAPI-Token (RFC 2743) ::= [APPLICATION 0] IMPLICIT SEQUENCE {
//! thisMech OBJECT IDENTIFIER, -- SPNEGO 1.3.6.1.5.5.2
//! innerContextToken ANY DEFINED BY thisMech
//! }
//!
//! NegotiationToken ::= CHOICE {
//! negTokenInit [0] NegTokenInit,
//! negTokenResp [1] NegTokenResp
//! }
//!
//! NegTokenInit ::= SEQUENCE {
//! mechTypes [0] MechTypeList,
//! reqFlags [1] ContextFlags OPTIONAL,
//! mechToken [2] OCTET STRING OPTIONAL,
//! mechListMIC [3] OCTET STRING OPTIONAL
//! }
//!
//! NegTokenResp ::= SEQUENCE {
//! negState [0] ENUMERATED OPTIONAL,
//! supportedMech [1] OBJECT IDENTIFIER OPTIONAL,
//! responseToken [2] OCTET STRING OPTIONAL,
//! mechListMIC [3] OCTET STRING OPTIONAL
//! }
//! ```
use crate::proto::error::{ProtoError, ProtoResult};
// --- Universal & well-known tags --------------------------------------------
const TAG_SEQUENCE: u8 = 0x30; // SEQUENCE OF / SEQUENCE (constructed)
const TAG_OBJECT: u8 = 0x06; // OBJECT IDENTIFIER
const TAG_OCTET: u8 = 0x04; // OCTET STRING
const TAG_ENUMERATED: u8 = 0x0a; // ENUMERATED
const TAG_APP_0: u8 = 0x60; // [APPLICATION 0] IMPLICIT — GSS-API outer
const TAG_CTX_0: u8 = 0xa0;
const TAG_CTX_1: u8 = 0xa1;
const TAG_CTX_2: u8 = 0xa2;
const TAG_CTX_3: u8 = 0xa3;
// --- OIDs ------------------------------------------------------------------
/// SPNEGO `1.3.6.1.5.5.2` encoded as the *content* of an OBJECT IDENTIFIER
/// (i.e. **without** the leading 0x06 tag + length).
pub const OID_SPNEGO: &[u8] = &[0x2b, 0x06, 0x01, 0x05, 0x05, 0x02];
/// NTLMSSP `1.3.6.1.4.1.311.2.2.10` encoded as OID *content*.
pub const OID_NTLMSSP: &[u8] = &[0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 0x02, 0x0a];
// --- NegState --------------------------------------------------------------
/// Values of the `negState` field in NegTokenResp.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum NegState {
AcceptCompleted = 0,
AcceptIncomplete = 1,
Reject = 2,
RequestMic = 3,
}
impl NegState {
fn from_byte(b: u8) -> ProtoResult<Self> {
match b {
0 => Ok(NegState::AcceptCompleted),
1 => Ok(NegState::AcceptIncomplete),
2 => Ok(NegState::Reject),
3 => Ok(NegState::RequestMic),
_ => Err(ProtoError::Auth("invalid NegState")),
}
}
}
// --- DER length helpers ----------------------------------------------------
/// Encode a DER length (definite-length form, MS-SPNG always uses definite).
fn der_len(n: usize, out: &mut Vec<u8>) {
if n < 0x80 {
out.push(n as u8);
return;
}
// Long form. Find minimum number of bytes.
let mut tmp = [0u8; 8];
let mut nb = 0;
let mut v = n;
while v > 0 {
tmp[nb] = (v & 0xff) as u8;
v >>= 8;
nb += 1;
}
out.push(0x80 | nb as u8);
for i in (0..nb).rev() {
out.push(tmp[i]);
}
}
/// Read a DER length from `buf` starting at `pos`. Returns `(length, next_pos)`.
fn read_len(buf: &[u8], pos: usize) -> ProtoResult<(usize, usize)> {
if pos >= buf.len() {
return Err(ProtoError::Auth("DER length truncated"));
}
let first = buf[pos];
if first < 0x80 {
return Ok((first as usize, pos + 1));
}
let nb = (first & 0x7f) as usize;
if nb == 0 || nb > 4 {
// Indefinite (nb=0) — never used by SPNEGO.
// We cap at 4 bytes (max ~4 GiB), more than enough for tokens.
return Err(ProtoError::Auth("DER length form unsupported"));
}
if pos + 1 + nb > buf.len() {
return Err(ProtoError::Auth("DER length truncated"));
}
let mut v = 0usize;
for i in 0..nb {
v = (v << 8) | buf[pos + 1 + i] as usize;
}
Ok((v, pos + 1 + nb))
}
/// Read `(tag, content_slice, next_pos)` at `pos`. Verifies the expected tag.
fn read_tlv(buf: &[u8], pos: usize, expected_tag: u8) -> ProtoResult<(&[u8], usize)> {
if pos >= buf.len() {
return Err(ProtoError::Auth("DER tag truncated"));
}
if buf[pos] != expected_tag {
return Err(ProtoError::Auth("unexpected DER tag"));
}
let (len, after_len) = read_len(buf, pos + 1)?;
let end = after_len + len;
if end > buf.len() {
return Err(ProtoError::Auth("DER content truncated"));
}
Ok((&buf[after_len..end], end))
}
/// Read any TLV (returning its tag plus the content slice & end position).
fn read_any_tlv(buf: &[u8], pos: usize) -> ProtoResult<(u8, &[u8], usize)> {
if pos >= buf.len() {
return Err(ProtoError::Auth("DER tag truncated"));
}
let tag = buf[pos];
let (len, after_len) = read_len(buf, pos + 1)?;
let end = after_len + len;
if end > buf.len() {
return Err(ProtoError::Auth("DER content truncated"));
}
Ok((tag, &buf[after_len..end], end))
}
// --- TLV writer helper -----------------------------------------------------
fn write_tlv(tag: u8, content: &[u8], out: &mut Vec<u8>) {
out.push(tag);
der_len(content.len(), out);
out.extend_from_slice(content);
}
// --- Public API ------------------------------------------------------------
/// Decoded `NegTokenInit` payload — only the bits we care about.
#[derive(Debug, Clone)]
pub struct NegTokenInit {
/// List of mechanism OIDs (each entry is the OID content bytes, no 0x06 tag).
pub mech_types: Vec<Vec<u8>>,
/// `mechToken [2]` if present — typically the NTLMSSP NEGOTIATE_MESSAGE bytes.
pub mech_token: Option<Vec<u8>>,
}
/// Decoded `NegTokenResp` payload.
#[derive(Debug, Clone, Default)]
pub struct NegTokenResp {
pub neg_state: Option<NegState>,
/// `supportedMech [1]` (OID content bytes).
pub supported_mech: Option<Vec<u8>>,
/// `responseToken [2]` — typically inner NTLMSSP CHALLENGE/AUTHENTICATE bytes.
pub response_token: Option<Vec<u8>>,
pub mech_list_mic: Option<Vec<u8>>,
}
/// Decode the **initial** SPNEGO blob from the client. This is wrapped in
/// the GSS-API outer `[APPLICATION 0]` tag, contains a `thisMech` OID
/// (SPNEGO), and a `[0] NegTokenInit`.
///
/// Returns the parsed `NegTokenInit`.
pub fn decode_init_token(buf: &[u8]) -> ProtoResult<NegTokenInit> {
// [APPLICATION 0] IMPLICIT SEQUENCE { thisMech OID, NegotiationToken }
let (gss_inner, _end) = read_tlv(buf, 0, TAG_APP_0)?;
// thisMech
let (mech, after_mech) = read_tlv(gss_inner, 0, TAG_OBJECT)?;
if mech != OID_SPNEGO {
return Err(ProtoError::Auth("not an SPNEGO token"));
}
// NegotiationToken — choice tagged [0] for init.
let (init_inner, _) = read_tlv(gss_inner, after_mech, TAG_CTX_0)?;
parse_neg_token_init_body(init_inner)
}
fn parse_neg_token_init_body(inner: &[u8]) -> ProtoResult<NegTokenInit> {
// Inner is a SEQUENCE.
let (seq_body, _) = read_tlv(inner, 0, TAG_SEQUENCE)?;
let mut pos = 0usize;
let mut mech_types: Vec<Vec<u8>> = Vec::new();
let mut mech_token: Option<Vec<u8>> = None;
while pos < seq_body.len() {
let (tag, content, next) = read_any_tlv(seq_body, pos)?;
match tag {
TAG_CTX_0 => {
// mechTypes [0] MechTypeList ::= SEQUENCE OF MechType (OID)
let (mt_seq, _) = read_tlv(content, 0, TAG_SEQUENCE)?;
let mut p = 0usize;
while p < mt_seq.len() {
let (oid, e) = read_tlv(mt_seq, p, TAG_OBJECT)?;
mech_types.push(oid.to_vec());
p = e;
}
}
TAG_CTX_1 => {
// reqFlags — ignored.
}
TAG_CTX_2 => {
// mechToken [2] OCTET STRING
let (oct, _) = read_tlv(content, 0, TAG_OCTET)?;
mech_token = Some(oct.to_vec());
}
TAG_CTX_3 => {
// mechListMIC — ignored on init.
}
_ => {
// Unknown — skip silently (forward-compat).
}
}
pos = next;
}
Ok(NegTokenInit {
mech_types,
mech_token,
})
}
/// Decode a subsequent `NegTokenResp`. These are sent without the GSS-API
/// outer wrapper — they begin directly with the `[1]` choice tag.
pub fn decode_resp_token(buf: &[u8]) -> ProtoResult<NegTokenResp> {
let (resp_inner, _) = read_tlv(buf, 0, TAG_CTX_1)?;
let (seq_body, _) = read_tlv(resp_inner, 0, TAG_SEQUENCE)?;
let mut pos = 0usize;
let mut out = NegTokenResp::default();
while pos < seq_body.len() {
let (tag, content, next) = read_any_tlv(seq_body, pos)?;
match tag {
TAG_CTX_0 => {
let (en, _) = read_tlv(content, 0, TAG_ENUMERATED)?;
if en.len() != 1 {
return Err(ProtoError::Auth("NegState ENUMERATED not 1 byte"));
}
out.neg_state = Some(NegState::from_byte(en[0])?);
}
TAG_CTX_1 => {
let (oid, _) = read_tlv(content, 0, TAG_OBJECT)?;
out.supported_mech = Some(oid.to_vec());
}
TAG_CTX_2 => {
let (oct, _) = read_tlv(content, 0, TAG_OCTET)?;
out.response_token = Some(oct.to_vec());
}
TAG_CTX_3 => {
let (oct, _) = read_tlv(content, 0, TAG_OCTET)?;
out.mech_list_mic = Some(oct.to_vec());
}
_ => {}
}
pos = next;
}
Ok(out)
}
/// Encode the **initial** server response to NEGOTIATE — a GSS-API-wrapped
/// `NegTokenInit` advertising NTLMSSP only. Used during SMB2 NEGOTIATE
/// when the server publishes its security blob.
pub fn encode_init_response() -> Vec<u8> {
// mechTypes SEQUENCE { OID NTLMSSP }
let mut mech_types_seq = Vec::new();
write_tlv(TAG_OBJECT, OID_NTLMSSP, &mut mech_types_seq);
let mut mech_types_outer = Vec::new();
write_tlv(TAG_SEQUENCE, &mech_types_seq, &mut mech_types_outer);
// mechTypes is [0] tagged.
let mut mech_types_ctx0 = Vec::new();
write_tlv(TAG_CTX_0, &mech_types_outer, &mut mech_types_ctx0);
// NegTokenInit SEQUENCE { mechTypes [0] }
let mut neg_token_init = Vec::new();
write_tlv(TAG_SEQUENCE, &mech_types_ctx0, &mut neg_token_init);
// [0] NegTokenInit (negotiationToken choice)
let mut choice_init = Vec::new();
write_tlv(TAG_CTX_0, &neg_token_init, &mut choice_init);
// Inside [APPLICATION 0]: { OID SPNEGO, [0] NegTokenInit }
let mut gss_inner = Vec::new();
write_tlv(TAG_OBJECT, OID_SPNEGO, &mut gss_inner);
gss_inner.extend_from_slice(&choice_init);
let mut out = Vec::new();
write_tlv(TAG_APP_0, &gss_inner, &mut out);
out
}
/// Encode a `NegTokenResp` wrapping the server's response token (typically
/// the NTLMSSP CHALLENGE_MESSAGE or a final empty-token AcceptCompleted).
///
/// `supported_mech` is included only with `AcceptIncomplete` (i.e. the very
/// first response to a NegTokenInit) — per RFC 4178 §4.2.2.
pub fn encode_resp_token(
state: NegState,
supported_mech: Option<&[u8]>,
response_token: Option<&[u8]>,
mech_list_mic: Option<&[u8]>,
) -> Vec<u8> {
let mut seq = Vec::new();
// [0] negState
{
let mut en = Vec::new();
write_tlv(TAG_ENUMERATED, &[state as u8], &mut en);
write_tlv(TAG_CTX_0, &en, &mut seq);
}
// [1] supportedMech
if let Some(oid) = supported_mech {
let mut o = Vec::new();
write_tlv(TAG_OBJECT, oid, &mut o);
write_tlv(TAG_CTX_1, &o, &mut seq);
}
// [2] responseToken
if let Some(tok) = response_token {
let mut o = Vec::new();
write_tlv(TAG_OCTET, tok, &mut o);
write_tlv(TAG_CTX_2, &o, &mut seq);
}
// [3] mechListMIC
if let Some(mic) = mech_list_mic {
let mut o = Vec::new();
write_tlv(TAG_OCTET, mic, &mut o);
write_tlv(TAG_CTX_3, &o, &mut seq);
}
let mut inner = Vec::new();
write_tlv(TAG_SEQUENCE, &seq, &mut inner);
let mut out = Vec::new();
write_tlv(TAG_CTX_1, &inner, &mut out);
out
}
// ===========================================================================
// Tests
// ===========================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn der_len_short() {
let mut v = Vec::new();
der_len(0x42, &mut v);
assert_eq!(v, [0x42]);
}
#[test]
fn der_len_long_one_byte() {
let mut v = Vec::new();
der_len(0xC8, &mut v);
assert_eq!(v, [0x81, 0xC8]);
}
#[test]
fn der_len_long_two_byte() {
let mut v = Vec::new();
der_len(0x1234, &mut v);
assert_eq!(v, [0x82, 0x12, 0x34]);
}
#[test]
fn read_len_round_trip() {
for n in [0usize, 1, 0x7F, 0x80, 0xFF, 0x100, 0xFFFF, 0x10000] {
let mut buf = Vec::new();
der_len(n, &mut buf);
let (got, next) = read_len(&buf, 0).unwrap();
assert_eq!(got, n);
assert_eq!(next, buf.len());
}
}
#[test]
fn init_response_is_decodable() {
let blob = encode_init_response();
// Must start with [APPLICATION 0] (0x60) tag.
assert_eq!(blob[0], TAG_APP_0);
// Decode with our own decoder going via decode_init_token.
// We craft a synthetic "init" by appending an empty mechToken? — not
// needed; decode_init_token tolerates absence. Test that the OID and
// the [0] mechTypes are reachable.
let init = decode_init_token(&blob).unwrap();
assert_eq!(init.mech_types.len(), 1);
assert_eq!(init.mech_types[0], OID_NTLMSSP);
assert!(init.mech_token.is_none());
}
#[test]
fn resp_token_round_trip_with_response() {
let payload = b"\x01\x02\x03\x04inner-blob";
let enc = encode_resp_token(
NegState::AcceptIncomplete,
Some(OID_NTLMSSP),
Some(payload),
None,
);
let dec = decode_resp_token(&enc).unwrap();
assert_eq!(dec.neg_state, Some(NegState::AcceptIncomplete));
assert_eq!(dec.supported_mech.as_deref(), Some(OID_NTLMSSP));
assert_eq!(dec.response_token.as_deref(), Some(&payload[..]));
assert!(dec.mech_list_mic.is_none());
}
#[test]
fn resp_token_round_trip_completed() {
let enc = encode_resp_token(NegState::AcceptCompleted, None, None, None);
let dec = decode_resp_token(&enc).unwrap();
assert_eq!(dec.neg_state, Some(NegState::AcceptCompleted));
assert!(dec.supported_mech.is_none());
assert!(dec.response_token.is_none());
}
#[test]
fn resp_token_with_mic() {
let mic = vec![0xAAu8; 16];
let enc = encode_resp_token(NegState::AcceptCompleted, None, None, Some(&mic));
let dec = decode_resp_token(&enc).unwrap();
assert_eq!(dec.mech_list_mic.as_deref(), Some(mic.as_slice()));
}
/// Build a NegTokenInit by hand (containing a mechToken) and decode it.
#[test]
fn decode_init_with_mech_token() {
let inner_token = b"NTLMSSP\x00fakeNegotiate";
// mechTypes
let mut mts = Vec::new();
write_tlv(TAG_OBJECT, OID_NTLMSSP, &mut mts);
let mut mts_seq = Vec::new();
write_tlv(TAG_SEQUENCE, &mts, &mut mts_seq);
let mut mts_ctx0 = Vec::new();
write_tlv(TAG_CTX_0, &mts_seq, &mut mts_ctx0);
// mechToken [2] OCTET STRING
let mut mt_oct = Vec::new();
write_tlv(TAG_OCTET, inner_token, &mut mt_oct);
let mut mt_ctx2 = Vec::new();
write_tlv(TAG_CTX_2, &mt_oct, &mut mt_ctx2);
// SEQUENCE { [0] mechTypes, [2] mechToken }
let mut seq = Vec::new();
seq.extend_from_slice(&mts_ctx0);
seq.extend_from_slice(&mt_ctx2);
let mut neg_token_init = Vec::new();
write_tlv(TAG_SEQUENCE, &seq, &mut neg_token_init);
let mut choice = Vec::new();
write_tlv(TAG_CTX_0, &neg_token_init, &mut choice);
let mut gss_inner = Vec::new();
write_tlv(TAG_OBJECT, OID_SPNEGO, &mut gss_inner);
gss_inner.extend_from_slice(&choice);
let mut blob = Vec::new();
write_tlv(TAG_APP_0, &gss_inner, &mut blob);
let dec = decode_init_token(&blob).unwrap();
assert_eq!(dec.mech_types.len(), 1);
assert_eq!(dec.mech_types[0], OID_NTLMSSP);
assert_eq!(dec.mech_token.as_deref(), Some(&inner_token[..]));
}
#[test]
fn rejects_non_spnego_oid() {
// Build a GSS token with a different OID inside.
let bad_oid = [0x2bu8, 0x06, 0x01, 0x01, 0x01, 0x01];
let mut gss_inner = Vec::new();
write_tlv(TAG_OBJECT, &bad_oid, &mut gss_inner);
// Empty [0] payload.
let mut empty = Vec::new();
write_tlv(TAG_SEQUENCE, &[], &mut empty);
let mut choice = Vec::new();
write_tlv(TAG_CTX_0, &empty, &mut choice);
gss_inner.extend_from_slice(&choice);
let mut blob = Vec::new();
write_tlv(TAG_APP_0, &gss_inner, &mut blob);
let err = decode_init_token(&blob).unwrap_err();
assert!(matches!(err, ProtoError::Auth(_)));
}
#[test]
fn rejects_truncated_blob() {
let err = decode_init_token(&[0x60, 0x05, 0xAA, 0xBB]).unwrap_err();
assert!(matches!(err, ProtoError::Auth(_)));
}
}

20
vendor/smb-server/src/proto/crypto.rs vendored Normal file
View File

@@ -0,0 +1,20 @@
//! SMB signing, key derivation, pre-auth integrity.
//!
//! Submodules:
//! * [`kdf`] — SP 800-108 CTR-mode KDF (`SMB2KDF`) and SMB-specific
//! signing/application key helpers (MS-SMB2 §3.1.4.2).
//! * [`sign`] — HMAC-SHA-256 (SMB 2.x) and AES-CMAC (SMB 3.x) signing of
//! SMB2 messages (MS-SMB2 §3.1.4.1).
//! * [`preauth`] — SMB 3.1.1 pre-auth integrity running SHA-512 hash
//! (MS-SMB2 §3.1.4.4.1, §3.3.5.4).
//!
//! Encryption (AES-CCM/AES-GCM) is intentionally out of scope for v1; see the
//! design spec.
pub mod kdf;
pub mod preauth;
pub mod sign;
pub use kdf::{signing_key_30, signing_key_311};
pub use preauth::PreauthIntegrity;
pub use sign::{SigningAlgo, sign, verify};

View File

@@ -0,0 +1,146 @@
//! SP 800-108 CTR-mode KDF using HMAC-SHA-256, as required by MS-SMB2 §3.1.4.2.
//!
//! Fixed input fed to the PRF (HMAC-SHA-256) is:
//!
//! ```text
//! i (u32be=1) || Label || 0x00 || Context || L (u32be=128)
//! ```
//!
//! Convention in this crate:
//! * Callers pass `label` and `context` *already including* a trailing `\x00`.
//! * The KDF then **also** emits a single `0x00` separator between `label`
//! and `context`, so the wire-level input has two consecutive NULs at that
//! boundary. This matches what real Windows clients require — a single NUL
//! produces a different signing key and Windows rejects with
//! `STATUS_ACCESS_DENIED` / event 31013 "signing validation failed".
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
/// SP 800-108 CTR-mode KDF using HMAC-SHA-256.
///
/// * `key` — the input key (session key, typically 16 bytes).
/// * `label` — the label string with trailing NUL (e.g. `b"SMB2AESCMAC\x00"`).
/// * `context` — the context string with trailing NUL (e.g. `b"SmbSign\x00"`).
///
/// Returns the first 16 bytes of `HMAC-SHA-256(key, fixed_input)` where
/// `fixed_input = [0,0,0,1] || label || 0x00 || context || [0,0,0,0x80]`.
/// The single separator `0x00` between `label` and `context` is required for
/// Windows interop; do not remove.
pub fn smb2_kdf(key: &[u8], label: &[u8], context: &[u8]) -> [u8; 16] {
let mut mac =
<HmacSha256 as Mac>::new_from_slice(key).expect("HMAC-SHA-256 accepts keys of any length");
// i = 1 (big-endian u32)
mac.update(&[0x00, 0x00, 0x00, 0x01]);
// Label (including trailing NUL provided by caller)
mac.update(label);
// SP 800-108 separator byte between Label and Context (in addition to any
// trailing NUL the caller already included in `label`).
mac.update(&[0x00]);
// Context (including trailing NUL provided by caller, or for 3.1.1 the
// 64-byte preauth hash)
mac.update(context);
// L = 128 bits (big-endian u32)
mac.update(&[0x00, 0x00, 0x00, 0x80]);
let full = mac.finalize().into_bytes();
let mut out = [0u8; 16];
out.copy_from_slice(&full[..16]);
out
}
// --- Convenience helpers ---------------------------------------------------
/// Signing key for SMB 3.0 / 3.0.2.
///
/// Label = `"SMB2AESCMAC\x00"`, Context = `"SmbSign\x00"` (MS-SMB2 §3.1.4.2).
pub fn signing_key_30(session_key: &[u8]) -> [u8; 16] {
smb2_kdf(session_key, b"SMB2AESCMAC\x00", b"SmbSign\x00")
}
/// Signing key for SMB 3.1.1.
///
/// Label = `"SMBSigningKey\x00"`, Context = pre-auth integrity hash
/// (the SHA-512 snapshot taken at SESSION_SETUP completion).
pub fn signing_key_311(session_key: &[u8], preauth_hash: &[u8; 64]) -> [u8; 16] {
smb2_kdf(session_key, b"SMBSigningKey\x00", preauth_hash)
}
#[cfg(test)]
mod tests {
use super::*;
/// Determinism / shape sanity: the function always produces 16 bytes and
/// is reproducible for the same inputs.
#[test]
fn smb2_kdf_is_deterministic() {
let key = [0x11u8; 16];
let a = smb2_kdf(&key, b"SMB2AESCMAC\x00", b"SmbSign\x00");
let b = smb2_kdf(&key, b"SMB2AESCMAC\x00", b"SmbSign\x00");
assert_eq!(a, b);
assert_eq!(a.len(), 16);
}
/// Different label or context → different output.
#[test]
fn smb2_kdf_label_and_context_matter() {
let key = [0x42u8; 16];
let signing = smb2_kdf(&key, b"SMB2AESCMAC\x00", b"SmbSign\x00");
let app = smb2_kdf(&key, b"SMB2APP\x00", b"SmbRpc\x00");
assert_ne!(signing, app);
let other_ctx = smb2_kdf(&key, b"SMB2AESCMAC\x00", b"OtherCtx\x00");
assert_ne!(signing, other_ctx);
}
/// Known-answer test computed directly from the documented fixed-input
/// construction. This pins the exact byte layout we feed to HMAC.
///
/// Reference computation (Python):
/// ```text
/// import hmac, hashlib
/// key = bytes(16) # all zeros
/// label = b"SMB2AESCMAC\x00"
/// context = b"SmbSign\x00"
/// data = b"\x00\x00\x00\x01" + label + b"\x00" + context + b"\x00\x00\x00\x80"
/// hmac.new(key, data, hashlib.sha256).hexdigest()[:32]
/// # = "9951088b83220f39d99420419d16d393"
/// ```
#[test]
fn smb2_kdf_known_answer_zero_key_signing_30() {
let key = [0u8; 16];
let out = signing_key_30(&key);
let expected = hex::decode("9951088b83220f39d99420419d16d393").unwrap();
assert_eq!(out.as_slice(), expected.as_slice());
}
/// 3.1.1 derivation differs from 3.0 (different label, 64-byte context).
#[test]
fn smb2_kdf_311_differs_from_30() {
let key = [0u8; 16];
let preauth = [0u8; 64];
let k30 = signing_key_30(&key);
let k311 = signing_key_311(&key, &preauth);
assert_ne!(k30, k311);
}
/// Known-answer test for 3.1.1 with zero key and zero pre-auth hash.
///
/// Reference computation (Python):
/// ```text
/// data = b"\x00\x00\x00\x01" + b"SMBSigningKey\x00" + b"\x00" + bytes(64) + b"\x00\x00\x00\x80"
/// hmac.new(bytes(16), data, hashlib.sha256).hexdigest()[:32]
/// # = "a06a153e09bd0f34706a5c671acaa37d"
/// ```
#[test]
fn smb2_kdf_known_answer_zero_key_signing_311() {
let key = [0u8; 16];
let preauth = [0u8; 64];
let out = signing_key_311(&key, &preauth);
let expected = hex::decode("a06a153e09bd0f34706a5c671acaa37d").unwrap();
assert_eq!(out.as_slice(), expected.as_slice());
}
}

View File

@@ -0,0 +1,115 @@
//! SMB 3.1.1 pre-auth integrity (MS-SMB2 §3.1.4.4.1, §3.3.5.4).
//!
//! A running SHA-512 hash, initialized to all zeros, that absorbs SMB 3.1.1
//! preauth messages (transport prefix excluded). Connection state uses this for
//! NEGOTIATE; each SESSION_SETUP exchange forks its own instance. Per spec:
//!
//! ```text
//! PreauthIntegrityHashValue =
//! SHA-512(PreauthIntegrityHashValue || RequestOrResponse)
//! ```
use sha2::{Digest, Sha512};
/// Running SMB 3.1.1 preauth integrity hash.
#[derive(Debug, Clone)]
pub struct PreauthIntegrity {
hash: [u8; 64],
}
impl Default for PreauthIntegrity {
fn default() -> Self {
Self::new()
}
}
impl PreauthIntegrity {
/// Create a fresh state, hash initialized to all zeros.
pub fn new() -> Self {
Self { hash: [0u8; 64] }
}
/// Absorb a frame's bytes (excluding the 4-byte Direct-TCP transport
/// prefix). Updates `hash` in place.
pub fn update(&mut self, frame: &[u8]) {
let mut hasher = Sha512::new();
hasher.update(self.hash);
hasher.update(frame);
let out = hasher.finalize();
self.hash.copy_from_slice(&out);
}
/// Take a copy of the current hash. Used as the KDF context for session
/// keys at SESSION_SETUP completion.
pub fn snapshot(&self) -> [u8; 64] {
self.hash
}
}
#[cfg(test)]
mod tests {
use super::*;
use sha2::{Digest, Sha512};
#[test]
fn new_starts_at_zero() {
let p = PreauthIntegrity::new();
assert_eq!(p.snapshot(), [0u8; 64]);
}
#[test]
fn default_starts_at_zero() {
let p = PreauthIntegrity::default();
assert_eq!(p.snapshot(), [0u8; 64]);
}
/// Two-step chain matches the literal spec formula.
#[test]
fn chain_two_buffers_matches_precomputed() {
let mut p = PreauthIntegrity::new();
let buf1 = b"NEGOTIATE_REQUEST_FIXTURE";
let buf2 = b"NEGOTIATE_RESPONSE_FIXTURE";
p.update(buf1);
p.update(buf2);
// Precomputed using Python:
// h = bytes(64)
// h = sha512(h + buf1).digest()
// h = sha512(h + buf2).digest()
let expected = hex::decode(
"62deb17d9d07d155b7c634dbfec3ac10c32b80981d925333499a6fbd168d0ee3\
4d29b093a185529fd927ade8d851c8e8b0d9b55608c7674e4d3e8d438343c95c",
)
.unwrap();
assert_eq!(p.snapshot().as_slice(), expected.as_slice());
}
/// Chained call equivalence: explicit SHA-512(prev || frame) on the side
/// must match what `update` produces internally.
#[test]
fn update_equals_manual_sha512() {
let buf = b"SOME_FRAME_BYTES_HERE_0123456789";
let mut p = PreauthIntegrity::new();
p.update(buf);
let mut hasher = Sha512::new();
hasher.update([0u8; 64]);
hasher.update(buf);
let manual = hasher.finalize();
assert_eq!(p.snapshot().as_slice(), manual.as_slice());
}
/// Snapshot must not be aliased — modifying state after snapshot must not
/// affect the snapshot already taken.
#[test]
fn snapshot_is_a_copy() {
let mut p = PreauthIntegrity::new();
p.update(b"first");
let snap = p.snapshot();
p.update(b"second");
assert_ne!(p.snapshot(), snap);
}
}

View File

@@ -0,0 +1,259 @@
//! SMB2/3 message signing per MS-SMB2 §3.1.4.1.
//!
//! Two algorithms are supported:
//! 1. **HMAC-SHA-256** for SMB 2.0.2 / 2.1 / 3.0 negotiating without 3.x
//! signing.
//! 2. **AES-CMAC** for SMB 3.0+.
//!
//! Both produce a 16-byte signature that lives at bytes 48..64 of the SMB2
//! header (the `Signature` field, MS-SMB2 §2.2.1.2).
//!
//! Algorithm:
//! 1. Zero out bytes 48..64 of the message.
//! 2. Compute MAC over the **entire** message (header + body).
//! 3. Place the first 16 bytes of MAC at bytes 48..64.
use aes::Aes128;
use cmac::Cmac;
use hmac::{Hmac, Mac};
use sha2::Sha256;
use crate::proto::error::{ProtoError, ProtoResult};
type HmacSha256 = Hmac<Sha256>;
type CmacAes128 = Cmac<Aes128>;
/// SMB2 header is 64 bytes; the 16-byte signature field starts at offset 48.
const SIG_OFF: usize = 48;
const SIG_LEN: usize = 16;
const SMB2_HEADER_LEN: usize = 64;
/// Which signing algorithm to use for a given session/dialect.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SigningAlgo {
/// HMAC-SHA-256, used by SMB 2.x.
HmacSha256,
/// AES-CMAC over AES-128, used by SMB 3.0+.
AesCmac,
}
/// Compute the 16-byte MAC over `msg` as if the SMB2 signature field were
/// zeroed, without copying the whole message.
fn compute_mac_zeroed_signature(msg: &[u8], key: &[u8; 16], algo: SigningAlgo) -> [u8; SIG_LEN] {
let mut out = [0u8; SIG_LEN];
let zero_signature = [0u8; SIG_LEN];
let prefix = &msg[..SIG_OFF];
let suffix = &msg[SIG_OFF + SIG_LEN..];
match algo {
SigningAlgo::HmacSha256 => {
let mut mac = <HmacSha256 as Mac>::new_from_slice(key)
.expect("HMAC-SHA-256 accepts keys of any length");
mac.update(prefix);
mac.update(&zero_signature);
mac.update(suffix);
let full = mac.finalize().into_bytes();
out.copy_from_slice(&full[..SIG_LEN]);
}
SigningAlgo::AesCmac => {
let mut mac = <CmacAes128 as Mac>::new_from_slice(key)
.expect("AES-128-CMAC requires a 16-byte key, which we have");
mac.update(prefix);
mac.update(&zero_signature);
mac.update(suffix);
let full = mac.finalize().into_bytes();
out.copy_from_slice(&full[..SIG_LEN]);
}
}
out
}
/// Compute and embed a signature in `msg`. Mutates `msg` in place.
///
/// The caller is responsible for setting the SMB2 SIGNED flag (`0x00000008`)
/// on the header *before* calling — it is part of the bytes that get MAC'd.
///
/// Errors if `msg` is too short to contain an SMB2 header (< 64 bytes).
pub fn sign(msg: &mut [u8], key: &[u8; 16], algo: SigningAlgo) -> ProtoResult<()> {
if msg.len() < SMB2_HEADER_LEN {
return Err(ProtoError::Crypto("message too short to sign"));
}
// Compute MAC over the whole message with the signature field treated as
// zero, then place the MAC into the signature field.
let mac = compute_mac_zeroed_signature(msg, key, algo);
msg[SIG_OFF..SIG_OFF + SIG_LEN].copy_from_slice(&mac);
Ok(())
}
/// Verify the signature in `msg`. Does **not** modify `msg`.
///
/// Uses constant-time comparison. Returns `Ok(())` if the embedded signature
/// matches the freshly computed MAC.
pub fn verify(msg: &[u8], key: &[u8; 16], algo: SigningAlgo) -> ProtoResult<()> {
if msg.len() < SMB2_HEADER_LEN {
return Err(ProtoError::Crypto("message too short to verify"));
}
// Capture the embedded signature.
let mut embedded = [0u8; SIG_LEN];
embedded.copy_from_slice(&msg[SIG_OFF..SIG_OFF + SIG_LEN]);
let computed = compute_mac_zeroed_signature(msg, key, algo);
if constant_time_eq(&embedded, &computed) {
Ok(())
} else {
Err(ProtoError::Crypto("signature mismatch"))
}
}
/// Constant-time comparison of two 16-byte arrays.
#[inline]
fn constant_time_eq(a: &[u8; SIG_LEN], b: &[u8; SIG_LEN]) -> bool {
let mut diff: u8 = 0;
for i in 0..SIG_LEN {
diff |= a[i] ^ b[i];
}
diff == 0
}
#[cfg(test)]
mod tests {
use super::*;
/// Build a 100-byte message: a plausible 64-byte SMB2 header followed by
/// 36 bytes of body. The signature region (bytes 48..64) is left zero;
/// `sign` will overwrite it.
fn fixture_message() -> Vec<u8> {
let mut msg = vec![0u8; 100];
// Magic: 0xFE 'S' 'M' 'B'
msg[0..4].copy_from_slice(&[0xFE, b'S', b'M', b'B']);
// StructureSize = 64
msg[4..6].copy_from_slice(&64u16.to_le_bytes());
// Pretend ChannelSequence = 0
msg[6..8].copy_from_slice(&0u16.to_le_bytes());
// Command = NEGOTIATE (0)
msg[12..14].copy_from_slice(&0u16.to_le_bytes());
// Flags: SIGNED (0x00000008)
msg[16..20].copy_from_slice(&0x0000_0008u32.to_le_bytes());
// Body filler
for (i, b) in msg[64..].iter_mut().enumerate() {
*b = (i as u8).wrapping_mul(7);
}
msg
}
#[test]
fn sign_and_verify_hmac_sha256() {
let key = [0xAAu8; 16];
let mut msg = fixture_message();
sign(&mut msg, &key, SigningAlgo::HmacSha256).expect("sign ok");
// Signature should now be non-zero (overwhelmingly likely).
assert_ne!(&msg[SIG_OFF..SIG_OFF + SIG_LEN], &[0u8; 16]);
verify(&msg, &key, SigningAlgo::HmacSha256).expect("verify ok");
}
#[test]
fn sign_and_verify_aes_cmac() {
let key = [0x55u8; 16];
let mut msg = fixture_message();
sign(&mut msg, &key, SigningAlgo::AesCmac).expect("sign ok");
assert_ne!(&msg[SIG_OFF..SIG_OFF + SIG_LEN], &[0u8; 16]);
verify(&msg, &key, SigningAlgo::AesCmac).expect("verify ok");
}
#[test]
fn tamper_outside_sig_fails_verify_hmac() {
let key = [0xAAu8; 16];
let mut msg = fixture_message();
sign(&mut msg, &key, SigningAlgo::HmacSha256).expect("sign ok");
// Flip one body byte.
msg[80] ^= 0x01;
let res = verify(&msg, &key, SigningAlgo::HmacSha256);
assert!(matches!(res, Err(ProtoError::Crypto(_))));
}
#[test]
fn tamper_outside_sig_fails_verify_cmac() {
let key = [0x55u8; 16];
let mut msg = fixture_message();
sign(&mut msg, &key, SigningAlgo::AesCmac).expect("sign ok");
// Flip a header byte (not in the sig region).
msg[10] ^= 0xFF;
let res = verify(&msg, &key, SigningAlgo::AesCmac);
assert!(matches!(res, Err(ProtoError::Crypto(_))));
}
#[test]
fn tamper_signature_fails_verify() {
let key = [0xAAu8; 16];
let mut msg = fixture_message();
sign(&mut msg, &key, SigningAlgo::HmacSha256).expect("sign ok");
msg[SIG_OFF] ^= 0x01;
let res = verify(&msg, &key, SigningAlgo::HmacSha256);
assert!(matches!(res, Err(ProtoError::Crypto(_))));
}
#[test]
fn wrong_key_fails_verify() {
let key = [0xAAu8; 16];
let bad_key = [0xBBu8; 16];
let mut msg = fixture_message();
sign(&mut msg, &key, SigningAlgo::HmacSha256).expect("sign ok");
let res = verify(&msg, &bad_key, SigningAlgo::HmacSha256);
assert!(matches!(res, Err(ProtoError::Crypto(_))));
}
#[test]
fn too_short_message_errors() {
let mut tiny = [0u8; 10];
let key = [0u8; 16];
let res = sign(&mut tiny, &key, SigningAlgo::HmacSha256);
assert!(matches!(res, Err(ProtoError::Crypto(_))));
let res = verify(&tiny, &key, SigningAlgo::HmacSha256);
assert!(matches!(res, Err(ProtoError::Crypto(_))));
}
#[test]
fn verify_does_not_mutate_message_hmac_sha256() {
let key = [0xAAu8; 16];
let mut msg = fixture_message();
sign(&mut msg, &key, SigningAlgo::HmacSha256).expect("sign ok");
let snapshot = msg.clone();
verify(&msg, &key, SigningAlgo::HmacSha256).expect("verify ok");
assert_eq!(msg, snapshot);
}
#[test]
fn verify_does_not_mutate_message_aes_cmac() {
let key = [0x55u8; 16];
let mut msg = fixture_message();
sign(&mut msg, &key, SigningAlgo::AesCmac).expect("sign ok");
let snapshot = msg.clone();
verify(&msg, &key, SigningAlgo::AesCmac).expect("verify ok");
assert_eq!(msg, snapshot);
}
#[test]
fn sign_ignores_existing_signature_bytes() {
let key = [0xAAu8; 16];
let mut clean = fixture_message();
let mut dirty = fixture_message();
dirty[SIG_OFF..SIG_OFF + SIG_LEN].fill(0xCC);
sign(&mut clean, &key, SigningAlgo::HmacSha256).expect("sign clean");
sign(&mut dirty, &key, SigningAlgo::HmacSha256).expect("sign dirty");
assert_eq!(
&clean[SIG_OFF..SIG_OFF + SIG_LEN],
&dirty[SIG_OFF..SIG_OFF + SIG_LEN]
);
verify(&dirty, &key, SigningAlgo::HmacSha256).expect("verify dirty");
}
}

26
vendor/smb-server/src/proto/error.rs vendored Normal file
View File

@@ -0,0 +1,26 @@
//! Crate-wide error type for the internal SMB protocol layer.
use thiserror::Error;
pub type ProtoResult<T> = Result<T, ProtoError>;
#[derive(Debug, Error)]
pub enum ProtoError {
#[error("malformed wire frame: {0}")]
Malformed(&'static str),
#[error("unsupported dialect: 0x{0:04x}")]
UnsupportedDialect(u16),
#[error("auth failure: {0}")]
Auth(&'static str),
#[error("crypto failure: {0}")]
Crypto(&'static str),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("binrw error: {0}")]
Binrw(#[from] binrw::Error),
}

155
vendor/smb-server/src/proto/framing.rs vendored Normal file
View File

@@ -0,0 +1,155 @@
//! Direct-TCP / NetBIOS-over-TCP framing for SMB2/3.
//!
//! MS-SMB2 §2.1 requires a 4-byte big-endian length prefix on every TCP frame:
//!
//! ```text
//! +-------+--------------------------------+
//! | 0x00 | 24-bit big-endian payload len |
//! +-------+--------------------------------+
//! | SMB2 packet ... |
//! +----------------------------------------+
//! ```
//!
//! The top byte is reserved (must be zero in Direct-TCP transport — it is the
//! NetBIOS session-message-type byte from RFC 1002 §4.3.1). The remaining 24
//! bits encode the payload length, so the absolute maximum on the wire is
//! `2^24 - 1 = 16_777_215` bytes (16 MiB - 1). We enforce that as the cap.
//!
//! This module is async-runtime-agnostic. Only sync helpers operating on byte
//! slices and `Vec<u8>` live here; the server crate wraps these with tokio
//! I/O.
use crate::proto::error::{ProtoError, ProtoResult};
/// Length of the Direct-TCP frame header (4 bytes).
pub const FRAME_HEADER_LEN: usize = 4;
/// Maximum payload size representable by the 3-byte length field.
///
/// MS-SMB2 §2.1 — `2^24 - 1 = 16_777_215` bytes.
pub const MAX_FRAME_PAYLOAD: u32 = 0x00FF_FFFF;
/// Encode a single Direct-TCP frame: 4-byte header + payload.
///
/// Panics in debug if the payload exceeds [`MAX_FRAME_PAYLOAD`]; release builds
/// silently truncate the high byte.
pub fn encode_frame(payload: &[u8], out: &mut Vec<u8>) {
debug_assert!(
payload.len() as u64 <= MAX_FRAME_PAYLOAD as u64,
"frame payload exceeds 16 MiB - 1"
);
let len = payload.len() as u32;
// Top byte is the NetBIOS session-message type (0x00 for Direct-TCP).
// Lower 3 bytes are payload length, big-endian.
out.reserve(FRAME_HEADER_LEN + payload.len());
out.push(0x00);
out.push(((len >> 16) & 0xFF) as u8);
out.push(((len >> 8) & 0xFF) as u8);
out.push((len & 0xFF) as u8);
out.extend_from_slice(payload);
}
/// Decode the 4-byte frame header, returning the payload length.
///
/// Returns [`ProtoError::Malformed`] if the top byte is non-zero (NetBIOS
/// session-message type other than `SESSION MESSAGE` is not supported in
/// Direct-TCP transport).
pub fn decode_frame_header(bytes: &[u8; FRAME_HEADER_LEN]) -> ProtoResult<u32> {
if bytes[0] != 0x00 {
return Err(ProtoError::Malformed(
"NetBIOS session-message type byte must be 0x00 for Direct-TCP",
));
}
let len = (u32::from(bytes[1]) << 16) | (u32::from(bytes[2]) << 8) | u32::from(bytes[3]);
Ok(len)
}
/// Convenience: read one full frame from a contiguous byte slice.
///
/// Returns the payload slice and the remaining bytes after the frame.
#[cfg(test)]
pub fn decode_frame(buf: &[u8]) -> ProtoResult<(&[u8], &[u8])> {
if buf.len() < FRAME_HEADER_LEN {
return Err(ProtoError::Malformed("short frame header"));
}
let mut hdr = [0u8; FRAME_HEADER_LEN];
hdr.copy_from_slice(&buf[..FRAME_HEADER_LEN]);
let len = decode_frame_header(&hdr)? as usize;
let total = FRAME_HEADER_LEN + len;
if buf.len() < total {
return Err(ProtoError::Malformed("truncated frame body"));
}
Ok((&buf[FRAME_HEADER_LEN..total], &buf[total..]))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encodes_empty_frame() {
let mut out = Vec::new();
encode_frame(&[], &mut out);
assert_eq!(out, [0x00, 0x00, 0x00, 0x00]);
}
#[test]
fn encodes_simple_frame() {
let mut out = Vec::new();
encode_frame(&[0xAA, 0xBB, 0xCC], &mut out);
assert_eq!(out, [0x00, 0x00, 0x00, 0x03, 0xAA, 0xBB, 0xCC]);
}
#[test]
fn round_trips_random_payload() {
let payload: Vec<u8> = (0u8..=200).collect();
let mut wire = Vec::new();
encode_frame(&payload, &mut wire);
let (decoded, rest) = decode_frame(&wire).unwrap();
assert_eq!(decoded, payload.as_slice());
assert!(rest.is_empty());
}
#[test]
fn decodes_header_three_byte_length() {
// 0x00_12_34_56 -> length 0x123456
let len = decode_frame_header(&[0x00, 0x12, 0x34, 0x56]).unwrap();
assert_eq!(len, 0x0012_3456);
}
#[test]
fn decodes_header_max_length() {
let len = decode_frame_header(&[0x00, 0xFF, 0xFF, 0xFF]).unwrap();
assert_eq!(len, MAX_FRAME_PAYLOAD);
}
#[test]
fn rejects_nonzero_top_byte() {
let err = decode_frame_header(&[0x81, 0x00, 0x00, 0x00]).unwrap_err();
assert!(matches!(err, ProtoError::Malformed(_)));
}
#[test]
fn decode_frame_handles_trailing_data() {
let mut wire = Vec::new();
encode_frame(&[1, 2, 3], &mut wire);
wire.extend_from_slice(&[9, 9, 9]); // simulate a partial second frame
let (payload, rest) = decode_frame(&wire).unwrap();
assert_eq!(payload, &[1, 2, 3]);
assert_eq!(rest, &[9, 9, 9]);
}
#[test]
fn decode_frame_short_header() {
let err = decode_frame(&[0x00, 0x00]).unwrap_err();
assert!(matches!(err, ProtoError::Malformed(_)));
}
#[test]
fn decode_frame_truncated_body() {
let err = decode_frame(&[0x00, 0x00, 0x00, 0x05, 0xAA]).unwrap_err();
assert!(matches!(err, ProtoError::Malformed(_)));
}
}

471
vendor/smb-server/src/proto/header.rs vendored Normal file
View File

@@ -0,0 +1,471 @@
//! SMB2 fixed 64-byte packet header (sync + async forms).
//!
//! References:
//! * MS-SMB2 §2.2.1 — Common header preamble.
//! * MS-SMB2 §2.2.1.1 — Async form (`Flags & SMB2_FLAGS_ASYNC_COMMAND`).
//! * MS-SMB2 §2.2.1.2 — Sync form.
//!
//! ## Encoding choice
//!
//! The two forms differ only in the 12-byte block at offset 0x18..0x24:
//!
//! * **Sync**: `ChannelSequence` (u16) + `Reserved` (u16) + `Reserved2` (u32) + `TreeId` (u32)
//! wait — actually the sync form is: `Reserved` (u32) + `TreeId` (u32) (bytes 0x20..0x28).
//! * **Async**: `AsyncId` (u64) at bytes 0x20..0x28.
//!
//! In *both* forms, bytes 0x10..0x14 are `Status` (or `ChannelSequence + Reserved` on
//! 3.x channel-sequence-aware requests; we treat them as a single u32 named
//! `channel_sequence_status`). Bytes 0x14..0x18 are `Command + CreditReqResp`,
//! 0x18..0x1C are `Flags`, 0x1C..0x20 are `NextCommand`, 0x20..0x28 are `MessageId`.
//! The discriminated 8-byte block lives at 0x28..0x30, followed by the 16-byte
//! `Signature` at 0x30..0x40 — totalling 64 bytes.
//!
//! We model this as a single `Smb2Header` struct with a `tail: HeaderTail` enum
//! that is `Sync { reserved: u32, tree_id: u32 }` or `Async { async_id: u64 }`,
//! discriminated by `Flags & SMB2_FLAGS_ASYNC_COMMAND`. This is the cleanest
//! mapping to the spec — every other field is shared.
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use crate::proto::error::{ProtoError, ProtoResult};
/// SMB2 protocol identifier ("\xfeSMB").
pub const SMB2_MAGIC: [u8; 4] = [0xFE, b'S', b'M', b'B'];
/// Fixed `StructureSize` of the SMB2 header (MS-SMB2 §2.2.1.1/§2.2.1.2).
pub const SMB2_HEADER_STRUCTURE_SIZE: u16 = 64;
/// Total wire size of the SMB2 header.
pub const SMB2_HEADER_LEN: usize = 64;
// ---------------------------------------------------------------------------
// Flags (MS-SMB2 §2.2.1.2 Flags field)
// ---------------------------------------------------------------------------
/// `SMB2_FLAGS_SERVER_TO_REDIR` — set on responses.
pub const SMB2_FLAGS_SERVER_TO_REDIR: u32 = 0x0000_0001;
/// `SMB2_FLAGS_ASYNC_COMMAND` — selects the async header form.
pub const SMB2_FLAGS_ASYNC_COMMAND: u32 = 0x0000_0002;
/// `SMB2_FLAGS_RELATED_OPERATIONS` — compound chain marker.
pub const SMB2_FLAGS_RELATED_OPERATIONS: u32 = 0x0000_0004;
/// `SMB2_FLAGS_SIGNED` — message is signed.
pub const SMB2_FLAGS_SIGNED: u32 = 0x0000_0008;
/// `SMB2_FLAGS_PRIORITY_MASK` — bits 4..6 hold priority (3.1.1+).
pub const SMB2_FLAGS_PRIORITY_MASK: u32 = 0x0000_0070;
/// `SMB2_FLAGS_DFS_OPERATIONS`.
pub const SMB2_FLAGS_DFS_OPERATIONS: u32 = 0x1000_0000;
/// `SMB2_FLAGS_REPLAY_OPERATION`.
pub const SMB2_FLAGS_REPLAY_OPERATION: u32 = 0x2000_0000;
// ---------------------------------------------------------------------------
// Command opcodes (MS-SMB2 §2.2.1.2 Command field)
// ---------------------------------------------------------------------------
/// SMB2 command opcodes (the 19 commands in v1).
#[binrw]
#[brw(little, repr = u16)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Command {
Negotiate = 0x0000,
SessionSetup = 0x0001,
Logoff = 0x0002,
TreeConnect = 0x0003,
TreeDisconnect = 0x0004,
Create = 0x0005,
Close = 0x0006,
Flush = 0x0007,
Read = 0x0008,
Write = 0x0009,
Lock = 0x000A,
Ioctl = 0x000B,
Cancel = 0x000C,
Echo = 0x000D,
QueryDirectory = 0x000E,
ChangeNotify = 0x000F,
QueryInfo = 0x0010,
SetInfo = 0x0011,
OplockBreak = 0x0012,
}
impl Command {
/// Raw opcode for diagnostics.
pub const fn as_u16(self) -> u16 {
self as u16
}
}
// ---------------------------------------------------------------------------
// Header struct
// ---------------------------------------------------------------------------
/// The 12-byte tail of the header that differs between sync and async forms.
///
/// The discriminant is `flags & SMB2_FLAGS_ASYNC_COMMAND`. We can't easily use
/// binrw's args+if without making the parent struct generic over the runtime
/// flag value, so the parent reads/writes this manually via `parse` / `write`
/// helpers and we expose a regular Rust enum here.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HeaderTail {
/// Sync form: `Reserved (u32)` + `TreeId (u32)` at bytes 0x24..0x2C.
/// (See note in module docs about offsets.)
Sync { reserved: u32, tree_id: u32 },
/// Async form: `AsyncId (u64)` at bytes 0x24..0x2C.
Async { async_id: u64 },
}
impl HeaderTail {
/// Default sync tail with `TreeId = 0`.
pub const fn sync(tree_id: u32) -> Self {
HeaderTail::Sync {
reserved: 0,
tree_id,
}
}
/// Default async tail.
pub const fn async_(async_id: u64) -> Self {
HeaderTail::Async { async_id }
}
}
/// SMB2 fixed 64-byte header.
///
/// On the wire the layout is (offsets in decimal — total 64 bytes):
///
/// | Offset | Size | Field |
/// |-------:|-----:|-------|
/// | 0 | 4 | ProtocolId (`0xFE 'S' 'M' 'B'`) |
/// | 4 | 2 | StructureSize (always 64) |
/// | 6 | 2 | CreditCharge |
/// | 8 | 4 | (Channel)Status |
/// | 12 | 2 | Command |
/// | 14 | 2 | CreditRequest/CreditResponse |
/// | 16 | 4 | Flags |
/// | 20 | 4 | NextCommand |
/// | 24 | 8 | MessageId |
/// | 32 | 8 | Reserved/TreeId (sync) **or** AsyncId (async) |
/// | 40 | 8 | SessionId |
/// | 48 | 16 | Signature |
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Smb2Header {
pub credit_charge: u16,
/// Bytes 8..12: in client→server requests on 3.x this can split into
/// `ChannelSequence(u16)` + `Reserved(u16)`; in server→client responses
/// it carries `Status` (NTSTATUS). We expose the raw u32 — handlers/
/// signing code interpret it.
pub channel_sequence_status: u32,
pub command: Command,
/// On requests this is `CreditRequest`; on responses, `CreditResponse`.
pub credit_request_response: u16,
pub flags: u32,
/// Offset to the next header in a compound chain, or 0 for the last.
pub next_command: u32,
pub message_id: u64,
/// Sync: `(reserved, tree_id)`. Async: `async_id`. Discriminated by
/// `flags & SMB2_FLAGS_ASYNC_COMMAND`.
pub tail: HeaderTail,
pub session_id: u64,
/// 16-byte signature; zeroed on unsigned messages.
pub signature: [u8; 16],
}
impl Default for Smb2Header {
fn default() -> Self {
Self {
credit_charge: 0,
channel_sequence_status: 0,
command: Command::Negotiate,
credit_request_response: 0,
flags: 0,
next_command: 0,
message_id: 0,
tail: HeaderTail::sync(0),
session_id: 0,
signature: [0u8; 16],
}
}
}
impl Smb2Header {
/// Convenience: is this an async-form header?
pub fn is_async(&self) -> bool {
self.flags & SMB2_FLAGS_ASYNC_COMMAND != 0
}
/// Convenience: is this a server→client response?
pub fn is_response(&self) -> bool {
self.flags & SMB2_FLAGS_SERVER_TO_REDIR != 0
}
/// Convenience: tree_id from a sync header (panics if async).
pub fn tree_id(&self) -> Option<u32> {
match self.tail {
HeaderTail::Sync { tree_id, .. } => Some(tree_id),
HeaderTail::Async { .. } => None,
}
}
/// Convenience: async_id from an async header.
pub fn async_id(&self) -> Option<u64> {
match self.tail {
HeaderTail::Async { async_id } => Some(async_id),
HeaderTail::Sync { .. } => None,
}
}
/// Parse from a byte slice. Returns the header and the remaining bytes.
pub fn parse(buf: &[u8]) -> ProtoResult<(Self, &[u8])> {
if buf.len() < SMB2_HEADER_LEN {
return Err(ProtoError::Malformed("short SMB2 header"));
}
let mut cursor = Cursor::new(&buf[..SMB2_HEADER_LEN]);
let raw = RawHeader::read(&mut cursor)?;
if raw.protocol_id != SMB2_MAGIC {
return Err(ProtoError::Malformed("bad SMB2 magic"));
}
if raw.structure_size != SMB2_HEADER_STRUCTURE_SIZE {
return Err(ProtoError::Malformed("SMB2 header structure_size != 64"));
}
let command = match Command::read_le(&mut Cursor::new(raw.command_raw.to_le_bytes())) {
Ok(c) => c,
Err(_) => {
return Err(ProtoError::Malformed("unknown SMB2 command opcode"));
}
};
let tail = if raw.flags & SMB2_FLAGS_ASYNC_COMMAND != 0 {
HeaderTail::Async {
async_id: u64::from_le_bytes(raw.tail_bytes),
}
} else {
let reserved = u32::from_le_bytes([
raw.tail_bytes[0],
raw.tail_bytes[1],
raw.tail_bytes[2],
raw.tail_bytes[3],
]);
let tree_id = u32::from_le_bytes([
raw.tail_bytes[4],
raw.tail_bytes[5],
raw.tail_bytes[6],
raw.tail_bytes[7],
]);
HeaderTail::Sync { reserved, tree_id }
};
Ok((
Smb2Header {
credit_charge: raw.credit_charge,
channel_sequence_status: raw.channel_sequence_status,
command,
credit_request_response: raw.credit_request_response,
flags: raw.flags,
next_command: raw.next_command,
message_id: raw.message_id,
tail,
session_id: raw.session_id,
signature: raw.signature,
},
&buf[SMB2_HEADER_LEN..],
))
}
/// Serialize the 64-byte header into `out`.
pub fn write(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let tail_bytes = match self.tail {
HeaderTail::Sync { reserved, tree_id } => {
let mut b = [0u8; 8];
b[..4].copy_from_slice(&reserved.to_le_bytes());
b[4..].copy_from_slice(&tree_id.to_le_bytes());
b
}
HeaderTail::Async { async_id } => async_id.to_le_bytes(),
};
let raw = RawHeader {
protocol_id: SMB2_MAGIC,
structure_size: SMB2_HEADER_STRUCTURE_SIZE,
credit_charge: self.credit_charge,
channel_sequence_status: self.channel_sequence_status,
command_raw: self.command.as_u16(),
credit_request_response: self.credit_request_response,
flags: self.flags,
next_command: self.next_command,
message_id: self.message_id,
tail_bytes,
session_id: self.session_id,
signature: self.signature,
};
let start = out.len();
let mut cursor = Cursor::new(Vec::with_capacity(SMB2_HEADER_LEN));
raw.write(&mut cursor)?;
out.extend_from_slice(&cursor.into_inner());
debug_assert_eq!(out.len() - start, SMB2_HEADER_LEN);
Ok(())
}
}
// ---------------------------------------------------------------------------
// Internal raw header for binrw plumbing.
// ---------------------------------------------------------------------------
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, Copy)]
struct RawHeader {
protocol_id: [u8; 4],
structure_size: u16,
credit_charge: u16,
channel_sequence_status: u32,
command_raw: u16,
credit_request_response: u16,
flags: u32,
next_command: u32,
message_id: u64,
tail_bytes: [u8; 8],
session_id: u64,
signature: [u8; 16],
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_sync() -> Smb2Header {
Smb2Header {
credit_charge: 1,
channel_sequence_status: 0,
command: Command::Negotiate,
credit_request_response: 1,
flags: 0,
next_command: 0,
message_id: 0,
tail: HeaderTail::Sync {
reserved: 0,
tree_id: 0,
},
session_id: 0,
signature: [0u8; 16],
}
}
fn sample_async() -> Smb2Header {
Smb2Header {
credit_charge: 4,
channel_sequence_status: 0,
command: Command::Read,
credit_request_response: 1,
flags: SMB2_FLAGS_ASYNC_COMMAND | SMB2_FLAGS_SERVER_TO_REDIR,
next_command: 0,
message_id: 42,
tail: HeaderTail::Async {
async_id: 0xDEAD_BEEF_CAFE_F00D,
},
session_id: 0x1122_3344_5566_7788,
signature: [0xAA; 16],
}
}
#[test]
fn sync_round_trips() {
let hdr = sample_sync();
let mut buf = Vec::new();
hdr.write(&mut buf).unwrap();
assert_eq!(buf.len(), SMB2_HEADER_LEN);
// First 4 bytes must be the magic.
assert_eq!(&buf[..4], &SMB2_MAGIC);
// StructureSize at offset 4 == 64
assert_eq!(u16::from_le_bytes([buf[4], buf[5]]), 64);
let (decoded, rest) = Smb2Header::parse(&buf).unwrap();
assert!(rest.is_empty());
assert_eq!(decoded, hdr);
}
#[test]
fn async_round_trips() {
let hdr = sample_async();
let mut buf = Vec::new();
hdr.write(&mut buf).unwrap();
assert_eq!(buf.len(), SMB2_HEADER_LEN);
let (decoded, _rest) = Smb2Header::parse(&buf).unwrap();
assert_eq!(decoded, hdr);
assert!(decoded.is_async());
assert!(decoded.is_response());
assert_eq!(decoded.async_id(), Some(0xDEAD_BEEF_CAFE_F00D));
assert_eq!(decoded.tree_id(), None);
}
#[test]
fn rejects_bad_magic() {
let hdr = sample_sync();
let mut buf = Vec::new();
hdr.write(&mut buf).unwrap();
buf[0] = 0xFF;
let err = Smb2Header::parse(&buf).unwrap_err();
assert!(matches!(err, ProtoError::Malformed(_)));
}
#[test]
fn rejects_bad_structure_size() {
let hdr = sample_sync();
let mut buf = Vec::new();
hdr.write(&mut buf).unwrap();
buf[4] = 0; // wreck the structure_size LE bytes
buf[5] = 0;
let err = Smb2Header::parse(&buf).unwrap_err();
assert!(matches!(err, ProtoError::Malformed(_)));
}
#[test]
fn rejects_short_buffer() {
let err = Smb2Header::parse(&[0u8; 32]).unwrap_err();
assert!(matches!(err, ProtoError::Malformed(_)));
}
#[test]
fn handcrafted_sync_negotiate_request() {
// Hand-built Sync NEGOTIATE request header: magic, size=64, no flags,
// command=0, mid=0, tree_id=0, sid=0, no signature.
let mut buf = vec![0u8; 64];
buf[..4].copy_from_slice(&SMB2_MAGIC);
buf[4..6].copy_from_slice(&64u16.to_le_bytes());
// command at offset 12 = 0 (NEGOTIATE), already zero
// everything else zero
let (hdr, _) = Smb2Header::parse(&buf).unwrap();
assert_eq!(hdr.command, Command::Negotiate);
assert!(!hdr.is_async());
assert_eq!(hdr.tree_id(), Some(0));
}
#[test]
fn command_round_trips_via_binrw() {
for cmd in [
Command::Negotiate,
Command::SessionSetup,
Command::Logoff,
Command::TreeConnect,
Command::TreeDisconnect,
Command::Create,
Command::Close,
Command::Flush,
Command::Read,
Command::Write,
Command::Lock,
Command::Ioctl,
Command::Cancel,
Command::Echo,
Command::QueryDirectory,
Command::ChangeNotify,
Command::QueryInfo,
Command::SetInfo,
Command::OplockBreak,
] {
let mut hdr = sample_sync();
hdr.command = cmd;
let mut buf = Vec::new();
hdr.write(&mut buf).unwrap();
let (decoded, _) = Smb2Header::parse(&buf).unwrap();
assert_eq!(decoded.command, cmd);
}
}
}

View File

@@ -0,0 +1,49 @@
//! CANCEL Request (MS-SMB2 §2.2.30). No response — server cancels in place.
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use crate::proto::error::ProtoResult;
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CancelRequest {
pub structure_size: u16,
pub reserved: u16,
}
impl Default for CancelRequest {
fn default() -> Self {
Self {
structure_size: 4,
reserved: 0,
}
}
}
impl CancelRequest {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips() {
let r = CancelRequest::default();
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(buf.len(), 4);
assert_eq!(CancelRequest::parse(&buf).unwrap(), r);
}
}

View File

@@ -0,0 +1,93 @@
//! CHANGE_NOTIFY Request/Response (MS-SMB2 §2.2.35 / §2.2.36).
//!
//! V1 returns `STATUS_NOT_SUPPORTED`, but we still parse/encode the wire
//! form so the dispatcher can recognize it.
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use super::create::FileId;
use crate::proto::error::ProtoResult;
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChangeNotifyRequest {
pub structure_size: u16,
pub flags: u16,
pub output_buffer_length: u32,
pub file_id: FileId,
pub completion_filter: u32,
pub reserved: u32,
}
impl ChangeNotifyRequest {
/// Flag: SMB2_WATCH_TREE.
pub const FLAG_WATCH_TREE: u16 = 0x0001;
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChangeNotifyResponse {
pub structure_size: u16,
pub output_buffer_offset: u16,
pub output_buffer_length: u32,
#[br(count = output_buffer_length as usize)]
pub buffer: Vec<u8>,
}
impl ChangeNotifyResponse {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_round_trips() {
let r = ChangeNotifyRequest {
structure_size: 32,
flags: ChangeNotifyRequest::FLAG_WATCH_TREE,
output_buffer_length: 0x1000,
file_id: FileId::new(1, 2),
completion_filter: 0xFF,
reserved: 0,
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(ChangeNotifyRequest::parse(&buf).unwrap(), r);
}
#[test]
fn response_round_trips() {
let r = ChangeNotifyResponse {
structure_size: 9,
output_buffer_offset: 0x48,
output_buffer_length: 0,
buffer: vec![],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(ChangeNotifyResponse::parse(&buf).unwrap(), r);
}
}

View File

@@ -0,0 +1,93 @@
//! CLOSE Request/Response (MS-SMB2 §2.2.15 / §2.2.16).
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use super::create::FileId;
use crate::proto::error::ProtoResult;
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CloseRequest {
pub structure_size: u16,
pub flags: u16,
pub reserved: u32,
pub file_id: FileId,
}
impl CloseRequest {
/// Flag: SMB2_CLOSE_FLAG_POSTQUERY_ATTRIB.
pub const FLAG_POSTQUERY_ATTRIB: u16 = 0x0001;
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CloseResponse {
pub structure_size: u16,
pub flags: u16,
pub reserved: u32,
pub creation_time: u64,
pub last_access_time: u64,
pub last_write_time: u64,
pub change_time: u64,
pub allocation_size: u64,
pub end_of_file: u64,
pub file_attributes: u32,
}
impl CloseResponse {
pub fn new() -> Self {
Self {
structure_size: 60,
..Default::default()
}
}
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips() {
let r = CloseRequest {
structure_size: 24,
flags: CloseRequest::FLAG_POSTQUERY_ATTRIB,
reserved: 0,
file_id: FileId::new(0x1, 0x2),
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(CloseRequest::parse(&buf).unwrap(), r);
let r = CloseResponse {
structure_size: 60,
..CloseResponse::new()
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(CloseResponse::parse(&buf).unwrap(), r);
}
}

View File

@@ -0,0 +1,437 @@
//! CREATE Request/Response (MS-SMB2 §2.2.13 / §2.2.14).
//!
//! `create_contexts` is a chained sequence of `SMB2_CREATE_CONTEXT` records
//! (MS-SMB2 §2.2.13.2). Each record has `Next` (offset to the next entry,
//! relative to the start of *this* entry; 0 marks the last), a name + data
//! pair, and 8-byte alignment.
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use crate::proto::error::{ProtoError, ProtoResult};
/// SMB2 FileId — opaque 16 bytes (volatile + persistent).
///
/// MS-SMB2 §2.2.14.1. We expose both halves; the server uses identical values
/// for both since durable handles are out of scope (spec §2 in the v1 design).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct FileId {
pub persistent: u64,
pub volatile: u64,
}
impl FileId {
pub const fn new(persistent: u64, volatile: u64) -> Self {
Self {
persistent,
volatile,
}
}
/// MS-SMB2: the "any" FileId is `0xFFFF…FFFF`.
pub const fn any() -> Self {
Self {
persistent: u64::MAX,
volatile: u64::MAX,
}
}
}
/// MS-SMB2 §2.2.13 CREATE Request — fixed prefix.
///
/// Variable-length tail: the file `name` (UTF-16LE) and `create_contexts`
/// blob, each at absolute offsets from the start of the SMB2 header. We hold
/// them as length-counted byte buffers immediately following the fixed
/// portion. The server crate parses contexts with [`CreateContext::parse_chain`].
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateRequest {
pub structure_size: u16,
pub security_flags: u8,
pub requested_oplock_level: u8,
pub impersonation_level: u32,
pub smb_create_flags: u64,
pub reserved: u64,
pub desired_access: u32,
pub file_attributes: u32,
pub share_access: u32,
pub create_disposition: u32,
pub create_options: u32,
pub name_offset: u16,
pub name_length: u16,
pub create_contexts_offset: u32,
pub create_contexts_length: u32,
/// UTF-16LE filename.
#[br(count = name_length as usize)]
pub name: Vec<u8>,
/// Raw create-contexts chain bytes; parse with
/// [`CreateContext::parse_chain`].
#[br(count = create_contexts_length as usize)]
pub create_contexts: Vec<u8>,
}
impl CreateRequest {
/// Decode the UTF-16LE filename.
pub fn name_str(&self) -> Option<String> {
if !self.name.len().is_multiple_of(2) {
return None;
}
let units: Vec<u16> = self
.name
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
Some(String::from_utf16_lossy(&units))
}
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
/// MS-SMB2 §2.2.14 CREATE Response.
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateResponse {
pub structure_size: u16,
pub oplock_level: u8,
pub flags: u8,
pub create_action: u32,
pub creation_time: u64,
pub last_access_time: u64,
pub last_write_time: u64,
pub change_time: u64,
pub allocation_size: u64,
pub end_of_file: u64,
pub file_attributes: u32,
pub reserved2: u32,
pub file_id: FileId,
pub create_contexts_offset: u32,
pub create_contexts_length: u32,
#[br(count = create_contexts_length as usize)]
pub create_contexts: Vec<u8>,
}
impl CreateResponse {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
// ---------------------------------------------------------------------------
// Create contexts (MS-SMB2 §2.2.13.2)
// ---------------------------------------------------------------------------
/// Generic SMB2_CREATE_CONTEXT envelope.
///
/// Per MS-SMB2 §2.2.13.2 each entry has:
/// * `Next` — offset (bytes) from the start of *this* entry to the start of
/// the next entry in the chain, or 0 for the last entry.
/// * `NameOffset`/`NameLength` — name (typically a 4-byte ASCII tag) at an
/// offset relative to the entry start.
/// * `Reserved` — 2 bytes.
/// * `DataOffset`/`DataLength` — payload at an offset relative to the entry
/// start.
///
/// We model the entry as `name` + `data` byte vectors plus the raw flags. The
/// chain reader / writer below handles `Next` and 8-byte alignment between
/// entries.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CreateContext {
pub name: Vec<u8>,
pub data: Vec<u8>,
}
impl CreateContext {
// Well-known names (MS-SMB2 §2.2.13.2 table). 4-byte ASCII tags.
pub const NAME_EXTA: &'static [u8; 4] = b"ExtA"; // SMB2_CREATE_EA_BUFFER
pub const NAME_SECD: &'static [u8; 4] = b"SecD"; // SMB2_CREATE_SD_BUFFER
pub const NAME_DHNQ: &'static [u8; 4] = b"DHnQ"; // DURABLE_HANDLE_REQUEST
pub const NAME_DHNC: &'static [u8; 4] = b"DHnC"; // DURABLE_HANDLE_RECONNECT
pub const NAME_ALSI: &'static [u8; 4] = b"AlSi"; // ALLOCATION_SIZE
pub const NAME_MXAC: &'static [u8; 4] = b"MxAc"; // QUERY_MAXIMAL_ACCESS
pub const NAME_TWRP: &'static [u8; 4] = b"TWrp"; // TIMEWARP_TOKEN
pub const NAME_QFID: &'static [u8; 4] = b"QFid"; // QUERY_ON_DISK_ID
pub const NAME_RQLS: &'static [u8; 4] = b"RqLs"; // REQUEST_LEASE
pub const NAME_DH2Q: &'static [u8; 4] = b"DH2Q"; // DURABLE_HANDLE_REQUEST_V2
pub const NAME_DH2C: &'static [u8; 4] = b"DH2C"; // DURABLE_HANDLE_RECONNECT_V2
/// Parse a chain of create-contexts from the raw chain bytes.
///
/// The chain is empty if `chain.is_empty()`. Otherwise we walk `Next`
/// offsets until we hit a zero terminator, validating bounds at each step.
pub fn parse_chain(chain: &[u8]) -> ProtoResult<Vec<CreateContext>> {
let mut out = Vec::new();
if chain.is_empty() {
return Ok(out);
}
let mut cursor_off = 0usize;
loop {
let entry = &chain
.get(cursor_off..)
.ok_or(ProtoError::Malformed("create context out of range"))?;
if entry.len() < 16 {
return Err(ProtoError::Malformed("create context too short"));
}
let next = u32::from_le_bytes([entry[0], entry[1], entry[2], entry[3]]) as usize;
let name_offset = u16::from_le_bytes([entry[4], entry[5]]) as usize;
let name_length = u16::from_le_bytes([entry[6], entry[7]]) as usize;
// entry[8..10] = reserved
let data_offset = u16::from_le_bytes([entry[10], entry[11]]) as usize;
let data_length =
u32::from_le_bytes([entry[12], entry[13], entry[14], entry[15]]) as usize;
let name = entry
.get(name_offset..name_offset + name_length)
.ok_or(ProtoError::Malformed("create context name out of range"))?
.to_vec();
let data = if data_length == 0 {
Vec::new()
} else {
entry
.get(data_offset..data_offset + data_length)
.ok_or(ProtoError::Malformed("create context data out of range"))?
.to_vec()
};
out.push(CreateContext { name, data });
if next == 0 {
break;
}
cursor_off = cursor_off
.checked_add(next)
.ok_or(ProtoError::Malformed("create context next overflow"))?;
}
Ok(out)
}
/// Encode a chain of create-contexts into `out`. Inserts `Next` offsets
/// and 8-byte alignment padding between entries.
pub fn encode_chain(list: &[CreateContext], out: &mut Vec<u8>) -> ProtoResult<()> {
if list.is_empty() {
return Ok(());
}
// We build the chain in a scratch buffer, then copy. Each entry is:
// 16-byte header + name + (pad to 8) + data + (pad to 8 if not last)
// The `Next` of every entry except the last is the size from this
// entry's start to the next entry's start.
let mut scratch: Vec<u8> = Vec::new();
let mut entry_starts: Vec<usize> = Vec::with_capacity(list.len());
for (i, ctx) in list.iter().enumerate() {
// Pad to 8-byte boundary before each entry (except possibly first
// — but contexts must be 8-byte aligned, and the chain itself is
// anchored at an 8-aligned offset by the server).
while !scratch.len().is_multiple_of(8) {
scratch.push(0);
}
entry_starts.push(scratch.len());
// Reserve 16 bytes for the header; will fill in once we know
// the actual offsets.
let header_pos = scratch.len();
scratch.extend_from_slice(&[0u8; 16]);
// Name immediately follows the header.
let name_offset_rel = (scratch.len() - header_pos) as u16;
scratch.extend_from_slice(&ctx.name);
// Pad to 8 before data.
while !(scratch.len() - header_pos).is_multiple_of(8) {
scratch.push(0);
}
let data_offset_rel = (scratch.len() - header_pos) as u16;
scratch.extend_from_slice(&ctx.data);
// Now backfill the header bytes (Next is patched after the loop).
let hdr = &mut scratch[header_pos..header_pos + 16];
hdr[0..4].copy_from_slice(&0u32.to_le_bytes()); // Next, fixed up below
hdr[4..6].copy_from_slice(&name_offset_rel.to_le_bytes());
hdr[6..8].copy_from_slice(&(ctx.name.len() as u16).to_le_bytes());
hdr[8..10].copy_from_slice(&0u16.to_le_bytes()); // Reserved
hdr[10..12].copy_from_slice(&data_offset_rel.to_le_bytes());
hdr[12..16].copy_from_slice(&(ctx.data.len() as u32).to_le_bytes());
// For non-last, pad the trailing data area to 8 so the next
// entry starts aligned.
if i + 1 < list.len() {
while !scratch.len().is_multiple_of(8) {
scratch.push(0);
}
}
}
// Patch `Next` offsets.
for i in 0..(entry_starts.len() - 1) {
let this = entry_starts[i];
let next = entry_starts[i + 1];
let delta = (next - this) as u32;
scratch[this..this + 4].copy_from_slice(&delta.to_le_bytes());
}
// Last entry's Next stays 0.
out.extend_from_slice(&scratch);
Ok(())
}
}
// ---------------------------------------------------------------------------
// Helper enums (oplock level, impersonation level)
// ---------------------------------------------------------------------------
/// MS-SMB2 §2.2.13 RequestedOplockLevel / §2.2.14 OplockLevel.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum OplockLevel {
None = 0x00,
Ii = 0x01,
Exclusive = 0x08,
Batch = 0x09,
Lease = 0xFF,
}
impl OplockLevel {
pub fn from_u8(v: u8) -> Option<Self> {
Some(match v {
0x00 => Self::None,
0x01 => Self::Ii,
0x08 => Self::Exclusive,
0x09 => Self::Batch,
0xFF => Self::Lease,
_ => return None,
})
}
}
/// MS-SMB2 §2.2.13 ImpersonationLevel.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum ImpersonationLevel {
Anonymous = 0x0000_0000,
Identification = 0x0000_0001,
Impersonation = 0x0000_0002,
Delegate = 0x0000_0003,
}
#[cfg(test)]
mod tests {
use super::*;
fn utf16le(s: &str) -> Vec<u8> {
s.encode_utf16().flat_map(u16::to_le_bytes).collect()
}
#[test]
fn request_round_trips() {
let name = utf16le("dir\\file.txt");
let r = CreateRequest {
structure_size: 57,
security_flags: 0,
requested_oplock_level: 0,
impersonation_level: ImpersonationLevel::Impersonation as u32,
smb_create_flags: 0,
reserved: 0,
desired_access: 0x0012_0089,
file_attributes: 0,
share_access: 0x0000_0007,
create_disposition: 1,
create_options: 0x0000_0040,
name_offset: 0x78,
name_length: name.len() as u16,
create_contexts_offset: 0,
create_contexts_length: 0,
name,
create_contexts: vec![],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
let decoded = CreateRequest::parse(&buf).unwrap();
assert_eq!(decoded, r);
assert_eq!(decoded.name_str().unwrap(), "dir\\file.txt");
}
#[test]
fn response_round_trips() {
let r = CreateResponse {
structure_size: 89,
oplock_level: 0,
flags: 0,
create_action: 1,
creation_time: 0x01D9_0000_0000_0000,
last_access_time: 0x01D9_0000_0000_0000,
last_write_time: 0x01D9_0000_0000_0000,
change_time: 0x01D9_0000_0000_0000,
allocation_size: 0x1000,
end_of_file: 0x800,
file_attributes: 0x0020,
reserved2: 0,
file_id: FileId::new(0x1234, 0x5678),
create_contexts_offset: 0,
create_contexts_length: 0,
create_contexts: vec![],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
let decoded = CreateResponse::parse(&buf).unwrap();
assert_eq!(decoded, r);
}
#[test]
fn create_context_chain_round_trips_single() {
let ctxs = vec![CreateContext {
name: b"MxAc".to_vec(),
data: vec![],
}];
let mut buf = Vec::new();
CreateContext::encode_chain(&ctxs, &mut buf).unwrap();
let decoded = CreateContext::parse_chain(&buf).unwrap();
assert_eq!(decoded, ctxs);
}
#[test]
fn create_context_chain_round_trips_multi() {
let ctxs = vec![
CreateContext {
name: b"DHnQ".to_vec(),
data: vec![0u8; 16],
},
CreateContext {
name: b"MxAc".to_vec(),
data: vec![],
},
CreateContext {
name: b"QFid".to_vec(),
data: vec![0xAA; 32],
},
];
let mut buf = Vec::new();
CreateContext::encode_chain(&ctxs, &mut buf).unwrap();
let decoded = CreateContext::parse_chain(&buf).unwrap();
assert_eq!(decoded, ctxs);
}
#[test]
fn empty_chain_round_trips() {
let ctxs: Vec<CreateContext> = vec![];
let mut buf = Vec::new();
CreateContext::encode_chain(&ctxs, &mut buf).unwrap();
assert!(buf.is_empty());
let decoded = CreateContext::parse_chain(&buf).unwrap();
assert!(decoded.is_empty());
}
}

View File

@@ -0,0 +1,83 @@
//! ECHO Request/Response (MS-SMB2 §2.2.28).
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use crate::proto::error::ProtoResult;
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EchoRequest {
pub structure_size: u16,
pub reserved: u16,
}
impl Default for EchoRequest {
fn default() -> Self {
Self {
structure_size: 4,
reserved: 0,
}
}
}
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EchoResponse {
pub structure_size: u16,
pub reserved: u16,
}
impl Default for EchoResponse {
fn default() -> Self {
Self {
structure_size: 4,
reserved: 0,
}
}
}
impl EchoRequest {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
impl EchoResponse {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips() {
let req = EchoRequest::default();
let mut buf = Vec::new();
req.write_to(&mut buf).unwrap();
assert_eq!(buf.len(), 4);
assert_eq!(EchoRequest::parse(&buf).unwrap(), req);
let resp = EchoResponse::default();
let mut buf = Vec::new();
resp.write_to(&mut buf).unwrap();
assert_eq!(EchoResponse::parse(&buf).unwrap(), resp);
}
}

View File

@@ -0,0 +1,84 @@
//! SMB2 ERROR Response (MS-SMB2 §2.2.2).
//!
//! Sent in place of any normal response when the server returns a non-zero
//! NTSTATUS. The SMB2 header carries the NTSTATUS in `channel_sequence_status`;
//! this body provides extended error context if any.
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use crate::proto::error::ProtoResult;
/// MS-SMB2 §2.2.2 ERROR Response.
///
/// `structure_size` is always 9; `byte_count` is the length of `error_data`
/// when there is no structured error context (the common case). When
/// `error_context_count > 0`, `error_data` holds a sequence of
/// [`ErrorContext`] entries (SMB 3.1.1+).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ErrorResponse {
pub structure_size: u16,
pub error_context_count: u8,
pub reserved: u8,
pub byte_count: u32,
#[br(count = if byte_count == 0 { 1 } else { byte_count as usize })]
pub error_data: Vec<u8>,
}
impl ErrorResponse {
/// Build a minimal ERROR response body for the given NTSTATUS.
///
/// Per MS-SMB2 §2.2.2 a zero-`byte_count` ERROR response still emits a
/// single byte of `error_data` (the field is mandatory, length 1 when
/// there is no payload).
pub fn status(_ntstatus: u32) -> Self {
Self {
structure_size: 9,
error_context_count: 0,
reserved: 0,
byte_count: 0,
error_data: vec![0],
}
}
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
let mut c = Cursor::new(buf);
Ok(Self::read(&mut c)?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
/// MS-SMB2 §2.2.2.1 ERROR Context Response (3.1.1+).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ErrorContext {
pub error_data_length: u32,
pub error_id: u32,
#[br(count = error_data_length as usize)]
pub error_context_data: Vec<u8>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips_status_helper() {
let r = ErrorResponse::status(0xC000_0022 /* STATUS_ACCESS_DENIED */);
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
let decoded = ErrorResponse::parse(&buf).unwrap();
assert_eq!(decoded, r);
// structure_size, contexts, reserved, bytecount, 1 byte payload = 9 bytes
assert_eq!(buf.len(), 9);
}
}

View File

@@ -0,0 +1,86 @@
//! FLUSH Request/Response (MS-SMB2 §2.2.17 / §2.2.18).
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use crate::proto::error::ProtoResult;
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FlushRequest {
pub structure_size: u16,
pub reserved1: u16,
pub reserved2: u32,
/// Volatile portion of the FileId.
pub file_id_persistent: u64,
/// Persistent portion of the FileId.
pub file_id_volatile: u64,
}
impl FlushRequest {
pub fn new(persistent: u64, volatile: u64) -> Self {
Self {
structure_size: 24,
reserved1: 0,
reserved2: 0,
file_id_persistent: persistent,
file_id_volatile: volatile,
}
}
}
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FlushResponse {
pub structure_size: u16,
pub reserved: u16,
}
impl Default for FlushResponse {
fn default() -> Self {
Self {
structure_size: 4,
reserved: 0,
}
}
}
macro_rules! impl_codec {
($t:ty) => {
impl $t {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(<Self as BinRead>::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
};
}
impl_codec!(FlushRequest);
impl_codec!(FlushResponse);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips() {
let r = FlushRequest::new(0x1122_3344_5566_7788, 0xAABB_CCDD_EEFF_0011);
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(buf.len(), 24);
assert_eq!(FlushRequest::parse(&buf).unwrap(), r);
let r = FlushResponse::default();
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(FlushResponse::parse(&buf).unwrap(), r);
}
}

View File

@@ -0,0 +1,206 @@
//! IOCTL Request/Response (MS-SMB2 §2.2.31 / §2.2.32).
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use super::create::FileId;
use crate::proto::error::ProtoResult;
/// File-system control codes we recognize at the wire layer.
///
/// MS-FSCC catalogues the FSCTL codes; we only enumerate the ones referenced
/// in the spec for v1. Unknown codes round-trip via [`Fsctl::Other`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Fsctl {
/// `FSCTL_VALIDATE_NEGOTIATE_INFO` — required handler in v1.
ValidateNegotiateInfo,
/// `FSCTL_DFS_GET_REFERRALS`.
DfsGetReferrals,
/// `FSCTL_DFS_GET_REFERRALS_EX`.
DfsGetReferralsEx,
/// `FSCTL_PIPE_TRANSCEIVE`.
PipeTranscede,
/// `FSCTL_PIPE_PEEK`.
PipePeek,
/// `FSCTL_PIPE_WAIT`.
PipeWait,
/// `FSCTL_LMR_REQUEST_RESILIENCY`.
LmrRequestResiliency,
/// `FSCTL_QUERY_NETWORK_INTERFACE_INFO`.
QueryNetworkInterfaceInfo,
/// Anything else.
Other(u32),
}
impl Fsctl {
pub const VALIDATE_NEGOTIATE_INFO: u32 = 0x0014_0204;
pub const DFS_GET_REFERRALS: u32 = 0x0006_0194;
pub const DFS_GET_REFERRALS_EX: u32 = 0x0006_0198;
pub const PIPE_TRANSCEIVE: u32 = 0x0011_C017;
pub const PIPE_PEEK: u32 = 0x0011_400C;
pub const PIPE_WAIT: u32 = 0x0011_C018;
pub const LMR_REQUEST_RESILIENCY: u32 = 0x001C_0017;
pub const QUERY_NETWORK_INTERFACE_INFO: u32 = 0x001F_C017;
pub fn from_u32(code: u32) -> Self {
match code {
Self::VALIDATE_NEGOTIATE_INFO => Self::ValidateNegotiateInfo,
Self::DFS_GET_REFERRALS => Self::DfsGetReferrals,
Self::DFS_GET_REFERRALS_EX => Self::DfsGetReferralsEx,
Self::PIPE_TRANSCEIVE => Self::PipeTranscede,
Self::PIPE_PEEK => Self::PipePeek,
Self::PIPE_WAIT => Self::PipeWait,
Self::LMR_REQUEST_RESILIENCY => Self::LmrRequestResiliency,
Self::QUERY_NETWORK_INTERFACE_INFO => Self::QueryNetworkInterfaceInfo,
other => Self::Other(other),
}
}
pub fn as_u32(self) -> u32 {
match self {
Self::ValidateNegotiateInfo => Self::VALIDATE_NEGOTIATE_INFO,
Self::DfsGetReferrals => Self::DFS_GET_REFERRALS,
Self::DfsGetReferralsEx => Self::DFS_GET_REFERRALS_EX,
Self::PipeTranscede => Self::PIPE_TRANSCEIVE,
Self::PipePeek => Self::PIPE_PEEK,
Self::PipeWait => Self::PIPE_WAIT,
Self::LmrRequestResiliency => Self::LMR_REQUEST_RESILIENCY,
Self::QueryNetworkInterfaceInfo => Self::QUERY_NETWORK_INTERFACE_INFO,
Self::Other(c) => c,
}
}
}
/// SMB2_IOCTL_REQUEST (MS-SMB2 §2.2.31).
///
/// `input_offset` and `output_offset` are absolute (from the start of the
/// SMB2 header). We model the input buffer immediately following the fixed
/// prefix; the output buffer area is unused on requests but kept for round
/// tripping and extension scenarios.
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IoctlRequest {
pub structure_size: u16,
pub reserved: u16,
pub ctl_code: u32,
pub file_id: FileId,
pub input_offset: u32,
pub input_count: u32,
pub max_input_response: u32,
pub output_offset: u32,
pub output_count: u32,
pub max_output_response: u32,
pub flags: u32,
pub reserved2: u32,
#[br(count = input_count as usize)]
pub input: Vec<u8>,
}
impl IoctlRequest {
/// Flag: SMB2_0_IOCTL_IS_FSCTL.
pub const FLAG_IS_FSCTL: u32 = 0x0000_0001;
pub fn fsctl(&self) -> Fsctl {
Fsctl::from_u32(self.ctl_code)
}
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
/// SMB2_IOCTL_RESPONSE (MS-SMB2 §2.2.32).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IoctlResponse {
pub structure_size: u16,
pub reserved: u16,
pub ctl_code: u32,
pub file_id: FileId,
pub input_offset: u32,
pub input_count: u32,
pub output_offset: u32,
pub output_count: u32,
pub flags: u32,
pub reserved2: u32,
/// Output buffer immediately following the fixed prefix.
#[br(count = output_count as usize)]
pub output: Vec<u8>,
}
impl IoctlResponse {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fsctl_decode_known() {
assert_eq!(Fsctl::from_u32(0x0014_0204), Fsctl::ValidateNegotiateInfo);
assert_eq!(Fsctl::from_u32(0xDEAD_BEEF), Fsctl::Other(0xDEAD_BEEF));
assert_eq!(Fsctl::ValidateNegotiateInfo.as_u32(), 0x0014_0204);
assert_eq!(Fsctl::Other(0xDEAD_BEEF).as_u32(), 0xDEAD_BEEF);
}
#[test]
fn request_round_trips() {
let r = IoctlRequest {
structure_size: 57,
reserved: 0,
ctl_code: Fsctl::VALIDATE_NEGOTIATE_INFO,
file_id: FileId::any(),
input_offset: 0x78,
input_count: 4,
max_input_response: 0,
output_offset: 0,
output_count: 0,
max_output_response: 0x1000,
flags: IoctlRequest::FLAG_IS_FSCTL,
reserved2: 0,
input: vec![0xCA, 0xFE, 0xBA, 0xBE],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
let decoded = IoctlRequest::parse(&buf).unwrap();
assert_eq!(decoded, r);
assert_eq!(decoded.fsctl(), Fsctl::ValidateNegotiateInfo);
}
#[test]
fn response_round_trips() {
let r = IoctlResponse {
structure_size: 49,
reserved: 0,
ctl_code: Fsctl::VALIDATE_NEGOTIATE_INFO,
file_id: FileId::any(),
input_offset: 0,
input_count: 0,
output_offset: 0x70,
output_count: 4,
flags: 0,
reserved2: 0,
output: vec![1, 2, 3, 4],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(IoctlResponse::parse(&buf).unwrap(), r);
}
}

View File

@@ -0,0 +1,118 @@
//! LOCK Request/Response (MS-SMB2 §2.2.26 / §2.2.27).
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use super::create::FileId;
use crate::proto::error::ProtoResult;
/// SMB2_LOCK_ELEMENT (MS-SMB2 §2.2.26.1) — exactly 24 bytes.
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LockElement {
pub offset: u64,
pub length: u64,
pub flags: u32,
pub reserved: u32,
}
impl LockElement {
pub const FLAG_SHARED_LOCK: u32 = 0x0000_0001;
pub const FLAG_EXCLUSIVE_LOCK: u32 = 0x0000_0002;
pub const FLAG_UNLOCK: u32 = 0x0000_0004;
pub const FLAG_FAIL_IMMEDIATELY: u32 = 0x0000_0010;
}
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LockRequest {
pub structure_size: u16,
pub lock_count: u16,
pub lock_sequence: u32,
pub file_id: FileId,
#[br(count = lock_count as usize)]
pub locks: Vec<LockElement>,
}
impl LockRequest {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LockResponse {
pub structure_size: u16,
pub reserved: u16,
}
impl Default for LockResponse {
fn default() -> Self {
Self {
structure_size: 4,
reserved: 0,
}
}
}
impl LockResponse {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_round_trips() {
let r = LockRequest {
structure_size: 48,
lock_count: 2,
lock_sequence: 0,
file_id: FileId::new(1, 2),
locks: vec![
LockElement {
offset: 0,
length: 16,
flags: LockElement::FLAG_EXCLUSIVE_LOCK,
reserved: 0,
},
LockElement {
offset: 0,
length: 16,
flags: LockElement::FLAG_UNLOCK,
reserved: 0,
},
],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(LockRequest::parse(&buf).unwrap(), r);
}
#[test]
fn response_round_trips() {
let r = LockResponse::default();
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(LockResponse::parse(&buf).unwrap(), r);
}
}

View File

@@ -0,0 +1,77 @@
//! LOGOFF Request/Response (MS-SMB2 §2.2.7 / §2.2.8).
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use crate::proto::error::ProtoResult;
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LogoffRequest {
pub structure_size: u16,
pub reserved: u16,
}
impl Default for LogoffRequest {
fn default() -> Self {
Self {
structure_size: 4,
reserved: 0,
}
}
}
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LogoffResponse {
pub structure_size: u16,
pub reserved: u16,
}
impl Default for LogoffResponse {
fn default() -> Self {
Self {
structure_size: 4,
reserved: 0,
}
}
}
macro_rules! impl_codec {
($t:ty) => {
impl $t {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(<Self as BinRead>::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
};
}
impl_codec!(LogoffRequest);
impl_codec!(LogoffResponse);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips() {
let r = LogoffRequest::default();
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(LogoffRequest::parse(&buf).unwrap(), r);
let r = LogoffResponse::default();
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(LogoffResponse::parse(&buf).unwrap(), r);
}
}

View File

@@ -0,0 +1,55 @@
//! Per-command request/response wire structs.
//!
//! Each SMB2 command (MS-SMB2 §2.2.3 — §2.2.18, §2.2.31, §2.2.37, §2.2.39)
//! gets its own submodule with a `…Request` and `…Response` struct, both
//! `binrw`-driven and round-trip safe.
//!
//! The crate does **not** implement command behavior — it only encodes/decodes
//! the wire bytes. The server crate owns dispatch and state.
pub mod cancel;
pub mod change_notify;
pub mod close;
pub mod create;
pub mod echo;
pub mod error_response;
pub mod flush;
pub mod ioctl;
pub mod lock;
pub mod logoff;
pub mod negotiate;
pub mod oplock_break;
pub mod query_directory;
pub mod query_info;
pub mod read;
pub mod session_setup;
pub mod set_info;
pub mod tree_connect;
pub mod tree_disconnect;
pub mod write;
pub use cancel::CancelRequest;
pub use change_notify::{ChangeNotifyRequest, ChangeNotifyResponse};
pub use close::{CloseRequest, CloseResponse};
pub use create::{
CreateContext, CreateRequest, CreateResponse, FileId, ImpersonationLevel, OplockLevel,
};
pub use echo::{EchoRequest, EchoResponse};
pub use error_response::{ErrorContext, ErrorResponse};
pub use flush::{FlushRequest, FlushResponse};
pub use ioctl::{Fsctl, IoctlRequest, IoctlResponse};
pub use lock::{LockElement, LockRequest, LockResponse};
pub use logoff::{LogoffRequest, LogoffResponse};
pub use negotiate::{
Dialect, EncryptionCapabilities, NegotiateContext, NegotiateContextData, NegotiateRequest,
NegotiateResponse, PreauthIntegrityCapabilities, SigningCapabilities,
};
pub use oplock_break::{OplockBreakAck, OplockBreakNotification};
pub use query_directory::{FileInfoClass, QueryDirectoryRequest, QueryDirectoryResponse};
pub use query_info::{InfoType, QueryInfoRequest, QueryInfoResponse};
pub use read::{ReadRequest, ReadResponse};
pub use session_setup::{SessionSetupRequest, SessionSetupResponse};
pub use set_info::{SetInfoRequest, SetInfoResponse};
pub use tree_connect::{TreeConnectRequest, TreeConnectResponse};
pub use tree_disconnect::{TreeDisconnectRequest, TreeDisconnectResponse};
pub use write::{WriteRequest, WriteResponse};

View File

@@ -0,0 +1,384 @@
//! NEGOTIATE Request/Response (MS-SMB2 §2.2.3 / §2.2.4) including the SMB
//! 3.1.1 negotiate-context machinery from §2.2.3.1.x and §2.2.4.x.
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use crate::proto::error::ProtoResult;
// ---------------------------------------------------------------------------
// Dialect
// ---------------------------------------------------------------------------
/// SMB2 dialect revision codes (MS-SMB2 §2.2.3 — DialectRevision).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u16)]
pub enum Dialect {
Smb202 = 0x0202,
Smb210 = 0x0210,
Smb300 = 0x0300,
Smb302 = 0x0302,
Smb311 = 0x0311,
/// Sent by SMB 2.0.2/2.1 clients via SMB1 negotiate; we accept it as a
/// signal to multi-protocol-negotiate. Value 0x02FF.
Smb2Wildcard = 0x02FF,
}
impl Dialect {
pub fn from_u16(v: u16) -> Option<Self> {
Some(match v {
0x0202 => Self::Smb202,
0x0210 => Self::Smb210,
0x0300 => Self::Smb300,
0x0302 => Self::Smb302,
0x0311 => Self::Smb311,
0x02FF => Self::Smb2Wildcard,
_ => return None,
})
}
pub const fn as_u16(self) -> u16 {
self as u16
}
}
// ---------------------------------------------------------------------------
// Negotiate request
// ---------------------------------------------------------------------------
/// MS-SMB2 §2.2.3 NEGOTIATE Request.
///
/// `dialects` is a sequence of u16 little-endian dialect codes; for SMB 3.1.1
/// the trailing `negotiate_context_list` carries variable-length contexts at
/// `negotiate_context_offset`.
///
/// Note on parsing: we deliberately don't try to read `negotiate_context_list`
/// here automatically, because its position is given by an absolute offset
/// from the *start of the SMB2 header*, not from the start of this body.
/// The server crate decodes this body, then if `dialects` includes 3.1.1 it
/// resolves `negotiate_context_offset` against the original packet buffer
/// and parses the contexts via [`NegotiateContext::parse_list`].
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NegotiateRequest {
pub structure_size: u16,
pub dialect_count: u16,
pub security_mode: u16,
pub reserved: u16,
pub capabilities: u32,
pub client_guid: [u8; 16],
/// 3.1.1: NegotiateContextOffset. 2.x/3.0/3.0.2: ClientStartTime.
pub negotiate_context_offset_or_client_start_time: u64,
#[br(count = dialect_count as usize)]
pub dialects: Vec<u16>,
}
impl NegotiateRequest {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
// ---------------------------------------------------------------------------
// Negotiate response
// ---------------------------------------------------------------------------
/// MS-SMB2 §2.2.4 NEGOTIATE Response.
///
/// The trailing `security_buffer` and (3.1.1) `negotiate_context_list` are
/// referenced by absolute offsets from the start of the SMB2 header. This
/// struct encodes the *fixed* portion plus a `security_buffer` that we treat
/// as a length-counted blob immediately following the fixed portion (the
/// common server layout). For 3.1.1 contexts, the server crate writes the
/// fixed portion via [`NegotiateResponse::write_to`], then appends 8-byte-
/// aligned negotiate contexts and patches `negotiate_context_offset` to the
/// post-padding offset.
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NegotiateResponse {
pub structure_size: u16,
pub security_mode: u16,
pub dialect_revision: u16,
/// 3.1.1: NegotiateContextCount. 2.x/3.0/3.0.2: Reserved.
pub negotiate_context_count_or_reserved: u16,
pub server_guid: [u8; 16],
pub capabilities: u32,
pub max_transact_size: u32,
pub max_read_size: u32,
pub max_write_size: u32,
/// 100ns ticks since 1601-01-01 UTC.
pub system_time: u64,
pub server_start_time: u64,
pub security_buffer_offset: u16,
pub security_buffer_length: u16,
/// 3.1.1: NegotiateContextOffset. 2.x/3.0/3.0.2: Reserved2.
pub negotiate_context_offset_or_reserved2: u32,
#[br(count = security_buffer_length as usize)]
pub security_buffer: Vec<u8>,
}
impl NegotiateResponse {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
// ---------------------------------------------------------------------------
// Negotiate contexts (SMB 3.1.1)
// ---------------------------------------------------------------------------
/// MS-SMB2 §2.2.3.1 / §2.2.4.x — NEGOTIATE_CONTEXT generic header.
///
/// Contexts are 8-byte-aligned in the chain (the trailing padding is between
/// contexts; see §2.2.3.1 "Each NEGOTIATE_CONTEXT MUST be 8-byte aligned").
/// `parse_list` / `encode_list` handle the alignment.
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NegotiateContext {
pub context_type: u16,
pub data_length: u16,
pub reserved: u32,
#[br(count = data_length as usize)]
pub data: Vec<u8>,
}
impl NegotiateContext {
pub const TYPE_PREAUTH_INTEGRITY: u16 = 0x0001;
pub const TYPE_ENCRYPTION: u16 = 0x0002;
pub const TYPE_COMPRESSION: u16 = 0x0003;
pub const TYPE_NETNAME_NEGOTIATE: u16 = 0x0005;
pub const TYPE_TRANSPORT_CAPS: u16 = 0x0006;
pub const TYPE_RDMA_TRANSFORM: u16 = 0x0007;
pub const TYPE_SIGNING: u16 = 0x0008;
/// Parse a chain of negotiate contexts from `buf`. The chain is a series
/// of (8-byte-aligned) [`NegotiateContext`] entries. `count` comes from
/// the parent message's `NegotiateContextCount`.
pub fn parse_list(mut buf: &[u8], count: u16) -> ProtoResult<Vec<NegotiateContext>> {
let mut out = Vec::with_capacity(count as usize);
let mut consumed_total = 0usize;
for _ in 0..count {
// Pad to 8-byte alignment relative to the start of the list.
let pad = (8 - (consumed_total % 8)) % 8;
if pad > 0 {
if buf.len() < pad {
return Err(crate::proto::error::ProtoError::Malformed(
"negotiate context alignment underflow",
));
}
buf = &buf[pad..];
consumed_total += pad;
}
let mut c = Cursor::new(buf);
let ctx = NegotiateContext::read(&mut c)?;
let consumed = c.position() as usize;
buf = &buf[consumed..];
consumed_total += consumed;
out.push(ctx);
}
Ok(out)
}
/// Encode a chain of negotiate contexts into `out`, inserting 8-byte
/// padding between entries.
pub fn encode_list(list: &[NegotiateContext], out: &mut Vec<u8>) -> ProtoResult<()> {
let start = out.len();
for (i, ctx) in list.iter().enumerate() {
if i > 0 {
let pad = (8 - ((out.len() - start) % 8)) % 8;
out.extend(std::iter::repeat_n(0u8, pad));
}
let mut c = Cursor::new(Vec::new());
BinWrite::write(ctx, &mut c)?;
out.extend_from_slice(&c.into_inner());
}
Ok(())
}
}
/// Parsed payload of a known [`NegotiateContext`] type. Convenience wrapper —
/// the wire form is always [`NegotiateContext`]; this enum is for callers who
/// prefer typed access.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NegotiateContextData {
PreauthIntegrity(PreauthIntegrityCapabilities),
Encryption(EncryptionCapabilities),
Signing(SigningCapabilities),
/// Unknown / unhandled context — preserve raw bytes for round-tripping.
Other {
context_type: u16,
data: Vec<u8>,
},
}
/// MS-SMB2 §2.2.3.1.1 / §2.2.4.1 SMB2_PREAUTH_INTEGRITY_CAPABILITIES.
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PreauthIntegrityCapabilities {
pub hash_algorithm_count: u16,
pub salt_length: u16,
#[br(count = hash_algorithm_count as usize)]
pub hash_algorithms: Vec<u16>,
#[br(count = salt_length as usize)]
pub salt: Vec<u8>,
}
impl PreauthIntegrityCapabilities {
/// Hash algorithm: SHA-512 (the only one defined in MS-SMB2 §2.2.3.1.1).
pub const HASH_SHA512: u16 = 0x0001;
}
/// MS-SMB2 §2.2.3.1.2 / §2.2.4.2 SMB2_ENCRYPTION_CAPABILITIES.
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EncryptionCapabilities {
pub cipher_count: u16,
#[br(count = cipher_count as usize)]
pub ciphers: Vec<u16>,
}
impl EncryptionCapabilities {
pub const CIPHER_AES_128_CCM: u16 = 0x0001;
pub const CIPHER_AES_128_GCM: u16 = 0x0002;
pub const CIPHER_AES_256_CCM: u16 = 0x0003;
pub const CIPHER_AES_256_GCM: u16 = 0x0004;
}
/// MS-SMB2 §2.2.3.1.7 / §2.2.4.7 SMB2_SIGNING_CAPABILITIES.
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SigningCapabilities {
pub signing_algorithm_count: u16,
#[br(count = signing_algorithm_count as usize)]
pub signing_algorithms: Vec<u16>,
}
impl SigningCapabilities {
pub const ALGORITHM_HMAC_SHA256: u16 = 0x0000;
pub const ALGORITHM_AES_CMAC: u16 = 0x0001;
pub const ALGORITHM_AES_GMAC: u16 = 0x0002;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn negotiate_request_round_trips() {
let req = NegotiateRequest {
structure_size: 36,
dialect_count: 5,
security_mode: 0x0001, // signing enabled
reserved: 0,
capabilities: 0x0000_007F,
client_guid: [0xAB; 16],
negotiate_context_offset_or_client_start_time: 0x0000_0070_0000_0000,
dialects: vec![0x0202, 0x0210, 0x0300, 0x0302, 0x0311],
};
let mut buf = Vec::new();
req.write_to(&mut buf).unwrap();
let decoded = NegotiateRequest::parse(&buf).unwrap();
assert_eq!(decoded, req);
}
#[test]
fn negotiate_response_round_trips() {
let resp = NegotiateResponse {
structure_size: 65,
security_mode: 0x0003,
dialect_revision: Dialect::Smb311.as_u16(),
negotiate_context_count_or_reserved: 3,
server_guid: [0xCD; 16],
capabilities: 0x0000_007F,
max_transact_size: 0x0010_0000,
max_read_size: 0x0010_0000,
max_write_size: 0x0010_0000,
system_time: 0x01D9_1234_5678_9ABC,
server_start_time: 0,
security_buffer_offset: 0x80,
security_buffer_length: 8,
negotiate_context_offset_or_reserved2: 0x100,
security_buffer: vec![1, 2, 3, 4, 5, 6, 7, 8],
};
let mut buf = Vec::new();
resp.write_to(&mut buf).unwrap();
let decoded = NegotiateResponse::parse(&buf).unwrap();
assert_eq!(decoded, resp);
}
#[test]
fn dialect_round_trips() {
for d in [
Dialect::Smb202,
Dialect::Smb210,
Dialect::Smb300,
Dialect::Smb302,
Dialect::Smb311,
Dialect::Smb2Wildcard,
] {
assert_eq!(Dialect::from_u16(d.as_u16()), Some(d));
}
assert_eq!(Dialect::from_u16(0xBEEF), None);
}
#[test]
fn preauth_caps_round_trips() {
let p = PreauthIntegrityCapabilities {
hash_algorithm_count: 1,
salt_length: 32,
hash_algorithms: vec![PreauthIntegrityCapabilities::HASH_SHA512],
salt: vec![0xAA; 32],
};
let mut buf = Vec::new();
let mut c = Cursor::new(&mut buf);
BinWrite::write(&p, &mut c).unwrap();
let decoded = PreauthIntegrityCapabilities::read(&mut Cursor::new(&buf)).unwrap();
assert_eq!(decoded, p);
}
#[test]
fn negotiate_context_list_round_trips() {
let list = vec![
NegotiateContext {
context_type: NegotiateContext::TYPE_PREAUTH_INTEGRITY,
data_length: 6,
reserved: 0,
data: vec![0x01, 0x00, 0x20, 0x00, 0x01, 0x00],
},
NegotiateContext {
context_type: NegotiateContext::TYPE_ENCRYPTION,
data_length: 4,
reserved: 0,
data: vec![0x02, 0x00, 0x02, 0x00],
},
NegotiateContext {
context_type: NegotiateContext::TYPE_SIGNING,
data_length: 4,
reserved: 0,
data: vec![0x01, 0x00, 0x01, 0x00],
},
];
let mut buf = Vec::new();
NegotiateContext::encode_list(&list, &mut buf).unwrap();
let parsed = NegotiateContext::parse_list(&buf, 3).unwrap();
assert_eq!(parsed, list);
}
}

View File

@@ -0,0 +1,59 @@
//! OPLOCK_BREAK Notification + Acknowledgement (MS-SMB2 §2.2.23 / §2.2.24).
//!
//! V1 never grants oplocks, so we never *send* a notification, but the
//! handler exists for safety. A client may send an OPLOCK_BREAK ACK before
//! the server has cleared its oplock state in the (rare) edge case during
//! teardown.
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use super::create::FileId;
use crate::proto::error::ProtoResult;
/// SMB2_OPLOCK_BREAK_NOTIFICATION (MS-SMB2 §2.2.23.1).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OplockBreakNotification {
pub structure_size: u16,
pub oplock_level: u8,
pub reserved: u8,
pub reserved2: u32,
pub file_id: FileId,
}
impl OplockBreakNotification {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
/// SMB2_OPLOCK_BREAK_ACK (MS-SMB2 §2.2.24.1) — same wire shape as the
/// notification.
pub type OplockBreakAck = OplockBreakNotification;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips() {
let r = OplockBreakNotification {
structure_size: 24,
oplock_level: 0,
reserved: 0,
reserved2: 0,
file_id: FileId::new(1, 2),
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(OplockBreakNotification::parse(&buf).unwrap(), r);
}
}

View File

@@ -0,0 +1,136 @@
//! QUERY_DIRECTORY Request/Response (MS-SMB2 §2.2.33 / §2.2.34).
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use super::create::FileId;
use crate::proto::error::ProtoResult;
/// File-info-class identifiers used in QUERY_DIRECTORY (MS-SMB2 §2.2.33
/// FileInformationClass).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum FileInfoClass {
FileDirectoryInformation = 0x01,
FileFullDirectoryInformation = 0x02,
FileBothDirectoryInformation = 0x03,
FileNamesInformation = 0x0C,
FileIdBothDirectoryInformation = 0x25,
FileIdFullDirectoryInformation = 0x26,
}
impl FileInfoClass {
pub fn from_u8(v: u8) -> Option<Self> {
Some(match v {
0x01 => Self::FileDirectoryInformation,
0x02 => Self::FileFullDirectoryInformation,
0x03 => Self::FileBothDirectoryInformation,
0x0C => Self::FileNamesInformation,
0x25 => Self::FileIdBothDirectoryInformation,
0x26 => Self::FileIdFullDirectoryInformation,
_ => return None,
})
}
}
/// SMB2_QUERY_DIRECTORY_REQUEST (MS-SMB2 §2.2.33).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueryDirectoryRequest {
pub structure_size: u16,
pub file_information_class: u8,
pub flags: u8,
pub file_index: u32,
pub file_id: FileId,
pub file_name_offset: u16,
pub file_name_length: u16,
pub output_buffer_length: u32,
/// UTF-16LE search pattern (e.g. "*").
#[br(count = file_name_length as usize)]
pub file_name: Vec<u8>,
}
impl QueryDirectoryRequest {
pub const FLAG_RESTART_SCANS: u8 = 0x01;
pub const FLAG_RETURN_SINGLE_ENTRY: u8 = 0x02;
pub const FLAG_INDEX_SPECIFIED: u8 = 0x04;
pub const FLAG_REOPEN: u8 = 0x10;
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
/// SMB2_QUERY_DIRECTORY_RESPONSE (MS-SMB2 §2.2.34).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueryDirectoryResponse {
pub structure_size: u16,
/// `OutputBufferOffset` is from the start of the SMB2 header.
pub output_buffer_offset: u16,
pub output_buffer_length: u32,
/// Variable-length info-class-specific buffer.
#[br(count = output_buffer_length as usize)]
pub buffer: Vec<u8>,
}
impl QueryDirectoryResponse {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn utf16le(s: &str) -> Vec<u8> {
s.encode_utf16().flat_map(u16::to_le_bytes).collect()
}
#[test]
fn request_round_trips() {
let pat = utf16le("*");
let r = QueryDirectoryRequest {
structure_size: 33,
file_information_class: FileInfoClass::FileIdBothDirectoryInformation as u8,
flags: QueryDirectoryRequest::FLAG_RESTART_SCANS,
file_index: 0,
file_id: FileId::new(1, 2),
file_name_offset: 0x60,
file_name_length: pat.len() as u16,
output_buffer_length: 0x10000,
file_name: pat,
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(QueryDirectoryRequest::parse(&buf).unwrap(), r);
}
#[test]
fn response_round_trips() {
let r = QueryDirectoryResponse {
structure_size: 9,
output_buffer_offset: 0x48,
output_buffer_length: 8,
buffer: vec![1, 2, 3, 4, 5, 6, 7, 8],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(QueryDirectoryResponse::parse(&buf).unwrap(), r);
}
}

View File

@@ -0,0 +1,140 @@
//! QUERY_INFO Request/Response (MS-SMB2 §2.2.37 / §2.2.38).
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use super::create::FileId;
use crate::proto::error::ProtoResult;
/// `InfoType` values (MS-SMB2 §2.2.37 InfoType field).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum InfoType {
File = 0x01,
FileSystem = 0x02,
Security = 0x03,
Quota = 0x04,
}
impl InfoType {
pub fn from_u8(v: u8) -> Option<Self> {
Some(match v {
0x01 => Self::File,
0x02 => Self::FileSystem,
0x03 => Self::Security,
0x04 => Self::Quota,
_ => return None,
})
}
}
/// SMB2_QUERY_INFO_REQUEST (MS-SMB2 §2.2.37).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueryInfoRequest {
pub structure_size: u16,
pub info_type: u8,
pub file_information_class: u8,
pub output_buffer_length: u32,
pub input_buffer_offset: u16,
pub reserved: u16,
pub input_buffer_length: u32,
/// `AdditionalInformation`: which fields of the security descriptor to
/// return when `info_type == Security`. Otherwise an additional info-class
/// selector for FS info.
pub additional_information: u32,
pub flags: u32,
pub file_id: FileId,
/// Optional input buffer (used by FILE/FS info classes that need it, e.g.
/// `FileFullEaInformation` extended-attribute name lists).
#[br(count = input_buffer_length as usize)]
pub input_buffer: Vec<u8>,
}
impl QueryInfoRequest {
/// Flag: SL_RESTART_SCAN.
pub const FLAG_RESTART_SCAN: u32 = 0x0000_0001;
/// Flag: SL_RETURN_SINGLE_ENTRY.
pub const FLAG_RETURN_SINGLE_ENTRY: u32 = 0x0000_0002;
/// Flag: SL_INDEX_SPECIFIED.
pub const FLAG_INDEX_SPECIFIED: u32 = 0x0000_0004;
pub fn info_type_enum(&self) -> Option<InfoType> {
InfoType::from_u8(self.info_type)
}
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
/// SMB2_QUERY_INFO_RESPONSE (MS-SMB2 §2.2.38).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueryInfoResponse {
pub structure_size: u16,
pub output_buffer_offset: u16,
pub output_buffer_length: u32,
#[br(count = output_buffer_length as usize)]
pub buffer: Vec<u8>,
}
impl QueryInfoResponse {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_round_trips() {
let r = QueryInfoRequest {
structure_size: 41,
info_type: InfoType::File as u8,
file_information_class: 0x05, // FileStandardInformation
output_buffer_length: 0x1000,
input_buffer_offset: 0,
reserved: 0,
input_buffer_length: 0,
additional_information: 0,
flags: 0,
file_id: FileId::new(1, 2),
input_buffer: vec![],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
let decoded = QueryInfoRequest::parse(&buf).unwrap();
assert_eq!(decoded, r);
assert_eq!(decoded.info_type_enum(), Some(InfoType::File));
}
#[test]
fn response_round_trips() {
let r = QueryInfoResponse {
structure_size: 9,
output_buffer_offset: 0x48,
output_buffer_length: 4,
buffer: vec![0xAB, 0xCD, 0xEF, 0x01],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(QueryInfoResponse::parse(&buf).unwrap(), r);
}
}

View File

@@ -0,0 +1,141 @@
//! READ Request/Response (MS-SMB2 §2.2.19 / §2.2.20).
//!
//! ## Data buffer offsets
//!
//! Both the READ request `ReadChannelInfoOffset` and the READ response
//! `DataOffset` are measured from the **start of the SMB2 header**, not from
//! the start of this structure (MS-SMB2 §2.2.20 explicitly: "DataOffset (1
//! byte): The offset, in bytes, from the beginning of the SMB2 header to the
//! data being read"). When constructing a response, the server crate must
//! compute `DataOffset = SMB2_HEADER_LEN + offset_within_body_of_data`.
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use super::create::FileId;
use crate::proto::error::ProtoResult;
/// SMB2_READ_REQUEST (MS-SMB2 §2.2.19).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReadRequest {
pub structure_size: u16,
pub padding: u8,
/// 3.0+ flags (`SMB2_READFLAG_*`); reserved on 2.x.
pub flags: u8,
pub length: u32,
pub offset: u64,
pub file_id: FileId,
pub minimum_count: u32,
pub channel: u32,
pub remaining_bytes: u32,
pub read_channel_info_offset: u16,
pub read_channel_info_length: u16,
/// MS-SMB2: "If ReadChannelInfoOffset and ReadChannelInfoLength are both
/// 0, the client MUST set this field to a single 0 byte." We follow that
/// — at least one byte of buffer is required on the wire.
#[br(count = if read_channel_info_length == 0 { 1 } else { read_channel_info_length as usize })]
pub buffer: Vec<u8>,
}
impl ReadRequest {
/// Flag: SMB2_READFLAG_READ_UNBUFFERED (3.0.2+).
pub const FLAG_READ_UNBUFFERED: u8 = 0x01;
/// Flag: SMB2_READFLAG_REQUEST_COMPRESSED (3.1.1+).
pub const FLAG_REQUEST_COMPRESSED: u8 = 0x02;
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
/// SMB2_READ_RESPONSE (MS-SMB2 §2.2.20).
///
/// `data_offset` is from the start of the SMB2 header. Use
/// [`ReadResponse::standard_data_offset`] for the canonical "data immediately
/// after the fixed prefix" layout.
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReadResponse {
pub structure_size: u16,
pub data_offset: u8,
pub reserved: u8,
pub data_length: u32,
pub data_remaining: u32,
/// 3.x: `Flags`. 2.x: reserved.
pub flags: u32,
#[br(count = data_length as usize)]
pub data: Vec<u8>,
}
impl ReadResponse {
/// Canonical `DataOffset` value when the data buffer immediately follows
/// the fixed 16-byte response prefix and the SMB2 header (64 + 16 = 80).
///
/// Most servers (ksmbd, Samba) emit 0x50 = 80 here.
pub const STANDARD_DATA_OFFSET: u8 = 0x50;
pub const fn standard_data_offset() -> u8 {
Self::STANDARD_DATA_OFFSET
}
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_round_trips() {
let r = ReadRequest {
structure_size: 49,
padding: 0x50,
flags: 0,
length: 0x1000,
offset: 0x2000,
file_id: FileId::new(0xAAAA, 0xBBBB),
minimum_count: 1,
channel: 0,
remaining_bytes: 0,
read_channel_info_offset: 0,
read_channel_info_length: 0,
buffer: vec![0],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(ReadRequest::parse(&buf).unwrap(), r);
}
#[test]
fn response_round_trips() {
let r = ReadResponse {
structure_size: 17,
data_offset: ReadResponse::STANDARD_DATA_OFFSET,
reserved: 0,
data_length: 5,
data_remaining: 0,
flags: 0,
data: vec![1, 2, 3, 4, 5],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(ReadResponse::parse(&buf).unwrap(), r);
}
}

View File

@@ -0,0 +1,113 @@
//! SESSION_SETUP Request/Response (MS-SMB2 §2.2.5 / §2.2.6).
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use crate::proto::error::ProtoResult;
/// SMB2_SESSION_SETUP_REQUEST (MS-SMB2 §2.2.5).
///
/// `security_buffer` is opaque GSS-API/SPNEGO data — the auth agent decodes it.
/// The wire offset is from the start of the SMB2 header; we encode/decode it
/// as length-counted data immediately following the fixed prefix, which is
/// the canonical layout. Server crate may patch the offset if it needs an
/// unusual layout.
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionSetupRequest {
pub structure_size: u16,
pub flags: u8,
pub security_mode: u8,
pub capabilities: u32,
pub channel: u32,
pub security_buffer_offset: u16,
pub security_buffer_length: u16,
pub previous_session_id: u64,
#[br(count = security_buffer_length as usize)]
pub security_buffer: Vec<u8>,
}
impl SessionSetupRequest {
/// Flag: SMB2_SESSION_FLAG_BINDING — bind to existing session (3.x).
pub const FLAG_BINDING: u8 = 0x01;
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
/// SMB2_SESSION_SETUP_RESPONSE (MS-SMB2 §2.2.6).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionSetupResponse {
pub structure_size: u16,
pub session_flags: u16,
pub security_buffer_offset: u16,
pub security_buffer_length: u16,
#[br(count = security_buffer_length as usize)]
pub security_buffer: Vec<u8>,
}
impl SessionSetupResponse {
/// Session flag: IS_GUEST.
pub const FLAG_IS_GUEST: u16 = 0x0001;
/// Session flag: IS_NULL (anonymous).
pub const FLAG_IS_NULL: u16 = 0x0002;
/// Session flag: ENCRYPT_DATA.
pub const FLAG_ENCRYPT_DATA: u16 = 0x0004;
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_round_trips() {
let r = SessionSetupRequest {
structure_size: 25,
flags: 0,
security_mode: 0x01,
capabilities: 0x01,
channel: 0,
security_buffer_offset: 0x58,
security_buffer_length: 6,
previous_session_id: 0,
security_buffer: vec![0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(SessionSetupRequest::parse(&buf).unwrap(), r);
}
#[test]
fn response_round_trips() {
let r = SessionSetupResponse {
structure_size: 9,
session_flags: SessionSetupResponse::FLAG_IS_GUEST,
security_buffer_offset: 0x48,
security_buffer_length: 4,
security_buffer: vec![1, 2, 3, 4],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(SessionSetupResponse::parse(&buf).unwrap(), r);
}
}

View File

@@ -0,0 +1,94 @@
//! SET_INFO Request/Response (MS-SMB2 §2.2.39 / §2.2.40).
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use super::create::FileId;
use crate::proto::error::ProtoResult;
/// SMB2_SET_INFO_REQUEST (MS-SMB2 §2.2.39).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SetInfoRequest {
pub structure_size: u16,
pub info_type: u8,
pub file_information_class: u8,
pub buffer_length: u32,
pub buffer_offset: u16,
pub reserved: u16,
pub additional_information: u32,
pub file_id: FileId,
#[br(count = buffer_length as usize)]
pub buffer: Vec<u8>,
}
impl SetInfoRequest {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
/// SMB2_SET_INFO_RESPONSE (MS-SMB2 §2.2.40).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SetInfoResponse {
pub structure_size: u16,
}
impl Default for SetInfoResponse {
fn default() -> Self {
Self { structure_size: 2 }
}
}
impl SetInfoResponse {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_round_trips() {
let r = SetInfoRequest {
structure_size: 33,
info_type: 0x01, // File
file_information_class: 0x14, // FileEndOfFileInformation
buffer_length: 8,
buffer_offset: 0x60,
reserved: 0,
additional_information: 0,
file_id: FileId::new(1, 2),
buffer: vec![0, 0, 0, 0x10, 0, 0, 0, 0],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(SetInfoRequest::parse(&buf).unwrap(), r);
}
#[test]
fn response_round_trips() {
let r = SetInfoResponse::default();
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(SetInfoResponse::parse(&buf).unwrap(), r);
assert_eq!(buf.len(), 2);
}
}

View File

@@ -0,0 +1,131 @@
//! TREE_CONNECT Request/Response (MS-SMB2 §2.2.9 / §2.2.10).
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use crate::proto::error::ProtoResult;
/// SMB2_TREE_CONNECT_REQUEST (MS-SMB2 §2.2.9).
///
/// `path` is UTF-16LE. The wire format gives `PathOffset` (from the start of
/// the SMB2 header) and `PathLength`; we encode/decode the path immediately
/// following the fixed prefix. The 3.1.1 tree-connect-context machinery
/// (extension `flags`, `path_offset`/`path_length` interpretation) is
/// preserved on the wire and the server crate inspects `flags` if needed.
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TreeConnectRequest {
pub structure_size: u16,
/// 3.1.1: flags. 2.x/3.0/3.0.2: reserved.
pub flags: u16,
pub path_offset: u16,
pub path_length: u16,
/// UTF-16LE share path bytes (e.g. `\\server\share`).
#[br(count = path_length as usize)]
pub path: Vec<u8>,
}
impl TreeConnectRequest {
/// Flag: SMB2_TREE_CONNECT_FLAG_CLUSTER_RECONNECT (3.1.1).
pub const FLAG_CLUSTER_RECONNECT: u16 = 0x0001;
/// Flag: SMB2_TREE_CONNECT_FLAG_REDIRECT_TO_OWNER (3.1.1).
pub const FLAG_REDIRECT_TO_OWNER: u16 = 0x0002;
/// Flag: SMB2_TREE_CONNECT_FLAG_EXTENSION_PRESENT (3.1.1).
pub const FLAG_EXTENSION_PRESENT: u16 = 0x0004;
/// Decode the UTF-16LE share path into a `String`. Returns `None` if the
/// stored bytes are not an even length (malformed UTF-16LE).
pub fn path_str(&self) -> Option<String> {
if !self.path.len().is_multiple_of(2) {
return None;
}
let units: Vec<u16> = self
.path
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
Some(String::from_utf16_lossy(&units))
}
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
/// SMB2_TREE_CONNECT_RESPONSE (MS-SMB2 §2.2.10).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TreeConnectResponse {
pub structure_size: u16,
pub share_type: u8,
pub reserved: u8,
pub share_flags: u32,
pub capabilities: u32,
pub maximal_access: u32,
}
impl TreeConnectResponse {
/// Share type: SMB2_SHARE_TYPE_DISK.
pub const SHARE_TYPE_DISK: u8 = 0x01;
pub const SHARE_TYPE_PIPE: u8 = 0x02;
pub const SHARE_TYPE_PRINT: u8 = 0x03;
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn utf16le(s: &str) -> Vec<u8> {
s.encode_utf16().flat_map(u16::to_le_bytes).collect()
}
#[test]
fn request_round_trips() {
let path = utf16le(r"\\server\share");
let r = TreeConnectRequest {
structure_size: 9,
flags: 0,
path_offset: 0x48,
path_length: path.len() as u16,
path,
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
let decoded = TreeConnectRequest::parse(&buf).unwrap();
assert_eq!(decoded, r);
assert_eq!(decoded.path_str().unwrap(), r"\\server\share");
}
#[test]
fn response_round_trips() {
let r = TreeConnectResponse {
structure_size: 16,
share_type: TreeConnectResponse::SHARE_TYPE_DISK,
reserved: 0,
share_flags: 0,
capabilities: 0,
maximal_access: 0x001F_01FF,
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(TreeConnectResponse::parse(&buf).unwrap(), r);
}
}

View File

@@ -0,0 +1,77 @@
//! TREE_DISCONNECT Request/Response (MS-SMB2 §2.2.11 / §2.2.12).
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use crate::proto::error::ProtoResult;
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TreeDisconnectRequest {
pub structure_size: u16,
pub reserved: u16,
}
impl Default for TreeDisconnectRequest {
fn default() -> Self {
Self {
structure_size: 4,
reserved: 0,
}
}
}
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TreeDisconnectResponse {
pub structure_size: u16,
pub reserved: u16,
}
impl Default for TreeDisconnectResponse {
fn default() -> Self {
Self {
structure_size: 4,
reserved: 0,
}
}
}
macro_rules! impl_codec {
($t:ty) => {
impl $t {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(<Self as BinRead>::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
};
}
impl_codec!(TreeDisconnectRequest);
impl_codec!(TreeDisconnectResponse);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips() {
let r = TreeDisconnectRequest::default();
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(TreeDisconnectRequest::parse(&buf).unwrap(), r);
let r = TreeDisconnectResponse::default();
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(TreeDisconnectResponse::parse(&buf).unwrap(), r);
}
}

View File

@@ -0,0 +1,123 @@
//! WRITE Request/Response (MS-SMB2 §2.2.21 / §2.2.22).
//!
//! ## Data buffer offsets
//!
//! `DataOffset` is from the **start of the SMB2 header**, not from the start
//! of this structure (MS-SMB2 §2.2.21). The canonical layout puts the data
//! immediately after the fixed 48-byte prefix, giving 64 + 48 = 112 = 0x70.
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use super::create::FileId;
use crate::proto::error::ProtoResult;
/// SMB2_WRITE_REQUEST (MS-SMB2 §2.2.21).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WriteRequest {
pub structure_size: u16,
pub data_offset: u16,
pub length: u32,
pub offset: u64,
pub file_id: FileId,
pub channel: u32,
pub remaining_bytes: u32,
pub write_channel_info_offset: u16,
pub write_channel_info_length: u16,
pub flags: u32,
/// MS-SMB2: at least 1 byte of payload buffer is required on the wire
/// even when length=0.
#[br(count = if length == 0 { 1 } else { length as usize })]
pub data: Vec<u8>,
}
impl WriteRequest {
/// Canonical `DataOffset` placing the data buffer immediately after the
/// fixed 48-byte WRITE prefix: 64 (SMB2 header) + 48 = 112 = 0x70.
pub const STANDARD_DATA_OFFSET: u16 = 0x70;
/// Flag: SMB2_WRITEFLAG_WRITE_THROUGH.
pub const FLAG_WRITE_THROUGH: u32 = 0x0000_0001;
/// Flag: SMB2_WRITEFLAG_WRITE_UNBUFFERED (3.0.2+).
pub const FLAG_WRITE_UNBUFFERED: u32 = 0x0000_0002;
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
/// SMB2_WRITE_RESPONSE (MS-SMB2 §2.2.22).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct WriteResponse {
pub structure_size: u16,
pub reserved: u16,
pub count: u32,
pub remaining: u32,
pub write_channel_info_offset: u16,
pub write_channel_info_length: u16,
}
impl WriteResponse {
pub fn new(count: u32) -> Self {
Self {
structure_size: 17,
reserved: 0,
count,
remaining: 0,
write_channel_info_offset: 0,
write_channel_info_length: 0,
}
}
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_round_trips() {
let r = WriteRequest {
structure_size: 49,
data_offset: WriteRequest::STANDARD_DATA_OFFSET,
length: 4,
offset: 0x100,
file_id: FileId::new(0xAA, 0xBB),
channel: 0,
remaining_bytes: 0,
write_channel_info_offset: 0,
write_channel_info_length: 0,
flags: 0,
data: vec![1, 2, 3, 4],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(WriteRequest::parse(&buf).unwrap(), r);
}
#[test]
fn response_round_trips() {
let r = WriteResponse::new(0x1000);
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(WriteResponse::parse(&buf).unwrap(), r);
}
}

16
vendor/smb-server/src/proto/mod.rs vendored Normal file
View File

@@ -0,0 +1,16 @@
//! SMB2/3 wire-format types, framing, signing, and authentication primitives.
//!
//! Layered into:
//! * [`framing`] — Direct-TCP/NetBIOS transport framing.
//! * [`header`] — SMB2 64-byte fixed header.
//! * [`messages`] — Per-command request/response structs.
//! * [`auth`] — NTLMv2 server-side authentication and minimal SPNEGO.
//! * [`crypto`] — Signing, key derivation, pre-auth integrity.
//! * [`error`] — Crate-wide error type.
pub mod auth;
pub mod crypto;
pub mod error;
pub mod framing;
pub mod header;
pub mod messages;