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

- Add VfsFile: Send supertrait for Mutex compatibility
- Fix SmbServerCommand: struct → Subcommand enum with Start variant
- Fix tracing_subscriber::init() → try_init() to avoid panic when
  logger already initialized
- Fix CLI subcommand name: smb-server → smb-start (flatten naming)
- Add #[command(name = "smb-start")] for CLI disambiguation
- Fix unused variable warnings (smb_fs.rs, smb_server_backend.rs)
- Remove unused VfsFile imports (webdav.rs, scp_handler.rs)
- Integration test: Docker smbclient verified (list, upload, read)
This commit is contained in:
Warren
2026-06-20 19:42:29 +08:00
parent 45d050c0b3
commit 7eb528d35f
167 changed files with 59897 additions and 12 deletions
+49
View File
@@ -0,0 +1,49 @@
//! CANCEL Request (MS-SMB2 §2.2.30). No response — server cancels in place.
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use crate::proto::error::ProtoResult;
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CancelRequest {
pub structure_size: u16,
pub reserved: u16,
}
impl Default for CancelRequest {
fn default() -> Self {
Self {
structure_size: 4,
reserved: 0,
}
}
}
impl CancelRequest {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips() {
let r = CancelRequest::default();
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(buf.len(), 4);
assert_eq!(CancelRequest::parse(&buf).unwrap(), r);
}
}
+93
View File
@@ -0,0 +1,93 @@
//! CHANGE_NOTIFY Request/Response (MS-SMB2 §2.2.35 / §2.2.36).
//!
//! V1 returns `STATUS_NOT_SUPPORTED`, but we still parse/encode the wire
//! form so the dispatcher can recognize it.
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use super::create::FileId;
use crate::proto::error::ProtoResult;
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChangeNotifyRequest {
pub structure_size: u16,
pub flags: u16,
pub output_buffer_length: u32,
pub file_id: FileId,
pub completion_filter: u32,
pub reserved: u32,
}
impl ChangeNotifyRequest {
/// Flag: SMB2_WATCH_TREE.
pub const FLAG_WATCH_TREE: u16 = 0x0001;
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChangeNotifyResponse {
pub structure_size: u16,
pub output_buffer_offset: u16,
pub output_buffer_length: u32,
#[br(count = output_buffer_length as usize)]
pub buffer: Vec<u8>,
}
impl ChangeNotifyResponse {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_round_trips() {
let r = ChangeNotifyRequest {
structure_size: 32,
flags: ChangeNotifyRequest::FLAG_WATCH_TREE,
output_buffer_length: 0x1000,
file_id: FileId::new(1, 2),
completion_filter: 0xFF,
reserved: 0,
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(ChangeNotifyRequest::parse(&buf).unwrap(), r);
}
#[test]
fn response_round_trips() {
let r = ChangeNotifyResponse {
structure_size: 9,
output_buffer_offset: 0x48,
output_buffer_length: 0,
buffer: vec![],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(ChangeNotifyResponse::parse(&buf).unwrap(), r);
}
}
+93
View File
@@ -0,0 +1,93 @@
//! CLOSE Request/Response (MS-SMB2 §2.2.15 / §2.2.16).
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use super::create::FileId;
use crate::proto::error::ProtoResult;
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CloseRequest {
pub structure_size: u16,
pub flags: u16,
pub reserved: u32,
pub file_id: FileId,
}
impl CloseRequest {
/// Flag: SMB2_CLOSE_FLAG_POSTQUERY_ATTRIB.
pub const FLAG_POSTQUERY_ATTRIB: u16 = 0x0001;
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CloseResponse {
pub structure_size: u16,
pub flags: u16,
pub reserved: u32,
pub creation_time: u64,
pub last_access_time: u64,
pub last_write_time: u64,
pub change_time: u64,
pub allocation_size: u64,
pub end_of_file: u64,
pub file_attributes: u32,
}
impl CloseResponse {
pub fn new() -> Self {
Self {
structure_size: 60,
..Default::default()
}
}
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips() {
let r = CloseRequest {
structure_size: 24,
flags: CloseRequest::FLAG_POSTQUERY_ATTRIB,
reserved: 0,
file_id: FileId::new(0x1, 0x2),
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(CloseRequest::parse(&buf).unwrap(), r);
let r = CloseResponse {
structure_size: 60,
..CloseResponse::new()
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(CloseResponse::parse(&buf).unwrap(), r);
}
}
+437
View File
@@ -0,0 +1,437 @@
//! CREATE Request/Response (MS-SMB2 §2.2.13 / §2.2.14).
//!
//! `create_contexts` is a chained sequence of `SMB2_CREATE_CONTEXT` records
//! (MS-SMB2 §2.2.13.2). Each record has `Next` (offset to the next entry,
//! relative to the start of *this* entry; 0 marks the last), a name + data
//! pair, and 8-byte alignment.
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use crate::proto::error::{ProtoError, ProtoResult};
/// SMB2 FileId — opaque 16 bytes (volatile + persistent).
///
/// MS-SMB2 §2.2.14.1. We expose both halves; the server uses identical values
/// for both since durable handles are out of scope (spec §2 in the v1 design).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct FileId {
pub persistent: u64,
pub volatile: u64,
}
impl FileId {
pub const fn new(persistent: u64, volatile: u64) -> Self {
Self {
persistent,
volatile,
}
}
/// MS-SMB2: the "any" FileId is `0xFFFF…FFFF`.
pub const fn any() -> Self {
Self {
persistent: u64::MAX,
volatile: u64::MAX,
}
}
}
/// MS-SMB2 §2.2.13 CREATE Request — fixed prefix.
///
/// Variable-length tail: the file `name` (UTF-16LE) and `create_contexts`
/// blob, each at absolute offsets from the start of the SMB2 header. We hold
/// them as length-counted byte buffers immediately following the fixed
/// portion. The server crate parses contexts with [`CreateContext::parse_chain`].
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateRequest {
pub structure_size: u16,
pub security_flags: u8,
pub requested_oplock_level: u8,
pub impersonation_level: u32,
pub smb_create_flags: u64,
pub reserved: u64,
pub desired_access: u32,
pub file_attributes: u32,
pub share_access: u32,
pub create_disposition: u32,
pub create_options: u32,
pub name_offset: u16,
pub name_length: u16,
pub create_contexts_offset: u32,
pub create_contexts_length: u32,
/// UTF-16LE filename.
#[br(count = name_length as usize)]
pub name: Vec<u8>,
/// Raw create-contexts chain bytes; parse with
/// [`CreateContext::parse_chain`].
#[br(count = create_contexts_length as usize)]
pub create_contexts: Vec<u8>,
}
impl CreateRequest {
/// Decode the UTF-16LE filename.
pub fn name_str(&self) -> Option<String> {
if !self.name.len().is_multiple_of(2) {
return None;
}
let units: Vec<u16> = self
.name
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
Some(String::from_utf16_lossy(&units))
}
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
/// MS-SMB2 §2.2.14 CREATE Response.
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateResponse {
pub structure_size: u16,
pub oplock_level: u8,
pub flags: u8,
pub create_action: u32,
pub creation_time: u64,
pub last_access_time: u64,
pub last_write_time: u64,
pub change_time: u64,
pub allocation_size: u64,
pub end_of_file: u64,
pub file_attributes: u32,
pub reserved2: u32,
pub file_id: FileId,
pub create_contexts_offset: u32,
pub create_contexts_length: u32,
#[br(count = create_contexts_length as usize)]
pub create_contexts: Vec<u8>,
}
impl CreateResponse {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
// ---------------------------------------------------------------------------
// Create contexts (MS-SMB2 §2.2.13.2)
// ---------------------------------------------------------------------------
/// Generic SMB2_CREATE_CONTEXT envelope.
///
/// Per MS-SMB2 §2.2.13.2 each entry has:
/// * `Next` — offset (bytes) from the start of *this* entry to the start of
/// the next entry in the chain, or 0 for the last entry.
/// * `NameOffset`/`NameLength` — name (typically a 4-byte ASCII tag) at an
/// offset relative to the entry start.
/// * `Reserved` — 2 bytes.
/// * `DataOffset`/`DataLength` — payload at an offset relative to the entry
/// start.
///
/// We model the entry as `name` + `data` byte vectors plus the raw flags. The
/// chain reader / writer below handles `Next` and 8-byte alignment between
/// entries.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CreateContext {
pub name: Vec<u8>,
pub data: Vec<u8>,
}
impl CreateContext {
// Well-known names (MS-SMB2 §2.2.13.2 table). 4-byte ASCII tags.
pub const NAME_EXTA: &'static [u8; 4] = b"ExtA"; // SMB2_CREATE_EA_BUFFER
pub const NAME_SECD: &'static [u8; 4] = b"SecD"; // SMB2_CREATE_SD_BUFFER
pub const NAME_DHNQ: &'static [u8; 4] = b"DHnQ"; // DURABLE_HANDLE_REQUEST
pub const NAME_DHNC: &'static [u8; 4] = b"DHnC"; // DURABLE_HANDLE_RECONNECT
pub const NAME_ALSI: &'static [u8; 4] = b"AlSi"; // ALLOCATION_SIZE
pub const NAME_MXAC: &'static [u8; 4] = b"MxAc"; // QUERY_MAXIMAL_ACCESS
pub const NAME_TWRP: &'static [u8; 4] = b"TWrp"; // TIMEWARP_TOKEN
pub const NAME_QFID: &'static [u8; 4] = b"QFid"; // QUERY_ON_DISK_ID
pub const NAME_RQLS: &'static [u8; 4] = b"RqLs"; // REQUEST_LEASE
pub const NAME_DH2Q: &'static [u8; 4] = b"DH2Q"; // DURABLE_HANDLE_REQUEST_V2
pub const NAME_DH2C: &'static [u8; 4] = b"DH2C"; // DURABLE_HANDLE_RECONNECT_V2
/// Parse a chain of create-contexts from the raw chain bytes.
///
/// The chain is empty if `chain.is_empty()`. Otherwise we walk `Next`
/// offsets until we hit a zero terminator, validating bounds at each step.
pub fn parse_chain(chain: &[u8]) -> ProtoResult<Vec<CreateContext>> {
let mut out = Vec::new();
if chain.is_empty() {
return Ok(out);
}
let mut cursor_off = 0usize;
loop {
let entry = &chain
.get(cursor_off..)
.ok_or(ProtoError::Malformed("create context out of range"))?;
if entry.len() < 16 {
return Err(ProtoError::Malformed("create context too short"));
}
let next = u32::from_le_bytes([entry[0], entry[1], entry[2], entry[3]]) as usize;
let name_offset = u16::from_le_bytes([entry[4], entry[5]]) as usize;
let name_length = u16::from_le_bytes([entry[6], entry[7]]) as usize;
// entry[8..10] = reserved
let data_offset = u16::from_le_bytes([entry[10], entry[11]]) as usize;
let data_length =
u32::from_le_bytes([entry[12], entry[13], entry[14], entry[15]]) as usize;
let name = entry
.get(name_offset..name_offset + name_length)
.ok_or(ProtoError::Malformed("create context name out of range"))?
.to_vec();
let data = if data_length == 0 {
Vec::new()
} else {
entry
.get(data_offset..data_offset + data_length)
.ok_or(ProtoError::Malformed("create context data out of range"))?
.to_vec()
};
out.push(CreateContext { name, data });
if next == 0 {
break;
}
cursor_off = cursor_off
.checked_add(next)
.ok_or(ProtoError::Malformed("create context next overflow"))?;
}
Ok(out)
}
/// Encode a chain of create-contexts into `out`. Inserts `Next` offsets
/// and 8-byte alignment padding between entries.
pub fn encode_chain(list: &[CreateContext], out: &mut Vec<u8>) -> ProtoResult<()> {
if list.is_empty() {
return Ok(());
}
// We build the chain in a scratch buffer, then copy. Each entry is:
// 16-byte header + name + (pad to 8) + data + (pad to 8 if not last)
// The `Next` of every entry except the last is the size from this
// entry's start to the next entry's start.
let mut scratch: Vec<u8> = Vec::new();
let mut entry_starts: Vec<usize> = Vec::with_capacity(list.len());
for (i, ctx) in list.iter().enumerate() {
// Pad to 8-byte boundary before each entry (except possibly first
// — but contexts must be 8-byte aligned, and the chain itself is
// anchored at an 8-aligned offset by the server).
while !scratch.len().is_multiple_of(8) {
scratch.push(0);
}
entry_starts.push(scratch.len());
// Reserve 16 bytes for the header; will fill in once we know
// the actual offsets.
let header_pos = scratch.len();
scratch.extend_from_slice(&[0u8; 16]);
// Name immediately follows the header.
let name_offset_rel = (scratch.len() - header_pos) as u16;
scratch.extend_from_slice(&ctx.name);
// Pad to 8 before data.
while !(scratch.len() - header_pos).is_multiple_of(8) {
scratch.push(0);
}
let data_offset_rel = (scratch.len() - header_pos) as u16;
scratch.extend_from_slice(&ctx.data);
// Now backfill the header bytes (Next is patched after the loop).
let hdr = &mut scratch[header_pos..header_pos + 16];
hdr[0..4].copy_from_slice(&0u32.to_le_bytes()); // Next, fixed up below
hdr[4..6].copy_from_slice(&name_offset_rel.to_le_bytes());
hdr[6..8].copy_from_slice(&(ctx.name.len() as u16).to_le_bytes());
hdr[8..10].copy_from_slice(&0u16.to_le_bytes()); // Reserved
hdr[10..12].copy_from_slice(&data_offset_rel.to_le_bytes());
hdr[12..16].copy_from_slice(&(ctx.data.len() as u32).to_le_bytes());
// For non-last, pad the trailing data area to 8 so the next
// entry starts aligned.
if i + 1 < list.len() {
while !scratch.len().is_multiple_of(8) {
scratch.push(0);
}
}
}
// Patch `Next` offsets.
for i in 0..(entry_starts.len() - 1) {
let this = entry_starts[i];
let next = entry_starts[i + 1];
let delta = (next - this) as u32;
scratch[this..this + 4].copy_from_slice(&delta.to_le_bytes());
}
// Last entry's Next stays 0.
out.extend_from_slice(&scratch);
Ok(())
}
}
// ---------------------------------------------------------------------------
// Helper enums (oplock level, impersonation level)
// ---------------------------------------------------------------------------
/// MS-SMB2 §2.2.13 RequestedOplockLevel / §2.2.14 OplockLevel.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum OplockLevel {
None = 0x00,
Ii = 0x01,
Exclusive = 0x08,
Batch = 0x09,
Lease = 0xFF,
}
impl OplockLevel {
pub fn from_u8(v: u8) -> Option<Self> {
Some(match v {
0x00 => Self::None,
0x01 => Self::Ii,
0x08 => Self::Exclusive,
0x09 => Self::Batch,
0xFF => Self::Lease,
_ => return None,
})
}
}
/// MS-SMB2 §2.2.13 ImpersonationLevel.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum ImpersonationLevel {
Anonymous = 0x0000_0000,
Identification = 0x0000_0001,
Impersonation = 0x0000_0002,
Delegate = 0x0000_0003,
}
#[cfg(test)]
mod tests {
use super::*;
fn utf16le(s: &str) -> Vec<u8> {
s.encode_utf16().flat_map(u16::to_le_bytes).collect()
}
#[test]
fn request_round_trips() {
let name = utf16le("dir\\file.txt");
let r = CreateRequest {
structure_size: 57,
security_flags: 0,
requested_oplock_level: 0,
impersonation_level: ImpersonationLevel::Impersonation as u32,
smb_create_flags: 0,
reserved: 0,
desired_access: 0x0012_0089,
file_attributes: 0,
share_access: 0x0000_0007,
create_disposition: 1,
create_options: 0x0000_0040,
name_offset: 0x78,
name_length: name.len() as u16,
create_contexts_offset: 0,
create_contexts_length: 0,
name,
create_contexts: vec![],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
let decoded = CreateRequest::parse(&buf).unwrap();
assert_eq!(decoded, r);
assert_eq!(decoded.name_str().unwrap(), "dir\\file.txt");
}
#[test]
fn response_round_trips() {
let r = CreateResponse {
structure_size: 89,
oplock_level: 0,
flags: 0,
create_action: 1,
creation_time: 0x01D9_0000_0000_0000,
last_access_time: 0x01D9_0000_0000_0000,
last_write_time: 0x01D9_0000_0000_0000,
change_time: 0x01D9_0000_0000_0000,
allocation_size: 0x1000,
end_of_file: 0x800,
file_attributes: 0x0020,
reserved2: 0,
file_id: FileId::new(0x1234, 0x5678),
create_contexts_offset: 0,
create_contexts_length: 0,
create_contexts: vec![],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
let decoded = CreateResponse::parse(&buf).unwrap();
assert_eq!(decoded, r);
}
#[test]
fn create_context_chain_round_trips_single() {
let ctxs = vec![CreateContext {
name: b"MxAc".to_vec(),
data: vec![],
}];
let mut buf = Vec::new();
CreateContext::encode_chain(&ctxs, &mut buf).unwrap();
let decoded = CreateContext::parse_chain(&buf).unwrap();
assert_eq!(decoded, ctxs);
}
#[test]
fn create_context_chain_round_trips_multi() {
let ctxs = vec![
CreateContext {
name: b"DHnQ".to_vec(),
data: vec![0u8; 16],
},
CreateContext {
name: b"MxAc".to_vec(),
data: vec![],
},
CreateContext {
name: b"QFid".to_vec(),
data: vec![0xAA; 32],
},
];
let mut buf = Vec::new();
CreateContext::encode_chain(&ctxs, &mut buf).unwrap();
let decoded = CreateContext::parse_chain(&buf).unwrap();
assert_eq!(decoded, ctxs);
}
#[test]
fn empty_chain_round_trips() {
let ctxs: Vec<CreateContext> = vec![];
let mut buf = Vec::new();
CreateContext::encode_chain(&ctxs, &mut buf).unwrap();
assert!(buf.is_empty());
let decoded = CreateContext::parse_chain(&buf).unwrap();
assert!(decoded.is_empty());
}
}
+83
View File
@@ -0,0 +1,83 @@
//! ECHO Request/Response (MS-SMB2 §2.2.28).
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use crate::proto::error::ProtoResult;
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EchoRequest {
pub structure_size: u16,
pub reserved: u16,
}
impl Default for EchoRequest {
fn default() -> Self {
Self {
structure_size: 4,
reserved: 0,
}
}
}
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EchoResponse {
pub structure_size: u16,
pub reserved: u16,
}
impl Default for EchoResponse {
fn default() -> Self {
Self {
structure_size: 4,
reserved: 0,
}
}
}
impl EchoRequest {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
impl EchoResponse {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips() {
let req = EchoRequest::default();
let mut buf = Vec::new();
req.write_to(&mut buf).unwrap();
assert_eq!(buf.len(), 4);
assert_eq!(EchoRequest::parse(&buf).unwrap(), req);
let resp = EchoResponse::default();
let mut buf = Vec::new();
resp.write_to(&mut buf).unwrap();
assert_eq!(EchoResponse::parse(&buf).unwrap(), resp);
}
}
+84
View File
@@ -0,0 +1,84 @@
//! SMB2 ERROR Response (MS-SMB2 §2.2.2).
//!
//! Sent in place of any normal response when the server returns a non-zero
//! NTSTATUS. The SMB2 header carries the NTSTATUS in `channel_sequence_status`;
//! this body provides extended error context if any.
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use crate::proto::error::ProtoResult;
/// MS-SMB2 §2.2.2 ERROR Response.
///
/// `structure_size` is always 9; `byte_count` is the length of `error_data`
/// when there is no structured error context (the common case). When
/// `error_context_count > 0`, `error_data` holds a sequence of
/// [`ErrorContext`] entries (SMB 3.1.1+).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ErrorResponse {
pub structure_size: u16,
pub error_context_count: u8,
pub reserved: u8,
pub byte_count: u32,
#[br(count = if byte_count == 0 { 1 } else { byte_count as usize })]
pub error_data: Vec<u8>,
}
impl ErrorResponse {
/// Build a minimal ERROR response body for the given NTSTATUS.
///
/// Per MS-SMB2 §2.2.2 a zero-`byte_count` ERROR response still emits a
/// single byte of `error_data` (the field is mandatory, length 1 when
/// there is no payload).
pub fn status(_ntstatus: u32) -> Self {
Self {
structure_size: 9,
error_context_count: 0,
reserved: 0,
byte_count: 0,
error_data: vec![0],
}
}
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
let mut c = Cursor::new(buf);
Ok(Self::read(&mut c)?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
/// MS-SMB2 §2.2.2.1 ERROR Context Response (3.1.1+).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ErrorContext {
pub error_data_length: u32,
pub error_id: u32,
#[br(count = error_data_length as usize)]
pub error_context_data: Vec<u8>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips_status_helper() {
let r = ErrorResponse::status(0xC000_0022 /* STATUS_ACCESS_DENIED */);
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
let decoded = ErrorResponse::parse(&buf).unwrap();
assert_eq!(decoded, r);
// structure_size, contexts, reserved, bytecount, 1 byte payload = 9 bytes
assert_eq!(buf.len(), 9);
}
}
+86
View File
@@ -0,0 +1,86 @@
//! FLUSH Request/Response (MS-SMB2 §2.2.17 / §2.2.18).
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use crate::proto::error::ProtoResult;
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FlushRequest {
pub structure_size: u16,
pub reserved1: u16,
pub reserved2: u32,
/// Volatile portion of the FileId.
pub file_id_persistent: u64,
/// Persistent portion of the FileId.
pub file_id_volatile: u64,
}
impl FlushRequest {
pub fn new(persistent: u64, volatile: u64) -> Self {
Self {
structure_size: 24,
reserved1: 0,
reserved2: 0,
file_id_persistent: persistent,
file_id_volatile: volatile,
}
}
}
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FlushResponse {
pub structure_size: u16,
pub reserved: u16,
}
impl Default for FlushResponse {
fn default() -> Self {
Self {
structure_size: 4,
reserved: 0,
}
}
}
macro_rules! impl_codec {
($t:ty) => {
impl $t {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(<Self as BinRead>::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
};
}
impl_codec!(FlushRequest);
impl_codec!(FlushResponse);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips() {
let r = FlushRequest::new(0x1122_3344_5566_7788, 0xAABB_CCDD_EEFF_0011);
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(buf.len(), 24);
assert_eq!(FlushRequest::parse(&buf).unwrap(), r);
let r = FlushResponse::default();
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(FlushResponse::parse(&buf).unwrap(), r);
}
}
+206
View File
@@ -0,0 +1,206 @@
//! IOCTL Request/Response (MS-SMB2 §2.2.31 / §2.2.32).
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use super::create::FileId;
use crate::proto::error::ProtoResult;
/// File-system control codes we recognize at the wire layer.
///
/// MS-FSCC catalogues the FSCTL codes; we only enumerate the ones referenced
/// in the spec for v1. Unknown codes round-trip via [`Fsctl::Other`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Fsctl {
/// `FSCTL_VALIDATE_NEGOTIATE_INFO` — required handler in v1.
ValidateNegotiateInfo,
/// `FSCTL_DFS_GET_REFERRALS`.
DfsGetReferrals,
/// `FSCTL_DFS_GET_REFERRALS_EX`.
DfsGetReferralsEx,
/// `FSCTL_PIPE_TRANSCEIVE`.
PipeTranscede,
/// `FSCTL_PIPE_PEEK`.
PipePeek,
/// `FSCTL_PIPE_WAIT`.
PipeWait,
/// `FSCTL_LMR_REQUEST_RESILIENCY`.
LmrRequestResiliency,
/// `FSCTL_QUERY_NETWORK_INTERFACE_INFO`.
QueryNetworkInterfaceInfo,
/// Anything else.
Other(u32),
}
impl Fsctl {
pub const VALIDATE_NEGOTIATE_INFO: u32 = 0x0014_0204;
pub const DFS_GET_REFERRALS: u32 = 0x0006_0194;
pub const DFS_GET_REFERRALS_EX: u32 = 0x0006_0198;
pub const PIPE_TRANSCEIVE: u32 = 0x0011_C017;
pub const PIPE_PEEK: u32 = 0x0011_400C;
pub const PIPE_WAIT: u32 = 0x0011_C018;
pub const LMR_REQUEST_RESILIENCY: u32 = 0x001C_0017;
pub const QUERY_NETWORK_INTERFACE_INFO: u32 = 0x001F_C017;
pub fn from_u32(code: u32) -> Self {
match code {
Self::VALIDATE_NEGOTIATE_INFO => Self::ValidateNegotiateInfo,
Self::DFS_GET_REFERRALS => Self::DfsGetReferrals,
Self::DFS_GET_REFERRALS_EX => Self::DfsGetReferralsEx,
Self::PIPE_TRANSCEIVE => Self::PipeTranscede,
Self::PIPE_PEEK => Self::PipePeek,
Self::PIPE_WAIT => Self::PipeWait,
Self::LMR_REQUEST_RESILIENCY => Self::LmrRequestResiliency,
Self::QUERY_NETWORK_INTERFACE_INFO => Self::QueryNetworkInterfaceInfo,
other => Self::Other(other),
}
}
pub fn as_u32(self) -> u32 {
match self {
Self::ValidateNegotiateInfo => Self::VALIDATE_NEGOTIATE_INFO,
Self::DfsGetReferrals => Self::DFS_GET_REFERRALS,
Self::DfsGetReferralsEx => Self::DFS_GET_REFERRALS_EX,
Self::PipeTranscede => Self::PIPE_TRANSCEIVE,
Self::PipePeek => Self::PIPE_PEEK,
Self::PipeWait => Self::PIPE_WAIT,
Self::LmrRequestResiliency => Self::LMR_REQUEST_RESILIENCY,
Self::QueryNetworkInterfaceInfo => Self::QUERY_NETWORK_INTERFACE_INFO,
Self::Other(c) => c,
}
}
}
/// SMB2_IOCTL_REQUEST (MS-SMB2 §2.2.31).
///
/// `input_offset` and `output_offset` are absolute (from the start of the
/// SMB2 header). We model the input buffer immediately following the fixed
/// prefix; the output buffer area is unused on requests but kept for round
/// tripping and extension scenarios.
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IoctlRequest {
pub structure_size: u16,
pub reserved: u16,
pub ctl_code: u32,
pub file_id: FileId,
pub input_offset: u32,
pub input_count: u32,
pub max_input_response: u32,
pub output_offset: u32,
pub output_count: u32,
pub max_output_response: u32,
pub flags: u32,
pub reserved2: u32,
#[br(count = input_count as usize)]
pub input: Vec<u8>,
}
impl IoctlRequest {
/// Flag: SMB2_0_IOCTL_IS_FSCTL.
pub const FLAG_IS_FSCTL: u32 = 0x0000_0001;
pub fn fsctl(&self) -> Fsctl {
Fsctl::from_u32(self.ctl_code)
}
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
/// SMB2_IOCTL_RESPONSE (MS-SMB2 §2.2.32).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IoctlResponse {
pub structure_size: u16,
pub reserved: u16,
pub ctl_code: u32,
pub file_id: FileId,
pub input_offset: u32,
pub input_count: u32,
pub output_offset: u32,
pub output_count: u32,
pub flags: u32,
pub reserved2: u32,
/// Output buffer immediately following the fixed prefix.
#[br(count = output_count as usize)]
pub output: Vec<u8>,
}
impl IoctlResponse {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fsctl_decode_known() {
assert_eq!(Fsctl::from_u32(0x0014_0204), Fsctl::ValidateNegotiateInfo);
assert_eq!(Fsctl::from_u32(0xDEAD_BEEF), Fsctl::Other(0xDEAD_BEEF));
assert_eq!(Fsctl::ValidateNegotiateInfo.as_u32(), 0x0014_0204);
assert_eq!(Fsctl::Other(0xDEAD_BEEF).as_u32(), 0xDEAD_BEEF);
}
#[test]
fn request_round_trips() {
let r = IoctlRequest {
structure_size: 57,
reserved: 0,
ctl_code: Fsctl::VALIDATE_NEGOTIATE_INFO,
file_id: FileId::any(),
input_offset: 0x78,
input_count: 4,
max_input_response: 0,
output_offset: 0,
output_count: 0,
max_output_response: 0x1000,
flags: IoctlRequest::FLAG_IS_FSCTL,
reserved2: 0,
input: vec![0xCA, 0xFE, 0xBA, 0xBE],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
let decoded = IoctlRequest::parse(&buf).unwrap();
assert_eq!(decoded, r);
assert_eq!(decoded.fsctl(), Fsctl::ValidateNegotiateInfo);
}
#[test]
fn response_round_trips() {
let r = IoctlResponse {
structure_size: 49,
reserved: 0,
ctl_code: Fsctl::VALIDATE_NEGOTIATE_INFO,
file_id: FileId::any(),
input_offset: 0,
input_count: 0,
output_offset: 0x70,
output_count: 4,
flags: 0,
reserved2: 0,
output: vec![1, 2, 3, 4],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(IoctlResponse::parse(&buf).unwrap(), r);
}
}
+118
View File
@@ -0,0 +1,118 @@
//! LOCK Request/Response (MS-SMB2 §2.2.26 / §2.2.27).
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use super::create::FileId;
use crate::proto::error::ProtoResult;
/// SMB2_LOCK_ELEMENT (MS-SMB2 §2.2.26.1) — exactly 24 bytes.
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LockElement {
pub offset: u64,
pub length: u64,
pub flags: u32,
pub reserved: u32,
}
impl LockElement {
pub const FLAG_SHARED_LOCK: u32 = 0x0000_0001;
pub const FLAG_EXCLUSIVE_LOCK: u32 = 0x0000_0002;
pub const FLAG_UNLOCK: u32 = 0x0000_0004;
pub const FLAG_FAIL_IMMEDIATELY: u32 = 0x0000_0010;
}
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LockRequest {
pub structure_size: u16,
pub lock_count: u16,
pub lock_sequence: u32,
pub file_id: FileId,
#[br(count = lock_count as usize)]
pub locks: Vec<LockElement>,
}
impl LockRequest {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LockResponse {
pub structure_size: u16,
pub reserved: u16,
}
impl Default for LockResponse {
fn default() -> Self {
Self {
structure_size: 4,
reserved: 0,
}
}
}
impl LockResponse {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_round_trips() {
let r = LockRequest {
structure_size: 48,
lock_count: 2,
lock_sequence: 0,
file_id: FileId::new(1, 2),
locks: vec![
LockElement {
offset: 0,
length: 16,
flags: LockElement::FLAG_EXCLUSIVE_LOCK,
reserved: 0,
},
LockElement {
offset: 0,
length: 16,
flags: LockElement::FLAG_UNLOCK,
reserved: 0,
},
],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(LockRequest::parse(&buf).unwrap(), r);
}
#[test]
fn response_round_trips() {
let r = LockResponse::default();
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(LockResponse::parse(&buf).unwrap(), r);
}
}
+77
View File
@@ -0,0 +1,77 @@
//! LOGOFF Request/Response (MS-SMB2 §2.2.7 / §2.2.8).
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use crate::proto::error::ProtoResult;
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LogoffRequest {
pub structure_size: u16,
pub reserved: u16,
}
impl Default for LogoffRequest {
fn default() -> Self {
Self {
structure_size: 4,
reserved: 0,
}
}
}
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LogoffResponse {
pub structure_size: u16,
pub reserved: u16,
}
impl Default for LogoffResponse {
fn default() -> Self {
Self {
structure_size: 4,
reserved: 0,
}
}
}
macro_rules! impl_codec {
($t:ty) => {
impl $t {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(<Self as BinRead>::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
};
}
impl_codec!(LogoffRequest);
impl_codec!(LogoffResponse);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips() {
let r = LogoffRequest::default();
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(LogoffRequest::parse(&buf).unwrap(), r);
let r = LogoffResponse::default();
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(LogoffResponse::parse(&buf).unwrap(), r);
}
}
+55
View File
@@ -0,0 +1,55 @@
//! Per-command request/response wire structs.
//!
//! Each SMB2 command (MS-SMB2 §2.2.3 — §2.2.18, §2.2.31, §2.2.37, §2.2.39)
//! gets its own submodule with a `…Request` and `…Response` struct, both
//! `binrw`-driven and round-trip safe.
//!
//! The crate does **not** implement command behavior — it only encodes/decodes
//! the wire bytes. The server crate owns dispatch and state.
pub mod cancel;
pub mod change_notify;
pub mod close;
pub mod create;
pub mod echo;
pub mod error_response;
pub mod flush;
pub mod ioctl;
pub mod lock;
pub mod logoff;
pub mod negotiate;
pub mod oplock_break;
pub mod query_directory;
pub mod query_info;
pub mod read;
pub mod session_setup;
pub mod set_info;
pub mod tree_connect;
pub mod tree_disconnect;
pub mod write;
pub use cancel::CancelRequest;
pub use change_notify::{ChangeNotifyRequest, ChangeNotifyResponse};
pub use close::{CloseRequest, CloseResponse};
pub use create::{
CreateContext, CreateRequest, CreateResponse, FileId, ImpersonationLevel, OplockLevel,
};
pub use echo::{EchoRequest, EchoResponse};
pub use error_response::{ErrorContext, ErrorResponse};
pub use flush::{FlushRequest, FlushResponse};
pub use ioctl::{Fsctl, IoctlRequest, IoctlResponse};
pub use lock::{LockElement, LockRequest, LockResponse};
pub use logoff::{LogoffRequest, LogoffResponse};
pub use negotiate::{
Dialect, EncryptionCapabilities, NegotiateContext, NegotiateContextData, NegotiateRequest,
NegotiateResponse, PreauthIntegrityCapabilities, SigningCapabilities,
};
pub use oplock_break::{OplockBreakAck, OplockBreakNotification};
pub use query_directory::{FileInfoClass, QueryDirectoryRequest, QueryDirectoryResponse};
pub use query_info::{InfoType, QueryInfoRequest, QueryInfoResponse};
pub use read::{ReadRequest, ReadResponse};
pub use session_setup::{SessionSetupRequest, SessionSetupResponse};
pub use set_info::{SetInfoRequest, SetInfoResponse};
pub use tree_connect::{TreeConnectRequest, TreeConnectResponse};
pub use tree_disconnect::{TreeDisconnectRequest, TreeDisconnectResponse};
pub use write::{WriteRequest, WriteResponse};
+384
View File
@@ -0,0 +1,384 @@
//! NEGOTIATE Request/Response (MS-SMB2 §2.2.3 / §2.2.4) including the SMB
//! 3.1.1 negotiate-context machinery from §2.2.3.1.x and §2.2.4.x.
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use crate::proto::error::ProtoResult;
// ---------------------------------------------------------------------------
// Dialect
// ---------------------------------------------------------------------------
/// SMB2 dialect revision codes (MS-SMB2 §2.2.3 — DialectRevision).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u16)]
pub enum Dialect {
Smb202 = 0x0202,
Smb210 = 0x0210,
Smb300 = 0x0300,
Smb302 = 0x0302,
Smb311 = 0x0311,
/// Sent by SMB 2.0.2/2.1 clients via SMB1 negotiate; we accept it as a
/// signal to multi-protocol-negotiate. Value 0x02FF.
Smb2Wildcard = 0x02FF,
}
impl Dialect {
pub fn from_u16(v: u16) -> Option<Self> {
Some(match v {
0x0202 => Self::Smb202,
0x0210 => Self::Smb210,
0x0300 => Self::Smb300,
0x0302 => Self::Smb302,
0x0311 => Self::Smb311,
0x02FF => Self::Smb2Wildcard,
_ => return None,
})
}
pub const fn as_u16(self) -> u16 {
self as u16
}
}
// ---------------------------------------------------------------------------
// Negotiate request
// ---------------------------------------------------------------------------
/// MS-SMB2 §2.2.3 NEGOTIATE Request.
///
/// `dialects` is a sequence of u16 little-endian dialect codes; for SMB 3.1.1
/// the trailing `negotiate_context_list` carries variable-length contexts at
/// `negotiate_context_offset`.
///
/// Note on parsing: we deliberately don't try to read `negotiate_context_list`
/// here automatically, because its position is given by an absolute offset
/// from the *start of the SMB2 header*, not from the start of this body.
/// The server crate decodes this body, then if `dialects` includes 3.1.1 it
/// resolves `negotiate_context_offset` against the original packet buffer
/// and parses the contexts via [`NegotiateContext::parse_list`].
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NegotiateRequest {
pub structure_size: u16,
pub dialect_count: u16,
pub security_mode: u16,
pub reserved: u16,
pub capabilities: u32,
pub client_guid: [u8; 16],
/// 3.1.1: NegotiateContextOffset. 2.x/3.0/3.0.2: ClientStartTime.
pub negotiate_context_offset_or_client_start_time: u64,
#[br(count = dialect_count as usize)]
pub dialects: Vec<u16>,
}
impl NegotiateRequest {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
// ---------------------------------------------------------------------------
// Negotiate response
// ---------------------------------------------------------------------------
/// MS-SMB2 §2.2.4 NEGOTIATE Response.
///
/// The trailing `security_buffer` and (3.1.1) `negotiate_context_list` are
/// referenced by absolute offsets from the start of the SMB2 header. This
/// struct encodes the *fixed* portion plus a `security_buffer` that we treat
/// as a length-counted blob immediately following the fixed portion (the
/// common server layout). For 3.1.1 contexts, the server crate writes the
/// fixed portion via [`NegotiateResponse::write_to`], then appends 8-byte-
/// aligned negotiate contexts and patches `negotiate_context_offset` to the
/// post-padding offset.
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NegotiateResponse {
pub structure_size: u16,
pub security_mode: u16,
pub dialect_revision: u16,
/// 3.1.1: NegotiateContextCount. 2.x/3.0/3.0.2: Reserved.
pub negotiate_context_count_or_reserved: u16,
pub server_guid: [u8; 16],
pub capabilities: u32,
pub max_transact_size: u32,
pub max_read_size: u32,
pub max_write_size: u32,
/// 100ns ticks since 1601-01-01 UTC.
pub system_time: u64,
pub server_start_time: u64,
pub security_buffer_offset: u16,
pub security_buffer_length: u16,
/// 3.1.1: NegotiateContextOffset. 2.x/3.0/3.0.2: Reserved2.
pub negotiate_context_offset_or_reserved2: u32,
#[br(count = security_buffer_length as usize)]
pub security_buffer: Vec<u8>,
}
impl NegotiateResponse {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
// ---------------------------------------------------------------------------
// Negotiate contexts (SMB 3.1.1)
// ---------------------------------------------------------------------------
/// MS-SMB2 §2.2.3.1 / §2.2.4.x — NEGOTIATE_CONTEXT generic header.
///
/// Contexts are 8-byte-aligned in the chain (the trailing padding is between
/// contexts; see §2.2.3.1 "Each NEGOTIATE_CONTEXT MUST be 8-byte aligned").
/// `parse_list` / `encode_list` handle the alignment.
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NegotiateContext {
pub context_type: u16,
pub data_length: u16,
pub reserved: u32,
#[br(count = data_length as usize)]
pub data: Vec<u8>,
}
impl NegotiateContext {
pub const TYPE_PREAUTH_INTEGRITY: u16 = 0x0001;
pub const TYPE_ENCRYPTION: u16 = 0x0002;
pub const TYPE_COMPRESSION: u16 = 0x0003;
pub const TYPE_NETNAME_NEGOTIATE: u16 = 0x0005;
pub const TYPE_TRANSPORT_CAPS: u16 = 0x0006;
pub const TYPE_RDMA_TRANSFORM: u16 = 0x0007;
pub const TYPE_SIGNING: u16 = 0x0008;
/// Parse a chain of negotiate contexts from `buf`. The chain is a series
/// of (8-byte-aligned) [`NegotiateContext`] entries. `count` comes from
/// the parent message's `NegotiateContextCount`.
pub fn parse_list(mut buf: &[u8], count: u16) -> ProtoResult<Vec<NegotiateContext>> {
let mut out = Vec::with_capacity(count as usize);
let mut consumed_total = 0usize;
for _ in 0..count {
// Pad to 8-byte alignment relative to the start of the list.
let pad = (8 - (consumed_total % 8)) % 8;
if pad > 0 {
if buf.len() < pad {
return Err(crate::proto::error::ProtoError::Malformed(
"negotiate context alignment underflow",
));
}
buf = &buf[pad..];
consumed_total += pad;
}
let mut c = Cursor::new(buf);
let ctx = NegotiateContext::read(&mut c)?;
let consumed = c.position() as usize;
buf = &buf[consumed..];
consumed_total += consumed;
out.push(ctx);
}
Ok(out)
}
/// Encode a chain of negotiate contexts into `out`, inserting 8-byte
/// padding between entries.
pub fn encode_list(list: &[NegotiateContext], out: &mut Vec<u8>) -> ProtoResult<()> {
let start = out.len();
for (i, ctx) in list.iter().enumerate() {
if i > 0 {
let pad = (8 - ((out.len() - start) % 8)) % 8;
out.extend(std::iter::repeat_n(0u8, pad));
}
let mut c = Cursor::new(Vec::new());
BinWrite::write(ctx, &mut c)?;
out.extend_from_slice(&c.into_inner());
}
Ok(())
}
}
/// Parsed payload of a known [`NegotiateContext`] type. Convenience wrapper —
/// the wire form is always [`NegotiateContext`]; this enum is for callers who
/// prefer typed access.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NegotiateContextData {
PreauthIntegrity(PreauthIntegrityCapabilities),
Encryption(EncryptionCapabilities),
Signing(SigningCapabilities),
/// Unknown / unhandled context — preserve raw bytes for round-tripping.
Other {
context_type: u16,
data: Vec<u8>,
},
}
/// MS-SMB2 §2.2.3.1.1 / §2.2.4.1 SMB2_PREAUTH_INTEGRITY_CAPABILITIES.
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PreauthIntegrityCapabilities {
pub hash_algorithm_count: u16,
pub salt_length: u16,
#[br(count = hash_algorithm_count as usize)]
pub hash_algorithms: Vec<u16>,
#[br(count = salt_length as usize)]
pub salt: Vec<u8>,
}
impl PreauthIntegrityCapabilities {
/// Hash algorithm: SHA-512 (the only one defined in MS-SMB2 §2.2.3.1.1).
pub const HASH_SHA512: u16 = 0x0001;
}
/// MS-SMB2 §2.2.3.1.2 / §2.2.4.2 SMB2_ENCRYPTION_CAPABILITIES.
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EncryptionCapabilities {
pub cipher_count: u16,
#[br(count = cipher_count as usize)]
pub ciphers: Vec<u16>,
}
impl EncryptionCapabilities {
pub const CIPHER_AES_128_CCM: u16 = 0x0001;
pub const CIPHER_AES_128_GCM: u16 = 0x0002;
pub const CIPHER_AES_256_CCM: u16 = 0x0003;
pub const CIPHER_AES_256_GCM: u16 = 0x0004;
}
/// MS-SMB2 §2.2.3.1.7 / §2.2.4.7 SMB2_SIGNING_CAPABILITIES.
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SigningCapabilities {
pub signing_algorithm_count: u16,
#[br(count = signing_algorithm_count as usize)]
pub signing_algorithms: Vec<u16>,
}
impl SigningCapabilities {
pub const ALGORITHM_HMAC_SHA256: u16 = 0x0000;
pub const ALGORITHM_AES_CMAC: u16 = 0x0001;
pub const ALGORITHM_AES_GMAC: u16 = 0x0002;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn negotiate_request_round_trips() {
let req = NegotiateRequest {
structure_size: 36,
dialect_count: 5,
security_mode: 0x0001, // signing enabled
reserved: 0,
capabilities: 0x0000_007F,
client_guid: [0xAB; 16],
negotiate_context_offset_or_client_start_time: 0x0000_0070_0000_0000,
dialects: vec![0x0202, 0x0210, 0x0300, 0x0302, 0x0311],
};
let mut buf = Vec::new();
req.write_to(&mut buf).unwrap();
let decoded = NegotiateRequest::parse(&buf).unwrap();
assert_eq!(decoded, req);
}
#[test]
fn negotiate_response_round_trips() {
let resp = NegotiateResponse {
structure_size: 65,
security_mode: 0x0003,
dialect_revision: Dialect::Smb311.as_u16(),
negotiate_context_count_or_reserved: 3,
server_guid: [0xCD; 16],
capabilities: 0x0000_007F,
max_transact_size: 0x0010_0000,
max_read_size: 0x0010_0000,
max_write_size: 0x0010_0000,
system_time: 0x01D9_1234_5678_9ABC,
server_start_time: 0,
security_buffer_offset: 0x80,
security_buffer_length: 8,
negotiate_context_offset_or_reserved2: 0x100,
security_buffer: vec![1, 2, 3, 4, 5, 6, 7, 8],
};
let mut buf = Vec::new();
resp.write_to(&mut buf).unwrap();
let decoded = NegotiateResponse::parse(&buf).unwrap();
assert_eq!(decoded, resp);
}
#[test]
fn dialect_round_trips() {
for d in [
Dialect::Smb202,
Dialect::Smb210,
Dialect::Smb300,
Dialect::Smb302,
Dialect::Smb311,
Dialect::Smb2Wildcard,
] {
assert_eq!(Dialect::from_u16(d.as_u16()), Some(d));
}
assert_eq!(Dialect::from_u16(0xBEEF), None);
}
#[test]
fn preauth_caps_round_trips() {
let p = PreauthIntegrityCapabilities {
hash_algorithm_count: 1,
salt_length: 32,
hash_algorithms: vec![PreauthIntegrityCapabilities::HASH_SHA512],
salt: vec![0xAA; 32],
};
let mut buf = Vec::new();
let mut c = Cursor::new(&mut buf);
BinWrite::write(&p, &mut c).unwrap();
let decoded = PreauthIntegrityCapabilities::read(&mut Cursor::new(&buf)).unwrap();
assert_eq!(decoded, p);
}
#[test]
fn negotiate_context_list_round_trips() {
let list = vec![
NegotiateContext {
context_type: NegotiateContext::TYPE_PREAUTH_INTEGRITY,
data_length: 6,
reserved: 0,
data: vec![0x01, 0x00, 0x20, 0x00, 0x01, 0x00],
},
NegotiateContext {
context_type: NegotiateContext::TYPE_ENCRYPTION,
data_length: 4,
reserved: 0,
data: vec![0x02, 0x00, 0x02, 0x00],
},
NegotiateContext {
context_type: NegotiateContext::TYPE_SIGNING,
data_length: 4,
reserved: 0,
data: vec![0x01, 0x00, 0x01, 0x00],
},
];
let mut buf = Vec::new();
NegotiateContext::encode_list(&list, &mut buf).unwrap();
let parsed = NegotiateContext::parse_list(&buf, 3).unwrap();
assert_eq!(parsed, list);
}
}
+59
View File
@@ -0,0 +1,59 @@
//! OPLOCK_BREAK Notification + Acknowledgement (MS-SMB2 §2.2.23 / §2.2.24).
//!
//! V1 never grants oplocks, so we never *send* a notification, but the
//! handler exists for safety. A client may send an OPLOCK_BREAK ACK before
//! the server has cleared its oplock state in the (rare) edge case during
//! teardown.
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use super::create::FileId;
use crate::proto::error::ProtoResult;
/// SMB2_OPLOCK_BREAK_NOTIFICATION (MS-SMB2 §2.2.23.1).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OplockBreakNotification {
pub structure_size: u16,
pub oplock_level: u8,
pub reserved: u8,
pub reserved2: u32,
pub file_id: FileId,
}
impl OplockBreakNotification {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
/// SMB2_OPLOCK_BREAK_ACK (MS-SMB2 §2.2.24.1) — same wire shape as the
/// notification.
pub type OplockBreakAck = OplockBreakNotification;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips() {
let r = OplockBreakNotification {
structure_size: 24,
oplock_level: 0,
reserved: 0,
reserved2: 0,
file_id: FileId::new(1, 2),
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(OplockBreakNotification::parse(&buf).unwrap(), r);
}
}
+136
View File
@@ -0,0 +1,136 @@
//! QUERY_DIRECTORY Request/Response (MS-SMB2 §2.2.33 / §2.2.34).
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use super::create::FileId;
use crate::proto::error::ProtoResult;
/// File-info-class identifiers used in QUERY_DIRECTORY (MS-SMB2 §2.2.33
/// FileInformationClass).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum FileInfoClass {
FileDirectoryInformation = 0x01,
FileFullDirectoryInformation = 0x02,
FileBothDirectoryInformation = 0x03,
FileNamesInformation = 0x0C,
FileIdBothDirectoryInformation = 0x25,
FileIdFullDirectoryInformation = 0x26,
}
impl FileInfoClass {
pub fn from_u8(v: u8) -> Option<Self> {
Some(match v {
0x01 => Self::FileDirectoryInformation,
0x02 => Self::FileFullDirectoryInformation,
0x03 => Self::FileBothDirectoryInformation,
0x0C => Self::FileNamesInformation,
0x25 => Self::FileIdBothDirectoryInformation,
0x26 => Self::FileIdFullDirectoryInformation,
_ => return None,
})
}
}
/// SMB2_QUERY_DIRECTORY_REQUEST (MS-SMB2 §2.2.33).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueryDirectoryRequest {
pub structure_size: u16,
pub file_information_class: u8,
pub flags: u8,
pub file_index: u32,
pub file_id: FileId,
pub file_name_offset: u16,
pub file_name_length: u16,
pub output_buffer_length: u32,
/// UTF-16LE search pattern (e.g. "*").
#[br(count = file_name_length as usize)]
pub file_name: Vec<u8>,
}
impl QueryDirectoryRequest {
pub const FLAG_RESTART_SCANS: u8 = 0x01;
pub const FLAG_RETURN_SINGLE_ENTRY: u8 = 0x02;
pub const FLAG_INDEX_SPECIFIED: u8 = 0x04;
pub const FLAG_REOPEN: u8 = 0x10;
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
/// SMB2_QUERY_DIRECTORY_RESPONSE (MS-SMB2 §2.2.34).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueryDirectoryResponse {
pub structure_size: u16,
/// `OutputBufferOffset` is from the start of the SMB2 header.
pub output_buffer_offset: u16,
pub output_buffer_length: u32,
/// Variable-length info-class-specific buffer.
#[br(count = output_buffer_length as usize)]
pub buffer: Vec<u8>,
}
impl QueryDirectoryResponse {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn utf16le(s: &str) -> Vec<u8> {
s.encode_utf16().flat_map(u16::to_le_bytes).collect()
}
#[test]
fn request_round_trips() {
let pat = utf16le("*");
let r = QueryDirectoryRequest {
structure_size: 33,
file_information_class: FileInfoClass::FileIdBothDirectoryInformation as u8,
flags: QueryDirectoryRequest::FLAG_RESTART_SCANS,
file_index: 0,
file_id: FileId::new(1, 2),
file_name_offset: 0x60,
file_name_length: pat.len() as u16,
output_buffer_length: 0x10000,
file_name: pat,
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(QueryDirectoryRequest::parse(&buf).unwrap(), r);
}
#[test]
fn response_round_trips() {
let r = QueryDirectoryResponse {
structure_size: 9,
output_buffer_offset: 0x48,
output_buffer_length: 8,
buffer: vec![1, 2, 3, 4, 5, 6, 7, 8],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(QueryDirectoryResponse::parse(&buf).unwrap(), r);
}
}
+140
View File
@@ -0,0 +1,140 @@
//! QUERY_INFO Request/Response (MS-SMB2 §2.2.37 / §2.2.38).
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use super::create::FileId;
use crate::proto::error::ProtoResult;
/// `InfoType` values (MS-SMB2 §2.2.37 InfoType field).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum InfoType {
File = 0x01,
FileSystem = 0x02,
Security = 0x03,
Quota = 0x04,
}
impl InfoType {
pub fn from_u8(v: u8) -> Option<Self> {
Some(match v {
0x01 => Self::File,
0x02 => Self::FileSystem,
0x03 => Self::Security,
0x04 => Self::Quota,
_ => return None,
})
}
}
/// SMB2_QUERY_INFO_REQUEST (MS-SMB2 §2.2.37).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueryInfoRequest {
pub structure_size: u16,
pub info_type: u8,
pub file_information_class: u8,
pub output_buffer_length: u32,
pub input_buffer_offset: u16,
pub reserved: u16,
pub input_buffer_length: u32,
/// `AdditionalInformation`: which fields of the security descriptor to
/// return when `info_type == Security`. Otherwise an additional info-class
/// selector for FS info.
pub additional_information: u32,
pub flags: u32,
pub file_id: FileId,
/// Optional input buffer (used by FILE/FS info classes that need it, e.g.
/// `FileFullEaInformation` extended-attribute name lists).
#[br(count = input_buffer_length as usize)]
pub input_buffer: Vec<u8>,
}
impl QueryInfoRequest {
/// Flag: SL_RESTART_SCAN.
pub const FLAG_RESTART_SCAN: u32 = 0x0000_0001;
/// Flag: SL_RETURN_SINGLE_ENTRY.
pub const FLAG_RETURN_SINGLE_ENTRY: u32 = 0x0000_0002;
/// Flag: SL_INDEX_SPECIFIED.
pub const FLAG_INDEX_SPECIFIED: u32 = 0x0000_0004;
pub fn info_type_enum(&self) -> Option<InfoType> {
InfoType::from_u8(self.info_type)
}
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
/// SMB2_QUERY_INFO_RESPONSE (MS-SMB2 §2.2.38).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueryInfoResponse {
pub structure_size: u16,
pub output_buffer_offset: u16,
pub output_buffer_length: u32,
#[br(count = output_buffer_length as usize)]
pub buffer: Vec<u8>,
}
impl QueryInfoResponse {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_round_trips() {
let r = QueryInfoRequest {
structure_size: 41,
info_type: InfoType::File as u8,
file_information_class: 0x05, // FileStandardInformation
output_buffer_length: 0x1000,
input_buffer_offset: 0,
reserved: 0,
input_buffer_length: 0,
additional_information: 0,
flags: 0,
file_id: FileId::new(1, 2),
input_buffer: vec![],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
let decoded = QueryInfoRequest::parse(&buf).unwrap();
assert_eq!(decoded, r);
assert_eq!(decoded.info_type_enum(), Some(InfoType::File));
}
#[test]
fn response_round_trips() {
let r = QueryInfoResponse {
structure_size: 9,
output_buffer_offset: 0x48,
output_buffer_length: 4,
buffer: vec![0xAB, 0xCD, 0xEF, 0x01],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(QueryInfoResponse::parse(&buf).unwrap(), r);
}
}
+141
View File
@@ -0,0 +1,141 @@
//! READ Request/Response (MS-SMB2 §2.2.19 / §2.2.20).
//!
//! ## Data buffer offsets
//!
//! Both the READ request `ReadChannelInfoOffset` and the READ response
//! `DataOffset` are measured from the **start of the SMB2 header**, not from
//! the start of this structure (MS-SMB2 §2.2.20 explicitly: "DataOffset (1
//! byte): The offset, in bytes, from the beginning of the SMB2 header to the
//! data being read"). When constructing a response, the server crate must
//! compute `DataOffset = SMB2_HEADER_LEN + offset_within_body_of_data`.
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use super::create::FileId;
use crate::proto::error::ProtoResult;
/// SMB2_READ_REQUEST (MS-SMB2 §2.2.19).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReadRequest {
pub structure_size: u16,
pub padding: u8,
/// 3.0+ flags (`SMB2_READFLAG_*`); reserved on 2.x.
pub flags: u8,
pub length: u32,
pub offset: u64,
pub file_id: FileId,
pub minimum_count: u32,
pub channel: u32,
pub remaining_bytes: u32,
pub read_channel_info_offset: u16,
pub read_channel_info_length: u16,
/// MS-SMB2: "If ReadChannelInfoOffset and ReadChannelInfoLength are both
/// 0, the client MUST set this field to a single 0 byte." We follow that
/// — at least one byte of buffer is required on the wire.
#[br(count = if read_channel_info_length == 0 { 1 } else { read_channel_info_length as usize })]
pub buffer: Vec<u8>,
}
impl ReadRequest {
/// Flag: SMB2_READFLAG_READ_UNBUFFERED (3.0.2+).
pub const FLAG_READ_UNBUFFERED: u8 = 0x01;
/// Flag: SMB2_READFLAG_REQUEST_COMPRESSED (3.1.1+).
pub const FLAG_REQUEST_COMPRESSED: u8 = 0x02;
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
/// SMB2_READ_RESPONSE (MS-SMB2 §2.2.20).
///
/// `data_offset` is from the start of the SMB2 header. Use
/// [`ReadResponse::standard_data_offset`] for the canonical "data immediately
/// after the fixed prefix" layout.
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReadResponse {
pub structure_size: u16,
pub data_offset: u8,
pub reserved: u8,
pub data_length: u32,
pub data_remaining: u32,
/// 3.x: `Flags`. 2.x: reserved.
pub flags: u32,
#[br(count = data_length as usize)]
pub data: Vec<u8>,
}
impl ReadResponse {
/// Canonical `DataOffset` value when the data buffer immediately follows
/// the fixed 16-byte response prefix and the SMB2 header (64 + 16 = 80).
///
/// Most servers (ksmbd, Samba) emit 0x50 = 80 here.
pub const STANDARD_DATA_OFFSET: u8 = 0x50;
pub const fn standard_data_offset() -> u8 {
Self::STANDARD_DATA_OFFSET
}
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_round_trips() {
let r = ReadRequest {
structure_size: 49,
padding: 0x50,
flags: 0,
length: 0x1000,
offset: 0x2000,
file_id: FileId::new(0xAAAA, 0xBBBB),
minimum_count: 1,
channel: 0,
remaining_bytes: 0,
read_channel_info_offset: 0,
read_channel_info_length: 0,
buffer: vec![0],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(ReadRequest::parse(&buf).unwrap(), r);
}
#[test]
fn response_round_trips() {
let r = ReadResponse {
structure_size: 17,
data_offset: ReadResponse::STANDARD_DATA_OFFSET,
reserved: 0,
data_length: 5,
data_remaining: 0,
flags: 0,
data: vec![1, 2, 3, 4, 5],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(ReadResponse::parse(&buf).unwrap(), r);
}
}
+113
View File
@@ -0,0 +1,113 @@
//! SESSION_SETUP Request/Response (MS-SMB2 §2.2.5 / §2.2.6).
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use crate::proto::error::ProtoResult;
/// SMB2_SESSION_SETUP_REQUEST (MS-SMB2 §2.2.5).
///
/// `security_buffer` is opaque GSS-API/SPNEGO data — the auth agent decodes it.
/// The wire offset is from the start of the SMB2 header; we encode/decode it
/// as length-counted data immediately following the fixed prefix, which is
/// the canonical layout. Server crate may patch the offset if it needs an
/// unusual layout.
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionSetupRequest {
pub structure_size: u16,
pub flags: u8,
pub security_mode: u8,
pub capabilities: u32,
pub channel: u32,
pub security_buffer_offset: u16,
pub security_buffer_length: u16,
pub previous_session_id: u64,
#[br(count = security_buffer_length as usize)]
pub security_buffer: Vec<u8>,
}
impl SessionSetupRequest {
/// Flag: SMB2_SESSION_FLAG_BINDING — bind to existing session (3.x).
pub const FLAG_BINDING: u8 = 0x01;
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
/// SMB2_SESSION_SETUP_RESPONSE (MS-SMB2 §2.2.6).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionSetupResponse {
pub structure_size: u16,
pub session_flags: u16,
pub security_buffer_offset: u16,
pub security_buffer_length: u16,
#[br(count = security_buffer_length as usize)]
pub security_buffer: Vec<u8>,
}
impl SessionSetupResponse {
/// Session flag: IS_GUEST.
pub const FLAG_IS_GUEST: u16 = 0x0001;
/// Session flag: IS_NULL (anonymous).
pub const FLAG_IS_NULL: u16 = 0x0002;
/// Session flag: ENCRYPT_DATA.
pub const FLAG_ENCRYPT_DATA: u16 = 0x0004;
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_round_trips() {
let r = SessionSetupRequest {
structure_size: 25,
flags: 0,
security_mode: 0x01,
capabilities: 0x01,
channel: 0,
security_buffer_offset: 0x58,
security_buffer_length: 6,
previous_session_id: 0,
security_buffer: vec![0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(SessionSetupRequest::parse(&buf).unwrap(), r);
}
#[test]
fn response_round_trips() {
let r = SessionSetupResponse {
structure_size: 9,
session_flags: SessionSetupResponse::FLAG_IS_GUEST,
security_buffer_offset: 0x48,
security_buffer_length: 4,
security_buffer: vec![1, 2, 3, 4],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(SessionSetupResponse::parse(&buf).unwrap(), r);
}
}
+94
View File
@@ -0,0 +1,94 @@
//! SET_INFO Request/Response (MS-SMB2 §2.2.39 / §2.2.40).
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use super::create::FileId;
use crate::proto::error::ProtoResult;
/// SMB2_SET_INFO_REQUEST (MS-SMB2 §2.2.39).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SetInfoRequest {
pub structure_size: u16,
pub info_type: u8,
pub file_information_class: u8,
pub buffer_length: u32,
pub buffer_offset: u16,
pub reserved: u16,
pub additional_information: u32,
pub file_id: FileId,
#[br(count = buffer_length as usize)]
pub buffer: Vec<u8>,
}
impl SetInfoRequest {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
/// SMB2_SET_INFO_RESPONSE (MS-SMB2 §2.2.40).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SetInfoResponse {
pub structure_size: u16,
}
impl Default for SetInfoResponse {
fn default() -> Self {
Self { structure_size: 2 }
}
}
impl SetInfoResponse {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_round_trips() {
let r = SetInfoRequest {
structure_size: 33,
info_type: 0x01, // File
file_information_class: 0x14, // FileEndOfFileInformation
buffer_length: 8,
buffer_offset: 0x60,
reserved: 0,
additional_information: 0,
file_id: FileId::new(1, 2),
buffer: vec![0, 0, 0, 0x10, 0, 0, 0, 0],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(SetInfoRequest::parse(&buf).unwrap(), r);
}
#[test]
fn response_round_trips() {
let r = SetInfoResponse::default();
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(SetInfoResponse::parse(&buf).unwrap(), r);
assert_eq!(buf.len(), 2);
}
}
+131
View File
@@ -0,0 +1,131 @@
//! TREE_CONNECT Request/Response (MS-SMB2 §2.2.9 / §2.2.10).
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use crate::proto::error::ProtoResult;
/// SMB2_TREE_CONNECT_REQUEST (MS-SMB2 §2.2.9).
///
/// `path` is UTF-16LE. The wire format gives `PathOffset` (from the start of
/// the SMB2 header) and `PathLength`; we encode/decode the path immediately
/// following the fixed prefix. The 3.1.1 tree-connect-context machinery
/// (extension `flags`, `path_offset`/`path_length` interpretation) is
/// preserved on the wire and the server crate inspects `flags` if needed.
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TreeConnectRequest {
pub structure_size: u16,
/// 3.1.1: flags. 2.x/3.0/3.0.2: reserved.
pub flags: u16,
pub path_offset: u16,
pub path_length: u16,
/// UTF-16LE share path bytes (e.g. `\\server\share`).
#[br(count = path_length as usize)]
pub path: Vec<u8>,
}
impl TreeConnectRequest {
/// Flag: SMB2_TREE_CONNECT_FLAG_CLUSTER_RECONNECT (3.1.1).
pub const FLAG_CLUSTER_RECONNECT: u16 = 0x0001;
/// Flag: SMB2_TREE_CONNECT_FLAG_REDIRECT_TO_OWNER (3.1.1).
pub const FLAG_REDIRECT_TO_OWNER: u16 = 0x0002;
/// Flag: SMB2_TREE_CONNECT_FLAG_EXTENSION_PRESENT (3.1.1).
pub const FLAG_EXTENSION_PRESENT: u16 = 0x0004;
/// Decode the UTF-16LE share path into a `String`. Returns `None` if the
/// stored bytes are not an even length (malformed UTF-16LE).
pub fn path_str(&self) -> Option<String> {
if !self.path.len().is_multiple_of(2) {
return None;
}
let units: Vec<u16> = self
.path
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
Some(String::from_utf16_lossy(&units))
}
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
/// SMB2_TREE_CONNECT_RESPONSE (MS-SMB2 §2.2.10).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TreeConnectResponse {
pub structure_size: u16,
pub share_type: u8,
pub reserved: u8,
pub share_flags: u32,
pub capabilities: u32,
pub maximal_access: u32,
}
impl TreeConnectResponse {
/// Share type: SMB2_SHARE_TYPE_DISK.
pub const SHARE_TYPE_DISK: u8 = 0x01;
pub const SHARE_TYPE_PIPE: u8 = 0x02;
pub const SHARE_TYPE_PRINT: u8 = 0x03;
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn utf16le(s: &str) -> Vec<u8> {
s.encode_utf16().flat_map(u16::to_le_bytes).collect()
}
#[test]
fn request_round_trips() {
let path = utf16le(r"\\server\share");
let r = TreeConnectRequest {
structure_size: 9,
flags: 0,
path_offset: 0x48,
path_length: path.len() as u16,
path,
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
let decoded = TreeConnectRequest::parse(&buf).unwrap();
assert_eq!(decoded, r);
assert_eq!(decoded.path_str().unwrap(), r"\\server\share");
}
#[test]
fn response_round_trips() {
let r = TreeConnectResponse {
structure_size: 16,
share_type: TreeConnectResponse::SHARE_TYPE_DISK,
reserved: 0,
share_flags: 0,
capabilities: 0,
maximal_access: 0x001F_01FF,
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(TreeConnectResponse::parse(&buf).unwrap(), r);
}
}
+77
View File
@@ -0,0 +1,77 @@
//! TREE_DISCONNECT Request/Response (MS-SMB2 §2.2.11 / §2.2.12).
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use crate::proto::error::ProtoResult;
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TreeDisconnectRequest {
pub structure_size: u16,
pub reserved: u16,
}
impl Default for TreeDisconnectRequest {
fn default() -> Self {
Self {
structure_size: 4,
reserved: 0,
}
}
}
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TreeDisconnectResponse {
pub structure_size: u16,
pub reserved: u16,
}
impl Default for TreeDisconnectResponse {
fn default() -> Self {
Self {
structure_size: 4,
reserved: 0,
}
}
}
macro_rules! impl_codec {
($t:ty) => {
impl $t {
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(<Self as BinRead>::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
};
}
impl_codec!(TreeDisconnectRequest);
impl_codec!(TreeDisconnectResponse);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips() {
let r = TreeDisconnectRequest::default();
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(TreeDisconnectRequest::parse(&buf).unwrap(), r);
let r = TreeDisconnectResponse::default();
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(TreeDisconnectResponse::parse(&buf).unwrap(), r);
}
}
+123
View File
@@ -0,0 +1,123 @@
//! WRITE Request/Response (MS-SMB2 §2.2.21 / §2.2.22).
//!
//! ## Data buffer offsets
//!
//! `DataOffset` is from the **start of the SMB2 header**, not from the start
//! of this structure (MS-SMB2 §2.2.21). The canonical layout puts the data
//! immediately after the fixed 48-byte prefix, giving 64 + 48 = 112 = 0x70.
use binrw::{BinRead, BinWrite, binrw};
use std::io::Cursor;
use super::create::FileId;
use crate::proto::error::ProtoResult;
/// SMB2_WRITE_REQUEST (MS-SMB2 §2.2.21).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WriteRequest {
pub structure_size: u16,
pub data_offset: u16,
pub length: u32,
pub offset: u64,
pub file_id: FileId,
pub channel: u32,
pub remaining_bytes: u32,
pub write_channel_info_offset: u16,
pub write_channel_info_length: u16,
pub flags: u32,
/// MS-SMB2: at least 1 byte of payload buffer is required on the wire
/// even when length=0.
#[br(count = if length == 0 { 1 } else { length as usize })]
pub data: Vec<u8>,
}
impl WriteRequest {
/// Canonical `DataOffset` placing the data buffer immediately after the
/// fixed 48-byte WRITE prefix: 64 (SMB2 header) + 48 = 112 = 0x70.
pub const STANDARD_DATA_OFFSET: u16 = 0x70;
/// Flag: SMB2_WRITEFLAG_WRITE_THROUGH.
pub const FLAG_WRITE_THROUGH: u32 = 0x0000_0001;
/// Flag: SMB2_WRITEFLAG_WRITE_UNBUFFERED (3.0.2+).
pub const FLAG_WRITE_UNBUFFERED: u32 = 0x0000_0002;
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
/// SMB2_WRITE_RESPONSE (MS-SMB2 §2.2.22).
#[binrw]
#[brw(little)]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct WriteResponse {
pub structure_size: u16,
pub reserved: u16,
pub count: u32,
pub remaining: u32,
pub write_channel_info_offset: u16,
pub write_channel_info_length: u16,
}
impl WriteResponse {
pub fn new(count: u32) -> Self {
Self {
structure_size: 17,
reserved: 0,
count,
remaining: 0,
write_channel_info_offset: 0,
write_channel_info_length: 0,
}
}
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
Ok(Self::read(&mut Cursor::new(buf))?)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
let mut c = Cursor::new(Vec::new());
BinWrite::write(self, &mut c)?;
out.extend_from_slice(&c.into_inner());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_round_trips() {
let r = WriteRequest {
structure_size: 49,
data_offset: WriteRequest::STANDARD_DATA_OFFSET,
length: 4,
offset: 0x100,
file_id: FileId::new(0xAA, 0xBB),
channel: 0,
remaining_bytes: 0,
write_channel_info_offset: 0,
write_channel_info_length: 0,
flags: 0,
data: vec![1, 2, 3, 4],
};
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(WriteRequest::parse(&buf).unwrap(), r);
}
#[test]
fn response_round_trips() {
let r = WriteResponse::new(0x1000);
let mut buf = Vec::new();
r.write_to(&mut buf).unwrap();
assert_eq!(WriteResponse::parse(&buf).unwrap(), r);
}
}