Add SMB Client Restrictions (Phase 1-3): access control for SMB clients
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled

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:
Warren
2026-06-21 12:51:37 +08:00
parent a475de45c9
commit 614275f77a
2 changed files with 499 additions and 0 deletions

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

View File

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