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

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

549
vendor/smb2/src/rpc/mod.rs vendored Normal file
View File

@@ -0,0 +1,549 @@
//! Named pipe RPC (MS-RPCE / NDR) for share enumeration.
//!
//! This module encodes and decodes DCE/RPC PDUs used over SMB2 named pipes.
//! The exchange for share enumeration is:
//!
//! 1. Open `\pipe\srvsvc` via CREATE
//! 2. Send RPC BIND request (type 11)
//! 3. Receive RPC BIND_ACK response (type 12)
//! 4. Send RPC REQUEST with NetShareEnumAll (type 0, opnum 15)
//! 5. Receive RPC RESPONSE with results (type 2)
//! 6. CLOSE the pipe
//!
//! Most users don't need this module directly -- use
//! [`SmbClient::list_shares`](crate::SmbClient::list_shares) instead.
//! The [`ShareInfo`](crate::ShareInfo) type is re-exported at the crate root.
pub mod srvsvc;
use crate::error::Result;
use crate::pack::guid::Guid;
use crate::pack::{Pack, ReadCursor, WriteCursor};
use crate::Error;
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/// RPC version 5.0 (connection-oriented).
const RPC_VERSION_MAJOR: u8 = 5;
/// RPC minor version.
const RPC_VERSION_MINOR: u8 = 0;
/// Data representation: little-endian, ASCII character set, IEEE floating point.
const DATA_REP: [u8; 4] = [0x10, 0x00, 0x00, 0x00];
/// RPC PDU type: REQUEST.
const PDU_TYPE_REQUEST: u8 = 0;
/// RPC PDU type: RESPONSE.
const PDU_TYPE_RESPONSE: u8 = 2;
/// RPC PDU type: BIND.
const PDU_TYPE_BIND: u8 = 11;
/// RPC PDU type: BIND_ACK.
const PDU_TYPE_BIND_ACK: u8 = 12;
/// Default maximum transmit fragment size.
const MAX_XMIT_FRAG: u16 = 4280;
/// Default maximum receive fragment size.
const MAX_RECV_FRAG: u16 = 4280;
/// PFC flags: first fragment.
const PFC_FIRST_FRAG: u8 = 0x01;
/// PFC flags: last fragment.
const PFC_LAST_FRAG: u8 = 0x02;
/// srvsvc abstract syntax UUID: `4B324FC8-1670-01D3-1278-5A47BF6EE188`.
const SRVSVC_UUID: Guid = Guid {
data1: 0x4B324FC8,
data2: 0x1670,
data3: 0x01D3,
data4: [0x12, 0x78, 0x5A, 0x47, 0xBF, 0x6E, 0xE1, 0x88],
};
/// srvsvc abstract syntax version.
const SRVSVC_VERSION: u32 = 3;
/// NDR transfer syntax UUID: `8A885D04-1CEB-11C9-9FE8-08002B104860`.
const NDR_UUID: Guid = Guid {
data1: 0x8A885D04,
data2: 0x1CEB,
data3: 0x11C9,
data4: [0x9F, 0xE8, 0x08, 0x00, 0x2B, 0x10, 0x48, 0x60],
};
/// NDR transfer syntax version.
const NDR_VERSION: u32 = 2;
// ---------------------------------------------------------------------------
// RPC PDU common header size
// ---------------------------------------------------------------------------
/// Size of the RPC PDU common header (16 bytes).
const RPC_HEADER_SIZE: usize = 16;
// ---------------------------------------------------------------------------
// Build functions
// ---------------------------------------------------------------------------
/// Build an RPC BIND request for the srvsvc interface.
///
/// The BIND PDU negotiates the presentation context, binding the srvsvc
/// abstract syntax with the NDR transfer syntax.
pub fn build_srvsvc_bind(call_id: u32) -> Vec<u8> {
let mut w = WriteCursor::with_capacity(72);
// Common header (16 bytes) -- FragLength will be backpatched
w.write_u8(RPC_VERSION_MAJOR);
w.write_u8(RPC_VERSION_MINOR);
w.write_u8(PDU_TYPE_BIND);
w.write_u8(PFC_FIRST_FRAG | PFC_LAST_FRAG);
w.write_bytes(&DATA_REP);
let frag_len_pos = w.position();
w.write_u16_le(0); // FragLength placeholder
w.write_u16_le(0); // AuthLength
w.write_u32_le(call_id);
// BIND-specific fields
w.write_u16_le(MAX_XMIT_FRAG);
w.write_u16_le(MAX_RECV_FRAG);
w.write_u32_le(0); // AssocGroup
// Presentation context list
w.write_u8(1); // NumCtxItems
w.write_bytes(&[0, 0, 0]); // Reserved
// Context item 0
w.write_u16_le(0); // ContextId
w.write_u8(1); // NumTransferSyntaxes
w.write_u8(0); // Reserved
// Abstract syntax: srvsvc
SRVSVC_UUID.pack(&mut w);
w.write_u32_le(SRVSVC_VERSION);
// Transfer syntax: NDR
NDR_UUID.pack(&mut w);
w.write_u32_le(NDR_VERSION);
// Backpatch FragLength
let total_len = w.position();
w.set_u16_le_at(frag_len_pos, total_len as u16);
w.into_inner()
}
/// Parse an RPC BIND_ACK response.
///
/// Verifies that the server accepted the presentation context (result == 0).
/// Returns `Ok(())` on success, or an error if the bind was rejected or
/// the response is malformed.
pub fn parse_bind_ack(data: &[u8]) -> Result<()> {
let mut r = ReadCursor::new(data);
// Common header
let version = r.read_u8()?;
let version_minor = r.read_u8()?;
if version != RPC_VERSION_MAJOR || version_minor != RPC_VERSION_MINOR {
return Err(Error::invalid_data(format!(
"unexpected RPC version {version}.{version_minor}, expected 5.0"
)));
}
let ptype = r.read_u8()?;
if ptype != PDU_TYPE_BIND_ACK {
return Err(Error::invalid_data(format!(
"expected BIND_ACK (type 12), got type {ptype}"
)));
}
let _flags = r.read_u8()?;
let _data_rep = r.read_bytes(4)?;
let _frag_length = r.read_u16_le()?;
let _auth_length = r.read_u16_le()?;
let _call_id = r.read_u32_le()?;
// BIND_ACK specific fields
let _max_xmit_frag = r.read_u16_le()?;
let _max_recv_frag = r.read_u16_le()?;
let _assoc_group = r.read_u32_le()?;
// Secondary address (variable length, padded to 4 bytes)
let sec_addr_len = r.read_u16_le()?;
r.skip(sec_addr_len as usize)?;
// Align to 4 bytes after secondary address (the 2-byte length + string)
let consumed = 2 + sec_addr_len as usize;
let padding = (4 - (consumed % 4)) % 4;
r.skip(padding)?;
// Result list
let num_results = r.read_u8()?;
r.skip(3)?; // Reserved
if num_results == 0 {
return Err(Error::invalid_data("BIND_ACK has no context results"));
}
// Check first result
let result = r.read_u16_le()?;
if result != 0 {
let reason = r.read_u16_le()?;
return Err(Error::invalid_data(format!(
"BIND rejected: result={result}, reason={reason}"
)));
}
Ok(())
}
/// Build an RPC REQUEST PDU wrapping the given stub data.
///
/// The caller provides the NDR-encoded stub (the operation payload) and the
/// operation number.
pub fn build_request(call_id: u32, opnum: u16, stub_data: &[u8]) -> Vec<u8> {
let mut w = WriteCursor::with_capacity(RPC_HEADER_SIZE + 8 + stub_data.len());
// Common header
w.write_u8(RPC_VERSION_MAJOR);
w.write_u8(RPC_VERSION_MINOR);
w.write_u8(PDU_TYPE_REQUEST);
w.write_u8(PFC_FIRST_FRAG | PFC_LAST_FRAG);
w.write_bytes(&DATA_REP);
let frag_len_pos = w.position();
w.write_u16_le(0); // FragLength placeholder
w.write_u16_le(0); // AuthLength
w.write_u32_le(call_id);
// REQUEST specific fields
w.write_u32_le(stub_data.len() as u32); // AllocHint
w.write_u16_le(0); // ContextId
w.write_u16_le(opnum);
// Stub data
w.write_bytes(stub_data);
// Backpatch FragLength
let total_len = w.position();
w.set_u16_le_at(frag_len_pos, total_len as u16);
w.into_inner()
}
/// Parse a single RPC RESPONSE PDU, returning its stub data and whether it is
/// the final fragment (`PFC_LAST_FRAG` set).
///
/// DCE/RPC servers may split a large response across several fragment PDUs,
/// clearing `PFC_LAST_FRAG` on every fragment but the last (MS-RPCE 2.2.2.6).
/// Callers reassemble by concatenating each fragment's stub until `is_last` is
/// `true`. See `client::shares` for the read-and-reassemble loop.
pub fn parse_response_fragment(data: &[u8]) -> Result<(&[u8], bool)> {
let mut r = ReadCursor::new(data);
// Common header
let version = r.read_u8()?;
let version_minor = r.read_u8()?;
if version != RPC_VERSION_MAJOR || version_minor != RPC_VERSION_MINOR {
return Err(Error::invalid_data(format!(
"unexpected RPC version {version}.{version_minor}, expected 5.0"
)));
}
let ptype = r.read_u8()?;
if ptype != PDU_TYPE_RESPONSE {
return Err(Error::invalid_data(format!(
"expected RESPONSE (type 2), got type {ptype}"
)));
}
let flags = r.read_u8()?;
let _data_rep = r.read_bytes(4)?;
let frag_length = r.read_u16_le()? as usize;
let _auth_length = r.read_u16_le()?;
let _call_id = r.read_u32_le()?;
// RESPONSE specific fields
let _alloc_hint = r.read_u32_le()?;
let _context_id = r.read_u16_le()?;
let _cancel_count = r.read_u8()?;
let _reserved = r.read_u8()?;
// Stub data is the rest (up to frag_length).
let header_consumed = r.position();
if frag_length < header_consumed {
return Err(Error::invalid_data(format!(
"RPC frag_length {frag_length} shorter than header {header_consumed}"
)));
}
let stub_data = r.read_bytes(frag_length - header_consumed)?;
let is_last = flags & PFC_LAST_FRAG != 0;
Ok((stub_data, is_last))
}
/// Parse an RPC RESPONSE PDU, returning the stub data.
///
/// Validates the PDU header and extracts the embedded stub data for
/// further NDR decoding. Assumes a single, complete fragment; for fragmented
/// responses use [`parse_response_fragment`] and reassemble.
pub fn parse_response(data: &[u8]) -> Result<&[u8]> {
parse_response_fragment(data).map(|(stub, _is_last)| stub)
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
use crate::pack::Unpack;
#[test]
fn bind_request_has_correct_header() {
let pdu = build_srvsvc_bind(1);
assert_eq!(pdu[0], RPC_VERSION_MAJOR, "version major");
assert_eq!(pdu[1], RPC_VERSION_MINOR, "version minor");
assert_eq!(pdu[2], PDU_TYPE_BIND, "packet type");
assert_eq!(pdu[3], PFC_FIRST_FRAG | PFC_LAST_FRAG, "flags");
// Data representation
assert_eq!(&pdu[4..8], &DATA_REP);
// FragLength should match actual PDU length
let frag_len = u16::from_le_bytes([pdu[8], pdu[9]]);
assert_eq!(frag_len as usize, pdu.len());
// AuthLength = 0
let auth_len = u16::from_le_bytes([pdu[10], pdu[11]]);
assert_eq!(auth_len, 0);
// CallId = 1
let call_id = u32::from_le_bytes([pdu[12], pdu[13], pdu[14], pdu[15]]);
assert_eq!(call_id, 1);
}
#[test]
fn bind_request_contains_srvsvc_uuid() {
let pdu = build_srvsvc_bind(1);
// After common header (16) + MaxXmitFrag(2) + MaxRecvFrag(2) + AssocGroup(4) +
// NumCtxItems(1) + Reserved(3) + ContextId(2) + NumTransferSyntaxes(1) + Reserved(1) = 32
let uuid_offset = 32;
// Extract the abstract syntax UUID bytes
let mut cursor = ReadCursor::new(&pdu[uuid_offset..]);
let guid = Guid::unpack(&mut cursor).unwrap();
assert_eq!(guid, SRVSVC_UUID);
let version = cursor.read_u32_le().unwrap();
assert_eq!(version, SRVSVC_VERSION);
}
#[test]
fn bind_request_contains_ndr_transfer_syntax() {
let pdu = build_srvsvc_bind(1);
// Transfer syntax starts after abstract syntax (UUID=16 + version=4 = 20 bytes after uuid_offset)
let transfer_offset = 32 + 20;
let mut cursor = ReadCursor::new(&pdu[transfer_offset..]);
let guid = Guid::unpack(&mut cursor).unwrap();
assert_eq!(guid, NDR_UUID);
let version = cursor.read_u32_le().unwrap();
assert_eq!(version, NDR_VERSION);
}
#[test]
fn bind_request_total_length() {
let pdu = build_srvsvc_bind(1);
// 16 (header) + 4 (max frags) + 4 (assoc) + 4 (ctx list header) +
// 4 (ctx item header) + 20 (abstract) + 20 (transfer) = 72
assert_eq!(pdu.len(), 72);
}
#[test]
fn parse_valid_bind_ack() {
let ack = build_test_bind_ack(0); // result = 0 = accepted
assert!(parse_bind_ack(&ack).is_ok());
}
#[test]
fn parse_rejected_bind_ack() {
let ack = build_test_bind_ack(2); // result = 2 = provider_rejection
let err = parse_bind_ack(&ack).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("rejected"),
"error should mention rejection: {msg}"
);
}
#[test]
fn parse_bind_ack_wrong_version() {
let mut ack = build_test_bind_ack(0);
ack[0] = 4; // wrong version
assert!(parse_bind_ack(&ack).is_err());
}
#[test]
fn parse_bind_ack_wrong_type() {
let mut ack = build_test_bind_ack(0);
ack[2] = PDU_TYPE_BIND; // wrong type
assert!(parse_bind_ack(&ack).is_err());
}
#[test]
fn request_pdu_has_correct_opnum() {
let stub = vec![0xAA, 0xBB, 0xCC];
let pdu = build_request(1, 15, &stub);
// OpNum is at offset 22 (header=16 + AllocHint=4 + ContextId=2)
let opnum = u16::from_le_bytes([pdu[22], pdu[23]]);
assert_eq!(opnum, 15);
}
#[test]
fn request_pdu_has_correct_alloc_hint() {
let stub = vec![0xAA, 0xBB, 0xCC];
let pdu = build_request(1, 15, &stub);
let alloc_hint = u32::from_le_bytes([pdu[16], pdu[17], pdu[18], pdu[19]]);
assert_eq!(alloc_hint, 3);
}
#[test]
fn request_pdu_contains_stub_data() {
let stub = vec![0xAA, 0xBB, 0xCC];
let pdu = build_request(1, 15, &stub);
// Stub starts at offset 24 (header=16 + request fields=8)
assert_eq!(&pdu[24..], &[0xAA, 0xBB, 0xCC]);
}
#[test]
fn request_pdu_frag_length_matches() {
let stub = vec![0xAA, 0xBB, 0xCC];
let pdu = build_request(1, 15, &stub);
let frag_len = u16::from_le_bytes([pdu[8], pdu[9]]);
assert_eq!(frag_len as usize, pdu.len());
}
#[test]
fn parse_response_extracts_stub() {
let stub = b"hello stub data";
let response_pdu = build_test_response(1, stub);
let extracted = parse_response(&response_pdu).unwrap();
assert_eq!(extracted, stub);
}
#[test]
fn parse_response_wrong_version() {
let mut pdu = build_test_response(1, b"data");
pdu[0] = 4; // wrong version
assert!(parse_response(&pdu).is_err());
}
#[test]
fn parse_response_fragment_reports_last_flag() {
// build_test_response sets PFC_FIRST_FRAG | PFC_LAST_FRAG.
let pdu = build_test_response(1, b"stub");
let (stub, is_last) = parse_response_fragment(&pdu).unwrap();
assert_eq!(stub, b"stub");
assert!(is_last, "FIRST|LAST PDU should be the last fragment");
// Clear PFC_LAST_FRAG in the flags byte: now it's a non-final fragment.
let mut frag = pdu.clone();
frag[3] &= !PFC_LAST_FRAG;
let (stub, is_last) = parse_response_fragment(&frag).unwrap();
assert_eq!(stub, b"stub");
assert!(!is_last, "FIRST-only PDU should not be the last fragment");
}
#[test]
fn parse_response_rejects_frag_length_shorter_than_header() {
let mut pdu = build_test_response(1, b"data");
// FragLength lives at offset 8 (u16 LE); set it below the 24-byte header.
pdu[8] = 4;
pdu[9] = 0;
assert!(parse_response(&pdu).is_err());
}
#[test]
fn parse_response_wrong_type() {
let mut pdu = build_test_response(1, b"data");
pdu[2] = PDU_TYPE_REQUEST; // wrong type
assert!(parse_response(&pdu).is_err());
}
// -- Test helpers --
/// Build a minimal BIND_ACK for testing.
fn build_test_bind_ack(result: u16) -> Vec<u8> {
let mut w = WriteCursor::with_capacity(64);
// Common header
w.write_u8(RPC_VERSION_MAJOR);
w.write_u8(RPC_VERSION_MINOR);
w.write_u8(PDU_TYPE_BIND_ACK);
w.write_u8(PFC_FIRST_FRAG | PFC_LAST_FRAG);
w.write_bytes(&DATA_REP);
let frag_len_pos = w.position();
w.write_u16_le(0); // FragLength placeholder
w.write_u16_le(0); // AuthLength
w.write_u32_le(1); // CallId
// BIND_ACK specific
w.write_u16_le(MAX_XMIT_FRAG);
w.write_u16_le(MAX_RECV_FRAG);
w.write_u32_le(0x12345); // AssocGroup
// Secondary address: "\pipe\srvsvc\0" (empty for simplicity -- use length 0)
w.write_u16_le(0); // SecAddrLen = 0
w.write_bytes(&[0, 0]); // Padding to 4-byte alignment
// Result list
w.write_u8(1); // NumResults
w.write_bytes(&[0, 0, 0]); // Reserved
// Result entry
w.write_u16_le(result); // Result
w.write_u16_le(0); // Reason
// Transfer syntax (16 bytes UUID + 4 bytes version)
NDR_UUID.pack(&mut w);
w.write_u32_le(NDR_VERSION);
let total_len = w.position();
w.set_u16_le_at(frag_len_pos, total_len as u16);
w.into_inner()
}
/// Build a minimal RPC RESPONSE PDU wrapping the given stub data.
fn build_test_response(call_id: u32, stub: &[u8]) -> Vec<u8> {
let mut w = WriteCursor::with_capacity(RPC_HEADER_SIZE + 8 + stub.len());
w.write_u8(RPC_VERSION_MAJOR);
w.write_u8(RPC_VERSION_MINOR);
w.write_u8(PDU_TYPE_RESPONSE);
w.write_u8(PFC_FIRST_FRAG | PFC_LAST_FRAG);
w.write_bytes(&DATA_REP);
let frag_len_pos = w.position();
w.write_u16_le(0); // FragLength placeholder
w.write_u16_le(0); // AuthLength
w.write_u32_le(call_id);
// RESPONSE specific
w.write_u32_le(stub.len() as u32); // AllocHint
w.write_u16_le(0); // ContextId
w.write_u8(0); // CancelCount
w.write_u8(0); // Reserved
w.write_bytes(stub);
let total_len = w.position();
w.set_u16_le_at(frag_len_pos, total_len as u16);
w.into_inner()
}
}