Share via

RDP DVC simple plugin – Rust

The Rust Simple implementation demonstrates a complete RDP DVC plugin system using Rust's windows crate and manual COM interface implementation:

  • Client Plugin: Out-of-process COM LocalServer built in Rust, implementing IWTSPlugin, IWTSListenerCallback, and IWTSVirtualChannelCallback using the windows crate's COM support
  • Server Application: Server-side DVC host running inside RDP sessions, with WTS API (synchronous) and File Handle (async overlapped) I/O modes
  • Shared Protocol: A minimal binary ping/echo protocol, wire-compatible with all other Simple samples

The server sends ping messages with a 5120-byte payload every second. The client plugin echoes them back. The server measures and logs round-trip time (RTT) for each ping.

Quick Facts

  • Channel: dvc::sample::simpleplugin
  • CLSID: {D9B80669-C06A-4BD1-9CB1-3B7168C9E3A3}
  • Client output: bin/<Platform>/<Configuration>/rdp-simple-plugin-client-rust.exe
  • Server output: bin/<Platform>/<Configuration>/rdp-simple-plugin-server-rust.exe
  • Register: client EXE /register (per-user); add /machine for machine-wide; /unregister to remove
  • Server modes: WTS API (default) or /filehandle
  • Interop: protocol compatible with all other Simple implementations

Contents

File/folder Description
rdp-simple-plugin-protocol/ Protocol definitions (Rust crate).
rdp-simple-plugin-client/ Client-side DVC plugin (COM LocalServer EXE).
rdp-simple-plugin-server/ Server-side application (EXE).
Cargo.toml Workspace configuration.
build.ps1 PowerShell build script.
README.md This README file.

Key Implementation Details

COM Implementation in Rust

The Rust client uses the windows crate to implement COM interfaces:

  • simple_class_factory.rs implements IClassFactory using the windows crate's implement macro
  • rdp_plugin.rs implements IWTSPlugin, IWTSListenerCallback, and IWTSVirtualChannelCallback as a single COM object
  • registry_helper.rs writes registry entries for LocalServer32 and AddIns using the windows crate's registry APIs
  • main.rs handles the --Embedding flag (COM out-of-process activation), calls CoRegisterClassObject, and runs a Windows message loop

Server I/O Modes

WTS API Mode (Default):

  • Synchronous reads via WTSVirtualChannelRead
  • Single-threaded send-receive loop
  • Simpler implementation; good for testing and low-throughput scenarios

File Handle Mode (/filehandle):

  • Asynchronous overlapped I/O
  • DvcReassembler (in dvc_reassembler.rs) handles PDU fragment reassembly

Rust-Specific Patterns

  • COM objects use Arc<Mutex<T>> for shared state across interface callbacks
  • The windows crate generates vtable wrappers; #[implement] handles QueryInterface, AddRef, Release
  • Registry access uses windows::Win32::System::Registry bindings directly (no external crate)
  • AtomicU32 for lock-free sequence counter in the server

Wire Protocol

Messages use a simple binary format (little-endian):

Frame: [frameLen:u32][sequenceNumber:u32][utcTicks:i64][payloadLen:u32][payload...]

Where:
- frameLen: Total size of the message (excluding this field)
- sequenceNumber: Incrementing sequence number for tracking
- utcTicks: UTC timestamp in 100ns ticks (Windows FILETIME)
- payloadLen: Size of the payload in bytes
- payload: Deterministic test pattern (0..255 repeating)

PDU Fragmentation

The default payload size is 5120 bytes, which exceeds the typical RDP channel chunk size (~1600 bytes). This demonstrates PDU fragmentation and reassembly:

  • Large messages are automatically fragmented by the RDP stack
  • Fragments are marked with CHANNEL_FLAG_FIRST and CHANNEL_FLAG_LAST flags
  • The DvcReassembler class handles reassembly for file-handle mode
  • WTS API mode (WTSVirtualChannelRead) handles reassembly automatically

See CHANNEL_PDU_HEADER for details on the fragmentation protocol.

Architecture

Client Plugin Flow

RDP Client
    │
    ├─> Reads AddIns registry for plugin CLSID {D9B80669-C06A-4BD1-9CB1-3B7168C9E3A3}
    │       └─> HKCU\Software\Microsoft\Terminal Server Client\Default\AddIns\SampleRdpSimplePlugin\Name
    │
    ├─> Calls CoCreateInstance with CLSID
    │       └─> COM looks up LocalServer32 registry path
    │           └─> HKCU\Software\Classes\CLSID\{...}\LocalServer32
    │
    ├─> COM SCM either:
    │       ├─> Connects to existing rdp-simple-plugin-client-rust.exe process, OR
    │       └─> Launches new process with registered executable path
    │
    ├─> Process calls CoRegisterClassObject to publish IClassFactory
    │
    ├─> COM calls IClassFactory::CreateInstance → returns IWTSPlugin
    │
    ├─> RDP client calls IWTSPlugin::Initialize
    │       └─> CreateListener("dvc::sample::simpleplugin")
    │
    ├─> Server opens channel from remote session
    │
    ├─> IWTSListenerCallback::OnNewChannelConnection
    │       └─> Creates per-channel callback instance
    │
    ├─> Server sends data via DVC
    │
    ├─> IWTSVirtualChannelCallback::OnDataReceived
    │       └─> Parse framed ping message
    │           └─> Echo all bytes back via IWTSVirtualChannel::Write
    │
    └─> IWTSPlugin::Terminated → cleanup

