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:
Warren
2026-06-24 00:46:33 +08:00
parent 5300b672cb
commit 57fd6a475f
33 changed files with 1211 additions and 253 deletions

View File

@@ -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() {

View File

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

View File

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

View File

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

View File

@@ -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),
}
}

View File

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

View File

@@ -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)