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, andIWTSVirtualChannelCallbackusing thewindowscrate'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/machinefor machine-wide;/unregisterto 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.rsimplementsIClassFactoryusing thewindowscrate'simplementmacrordp_plugin.rsimplementsIWTSPlugin,IWTSListenerCallback, andIWTSVirtualChannelCallbackas a single COM objectregistry_helper.rswrites registry entries forLocalServer32and AddIns using thewindowscrate's registry APIsmain.rshandles the--Embeddingflag (COM out-of-process activation), callsCoRegisterClassObject, 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(indvc_reassembler.rs) handles PDU fragment reassembly
Rust-Specific Patterns
- COM objects use
Arc<Mutex<T>>for shared state across interface callbacks - The
windowscrate generates vtable wrappers;#[implement]handlesQueryInterface,AddRef,Release - Registry access uses
windows::Win32::System::Registrybindings directly (no external crate) AtomicU32for 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_FIRSTandCHANNEL_FLAG_LASTflags - The
DvcReassemblerclass 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 (
rustupfrom 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
- Start the RDP client and connect to the remote server
- Attach WinDbg or Visual Studio to
rdp-simple-plugin-client-rust.exe - 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
- Run
cargo buildthen launch the server under a debugger withdevenv /debugexeor attach after launch - Alternatively, add
println!oreprintln!statements and observe console output - 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}\LocalServer32points 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-msvcoraarch64-pc-windows-msvcas 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
/filehandlemode if WTS API mode fails
Build errors
- Ensure Rust toolchain is installed (https://rustup.rs/)
- Run
cargo buildto verify dependencies
Next steps
RDP Virtual Channels
- Terminal Services Virtual Channels
- IWTSPlugin Interface
- IWTSListenerCallback Interface
- IWTSVirtualChannelCallback Interface
- IWTSVirtualChannel Interface
- WTSVirtualChannelOpenEx
- WTSVirtualChannelRead
- WTSVirtualChannelWrite
- DVC Plug-in Registration
COM Development
Rust on Windows
Debugging
- DebugView - Capture OutputDebugString output
- Process Monitor - Trace registry and file I/O
License
See repository root for license information.