132 lines
5.0 KiB
Rust
132 lines
5.0 KiB
Rust
//! Driver-free privacy mode. A fullscreen, topmost, opaque-black overlay marked
|
|
//! `WDA_EXCLUDEFROMCAPTURE`, so:
|
|
//! - the LOCAL monitor(s) show solid black — a bystander at the host sees
|
|
//! nothing of what the remote operator is doing;
|
|
//! - the host's OWN screen capture (DXGI Desktop Duplication + the GDI BitBlt
|
|
//! fallback) EXCLUDES the overlay, so the remote operator still sees and
|
|
//! controls the real desktop behind it.
|
|
//! The overlay is also click-through + non-activating (WS_EX_TRANSPARENT |
|
|
//! WS_EX_NOACTIVATE), so the operator's injected SendInput passes straight
|
|
//! through to the real windows; blocking the LOCAL keyboard/mouse is the
|
|
//! separate `block_input` toggle. No virtual-display driver is needed — verified
|
|
//! by the `netdesk-privacy-probe` bin (WDA_EXCLUDEFROMCAPTURE hid a window from
|
|
//! both DXGI and GDI capture, 197600 -> 0 magenta pixels).
|
|
//!
|
|
//! The overlay lives on its own thread with a message pump (a window must be
|
|
//! serviced on its creating thread); dropping [`Privacy`] tears it down and
|
|
//! restores the screen.
|
|
|
|
#![cfg(windows)]
|
|
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
use std::sync::Arc;
|
|
use std::thread::JoinHandle;
|
|
use std::time::Duration;
|
|
|
|
use windows::core::w;
|
|
use windows::Win32::Foundation::{COLORREF, HINSTANCE, HWND, LPARAM, LRESULT, WPARAM};
|
|
use windows::Win32::Graphics::Gdi::CreateSolidBrush;
|
|
use windows::Win32::UI::WindowsAndMessaging::{
|
|
CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, GetSystemMetrics,
|
|
PeekMessageW, RegisterClassW, SetLayeredWindowAttributes, SetWindowDisplayAffinity, ShowWindow,
|
|
TranslateMessage, LWA_ALPHA, MSG, PM_REMOVE, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN,
|
|
SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN, SW_SHOWNA, WDA_EXCLUDEFROMCAPTURE, WNDCLASSW,
|
|
WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TOPMOST, WS_EX_TRANSPARENT, WS_POPUP,
|
|
WS_VISIBLE,
|
|
};
|
|
|
|
unsafe extern "system" fn wndproc(h: HWND, m: u32, w: WPARAM, l: LPARAM) -> LRESULT {
|
|
DefWindowProcW(h, m, w, l)
|
|
}
|
|
|
|
/// A live privacy-mode blackout. Drop it to restore the screen.
|
|
pub struct Privacy {
|
|
stop: Arc<AtomicBool>,
|
|
thread: Option<JoinHandle<()>>,
|
|
}
|
|
|
|
impl Privacy {
|
|
/// Raise the blackout overlay (returns once the window thread has started).
|
|
pub fn start() -> Privacy {
|
|
let stop = Arc::new(AtomicBool::new(false));
|
|
let s2 = stop.clone();
|
|
let thread = std::thread::Builder::new()
|
|
.name("netdesk-privacy".into())
|
|
.spawn(move || unsafe { run(s2) })
|
|
.ok();
|
|
Privacy { stop, thread }
|
|
}
|
|
}
|
|
|
|
impl Drop for Privacy {
|
|
fn drop(&mut self) {
|
|
self.stop.store(true, Ordering::Relaxed);
|
|
if let Some(t) = self.thread.take() {
|
|
let _ = t.join();
|
|
}
|
|
}
|
|
}
|
|
|
|
unsafe fn run(stop: Arc<AtomicBool>) {
|
|
let hinst = HINSTANCE::default();
|
|
let brush = CreateSolidBrush(COLORREF(0x0000_0000)); // solid black
|
|
let class = w!("NdPrivacyBlackout");
|
|
let wc = WNDCLASSW {
|
|
lpfnWndProc: Some(wndproc),
|
|
hInstance: hinst,
|
|
lpszClassName: class,
|
|
hbrBackground: brush,
|
|
..Default::default()
|
|
};
|
|
// 0 if the class is already registered (a prior toggle this process) — fine.
|
|
let _ = RegisterClassW(&wc);
|
|
|
|
// Cover the whole virtual desktop (all monitors).
|
|
let x = GetSystemMetrics(SM_XVIRTUALSCREEN);
|
|
let y = GetSystemMetrics(SM_YVIRTUALSCREEN);
|
|
let cx = GetSystemMetrics(SM_CXVIRTUALSCREEN).max(1);
|
|
let cy = GetSystemMetrics(SM_CYVIRTUALSCREEN).max(1);
|
|
|
|
let hwnd = match CreateWindowExW(
|
|
WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_NOACTIVATE,
|
|
class,
|
|
w!("nd-privacy"),
|
|
WS_POPUP | WS_VISIBLE,
|
|
x,
|
|
y,
|
|
cx,
|
|
cy,
|
|
None,
|
|
None,
|
|
hinst,
|
|
None,
|
|
) {
|
|
Ok(h) => h,
|
|
Err(e) => {
|
|
log::warn!("privacy: window create failed: {e}");
|
|
return;
|
|
}
|
|
};
|
|
// Opaque (alpha 255) so the overlay is solid black on the physical screen.
|
|
let _ = SetLayeredWindowAttributes(hwnd, COLORREF(0), 255, LWA_ALPHA);
|
|
// Exclude from THIS host's own capture so the remote still sees the desktop.
|
|
if let Err(e) = SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE) {
|
|
log::warn!("privacy: SetWindowDisplayAffinity failed: {e}");
|
|
}
|
|
// Show without activating (keyboard focus stays on the real foreground app,
|
|
// so injected keystrokes still land there).
|
|
let _ = ShowWindow(hwnd, SW_SHOWNA);
|
|
log::info!("privacy: blackout up ({cx}x{cy} @ {x},{y}); excluded from capture, click-through");
|
|
|
|
let mut msg = MSG::default();
|
|
while !stop.load(Ordering::Relaxed) {
|
|
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
|
|
let _ = TranslateMessage(&msg);
|
|
DispatchMessageW(&msg);
|
|
}
|
|
std::thread::sleep(Duration::from_millis(30));
|
|
}
|
|
let _ = DestroyWindow(hwnd);
|
|
log::info!("privacy: blackout down; screen restored");
|
|
}
|