RDP DVC simple plugin – Python
The Python Simple implementation demonstrates a complete RDP DVC plugin system using the win32more package for COM and WTS APIs:
- Client Plugin: Out-of-process COM LocalServer implemented in Python using
win32morefor COM interface implementation and registry management - 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-python.exe - Server output:
bin/<Platform>/<Configuration>/rdp-simple-plugin-server-python.exe - Register: client EXE
/register(per-user); add/machinefor machine-wide;/unregisterto remove; also supportspython -m rdp_simple_plugin_client - 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 (Python package). |
rdp_simple_plugin_client/ |
Client-side DVC plugin (COM LocalServer). |
rdp_simple_plugin_server/ |
Server-side application. |
pyproject.toml |
Project metadata and build config. |
requirements.txt |
Python dependencies. |
build.ps1 |
PowerShell build script. |
README.md |
This README file. |
Key Implementation Details
COM Implementation via win32more
The client plugin uses win32more to implement COM interfaces in Python:
simple_plugin.pysubclassesIWTSPlugin,IWTSListenerCallback, andIWTSVirtualChannelCallbackfromwin32morewin32morehandles vtable layout and COM marshaling automaticallyregistry_helper.pywritesLocalServer32and AddIns registry entries usingwinreg
Server I/O Modes
WTS API Mode (Default):
- Synchronous reads via
WTSVirtualChannelRead(ctypes) - Single-threaded send-receive loop
- Simpler implementation; good for testing
File Handle Mode (--filehandle):
- Asynchronous overlapped I/O using Windows
ReadFile/WriteFilewith OVERLAPPED via ctypes DvcReassemblerindvc_reassembler.pyhandles PDU fragment reassembly
Running as a Module vs Executable
The client and server can be run either as standalone executables (built with PyInstaller) or directly as Python modules:
# As executable (after build):
.\bin\x64\Debug\rdp-simple-plugin-client-python.exe /register
# As Python module (development):
python -m rdp_simple_plugin_client /register
python -m rdp_simple_plugin_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\SimpleRdpPlugin\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-python.exe process, OR
│ └─> Launches new process with registered executable path
│
├─> Process registers IClassFactory via CoRegisterClassObject (win32more)
│
├─> 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 (ctypes)
│
├─> 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
- Python 3.10 or later
- Windows 10/11 or Windows Server 2019+
- win32more >= 0.6.9 (for client plugin COM support)
- PyInstaller (for building standalone executables)
Setup
Using PowerShell Script
# Install dependencies and build executables
.\build.ps1 -Install
# Build all executables (auto-detects platform)
.\build.ps1
# Build specific configuration/platform
.\build.ps1 -Configuration Release -Platform x64
# Build only client or server
.\build.ps1 -Project client
.\build.ps1 -Project server
# Force rebuild (clean first)
.\build.ps1 -Force
# Clean all build outputs
.\build.ps1 -Clean
Development Mode
# Create virtual environment (recommended)
python -m venv .venv
.\.venv\Scripts\Activate.ps1
# Install dependencies
pip install -r requirements.txt
Running the sample
1. Register the Client Plugin
Run on the RDP client machine (where the RDP client runs):
# Using built executable:
.\bin\x64\Debug\rdp-simple-plugin-client-python.exe /register
# Using Python module:
python -m rdp_simple_plugin_client /register
# Register (machine-wide, requires admin)
.\bin\x64\Debug\rdp-simple-plugin-client-python.exe /register /machine
# Unregister
.\bin\x64\Debug\rdp-simple-plugin-client-python.exe /unregister
2. Start the Server
Run inside the RDP session (on the remote server):
# Using built executable:
.\bin\x64\Debug\rdp-simple-plugin-server-python.exe
# Using Python module:
python -m rdp_simple_plugin_server
# File handle mode (overlapped I/O)
.\bin\x64\Debug\rdp-simple-plugin-server-python.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
- Run the client as a Python module (not packaged EXE) so you can add
printstatements:python -m rdp_simple_plugin_client - Add logging in
simple_plugin.py→OnDataReceivedto observe echo logic - Use DebugView if the plugin writes via
OutputDebugString(through win32more)
Server Application
- Run the server as a Python module for live output:
python -m rdp_simple_plugin_server - Add
printorloggingstatements insimple_server.pyfor detailed tracing - Connect with the RDP client and observe ping/echo messages
Common Issues
Plugin not loaded:
- Verify
HKCU\Software\Classes\CLSID\{D9B80669-C06A-4BD1-9CB1-3B7168C9E3A3}\LocalServer32points to the correct EXE or Python script - 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
Troubleshooting
Client plugin not loading
- Verify the plugin is registered in the registry
- Check if the RDP client is loading the plugin (use Process Monitor)
- Ensure win32more package is installed (
pip install win32more)
Server fails to open channel
- Must run inside an RDP session (not local console)
- Check WTSVirtualChannelOpenEx error code
- Ensure a client plugin is registered and the RDP client has loaded it
Import errors
- Ensure you're running from the
Simple/pythondirectory - Or install as a package:
pip install -e .
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
Python on Windows
- win32more - Python COM and Windows API bindings
- ctypes - C foreign function interface for Python
- PyInstaller - Package Python apps as standalone executables
Debugging
- DebugView - Capture OutputDebugString output
- Process Monitor - Trace registry and file I/O
License
See repository root for license information.