Implement SMB Oplocks Phase 1-2
Phase 1: Data structures - Add oplock_level and share_access fields to Open struct - Update Open::new() signature with new parameters - Update handlers/create.rs to pass oplock params Phase 2: OplockManager - Create oplock.rs with OplockManager struct - OplockEntry for tracking per-client oplock state - can_grant() - check ShareAccess compatibility - register() / unregister() - lifecycle management - break_oplock() - generate OPLOCK_BREAK_NOTIFICATION - Add OplockManager to ServerState - Add Hash trait to SmbPath for HashMap key All 229 tests pass.
This commit is contained in:
7
vendor/smb-server/src/conn/state.rs
vendored
7
vendor/smb-server/src/conn/state.rs
vendored
@@ -294,6 +294,9 @@ pub struct Open {
|
|||||||
pub is_directory: bool,
|
pub is_directory: bool,
|
||||||
pub delete_on_close: bool,
|
pub delete_on_close: bool,
|
||||||
pub search_state: Option<DirCursor>,
|
pub search_state: Option<DirCursor>,
|
||||||
|
// Oplock fields (MS-SMB2 §2.2.13 / §2.2.14)
|
||||||
|
pub oplock_level: u8,
|
||||||
|
pub share_access: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Open {
|
impl Open {
|
||||||
@@ -304,6 +307,8 @@ impl Open {
|
|||||||
last_path: SmbPath,
|
last_path: SmbPath,
|
||||||
is_directory: bool,
|
is_directory: bool,
|
||||||
delete_on_close: bool,
|
delete_on_close: bool,
|
||||||
|
oplock_level: u8,
|
||||||
|
share_access: u32,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
file_id,
|
file_id,
|
||||||
@@ -313,6 +318,8 @@ impl Open {
|
|||||||
is_directory,
|
is_directory,
|
||||||
delete_on_close,
|
delete_on_close,
|
||||||
search_state: None,
|
search_state: None,
|
||||||
|
oplock_level,
|
||||||
|
share_access,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
vendor/smb-server/src/handlers/create.rs
vendored
8
vendor/smb-server/src/handlers/create.rs
vendored
@@ -153,6 +153,10 @@ pub async fn handle(
|
|||||||
// Allocate FileId, register Open.
|
// Allocate FileId, register Open.
|
||||||
let tree = tree_arc.write().await;
|
let tree = tree_arc.write().await;
|
||||||
let file_id = tree.alloc_file_id();
|
let file_id = tree.alloc_file_id();
|
||||||
|
|
||||||
|
// Phase 4: Oplock support - determine oplock level
|
||||||
|
let granted_oplock = 0; // Phase 1 placeholder (will be dynamic in Phase 4)
|
||||||
|
|
||||||
let open = Open::new(
|
let open = Open::new(
|
||||||
file_id,
|
file_id,
|
||||||
handle,
|
handle,
|
||||||
@@ -160,6 +164,8 @@ pub async fn handle(
|
|||||||
path,
|
path,
|
||||||
info.is_directory,
|
info.is_directory,
|
||||||
delete_on_close,
|
delete_on_close,
|
||||||
|
granted_oplock, // oplock_level
|
||||||
|
req.share_access, // share_access
|
||||||
);
|
);
|
||||||
let open_arc = Arc::new(tokio::sync::RwLock::new(open));
|
let open_arc = Arc::new(tokio::sync::RwLock::new(open));
|
||||||
tree.opens.write().await.insert(file_id, open_arc);
|
tree.opens.write().await.insert(file_id, open_arc);
|
||||||
@@ -172,7 +178,7 @@ pub async fn handle(
|
|||||||
};
|
};
|
||||||
let resp = CreateResponse {
|
let resp = CreateResponse {
|
||||||
structure_size: 89,
|
structure_size: 89,
|
||||||
oplock_level: 0,
|
oplock_level: granted_oplock, // Phase 4: will be dynamic
|
||||||
flags: 0,
|
flags: 0,
|
||||||
create_action,
|
create_action,
|
||||||
creation_time: info.creation_time,
|
creation_time: info.creation_time,
|
||||||
|
|||||||
1
vendor/smb-server/src/lib.rs
vendored
1
vendor/smb-server/src/lib.rs
vendored
@@ -26,6 +26,7 @@ mod fs;
|
|||||||
mod handlers;
|
mod handlers;
|
||||||
pub(crate) mod info_class;
|
pub(crate) mod info_class;
|
||||||
pub mod ntstatus;
|
pub mod ntstatus;
|
||||||
|
mod oplock;
|
||||||
mod path;
|
mod path;
|
||||||
mod proto;
|
mod proto;
|
||||||
mod server;
|
mod server;
|
||||||
|
|||||||
176
vendor/smb-server/src/oplock.rs
vendored
Normal file
176
vendor/smb-server/src/oplock.rs
vendored
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
//! Oplock Manager — global state tracking for opportunistic locking.
|
||||||
|
//!
|
||||||
|
//! MS-SMB2 §2.2.13 / §2.2.14: Oplocks allow clients to cache file data locally,
|
||||||
|
//! reducing network round-trips. The server tracks all opens per file and
|
||||||
|
//! triggers OPLOCK_BREAK_NOTIFICATION when conflicting opens occur.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
use crate::builder::Access;
|
||||||
|
use crate::path::SmbPath;
|
||||||
|
use crate::proto::messages::{FileId, OplockBreakNotification, OplockLevel};
|
||||||
|
|
||||||
|
/// An entry tracking one client's oplock on a file.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct OplockEntry {
|
||||||
|
pub file_id: FileId,
|
||||||
|
pub tree_id: u32,
|
||||||
|
pub session_id: u64,
|
||||||
|
pub oplock_level: u8,
|
||||||
|
pub share_access: u32,
|
||||||
|
pub granted_access: Access,
|
||||||
|
pub connection_id: u64, // For notification routing
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Global oplock state manager (MS-SMB2 §3.3.1.6).
|
||||||
|
pub struct OplockManager {
|
||||||
|
/// File path → all opens with oplocks on that file.
|
||||||
|
file_opens: RwLock<HashMap<SmbPath, Vec<OplockEntry>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OplockManager {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
file_opens: RwLock::new(HashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if requested oplock can be granted (MS-SMB2 §3.3.5.9).
|
||||||
|
/// Returns the granted level (may be lower than requested).
|
||||||
|
pub async fn can_grant(
|
||||||
|
&self,
|
||||||
|
path: &SmbPath,
|
||||||
|
requested_level: u8,
|
||||||
|
share_access: u32,
|
||||||
|
granted_access: Access,
|
||||||
|
) -> Option<u8> {
|
||||||
|
let file_opens = self.file_opens.read().await;
|
||||||
|
let existing = file_opens.get(path);
|
||||||
|
|
||||||
|
// No existing opens → grant requested level
|
||||||
|
if existing.is_none() || existing.unwrap().is_empty() {
|
||||||
|
return Some(requested_level);
|
||||||
|
}
|
||||||
|
|
||||||
|
let existing_opens = existing.unwrap();
|
||||||
|
|
||||||
|
// Check ShareAccess conflicts (MS-SMB2 §3.3.5.9)
|
||||||
|
for entry in existing_opens {
|
||||||
|
// If existing open doesn't allow sharing, deny oplock
|
||||||
|
if !share_access_compatible(entry.share_access, share_access) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If existing has exclusive/batch oplock, can only grant Level II
|
||||||
|
if entry.oplock_level == OplockLevel::Exclusive as u8
|
||||||
|
|| entry.oplock_level == OplockLevel::Batch as u8
|
||||||
|
{
|
||||||
|
// Can grant Level II if share access compatible
|
||||||
|
if requested_level == OplockLevel::Ii as u8
|
||||||
|
&& share_access_compatible(entry.share_access, share_access)
|
||||||
|
{
|
||||||
|
return Some(OplockLevel::Ii as u8);
|
||||||
|
}
|
||||||
|
// Otherwise deny
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All existing opens are Level II → grant requested level
|
||||||
|
Some(requested_level)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register a new open with oplock (MS-SMB2 §3.3.5.9).
|
||||||
|
pub async fn register(&self, path: &SmbPath, entry: OplockEntry) {
|
||||||
|
let mut file_opens = self.file_opens.write().await;
|
||||||
|
file_opens
|
||||||
|
.entry(path.clone())
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove an open when closed (MS-SMB2 §3.3.5.7).
|
||||||
|
pub async fn unregister(&self, path: &SmbPath, file_id: &FileId) {
|
||||||
|
let mut file_opens = self.file_opens.write().await;
|
||||||
|
if let Some(entries) = file_opens.get_mut(path) {
|
||||||
|
entries.retain(|e| e.file_id != *file_id);
|
||||||
|
if entries.is_empty() {
|
||||||
|
file_opens.remove(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trigger oplock break when conflicting open occurs (MS-SMB2 §3.3.5.9).
|
||||||
|
/// Returns notifications to send to affected clients.
|
||||||
|
pub async fn break_oplock(
|
||||||
|
&self,
|
||||||
|
path: &SmbPath,
|
||||||
|
new_share_access: u32,
|
||||||
|
new_granted_access: Access,
|
||||||
|
) -> Vec<OplockBreakNotification> {
|
||||||
|
let mut notifications = Vec::new();
|
||||||
|
let mut file_opens = self.file_opens.write().await;
|
||||||
|
|
||||||
|
if let Some(entries) = file_opens.get_mut(path) {
|
||||||
|
for entry in entries.iter_mut() {
|
||||||
|
// Check if new open conflicts with existing oplock
|
||||||
|
if !share_access_compatible(entry.share_access, new_share_access) {
|
||||||
|
// Need to break the oplock
|
||||||
|
let new_level = OplockLevel::Ii as u8; // Downgrade to Level II
|
||||||
|
|
||||||
|
// Build notification (MS-SMB2 §2.2.23.1)
|
||||||
|
notifications.push(OplockBreakNotification {
|
||||||
|
structure_size: 24,
|
||||||
|
oplock_level: new_level,
|
||||||
|
reserved: 0,
|
||||||
|
reserved2: 0,
|
||||||
|
file_id: entry.file_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update entry's oplock level
|
||||||
|
entry.oplock_level = new_level;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notifications
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all opens for a file (for diagnostics).
|
||||||
|
pub async fn get_opens(&self, path: &SmbPath) -> Vec<OplockEntry> {
|
||||||
|
let file_opens = self.file_opens.read().await;
|
||||||
|
file_opens.get(path).cloned().unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for OplockManager {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check ShareAccess compatibility (MS-SMB2 §3.3.5.9).
|
||||||
|
pub fn share_access_compatible(existing: u32, new: u32) -> bool {
|
||||||
|
const FILE_SHARE_READ: u32 = 0x00000001;
|
||||||
|
const FILE_SHARE_WRITE: u32 = 0x00000002;
|
||||||
|
const FILE_SHARE_DELETE: u32 = 0x00000004;
|
||||||
|
|
||||||
|
// If existing denies read sharing and new wants read → conflict
|
||||||
|
if (existing & FILE_SHARE_READ) == 0 && (new & FILE_SHARE_READ) != 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If existing denies write sharing and new wants write → conflict
|
||||||
|
if (existing & FILE_SHARE_WRITE) == 0 && (new & FILE_SHARE_WRITE) != 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If existing denies delete sharing and new wants delete → conflict
|
||||||
|
if (existing & FILE_SHARE_DELETE) == 0 && (new & FILE_SHARE_DELETE) != 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
2
vendor/smb-server/src/path.rs
vendored
2
vendor/smb-server/src/path.rs
vendored
@@ -12,7 +12,7 @@ use crate::error::{SmbError, SmbResult};
|
|||||||
/// A validated, component-list path. No `..`, no Windows-forbidden chars, no
|
/// A validated, component-list path. No `..`, no Windows-forbidden chars, no
|
||||||
/// alternate streams. Always relative to the share root — the empty path is
|
/// alternate streams. Always relative to the share root — the empty path is
|
||||||
/// the root.
|
/// the root.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
|
||||||
pub struct SmbPath {
|
pub struct SmbPath {
|
||||||
components: Vec<String>,
|
components: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|||||||
3
vendor/smb-server/src/server.rs
vendored
3
vendor/smb-server/src/server.rs
vendored
@@ -196,6 +196,8 @@ pub struct ServerState {
|
|||||||
/// iteration and connection loops abandon their next read.
|
/// iteration and connection loops abandon their next read.
|
||||||
pub shutdown: Arc<Notify>,
|
pub shutdown: Arc<Notify>,
|
||||||
pub shutting_down: Arc<AtomicBool>,
|
pub shutting_down: Arc<AtomicBool>,
|
||||||
|
/// Global oplock state manager (Phase 2).
|
||||||
|
pub oplock_manager: Arc<crate::oplock::OplockManager>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerState {
|
impl ServerState {
|
||||||
@@ -208,6 +210,7 @@ impl ServerState {
|
|||||||
server_start_filetime: now_filetime(),
|
server_start_filetime: now_filetime(),
|
||||||
shutdown: Arc::new(Notify::new()),
|
shutdown: Arc::new(Notify::new()),
|
||||||
shutting_down: Arc::new(AtomicBool::new(false)),
|
shutting_down: Arc::new(AtomicBool::new(false)),
|
||||||
|
oplock_manager: Arc::new(crate::oplock::OplockManager::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user