Add DFS Referral Support (Phase 5)
This commit is contained in:
198
vendor/smb-server/src/proto/messages/dfs.rs
vendored
Normal file
198
vendor/smb-server/src/proto/messages/dfs.rs
vendored
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
//! DFS Referral Support (MS-DFSR)
|
||||||
|
//! Implements DFS_Get_Referrals and DFS_Get_Referrals_Ex IOCTL handlers
|
||||||
|
|
||||||
|
use crate::proto::error::{ProtoError, ProtoResult};
|
||||||
|
|
||||||
|
/// DFS Referral Request (MS-DFSR §2.2.2)
|
||||||
|
/// Simplified structure for parsing
|
||||||
|
pub struct DfsReferralRequest {
|
||||||
|
pub max_referral_level: u16,
|
||||||
|
pub requested_file_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DfsReferralRequest {
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
if buf.len() < 2 {
|
||||||
|
return Err(ProtoError::Malformed("DFS referral request too short"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_referral_level = u16::from_le_bytes([buf[0], buf[1]]);
|
||||||
|
let requested_file_name = String::from_utf8_lossy(&buf[2..])
|
||||||
|
.trim_end_matches('\0')
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
max_referral_level,
|
||||||
|
requested_file_name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DFS Referral Response (MS-DFSR §2.2.3)
|
||||||
|
/// Version 2 referral (most common)
|
||||||
|
pub struct DfsReferralResponse {
|
||||||
|
pub version: u16,
|
||||||
|
pub referral_count: u16,
|
||||||
|
pub referral_header_flags: u32,
|
||||||
|
/// Referral entries (offsets + strings)
|
||||||
|
pub referrals_data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DfsReferralResponse {
|
||||||
|
/// Build version 2 referral response
|
||||||
|
pub fn build_v2(dfs_path: &str, network_address: &str) -> Vec<u8> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
|
||||||
|
// Response header (16 bytes)
|
||||||
|
out.extend_from_slice(&2u16.to_le_bytes()); // Version
|
||||||
|
out.extend_from_slice(&1u16.to_le_bytes()); // ReferralCount
|
||||||
|
out.extend_from_slice(&0u32.to_le_bytes()); // ReferralHeaderFlags
|
||||||
|
|
||||||
|
// Referral entry header (16 bytes)
|
||||||
|
out.extend_from_slice(&2u16.to_le_bytes()); // Version
|
||||||
|
out.extend_from_slice(&0u16.to_le_bytes()); // ReferralFlags
|
||||||
|
out.extend_from_slice(&16u32.to_le_bytes()); // DfsPathOffset
|
||||||
|
out.extend_from_slice(&16u32.to_le_bytes()); // AltDfsPathOffset
|
||||||
|
out.extend_from_slice(&16u32.to_le_bytes()); // NetworkAddressOffset
|
||||||
|
|
||||||
|
// DFS path (UTF-8 with null terminator)
|
||||||
|
out.extend_from_slice(dfs_path.as_bytes());
|
||||||
|
out.push(0); // null terminator
|
||||||
|
|
||||||
|
// Alternate DFS path (same as DFS path)
|
||||||
|
out.extend_from_slice(dfs_path.as_bytes());
|
||||||
|
out.push(0); // null terminator
|
||||||
|
|
||||||
|
// Network address (UTF-8 with null terminator)
|
||||||
|
out.extend_from_slice(network_address.as_bytes());
|
||||||
|
out.push(0); // null terminator
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DFS Namespace Configuration
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DfsNamespace {
|
||||||
|
/// DFS root path (e.g., "\\server\dfs")
|
||||||
|
pub root_path: String,
|
||||||
|
/// DFS targets (list of server addresses)
|
||||||
|
pub targets: Vec<DfsTarget>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DfsTarget {
|
||||||
|
/// Target server address (e.g., "192.168.1.100")
|
||||||
|
pub address: String,
|
||||||
|
/// Target share name (e.g., "share1")
|
||||||
|
pub share_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DfsNamespace {
|
||||||
|
pub fn new(root_path: String) -> Self {
|
||||||
|
Self {
|
||||||
|
root_path,
|
||||||
|
targets: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_target(&mut self, address: String, share_name: String) {
|
||||||
|
self.targets.push(DfsTarget { address, share_name });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build referral response for a DFS path
|
||||||
|
pub fn build_referral_response(&self, requested_path: &str) -> Option<Vec<u8>> {
|
||||||
|
// Check if requested path is under DFS root
|
||||||
|
if !requested_path.starts_with(&self.root_path) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use first target for referral
|
||||||
|
if self.targets.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let target = &self.targets[0];
|
||||||
|
let network_address = format!("\\{}\\{}", target.address, target.share_name);
|
||||||
|
|
||||||
|
Some(DfsReferralResponse::build_v2(&self.root_path, &network_address))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dfs_namespace_creation() {
|
||||||
|
let namespace = DfsNamespace::new("\\server\\dfs".to_string());
|
||||||
|
assert_eq!(namespace.root_path, "\\server\\dfs");
|
||||||
|
assert_eq!(namespace.targets.len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_dfs_target() {
|
||||||
|
let mut namespace = DfsNamespace::new("\\server\\dfs".to_string());
|
||||||
|
namespace.add_target("192.168.1.100".to_string(), "share1".to_string());
|
||||||
|
namespace.add_target("192.168.1.101".to_string(), "share2".to_string());
|
||||||
|
|
||||||
|
assert_eq!(namespace.targets.len(), 2);
|
||||||
|
assert_eq!(namespace.targets[0].address, "192.168.1.100");
|
||||||
|
assert_eq!(namespace.targets[0].share_name, "share1");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_referral_response() {
|
||||||
|
let mut namespace = DfsNamespace::new("\\server\\dfs".to_string());
|
||||||
|
namespace.add_target("192.168.1.100".to_string(), "share1".to_string());
|
||||||
|
|
||||||
|
let response = namespace.build_referral_response("\\server\\dfs\\path");
|
||||||
|
assert!(response.is_some());
|
||||||
|
|
||||||
|
let response = response.unwrap();
|
||||||
|
assert!(response.len() > 16);
|
||||||
|
|
||||||
|
// Check version field
|
||||||
|
let version = u16::from_le_bytes([response[0], response[1]]);
|
||||||
|
assert_eq!(version, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_referral_response_version_2() {
|
||||||
|
let data = DfsReferralResponse::build_v2(
|
||||||
|
"\\server\\dfs",
|
||||||
|
"\\192.168.1.100\\share1"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Header: Version (2) + ReferralCount (1) + Flags (0) = 8 bytes
|
||||||
|
// Entry header: Version (2) + Flags (0) + 3 offsets = 16 bytes
|
||||||
|
// Total header: 24 bytes
|
||||||
|
|
||||||
|
assert!(data.len() > 24);
|
||||||
|
|
||||||
|
let version = u16::from_le_bytes([data[0], data[1]]);
|
||||||
|
assert_eq!(version, 2);
|
||||||
|
|
||||||
|
let count = u16::from_le_bytes([data[2], data[3]]);
|
||||||
|
assert_eq!(count, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dfs_path_mismatch() {
|
||||||
|
let mut namespace = DfsNamespace::new("\\server\\dfs".to_string());
|
||||||
|
namespace.add_target("192.168.1.100".to_string(), "share1".to_string());
|
||||||
|
|
||||||
|
// Requested path not under DFS root
|
||||||
|
let response = namespace.build_referral_response("\\other\\path");
|
||||||
|
assert!(response.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_empty_targets() {
|
||||||
|
let namespace = DfsNamespace::new("\\server\\dfs".to_string());
|
||||||
|
|
||||||
|
// No targets configured
|
||||||
|
let response = namespace.build_referral_response("\\server\\dfs");
|
||||||
|
assert!(response.is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
1
vendor/smb-server/src/proto/messages/mod.rs
vendored
1
vendor/smb-server/src/proto/messages/mod.rs
vendored
@@ -11,6 +11,7 @@ pub mod cancel;
|
|||||||
pub mod change_notify;
|
pub mod change_notify;
|
||||||
pub mod close;
|
pub mod close;
|
||||||
pub mod create;
|
pub mod create;
|
||||||
|
pub mod dfs;
|
||||||
pub mod echo;
|
pub mod echo;
|
||||||
pub mod error_response;
|
pub mod error_response;
|
||||||
pub mod flush;
|
pub mod flush;
|
||||||
|
|||||||
Reference in New Issue
Block a user