โ† Files

RDP Diagnostic & Remediation Tool#

A comprehensive PowerShell-based diagnostic and remediation tool for Remote Desktop Protocol (RDP) connectivity on Windows 10/11 and Windows Server. One script, zero dependencies: it audits every RDP-related subsystem, repairs what it finds, proves the RDP stack actually answers with a real protocol handshake, and exports HTML/JSON reports for humans and automation alike.

๐Ÿ“‹ Table of Contents#

What's new in v5#

v5.0.0 is a full production refactor of v4.2.0. Highlights (see CHANGELOG.md for the complete list):

Area v5.0.0
Help Comprehensive built-in -Help + full comment-based help (Get-Help -Full)
Updates -CheckUpdate / -Update / -UpdateSource - GitHub-release-backed self-update with validation & backups
Diagnostics Real X.224/RDP negotiation handshake (-TestRdpTarget), TLS certificate audit, GPO overlay detection, session lifecycle & brute-force event mining, Public-profile firewall gap detection
Actions -DisableRDP, -AddUser, -RestartServices, -Elevate, standalone -SetPort, -WhatIf/-Confirm everywhere
Sessions -ShowSessions via WTS API (qwinsta fallback)
Reporting -ExportJson, -LogPath, -Quiet, -NoColor, -NoEmoji, -NoBrowser, redesigned HTML report
Correctness ~20 bug fixes: Home N (CoreN) edition misdetection, Get-WmiObject on PS7, Invoke-Expression remediations, broken firewall port matching, $Verbose gating bug, TermService "Automatic" false positive, ineffective #Requires, and more
Engineering GitHub Actions CI, PSScriptAnalyzer gate, Pester test-suite, strict mode, guarded check pipeline (one broken subsystem can't abort the run)

Quick Start#

# 1) Get the tool - npm channel (Node.js 18+)
npx rdp-diagnostic-tool -Version          # try it without installing
npm install -g rdp-diagnostic-tool        # permanent: rdp-tool on PATH

#    ... or download RDP-Tool.ps1 from the latest Release, or:
git clone https://github.com/paulmann/RDP-Diagnostic-Tool

# 2) Audit this machine (read-only, works unelevated)
rdp-tool                                  # npm channel
pwsh -NoProfile -File .\RDP-Tool.ps1       # script channel

# 3) Enable + repair RDP end-to-end (elevated, no prompts)
pwsh -NoProfile -File .\RDP-Tool.ps1 -EnableRDP -Force

# 4) Prove RDP answers on a remote host without credentials
pwsh -NoProfile -File .\RDP-Tool.ps1 -TestRdpTarget "srv01.contoso.com:3390"

# 5) Full reference
pwsh -NoProfile -File .\RDP-Tool.ps1 -Help

If script execution is restricted, launch through pwsh -File ... as shown above (bypasses ExecutionPolicy for that invocation) or use Unblock-File .\RDP-Tool.ps1 after downloading.

Installation#

Any one channel is enough - every channel ships the same RDP-Tool.ps1.

Channel Command Requires
npm (one-off) npx rdp-diagnostic-tool@5.0.1 -Version Node.js 18+
npm (permanent) npm i -g rdp-diagnostic-tool, then rdp-tool Node.js 18+
Direct download RDP-Tool.ps1 from the latest release, then Unblock-File .\RDP-Tool.ps1 PowerShell 7 only
Git clone git clone https://github.com/paulmann/RDP-Diagnostic-Tool git
winget winget install Paulmann.RDPDiagnosticTool (once the manifest is merged into winget-pkgs) winget
Scoop scoop install https://raw.githubusercontent.com/paulmann/RDP-Diagnostic-Tool/main/scoop/rdp-diagnostic-tool.json Scoop

PowerShell 7 (pwsh) is required on every channel; install it with winget install Microsoft.PowerShell. Through npm and the package managers the command is on PATH as rdp-tool:

rdp-tool                                  # full read-only audit
rdp-tool -TestRdpTarget "srv01:3389"       # prove the listener really speaks RDP
rdp-tool -EnableRDP -Force                # elevated repair

Self-update (-CheckUpdate / -Update) behaves identically on all channels: the script refreshes itself in place from GitHub Releases. npm-specific details: docs/NPM.md.

Features#

Diagnostics (50+ checks, individually guarded)#

Area What is checked
Prerequisites Elevation, PowerShell 7, OS/edition/SKU RDP-host support (explicit matrix incl. Home N & Windows SE), pending reboot (registry + optional DISM), RDP binaries (termsrv.dll version & friends), Server-SKU feature state
Registry fDenyTSConnections, fEnableWinStation, PortNumber, UserAuthentication (NLA), SecurityLayer, MinEncryptionLevel, transport selection, session limits, blank-password policy - each with Group Policy overlay detection (policy-managed values are reported, never "repaired" locally)
Group Policy Terminal Services / RDP client / CredentialsDelegation policy dump with risky-policy flags (RDP denied by GPO, NLA off by GPO, legacy SecurityLayer)
Services TermService (trigger-start aware), UmRdpService, SessionEnv, RpcSs/RpcEptMapper, audio stack for redirection; dependency health; WMI listener cross-check
Firewall MpsSvc, profile states, built-in RDP rule sets matched language-independently, custom port rules via port-filter association, BLOCK-rule conflicts, Public-profile coverage gap
Network Adapters, IPv4/IPv6, profile categories, gateway/DNS sanity
Listener & ports TCP/UDP listeners, owning process, non-standard port-ownership (squatting), fast loopback TCP probe
Protocol Unauthenticated X.224/RDP negotiation handshake against local and remote endpoints; detects non-RDP services on the port; reports TLS/CredSSP requirement; cross-checks wire vs registry NLA
Certificates Auto-generated & pinned (SSLCertificateSHA1Hash) certificates: expiry, private key, store lookup
Permissions "Remote Desktop Users" via well-known SID (works on localized Windows), current-user rights, member listing
Security posture NLA, UAC, Defender, Credential Guard/VBS, logon auditing presence
Events TermService errors, RCM 1148/1149 auth events, LSM session lifecycle 21-25, Security 4624/4625 (LogonType 10) with brute-force source-IP rollup, 4778/4779 reconnects
Resources RAM/CPU/disk thresholds, GPU inventory
External (-TestExternal) DNS, github.com:443 (updates), generic egress, TLS 1.2+

Remediation (explicit, safe, reversible)#

Reporting#

Command-Line Reference#

.\RDP-Tool.ps1 [<mode>] [<actions>] [<probes>] [<reporting>] [<update>] [<misc>]

Modes (pick at most one)#

Switch Description
(none) Full read-only diagnostic pass (default)
-DiagnoseOnly Explicit read-only audit; makes no changes even if combined with baseline flags like -SetPort/-DisableNLA (they only change what is audited against)
-EnableRDP Enable RDP + auto-remediate everything repairable
-DisableRDP Securely disable inbound RDP (registry + firewall)
-CheckUpdate Compare local version with the latest GitHub release (exit 5 = update available)
-Update Validate + back up + self-replace with the latest release

Configuration actions (require elevation)#

Switch Description
-SetPort <1024-65535> Change the listener port (alias: -ChangePort); composes with -EnableRDP
-DisableNLA Turn Network Level Authentication off (insecure - confirmation required)
-AddUser <u1,u2,...> Add accounts to "Remote Desktop Users" (local or DOMAIN\user)
-RestartServices Restart TermService/SessionEnv/UmRdpService (disconnect guard)
-ForcePrivateNetwork Re-categorize Public profiles to Private

Probes & inspection#

Switch Description
-TestRdpTarget <host[:port]> Real RDP negotiation handshake against a local/remote endpoint (no credentials)
-ShowSessions List live terminal sessions (WTS API, qwinsta fallback)
-TestExternal Outbound DNS/TCP connectivity checks
-SkipRebootCheck Skip pending-reboot detection
-SkipSlowChecks Skip DISM scan and full port-filter enumeration

Reporting#

Switch Description
-ExportReport <file.html> Styled self-contained HTML report
-ExportJson <file.json> Machine-readable report
-LogPath <file.log> Timestamped run log (appended)
-NoBrowser Don't auto-open the HTML report
-Quiet Summary + problems only
-NoColor / -NoEmoji Plain output (also auto-detected; NO_COLOR honored)

Update & misc#

Switch Description
-UpdateSource <src> owner/repo slug or direct HTTPS URL (mirrors/forks/air-gapped)
-Elevate Relaunch elevated with identical arguments (UAC)
-Force Skip interactive confirmations
-Version Version/runtime info
-Verbose Detailed per-check output (-ShowVerbose still works, deprecated)
-WhatIf / -Confirm Preview / gate every system change
-Help, -?, -h Comprehensive built-in help

Command Examples#

# Daily-driver audit with all reports for a support ticket
.\RDP-Tool.ps1 -ExportReport .\rdp-report.html -ExportJson .\rdp-report.json -LogPath .\rdp.log

# Unattended fleet repair (SCCM/Intune/RMM): enable RDP, custom port, grant helpdesk
.\RDP-Tool.ps1 -EnableRDP -SetPort 3390 -AddUser 'CONTOSO\rdp-helpdesk' -Force -Quiet -ExportJson C:\Temp\rdp.json

# Dry run: show exactly what would be changed, touch nothing
.\RDP-Tool.ps1 -EnableRDP -WhatIf

# Probe a remote host: is that port REALLY RDP, and does it demand NLA?
.\RDP-Tool.ps1 -TestRdpTarget 10.0.0.5:3390

# Who is connected right now?
.\RDP-Tool.ps1 -ShowSessions

# Incident response: lock the machine down immediately
.\RDP-Tool.ps1 -DisableRDP -Force

# CI: fail the pipeline when RDP is broken or a newer tool version exists
.\RDP-Tool.ps1 -DiagnoseOnly -ExportJson report.json     # exit 1/2 = broken
.\RDP-Tool.ps1 -CheckUpdate                              # exit 5 = update available

# Keep the tool current
.\RDP-Tool.ps1 -Update -Force

The RDP Protocol Handshake Probe#

Most tools stop at "port 3389 is open". RDP-Tool goes one layer deeper and speaks
just enough of the protocol to prove the RDP stack itself is answering:

Client โ”€โ”€โ–บ TPKT + X.224 Connection Request + RDP Negotiation Request (TLS|CredSSP)
Server โ”€โ”€โ–บ X.224 Connection Confirm + Negotiation Response
             โ”œโ”€ selectedProtocol: Standard RDP | TLS | CredSSP (NLA)
             โ””โ”€ or failure code: SSL_REQUIRED_BY_SERVER, HYBRID_REQUIRED_BY_SERVER, ...
<details> <summary><b>What it looks like on the wire (click to expand)</b></summary>
$ rdp-tool -TestRdpTarget "srv01.contoso.com:3389"

  โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  ๐Ÿ”Œ  RDP PROTOCOL HANDSHAKE PROBE
  โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

  Target                : srv01.contoso.com:3389
  TCP connect           : OK (18 ms)
  X.224 CR sent         : 19 bytes (TLS | CredSSP requested)
  mstshash cookie       : aG9zdFxcUkRQ     (from the calling account)

  Server response       : X.224 Connection Confirm + RDP_NEG_RSP
  Negotiated protocol   : CredSSP (NLA)     <- enforced on the wire
  Certificate           : TLS handshake available (no auth attempted)
  Registry / GPO says   : UserAuthentication=1  (matches the wire)

  โœ…  The RDP stack answers and requires Network Level Authentication.
      No credentials were sent.
</details>

This is the same negotiation mstsc.exe performs before authentication - no
credentials are sent or needed
. The probe:

Auto-Update#

The tool carries a machine-readable version marker and knows its own repository:

.\RDP-Tool.ps1 -CheckUpdate     # exit 0 = up to date, 5 = update available, 6 = check failed
.\RDP-Tool.ps1 -Update          # interactive; add -Force for unattended

How -Update works:

  1. Resolve the newest version: GitHub Releases API (releases/latest) โ†’ if the asset RDP-Tool.ps1 exists, it is used; otherwise the tagged raw file; if the API is unavailable/rate-limited โ†’ raw-file probe of the default branch with version-marker parsing.

  2. Download over TLS and validate: non-empty, minimum size, not an HTML error page, version marker present and matching the release, full PowerShell syntax parse of the payload.

  3. Back up the current script next to it (RDP-Tool.ps1.bak-<timestamp>; falls back to %TEMP%).

  4. Replace atomically (temp file + move). Re-run the tool to use the new version.

Enterprise mirrors/air-gapped networks:

.\RDP-Tool.ps1 -Update -UpdateSource https://intranet/tools/RDP-Tool.ps1   # direct file
.\RDP-Tool.ps1 -Update -UpdateSource contoso/RDP-Diagnostic-Tool           # fork slug

Reporting & Automation#

Example (fail a build on any RDP error):

pwsh -NoProfile -File .\RDP-Tool.ps1 -DiagnoseOnly -Quiet -ExportJson rdp.json
if ($LASTEXITCODE -ge 1 -and $LASTEXITCODE -le 2) { throw "RDP misconfigured (exit $LASTEXITCODE)" }

Exit Codes#

Code Meaning
0 Success - system ready / action completed / up to date
1 Critical issues - RDP will not work until resolved
2 Errors detected - RDP degraded or limited
3 Unsupported Windows edition (Home / SE cannot host RDP)
4 Prerequisite not met (elevation required, invalid parameter combination, non-Windows platform)
5 Update available (-CheckUpdate only)
6 Update failed
99 Unhandled script error

Supported Windows Editions#

Edition RDP host Notes
Windows 10/11 Pro, Pro for Workstations, Pro Education โœ…
Windows 10/11 Enterprise (incl. LTSC), Education โœ…
Windows IoT Enterprise / Enterprise multi-session (ServerRdsh) โœ…
Windows Server 2016 / 2019 / 2022 / 2025 โœ… Admin sessions; RDS role for published desktops
Windows 10/11 Home / Home N / Single Language โŒ Client connections only; tool exits 3 with guidance
Windows 11 SE (CloudEdition) โŒ

The v4 edition regex misidentified Home N (CoreN) as RDP-capable. v5 uses an explicit, tested edition matrix (see tests/).

Architecture & Design#

<details> <summary><b>Module map (click to expand)</b></summary>
RDP-Tool.ps1 (single file, ~4300 lines, UTF-8 BOM, PowerShell 7+)
โ”‚
โ”œโ”€ Metadata           single version marker (release & update contract)
โ”œโ”€ Console/Logging    color+emoji auto-detection, ASCII fallback, file logger
โ”œโ”€ State              script-scoped result/remediation/test stores (no $global:)
โ”œโ”€ Validation         parameter-combination matrix with actionable errors
โ”œโ”€ Help/Version       built-in reference + comment-based help for Get-Help
โ”œโ”€ Update engine      releases API โ†’ raw probe fallback, validate โ†’ backup โ†’ atomic swap
โ”œโ”€ Checks (Test-*)    each wrapped in Invoke-Check (isolated failures)
โ”‚   โ””โ”€ registry(+GPO) ยท services ยท firewall ยท network ยท listener ยท handshake ยท
โ”‚      certificate ยท permissions ยท security ยท resources ยท event logs
โ”œโ”€ Session engine     WTS P/Invoke (Unicode-correct) + qwinsta fallback
โ”œโ”€ Remediation        ShouldProcess-aware, GPO-aware, idempotent, recorded
โ”œโ”€ Reporting          HTML / JSON / console summary / exit-code mapping
โ””โ”€ Main               phase pipeline: prereqs โ†’ remediate โ†’ verify โ†’ report
</details>

Design rules the codebase follows:

  1. Never abort early - every check is guarded; a dead WMI or firewall API degrades the report, it doesn't kill it.

  2. Never guess - localized systems are handled via well-known SIDs and language-independent firewall rule names; GPO-managed values are detected instead of being silently overwritten.

  3. Never surprise - no changes without explicit action switches; -WhatIf end-to-end; dangerous actions (NLA off, service restart in a live session, disabling RDP remotely) require confirmation or -Force.

  4. Be testable - pure logic (protocol codecs, edition matrix, validators) is unit-tested; RDPDIAG_LIB=1 enables dot-sourcing without side effects.

Safety Model#

Development & CI#

# Lint
Invoke-ScriptAnalyzer -Path .\RDP-Tool.ps1 -Settings .\ScriptAnalyzerSettings.psd1

# Test (Pester 5+)
Invoke-Pester .\tests -Output Detailed

CI (.github/workflows/ci.yml) runs on every push/PR:
syntax parse gate (Windows + Ubuntu), PSScriptAnalyzer gate (zero Error/Warning with project settings), and the Pester suite on windows-latest (integrity, help coverage, parameter surface, protocol codec, edition matrix, update validation, CLI behavior incl. exit codes).

Releasing (Maintainers)#

  1. Bump $script:ToolVersion and the .NOTES version in the comment help and add the CHANGELOG.md entry (the test-suite enforces marker โ†” changelog consistency). tools/npm/package.json is checked against both by tools/npm/check-npm-version.mjs.

  2. Merge to main; CI must be green (the package job builds the npm tarball, the release assets and validates the winget manifests).

  3. Build and publish the release assets:

    pwsh -NoLogo -NoProfile -NonInteractive -File .\tools\release\make-release-assets.ps1      # RDP-Tool.ps1, ZIP, SHA256SUMS.txt
    pwsh -NoLogo -NoProfile -NonInteractive -File .\tools\release\make-package-manifests.ps1   # winget + Scoop manifests
    pwsh -NoLogo -NoProfile -NonInteractive -File .\tools\release\publish-release.ps1          # tag + GitHub Release (needs gh auth login)

    Tag the release as X.Y.Z (a v prefix is tolerated). Asset names matter: the self-updater downloads the asset named exactly RDP-Tool.ps1, and the winget/Scoop manifests download the ZIP.

  4. Publish the npm package (first release only, interactive): node tools/npm/build-npm-package.mjs, then cd dist/npm; npm publish --access public with the one-time password. Afterwards, configure a Trusted Publisher on npmjs.com and let the publish-npm.yml workflow publish on tag pushes.

  5. Update the winget and Scoop manifests' URLs/hashes (step 3 regenerates them) and submit the winget manifest via a PR to microsoft/winget-pkgs (see winget/README.md).

  6. Verify: .\RDP-Tool.ps1 -CheckUpdate from an older copy must report "update available" (exit 5), and npx rdp-diagnostic-tool@X.Y.Z -Version must print the new version.

Troubleshooting#

Symptom Fix
"cannot be loaded because running scripts is disabled" Run via pwsh -NoProfile -File .\RDP-Tool.ps1 ... or Unblock-File .\RDP-Tool.ps1
Tool says ELEVATION REQUIRED (exit 4) Re-run with -Elevate, or start PowerShell as Administrator
Emoji render as boxes Legacy console: the tool auto-falls back to ASCII; force with -NoEmoji; or use Windows Terminal
-CheckUpdate fails behind a proxy Configure pwsh proxy settings or use -UpdateSource <internal mirror URL>
Home edition (exit 3) RDP hosting is a Microsoft licensing restriction; see in-tool guidance for alternatives
Changes don't take effect Check for GPO-managed values (the report flags them) and pending reboots

More: wiki/Troubleshooting

Support#