Add DFS Referral Support (Phase 5)
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled

This commit is contained in:
Warren
2026-06-22 05:30:16 +08:00
parent 4db72fff4a
commit e9eca1b492
2 changed files with 199 additions and 0 deletions

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

View File

@@ -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;