Server Application Flow

RDP Session (remote machine)
    │
    ├─> OpenChannel("dvc::sample::simpleplugin")
    │       └─> WTSVirtualChannelOpenEx with WTS_CHANNEL_OPTION_DYNAMIC
    │
    ├─> Send ping messages every 1 second
    │       └─> Build frame: [frameLen][seqNum][utcTicks][payloadLen][payload]
    │
    ├─> Receive echo responses
    │       └─> DvcReassembler handles PDU fragmentation
    │           └─> Parse complete message once reassembled
    │
    ├─> Calculate and log RTT
    │
    └─> Ctrl+C → CloseChannel, cleanup, exit

Protocol Message Flow

Server                                         Client Plugin
  │                                                 │
  ├─ BuildPingFrame(seq=1, payload=5120 bytes)      │
  ├─ WTSVirtualChannelWrite ──────────────────────> │
  │                                                 ├─ OnDataReceived
  │                                                 ├─ Parse PingMessage
  │                                                 └─ Echo bytes back
  │ <────────────────────────────────────────────── │
  ├─ Parse echo response                            │
  ├─ Calculate RTT                                  │
  └─ Log "Ping#1 payload=5120 rttMs=12.34"          │

Prerequisites

  • Rust stable toolchain (rustup from https://rustup.rs/)
  • Visual Studio 2022 with C++ workload (MSVC linker required)
  • Windows 10/11 or Windows Server 2019+
  • Windows SDK 10.0.17763.0 or later

Setup

Using PowerShell Script

# Build all projects (Debug, current platform)
.\build.ps1

# Install prerequisites (Rust toolchain)
.\build.ps1 -Install

# Build Release for x64
.\build.ps1 -Configuration Release -Platform x64

# Build for ARM64
.\build.ps1 -Platform ARM64 -Configuration Release

# Clean all outputs
.\build.ps1 -Clean

Using cargo CLI

# Build solution
cargo build

# Build release
cargo build --release

# Run tests
cargo test

Running the sample

1. Register the Client Plugin

Run on the RDP client machine (where the RDP client runs):

# Register (per-user, no admin required)
.\bin\x64\Debug\rdp-simple-plugin-client-rust.exe /register

# Register (machine-wide, requires admin)
.\bin\x64\Debug\rdp-simple-plugin-client-rust.exe /register /machine

# Unregister
.\bin\x64\Debug\rdp-simple-plugin-client-rust.exe /unregister

2. Start the Server

Run inside the RDP session (on the remote server):

# WTS API mode (default)
.\bin\x64\Debug\rdp-simple-plugin-server-rust.exe

# File handle mode (overlapped I/O)
.\bin\x64\Debug\rdp-simple-plugin-server-rust.exe /filehandle

3. Connect via RDP

After registration, launch the RDP client and connect to the remote session. The DVC channel will be established automatically, and you'll see ping/echo messages in both console windows.

Interoperability

All language implementations use the same:

  • Channel name: dvc::sample::simpleplugin
  • Wire protocol format
  • COM CLSID: {D9B80669-C06A-4BD1-9CB1-3B7168C9E3A3}

This means you can mix and match client and server implementations across languages.

Debugging

Client Plugin

  1. Start the RDP client and connect to the remote server
  2. Attach WinDbg or Visual Studio to rdp-simple-plugin-client-rust.exe
  3. The plugin logs to OutputDebugString; use DebugView to capture output without attaching a debugger

For source-level debugging in Visual Studio, open the generated .sln and attach to the running process.

Server Application

  1. Run cargo build then launch the server under a debugger with devenv /debugexe or attach after launch
  2. Alternatively, add println! or eprintln! statements and observe console output
  3. Connect with the RDP client and watch ping/echo messages in the console

Common Issues

Plugin not loaded:

  • Verify HKCU\Software\Classes\CLSID\{D9B80669-C06A-4BD1-9CB1-3B7168C9E3A3}\LocalServer32 points to the correct EXE
  • Check Windows Event Viewer → Application for COM activation errors

Channel not opening:

  • Must run inside an active RDP session, not the local console
  • Confirm the RDP client has established the connection before starting the server

Build errors (link.exe not found):

  • Ensure Visual Studio 2022 C++ workload is installed; Rust on Windows requires the MSVC linker
  • Run rustup target add x86_64-pc-windows-msvc or aarch64-pc-windows-msvc as needed

Troubleshooting

Channel not opening

  • Verify the plugin is registered in the registry
  • Check if the RDP client is loading the plugin (use Process Monitor)
  • Ensure the server is running inside the RDP session

Server fails to open channel

  • Must run inside an RDP session (not local console)
  • Check WTSVirtualChannelOpenEx error code
  • Try /filehandle mode if WTS API mode fails

Build errors

  • Ensure Rust toolchain is installed (https://rustup.rs/)
  • Run cargo build to verify dependencies

Next steps

RDP Virtual Channels

COM Development

Rust on Windows

Debugging

License

See repository root for license information.