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

+1 -1
View File
@@ -57,7 +57,7 @@ Die gesamte Genese dieses Projekts (Audit-Behebung aller 39 Findings, Carrier V2
| [`RELEASE_PROCESS.md`](RELEASE_PROCESS.md) | Multi-Platform CI/CD Release-Architektur, Toolchain-Pins und Minisign-Signierung | | [`RELEASE_PROCESS.md`](RELEASE_PROCESS.md) | Multi-Platform CI/CD Release-Architektur, Toolchain-Pins und Minisign-Signierung |
| [`QUICKSTART.md`](QUICKSTART.md) | Kompakte Schritt-für-Schritt-Anleitung für Endanwender | | [`QUICKSTART.md`](QUICKSTART.md) | Kompakte Schritt-für-Schritt-Anleitung für Endanwender |
| [`INSTALL.md`](INSTALL.md) | Installationsanweisungen (Binaries, Scoop, WinGet, Cargo) | | [`INSTALL.md`](INSTALL.md) | Installationsanweisungen (Binaries, Scoop, WinGet, Cargo) |
| [`CHANGELOG.md`](CHANGELOG.md) | Keep-a-Changelog Versionshistorie (aktuell: v0.9.5) | | [`CHANGELOG.md`](CHANGELOG.md) | Keep-a-Changelog Versionshistorie (aktuell: v0.9.6) |
| [`deny.toml`](deny.toml) | Strikte Konfiguration für `cargo-deny` (Advisories, Bans, Lizenzen, Quellen) | | [`deny.toml`](deny.toml) | Strikte Konfiguration für `cargo-deny` (Advisories, Bans, Lizenzen, Quellen) |
| [`.gitea/workflows/release.yaml`](.gitea/workflows/release.yaml) | Automatisierte CI/CD Release-Pipeline für Windows & Linux musl | | [`.gitea/workflows/release.yaml`](.gitea/workflows/release.yaml) | Automatisierte CI/CD Release-Pipeline für Windows & Linux musl |
+17
View File
@@ -5,6 +5,23 @@ Alle nennenswerten Änderungen an diesem Projekt werden in dieser Datei dokument
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/) Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/)
und dieses Projekt folgt den Richtlinien von [Semantic Versioning](https://semver.org/lang/de/). und dieses Projekt folgt den Richtlinien von [Semantic Versioning](https://semver.org/lang/de/).
## [0.9.6] - 2026-10-21
### Windows Cloud Files Engine (`cldapi.dll`) & Userland-Fallback ohne `WebClient`
Dieses Release macht `sanctum mount` unter Windows unabhängig vom Windows-Dienst `WebClient` (`mrxdav.sys`), sodass Container auch dann als regulärer Laufwerksbuchstabe (z. B. `S:` oder `Z:`) im Windows Explorer eingebunden werden können, wenn `WebClient` durch Gruppenrichtlinien oder die Systemadministration deaktiviert (`Disabled`) wurde – komplett im Userland und ohne Administratorrechte:
- **Native Windows Cloud Files API (`src/cloudfiles.rs`)**:
- Verbindet `SanctumFs` direkt mit der in Windows 10/11 integrierten Cloud-Filter-Engine (`cldapi.dll` / `cldflt.sys`) und weist das Staging-Verzeichnis via `DefineDosDeviceW` (`DDD_NO_BROADCAST_SYSTEM` + `SHChangeNotify`) dem gewünschten oder automatisch gewählten freien Laufwerksbuchstaben (`--drive` bzw. `Z:` abwärts) zu.
- **0-Byte Placeholders**: Beim Einbinden werden mittels `CfCreatePlaceholders` ausschließlich 0-Byte-Platzhalterdateien angelegt; es befindet sich beim Start kein entschlüsselter Dateiinhalt auf dem Host-Datenträger.
- **On-Demand Hydration (`CF_CALLBACK_TYPE_FETCH_DATA`)**: Erst beim Öffnen/Lesen einer Datei entschlüsselt Sanctum die angeforderten 1-MB-Chunks live aus `SanctumFs` (regulärer Tresor sowie versteckter `CarrierFs`-Tresor), überträgt sie via `CfExecute` an den Kernel und nullt den temporären Puffer (`Zeroizing`).
- **Bidirektionale Synchronisation, Auto-Dehydration & CSPRNG-Shredding**:
- Neue, geänderte oder gelöschte Dateien und Ordner auf dem Laufwerk werden unter Einhaltung des **Anti-Leak Shields** und des **Carrier-Schutzes** laufend in den verschlüsselten Container zurückgeschrieben.
- Nicht mehr genutzte hydrierte Dateien werden nach Inaktivität automatisch via `CfDehydratePlaceholder` wieder auf 0 Bytes dehydriert.
- Beim Aushängen (`unmount`, `Ctrl+C`, Sitzungssperre oder Idle-Timeout) werden etwaige auf Disk liegende Dateibytes mit kryptografischem Zufallsrauschen (`OsRng`) überschrieben (geshreddet) und das Staging-Verzeichnis samt Laufwerksbuchstaben restlos entfernt.
- **Dreistufiges Mount-Backend (`--backend auto|cloudfiles|webdav`) & ETW-Service-Trigger**:
- `auto` (Standard): Prüft ohne Administratorrechte den Status des Dienstes `WebClient`. Ist er lediglich gestoppt (`Manual`), weckt Sanctum ihn automatisch per Userland-ETW-Trigger (`22b6d684-fa63-4578-87c9-effcbe6643c7`) auf. Ist `WebClient` administrativ deaktiviert (`Disabled`) oder schlägt `WNetAddConnection2W` fehl, wechselt Sanctum nahtlos auf die `CloudFilesSession` auf demselben Laufwerksbuchstaben.
- `cloudfiles`: Erzwingt das direkte Einbinden über `cldapi.dll` + `DefineDosDeviceW` ohne `WebClient` (z. B. auch zur Umgehung des 4-GB-Limits von `WebClient`).
- `webdav`: Erzwingt das klassische Einbinden über `WNetAddConnection2W`.
## [0.9.5] - 2026-09-22 ## [0.9.5] - 2026-09-22
### Transcript-Version & Upgrade-MAC (F-04, F-05) ### Transcript-Version & Upgrade-MAC (F-04, F-05)
Generated
+1 -1
View File
@@ -1457,7 +1457,7 @@ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]] [[package]]
name = "sanctum" name = "sanctum"
version = "0.9.5" version = "0.9.6"
dependencies = [ dependencies = [
"aes-gcm", "aes-gcm",
"anyhow", "anyhow",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "sanctum" name = "sanctum"
version = "0.9.5" version = "0.9.6"
edition = "2021" edition = "2021"
authors = ["Harald Pansi <harald@pansi.eu>", "Sanctum Engineering Team"] authors = ["Harald Pansi <harald@pansi.eu>", "Sanctum Engineering Team"]
description = "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV" description = "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV"
+3
View File
@@ -102,6 +102,9 @@ sanctum.exe mount --path "C:\Pfad\tresor.sanctum"
# Bestimmten Laufwerksbuchstaben erzwingen: # Bestimmten Laufwerksbuchstaben erzwingen:
sanctum.exe mount --path "C:\Pfad\tresor.sanctum" --drive S sanctum.exe mount --path "C:\Pfad\tresor.sanctum" --drive S
# Mount-Backend wählen (auto | cloudfiles | webdav; Standard: auto mit Cloud-Files-Fallback falls WebClient deaktiviert ist):
sanctum.exe mount --path "C:\Pfad\tresor.sanctum" --backend cloudfiles
# Mit Inaktivitäts-Timeout (in Sekunden): # Mit Inaktivitäts-Timeout (in Sekunden):
sanctum.exe mount --path "C:\Pfad\tresor.sanctum" --idle-timeout 300 sanctum.exe mount --path "C:\Pfad\tresor.sanctum" --idle-timeout 300
+1 -1
View File
@@ -10,7 +10,7 @@ Sicherheitsupdates und Patches werden jeweils für die neueste Version von Sanct
| Version | Status | Sicherheits-Support | | Version | Status | Sicherheits-Support |
|:---:|:---:|:---:| |:---:|:---:|:---:|
| **v0.9.x** | **Aktiv (Aktuell: v0.9.5)** | **Vollständig unterstützt** | | **v0.9.x** | **Aktiv (Aktuell: v0.9.6)** | **Vollständig unterstützt** |
| <= v0.8.x | Veraltet | Nicht mehr unterstützt (Upgrade empfohlen) | | <= v0.8.x | Veraltet | Nicht mehr unterstützt (Upgrade empfohlen) |
--- ---
+3 -3
View File
@@ -1,12 +1,12 @@
{ {
"version": "0.9.5", "version": "0.9.6",
"description": "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV", "description": "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV",
"homepage": "https://gitea.pansi.eu/harald/sanctum", "homepage": "https://gitea.pansi.eu/harald/sanctum",
"license": "MIT", "license": "MIT",
"architecture": { "architecture": {
"64bit": { "64bit": {
"url": "https://gitea.pansi.eu/harald/sanctum/releases/download/v0.9.5/sanctum-v0.9.5-windows-x86_64.zip", "url": "https://gitea.pansi.eu/harald/sanctum/releases/download/v0.9.6/sanctum-v0.9.6-windows-x86_64.zip",
"hash": "f47d43e455a73d6ed0affa7af07437b82c7f61188aade9b2a46cbb4ea43968fc", "hash": "f5236b24d0c8c645364e1c5d3ca9c503518646c879163cedabe160029db9a099",
"bin": "sanctum.exe" "bin": "sanctum.exe"
} }
}, },
+4 -4
View File
@@ -1,6 +1,6 @@
# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.6.0.schema.json # yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.6.0.schema.json
PackageIdentifier: HaraldPansi.Sanctum PackageIdentifier: HaraldPansi.Sanctum
PackageVersion: 0.9.5 PackageVersion: 0.9.6
PackageName: Sanctum PackageName: Sanctum
Publisher: Harald Pansi Publisher: Harald Pansi
PublisherUrl: https://gitea.pansi.eu/harald PublisherUrl: https://gitea.pansi.eu/harald
@@ -18,7 +18,7 @@ Tags:
- webdav - webdav
- container - container
- plausible-deniability - plausible-deniability
ReleaseNotesUrl: https://gitea.pansi.eu/harald/sanctum/releases/tag/v0.9.5 ReleaseNotesUrl: https://gitea.pansi.eu/harald/sanctum/releases/tag/v0.9.6
Installers: Installers:
- Architecture: x64 - Architecture: x64
InstallerType: zip InstallerType: zip
@@ -26,7 +26,7 @@ Installers:
NestedInstallerFiles: NestedInstallerFiles:
- RelativeFilePath: sanctum.exe - RelativeFilePath: sanctum.exe
PortableCommandAlias: sanctum PortableCommandAlias: sanctum
InstallerUrl: https://gitea.pansi.eu/harald/sanctum/releases/download/v0.9.5/sanctum-v0.9.5-windows-x86_64.zip InstallerUrl: https://gitea.pansi.eu/harald/sanctum/releases/download/v0.9.6/sanctum-v0.9.6-windows-x86_64.zip
InstallerSha256: f47d43e455a73d6ed0affa7af07437b82c7f61188aade9b2a46cbb4ea43968fc InstallerSha256: f5236b24d0c8c645364e1c5d3ca9c503518646c879163cedabe160029db9a099
ManifestType: singleton ManifestType: singleton
ManifestVersion: 1.6.0 ManifestVersion: 1.6.0
+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 carrier;
pub mod cloudfiles;
pub mod crypto; pub mod crypto;
pub mod mount; pub mod mount;
pub mod pathutil; 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, mnemonic_to_dek, set_allow_legacy_names, validate_password, wrap_slot0_payload,
wrap_slot1_payload, KdfParams, FORMAT_VERSION, FORMAT_VERSION_V3, 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::{ use sanctum::recovery::{
detect_recovery_key_slot, export_header_backup, restore_header_backup, detect_recovery_key_slot, export_header_backup, restore_header_backup,
restore_slot_from_recovery_key, restore_slot_from_recovery_key,
@@ -121,6 +123,10 @@ enum Commands {
/// Autorisiert den Neuaufbau des Metadaten-MAC nach einem Header-Restore (F-02) /// Autorisiert den Neuaufbau des Metadaten-MAC nach einem Header-Restore (F-02)
#[arg(long, default_value_t = false)] #[arg(long, default_value_t = false)]
rebuild_mac: bool, 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 /// Trennt ein eingebundenes Netzlaufwerk manuell
@@ -1648,7 +1654,9 @@ async fn run() -> Result<()> {
anti_leak_list, anti_leak_list,
stealth, stealth,
rebuild_mac, rebuild_mac,
backend,
} => { } => {
let mount_backend = MountBackend::from_str_opt(&backend)?;
let drive_char = match drive { let drive_char = match drive {
Some(ref d) => parse_drive_letter(d)?, Some(ref d) => parse_drive_letter(d)?,
None => { None => {
@@ -1688,7 +1696,7 @@ async fn run() -> Result<()> {
let enable_tray = !no_tray && !stealth; let enable_tray = !no_tray && !stealth;
let lock_on_screen_lock = !no_screen_lock; let lock_on_screen_lock = !no_screen_lock;
let anti_leak = !no_anti_leak; let anti_leak = !no_anti_leak;
mount_container( mount_container_with_backend(
&path, &path,
drive_char, drive_char,
mount_point.as_deref(), mount_point.as_deref(),
@@ -1702,6 +1710,7 @@ async fn run() -> Result<()> {
anti_leak_list.as_deref(), anti_leak_list.as_deref(),
stealth, stealth,
rebuild_mac, rebuild_mac,
mount_backend,
) )
.await?; .await?;
} }
+218 -33
View File
@@ -29,6 +29,32 @@ pub enum ContainerAuth {
RecoveryKey(Zeroizing<String>), 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:") /// Hilfsfunktion zur Formatierung des Laufwerksbuchstabens (z. B. 'S' -> "S:")
pub fn format_drive(drive_letter: char) -> String { pub fn format_drive(drive_letter: char) -> String {
format!("{}:", drive_letter.to_ascii_uppercase()) format!("{}:", drive_letter.to_ascii_uppercase())
@@ -88,6 +114,42 @@ pub async fn mount_container(
anti_leak_list: Option<&Path>, anti_leak_list: Option<&Path>,
stealth: bool, stealth: bool,
rebuild_mac: 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<()> { ) -> Result<()> {
let drive_str = format_drive(drive_letter); let drive_str = format_drive(drive_letter);
let _ = &drive_str; let _ = &drive_str;
@@ -95,6 +157,7 @@ pub async fn mount_container(
let _ = open_explorer; let _ = open_explorer;
let _ = enable_tray; let _ = enable_tray;
let _ = lock_on_screen_lock; let _ = lock_on_screen_lock;
let _ = backend;
if !container_path.exists() { if !container_path.exists() {
bail!( bail!(
@@ -346,15 +409,19 @@ pub async fn mount_container(
if !stealth { if !stealth {
#[cfg(windows)] #[cfg(windows)]
ui::step( {
4, let step_msg = match backend {
4, MountBackend::CloudFiles => format!(
"🌐", "Initialisiere Cloud-Files Userland-Engine (cldapi.dll) & binde Laufwerk {} ein...",
&format!( drive_str
"Starte WebDAV (Port {}) & binde Netzlaufwerk {} ein...", ),
bound_port, drive_str _ => format!(
), "Starte WebDAV (Port {}) & binde Laufwerk {} ein...",
); bound_port, drive_str
),
};
ui::step(4, 4, "🌐", &step_msg);
}
#[cfg(not(windows))] #[cfg(not(windows))]
ui::step( ui::step(
4, 4,
@@ -377,13 +444,80 @@ pub async fn mount_container(
shutdown_rx, shutdown_rx,
)); ));
// Netzlaufwerk bzw. Verzeichnis einbinden // Netzlaufwerk bzw. Cloud-Files-Laufwerk einbinden
if let Err(e) = run_mount_command(drive_letter, bound_port, &session_token) { let mut cf_session: Option<crate::cloudfiles::CloudFilesSession> = None;
let _ = fs_clone.sync_carrier_manifest(); #[cfg(windows)]
let _ = shutdown_tx.send(true); {
let _ = server_handle.await; match backend {
return Err(e); 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) // Optional automatisch im Windows Explorer öffnen (visuelle Parität für Decoy und Hidden Vault)
#[cfg(windows)] #[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) // Windows-Sitzungssperre (Win + L Auto-Lock, nur Windows)
#[cfg(windows)] #[cfg(windows)]
let (session_lock_tx, mut session_lock_rx) = tokio::sync::mpsc::channel::<()>(1); let (session_lock_tx, mut session_lock_rx) = tokio::sync::mpsc::channel::<()>(1);
@@ -479,10 +631,19 @@ pub async fn mount_container(
println!(); println!();
println!(" • Container: {}", container_path.display()); println!(" • Container: {}", container_path.display());
#[cfg(windows)] #[cfg(windows)]
println!( {
" • Netzlaufwerk: {} (im Windows Explorer bereit)", if using_cloudfiles {
ui::cyan(&drive_str) 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))] #[cfg(not(windows))]
{ {
if let Some(mp) = mount_point { if let Some(mp) = mount_point {
@@ -569,7 +730,7 @@ pub async fn mount_container(
#[cfg(unix)] #[cfg(unix)]
let mut sighup = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup()).ok(); 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)] #[cfg(windows)]
tokio::select! { tokio::select! {
res = tokio::signal::ctrl_c() => { 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("[!]")); 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)] #[cfg(unix)]
@@ -662,8 +829,12 @@ pub async fn mount_container(
} }
if stealth { 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 _ = fs_clone.sync_carrier_manifest();
let _ = unmount_drive(drive_letter);
let _ = shutdown_tx.send(true); let _ = shutdown_tx.send(true);
let _ = server_handle.await; let _ = server_handle.await;
if let Ok(freelist) = db.freelist_count() { if let Ok(freelist) = db.freelist_count() {
@@ -677,23 +848,37 @@ pub async fn mount_container(
#[cfg(not(windows))] #[cfg(not(windows))]
println!("Sanctum: Container geschlossen und WebDAV-Server beendet."); println!("Sanctum: Container geschlossen und WebDAV-Server beendet.");
} else { } else {
let _ = fs_clone.sync_carrier_manifest();
#[cfg(windows)] #[cfg(windows)]
{ {
print!( if let Some(mut cf) = cf_session.take() {
" {} Trenne Windows-Netzlaufwerk {} ... ", print!(
ui::dim("[-]"), " {} Synchronisiere & schreddere Cloud-Files-Staging für {} ... ",
drive_str ui::dim("[-]"),
); drive_str
let _ = std::io::Write::flush(&mut std::io::stdout()); );
let _ = std::io::Write::flush(&mut std::io::stdout());
// Automatisches Unmount if let Err(e) = cf.shutdown().await {
if let Err(e) = unmount_drive(drive_letter) { println!("{}", ui::yellow(&format!("Warnung ({e})")));
println!("{}", ui::yellow(&format!("Warnung ({e})"))); } else {
println!("{}", ui::green("OK"));
}
} else { } 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))] #[cfg(not(windows))]
{ {
print!(" {} Beende WebDAV-Server ... ", ui::dim("[-]")); 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(); drive_str.encode_utf16().chain(std::iter::once(0)).collect();
unsafe { unsafe {
WNetCancelConnection2W(wide_drive.as_ptr(), 0, 1); 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). /// Bindet ein Windows-Netzlaufwerk über WNetAddConnection2W ein (in-process, ohne argv-Token-Leak).
#[cfg(windows)] #[cfg(windows)]
pub fn mount_drive_wnet(drive_letter: char, port: u16, session_token: &str) -> Result<()> { 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 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 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); 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 { if res != 0 {
let webclient_hint = if res == 67 { let webclient_hint = if res == 67 || res == 1244 {
"\n\nHinweis: Das Einbinden von Netzlaufwerken erfordert den Windows-Dienst 'WebClient'. Prüfen Sie in einer Administrator-Konsole: 'net start WebClient'." "\n\nHinweis: Das Einbinden von WebDAV-Netzlaufwerken erfordert den Windows-Dienst 'WebClient'."
} else { } else {
"" ""
}; };
bail!( bail!(
"Laufwerk {} konnte nicht eingebunden werden (Win32 Fehlercode {}){}", "Laufwerk {} konnte nicht über WebClient eingebunden werden (Win32 Fehlercode {}){}",
drive_str, drive_str,
res, res,
webclient_hint webclient_hint
@@ -787,7 +1121,7 @@ pub fn mount_drive_wnet(drive_letter: char, port: u16, session_token: &str) -> R
Ok(()) 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)] #[cfg(windows)]
pub fn unmount_drive_wnet(drive_letter: char) -> Result<()> { pub fn unmount_drive_wnet(drive_letter: char) -> Result<()> {
let drive_str = format!("{}:", drive_letter.to_ascii_uppercase()); 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 */) 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) // 0 = NO_ERROR, 2250 = ERROR_NOT_CONNECTED (bereits getrennt)
if res != 0 && res != 2250 { if res != 0 && res != 2250 {
bail!( bail!(