Files
punktfunk/docs/windows-secure-desktop.md
T
enricobuehler 140209bbfc feat(host/windows): two-process secure-desktop step 5 — DDA mux on Winlogon
`virtual_stream_relay` now muxes the AU source by input desktop. A DesktopWatcher
(SYSTEM-only Winlogon-name poll) drives it: the user-session WGC helper relay
feeds the normal (Default) desktop; the host's OWN DDA capturer+encoder — opened
lazily on the first secure transition, on the same SudoVDA target with a no-op
keepalive (the host still holds the real isolation owner) — captures the secure
(Winlogon: UAC/lock/login) desktop that WGC can't see. Every switch latches
"wait for IDR" and forces the now-active source to emit a keyframe (the two
encoders keep independent infinite-GOP state, so the client must resume on an
IDR); returning to the helper also drains its stale buffered AUs first.
Reconfigure drops the stale-target DDA; keyframe requests route to the live
source. Send path (FEC/seal/paced-send) unchanged.

Also: wgc_relay gains try_recv (drain on switch-back); open_dda takes dims as
args (avoids a closure borrow of the reassigned cur_mode); the forward! macro
returns bool with `break 'outer` at the call site (no in-macro label hygiene).

cfg-gated windows-only. Live validation (UAC switch over a session) pending.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 07:55:29 +00:00

