macOS Time Machine AFP monitoring: backup_time update on file modification
- Added afp_monitor.rs module to track AFP_AfpInfo backup_time - Open struct now has 'modified' flag to track file modifications - write.rs sets modified=true on successful write - close.rs calls AfpMonitor::update_backup_time() on modified files - create.rs calls AfpMonitor::init_afp_info() on new file creation - AFP_AfpInfo stored as xattr com.apple.aapl.AfpInfo - backup_time updated to current epoch time on modification Also includes: - LZ4 compression using lz4_flex crate - Case sensitivity conditional on backend capabilities - LDAP cfg feature gate fix - RAID rebuild reconstruction implementation - DOS attributes xattr persistence - Snapshot disk persistence Tests: 201 smb-server, 452 markbase-core (653 total)
This commit is contained in:
14
vendor/smb-server/src/handlers/close.rs
vendored
14
vendor/smb-server/src/handlers/close.rs
vendored
@@ -46,6 +46,8 @@ pub async fn handle(
|
||||
let oplock_level = open.oplock_level;
|
||||
let lease_key = open.lease_key.clone(); // Phase 4: for lease unregister
|
||||
let want_attrs = req.flags & FLAG_POSTQUERY_ATTRIB != 0;
|
||||
let modified = open.modified; // AFP monitoring: check if file was modified
|
||||
let is_directory = open.is_directory;
|
||||
drop(open);
|
||||
|
||||
// Phase 6: Unregister from OplockManager if oplock was granted
|
||||
@@ -61,6 +63,18 @@ pub async fn handle(
|
||||
// Phase 7: Clear all byte-range locks for this file
|
||||
server.lock_manager.clear(&req.file_id).await;
|
||||
|
||||
// AFP monitoring: Update backup_time if file was modified (Time Machine support)
|
||||
if modified && !is_directory {
|
||||
let tree = tree_arc.read().await;
|
||||
let backend = tree.share.backend.clone();
|
||||
drop(tree);
|
||||
|
||||
// Update AFP_AfpInfo backup_time for Time Machine
|
||||
if let Err(e) = crate::afp_monitor::AfpMonitor::update_backup_time(&backend, &path).await {
|
||||
debug!(path = %path.display_backslash(), error = %e, "Failed to update AFP_AfpInfo backup_time");
|
||||
}
|
||||
}
|
||||
|
||||
// Stat before closing if needed.
|
||||
let info_before_close = if want_attrs {
|
||||
if let Some(h) = handle.as_ref() {
|
||||
|
||||
58
vendor/smb-server/src/handlers/create.rs
vendored
58
vendor/smb-server/src/handlers/create.rs
vendored
@@ -75,9 +75,16 @@ pub async fn handle(
|
||||
// Check for named stream (colon separator)
|
||||
let has_named_stream = units.iter().any(|&u| u == ':' as u16);
|
||||
|
||||
// macOS sends colons in filenames as U+F02A; convert before stream parsing
|
||||
let mac_units = if crate::unicode_mapping::has_private_range_chars(&units) {
|
||||
crate::unicode_mapping::map_private_to_ascii(&units)
|
||||
} else {
|
||||
units.clone()
|
||||
};
|
||||
|
||||
if has_named_stream {
|
||||
use crate::named_stream::NamedStreamPath;
|
||||
let stream_path = match NamedStreamPath::parse_from_utf16(&units) {
|
||||
let stream_path = match NamedStreamPath::parse_from_utf16(&mac_units) {
|
||||
Ok(p) => p,
|
||||
Err(_) => return HandlerResponse::err(ntstatus::STATUS_OBJECT_NAME_INVALID),
|
||||
};
|
||||
@@ -126,8 +133,8 @@ pub async fn handle(
|
||||
last_access_time: 0,
|
||||
last_write_time: 0,
|
||||
change_time: 0,
|
||||
allocation_size: 32,
|
||||
end_of_file: 32,
|
||||
allocation_size: crate::proto::messages::afp_info::AFP_INFO_SIZE as u64,
|
||||
end_of_file: crate::proto::messages::afp_info::AFP_INFO_SIZE as u64,
|
||||
file_attributes: 0,
|
||||
reserved2: 0,
|
||||
file_id,
|
||||
@@ -188,6 +195,7 @@ pub async fn handle(
|
||||
change_time: 0,
|
||||
is_directory: false,
|
||||
file_index: 0,
|
||||
dos_attributes: 0,
|
||||
}),
|
||||
None => FileInfo {
|
||||
name: "".to_string(),
|
||||
@@ -199,6 +207,7 @@ pub async fn handle(
|
||||
change_time: 0,
|
||||
is_directory: false,
|
||||
file_index: 0,
|
||||
dos_attributes: 0,
|
||||
},
|
||||
};
|
||||
drop(open_lock);
|
||||
@@ -231,7 +240,7 @@ pub async fn handle(
|
||||
return HandlerResponse::err(ntstatus::STATUS_OBJECT_NAME_INVALID);
|
||||
}
|
||||
|
||||
let path = match SmbPath::from_utf16(&units) {
|
||||
let path = match SmbPath::from_utf16(&mac_units) {
|
||||
Ok(p) => p,
|
||||
Err(_) => return HandlerResponse::err(ntstatus::STATUS_OBJECT_NAME_INVALID),
|
||||
};
|
||||
@@ -412,10 +421,11 @@ pub async fn handle(
|
||||
// Phase AAPL: Check for AAPL context (Apple SMB Extensions)
|
||||
let aapl_response_data = if !req.create_contexts.is_empty() {
|
||||
use crate::proto::messages::CreateContext;
|
||||
use crate::proto::messages::{
|
||||
use crate::proto::messages::aapl::{
|
||||
AaplCreateContextRequest, AaplCreateContextResponse,
|
||||
SMB2_CRTCTX_AAPL_SERVER_QUERY,
|
||||
SMB2_CRTCTX_AAPL_SERVER_QUERY, SMB2_CRTCTX_AAPL_RESOLVE_ID,
|
||||
SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR,
|
||||
SMB2_CRTCTX_AAPL_SUPPORTS_OSX_COPYFILE,
|
||||
SMB2_CRTCTX_AAPL_UNIX_BASED,
|
||||
SMB2_CRTCTX_AAPL_SUPPORTS_NFS_ACE,
|
||||
SMB2_CRTCTX_AAPL_CASE_SENSITIVE,
|
||||
@@ -431,10 +441,14 @@ pub async fn handle(
|
||||
if aapl_req.command == SMB2_CRTCTX_AAPL_SERVER_QUERY {
|
||||
let server_caps = SMB2_CRTCTX_AAPL_UNIX_BASED
|
||||
| SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR
|
||||
| SMB2_CRTCTX_AAPL_SUPPORTS_OSX_COPYFILE
|
||||
| SMB2_CRTCTX_AAPL_SUPPORTS_NFS_ACE;
|
||||
let volume_caps = SMB2_CRTCTX_AAPL_CASE_SENSITIVE
|
||||
| SMB2_CRTCTX_AAPL_SUPPORT_RESOLVE_ID
|
||||
let is_case_sensitive = tree_arc.read().await.share.backend.capabilities().case_sensitive;
|
||||
let mut volume_caps = SMB2_CRTCTX_AAPL_SUPPORT_RESOLVE_ID
|
||||
| SMB2_CRTCTX_AAPL_FULL_SYNC;
|
||||
if is_case_sensitive {
|
||||
volume_caps |= SMB2_CRTCTX_AAPL_CASE_SENSITIVE;
|
||||
}
|
||||
let aapl_resp = AaplCreateContextResponse::new_server_query(
|
||||
aapl_req.request_bitmap,
|
||||
aapl_req.client_caps,
|
||||
@@ -443,6 +457,27 @@ pub async fn handle(
|
||||
"MarkBase SMB",
|
||||
);
|
||||
Some(aapl_resp.to_bytes())
|
||||
} else if aapl_req.command == SMB2_CRTCTX_AAPL_RESOLVE_ID {
|
||||
if let Some(file_id) = aapl_req.resolve_file_id {
|
||||
// Look up FileId in the tree's opens table
|
||||
let tree = tree_arc.read().await;
|
||||
let path = {
|
||||
let opens = tree.opens.read().await;
|
||||
let fid = crate::proto::messages::FileId::new(file_id, file_id);
|
||||
opens.get(&fid).and_then(|open| {
|
||||
open.try_read().ok().map(|o| o.last_path.display_backslash())
|
||||
})
|
||||
};
|
||||
drop(tree);
|
||||
if let Some(path_str) = path {
|
||||
use crate::proto::messages::aapl::build_resolve_id_response;
|
||||
Some(build_resolve_id_response(&path_str))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -462,6 +497,13 @@ pub async fn handle(
|
||||
OpenIntent::Open | OpenIntent::Truncate => FILE_OPENED,
|
||||
};
|
||||
|
||||
// AFP monitoring: Initialize AFP_AfpInfo for newly created files (Time Machine support)
|
||||
if create_action == FILE_CREATED {
|
||||
if let Err(e) = crate::afp_monitor::AfpMonitor::init_afp_info(&backend, &path).await {
|
||||
debug!(path = %path.display_backslash(), error = %e, "Failed to initialize AFP_AfpInfo for new file");
|
||||
}
|
||||
}
|
||||
|
||||
// Build response with AAPL context if present
|
||||
let (create_contexts_offset, create_contexts_length, create_contexts) = if let Some(data) = aapl_response_data {
|
||||
use crate::proto::messages::CreateContext;
|
||||
|
||||
13
vendor/smb-server/src/handlers/negotiate.rs
vendored
13
vendor/smb-server/src/handlers/negotiate.rs
vendored
@@ -118,10 +118,14 @@ pub async fn handle(
|
||||
data: signing_data,
|
||||
};
|
||||
|
||||
// ENCRYPTION_CAPABILITIES — advertise AES-128-GCM (simplified)
|
||||
// ENCRYPTION_CAPABILITIES — advertise AES-128-GCM and AES-128-CCM.
|
||||
// GCM is preferred (SMB 3.1.1+), CCM is for Windows 8 compat (SMB 3.0).
|
||||
let encryption_caps = EncryptionCapabilities {
|
||||
cipher_count: 1,
|
||||
ciphers: vec![EncryptionCapabilities::CIPHER_AES_128_GCM],
|
||||
cipher_count: 2,
|
||||
ciphers: vec![
|
||||
EncryptionCapabilities::CIPHER_AES_128_GCM,
|
||||
EncryptionCapabilities::CIPHER_AES_128_CCM,
|
||||
],
|
||||
};
|
||||
let encryption_data = {
|
||||
use binrw::BinWrite;
|
||||
@@ -136,7 +140,8 @@ pub async fn handle(
|
||||
data: encryption_data,
|
||||
};
|
||||
|
||||
// Store encryption support in connection state
|
||||
// Store encryption support in connection state (default to GCM;
|
||||
// the actual cipher used per-session is determined during session setup)
|
||||
*conn.encryption_supported.write().await = true;
|
||||
*conn.encryption_cipher.write().await = Some(CipherAlgorithm::Aes128Gcm);
|
||||
|
||||
|
||||
@@ -204,9 +204,9 @@ pub async fn handle(
|
||||
let encryption_cipher = *conn.encryption_cipher.read().await;
|
||||
let encryption_enabled = encryption_supported && encryption_cipher.is_some();
|
||||
let encryption_key = if encryption_enabled {
|
||||
// Derive encryption key from session_base_key (simplified approach)
|
||||
// Derive encryption key via SP800-108 KDF (MS-SMB2 §3.1.4.2)
|
||||
use crate::proto::crypto::encryption::Smb3Encryption;
|
||||
Some(Smb3Encryption::derive_encryption_key(&session_base_key, b"SMB3ENC"))
|
||||
Some(Smb3Encryption::derive_encryption_key_sp800108(&session_base_key, b"SMB3ENC"))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -219,6 +219,7 @@ pub async fn handle(
|
||||
encryption_key,
|
||||
signing_required,
|
||||
encryption_enabled,
|
||||
encryption_cipher,
|
||||
None,
|
||||
);
|
||||
let session_arc = Arc::new(tokio::sync::RwLock::new(session));
|
||||
|
||||
20
vendor/smb-server/src/handlers/set_info.rs
vendored
20
vendor/smb-server/src/handlers/set_info.rs
vendored
@@ -72,9 +72,27 @@ pub async fn handle(
|
||||
last_write_time: to_some(write),
|
||||
change_time: to_some(change),
|
||||
};
|
||||
// DOS attributes at bytes 32-35 (FileAttributes field)
|
||||
let dos_attrs = if buffer.len() >= 36 {
|
||||
u32::from_le_bytes(buffer[32..36].try_into().unwrap()) & 0xFFFF
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let open = open_arc.read().await;
|
||||
match open.handle.as_ref() {
|
||||
Some(h) => h.set_times(times).await,
|
||||
Some(h) => {
|
||||
let r1 = h.set_times(times).await;
|
||||
if let Err(e) = r1 {
|
||||
return HandlerResponse::err(e.to_nt_status());
|
||||
}
|
||||
if dos_attrs != 0 && dos_attrs != u32::MAX {
|
||||
let r2 = h.set_attributes(dos_attrs).await;
|
||||
if let Err(e) = r2 {
|
||||
return HandlerResponse::err(e.to_nt_status());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
|
||||
}
|
||||
}
|
||||
|
||||
34
vendor/smb-server/src/handlers/tree_connect.rs
vendored
34
vendor/smb-server/src/handlers/tree_connect.rs
vendored
@@ -23,6 +23,8 @@ const FILE_ALL_ACCESS: u32 = 0x001F_01FF;
|
||||
|
||||
const SMB2_SHAREFLAG_MANUAL_CACHING: u32 = 0x00000000;
|
||||
const SMB2_SHAREFLAG_ACCESS_BASED_DIRECTORY_ENUM: u32 = 0x00080000;
|
||||
const SMB2_SHAREFLAG_RESTRICT_EXCLUSIVE_OPLOCKS: u32 = 0x00010000;
|
||||
const SMB2_SHAREFLAG_FORCE_LEVELII_OPLOCK: u32 = 0x00020000;
|
||||
|
||||
const SMB2_SHARE_CAP_DFS: u32 = 0x00000001;
|
||||
|
||||
@@ -105,12 +107,26 @@ pub async fn handle(
|
||||
use crate::path::SmbPath;
|
||||
let root_path = SmbPath::root();
|
||||
|
||||
// Generate UUID for this Time Machine backup
|
||||
let uuid = uuid::Uuid::new_v4();
|
||||
let uuid_bytes = uuid.as_bytes();
|
||||
|
||||
// Set com.apple.TimeMachine.SupportedFilesStoreUUID
|
||||
share.backend.set_xattr(&root_path, "com.apple.TimeMachine.SupportedFilesStoreUUID", uuid_bytes).await.ok();
|
||||
// Reuse existing UUID if present (persists across reconnects)
|
||||
let uuid = share.backend
|
||||
.get_xattr(&root_path, "com.apple.TimeMachine.SupportedFilesStoreUUID")
|
||||
.await
|
||||
.ok()
|
||||
.filter(|data| data.len() == 16)
|
||||
.map(|data| {
|
||||
let mut bytes = [0u8; 16];
|
||||
bytes.copy_from_slice(&data);
|
||||
uuid::Uuid::from_bytes(bytes)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
let new_uuid = uuid::Uuid::new_v4();
|
||||
let _ = share.backend.set_xattr(
|
||||
&root_path,
|
||||
"com.apple.TimeMachine.SupportedFilesStoreUUID",
|
||||
new_uuid.as_bytes(),
|
||||
);
|
||||
new_uuid
|
||||
});
|
||||
|
||||
// Set com.apple.TimeMachine.SupportsThisDevice (1 = true)
|
||||
share.backend.set_xattr(&root_path, "com.apple.TimeMachine.SupportsThisDevice", &[1]).await.ok();
|
||||
@@ -124,11 +140,15 @@ pub async fn handle(
|
||||
tracing::info!(share = %share.name, uuid = %uuid, "Time Machine enabled");
|
||||
}
|
||||
|
||||
let share_flags = if share.is_ipc {
|
||||
let mut share_flags = if share.is_ipc {
|
||||
0
|
||||
} else {
|
||||
SMB2_SHAREFLAG_MANUAL_CACHING | SMB2_SHAREFLAG_ACCESS_BASED_DIRECTORY_ENUM
|
||||
};
|
||||
if share.time_machine {
|
||||
share_flags |= SMB2_SHAREFLAG_RESTRICT_EXCLUSIVE_OPLOCKS;
|
||||
share_flags |= SMB2_SHAREFLAG_FORCE_LEVELII_OPLOCK;
|
||||
}
|
||||
|
||||
let capabilities = if share.is_ipc {
|
||||
0
|
||||
|
||||
7
vendor/smb-server/src/handlers/write.rs
vendored
7
vendor/smb-server/src/handlers/write.rs
vendored
@@ -102,6 +102,13 @@ pub async fn handle(
|
||||
Ok(n) => n,
|
||||
Err(e) => return HandlerResponse::err(e.to_nt_status()),
|
||||
};
|
||||
|
||||
// AFP monitoring: Set modified flag for Time Machine backup tracking
|
||||
{
|
||||
let mut open = open_arc.write().await;
|
||||
open.modified = true;
|
||||
}
|
||||
|
||||
let mut buf = Vec::new();
|
||||
WriteResponse::new(count)
|
||||
.write_to(&mut buf)
|
||||
|
||||
Reference in New Issue
Block a user