Add SMB Client Restrictions (Phase 1-3): access control for SMB clients
Features: - IpSpec: IP address specification (Single/Cidr/Range/Any) - TimeSpec: Time-based restrictions (HourRange/DayOfWeek/DayHour) - ClientRule: Allow/Deny rules with IP/user/time/share - ClientAcl: Priority-based rule matching - ClientRestrictionManager: Global/Share/User ACLs Security: - Restrict SMB client access by IP address - Time-based access control (business hours only) - User-specific and share-specific ACLs - CIDR notation support (192.168.1.0/24) Files: - vendor/smb-server/src/client_restrictions.rs (443 lines) - vendor/smb-server/src/lib.rs (+1 line) Tests: 7 passed (smb-server), 317 passed (markbase-core)
This commit is contained in:
498
vendor/smb-server/src/client_restrictions.rs
vendored
Normal file
498
vendor/smb-server/src/client_restrictions.rs
vendored
Normal file
@@ -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<u8>),
|
||||
/// Combined: DayOfWeek + HourRange
|
||||
DayHour(HashSet<u8>, 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<String>,
|
||||
/// Time-based restriction
|
||||
pub time: TimeSpec,
|
||||
/// Share name restriction (None = any share)
|
||||
pub share: Option<String>,
|
||||
/// Priority (higher = more specific)
|
||||
pub priority: u32,
|
||||
}
|
||||
|
||||
impl ClientRule {
|
||||
/// Create a new client rule
|
||||
pub fn new(
|
||||
action: ClientAction,
|
||||
ip_spec: IpSpec,
|
||||
user: Option<String>,
|
||||
time: TimeSpec,
|
||||
share: Option<String>,
|
||||
) -> 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<Vec<ClientRule>>,
|
||||
/// 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<ClientRule> {
|
||||
self.rules.read().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// SMB client restriction manager
|
||||
pub struct ClientRestrictionManager {
|
||||
/// Global ACL
|
||||
global_acl: RwLock<ClientAcl>,
|
||||
/// Per-share ACLs
|
||||
share_acls: RwLock<HashMap<String, Arc<ClientAcl>>>,
|
||||
/// Per-user ACLs
|
||||
user_acls: RwLock<HashMap<String, Arc<ClientAcl>>>,
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
1
vendor/smb-server/src/lib.rs
vendored
1
vendor/smb-server/src/lib.rs
vendored
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user