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 close;
|
||||
pub mod create;
|
||||
pub mod dfs;
|
||||
pub mod echo;
|
||||
pub mod error_response;
|
||||
pub mod flush;
|
||||
|
||||
Reference in New Issue
Block a user