<# .SYNOPSIS Installs and configures the RustDesk client against the BroadView self-hosted server. .DESCRIPTION Downloads (or uses a local copy of) the official RustDesk Windows client, silent-installs it, and points it at remotesupport.broadviewsoftware.com with the BroadView server public key by writing RustDesk2.toml for both the user and the RustDesk service (unattended). Optionally sets a permanent password for unattended access. Report-only with -WhatIf. .NOTES Server details: docs/rustdesk.md. Suitable for manual runs and as an Intune Win32 app install command: powershell -ExecutionPolicy Bypass -File Install-RustDeskClient.ps1 -Confirm:$false .EXAMPLE .\Install-RustDeskClient.ps1 -WhatIf .EXAMPLE .\Install-RustDeskClient.ps1 -Password (Read-Host -AsSecureString 'Permanent password') #> [CmdletBinding(SupportsShouldProcess=$true)] param( [string]$IdServer = 'remotesupport.broadviewsoftware.com', [string]$Key = 'fjfOl2QtaUWm8G0CMD5Woocqpkj4egev74DViet1ATg=', [string]$DownloadUrl, # explicit installer URL; default resolves the latest x86_64 exe via the GitHub API [string]$InstallerPath, # use a pre-downloaded exe instead of downloading [securestring]$Password, # optional: permanent password for unattended access [switch]$Force # reinstall even if RustDesk is already present ) $ErrorActionPreference = 'Stop' # Windows PowerShell 5.1 defaults to TLS 1.0 - GitHub requires 1.2+ [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 # $PSCmdlet is $null when run via `irm | iex` - fall back to always-proceed (no -WhatIf there) $script:cmdletCtx = $PSCmdlet function Test-Step([string]$Target, [string]$Action) { if ($script:cmdletCtx) { return $script:cmdletCtx.ShouldProcess($Target, $Action) } return $true } if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'Run elevated: installation and service configuration require administrator rights.' } $rustdeskExe = Join-Path $env:ProgramFiles 'RustDesk\rustdesk.exe' # 1. Obtain installer (release assets are version-stamped, so resolve the latest via the API) if ((Test-Path $rustdeskExe) -and -not $Force) { Write-Host 'RustDesk already installed - skipping download/install (use -Force to reinstall).' -ForegroundColor Yellow } elseif (-not $InstallerPath) { if (-not $DownloadUrl) { $release = Invoke-RestMethod -Uri 'https://api.github.com/repos/rustdesk/rustdesk/releases/latest' -UseBasicParsing $asset = $release.assets | Where-Object { $_.name -match '^rustdesk-[\d.]+-x86_64\.exe$' } | Select-Object -First 1 if (-not $asset) { throw "No x86_64 Windows exe found in latest release $($release.tag_name)." } $DownloadUrl = $asset.browser_download_url Write-Host "Latest release: $($release.tag_name) -> $($asset.name)" -ForegroundColor DarkGray } $InstallerPath = Join-Path $env:TEMP 'rustdesk-installer.exe' if (Test-Step $DownloadUrl "download RustDesk client to $InstallerPath") { Write-Host "Downloading $DownloadUrl ..." -ForegroundColor Cyan Invoke-WebRequest -Uri $DownloadUrl -OutFile $InstallerPath -UseBasicParsing # strip mark-of-the-web so SmartScreen can't silently block the silent install Unblock-File -Path $InstallerPath -ErrorAction SilentlyContinue } } if ((Test-Path $InstallerPath) -and ((Get-AuthenticodeSignature $InstallerPath).Status -ne 'Valid')) { Write-Warning "Installer signature not valid: $InstallerPath - verify the download source." } # 2. Silent install. Do NOT -Wait on the installer: rustdesk's --silent-install completes the # install but the process never exits. Poll for the installed exe + service instead. if ((Test-Path $rustdeskExe) -and -not $Force) { # already installed - nothing to do } elseif (Test-Step 'RustDesk client' 'silent install') { Write-Host 'Installing (waiting up to 3 min for the exe + service to appear) ...' -ForegroundColor Cyan $proc = Start-Process -FilePath $InstallerPath -ArgumentList '--silent-install' -PassThru $deadline = (Get-Date).AddMinutes(3) while ((Get-Date) -lt $deadline -and -not ((Test-Path $rustdeskExe) -and (Get-Service RustDesk -ErrorAction SilentlyContinue))) { Write-Host '.' -NoNewline; Start-Sleep -Seconds 5 } Write-Host '' if (-not (Test-Path $rustdeskExe)) { throw "Install did not complete within 3 minutes. Check for a hidden SmartScreen/UAC dialog, then try running the installer by hand: $InstallerPath --silent-install" } if ($proc -and -not $proc.HasExited) { Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue } Get-Process rustdesk -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq $InstallerPath } | Stop-Process -Force -ErrorAction SilentlyContinue } # 3. Server config - written for the installing user AND the service (unattended sessions) $toml = @" rendezvous_server = '$IdServer' [options] custom-rendezvous-server = '$IdServer' relay-server = '$IdServer' key = '$Key' "@ $configTargets = @( (Join-Path $env:APPDATA 'RustDesk\config\RustDesk2.toml'), 'C:\Windows\ServiceProfiles\LocalService\AppData\Roaming\RustDesk\config\RustDesk2.toml' ) foreach ($path in $configTargets) { if (Test-Step $path "write server config (ID server $IdServer)") { Write-Host "Writing config: $path" -ForegroundColor Cyan New-Item -ItemType Directory -Force -Path (Split-Path -Parent $path) | Out-Null [IO.File]::WriteAllText($path, $toml) } } if (Test-Step 'RustDesk service' 'restart to pick up config') { # Right after --silent-install the service can still be start-pending, which makes # Restart-Service throw "Cannot open RustDesk service" - retry, and never die over it: # the config is already on disk and applies at the next service start anyway. Write-Host 'Restarting RustDesk service ...' -ForegroundColor Cyan $restarted = $false foreach ($attempt in 1..3) { try { $svc = Get-Service -Name RustDesk -ErrorAction Stop if ($svc.Status -ne 'Running') { $svc.WaitForStatus('Running', '00:00:20') } Restart-Service -Name RustDesk -ErrorAction Stop $restarted = $true; break } catch { if ($attempt -lt 3) { Start-Sleep -Seconds 10 } } } if (-not $restarted) { Write-Warning 'Could not restart the RustDesk service - config is written and will apply at the next service start. Run "Restart-Service RustDesk" later or reboot.' } } # 4. Optional permanent password (unattended access) if ($Password -and (Test-Step 'RustDesk' 'set permanent password')) { $plain = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password)) & "$env:ProgramFiles\RustDesk\rustdesk.exe" --password $plain $plain = $null Write-Host 'Permanent password set - record it in the vault against this machine.' -ForegroundColor Yellow } # 5. Report resulting ID (--get-id can hang on some machines - give it 15s, it's cosmetic) Write-Host 'Querying client ID (15s timeout) ...' -ForegroundColor Cyan $idJob = Start-Job -ScriptBlock { & "$env:ProgramFiles\RustDesk\rustdesk.exe" --get-id 2>$null } $id = if (Wait-Job $idJob -Timeout 15) { Receive-Job $idJob } else { '(timed out - read it from the RustDesk window)' } Remove-Job $idJob -Force -ErrorAction SilentlyContinue [pscustomobject]@{ Installed = Test-Path $rustdeskExe IdServer = $IdServer ClientId = $id } | Format-List