rdp_pipe
Python desktop RDP client for Linux operators connecting to remote hosts over RDP with NLA (CredSSP) and NTLM password authentication. The UI is a native PyQt6 window with pointer-gated keyboard input and seamless resize via MS-RDPEDISP when the server supports it.
Prerequisites
- pyenv with the Python version named in
.python-versioninstalled locally (the file is required in every clone; install that interpreter with pyenv before pip) - An X11 or Wayland desktop session for PyQt6
- Network reachability to the target host on TCP 3389 (or the port in the URL)
Install
From the repo root with pyenv active for .python-version:
pip install -e .
Optional wrapper: ./script/install.sh (runs the same command).
This project does not use a repo .venv. Console scripts (rdp, rdpr) land on your pyenv shim path after editable install.
If imports fail after moving the repo, remove any leftover .venv directory (it may still point at an old checkout path) and run pip install -e . again. The installable Python package is rdp_pipe, not rdp.
Connect
rdp [-h] [--headless] [--pipe] [--no-autoresize] URL
GUI mode (default) opens a PyQt6 window. --headless runs without a display and requires --pipe for automation on the command socket under the system temp directory (rdp.sock).
Press Ctrl+C in the terminal where you launched rdp to disconnect and exit cleanly (this does not affect keyboard input forwarded inside the session window).
URL is an aardwolf-style connection string or bare host shorthand:
rdp rdp://DOMAIN\\Administrator@10.0.0.5:3389
rdp rdp+ntlm-password://DOMAIN\\Administrator@10.0.0.5:3389
rdp 10.0.0.5
Generic rdp:// URLs prefer NTLM password authentication; the server selects NLA (CredSSP), TLS-only, or legacy RDP during X.224 negotiation.
Server certificate trust
On NLA connect, the client stores the server TLS certificate under ~/.config/rdpipe/:
clients/<ip>— one-line file mapping the remote IP to a certificate fingerprintcertificates/<fingerprint>/—certificate(PEM),ip, andmetadata(JSON)
The first connection to an IP is accepted automatically. If the server later presents a different certificate for the same IP, the client refuses to connect and prints paths to remove, for example:
~/.config/rdpipe/clients/10.0.0.5
~/.config/rdpipe/certificates/<old-fingerprint>/
Remove the clients/<ip> file to trust a new certificate on the next connect. IPv6 addresses use underscores instead of colons in client filenames (e.g. 2001_db8__1).
Password resolution
Passwords are resolved in order (first match wins):
- Embedded in URL userinfo (
user:password@host) RDP_PASSWORDenvironment variable- Standard input —
getpasson a TTY, otherwise one line from a pipe
If no password is available after these steps, the CLI exits with a non-zero status and an error on stderr.
Examples:
export RDP_PASSWORD='your-password'
rdp 'rdp+ntlm-password://DOMAIN\Administrator@10.0.0.5'
echo 'your-password' | rdp 'rdp+ntlm-password://Administrator@10.0.0.5'
Headless automation
export RDP_PASSWORD='your-password'
rdp --headless --pipe '10.0.0.5'
Headless mode uses a fixed 1280×800 session geometry (no window resize). The process listens on the Unix socket for newline-delimited JSON commands. Use --color-depth 24 (or 16) when you need a specific bpp for screen analysis via receive_screenshot.
Full wire format, command parameters, response shapes, and client examples: REMOTE_COMMAND_PROTOCOL.md.
Quick probe with rdpr:
rdpr command_receive_geometry
rdpr command_send_geometry 1920 1080
rdpr command_send_click 640 400 --button left
rdpr command_receive_screenshot --format png
Or with nc:
printf '%s\n' '{"command":"receive_geometry"}' | nc -U "${TMPDIR:-/tmp}/rdp.sock"
--pipe is optional in GUI mode (automation socket at {system temp}/rdp.sock alongside the window). Every session also touches {system temp}/rdp_activity.stamp on remote framebuffer updates.
MCP automation
A standard MCP host config is provided at mcp/mcp.json. It launches mcp/run_server.sh, which selects the pyenv interpreter from .python-version, sets RDPR_COMMAND to that environment’s rdpr, and runs mcp/server.py. Install the optional MCP extra into that interpreter, then copy the file or merge its rdp_pipe entry into your host’s MCP settings:
pip install -e ".[mcp]"
Socket default: {system temp}/rdp.sock (honors $TMPDIR; typically /tmp on Linux).
For Cursor, copy or symlink into .cursor/mcp.json (that directory is gitignored):
mkdir -p .cursor
cp mcp/mcp.json .cursor/mcp.json
Typical LLM flow with an active rdp --pipe session:
command_receive_geometry— learn coordinate boundscommand_receive_screenshot— capture state (inline image; server always passes--no-writetordpr)command_send_click/command_send_key— act on the remote desktop
MCP test prompt
Paste this to an agent with rdp_pipe connected and a live rdp --headless --pipe (or GUI + --pipe) session:
In RDP:
1. take screenshot
2. open calculator
3. take screenshot
4. close calculator
5. take screenshot
6. open command prompt
7. take screenshot
8. list directory
9. take screenshot
Take a screenshot after steps 1, 3, 5, 7, and 9 only. After step 4, confirm step 5 shows Calculator gone before opening Command Prompt.
On Windows Server with legacy Calculator (win32calc.exe), search-box automation works for launch; close with taskkill /F /IM win32calc.exe from search (title-bar X clicks are unreliable over RDP). Open Command Prompt with search → cmd → Enter; run dir in the CMD window.
MCP reference screenshots
Committed captures from a successful run (1280×800 session) live under asset/mcp/screenshot/:
| Step | Expected state | File |
|---|---|---|
| 1 | Clean desktop | 01.png |
| 3 | Calculator open | 03.png |
| 5 | Calculator closed | 05.png |
| 7 | Command Prompt open | 07.png |
| 9 | dir in C:\Users\Administrator |
09.png |
Step 1 — initial desktop:
Step 3 — Calculator open:
Step 5 — Calculator closed:
Step 7 — Command Prompt open:
Step 9 — directory listing:
Wire protocol details: REMOTE_COMMAND_PROTOCOL.md.
Monkey-patching
This client patches aardwolf at runtime instead of vendoring or forking the library. Patches live in production code under src/rdp_pipe/ (not in tests). Each patch replaces a symbol on import; subclass overrides on RdpDesktopConnection are normal inheritance and are not listed here.
| Target | Replacement | Why |
|---|---|---|
aardwolf.protocol.T124.userdata.clientcoredata.TS_UD_CS_CORE.to_bytes |
_patched_ts_ud_cs_core_to_bytes in rdp_connection.py |
During desktop connect, advertise SUPPORT_MONITOR_LAYOUT_PDU and (when --color-depth is 32) WANT_32BPP_SESSION / 24-bpp high color in Client Core Data. Active only while RdpDesktopConnection.connect() runs (_CONNECTING_DESKTOP_CONNECTION gate). |
aardwolf.protocol.pdu.capabilities.pointer.TS_POINTER_CAPABILITYSET.__init__ |
_patched_pointer_capabilityset_init in rdp_connection.py |
Advertise colorPointerFlag=True so the server sends color/cached pointer updates for hover cursor shapes. |
aardwolf.connection.RDPConnection.handle_out_data |
_patched_handle_out_data in rdp_connection.py |
Before every Confirm Active (connect and mid-session reactivation), set desktopResizeFlag=True and add LARGE_POINTER / UNICODE / MOUSE_HWHEEL that aardwolf omits. |
RdpDesktopConnection._RDPConnection__process_fastpath |
_rdp_desktop_process_fastpath in rdp_connection.py |
Stock aardwolf handles fast-path BITMAP only. Our handler also forwards bitmap tiles without inferring resolution from tile size, and emits pointer updates (COLOR, POINTER, CACHED, etc.) for remote cursor mirroring. |
aardwolf.extensions.RDPECLIP.channel.RDPECLIPChannel._handle_format_data_request |
_patched_rdpeclip_handle_format_data_request in rdp_connection.py |
aardwolf dereferences clipboard.data.datatype without checking for None. After connect we advertise standard clipboard formats even when the local clipboard is empty; a remote CB_FORMAT_DATA_REQUEST then crashed the x224 reader. Reply with CB_RESPONSE_FAIL when clipboard.data is unset. |
aardwolf.connection.RDPConnection.terminate |
_patched_aardwolf_terminate in rdp_connection.py |
Stock aardwolf always calls send_disconnect() and logs Error while requesting shutdown when the MCS channel is missing (failed connect) or the TCP socket is already reset. Skip graceful shutdown when MCS/server connect data are absent; suppress the warning for expected transport errors during reconnect teardown. |
aardwolf.connection.RDPConnection.check_logon_status |
_patched_check_logon_status in rdp_connection.py |
After aardwolf records SET_ERROR_INFO, copy SERVER_SHUTDOWN / SERVER_REBOOT onto RdpDesktopConnection.pending_remote_session_end_reason so the GUI can prompt for reconnect vs exit. |
When adding or changing a runtime monkey-patch, update this section in the same change set.
Release (PyPI)
Operator workflow mirrors a static frontend build → package → deploy, but artifacts are Python wheels/sdists and deploy uploads with twine (not S3).
Activate pyenv for .python-version, install dev tools once (pip install -e ".[dev]"), then from the repo root:
./script/build.sh
ARCHIVE="$(./script/package.sh patch)"
./script/deploy.sh "${ARCHIVE}"
| Script | Role |
|---|---|
script/build.sh |
python -m build → dist/ (wheel + sdist). No version bump. |
script/package.sh |
patch | minor | major: clean tree, bump pyproject.toml, commit, annotated vX.Y.Z, run build.sh, pack build/archives/vX.Y.Z_rdp_pipe.tar.gz. Stdout = archive path. |
script/deploy.sh |
Positional archive path: extract, twine check, twine upload. Writes releases/pypi/X.Y.Z.md when the tree is clean. |
PyPI credentials: ~/.pypirc or TWINE_USERNAME / TWINE_PASSWORD. For TestPyPI, set TWINE_REPOSITORY_URL=https://test.pypi.org/legacy/ before deploy.sh.
After package.sh, push the release commit and tag when ready: git push origin HEAD vX.Y.Z.
Test
pip install -e ".[dev]"
python -m pytest -q
MCP server tests need the mcp extra: pip install -e ".[dev,mcp]".
Offline unit tests cover URL/password handling, RDPDISP PDU encoding, pointer mask decoding, command socket protocol, and CLI validation.
Manual smoke
Connect to an RDP host with NLA enabled. Resize the client window and confirm the remote display resolution tracks when the server supports RDPDISP. Servers without RDPDISP keep a fixed session resolution; the local window may letterbox until disconnect.
To trace remote pointer mirroring (PDU types, apply/skip decisions, forwarded hover coordinates), run with RDP_POINTER_DEBUG=1 and watch stderr while moving the mouse over window borders and text fields.
To trace keyboard capture after idle (Qt key events, pointer-inside gating, enqueue, forwarder dequeue), run with RDP_KEYBOARD_DEBUG=1. If GUI keys show no keyPressEvent lines but rdpr send_key still works, focus was stolen locally; if both fail, check stderr for RDP deactivation-reactivation failed.
Metadata
Release files for rdp-pipe 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| rdp_pipe-0.1.0.tar.gz | 94.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| rdp_pipe-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 161.9 kB
Release files / rdp_pipe-0.1.0.tar.gz
| Download URL | rdp_pipe-0.1.0.tar.gz |
|---|---|
| Size | 94.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0d6bc25a215e1577a10d9b2ab385b7c4ebe79f07b19bc37728150bbffaf3d88d
|
|
BLAKE2b-256 checksum How to use checksums |
d6bfd64710d8b58bbdc8dbd32875ba6e53a1dbca724342dd6a59fe3d814d447b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.0
|
Release files / rdp_pipe-0.1.0-py3-none-any.whl
| Download URL | rdp_pipe-0.1.0-py3-none-any.whl |
|---|---|
| Size | 67.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f2a1861cb691f2b6b328e5ed0fab6f09e1893e0cc4b454eb62c67e937777ddaf
|
|
BLAKE2b-256 checksum How to use checksums |
b26d848131a2da73db74c8fa335558a0f88f92e228bf828eb706d2cb186633fd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.0
|