Files
netdesk/native/host/src/privacy.rs
T

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");
}