feat(mount): add native Windows Cloud Files Engine (cldapi.dll + DefineDosDeviceW) fallback without WebClient
Sanctum Release / Build & Test (Windows x86_64 & Linux musl) (push) Canceled after 0s
Sanctum Release / Sign & Release (push) Canceled after 0s

This commit is contained in:
harald committed 2026-10-21 12:58:20 +02:00
1 parent 76c23dbfa3
commit d93b03322e
13 files changed
+2371 -50

No files matched your search

+1767
View File
File diff suppressed because it is too large. Load diff
+1
View File
@@ -1,4 +1,5 @@
pub mod carrier;
pub mod cloudfiles;
pub mod crypto;
pub mod mount;
pub mod pathutil;
+11 -2
View File
@@ -10,7 +10,9 @@ use sanctum::crypto::{
mnemonic_to_dek, set_allow_legacy_names, validate_password, wrap_slot0_payload,
wrap_slot1_payload, KdfParams, FORMAT_VERSION, FORMAT_VERSION_V3,
};
use sanctum::mount::{format_drive, mount_container, unmount_drive, ContainerAuth};
use sanctum::mount::{
format_drive, mount_container_with_backend, unmount_drive, ContainerAuth, MountBackend,
};
use sanctum::recovery::{
detect_recovery_key_slot, export_header_backup, restore_header_backup,
restore_slot_from_recovery_key,
@@ -121,6 +123,10 @@ enum Commands {
/// Autorisiert den Neuaufbau des Metadaten-MAC nach einem Header-Restore (F-02)
#[arg(long, default_value_t = false)]
rebuild_mac: bool,
/// Mount-Backend unter Windows: 'auto' (WebClient mit nahtlosem Fallback auf Cloud-Files), 'cloudfiles' (cldapi.dll ohne WebClient) oder 'webdav'
#[arg(long, default_value = "auto", value_name = "MODE")]
backend: String,
},
/// Trennt ein eingebundenes Netzlaufwerk manuell
@@ -1648,7 +1654,9 @@ async fn run() -> Result<()> {
anti_leak_list,
stealth,
rebuild_mac,
backend,
} => {
let mount_backend = MountBackend::from_str_opt(&backend)?;
let drive_char = match drive {
Some(ref d) => parse_drive_letter(d)?,
None => {
@@ -1688,7 +1696,7 @@ async fn run() -> Result<()> {
let enable_tray = !no_tray && !stealth;
let lock_on_screen_lock = !no_screen_lock;
let anti_leak = !no_anti_leak;
mount_container(
mount_container_with_backend(
&path,
drive_char,
mount_point.as_deref(),
@@ -1702,6 +1710,7 @@ async fn run() -> Result<()> {
anti_leak_list.as_deref(),
stealth,
rebuild_mac,
mount_backend,
)
.await?;
}
+218 -33
View File
@@ -29,6 +29,32 @@ pub enum ContainerAuth {
RecoveryKey(Zeroizing<String>),
}
/// Auswahl des Mount-Backends unter Windows.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MountBackend {
/// Versucht zuerst WebClient (inkl. Userland-ETW-Wakeup); falls deaktiviert,
/// übernimmt nahtlos die Windows Cloud-Files-Engine (`cldapi.dll` + `DefineDosDeviceW`).
Auto,
/// Nutzt direkt die Windows Cloud-Files-Engine (`cldapi.dll` + `DefineDosDeviceW`) ohne WebClient.
CloudFiles,
/// Nutzt ausschließlich das klassische WebDAV-Netzlaufwerk (`WNetAddConnection2W`).
WebDav,
}
impl MountBackend {
pub fn from_str_opt(s: &str) -> Result<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"auto" => Ok(Self::Auto),
"cloudfiles" | "cfapi" | "cldapi" | "cloud" => Ok(Self::CloudFiles),
"webdav" | "wnet" | "dav" => Ok(Self::WebDav),
other => bail!(
"Unbekanntes Mount-Backend '{}'. Erlaubte Werte: auto, cloudfiles, webdav",
other
),
}
}
}
/// Hilfsfunktion zur Formatierung des Laufwerksbuchstabens (z. B. 'S' -> "S:")
pub fn format_drive(drive_letter: char) -> String {
format!("{}:", drive_letter.to_ascii_uppercase())
@@ -88,6 +114,42 @@ pub async fn mount_container(
anti_leak_list: Option<&Path>,
stealth: bool,
rebuild_mac: bool,
) -> Result<()> {
mount_container_with_backend(
container_path,
drive_letter,
mount_point,
requested_port,
auth,
open_explorer,
enable_tray,
idle_timeout,
lock_on_screen_lock,
anti_leak,
anti_leak_list,
stealth,
rebuild_mac,
MountBackend::Auto,
)
.await
}
/// Startet das Sanctum-VFS mit expliziter Auswahl des Mount-Backends (`Auto`, `CloudFiles`, `WebDav`).
pub async fn mount_container_with_backend(
container_path: &Path,
drive_letter: char,
mount_point: Option<&Path>,
requested_port: Option<u16>,
auth: ContainerAuth,
open_explorer: bool,
enable_tray: bool,
idle_timeout: Option<u64>,
lock_on_screen_lock: bool,
anti_leak: bool,
anti_leak_list: Option<&Path>,
stealth: bool,
rebuild_mac: bool,
backend: MountBackend,
) -> Result<()> {
let drive_str = format_drive(drive_letter);
let _ = &drive_str;
@@ -95,6 +157,7 @@ pub async fn mount_container(
let _ = open_explorer;
let _ = enable_tray;
let _ = lock_on_screen_lock;
let _ = backend;
if !container_path.exists() {
bail!(
@@ -346,15 +409,19 @@ pub async fn mount_container(
if !stealth {
#[cfg(windows)]
ui::step(
4,
4,
"🌐",
&format!(
"Starte WebDAV (Port {}) & binde Netzlaufwerk {} ein...",
bound_port, drive_str
),
);
{
let step_msg = match backend {
MountBackend::CloudFiles => format!(
"Initialisiere Cloud-Files Userland-Engine (cldapi.dll) & binde Laufwerk {} ein...",
drive_str
),
_ => format!(
"Starte WebDAV (Port {}) & binde Laufwerk {} ein...",
bound_port, drive_str
),
};
ui::step(4, 4, "🌐", &step_msg);
}
#[cfg(not(windows))]
ui::step(
4,
@@ -377,13 +444,80 @@ pub async fn mount_container(
shutdown_rx,
));
// Netzlaufwerk bzw. Verzeichnis einbinden
if let Err(e) = run_mount_command(drive_letter, bound_port, &session_token) {
let _ = fs_clone.sync_carrier_manifest();
let _ = shutdown_tx.send(true);
let _ = server_handle.await;
return Err(e);
// Netzlaufwerk bzw. Cloud-Files-Laufwerk einbinden
let mut cf_session: Option<crate::cloudfiles::CloudFilesSession> = None;
#[cfg(windows)]
{
match backend {
MountBackend::CloudFiles => {
match crate::cloudfiles::CloudFilesSession::mount(drive_letter, fs_clone.clone())
.await
{
Ok(s) => cf_session = Some(s),
Err(e) => {
let _ = fs_clone.sync_carrier_manifest();
let _ = shutdown_tx.send(true);
let _ = server_handle.await;
return Err(e);
}
}
}
MountBackend::WebDav => {
if let Err(e) = run_mount_command(drive_letter, bound_port, &session_token) {
let _ = fs_clone.sync_carrier_manifest();
let _ = shutdown_tx.send(true);
let _ = server_handle.await;
return Err(e);
}
}
MountBackend::Auto => {
if let Err(wnet_err) = run_mount_command(drive_letter, bound_port, &session_token) {
if !stealth {
println!(
" {} Windows WebClient nicht verfügbar ({}) — schalte nahtlos auf Userland Cloud-Files-Engine (cldapi.dll) um...",
ui::yellow("[i]"),
wnet_err
.to_string()
.lines()
.next()
.unwrap_or("WebClient deaktiviert")
);
}
match crate::cloudfiles::CloudFilesSession::mount(
drive_letter,
fs_clone.clone(),
)
.await
{
Ok(s) => cf_session = Some(s),
Err(cf_err) => {
let _ = fs_clone.sync_carrier_manifest();
let _ = shutdown_tx.send(true);
let _ = server_handle.await;
bail!(
"Weder WebClient ({}) noch Cloud-Files-Engine ({}) konnten Laufwerk {} einbinden.",
wnet_err,
cf_err,
drive_str
);
}
}
}
}
}
}
#[cfg(not(windows))]
{
if let Err(e) = run_mount_command(drive_letter, bound_port, &session_token) {
let _ = fs_clone.sync_carrier_manifest();
let _ = shutdown_tx.send(true);
let _ = server_handle.await;
return Err(e);
}
}
let using_cloudfiles = cf_session.is_some();
let _ = using_cloudfiles;
// Optional automatisch im Windows Explorer öffnen (visuelle Parität für Decoy und Hidden Vault)
#[cfg(windows)]
@@ -450,6 +584,24 @@ pub async fn mount_container(
}
}
// Monitor für externes `sanctum unmount --drive <LETTER>` bei Cloud-Files DOS-Device
#[cfg(windows)]
let (ext_unmount_tx, mut ext_unmount_rx) = tokio::sync::mpsc::channel::<()>(1);
#[cfg(windows)]
if using_cloudfiles {
let dl = drive_letter;
tokio::spawn(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(1));
loop {
interval.tick().await;
if !crate::windows::is_dos_device_mounted(dl) {
let _ = ext_unmount_tx.send(()).await;
break;
}
}
});
}
// Windows-Sitzungssperre (Win + L Auto-Lock, nur Windows)
#[cfg(windows)]
let (session_lock_tx, mut session_lock_rx) = tokio::sync::mpsc::channel::<()>(1);
@@ -479,10 +631,19 @@ pub async fn mount_container(
println!();
println!(" • Container: {}", container_path.display());
#[cfg(windows)]
println!(
" • Netzlaufwerk: {} (im Windows Explorer bereit)",
ui::cyan(&drive_str)
);
{
if using_cloudfiles {
println!(
" • Laufwerk: {} (Cloud-Files Userland-Engine cldapi.dll, ohne WebClient)",
ui::cyan(&drive_str)
);
} else {
println!(
" • Netzlaufwerk: {} (im Windows Explorer bereit)",
ui::cyan(&drive_str)
);
}
}
#[cfg(not(windows))]
{
if let Some(mp) = mount_point {
@@ -569,7 +730,7 @@ pub async fn mount_container(
#[cfg(unix)]
let mut sighup = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup()).ok();
// Warten auf Beendigungssignal (Ctrl+C, Tray-Klick, Inaktivität, Win+L, Konsolenfenster-Schließen)
// Warten auf Beendigungssignal (Ctrl+C, Tray-Klick, Inaktivität, Win+L, Konsolenfenster-Schließen, externes Unmount)
#[cfg(windows)]
tokio::select! {
res = tokio::signal::ctrl_c() => {
@@ -603,6 +764,12 @@ pub async fn mount_container(
println!(" {} Inaktivitäts-Timeout erreicht — Auto-Lock ausgelöst!", ui::yellow("[!]"));
}
}
Some(()) = ext_unmount_rx.recv() => {
if !stealth {
println!();
println!(" {} Externer Unmount-Befehl für {} erkannt — schließe Container sicher ab!", ui::yellow("[!]"), drive_str);
}
}
}
#[cfg(unix)]
@@ -662,8 +829,12 @@ pub async fn mount_container(
}
if stealth {
if let Some(mut cf) = cf_session.take() {
let _ = cf.shutdown().await;
} else {
let _ = unmount_drive(drive_letter);
}
let _ = fs_clone.sync_carrier_manifest();
let _ = unmount_drive(drive_letter);
let _ = shutdown_tx.send(true);
let _ = server_handle.await;
if let Ok(freelist) = db.freelist_count() {
@@ -677,23 +848,37 @@ pub async fn mount_container(
#[cfg(not(windows))]
println!("Sanctum: Container geschlossen und WebDAV-Server beendet.");
} else {
let _ = fs_clone.sync_carrier_manifest();
#[cfg(windows)]
{
print!(
" {} Trenne Windows-Netzlaufwerk {} ... ",
ui::dim("[-]"),
drive_str
);
let _ = std::io::Write::flush(&mut std::io::stdout());
// Automatisches Unmount
if let Err(e) = unmount_drive(drive_letter) {
println!("{}", ui::yellow(&format!("Warnung ({e})")));
if let Some(mut cf) = cf_session.take() {
print!(
" {} Synchronisiere & schreddere Cloud-Files-Staging für {} ... ",
ui::dim("[-]"),
drive_str
);
let _ = std::io::Write::flush(&mut std::io::stdout());
if let Err(e) = cf.shutdown().await {
println!("{}", ui::yellow(&format!("Warnung ({e})")));
} else {
println!("{}", ui::green("OK"));
}
} else {
println!("{}", ui::green("OK"));
print!(
" {} Trenne Windows-Netzlaufwerk {} ... ",
ui::dim("[-]"),
drive_str
);
let _ = std::io::Write::flush(&mut std::io::stdout());
// Automatisches Unmount
if let Err(e) = unmount_drive(drive_letter) {
println!("{}", ui::yellow(&format!("Warnung ({e})")));
} else {
println!("{}", ui::green("OK"));
}
}
}
let _ = fs_clone.sync_carrier_manifest();
#[cfg(not(windows))]
{
print!(" {} Beende WebDAV-Server ... ", ui::dim("[-]"));
+343 -4
View File
@@ -581,6 +581,11 @@ unsafe extern "system" fn console_ctrl_routine(ctrl_type: u32) -> i32 {
drive_str.encode_utf16().chain(std::iter::once(0)).collect();
unsafe {
WNetCancelConnection2W(wide_drive.as_ptr(), 0, 1);
DefineDosDeviceW(
0x0000_0002, /* DDD_REMOVE_DEFINITION */
wide_drive.as_ptr(),
std::ptr::null(),
);
}
}
}
@@ -722,9 +727,338 @@ pub fn unlock_memory(ptr: *const u8, len: usize) -> bool {
}
}
#[cfg(windows)]
extern "system" {
fn DefineDosDeviceW(
dw_flags: u32,
lp_device_name: *const u16,
lp_target_path: *const u16,
) -> i32;
fn QueryDosDeviceW(lp_device_name: *const u16, lp_target_path: *mut u16, ucch_max: u32) -> u32;
}
/// Prüft ohne Administratorrechte über den Windows Service Control Manager,
/// ob der Dienst `WebClient` deaktiviert (`SERVICE_DISABLED = 4`) oder bereits aktiv ist.
/// Rückgabe: `(is_running, is_disabled)`
#[cfg(windows)]
pub fn query_webclient_service_state() -> (bool, bool) {
#[repr(C)]
struct SERVICE_STATUS {
dw_service_type: u32,
dw_current_state: u32,
dw_controls_accepted: u32,
dw_win32_exit_code: u32,
dw_service_specific_exit_code: u32,
dw_check_point: u32,
dw_wait_hint: u32,
}
#[repr(C)]
struct QUERY_SERVICE_CONFIGW {
dw_service_type: u32,
dw_start_type: u32,
dw_error_control: u32,
lp_binary_path_name: *mut u16,
lp_load_order_group: *mut u16,
dw_tag_id: u32,
lp_dependencies: *mut u16,
lp_service_start_name: *mut u16,
lp_display_name: *mut u16,
}
#[link(name = "advapi32")]
extern "system" {
fn OpenSCManagerW(
lp_machine_name: *const u16,
lp_database_name: *const u16,
dw_desired_access: u32,
) -> isize;
fn OpenServiceW(
h_sc_manager: isize,
lp_service_name: *const u16,
dw_desired_access: u32,
) -> isize;
fn QueryServiceStatus(h_service: isize, lp_service_status: *mut SERVICE_STATUS) -> i32;
fn QueryServiceConfigW(
h_service: isize,
lp_service_config: *mut QUERY_SERVICE_CONFIGW,
cb_buf_size: u32,
pcb_bytes_needed: *mut u32,
) -> i32;
fn CloseServiceHandle(h_sc_object: isize) -> i32;
}
const SC_MANAGER_CONNECT: u32 = 0x0001;
const SERVICE_QUERY_CONFIG: u32 = 0x0001;
const SERVICE_QUERY_STATUS: u32 = 0x0004;
const SERVICE_RUNNING: u32 = 4;
const SERVICE_DISABLED: u32 = 4;
unsafe {
let scm = OpenSCManagerW(std::ptr::null(), std::ptr::null(), SC_MANAGER_CONNECT);
if scm == 0 {
return (false, false);
}
let svc_name: Vec<u16> = "WebClient\0".encode_utf16().collect();
let svc = OpenServiceW(
scm,
svc_name.as_ptr(),
SERVICE_QUERY_STATUS | SERVICE_QUERY_CONFIG,
);
if svc == 0 {
CloseServiceHandle(scm);
return (false, true);
}
let mut status: SERVICE_STATUS = std::mem::zeroed();
let is_running =
QueryServiceStatus(svc, &mut status) != 0 && status.dw_current_state == SERVICE_RUNNING;
let mut bytes_needed: u32 = 0;
let _ = QueryServiceConfigW(svc, std::ptr::null_mut(), 0, &mut bytes_needed);
let mut is_disabled = false;
if bytes_needed > 0 {
let mut buf = vec![0u8; bytes_needed as usize];
let cfg_ptr = buf.as_mut_ptr() as *mut QUERY_SERVICE_CONFIGW;
if QueryServiceConfigW(svc, cfg_ptr, bytes_needed, &mut bytes_needed) != 0 {
if (*cfg_ptr).dw_start_type == SERVICE_DISABLED {
is_disabled = true;
}
}
}
CloseServiceHandle(svc);
CloseServiceHandle(scm);
(is_running, is_disabled)
}
}
/// Weckt den Windows-Dienst `WebClient` aus dem reinen Userland (ohne Administratorrechte)
/// über seinen offiziellen Microsoft ETW-Service-Trigger (`22b6d684-fa63-4578-87c9-effcbe6643c7`) auf.
#[cfg(windows)]
pub fn trigger_webclient_service_etw() -> bool {
#[repr(C)]
struct GUID {
data1: u32,
data2: u16,
data3: u16,
data4: [u8; 8],
}
#[repr(C)]
struct EVENT_DESCRIPTOR {
id: u16,
version: u8,
channel: u8,
level: u8,
opcode: u8,
task: u16,
keyword: u64,
}
#[link(name = "advapi32")]
extern "system" {
fn EventRegister(
provider_id: *const GUID,
enable_callback: *const std::ffi::c_void,
callback_context: *const std::ffi::c_void,
reg_handle: *mut u64,
) -> u32;
fn EventWrite(
reg_handle: u64,
event_descriptor: *const EVENT_DESCRIPTOR,
user_data_count: u32,
user_data: *const std::ffi::c_void,
) -> u32;
fn EventUnregister(reg_handle: u64) -> u32;
}
let (running, disabled) = query_webclient_service_state();
if running {
return true;
}
if disabled {
return false;
}
// Offizielle Microsoft ETW-Trigger-GUID für WebClient (MRxDAV): 22b6d684-fa63-4578-87c9-effcbe6643c7
let webclient_trigger_guid = GUID {
data1: 0x22b6_d684,
data2: 0xfa63,
data3: 0x4578,
data4: [0x87, 0xc9, 0xef, 0xfc, 0xbe, 0x66, 0x43, 0xc7],
};
unsafe {
let mut reg_handle: u64 = 0;
if EventRegister(
&webclient_trigger_guid,
std::ptr::null(),
std::ptr::null(),
&mut reg_handle,
) == 0
{
let desc = EVENT_DESCRIPTOR {
id: 1,
version: 0,
channel: 0,
level: 0,
opcode: 0,
task: 0,
keyword: 0,
};
let _ = EventWrite(reg_handle, &desc, 0, std::ptr::null());
let _ = EventUnregister(reg_handle);
}
}
// Warte kurz (max. 1.5s), bis der SCM den Dienst auf SERVICE_RUNNING geschaltet hat
for _ in 0..15 {
std::thread::sleep(std::time::Duration::from_millis(100));
let (now_running, _) = query_webclient_service_state();
if now_running {
return true;
}
}
false
}
/// Bindet ein lokales Verzeichnis als Laufwerksbuchstaben im aktuellen Benutzersitzungs-Namensraum
/// via `DefineDosDeviceW` ein (erfordert keine Administratorrechte und keinen WebClient-Dienst).
#[cfg(windows)]
pub fn mount_dos_device(drive_letter: char, target_dir: &std::path::Path) -> Result<()> {
use std::os::windows::ffi::OsStrExt;
let drive_str = format!("{}:", drive_letter.to_ascii_uppercase());
let wide_drive: Vec<u16> = drive_str.encode_utf16().chain(std::iter::once(0)).collect();
let wide_target: Vec<u16> = target_dir
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();
// Bereinige ggf. verwaiste Zuordnung auf demselben Buchstaben
if is_dos_device_mounted(drive_letter) {
unsafe {
DefineDosDeviceW(0x0000_0002, wide_drive.as_ptr(), std::ptr::null());
}
}
let ok = unsafe { DefineDosDeviceW(0, wide_drive.as_ptr(), wide_target.as_ptr()) };
if ok == 0 {
bail!(
"DefineDosDeviceW konnte Laufwerk {} nicht auf '{}' zuweisen",
drive_str,
target_dir.display()
);
}
// Benachrichtige die Windows Shell (Explorer) über das neue Laufwerk (SHCNE_DRIVEADD)
notify_shell_drive_change(drive_letter, true);
Ok(())
}
/// Entfernt eine über `DefineDosDeviceW` erstellte Laufwerkszuordnung.
#[cfg(windows)]
pub fn unmount_dos_device(drive_letter: char, target_dir: Option<&std::path::Path>) -> Result<()> {
use std::os::windows::ffi::OsStrExt;
const DDD_REMOVE_DEFINITION: u32 = 0x0000_0002;
const DDD_EXACT_MATCH_ON_REMOVE: u32 = 0x0000_0004;
let drive_str = format!("{}:", drive_letter.to_ascii_uppercase());
let wide_drive: Vec<u16> = drive_str.encode_utf16().chain(std::iter::once(0)).collect();
let ok = if let Some(dir) = target_dir {
let wide_target: Vec<u16> = dir
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();
unsafe {
DefineDosDeviceW(
DDD_REMOVE_DEFINITION | DDD_EXACT_MATCH_ON_REMOVE,
wide_drive.as_ptr(),
wide_target.as_ptr(),
)
}
} else {
unsafe { DefineDosDeviceW(DDD_REMOVE_DEFINITION, wide_drive.as_ptr(), std::ptr::null()) }
};
if ok == 0 {
// Fallback ohne exakten Pfad
let _ = unsafe {
DefineDosDeviceW(DDD_REMOVE_DEFINITION, wide_drive.as_ptr(), std::ptr::null())
};
}
notify_shell_drive_change(drive_letter, false);
Ok(())
}
/// Prüft, ob ein Laufwerksbuchstabe aktuell als DOS-Device existiert.
#[cfg(windows)]
pub fn is_dos_device_mounted(drive_letter: char) -> bool {
let drive_str = format!("{}:", drive_letter.to_ascii_uppercase());
let wide_drive: Vec<u16> = drive_str.encode_utf16().chain(std::iter::once(0)).collect();
let mut buf = [0u16; 512];
let len = unsafe { QueryDosDeviceW(wide_drive.as_ptr(), buf.as_mut_ptr(), buf.len() as u32) };
len > 0
}
#[cfg(not(windows))]
pub fn is_dos_device_mounted(_drive_letter: char) -> bool {
false
}
#[cfg(windows)]
fn notify_shell_drive_change(drive_letter: char, added: bool) {
extern "system" {
fn SHChangeNotify(
w_event_id: i32,
u_flags: u32,
dw_item1: *const std::ffi::c_void,
dw_item2: *const std::ffi::c_void,
);
}
const SHCNE_DRIVEADD: i32 = 0x0000_0100;
const SHCNE_DRIVEREMOVED: i32 = 0x0000_0080;
const SHCNF_PATHW: u32 = 0x0005;
let root_str = format!("{}:\\", drive_letter.to_ascii_uppercase());
let wide_root: Vec<u16> = root_str.encode_utf16().chain(std::iter::once(0)).collect();
let event = if added {
SHCNE_DRIVEADD
} else {
SHCNE_DRIVEREMOVED
};
unsafe {
SHChangeNotify(
event,
SHCNF_PATHW,
wide_root.as_ptr() as *const std::ffi::c_void,
std::ptr::null(),
);
}
}
/// Bindet ein Windows-Netzlaufwerk über WNetAddConnection2W ein (in-process, ohne argv-Token-Leak).
#[cfg(windows)]
pub fn mount_drive_wnet(drive_letter: char, port: u16, session_token: &str) -> Result<()> {
// Versuche vorab den WebClient-Dienst per Userland-ETW-Trigger aufzuwecken, falls er nur gestoppt (Manual) ist
let (running, disabled) = query_webclient_service_state();
if disabled {
bail!(
"Der Windows-Dienst 'WebClient' wurde durch die Systemadministration deaktiviert (StartType = Disabled)."
);
}
if !running {
debug!("WebClient-Dienst ist gestoppt — sende Userland-ETW-Wakeup-Trigger...");
let _ = trigger_webclient_service_etw();
}
let drive_str = format!("{}:", drive_letter.to_ascii_uppercase());
let mut local_name: Vec<u16> = drive_str.encode_utf16().chain(std::iter::once(0)).collect();
let remote_url = format!("http://127.0.0.1:{}/", port);
@@ -771,13 +1105,13 @@ pub fn mount_drive_wnet(drive_letter: char, port: u16, session_token: &str) -> R
}
if res != 0 {
let webclient_hint = if res == 67 {
"\n\nHinweis: Das Einbinden von Netzlaufwerken erfordert den Windows-Dienst 'WebClient'. Prüfen Sie in einer Administrator-Konsole: 'net start WebClient'."
let webclient_hint = if res == 67 || res == 1244 {
"\n\nHinweis: Das Einbinden von WebDAV-Netzlaufwerken erfordert den Windows-Dienst 'WebClient'."
} else {
""
};
bail!(
"Laufwerk {} konnte nicht eingebunden werden (Win32 Fehlercode {}){}",
"Laufwerk {} konnte nicht über WebClient eingebunden werden (Win32 Fehlercode {}){}",
drive_str,
res,
webclient_hint
@@ -787,7 +1121,7 @@ pub fn mount_drive_wnet(drive_letter: char, port: u16, session_token: &str) -> R
Ok(())
}
/// Trennt ein eingebundenes Windows-Netzlaufwerk via WNetCancelConnection2W (in-process).
/// Trennt ein eingebundenes Windows-Netzlaufwerk via WNetCancelConnection2W oder Cloud-Files DOS-Device (in-process).
#[cfg(windows)]
pub fn unmount_drive_wnet(drive_letter: char) -> Result<()> {
let drive_str = format!("{}:", drive_letter.to_ascii_uppercase());
@@ -797,6 +1131,11 @@ pub fn unmount_drive_wnet(drive_letter: char) -> Result<()> {
WNetCancelConnection2W(local_name.as_ptr(), 0, 1 /* fForce = TRUE */)
};
// Falls kein WNet-Laufwerk verbunden war (2250 = ERROR_NOT_CONNECTED), prüfe auf DefineDosDeviceW (Cloud Files)
if res == 2250 && is_dos_device_mounted(drive_letter) {
return unmount_dos_device(drive_letter, None);
}
// 0 = NO_ERROR, 2250 = ERROR_NOT_CONNECTED (bereits getrennt)
if res != 0 && res != 2250 {
bail!(