use super::types::{DceRpcInterfaceUuid, NtlmAuth, SmbCommand, SmbDialect, SmbMessage};
pub const PARSER_KIND_STR: &str = "smb";
pub fn parser_kind() -> &'static str {
PARSER_KIND_STR
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParseError {
Truncated { need: usize, have: usize },
UnknownProtocol,
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Truncated { need, have } => {
write!(f, "truncated SMB header: need {need}, have {have}")
}
Self::UnknownProtocol => f.write_str("unknown SMB protocol marker"),
}
}
}
impl std::error::Error for ParseError {}
pub fn parse(payload: &[u8]) -> Result<SmbMessage, ParseError> {
let header = payload.get(..4).ok_or(ParseError::Truncated {
need: 4,
have: payload.len(),
})?;
let dialect = match header {
[0xFF, b'S', b'M', b'B'] => SmbDialect::V1,
[0xFE, b'S', b'M', b'B'] => SmbDialect::V2,
[0xFD, b'S', b'M', b'B'] => SmbDialect::EncryptedTransform,
[0xFC, b'S', b'M', b'B'] => SmbDialect::CompressedTransform,
_ => return Err(ParseError::UnknownProtocol),
};
match dialect {
SmbDialect::V1 => parse_smb1(payload),
SmbDialect::V2 => parse_smb2(payload),
SmbDialect::EncryptedTransform | SmbDialect::CompressedTransform => {
Ok(SmbMessage::new(dialect, SmbCommand::NotApplicable))
}
}
}
fn parse_smb1(payload: &[u8]) -> Result<SmbMessage, ParseError> {
let cmd = *payload.get(4).ok_or(ParseError::Truncated {
need: 5,
have: payload.len(),
})?;
Ok(SmbMessage::new(SmbDialect::V1, SmbCommand::OtherSmb1(cmd)))
}
fn parse_smb2(payload: &[u8]) -> Result<SmbMessage, ParseError> {
if payload.len() < 64 {
return Err(ParseError::Truncated {
need: 64,
have: payload.len(),
});
}
let command_lo = payload[12];
let command_hi = payload[13];
let command_raw = u16::from_le_bytes([command_lo, command_hi]);
let command = SmbCommand::from_smb2(command_raw);
let flags = u32::from_le_bytes([payload[16], payload[17], payload[18], payload[19]]);
let is_async = (flags & 0x0000_0002) != 0;
let message_id = u64::from_le_bytes([
payload[24],
payload[25],
payload[26],
payload[27],
payload[28],
payload[29],
payload[30],
payload[31],
]);
let (tree_id, session_id) = if is_async {
let sid = u64::from_le_bytes([
payload[40],
payload[41],
payload[42],
payload[43],
payload[44],
payload[45],
payload[46],
payload[47],
]);
(None, Some(sid))
} else {
let tid = u32::from_le_bytes([payload[36], payload[37], payload[38], payload[39]]);
let sid = u64::from_le_bytes([
payload[40],
payload[41],
payload[42],
payload[43],
payload[44],
payload[45],
payload[46],
payload[47],
]);
(Some(tid), Some(sid))
};
let mut msg = SmbMessage::new(SmbDialect::V2, command);
msg.message_id = Some(message_id);
msg.tree_id = tree_id;
msg.session_id = session_id;
if matches!(command, SmbCommand::TreeConnect) {
let body = &payload[64..];
if body.len() >= 8 {
let _structure_size = u16::from_le_bytes([body[0], body[1]]);
let _flags = u16::from_le_bytes([body[2], body[3]]);
let path_offset = u16::from_le_bytes([body[4], body[5]]) as usize;
let path_len = u16::from_le_bytes([body[6], body[7]]) as usize;
if path_offset >= 64
&& path_offset + path_len <= payload.len()
&& path_len > 0
&& path_len.is_multiple_of(2)
{
let path_bytes = &payload[path_offset..path_offset + path_len];
if let Some(path) = utf16le_to_string(path_bytes) {
msg.tree_connect_is_admin_share = is_admin_share(&path);
msg.tree_connect_path = Some(path);
}
}
}
}
if matches!(command, SmbCommand::Create) {
let body = &payload[64..];
if body.len() >= 56 {
let name_offset = u16::from_le_bytes([body[44], body[45]]) as usize;
let name_length = u16::from_le_bytes([body[46], body[47]]) as usize;
if name_offset >= 64
&& name_offset + name_length <= payload.len()
&& name_length.is_multiple_of(2)
{
let name_bytes = &payload[name_offset..name_offset + name_length];
if let Some(name) = utf16le_to_string(name_bytes) {
msg.create_is_admin_named_pipe = is_admin_named_pipe(&name);
msg.create_path = Some(name);
}
}
}
}
if matches!(command, SmbCommand::Read) {
let body = &payload[64..];
if body.len() >= 16 {
let length = u32::from_le_bytes([body[4], body[5], body[6], body[7]]);
let offset = u64::from_le_bytes([
body[8], body[9], body[10], body[11], body[12], body[13], body[14], body[15],
]);
msg.read_length = Some(length);
msg.read_offset = Some(offset);
}
}
if matches!(command, SmbCommand::Write) {
let body = &payload[64..];
if body.len() >= 16 {
let length = u32::from_le_bytes([body[4], body[5], body[6], body[7]]);
let offset = u64::from_le_bytes([
body[8], body[9], body[10], body[11], body[12], body[13], body[14], body[15],
]);
msg.write_length = Some(length);
msg.write_offset = Some(offset);
let data_offset = u16::from_le_bytes([body[2], body[3]]) as usize;
let len_usize = length as usize;
if data_offset >= 64 && data_offset + len_usize <= payload.len() {
let data = &payload[data_offset..data_offset + len_usize];
if let Some(uuids) = decode_dcerpc_bind(data) {
msg.dcerpc_bind_uuids = uuids;
}
}
}
}
if matches!(command, SmbCommand::SessionSetup) {
let body = &payload[64..];
if body.len() >= 24 {
let sec_off = u16::from_le_bytes([body[12], body[13]]) as usize;
let sec_len = u16::from_le_bytes([body[14], body[15]]) as usize;
if sec_off >= 64 && sec_off + sec_len <= payload.len() {
let sec_buf = &payload[sec_off..sec_off + sec_len];
if let Some(auth) = scan_ntlm_authenticate(sec_buf) {
msg.ntlm_auth = Some(auth);
}
}
}
}
Ok(msg)
}
fn scan_ntlm_authenticate(buf: &[u8]) -> Option<NtlmAuth> {
const MAGIC: &[u8; 8] = b"NTLMSSP\0";
let start =
(0..buf.len().saturating_sub(MAGIC.len())).find(|&i| &buf[i..i + MAGIC.len()] == MAGIC)?;
let msg = &buf[start..];
if msg.len() < 12 {
return None;
}
let message_type = u32::from_le_bytes([msg[8], msg[9], msg[10], msg[11]]);
if message_type != 3 {
return None;
}
if msg.len() < 64 {
return None;
}
let flags = u32::from_le_bytes([msg[60], msg[61], msg[62], msg[63]]);
let unicode = (flags & 0x0000_0001) != 0; let domain = read_security_buffer_string(msg, 28, unicode);
let username = read_security_buffer_string(msg, 36, unicode);
let workstation = read_security_buffer_string(msg, 44, unicode);
Some(NtlmAuth {
domain,
username,
workstation,
})
}
fn read_security_buffer_string(msg: &[u8], field_off: usize, unicode: bool) -> Option<String> {
let entry = msg.get(field_off..field_off + 8)?;
let len = u16::from_le_bytes([entry[0], entry[1]]) as usize;
let offset = u32::from_le_bytes([entry[4], entry[5], entry[6], entry[7]]) as usize;
if len == 0 {
return None;
}
let bytes = msg.get(offset..offset + len)?;
if unicode {
if !bytes.len().is_multiple_of(2) {
return None;
}
let units: Vec<u16> = bytes
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
String::from_utf16(&units).ok()
} else {
Some(String::from_utf8_lossy(bytes).into_owned())
}
}
fn decode_dcerpc_bind(data: &[u8]) -> Option<Vec<DceRpcInterfaceUuid>> {
if data.len() < 28 {
return None;
}
if data[0] != 0x05 || data[2] != 0x0B {
return None;
}
let n_ctx = data[24] as usize;
let mut cursor = 28;
let mut uuids = Vec::with_capacity(n_ctx);
for _ in 0..n_ctx {
if cursor + 24 > data.len() {
break;
}
let n_transfer = data[cursor + 2] as usize;
let mut bytes = [0u8; 16];
bytes.copy_from_slice(&data[cursor + 4..cursor + 20]);
uuids.push(DceRpcInterfaceUuid::from_bytes(bytes));
cursor = cursor
.saturating_add(24)
.saturating_add(n_transfer.saturating_mul(20));
}
Some(uuids)
}
fn utf16le_to_string(bytes: &[u8]) -> Option<String> {
if !bytes.len().is_multiple_of(2) {
return None;
}
let units: Vec<u16> = bytes
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
String::from_utf16(&units).ok()
}
fn is_admin_share(path: &str) -> bool {
let trailing = path.rsplit('\\').next().unwrap_or("");
matches!(
trailing.to_ascii_uppercase().as_str(),
"C$" | "ADMIN$" | "IPC$" | "NETLOGON" | "SYSVOL" | "D$" | "E$" | "F$"
)
}
fn is_admin_named_pipe(name: &str) -> bool {
let bare = name.trim_start_matches('\\');
matches!(
bare.to_ascii_lowercase().as_str(),
"svcctl" | "winreg" | "lsarpc" | "samr" | "netlogon" | "spoolss" | "atsvc" | "eventlog"
| "ntsvcs" | "wkssvc" | "srvsvc" | "drsuapi" )
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_garbage() {
assert_eq!(
parse(&[]).unwrap_err(),
ParseError::Truncated { need: 4, have: 0 }
);
assert_eq!(
parse(&[0xAA, 0xBB, 0xCC, 0xDD]).unwrap_err(),
ParseError::UnknownProtocol
);
}
#[test]
fn dialect_smb1_recognised() {
let mut frame = [0u8; 32];
frame[0..4].copy_from_slice(&[0xFF, b'S', b'M', b'B']);
frame[4] = 0x72; let msg = parse(&frame).expect("parse");
assert_eq!(msg.dialect, SmbDialect::V1);
assert_eq!(msg.command, SmbCommand::OtherSmb1(0x72));
}
#[test]
fn dialect_smb2_negotiate_recognised() {
let mut frame = [0u8; 64];
frame[0..4].copy_from_slice(&[0xFE, b'S', b'M', b'B']);
let msg = parse(&frame).expect("parse");
assert_eq!(msg.dialect, SmbDialect::V2);
assert_eq!(msg.command, SmbCommand::Negotiate);
}
#[test]
fn dialect_encrypted_transform_marks_opaque() {
let mut frame = [0u8; 52];
frame[0..4].copy_from_slice(&[0xFD, b'S', b'M', b'B']);
let msg = parse(&frame).expect("parse");
assert_eq!(msg.dialect, SmbDialect::EncryptedTransform);
assert!(msg.encrypted);
assert!(msg.dialect.is_opaque());
assert_eq!(msg.command, SmbCommand::NotApplicable);
}
#[test]
fn tree_connect_decodes_admin_share_path() {
let path = "\\\\srv\\C$";
let path_utf16: Vec<u8> = path.encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
let path_offset = 64 + 8;
let path_len = path_utf16.len();
let mut frame = vec![0u8; path_offset + path_len];
frame[0..4].copy_from_slice(&[0xFE, b'S', b'M', b'B']);
frame[12] = 0x03;
frame[13] = 0x00;
frame[64] = 9;
frame[65] = 0;
let off_bytes = (path_offset as u16).to_le_bytes();
let len_bytes = (path_len as u16).to_le_bytes();
frame[68] = off_bytes[0];
frame[69] = off_bytes[1];
frame[70] = len_bytes[0];
frame[71] = len_bytes[1];
frame[path_offset..path_offset + path_len].copy_from_slice(&path_utf16);
let msg = parse(&frame).expect("parse");
assert_eq!(msg.command, SmbCommand::TreeConnect);
assert_eq!(msg.tree_connect_path.as_deref(), Some(path));
assert!(msg.tree_connect_is_admin_share);
}
#[test]
fn is_admin_share_classifies_correctly() {
assert!(is_admin_share("\\\\srv\\C$"));
assert!(is_admin_share("\\\\srv\\ADMIN$"));
assert!(is_admin_share("\\\\srv\\IPC$"));
assert!(is_admin_share("\\\\srv\\netlogon"));
assert!(is_admin_share("\\\\srv\\SYSVOL"));
assert!(!is_admin_share("\\\\srv\\Users"));
assert!(!is_admin_share("\\\\srv\\Share1"));
}
#[test]
fn smb2_async_header_skips_tree_id() {
let mut frame = [0u8; 64];
frame[0..4].copy_from_slice(&[0xFE, b'S', b'M', b'B']);
frame[16] = 0x02;
let msg = parse(&frame).expect("parse");
assert!(msg.tree_id.is_none());
assert!(msg.session_id.is_some());
}
#[test]
fn command_slugs_stable() {
assert_eq!(SmbCommand::TreeConnect.as_str(), "tree_connect");
assert_eq!(SmbCommand::Create.as_str(), "create");
assert_eq!(SmbCommand::Other(0x99).as_str(), "other");
assert_eq!(SmbCommand::OtherSmb1(0x72).as_str(), "smb1");
}
#[test]
fn create_decodes_named_pipe_path() {
let name = "svcctl";
let name_utf16: Vec<u8> = name.encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
let name_offset = 64 + 56;
let name_length = name_utf16.len();
let mut frame = vec![0u8; name_offset + name_length];
frame[0..4].copy_from_slice(&[0xFE, b'S', b'M', b'B']);
frame[12] = 0x05;
frame[13] = 0x00;
frame[64] = 57;
frame[65] = 0;
let off_bytes = (name_offset as u16).to_le_bytes();
let len_bytes = (name_length as u16).to_le_bytes();
frame[64 + 44] = off_bytes[0];
frame[64 + 45] = off_bytes[1];
frame[64 + 46] = len_bytes[0];
frame[64 + 47] = len_bytes[1];
frame[name_offset..name_offset + name_length].copy_from_slice(&name_utf16);
let msg = parse(&frame).expect("parse");
assert_eq!(msg.command, SmbCommand::Create);
assert_eq!(msg.create_path.as_deref(), Some("svcctl"));
assert!(
msg.create_is_admin_named_pipe,
"svcctl must classify as admin named pipe"
);
}
#[test]
fn create_path_on_file_share_not_admin_pipe() {
let name = "Users\\bob\\Documents\\report.docx";
let name_utf16: Vec<u8> = name.encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
let name_offset = 64 + 56;
let name_length = name_utf16.len();
let mut frame = vec![0u8; name_offset + name_length];
frame[0..4].copy_from_slice(&[0xFE, b'S', b'M', b'B']);
frame[12] = 0x05; frame[64] = 57; let off_bytes = (name_offset as u16).to_le_bytes();
let len_bytes = (name_length as u16).to_le_bytes();
frame[64 + 44] = off_bytes[0];
frame[64 + 45] = off_bytes[1];
frame[64 + 46] = len_bytes[0];
frame[64 + 47] = len_bytes[1];
frame[name_offset..name_offset + name_length].copy_from_slice(&name_utf16);
let msg = parse(&frame).expect("parse");
assert_eq!(msg.create_path.as_deref(), Some(name));
assert!(!msg.create_is_admin_named_pipe);
}
#[test]
fn is_admin_named_pipe_classifies_correctly() {
assert!(is_admin_named_pipe("svcctl"));
assert!(is_admin_named_pipe("\\svcctl"));
assert!(is_admin_named_pipe("SVCCTL"));
assert!(is_admin_named_pipe("lsarpc"));
assert!(is_admin_named_pipe("samr"));
assert!(is_admin_named_pipe("spoolss"));
assert!(is_admin_named_pipe("drsuapi"));
assert!(!is_admin_named_pipe("notepad.exe"));
assert!(!is_admin_named_pipe("Documents\\report.docx"));
}
#[test]
fn read_surfaces_offset_and_length() {
let mut frame = vec![0u8; 64 + 16];
frame[0..4].copy_from_slice(&[0xFE, b'S', b'M', b'B']);
frame[12] = 0x08; frame[64 + 4] = 0x00;
frame[64 + 5] = 0x00;
frame[64 + 6] = 0x10;
frame[64 + 7] = 0x00;
frame[64 + 8] = 0x78;
frame[64 + 9] = 0x56;
frame[64 + 10] = 0x34;
frame[64 + 11] = 0x12;
let msg = parse(&frame).expect("parse");
assert_eq!(msg.command, SmbCommand::Read);
assert_eq!(msg.read_length, Some(0x0010_0000));
assert_eq!(msg.read_offset, Some(0x1234_5678));
}
#[test]
fn session_setup_extracts_ntlm_identity() {
let domain = "CONTOSO";
let user = "alice";
let workstation = "WS01";
let domain_u: Vec<u8> = domain
.encode_utf16()
.flat_map(|u| u.to_le_bytes())
.collect();
let user_u: Vec<u8> = user.encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
let ws_u: Vec<u8> = workstation
.encode_utf16()
.flat_map(|u| u.to_le_bytes())
.collect();
let mut blob = Vec::new();
blob.extend_from_slice(b"NTLMSSP\0");
blob.extend_from_slice(&3u32.to_le_bytes()); let header_len: u32 = 64;
let domain_off = header_len;
let user_off = domain_off + domain_u.len() as u32;
let ws_off = user_off + user_u.len() as u32;
blob.extend_from_slice(&0u16.to_le_bytes()); blob.extend_from_slice(&0u16.to_le_bytes()); blob.extend_from_slice(&0u32.to_le_bytes()); blob.extend_from_slice(&0u16.to_le_bytes());
blob.extend_from_slice(&0u16.to_le_bytes());
blob.extend_from_slice(&0u32.to_le_bytes());
blob.extend_from_slice(&(domain_u.len() as u16).to_le_bytes());
blob.extend_from_slice(&(domain_u.len() as u16).to_le_bytes());
blob.extend_from_slice(&domain_off.to_le_bytes());
blob.extend_from_slice(&(user_u.len() as u16).to_le_bytes());
blob.extend_from_slice(&(user_u.len() as u16).to_le_bytes());
blob.extend_from_slice(&user_off.to_le_bytes());
blob.extend_from_slice(&(ws_u.len() as u16).to_le_bytes());
blob.extend_from_slice(&(ws_u.len() as u16).to_le_bytes());
blob.extend_from_slice(&ws_off.to_le_bytes());
blob.extend_from_slice(&0u16.to_le_bytes());
blob.extend_from_slice(&0u16.to_le_bytes());
blob.extend_from_slice(&0u32.to_le_bytes());
blob.extend_from_slice(&0x0000_0001u32.to_le_bytes());
blob.extend_from_slice(&domain_u);
blob.extend_from_slice(&user_u);
blob.extend_from_slice(&ws_u);
let sec_offset_in_payload = 64 + 24; let blob_len = blob.len();
let mut frame = vec![0u8; sec_offset_in_payload + blob_len];
frame[0..4].copy_from_slice(&[0xFE, b'S', b'M', b'B']);
frame[12] = 0x01; frame[64] = 25;
frame[65] = 0;
let off_bytes = (sec_offset_in_payload as u16).to_le_bytes();
let len_bytes = (blob_len as u16).to_le_bytes();
frame[64 + 12] = off_bytes[0];
frame[64 + 13] = off_bytes[1];
frame[64 + 14] = len_bytes[0];
frame[64 + 15] = len_bytes[1];
frame[sec_offset_in_payload..sec_offset_in_payload + blob_len].copy_from_slice(&blob);
let msg = parse(&frame).expect("parse");
let auth = msg.ntlm_auth.expect("ntlm");
assert_eq!(auth.domain.as_deref(), Some("CONTOSO"));
assert_eq!(auth.username.as_deref(), Some("alice"));
assert_eq!(auth.workstation.as_deref(), Some("WS01"));
}
#[test]
fn session_setup_kerberos_yields_no_ntlm_auth() {
let mut frame = vec![0u8; 64 + 24 + 16];
frame[0..4].copy_from_slice(&[0xFE, b'S', b'M', b'B']);
frame[12] = 0x01;
frame[64] = 25;
let off_bytes = (64u16 + 24).to_le_bytes();
frame[64 + 12] = off_bytes[0];
frame[64 + 13] = off_bytes[1];
frame[64 + 14] = 16;
frame[64 + 15] = 0;
for b in frame.iter_mut().skip(64 + 24).take(16) {
*b = 0x42;
}
let msg = parse(&frame).expect("parse");
assert!(msg.ntlm_auth.is_none());
}
#[test]
fn write_surfaces_dcerpc_bind_uuids() {
let uuid_le: [u8; 16] = [
0x81, 0xbb, 0x7a, 0x36, 0x44, 0x98, 0xf1, 0x35, 0xad, 0x32, 0x98, 0xf0, 0x38, 0x00, 0x10, 0x03,
];
let mut pdu = vec![
5, 0, 0x0B, 0, ];
pdu.extend_from_slice(&[0x10, 0, 0, 0]); pdu.extend_from_slice(&[0, 0]); pdu.extend_from_slice(&[0, 0]); pdu.extend_from_slice(&[0, 0, 0, 0]); pdu.extend_from_slice(&[0, 0]); pdu.extend_from_slice(&[0, 0]); pdu.extend_from_slice(&[0, 0, 0, 0]); pdu.push(1); pdu.extend_from_slice(&[0, 0, 0]); pdu.extend_from_slice(&[0, 0]); pdu.push(0); pdu.push(0); pdu.extend_from_slice(&uuid_le);
pdu.extend_from_slice(&[0, 0, 0, 0]);
let data_offset_in_payload = 64 + 16; let mut frame = vec![0u8; data_offset_in_payload + pdu.len()];
frame[0..4].copy_from_slice(&[0xFE, b'S', b'M', b'B']);
frame[12] = 0x09; let off_bytes = (data_offset_in_payload as u16).to_le_bytes();
frame[64 + 2] = off_bytes[0];
frame[64 + 3] = off_bytes[1];
let len_bytes = (pdu.len() as u32).to_le_bytes();
frame[64 + 4] = len_bytes[0];
frame[64 + 5] = len_bytes[1];
frame[64 + 6] = len_bytes[2];
frame[64 + 7] = len_bytes[3];
frame[data_offset_in_payload..data_offset_in_payload + pdu.len()].copy_from_slice(&pdu);
let msg = parse(&frame).expect("parse");
assert_eq!(msg.command, SmbCommand::Write);
assert_eq!(msg.dcerpc_bind_uuids.len(), 1);
let uuid = &msg.dcerpc_bind_uuids[0];
assert_eq!(uuid.to_string(), "367abb81-9844-35f1-ad32-98f038001003");
assert_eq!(uuid.well_known_name(), Some("svcctl"));
}
#[test]
fn write_without_dcerpc_yields_empty_uuid_list() {
let mut frame = vec![0u8; 64 + 16 + 8];
frame[0..4].copy_from_slice(&[0xFE, b'S', b'M', b'B']);
frame[12] = 0x09;
let off_bytes = (64u16 + 16).to_le_bytes();
frame[64 + 2] = off_bytes[0];
frame[64 + 3] = off_bytes[1];
frame[64 + 4] = 8;
let msg = parse(&frame).expect("parse");
assert!(msg.dcerpc_bind_uuids.is_empty());
}
#[test]
fn write_surfaces_offset_and_length() {
let mut frame = vec![0u8; 64 + 16];
frame[0..4].copy_from_slice(&[0xFE, b'S', b'M', b'B']);
frame[12] = 0x09; frame[64 + 4] = 0x00;
frame[64 + 5] = 0x10;
frame[64 + 6] = 0x00;
frame[64 + 7] = 0x00;
frame[64 + 8] = 0xAA;
frame[64 + 9] = 0xBB;
frame[64 + 10] = 0xCC;
frame[64 + 11] = 0xDD;
let msg = parse(&frame).expect("parse");
assert_eq!(msg.command, SmbCommand::Write);
assert_eq!(msg.write_length, Some(0x0000_1000));
assert_eq!(msg.write_offset, Some(0xDDCC_BBAA));
}
}