Skip to content

Add RDP & VNC remote desktop support (in-browser + native VNC) with session recording#1987

Open
theharold wants to merge 15 commits into
warp-tech:mainfrom
theharold:feat/rdp-vnc-desktop
Open

Add RDP & VNC remote desktop support (in-browser + native VNC) with session recording#1987
theharold wants to merge 15 commits into
warp-tech:mainfrom
theharold:feat/rdp-vnc-desktop

Conversation

@theharold

Copy link
Copy Markdown

Summary

Adds remote desktop support to Warpgate — addressing #894 (RDP) and extending it to VNC — plus session recording for those sessions.

  • VNC works two ways, like SSH:
    • Native — connect a standard VNC viewer to Warpgate's VeNCrypt + TLS port and authenticate with a user:target username and your Warpgate password (reuses the existing auth state / ticket flow, exactly like the MySQL/Postgres proxies).
    • In-browser — open the desktop straight from the web portal (canvas renderer over a WebSocket).
  • RDP is accessible in the browser through the web portal, rendered server-side and streamed to a canvas.
  • Session recording for in-browser desktop sessions (RDP + VNC) with playback and live viewing in the admin UI, alongside the existing SSH / database / Kubernetes recordings.

How it works

  • New warpgate-protocol-vnc (native VeNCrypt listener + backend client) and warpgate-protocol-rdp (subprocess bridge) crates. Both normalise their backend into a shared DesktopEvent stream consumed by a new warpgate-web-desktop crate (mirrors warpgate-web-ssh) and rendered by gateway/WebDesktop.svelte.
  • RDP is built as a standalone helper binary, warpgate-rdp-helper, intentionally kept outside the cargo workspace: IronRDP's CredSSP stack (sspi/picky) exact-pins RustCrypto pre-release crates that conflict irreconcilably with russh's pins in a single lockfile (no russh/IronRDP version pair aligns them, and [patch] can't fix =-vs-= clashes). The helper has its own lockfile and talks to Warpgate over stdio — the same isolation approach Apache Guacamole uses with guacd.
  • Recording captures the normalised framebuffer event stream as timestamped JSONL (RecordingKind::Desktop); the admin player replays it on a canvas and can tail a live session.

Limitations / notes

  • RDP is browser-only for now; a native RDP acceptor (NLA server for mstsc) is future work.
  • The native VNC proxy is a transparent relay and is not recorded (it never decodes the framebuffer) — only in-browser desktop sessions are recorded.
  • The RDP helper is built separately (cd warpgate-rdp-helper && cargo build) and located at runtime via PATH or the WARPGATE_RDP_HELPER env var.
  • Desktop-recording seek replays from the start (no keyframe index yet); the RDP helper does not yet verify the target's TLS certificate.
  • docs-drafts/desktop-recording.md contains ready-to-paste content for the docs site.

Trying it out

  1. Build: cargo build --workspace; the RDP helper cd warpgate-rdp-helper && cargo build; frontend cd warpgate-web && yarn && yarn build.
  2. Add a VNC and/or RDP target in the admin UI. For the native VNC listener, set vnc.enable + a TLS cert/key in config.
  3. Browser: click the target in the user portal → the desktop opens in a canvas.
  4. Native VNC: point a VeNCrypt-capable viewer (e.g. TigerVNC) at the VNC port, username user:target.
  5. Recording: set recordings.enable: true, start a session, then open it under the session in the admin UI to play back / watch live.

Closes #894

theharold added 8 commits May 30, 2026 20:15
Add the VNC protocol to the core data model so it can be configured and
stored, ahead of the protocol implementation:

- TargetVncOptions + VncTargetAuth (None/Password) and TargetOptions::Vnc
- TargetKind::Vnc and the From<&TargetOptions> mapping
- VncConfig (enable/listen/external_port/external_host) on WarpgateConfigStore
- vnc field on UserRequireCredentialsPolicy + per-protocol policy wiring
- vnc in PortsInfo/ExternalHostsInfo info API
- default VNC port/listen (5900)
- warpgate-core: protocol-agnostic DesktopEvent/DesktopInput/DesktopRect/
  DesktopState types shared by all graphical protocols (VNC, later RDP) and
  by the native + in-browser paths
- warpgate-protocol-vnc: backend VNC client built on vnc-rs that connects to
  a target, authenticates, decodes the framebuffer to normalised DesktopEvents
  (BGRA), and forwards DesktopInput as RFB input events
- warpgate-web-desktop crate (mirrors warpgate-web-ssh): WebDesktopClientManager,
  session with reconnect buffer, WS protocol (JSON control + base64 framebuffer),
  and ws_handler. Reuses the warpgate-protocol-vnc backend client and normalised
  DesktopEvent/DesktopInput stream
- warpgate-protocol-http: /web-desktop/sessions REST API (authorize_target +
  per-user ownership checks) and the /web-desktop/sessions/:id/stream WebSocket
  route, wired alongside the existing web-ssh endpoints
- admin: VNC option in ChooseTargetKind/CreateTarget, vnc/Options.svelte editor
  (host/port/auth) wired into Target.svelte
- gateway: route VNC targets to a new in-browser desktop viewer
  (WebDesktop.svelte) that renders the framebuffer to a <canvas> over the
  /web-desktop WebSocket and forwards pointer/keyboard input
- regenerate admin + gateway OpenAPI schemas for the new VNC target options
  and web-desktop endpoints
Lets native VNC viewers connect through Warpgate (like the SSH proxy):
- VncProtocolServer (ProtocolServer) + run_server, gated on config.store.vnc.enable
- Warpgate acts as an RFB server offering only VeNCrypt X509Plain: the viewer
  authenticates over TLS with a full-length user:target username + password,
  reusing the standard auth_state_store/validate_credential/authorize_target
  flow (and the ticket selector), exactly like the MySQL/Postgres proxies
- backend RFB client handshake (None or VNC Auth via DES) to the target, then
  a transparent bidirectional relay
- VncConfig gains certificate/key (for the VeNCrypt TLS cert); config-schema.json
  regenerated
Mirror of the VNC foundation for RDP:
- TargetRdpOptions (host/port/username/domain) + RdpTargetAuth (password) and
  TargetOptions::Rdp
- TargetKind::Rdp and the From<&TargetOptions> mapping
- RdpConfig (enable/listen/external_port/external_host/certificate/key) on the
  config store
- rdp field on UserRequireCredentialsPolicy + per-protocol policy wiring
- rdp in PortsInfo/ExternalHostsInfo
- default RDP port/listen (3389); regenerated config + OpenAPI schemas
…owser path

IronRDP's CredSSP stack (sspi/picky) exact-pins RustCrypto pre-release crates
that conflict irreconcilably with russh's pins in a shared lockfile (no russh/
ironrdp version pair aligns; [patch] can't fix =-vs-= clashes). Resolved by
building RDP as a standalone helper binary with its own lockfile (the guacd
model):

- warpgate-rdp-helper: standalone binary (excluded from the workspace) that
  connects to an RDP target via IronRDP (TLS + NLA/CredSSP), decodes the
  framebuffer, and speaks line-delimited JSON over stdio (config in, framebuffer
  out as base64 BGRA, input incl. mouse + keysym->scancode/Unicode keyboard)
- warpgate-protocol-rdp: in-workspace crate (no IronRDP deps) that spawns the
  helper and bridges its stdio to the shared DesktopEvent/DesktopInput streams
- web-desktop manager dispatches VNC and RDP targets uniformly
- admin RDP target editor + portal routing to the in-browser desktop viewer

Native RDP acceptor (NLA server for native mstsc clients) is future work.
Add session recording for in-browser desktop (RDP/VNC) sessions, reusing the
existing recording infrastructure:

- RecordingKind::Desktop + DesktopRecorder (warpgate-core/src/recordings/desktop.rs):
  serialises the normalised DesktopEvent framebuffer stream as timestamped JSONL
  whose shape mirrors the web-desktop ServerMessage; DesktopRecordingMetadata
  carries protocol (vnc/rdp) + target
- web-desktop manager taps every DesktopEvent into the recorder before pushing to
  the browser; recording is a no-op when disabled
- admin API: GET /recordings/:id/desktop (streams the file) and a live
  WS /recordings/:id/desktop-stream, mounted alongside the existing routes
- frontend: shared common/desktopCanvas.ts renderer (extracted from WebDesktop,
  reused by both live client and player); DesktopRecordingPlayer.svelte with
  play/pause/seek + live tail; wired into Recording.svelte + recordings.ts
- README + docs-drafts/desktop-recording.md (external-site content)

Limitations: only in-browser sessions are recorded (native VNC proxy is a
transparent relay and can't be decoded); seeking replays from the start.
@theharold

Copy link
Copy Markdown
Author

🧪 Call for testers!

This is a sizeable feature spanning new protocols and a graphical pipeline, and I'd really appreciate extra eyes and hands-on testing before merge. If you have an RDP or VNC target handy, please give it a spin and report back:

  • VNC in the browser — add a VNC target, open it from the user portal.
  • Native VNC — connect a VeNCrypt/TLS-capable viewer (e.g. TigerVNC) to the VNC port with a user:target username.
  • RDP in the browser — add an RDP target (build the warpgate-rdp-helper first), open it from the portal. Reports on different Windows/xrdp versions and NLA setups are especially valuable.
  • Recording & playback — set recordings.enable: true, run a desktop session, then play it back and try the live view in the admin UI.

Feedback on input handling (keyboard layouts/modifiers, mouse), rendering correctness, performance, and the auth/routing flows would all be very welcome. Bug reports with the target type/version + Warpgate logs are 🙏. Thanks!

@theharold theharold mentioned this pull request May 31, 2026
Comment thread warpgate-protocol-vnc/src/server/rfb.rs Fixed
…support

- deny.toml: allow the Unlicense license scoped to async_io_stream (a permissive
  public-domain dependency pulled in transitively by vnc-rs), which was failing
  cargo-deny's license check
- check-schema-compatibility: add an oasdiff severity-levels file that treats
  additive response changes (new oneOf union members / new enum values) as
  informational rather than breaking. Adding RDP/VNC target options and the
  Desktop recording kind only *adds* variants, which is backward-compatible for
  the lockstep-generated client
- rfb.rs: document that the DES usage is mandated by the RFB VNC Authentication
  security type (challenge-response only, not used for confidentiality), to
  explain the accepted CodeQL weak-crypto finding
@theharold

Copy link
Copy Markdown
Author

Pushed 5b9d2d0 to address the failing checks:

  • cargo-deny (licenses)vnc-rs pulls async_io_stream, which is released under the Unlicense (a permissive public-domain dedication). Added a scoped [[licenses.exceptions]] for that one crate in deny.toml (rather than allowing Unlicense globally).
  • check-schema-compatibility — adding the RDP/VNC TargetOptions variants and the Desktop recording kind only adds members to response oneOf unions / enums, which oasdiff flags as breaking under --fail-on WARN. These are backward-compatible for Warpgate (the typed client is regenerated in lockstep and discriminates on kind), so I added an oasdiff-severity.txt that downgrades response-property-one-of-added / response-property-enum-value-added to informational, and wired --severity-levels into the workflow. Verified locally with tufin/oasdiff against main (both admin and gateway now pass). Happy to instead scope this differently if you'd prefer.
  • CodeQL (DES) — replied on the inline alert: it's the protocol-mandated VNC Authentication challenge-response (not used for confidentiality; the session is TLS-wrapped). Documented in code; would appreciate a maintainer dismissing that alert.

Unrelated follow-up noted in the description: the RDP helper currently skips target-certificate verification — I'll make that configurable in a follow-up.

@Eugeny

Eugeny commented Jun 1, 2026

Copy link
Copy Markdown
Member

Just letting you know that I saw this, massive work - but will take me at least until next week to start reviewing 🤝

@theharold

Copy link
Copy Markdown
Author

Thank you @Eugeny
appreciate your effort, please feel free to make comments as appropriate and I'll try to address those. Appreciate there might be a lot as this is a massive feature.

@chris-morris-h2o

Copy link
Copy Markdown

I do have a branch I made privately to try to do RDP, it works with native RDP, not the web browser and doesn't support VNC. It also does recording. I was working on cleaning it up to submit as a PR when I saw this already open today.

I very much want to get native RDP working, not just through the web browser. Should I wait for this to be merged in and build off of this foundation for native RDP usage?

Comment thread warpgate-protocol-vnc/src/server/rfb.rs Fixed
for (i, b) in password.bytes().take(8).enumerate() {
key[i] = b.reverse_bits();
}
let cipher = Des::new(&key.into());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RDP

4 participants