diff --git a/vendor/smb-server/src/client_restrictions.rs b/vendor/smb-server/src/client_restrictions.rs new file mode 100644 index 0000000..f2facad --- /dev/null +++ b/vendor/smb-server/src/client_restrictions.rs @@ -0,0 +1,498 @@ +//! SMB Client Restrictions (Access Control) +//! +//! Restricts SMB client access based on IP address, username, time, etc. +//! Based on Windows SMB Server access control mechanisms. + +use std::collections::{HashMap, HashSet}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::sync::{Arc, RwLock}; +use std::time::{SystemTime, UNIX_EPOCH}; + +/// Client restriction action +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ClientAction { + Allow, + Deny, +} + +/// IP address range specification +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum IpSpec { + /// Single IP address + Single(IpAddr), + /// CIDR range (address, prefix length) + Cidr(IpAddr, u8), + /// IP range (start, end) + Range(IpAddr, IpAddr), + /// Any IP (wildcard) + Any, +} + +impl IpSpec { + /// Check if an IP address matches this specification + pub fn matches(&self, ip: &IpAddr) -> bool { + match self { + IpSpec::Single(addr) => addr == ip, + IpSpec::Cidr(base, prefix) => { + // Check if ip is in CIDR range + match (base, ip) { + (IpAddr::V4(base_v4), IpAddr::V4(ip_v4)) => { + let mask = if *prefix >= 32 { + 0xFFFFFFFFu32 + } else { + 0xFFFFFFFFu32 << (32 - *prefix) + }; + let base_int = u32::from_be_bytes(base_v4.octets()); + let ip_int = u32::from_be_bytes(ip_v4.octets()); + (base_int & mask) == (ip_int & mask) + } + (IpAddr::V6(base_v6), IpAddr::V6(ip_v6)) => { + let prefix = *prefix; + if prefix == 0 { + return true; + } + if prefix > 128 { + return false; + } + let base_bytes = base_v6.octets(); + let ip_bytes = ip_v6.octets(); + + // Check prefix bits + let full_bytes = prefix / 8; + let remaining_bits = prefix % 8; + + // Check full bytes + for i in 0..full_bytes as usize { + if base_bytes[i] != ip_bytes[i] { + return false; + } + } + + // Check remaining bits in the next byte + if remaining_bits > 0 { + let idx = full_bytes as usize; + let mask = 0xFF << (8 - remaining_bits); + if (base_bytes[idx] & mask) != (ip_bytes[idx] & mask) { + return false; + } + } + + true + } + _ => false, // IPv4 vs IPv6 mismatch + } + } + IpSpec::Range(start, end) => { + // Check if ip is in range (simple comparison) + match (start, end, ip) { + (IpAddr::V4(start_v4), IpAddr::V4(end_v4), IpAddr::V4(ip_v4)) => { + let start_int = u32::from_be_bytes(start_v4.octets()); + let end_int = u32::from_be_bytes(end_v4.octets()); + let ip_int = u32::from_be_bytes(ip_v4.octets()); + ip_int >= start_int && ip_int <= end_int + } + (IpAddr::V6(start_v6), IpAddr::V6(end_v6), IpAddr::V6(ip_v6)) => { + // IPv6 range comparison (byte-by-byte) + let start_bytes = start_v6.octets(); + let end_bytes = end_v6.octets(); + let ip_bytes = ip_v6.octets(); + + let mut in_range = true; + for i in 0..16 { + if ip_bytes[i] < start_bytes[i] || ip_bytes[i] > end_bytes[i] { + in_range = false; + break; + } + } + in_range + } + _ => false, + } + } + IpSpec::Any => true, + } + } +} + +/// Time-based restriction +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TimeSpec { + /// Always allowed + Always, + /// Time range (start_hour, end_hour) in 24-hour format + HourRange(u8, u8), + /// Day of week restriction (0=Sunday, 6=Saturday) + DayOfWeek(HashSet), + /// Combined: DayOfWeek + HourRange + DayHour(HashSet, u8, u8), +} + +impl TimeSpec { + /// Check if current time matches this specification + pub fn matches(&self) -> bool { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or(std::time::Duration::ZERO); + + let total_secs = now.as_secs(); + let hours = ((total_secs / 3600) % 24) as u8; + let days_since_epoch = total_secs / 86400; + let day_of_week = ((days_since_epoch + 4) % 7) as u8; // 1970-01-01 was Thursday (day 4) + + match self { + TimeSpec::Always => true, + TimeSpec::HourRange(start, end) => { + if start <= end { + hours >= *start && hours <= *end + } else { + // Overnight range (e.g., 22:00 - 06:00) + hours >= *start || hours <= *end + } + } + TimeSpec::DayOfWeek(days) => days.contains(&day_of_week), + TimeSpec::DayHour(days, start_hour, end_hour) => { + if !days.contains(&day_of_week) { + return false; + } + if start_hour <= end_hour { + hours >= *start_hour && hours <= *end_hour + } else { + hours >= *start_hour || hours <= *end_hour + } + } + } + } +} + +/// SMB client restriction rule +#[derive(Debug, Clone)] +pub struct ClientRule { + /// Action: Allow or Deny + pub action: ClientAction, + /// IP address specification + pub ip_spec: IpSpec, + /// User name restriction (None = any user) + pub user: Option, + /// Time-based restriction + pub time: TimeSpec, + /// Share name restriction (None = any share) + pub share: Option, + /// Priority (higher = more specific) + pub priority: u32, +} + +impl ClientRule { + /// Create a new client rule + pub fn new( + action: ClientAction, + ip_spec: IpSpec, + user: Option, + time: TimeSpec, + share: Option, + ) -> Self { + // Calculate priority based on specificity + let ip_priority = match &ip_spec { + IpSpec::Single(_) => 100, + IpSpec::Cidr(_, prefix) => 50 + *prefix as u32, + IpSpec::Range(_, _) => 60, + IpSpec::Any => 10, + }; + + let user_priority = if user.is_some() { 50 } else { 0 }; + let time_priority = match &time { + TimeSpec::Always => 0, + TimeSpec::HourRange(_, _) => 20, + TimeSpec::DayOfWeek(_) => 30, + TimeSpec::DayHour(_, _, _) => 40, + }; + let share_priority = if share.is_some() { 30 } else { 0 }; + + Self { + action, + ip_spec, + user, + time, + share, + priority: ip_priority + user_priority + time_priority + share_priority, + } + } + + /// Check if this rule matches a client connection + pub fn matches(&self, client_ip: &IpAddr, user: Option<&str>, share: Option<&str>) -> bool { + // Check IP + if !self.ip_spec.matches(client_ip) { + return false; + } + + // Check user + if let Some(rule_user) = &self.user { + if let Some(client_user) = user { + if rule_user != client_user { + return false; + } + } else { + return false; + } + } + + // Check time + if !self.time.matches() { + return false; + } + + // Check share + if let Some(rule_share) = &self.share { + if let Some(client_share) = share { + if rule_share != client_share { + return false; + } + } else { + return false; + } + } + + true + } +} + +/// SMB client restriction ACL +pub struct ClientAcl { + /// Rules list + rules: RwLock>, + /// Default action when no rule matches + default_action: ClientAction, +} + +impl ClientAcl { + /// Create a new client ACL + pub fn new(default_action: ClientAction) -> Self { + Self { + rules: RwLock::new(Vec::new()), + default_action, + } + } + + /// Add a rule to the ACL + pub fn add_rule(&self, rule: ClientRule) { + self.rules.write().unwrap().push(rule); + } + + /// Clear all rules + pub fn clear_rules(&self) { + self.rules.write().unwrap().clear(); + } + + /// Check if a client connection is allowed + pub fn check(&self, client_ip: &IpAddr, user: Option<&str>, share: Option<&str>) -> ClientAction { + let rules = self.rules.read().unwrap(); + + // Find matching rules, sort by priority (highest first) + let matching_rules: Vec<&ClientRule> = rules + .iter() + .filter(|r| r.matches(client_ip, user, share)) + .collect(); + + // Return action of highest-priority matching rule + if let Some(rule) = matching_rules.first() { + rule.action + } else { + self.default_action + } + } + + /// List all rules + pub fn list_rules(&self) -> Vec { + self.rules.read().unwrap().clone() + } +} + +/// SMB client restriction manager +pub struct ClientRestrictionManager { + /// Global ACL + global_acl: RwLock, + /// Per-share ACLs + share_acls: RwLock>>, + /// Per-user ACLs + user_acls: RwLock>>, +} + +impl ClientRestrictionManager { + pub fn new() -> Self { + Self { + global_acl: RwLock::new(ClientAcl::new(ClientAction::Allow)), + share_acls: RwLock::new(HashMap::new()), + user_acls: RwLock::new(HashMap::new()), + } + } + + /// Set global ACL + pub fn set_global_acl(&self, acl: ClientAcl) { + *self.global_acl.write().unwrap() = acl; + } + + /// Add share-specific ACL + pub fn add_share_acl(&self, share: String, acl: ClientAcl) { + self.share_acls.write().unwrap().insert(share, Arc::new(acl)); + } + + /// Add user-specific ACL + pub fn add_user_acl(&self, user: String, acl: ClientAcl) { + self.user_acls.write().unwrap().insert(user, Arc::new(acl)); + } + + /// Check if a client connection is allowed + pub fn check(&self, client_ip: &IpAddr, user: Option<&str>, share: Option<&str>) -> ClientAction { + // Check user-specific ACL first (highest priority) + if let Some(user) = user { + if let Some(user_acl) = self.user_acls.read().unwrap().get(user) { + let action = user_acl.check(client_ip, Some(user), share); + if action != ClientAction::Allow { + return action; // Deny from user ACL + } + } + } + + // Check share-specific ACL + if let Some(share) = share { + if let Some(share_acl) = self.share_acls.read().unwrap().get(share) { + let action = share_acl.check(client_ip, user, Some(share)); + if action != ClientAction::Allow { + return action; // Deny from share ACL + } + } + } + + // Check global ACL (lowest priority) + self.global_acl.read().unwrap().check(client_ip, user, share) + } +} + +impl Default for ClientRestrictionManager { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ip_spec_single() { + let spec = IpSpec::Single(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100))); + + assert!(spec.matches(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)))); + assert!(!spec.matches(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)))); + } + + #[test] + fn test_ip_spec_cidr() { + let spec = IpSpec::Cidr(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 0)), 24); + + assert!(spec.matches(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)))); + assert!(spec.matches(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 255)))); + assert!(!spec.matches(&IpAddr::V4(Ipv4Addr::new(192, 168, 2, 1)))); + } + + #[test] + fn test_ip_spec_range() { + let spec = IpSpec::Range( + IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), + IpAddr::V4(Ipv4Addr::new(192, 168, 1, 200)), + ); + + assert!(spec.matches(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 150)))); + assert!(!spec.matches(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 50)))); + } + + #[test] + fn test_client_rule_ip_only() { + let rule = ClientRule::new( + ClientAction::Deny, + IpSpec::Single(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))), + None, + TimeSpec::Always, + None, + ); + + assert!(rule.matches(&IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), None, None)); + assert!(!rule.matches(&IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)), None, None)); + } + + #[test] + fn test_client_rule_user_specific() { + let rule = ClientRule::new( + ClientAction::Allow, + IpSpec::Any, + Some("alice".to_string()), + TimeSpec::Always, + None, + ); + + assert!(rule.matches(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), Some("alice"), None)); + assert!(!rule.matches(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), Some("bob"), None)); + } + + #[test] + fn test_client_acl_priority() { + let acl = ClientAcl::new(ClientAction::Allow); + + // Add deny rule for specific IP (high priority) + acl.add_rule(ClientRule::new( + ClientAction::Deny, + IpSpec::Single(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))), + None, + TimeSpec::Always, + None, + )); + + // Add allow rule for any IP (low priority) + acl.add_rule(ClientRule::new( + ClientAction::Allow, + IpSpec::Any, + None, + TimeSpec::Always, + None, + )); + + // Specific IP should be denied + assert_eq!( + acl.check(&IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), None, None), + ClientAction::Deny + ); + + // Other IPs should be allowed + assert_eq!( + acl.check(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), None, None), + ClientAction::Allow + ); + } + + #[test] + fn test_client_restriction_manager() { + let manager = ClientRestrictionManager::new(); + + // Set global ACL with deny rule + let global_acl = ClientAcl::new(ClientAction::Allow); + global_acl.add_rule(ClientRule::new( + ClientAction::Deny, + IpSpec::Cidr(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 0)), 24), + None, + TimeSpec::Always, + None, + )); + manager.set_global_acl(global_acl); + + // 10.0.0.x should be denied globally + assert_eq!( + manager.check(&IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), None, None), + ClientAction::Deny + ); + + // Other IPs should be allowed + assert_eq!( + manager.check(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), None, None), + ClientAction::Allow + ); + } +} \ No newline at end of file diff --git a/vendor/smb-server/src/lib.rs b/vendor/smb-server/src/lib.rs index 29354e9..b42158b 100644 --- a/vendor/smb-server/src/lib.rs +++ b/vendor/smb-server/src/lib.rs @@ -32,6 +32,7 @@ mod path; mod proto; mod server; mod snapshot; +mod client_restrictions; mod utils; pub use backend::{BackendCapabilities, DirEntry, FileInfo, FileTimes, Handle, NullHandle, OpenIntent, OpenOptions, ShareBackend};