SMB Server Phase 2: VFS backend build fix + integration test
- 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:
11
vendor/smb-server/src/proto/auth.rs
vendored
Normal file
11
vendor/smb-server/src/proto/auth.rs
vendored
Normal 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
1053
vendor/smb-server/src/proto/auth/ntlm.rs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
524
vendor/smb-server/src/proto/auth/spnego.rs
vendored
Normal file
524
vendor/smb-server/src/proto/auth/spnego.rs
vendored
Normal 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
20
vendor/smb-server/src/proto/crypto.rs
vendored
Normal 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};
|
||||
146
vendor/smb-server/src/proto/crypto/kdf.rs
vendored
Normal file
146
vendor/smb-server/src/proto/crypto/kdf.rs
vendored
Normal 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());
|
||||
}
|
||||
}
|
||||
115
vendor/smb-server/src/proto/crypto/preauth.rs
vendored
Normal file
115
vendor/smb-server/src/proto/crypto/preauth.rs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
259
vendor/smb-server/src/proto/crypto/sign.rs
vendored
Normal file
259
vendor/smb-server/src/proto/crypto/sign.rs
vendored
Normal 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
26
vendor/smb-server/src/proto/error.rs
vendored
Normal 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
155
vendor/smb-server/src/proto/framing.rs
vendored
Normal 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
471
vendor/smb-server/src/proto/header.rs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
49
vendor/smb-server/src/proto/messages/cancel.rs
vendored
Normal file
49
vendor/smb-server/src/proto/messages/cancel.rs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
93
vendor/smb-server/src/proto/messages/change_notify.rs
vendored
Normal file
93
vendor/smb-server/src/proto/messages/change_notify.rs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
93
vendor/smb-server/src/proto/messages/close.rs
vendored
Normal file
93
vendor/smb-server/src/proto/messages/close.rs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
437
vendor/smb-server/src/proto/messages/create.rs
vendored
Normal file
437
vendor/smb-server/src/proto/messages/create.rs
vendored
Normal 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());
|
||||
}
|
||||
}
|
||||
83
vendor/smb-server/src/proto/messages/echo.rs
vendored
Normal file
83
vendor/smb-server/src/proto/messages/echo.rs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
84
vendor/smb-server/src/proto/messages/error_response.rs
vendored
Normal file
84
vendor/smb-server/src/proto/messages/error_response.rs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
86
vendor/smb-server/src/proto/messages/flush.rs
vendored
Normal file
86
vendor/smb-server/src/proto/messages/flush.rs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
206
vendor/smb-server/src/proto/messages/ioctl.rs
vendored
Normal file
206
vendor/smb-server/src/proto/messages/ioctl.rs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
118
vendor/smb-server/src/proto/messages/lock.rs
vendored
Normal file
118
vendor/smb-server/src/proto/messages/lock.rs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
77
vendor/smb-server/src/proto/messages/logoff.rs
vendored
Normal file
77
vendor/smb-server/src/proto/messages/logoff.rs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
55
vendor/smb-server/src/proto/messages/mod.rs
vendored
Normal file
55
vendor/smb-server/src/proto/messages/mod.rs
vendored
Normal 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};
|
||||
384
vendor/smb-server/src/proto/messages/negotiate.rs
vendored
Normal file
384
vendor/smb-server/src/proto/messages/negotiate.rs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
59
vendor/smb-server/src/proto/messages/oplock_break.rs
vendored
Normal file
59
vendor/smb-server/src/proto/messages/oplock_break.rs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
136
vendor/smb-server/src/proto/messages/query_directory.rs
vendored
Normal file
136
vendor/smb-server/src/proto/messages/query_directory.rs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
140
vendor/smb-server/src/proto/messages/query_info.rs
vendored
Normal file
140
vendor/smb-server/src/proto/messages/query_info.rs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
141
vendor/smb-server/src/proto/messages/read.rs
vendored
Normal file
141
vendor/smb-server/src/proto/messages/read.rs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
113
vendor/smb-server/src/proto/messages/session_setup.rs
vendored
Normal file
113
vendor/smb-server/src/proto/messages/session_setup.rs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
94
vendor/smb-server/src/proto/messages/set_info.rs
vendored
Normal file
94
vendor/smb-server/src/proto/messages/set_info.rs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
131
vendor/smb-server/src/proto/messages/tree_connect.rs
vendored
Normal file
131
vendor/smb-server/src/proto/messages/tree_connect.rs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
77
vendor/smb-server/src/proto/messages/tree_disconnect.rs
vendored
Normal file
77
vendor/smb-server/src/proto/messages/tree_disconnect.rs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
123
vendor/smb-server/src/proto/messages/write.rs
vendored
Normal file
123
vendor/smb-server/src/proto/messages/write.rs
vendored
Normal 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
16
vendor/smb-server/src/proto/mod.rs
vendored
Normal 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;
|
||||
Reference in New Issue
Block a user