107 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Windows secure-desktop capture — two-process design
Status: **steps 15 implemented (compiles on the 4090); live validation pending.** The WGC animation
fix ships and works (host in user-mode); this doc is the plan for adding **secure-desktop (UAC / lock
/ login) coverage** on top of it, since WGC and the secure desktop need conflicting process tokens.
Implemented so far:
- **Step 1 — DesktopWatcher** (`capture/desktop_watch.rs`): polls the input-desktop name → atomic
`Default`/`Winlogon`. Committed `80e222d`.
- **Step 3 — WGC helper subcommand** (`wgc_helper.rs`, `m3-host wgc-helper`): WGC→NVENC→framed AUs on
stdout, stdin keyframe control. Committed `a0f6cdd`.
- **Step 4 — spawn + relay** (`capture/wgc_relay.rs`, `m3::virtual_stream_relay`): SYSTEM host spawns
the helper via `CreateProcessAsUserW` into `winsta0\default`, relays its stdout AUs to the QUIC send
thread, forwards keyframe requests, surfaces helper stderr in host tracing. Committed `9f50b39`.
- **Step 5 — source mux** (`m3::virtual_stream_relay`): the DesktopWatcher switches the AU source —
helper relay on `Default`, the host's own DDA capturer+encoder on `Winlogon`; every switch latches
"wait for IDR" + forces the now-active source to emit a keyframe.
Remaining: **step 6** (helper relaunch watchdog on console connect/disconnect + crash, then a
lock/unlock+UAC soak) and **step 2** (SendInput retry-on-failure refactor — input works today via the
existing path; this hardens it across the desktop boundary).
Live validation to run when the box is up (single session, host as SYSTEM via the `-s -i 1` scheduled
task): connect a client → confirm video via the helper relay on the normal desktop (host log
`source switch … normal(WGC relay)` + `WGC helper spawned`), trigger a UAC prompt → the stream shows
the UAC dialog (host log `source switch … secure(DDA)`), dismiss → back to the helper; the QUIC
session stays up throughout.
## The constraint (verified live on the RTX 4090)
- **WGC** (the composed-desktop capture that fixes frozen HDR animations) **will not activate under
the SYSTEM account** — `CreateForMonitor``0x80070424`. Thread-level `ImpersonateLoggedOnUser` is
**insufficient** (tested: `impersonated=true`, still `0x80070424`). WGC needs the *process* to run
as the interactive user.
- **DDA + SendInput on the secure desktop (Winlogon: UAC/lock/login) require LOCAL_SYSTEM** (attach to
the Winlogon desktop). This is already shipped (task #17) when the host runs as SYSTEM.
- Therefore one process can't do both. Single-process (the simpler design) is **out**.
## Architecture: SYSTEM host + USER-session WGC helper, AU-relay (no shared GPU texture)
- **SYSTEM host** (the existing `m3-host`, launched as SYSTEM in interactive Session 1 via the
scheduled task → PsExec `-s -i 1`): owns the punktfunk/1 QUIC session, the single SudoVDA virtual
output (+ isolate/restore RAII — the *only* topology owner), the **DDA capture + NVENC encoder for
the secure desktop**, the **single SendInput injector** (serves *both* desktops), and the **AU
source mux** that feeds the QUIC data plane.
- **USER-session WGC helper** (a new `m3-host` subcommand, spawned by the SYSTEM host via
`WTSQueryUserToken(activeConsoleSessionId)``DuplicateTokenEx(TokenPrimary)`
`CreateProcessAsUserW(lpDesktop="winsta0\\default", CREATE_NO_WINDOW)`): runs the existing
**WGC → scRGB/PQ → NVENC** pipeline and ships **Annex-B AUs** (`{data, pts_ns, keyframe}`) to the
SYSTEM host over a **named pipe**. It captures the SAME SudoVDA output **by GDI name only** — it
must NOT create its own virtual output / touch display topology (a second topology owner re-triggers
the ACCESS_LOST born-lost storm).
- **Mux**: the SYSTEM host relays the helper's AUs onto QUIC while the input desktop is `Default`
(normal — WGC, HDR/animation-correct), and switches to its own DDA encoder while it's `Winlogon`
(secure — UAC/lock/login). The client sees one continuous stream; the encoder/FEC/AES-GCM/QUIC send
path is untouched (same `EncodedFrame` flow). NVENC re-inits only on a size/format change across the
swap (already handled); same-mode is a pointer re-register.
- **Input**: stays entirely in the SYSTEM host (only it can attach to Winlogon). One windowless
SendInput thread, Sunshine's **retry-on-failure-only** model (cache HDESK thread-local; SendInput
first; only on 0-injected re-`OpenInputDesktop`+`SetThreadDesktop` and retry once) — serves both
desktops with no per-event reattach. (Ctrl+Alt+Del/SAS needs `SendSAS`, out of scope; clicking UAC
Yes/No + typing the login password are plain SendInput on Winlogon.)
Rejected: a shared NT-handle GPU texture (MIC/SDDL pain SYSTEM→user, keyed-mutex ring at 240 Hz,
nvenc pointer-cache churn — all for a static lock dialog). AU bytes over a pipe are far simpler.
## Detection
`DesktopWatcher`: a dedicated thread polling the input-desktop NAME at 3060 Hz —
`OpenInputDesktop(0,FALSE,0)` + `GetUserObjectInformationW(UOI_NAME)` == `"Winlogon"` (secure) vs
`"Default"` (normal) → `Arc<AtomicU8>`. This is the authoritative signal; WTS session notifications
miss UAC entirely. (May also register `WTSRegisterSessionNotification` to short-circuit lock/unlock.)
## Implementation steps (each independently buildable/testable on the 4090)
1. **DesktopWatcher** (`capture/desktop_watch.rs`, ~40 lines): the poll + atomic. Test: lock / trigger
UAC over the existing stream, confirm the atomic flips `Default↔Winlogon` within a poll interval.
2. **SendInput retry-on-failure model** (`inject/sendinput.rs`): replace per-event reattach with the
cached-HDESK + retry-once model. Test: normal input unchanged; click UAC + type the lock password
land (works today via per-event reattach — this is a refactor).
3. **WGC helper subcommand** (`m3-host wgc-helper` or similar): the existing WGC pipeline → NVENC →
Annex-B AUs over a named-pipe server. Test standalone: as the user it writes a valid `.h265` to the
pipe (capturing the SudoVDA output by GDI name, no topology changes).
4. **Spawn + relay**: SYSTEM host spawns the helper (`CreateProcessAsUserW`), connects the pipe,
relays its AUs onto the live QUIC session. Test: normal-desktop stream sourced via the helper relay.
5. **Source mux**: relay helper AUs while `Default`, switch to the host's own DDA encoder while
`Winlogon` (reusing the DesktopWatcher). Test: normal (WGC, HDR) → trigger UAC → stream shows the
UAC dialog (DDA) → dismiss → back to WGC; QUIC session stays up throughout. **Full-coverage milestone.**
6. **Relaunch watchdog + soak**: `SERVICE_CONTROL_SESSIONCHANGE`-style relaunch of the helper on
console connect/disconnect; soak a few hundred lock/unlock+UAC switches (cf. task #17's 1012-switch
run) — no leak / black / disconnect. Cargo features for the fallback: `Win32_System_Threading`,
`Win32_System_Pipes`, `Win32_System_RemoteDesktop`.
## Risks / notes
- Validate on the real 4090 only (`ssh "Enrico Bühler"@192.168.1.174`, Session 1 via the Interactive
scheduled task) — the headless build VM can't reproduce Winlogon-on-virtual-display or WGC.
- The helper MUST capture the SudoVDA by GDI name and never create a second virtual output (avoids the
ACCESS_LOST born-lost storm — one isolate owner = the SYSTEM host).
- Confirm `reisolate` fires on a FRESH mid-session DDA open at the desktop boundary (task #17 only
validated DDA recovery within an already-DDA session).
- Brief one-frame repeat/flicker at the WGC↔DDA boundary is acceptable (the local lock/UAC transition
flickers too); never starve the encoder (repeat last frame across the swap gap).
- Pragmatic alternative if full coverage isn't worth the build: `PromptOnSecureDesktop=0` (UAC renders
on the normal desktop → WGC captures it) covers UAC (not lock/login) with one reversible registry
change.