A high-performance, web-native RDP client built with Rust and the IronRDP ecosystem. This project implements a lightweight agent that runs directly on target machines, providing a smooth HTML5 experience with minimal latency.
Traditional web-based RDP solutions often rely on heavy server-side proxies (like Guacamole) that decode RDP traffic, re-encode it into image streams (MJPEG/PNG), and send pixels to the browser. This introduces significant CPU overhead and latency.
This solution is different because:
- In-Browser Decoding: The entire RDP protocol state machine and graphics decoding run in the browser via WebAssembly (WASM).
- Transparent Proxy: The Rust server acts only as a simple WebSocket-to-TCP relay. It does zero PDU inspection or pixel re-encoding.
- Zero-Copy Rendering: Decoded pixels are written directly to the HTML5 Canvas using
putImageData, achieving native-like performance. - Security: Credential negotiation (NLA/CredSSP) is handled client-side using the
sspicrate in WASM.
graph LR
subgraph Browser
JS[Web UI] --- WASM[IronRDP WASM]
WASM --- Canvas[HTML5 Canvas]
end
WASM <== WebSocket / Binary ==> Proxy[Rust Axum Server]
Proxy <== TCP ==> RDP[localhost:3389]
server/: The backend proxy built withAxumandTokio.wasm/: The core RDP logic usingironrdp. Compiles to WebAssembly.web/: The frontend UI (Vanilla JavaScript + CSS).scripts/: Automated build scripts for Windows and Ubuntu.
- Rust: Latest stable version.
- Wasm-pack: For building the WASM module.
cargo install wasm-pack - Wasm-opt: (Optional but recommended) for optimizing WASM binary size. Usually bundled with wasm-pack.
Use the provided build script for your platform:
Windows:
.\scripts\build-windows.ps1Ubuntu/Linux:
./scripts/build-ubuntu.shThe server serves both the static web files and the WebSocket proxy.
.\target\release\server.exe --port 8080 --rdp-target localhost:3389Open http://localhost:8080 in any modern browser. Enter your credentials and enjoy a high-performance RDP session.
- PDU Framing: Since WebSockets are message-based but RDP is stream-oriented, we implement custom framing in
wasm/src/framed.rsto extract TPKT (X224) and FastPath packets. - Connector Sequence: The
ironrdp-connectorstate machine drives the handshake through initiation, security upgrades (TLS/CredSSP), and capability negotiation. - Active Session: Once connected, the
ActiveStageprocesses incoming graphics PDUs, updating a local framebuffer which is then rendered bycanvas.rs. - Input Handling: Keyboard and mouse events are captured in JS, converted to AT-101 scancodes, and sent to WASM to be encoded as RDP input PDUs.
Based on challenges faced during the initial implementation:
- File Access Denied (os error 32): Frequently caused by Windows Defender or
rust-analyzerlocking files in thetarget/directory during high-intensity compilation (especially duringwasm-pack). Try closing your IDE or disabling real-time scanning for the project folder. - WASM Memory Limit: If the WASM module fails to load, ensure you aren't initializing multiple large framebuffers. We use a single shared buffer for the canvas.
- WebSocket Disconnection: Ensure the
--rdp-targetis reachable from the server. If targeting a remote Windows machine, check firewall rule 3389. - Blank Screen: Headless browser environments (like testing tools) may not render the UI correctly if using
backdrop-filter: blur(). Verify in a physical browser. - Credentials/NLA: If connection fails during the "CredSSP" state, verify that NLA is correctly configured on the target machine.
- Axum Router: Axum 0.8+ no longer supports
nest_serviceat the root path. The server usesfallback_serviceto correctly handle static assets alongside the/wsroute. - Web-Sys Features: Several DOM APIs used by the client (like
ClipboardorCssStyleDeclaration) require explicit feature flags inwasm/Cargo.toml.
The CredSSP implementation uses a proxy-mediated TLS approach:
sequenceDiagram
participant Browser as WASM Client
participant Proxy as Server Proxy
participant RDP as RDP Server
Browser->>Proxy: X.224 Connection Request (via WS)
Proxy->>RDP: X.224 Connection Request (via TCP)
RDP->>Proxy: X.224 Connection Confirm (HYBRID)
Proxy->>Browser: X.224 Connection Confirm
Note over Browser: Connector β EnhancedSecurityUpgrade
Browser->>Proxy: {"cmd":"tls_upgrade"} (WS Text)
Proxy->>RDP: TLS Handshake (TCPβTLS)
RDP->>Proxy: TLS Established
Proxy->>Browser: {"cmd":"tls_ready","server_cert":"<hex>"} (WS Text)
Note over Browser: Connector β CredSSP
loop NTLM Rounds (2-3)
Browser->>Proxy: TSRequest (NTLM Token) (WS Binary)
Proxy->>RDP: TSRequest (via TLS)
RDP->>Proxy: TSRequest (Challenge) (via TLS)
Proxy->>Browser: TSRequest (WS Binary)
end
Browser->>Proxy: TSRequest (Final + auth_info) (WS Binary)
Proxy->>RDP: TSRequest (via TLS)
Note over Browser: Connector β BasicSettingsExchange
Note over Browser,RDP: Normal RDP session (all traffic via TLS tunnel)
| Component | Status | Notes |
|---|---|---|
WASM (--release) |
β Exit 0 | 3 warnings (dead code, unused doc comment) |
Server (--release) |
β Exit 0 | Clean |
Full build-windows.ps1 |
β Exit 0 | End-to-end verified |
- Build:
.\scripts\build-windows.ps1 - Run:
.\target\release\server.exe --port 8080 --rdp-target localhost:3389 - Connect via browser:
http://localhost:8080 - Enter username/password/domain β click Connect
- Expected console logs (if NLA is enabled on target):
Security upgrade β requesting TLS from proxy... TLS upgrade complete β server cert: XXX bytes CredSSP: starting NTLM authentication... CredSSP round 1 CredSSP round 2 CredSSP round 3 CredSSP: handshake complete CredSSP: authentication successful! Connector state: BasicSettingsExchangeSendInitial ... RDP connected!
Note
If the RDP target has NLA disabled, the connector will skip directly to BasicSettingsExchange (no TLS upgrade or CredSSP occurs). The code handles both paths.