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

51
vendor/smb2/src/rpc/CLAUDE.md vendored Normal file
View File

@@ -0,0 +1,51 @@
# RPC -- named pipe RPC for share enumeration
DCE/RPC over SMB2 named pipes. Used to list shares on a server via the srvsvc interface.
## Key files
| File | Purpose |
|---|---|
| `mod.rs` | RPC PDU building/parsing: BIND, BIND_ACK, REQUEST, RESPONSE |
| `srvsvc.rs` | NDR encoding for `NetShareEnumAll` (opnum 15), `ShareInfo` type |
## Protocol flow
1. Tree connect to `IPC$`
2. CREATE `srvsvc` pipe (server prepends `\pipe\`)
3. WRITE: RPC BIND (call_id=1, srvsvc UUID + NDR transfer syntax)
4. READ: RPC BIND_ACK -- verify context accepted
5. WRITE: RPC REQUEST (call_id=2, opnum=15, NDR-encoded NetShareEnumAll)
6. READ: RPC RESPONSE -- NDR-decode share list
7. CLOSE pipe
8. Tree disconnect IPC$
Used by `client/shares.rs` which orchestrates the full flow via `SmbClient::list_shares()`.
## NDR encoding
`srvsvc.rs` handles NDR (Network Data Representation) encoding/decoding:
- Conformant arrays: max_count prefix, then elements
- Conformant varying strings: max_count + offset + actual_count + UTF-16LE data
- Referent pointers: non-zero pointer ID, then deferred data
- All 4-byte aligned
## Key decisions
- **call_id convention**: 1 for BIND, 2 for REQUEST. Arbitrary but consistent with smb-rs.
- **Max fragment size 4280**: Default `MAX_XMIT_FRAG` / `MAX_RECV_FRAG`. Matches common implementations.
## Response reassembly (two independent layers)
A `NetShareEnum` reply can be split two different ways, and the client handles both. They compose: a fragment loop wrapping a buffer-overflow loop.
- **DCE/RPC fragmentation (MS-RPCE 2.2.2.6)**: a large response may arrive as several RESPONSE PDUs, each its own pipe message, with `PFC_LAST_FRAG` set only on the last. `parse_response_fragment` returns `(stub, is_last)`; `client/shares.rs` loops reading PDUs and concatenating stubs until `is_last`, then NDR-decodes the joined stub via `srvsvc::parse_net_share_enum_all_stub`. `parse_response` is the single-fragment convenience wrapper (`parse_response_fragment(..).map(|(s, _)| s)`).
- **SMB pipe `STATUS_BUFFER_OVERFLOW` (MS-SMB2 3.3.5.10)**: a single pipe message larger than our 64 KiB read buffer comes back as overflow reads (partial data) terminated by a `SUCCESS` read. `client::shares::read_pipe_message` follows this, appending chunks until `SUCCESS`. The two phenomena are usually mutually exclusive in practice (fragments ≤ `MAX_RECV_FRAG` 4280 fit in one read; a server that ignores the frag cap sends one big PDU that overflows), but the code handles either or both.
## Gotchas
- **Pipe name is `srvsvc`**: The server prepends `\pipe\` automatically. Don't include it in the CREATE request.
- **Admin shares filtered out**: `list_shares` filters shares ending with `$` (IPC$, ADMIN$, C$). Only disk shares returned by default.
- **RPC version is 5.0**: Connection-oriented RPC. `PFC_FIRST_FRAG | PFC_LAST_FRAG` together mark a complete single-fragment PDU; a cleared `PFC_LAST_FRAG` means more fragments follow (see reassembly above).
- **NDR string alignment**: After each string, pad to 4-byte boundary. Missing alignment causes the server to reject the request silently.
- **Don't gate pipe reads on `SUCCESS` only**: `STATUS_BUFFER_OVERFLOW` is a warning (partial data), not a failure. Use `NtStatus::is_success_or_partial` and read again, or you truncate/error on large replies from servers that chunk them. This previously made `list_shares` fail on servers whose listing exceeded one read or one fragment.

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()
}
}

554
vendor/smb2/src/rpc/srvsvc.rs vendored Normal file
View File

@@ -0,0 +1,554 @@
//! NetShareEnumAll NDR encoding/decoding for the srvsvc interface.
//!
//! Encodes the NetrShareEnum request (opnum 15) and decodes the response,
//! extracting share names, types, and comments.
use crate::error::Result;
use crate::pack::{ReadCursor, WriteCursor};
use crate::Error;
/// Share type: disk share.
pub const STYPE_DISKTREE: u32 = 0x0000_0000;
/// Share type: printer queue.
pub const STYPE_PRINTQ: u32 = 0x0000_0001;
/// Share type: device.
pub const STYPE_DEVICE: u32 = 0x0000_0002;
/// Share type: IPC (inter-process communication).
pub const STYPE_IPC: u32 = 0x0000_0003;
/// Share type modifier: special/admin share (combined with above via OR).
pub const STYPE_SPECIAL: u32 = 0x8000_0000;
/// Mask for the base share type (low bits).
const STYPE_BASE_MASK: u32 = 0x0000_FFFF;
/// Information about a single network share.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShareInfo {
/// The share name (for example, "Documents" or "IPC$").
pub name: String,
/// The share type as a raw u32 (see `STYPE_*` constants).
pub share_type: u32,
/// An optional comment/description for the share.
pub comment: String,
}
/// Build the NDR-encoded stub data for a NetShareEnumAll request.
///
/// The stub is meant to be wrapped in an RPC REQUEST PDU with opnum 15.
pub fn build_net_share_enum_all_stub(server_name: &str) -> Vec<u8> {
let mut w = WriteCursor::with_capacity(128);
// ServerName: NDR unique pointer to conformant+varying string (UTF-16LE, null-terminated)
// Referent ID (non-null pointer)
w.write_u32_le(0x0002_0000); // referent ID
// Encode the server name as a conformant+varying NDR string
let name_utf16: Vec<u16> = server_name
.encode_utf16()
.chain(std::iter::once(0))
.collect();
let char_count = name_utf16.len() as u32;
// MaxCount
w.write_u32_le(char_count);
// Offset
w.write_u32_le(0);
// ActualCount
w.write_u32_le(char_count);
// String data (UTF-16LE)
for &code_unit in &name_utf16 {
w.write_u16_le(code_unit);
}
// Align to 4 bytes after string data
w.align_to(4);
// InfoStruct: SHARE_ENUM_STRUCT
// Level = 1 (we want SHARE_INFO_1)
w.write_u32_le(1);
// ShareInfo union discriminant = 1 (matches level)
w.write_u32_le(1);
// Pointer to SHARE_INFO_1_CONTAINER (unique pointer)
w.write_u32_le(0x0002_0004); // referent ID
// SHARE_INFO_1_CONTAINER (deferred pointer data)
// EntriesRead = 0 (server fills this)
w.write_u32_le(0);
// Buffer pointer = NULL (let server allocate)
w.write_u32_le(0);
// PreferedMaximumLength = 0xFFFFFFFF (no limit)
w.write_u32_le(0xFFFF_FFFF);
// ResumeHandle: unique pointer to u32
// NULL pointer (no resume)
w.write_u32_le(0);
w.into_inner()
}
/// Build a complete RPC REQUEST PDU for NetShareEnumAll.
///
/// Combines the RPC REQUEST header (opnum 15) with the NDR stub data.
pub fn build_net_share_enum_all(call_id: u32, server_name: &str) -> Vec<u8> {
let stub = build_net_share_enum_all_stub(server_name);
super::build_request(call_id, 15, &stub)
}
/// Parse the NDR stub data from a NetShareEnumAll RPC RESPONSE.
///
/// Extracts all share entries from the response. The caller should use
/// [`filter_disk_shares`] to get only disk shares.
pub fn parse_net_share_enum_all_response(data: &[u8]) -> Result<Vec<ShareInfo>> {
// First, parse the RPC RESPONSE envelope to get the stub data
let stub = super::parse_response(data)?;
parse_net_share_enum_all_stub(stub)
}
/// Parse the NDR stub data directly (without the RPC envelope).
///
/// Used by the share-enumeration reassembly path, which concatenates the stub
/// of each RPC fragment before decoding.
pub(crate) fn parse_net_share_enum_all_stub(stub: &[u8]) -> Result<Vec<ShareInfo>> {
let mut r = ReadCursor::new(stub);
// Level (u32) -- should be 1
let level = r.read_u32_le()?;
if level != 1 {
return Err(Error::invalid_data(format!(
"expected share info level 1, got {level}"
)));
}
// Union discriminant (u32) -- should be 1
let discriminant = r.read_u32_le()?;
if discriminant != 1 {
return Err(Error::invalid_data(format!(
"expected union discriminant 1, got {discriminant}"
)));
}
// Pointer to SHARE_INFO_1_CONTAINER
let container_ptr = r.read_u32_le()?;
if container_ptr == 0 {
return Ok(Vec::new());
}
// SHARE_INFO_1_CONTAINER
let count = r.read_u32_le()?;
// Pointer to array of SHARE_INFO_1
let array_ptr = r.read_u32_le()?;
if array_ptr == 0 || count == 0 {
return Ok(Vec::new());
}
// Array: MaxCount header
let max_count = r.read_u32_le()?;
if max_count < count {
return Err(Error::invalid_data(format!(
"array max_count ({max_count}) < entries ({count})"
)));
}
// Read the fixed-size parts of each SHARE_INFO_1 entry:
// Each entry has: name_ptr (u32), type (u32), comment_ptr (u32)
struct RawEntry {
name_ptr: u32,
share_type: u32,
comment_ptr: u32,
}
let mut entries = Vec::with_capacity(count as usize);
for _ in 0..count {
let name_ptr = r.read_u32_le()?;
let share_type = r.read_u32_le()?;
let comment_ptr = r.read_u32_le()?;
entries.push(RawEntry {
name_ptr,
share_type,
comment_ptr,
});
}
// Now read the deferred pointer data (conformant+varying strings)
let mut shares = Vec::with_capacity(count as usize);
for entry in &entries {
let name = if entry.name_ptr != 0 {
read_ndr_string(&mut r)?
} else {
String::new()
};
let comment = if entry.comment_ptr != 0 {
read_ndr_string(&mut r)?
} else {
String::new()
};
shares.push(ShareInfo {
name,
share_type: entry.share_type,
comment,
});
}
Ok(shares)
}
/// Read an NDR conformant+varying UTF-16LE string from the cursor.
///
/// Format: MaxCount(u32) + Offset(u32) + ActualCount(u32) + UTF-16LE data.
/// The string is null-terminated on the wire; we strip the null.
fn read_ndr_string(r: &mut ReadCursor<'_>) -> Result<String> {
let _max_count = r.read_u32_le()?;
let _offset = r.read_u32_le()?;
let actual_count = r.read_u32_le()?;
if actual_count == 0 {
return Ok(String::new());
}
let byte_len = actual_count as usize * 2;
let s = r.read_utf16_le(byte_len)?;
// Align to 4 bytes after reading string data
let pos = r.position();
let padding = (4 - (pos % 4)) % 4;
if padding > 0 && r.remaining() >= padding {
r.skip(padding)?;
}
// Strip trailing null
Ok(s.trim_end_matches('\0').to_string())
}
/// Filter shares, keeping only disk shares and excluding admin shares (ending with `$`).
pub fn filter_disk_shares(shares: Vec<ShareInfo>) -> Vec<ShareInfo> {
shares
.into_iter()
.filter(|s| {
let base_type = s.share_type & STYPE_BASE_MASK;
let is_disk = base_type == STYPE_DISKTREE;
let is_special = (s.share_type & STYPE_SPECIAL) != 0;
let ends_with_dollar = s.name.ends_with('$');
is_disk && !is_special && !ends_with_dollar
})
.collect()
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_request_has_opnum_15() {
let pdu = build_net_share_enum_all(1, r"\\server");
// OpNum is at offset 22 in the RPC REQUEST PDU
let opnum = u16::from_le_bytes([pdu[22], pdu[23]]);
assert_eq!(opnum, 15);
}
#[test]
fn build_request_stub_contains_server_name() {
let stub = build_net_share_enum_all_stub(r"\\server");
// The server name should appear as UTF-16LE somewhere in the stub
let expected_utf16: Vec<u8> = r"\\server"
.encode_utf16()
.flat_map(|c| c.to_le_bytes())
.collect();
let found = stub
.windows(expected_utf16.len())
.any(|window| window == expected_utf16.as_slice());
assert!(found, "stub should contain the server name in UTF-16LE");
}
#[test]
fn parse_response_with_three_shares() {
let response_pdu = build_test_enum_response(&[
("Documents", STYPE_DISKTREE, "Shared docs"),
("IPC$", STYPE_IPC | STYPE_SPECIAL, "Remote IPC"),
("C$", STYPE_DISKTREE | STYPE_SPECIAL, "Default share"),
]);
let shares = parse_net_share_enum_all_response(&response_pdu).unwrap();
assert_eq!(shares.len(), 3);
assert_eq!(shares[0].name, "Documents");
assert_eq!(shares[0].share_type, STYPE_DISKTREE);
assert_eq!(shares[0].comment, "Shared docs");
assert_eq!(shares[1].name, "IPC$");
assert_eq!(shares[2].name, "C$");
}
#[test]
fn filter_keeps_disk_shares() {
let shares = vec![
ShareInfo {
name: "Documents".to_string(),
share_type: STYPE_DISKTREE,
comment: "Shared docs".to_string(),
},
ShareInfo {
name: "Photos".to_string(),
share_type: STYPE_DISKTREE,
comment: String::new(),
},
];
let filtered = filter_disk_shares(shares);
assert_eq!(filtered.len(), 2);
}
#[test]
fn filter_removes_ipc() {
let shares = vec![ShareInfo {
name: "IPC$".to_string(),
share_type: STYPE_IPC | STYPE_SPECIAL,
comment: "Remote IPC".to_string(),
}];
let filtered = filter_disk_shares(shares);
assert!(filtered.is_empty());
}
#[test]
fn filter_removes_admin_shares() {
let shares = vec![
ShareInfo {
name: "C$".to_string(),
share_type: STYPE_DISKTREE | STYPE_SPECIAL,
comment: "Default share".to_string(),
},
ShareInfo {
name: "ADMIN$".to_string(),
share_type: STYPE_DISKTREE | STYPE_SPECIAL,
comment: "Remote Admin".to_string(),
},
];
let filtered = filter_disk_shares(shares);
assert!(filtered.is_empty());
}
#[test]
fn filter_mixed_shares() {
let shares = vec![
ShareInfo {
name: "Documents".to_string(),
share_type: STYPE_DISKTREE,
comment: "Shared docs".to_string(),
},
ShareInfo {
name: "IPC$".to_string(),
share_type: STYPE_IPC | STYPE_SPECIAL,
comment: "Remote IPC".to_string(),
},
ShareInfo {
name: "C$".to_string(),
share_type: STYPE_DISKTREE | STYPE_SPECIAL,
comment: "Default share".to_string(),
},
ShareInfo {
name: "Photos".to_string(),
share_type: STYPE_DISKTREE,
comment: String::new(),
},
ShareInfo {
name: "Printer".to_string(),
share_type: STYPE_PRINTQ,
comment: "Office printer".to_string(),
},
];
let filtered = filter_disk_shares(shares);
assert_eq!(filtered.len(), 2);
assert_eq!(filtered[0].name, "Documents");
assert_eq!(filtered[1].name, "Photos");
}
#[test]
fn parse_empty_share_list() {
let response_pdu = build_test_enum_response(&[]);
let shares = parse_net_share_enum_all_response(&response_pdu).unwrap();
assert!(shares.is_empty());
}
#[test]
fn parse_share_with_unicode_name() {
let response_pdu = build_test_enum_response(&[(
"\u{00C4}rchive",
STYPE_DISKTREE,
"Archiv f\u{00FC}r Dateien",
)]);
let shares = parse_net_share_enum_all_response(&response_pdu).unwrap();
assert_eq!(shares.len(), 1);
assert_eq!(shares[0].name, "\u{00C4}rchive");
assert_eq!(shares[0].comment, "Archiv f\u{00FC}r Dateien");
}
#[test]
fn parse_share_with_cjk_characters() {
let response_pdu = build_test_enum_response(&[(
"\u{5171}\u{6709}",
STYPE_DISKTREE,
"\u{5171}\u{6709}\u{30D5}\u{30A9}\u{30EB}\u{30C0}",
)]);
let shares = parse_net_share_enum_all_response(&response_pdu).unwrap();
assert_eq!(shares.len(), 1);
assert_eq!(shares[0].name, "\u{5171}\u{6709}");
assert_eq!(
shares[0].comment,
"\u{5171}\u{6709}\u{30D5}\u{30A9}\u{30EB}\u{30C0}"
);
}
#[test]
fn roundtrip_build_and_parse() {
// Build a request, then manually construct a response and parse it
let _request = build_net_share_enum_all(1, r"\\testserver");
let response_pdu = build_test_enum_response(&[
("Share1", STYPE_DISKTREE, "First share"),
("Share2", STYPE_DISKTREE, "Second share"),
]);
let shares = parse_net_share_enum_all_response(&response_pdu).unwrap();
assert_eq!(shares.len(), 2);
assert_eq!(shares[0].name, "Share1");
assert_eq!(shares[0].comment, "First share");
assert_eq!(shares[1].name, "Share2");
assert_eq!(shares[1].comment, "Second share");
}
#[test]
fn filter_preserves_non_dollar_disk_shares_only() {
// A share named "My$hare" (dollar in middle) should be kept
let shares = vec![ShareInfo {
name: "My$hare".to_string(),
share_type: STYPE_DISKTREE,
comment: String::new(),
}];
let filtered = filter_disk_shares(shares);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].name, "My$hare");
}
// -- Test helpers --
/// Write an NDR conformant+varying UTF-16LE string into the cursor.
fn write_ndr_string(w: &mut WriteCursor, s: &str) {
let utf16: Vec<u16> = s.encode_utf16().chain(std::iter::once(0)).collect();
let char_count = utf16.len() as u32;
w.write_u32_le(char_count); // MaxCount
w.write_u32_le(0); // Offset
w.write_u32_le(char_count); // ActualCount
for &code_unit in &utf16 {
w.write_u16_le(code_unit);
}
w.align_to(4);
}
/// Build a complete RPC RESPONSE PDU containing the given shares.
///
/// This constructs valid NDR stub data wrapped in an RPC RESPONSE envelope.
fn build_test_enum_response(shares: &[(&str, u32, &str)]) -> Vec<u8> {
let stub = build_test_enum_stub(shares);
build_test_response_pdu(1, &stub)
}
/// Build NDR stub data for a NetShareEnumAll response.
fn build_test_enum_stub(shares: &[(&str, u32, &str)]) -> Vec<u8> {
let mut w = WriteCursor::with_capacity(512);
let count = shares.len() as u32;
// Level = 1
w.write_u32_le(1);
// Union discriminant = 1
w.write_u32_le(1);
if count == 0 {
// Null container pointer
w.write_u32_le(0);
// TotalEntries
w.write_u32_le(0);
// ResumeHandle pointer (null)
w.write_u32_le(0);
// Return value (Windows error code, 0 = success)
w.write_u32_le(0);
return w.into_inner();
}
// Container pointer (non-null)
w.write_u32_le(0x0002_0000);
// SHARE_INFO_1_CONTAINER
w.write_u32_le(count); // EntriesRead
w.write_u32_le(0x0002_0004); // Array pointer (non-null)
// Array: MaxCount
w.write_u32_le(count);
// Fixed-size entries: name_ptr, type, comment_ptr
for (i, &(_, share_type, _)) in shares.iter().enumerate() {
w.write_u32_le(0x0002_0008 + (i as u32) * 2); // name referent ID
w.write_u32_le(share_type);
w.write_u32_le(0x0002_0108 + (i as u32) * 2); // comment referent ID
}
// Deferred string data (name then comment for each entry)
for &(name, _, comment) in shares {
write_ndr_string(&mut w, name);
write_ndr_string(&mut w, comment);
}
// TotalEntries
w.write_u32_le(count);
// ResumeHandle pointer (null)
w.write_u32_le(0);
// Return value (0 = success)
w.write_u32_le(0);
w.into_inner()
}
/// Build a minimal RPC RESPONSE PDU wrapping stub data.
fn build_test_response_pdu(call_id: u32, stub: &[u8]) -> Vec<u8> {
use crate::pack::WriteCursor;
let mut w = WriteCursor::with_capacity(24 + stub.len());
// Common header
w.write_u8(5); // Version
w.write_u8(0); // VersionMinor
w.write_u8(2); // PacketType = RESPONSE
w.write_u8(0x03); // Flags (first + last)
w.write_bytes(&[0x10, 0x00, 0x00, 0x00]); // DataRep
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()
}
}