Add self-hosted RustDesk bootstrap management
This commit is contained in:
@@ -51,6 +51,7 @@ Operational documentation:
|
||||
- [Windows domain join and remote-access onboarding](docs/windows-client-onboarding.md)
|
||||
- [Required Credential Provider client enrollment](docs/client-enrollment.md)
|
||||
- [Linux client enrollment with realmd and SSSD](docs/linux-client-enrollment.md)
|
||||
- [Self-hosted RustDesk server and managed Windows remote access](docs/rustdesk-operations.md)
|
||||
- [Domain monitoring, usage reports, and six-month retention](docs/monitoring.md)
|
||||
- [Decision: do not persist password verifiers in Redis](docs/decisions/0001-no-password-cache.md)
|
||||
|
||||
|
||||
@@ -55,6 +55,8 @@ El proceso crea o configura de forma idempotente:
|
||||
- RDP con NLA, WinRM/PowerShell Remoting y reglas administrativas limitadas a
|
||||
la subred privada indicada, incluso si Windows tarda en reconocer el perfil
|
||||
Domain después de la promoción;
|
||||
- servidor RustDesk OSS autoalojado (`hbbs` y `hbbr`) y su cliente administrado
|
||||
en el propio DC, con puertos de administración limitados a la subred privada;
|
||||
- pantalla, suspensión e hibernación en Nunca.
|
||||
|
||||
En un servidor con dos NIC, el bootstrap desactiva el registro DNS de la NIC de
|
||||
@@ -111,6 +113,9 @@ equipos. La contraseña existe sólo en memoria. El bootstrap:
|
||||
8. sólo entonces ejecuta `Add-Computer` dentro de `OU=Laboratorio` y reinicia;
|
||||
9. al arrancar, activa RDP/NLA, WinRM y las reglas Domain, y vuelve a validar el
|
||||
enrolamiento.
|
||||
10. instala RustDesk desde el MSI oficial comprobado, lo apunta al servidor
|
||||
`rustdesk.lci.lasalle.mx` y registra el ID del dispositivo en el inventario
|
||||
protegido del DC.
|
||||
|
||||
Para elegir adaptador o nombre del equipo explícitamente:
|
||||
|
||||
@@ -126,6 +131,12 @@ La IP del argumento es siempre la IP fija **actual del servidor**, no una IP que
|
||||
queda compilada en el Credential Provider. El proveedor usa después el nombre
|
||||
DNS `sgu-auth.lci.lasalle.mx`, que el bootstrap del servidor actualiza.
|
||||
|
||||
La administración gráfica autoalojada se documenta en
|
||||
[rustdesk-operations.md](rustdesk-operations.md). Durante la primera instalación
|
||||
el servidor y los clientes necesitan salida HTTPS para obtener los instaladores
|
||||
RustDesk verificados; el tráfico de soporte posterior permanece dentro de la
|
||||
subred privada del laboratorio.
|
||||
|
||||
Un administrador del dominio todavía puede ignorar deliberadamente este flujo y
|
||||
ejecutar `Add-Computer` a mano; ninguna GPO puede impedir a un administrador del
|
||||
bosque modificar el dominio. Para la operación soportada, el script aplica una
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
# RustDesk autoalojado y acceso remoto administrado
|
||||
|
||||
El bootstrap del controlador de dominio instala un servidor RustDesk OSS
|
||||
autoalojado y el bootstrap de cada cliente Windows inscrito instala el agente
|
||||
RustDesk administrado. Esto permite administrar el propio controlador y cada
|
||||
cliente del laboratorio sin depender de los servidores públicos de RustDesk.
|
||||
|
||||
## Componentes y red
|
||||
|
||||
El controlador inicia ambos componentes bajo `SYSTEM` mediante las tareas
|
||||
programadas `SGU-RustDesk-hbbs` y `SGU-RustDesk-hbbr`:
|
||||
|
||||
| Componente | Función | Puerto entrante |
|
||||
| --- | --- | --- |
|
||||
| `hbbs` | ID/rendezvous y prueba NAT | TCP 21115-21116 y UDP 21116 |
|
||||
| `hbbr` | Relay para sesiones que no pueden ser directas | TCP 21117 |
|
||||
|
||||
Las reglas se llaman **SGU RustDesk** y sólo aceptan la subred privada que se
|
||||
indicó al bootstrap del servidor. No se habilitan el servidor web ni los puertos
|
||||
21118/21119. Los clientes y el servidor necesitan salida HTTPS a GitHub sólo
|
||||
durante una primera instalación o actualización, para descargar el binario
|
||||
verificado por SHA-256.
|
||||
|
||||
El nombre interno usado por los clientes es `rustdesk.lci.lasalle.mx`; el
|
||||
bootstrap del DC mantiene su registro A en DNS apuntando a la IP fija del
|
||||
controlador.
|
||||
|
||||
## Alta automática de un equipo Windows
|
||||
|
||||
Al finalizar `Invoke-SguClientBootstrap.ps1`, antes de considerar válido el
|
||||
enrolamiento, el flujo:
|
||||
|
||||
1. lee la clave pública del servidor a través de la sesión autenticada con el
|
||||
DC;
|
||||
2. instala RustDesk desde el MSI oficial, comprobando el SHA-256 fijado;
|
||||
3. instala y arranca el servicio `RustDesk` como automático;
|
||||
4. configura ID, relay y clave pública del servidor privado en el contexto del
|
||||
servicio y para perfiles nuevos;
|
||||
5. crea una contraseña única de acceso desatendido, cifrada con DPAPI local y
|
||||
protegida por ACL para `SYSTEM` y administradores locales;
|
||||
6. registra únicamente el ID y la contraseña cifrada en el inventario del DC.
|
||||
|
||||
La contraseña no se imprime, no se pone en el manifiesto y no se añade a los
|
||||
logs. El inventario del servidor usa DPAPI de la máquina y está limitado por ACL
|
||||
a `SYSTEM` y administradores del servidor.
|
||||
|
||||
El mismo flujo se aplica al DC, por lo que también se puede administrar de forma
|
||||
remota. Reejecutar el bootstrap mantiene el ID y la contraseña existentes, y
|
||||
vuelve a validar configuración, tareas, servicio y conectividad sin crear otro
|
||||
registro.
|
||||
|
||||
## Verificación y operación
|
||||
|
||||
En el DC, como administrador:
|
||||
|
||||
```powershell
|
||||
Get-ScheduledTask SGU-RustDesk-hbbs,SGU-RustDesk-hbbr |
|
||||
Select-Object TaskName,State
|
||||
Get-NetTCPConnection -State Listen -LocalPort 21116,21117
|
||||
Get-Content C:\ProgramData\SGU\RustDesk\server.json
|
||||
& C:\ProgramData\SGU\RustDesk\Get-SguRustDeskDevice.ps1
|
||||
```
|
||||
|
||||
El último comando muestra los nombres, IDs y fecha de alta, sin contraseñas. Si
|
||||
un administrador necesita recuperar una contraseña para conectarse desde el
|
||||
cliente controlador de RustDesk, puede hacerlo explícitamente en la consola del
|
||||
DC:
|
||||
|
||||
```powershell
|
||||
& C:\ProgramData\SGU\RustDesk\Get-SguRustDeskDevice.ps1 `
|
||||
-ComputerName LCI-01 -RevealPassword
|
||||
```
|
||||
|
||||
Trata esa salida como una credencial administrativa: no la pegues en tickets,
|
||||
capturas ni registros. En RustDesk, conecta usando el ID inventariado y el modo
|
||||
de autenticación por contraseña permanente.
|
||||
|
||||
En un cliente, los indicadores locales son:
|
||||
|
||||
```powershell
|
||||
Get-Service RustDesk
|
||||
Get-Content C:\ProgramData\SGU\RustDesk\Client\device.json
|
||||
Test-NetConnection rustdesk.lci.lasalle.mx -Port 21116
|
||||
```
|
||||
|
||||
Si un agente deja de funcionar, se puede repetir el bootstrap del cliente. El
|
||||
guardián de enrolamiento también repara la configuración de RustDesk al inicio
|
||||
cuando la información del servidor sigue presente en su estado de enrolamiento.
|
||||
|
||||
## Límites operativos
|
||||
|
||||
Este alcance automatiza el cliente Windows entregado por
|
||||
`Invoke-SguClientBootstrap.ps1`. Los clientes Linux conservan su bootstrap PAM/
|
||||
SSSD independiente; su integración con RustDesk requiere un flujo de inventario
|
||||
de credenciales propio y no reutiliza ni expone las contraseñas de Windows.
|
||||
@@ -24,6 +24,8 @@ param(
|
||||
[string[]]$DomainDnsServerAddresses = @('192.168.50.10'),
|
||||
[string]$RemoteDesktopPrincipal = 'LCI\SG-Laboratorio-Usuarios-RDP',
|
||||
[string]$DotNetRuntimeInstallerPath,
|
||||
[string]$RustDeskServerAddress,
|
||||
[string]$RustDeskServerPublicKey,
|
||||
[switch]$SkipRestart
|
||||
)
|
||||
|
||||
@@ -40,7 +42,8 @@ foreach ($scriptName in @(
|
||||
'Test-SguClientEnrollment.ps1',
|
||||
'Repair-SguClientEnrollment.ps1',
|
||||
'Enable-LabRemoteAccess.ps1',
|
||||
'Enable-SguClientMonitoring.ps1')) {
|
||||
'Enable-SguClientMonitoring.ps1',
|
||||
'Install-SguRustDeskClient.ps1')) {
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $PSScriptRoot $scriptName) -PathType Leaf)) {
|
||||
throw "$scriptName must be beside Enroll-SguDomainClient.ps1."
|
||||
}
|
||||
@@ -73,8 +76,16 @@ $guardParams = @{
|
||||
TimeoutSeconds = 90
|
||||
RemoteDesktopPrincipal = $RemoteDesktopPrincipal
|
||||
DotNetRuntimeInstallerPath = $DotNetRuntimeInstallerPath
|
||||
RustDeskServerAddress = $RustDeskServerAddress
|
||||
RustDeskServerPublicKey = $RustDeskServerPublicKey
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($RustDeskServerAddress) -xor
|
||||
[string]::IsNullOrWhiteSpace($RustDeskServerPublicKey)) {
|
||||
throw 'RustDeskServerAddress and RustDeskServerPublicKey must be supplied together.'
|
||||
}
|
||||
$rustDeskResult = $null
|
||||
|
||||
if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Install and verify SGU before joining the domain')) {
|
||||
# The broker uses a domain DNS name even before the machine joins the
|
||||
# domain. Point at AD DNS first so the provider-first health check works on
|
||||
@@ -85,10 +96,19 @@ if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Install and verify SGU before jo
|
||||
Resolve-DnsName -Type SRV "_ldap._tcp.dc._msdcs.$DomainName" -ErrorAction Stop | Out-Null
|
||||
|
||||
& (Join-Path $PSScriptRoot 'Install-CredentialProvider.ps1') @installParams | Out-Null
|
||||
if ($RustDeskServerAddress) {
|
||||
$rustDeskResult = & (Join-Path $PSScriptRoot 'Install-SguRustDeskClient.ps1') `
|
||||
-ServerAddress $RustDeskServerAddress `
|
||||
-ServerPublicKey $RustDeskServerPublicKey
|
||||
}
|
||||
& (Join-Path $PSScriptRoot 'Install-SguEnrollmentGuard.ps1') @guardParams | Out-Null
|
||||
|
||||
$preJoin = & (Join-Path $PSScriptRoot 'Test-SguClientEnrollment.ps1') `
|
||||
-RequireBrokerHealth
|
||||
$testParameters = @{ RequireBrokerHealth = $true }
|
||||
if ($RustDeskServerAddress) {
|
||||
$testParameters.RequireRustDesk = $true
|
||||
$testParameters.RustDeskServerAddress = $RustDeskServerAddress
|
||||
}
|
||||
$preJoin = & (Join-Path $PSScriptRoot 'Test-SguClientEnrollment.ps1') @testParameters
|
||||
if (-not $preJoin.IsValid) {
|
||||
throw "Domain join refused because SGU enrollment is invalid: $($preJoin.Issues -join ' ')"
|
||||
}
|
||||
@@ -98,10 +118,18 @@ if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Install and verify SGU before jo
|
||||
-RemoteDesktopPrincipal $RemoteDesktopPrincipal `
|
||||
-EnableAdministrativeFirewallGroups | Out-Null
|
||||
& (Join-Path $PSScriptRoot 'Enable-SguClientMonitoring.ps1') | Out-Null
|
||||
return & (Join-Path $PSScriptRoot 'Test-SguClientEnrollment.ps1') `
|
||||
-RequireDomainJoined `
|
||||
-RequireRemoteAccess `
|
||||
-RemoteDesktopPrincipal $RemoteDesktopPrincipal
|
||||
$postJoinParameters = @{
|
||||
RequireDomainJoined = $true
|
||||
RequireRemoteAccess = $true
|
||||
RemoteDesktopPrincipal = $RemoteDesktopPrincipal
|
||||
}
|
||||
if ($RustDeskServerAddress) {
|
||||
$postJoinParameters.RequireRustDesk = $true
|
||||
$postJoinParameters.RustDeskServerAddress = $RustDeskServerAddress
|
||||
}
|
||||
$postJoin = & (Join-Path $PSScriptRoot 'Test-SguClientEnrollment.ps1') @postJoinParameters
|
||||
$postJoin | Add-Member -NotePropertyName RustDesk -NotePropertyValue $rustDeskResult
|
||||
return $postJoin
|
||||
}
|
||||
|
||||
if (-not $DomainCredential) {
|
||||
@@ -132,5 +160,6 @@ if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Install and verify SGU before jo
|
||||
ComputerName = if ($NewComputerName) { $NewComputerName } else { $env:COMPUTERNAME }
|
||||
DomainName = $DomainName
|
||||
ProviderValidatedBeforeJoin = $true
|
||||
RustDesk = $rustDeskResult
|
||||
RestartRequired = [bool]$SkipRestart
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ComputerName,
|
||||
[switch]$RevealPassword,
|
||||
[string]$InventoryRoot = "$env:ProgramData\SGU\RustDesk\Devices"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Assert-Administrator {
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
|
||||
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
||||
throw 'Only a local administrator can read the RustDesk device inventory.'
|
||||
}
|
||||
}
|
||||
|
||||
function Initialize-DataProtection {
|
||||
if (-not ('SguRustDeskDataProtection' -as [type])) {
|
||||
Add-Type -TypeDefinition @'
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
public static class SguRustDeskDataProtection {
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct DataBlob { public int cbData; public IntPtr pbData; }
|
||||
[DllImport("crypt32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern bool CryptProtectData(ref DataBlob input, string description, IntPtr entropy, IntPtr reserved, IntPtr prompt, int flags, out DataBlob output);
|
||||
[DllImport("crypt32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern bool CryptUnprotectData(ref DataBlob input, IntPtr description, IntPtr entropy, IntPtr reserved, IntPtr prompt, int flags, out DataBlob output);
|
||||
[DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr LocalFree(IntPtr memory);
|
||||
private const int CryptProtectLocalMachine = 0x4;
|
||||
private static DataBlob ToBlob(byte[] value) { var blob = new DataBlob { cbData = value.Length, pbData = IntPtr.Zero }; if (value.Length > 0) { blob.pbData = Marshal.AllocHGlobal(value.Length); Marshal.Copy(value, 0, blob.pbData, value.Length); } return blob; }
|
||||
private static byte[] FromBlob(DataBlob blob) { var value = new byte[blob.cbData]; if (blob.cbData > 0) Marshal.Copy(blob.pbData, value, 0, blob.cbData); return value; }
|
||||
public static byte[] Protect(byte[] value) { var input = ToBlob(value); var output = new DataBlob(); try { if (!CryptProtectData(ref input, null, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, CryptProtectLocalMachine, out output)) throw new Win32Exception(Marshal.GetLastWin32Error()); return FromBlob(output); } finally { if (input.pbData != IntPtr.Zero) Marshal.FreeHGlobal(input.pbData); if (output.pbData != IntPtr.Zero) LocalFree(output.pbData); } }
|
||||
public static byte[] Unprotect(byte[] value) { var input = ToBlob(value); var output = new DataBlob(); try { if (!CryptUnprotectData(ref input, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, 0, out output)) throw new Win32Exception(Marshal.GetLastWin32Error()); return FromBlob(output); } finally { if (input.pbData != IntPtr.Zero) Marshal.FreeHGlobal(input.pbData); if (output.pbData != IntPtr.Zero) LocalFree(output.pbData); } }
|
||||
}
|
||||
'@ -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
|
||||
Assert-Administrator
|
||||
Initialize-DataProtection
|
||||
if (-not (Test-Path -LiteralPath $InventoryRoot -PathType Container)) {
|
||||
return @()
|
||||
}
|
||||
|
||||
$entries = @(Get-ChildItem -LiteralPath $InventoryRoot -Filter '*.json' -File |
|
||||
ForEach-Object {
|
||||
$metadata = Get-Content -LiteralPath $_.FullName -Raw | ConvertFrom-Json
|
||||
if ($ComputerName -and -not $metadata.ComputerName.Equals($ComputerName, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
return
|
||||
}
|
||||
$result = [ordered]@{
|
||||
ComputerName = [string]$metadata.ComputerName
|
||||
RustDeskId = [string]$metadata.RustDeskId
|
||||
RegisteredAt = [datetime]$metadata.RegisteredAt
|
||||
}
|
||||
if ($RevealPassword) {
|
||||
$secretPath = [string]$metadata.SecretPath
|
||||
if (-not (Test-Path -LiteralPath $secretPath -PathType Leaf)) {
|
||||
throw "The protected RustDesk credential for $($metadata.ComputerName) is missing."
|
||||
}
|
||||
$result.AccessPassword = [Text.Encoding]::UTF8.GetString(
|
||||
[SguRustDeskDataProtection]::Unprotect(
|
||||
[IO.File]::ReadAllBytes($secretPath)))
|
||||
}
|
||||
[pscustomobject]$result
|
||||
})
|
||||
|
||||
$entries | Sort-Object ComputerName
|
||||
@@ -10,6 +10,7 @@ param(
|
||||
[string]$DomainName = 'lci.lasalle.mx',
|
||||
[string]$DomainNetbios = 'LCI',
|
||||
[string]$BrokerRecordName = 'sgu-auth',
|
||||
[string]$RustDeskRecordName = 'rustdesk',
|
||||
[string]$PackageSharePath = 'C:\Packages',
|
||||
[securestring]$SafeModeAdministratorPassword,
|
||||
[switch]$SkipRestart,
|
||||
@@ -313,6 +314,7 @@ if ($Resume -or (-not $ServerIPv4Address -and $existingState)) {
|
||||
$DomainName = [string]$existingState.DomainName
|
||||
$DomainNetbios = [string]$existingState.DomainNetbios
|
||||
$BrokerRecordName = [string]$existingState.BrokerRecordName
|
||||
$RustDeskRecordName = if ($existingState.RustDeskRecordName) { [string]$existingState.RustDeskRecordName } else { $RustDeskRecordName }
|
||||
$PackageSharePath = [string]$existingState.PackageSharePath
|
||||
}
|
||||
|
||||
@@ -336,6 +338,7 @@ else {
|
||||
$NetworkInterfaceAlias = Resolve-PrivateInterfaceAlias -RequestedAlias $NetworkInterfaceAlias
|
||||
$baseDn = Get-DomainBaseDn -DnsDomainName $DomainName
|
||||
$brokerDnsName = "$BrokerRecordName.$DomainName"
|
||||
$rustDeskDnsName = "$RustDeskRecordName.$DomainName"
|
||||
$stagedScriptPath = Join-Path $bootstrapRoot 'Initialize-SguDomainController.ps1'
|
||||
$scriptsRoot = Join-Path $bootstrapRoot 'payload\scripts'
|
||||
$brokerPublishPath = Join-Path $bootstrapRoot 'payload\broker'
|
||||
@@ -349,9 +352,13 @@ foreach ($requiredPath in @(
|
||||
(Join-Path $scriptsRoot 'Set-SguDomainUserPolicies.ps1'),
|
||||
(Join-Path $scriptsRoot 'Enable-SguServerRemoteManagement.ps1'),
|
||||
(Join-Path $scriptsRoot 'Install-SguDomainMonitoring.ps1'),
|
||||
(Join-Path $scriptsRoot 'Install-SguRustDeskClient.ps1'),
|
||||
(Join-Path $scriptsRoot 'Install-SguRustDeskServer.ps1'),
|
||||
(Join-Path $scriptsRoot 'Invoke-SguMonitoringMaintenance.ps1'),
|
||||
(Join-Path $scriptsRoot 'Get-SguRustDeskDevice.ps1'),
|
||||
(Join-Path $scriptsRoot 'Get-SguUsageReport.ps1'),
|
||||
(Join-Path $scriptsRoot 'Get-SguBrokerLog.ps1'),
|
||||
(Join-Path $scriptsRoot 'Register-SguRustDeskDevice.ps1'),
|
||||
(Join-Path $brokerPublishPath 'SGU.AuthBroker.exe'))) {
|
||||
if (-not (Test-Path -LiteralPath $requiredPath -PathType Leaf)) {
|
||||
throw "The server bootstrap package is incomplete: $requiredPath"
|
||||
@@ -378,6 +385,7 @@ if (-not $existingState) {
|
||||
DomainName = $DomainName
|
||||
DomainNetbios = $DomainNetbios
|
||||
BrokerRecordName = $BrokerRecordName
|
||||
RustDeskRecordName = $RustDeskRecordName
|
||||
PackageSharePath = $PackageSharePath
|
||||
}
|
||||
[IO.File]::WriteAllText(
|
||||
@@ -516,6 +524,10 @@ if (-not $remoteDesktopGroup) {
|
||||
-RecordName $BrokerRecordName `
|
||||
-IPv4Address $ServerIPv4Address `
|
||||
-ExternalForwarders $DnsForwarders | Out-Null
|
||||
& (Join-Path $scriptsRoot 'Set-LabBrokerDns.ps1') `
|
||||
-ZoneName $DomainName `
|
||||
-RecordName $RustDeskRecordName `
|
||||
-IPv4Address $ServerIPv4Address | Out-Null
|
||||
|
||||
$certificateDirectory = Join-Path $bootstrapRoot 'certificates'
|
||||
$serverCertificate = Get-ChildItem Cert:\LocalMachine\My |
|
||||
@@ -626,6 +638,32 @@ $userPolicyParameters = @{
|
||||
}
|
||||
& (Join-Path $scriptsRoot 'Set-SguDomainUserPolicies.ps1') @userPolicyParameters | Out-Null
|
||||
|
||||
$rustDeskServer = & (Join-Path $scriptsRoot 'Install-SguRustDeskServer.ps1') `
|
||||
-ServerAddress $rustDeskDnsName `
|
||||
-FirewallRemoteAddress $privateSubnet
|
||||
$rustDeskManagementRoot = Join-Path $env:ProgramData 'SGU\RustDesk'
|
||||
New-Item -ItemType Directory -Path $rustDeskManagementRoot -Force | Out-Null
|
||||
foreach ($scriptName in @('Register-SguRustDeskDevice.ps1', 'Get-SguRustDeskDevice.ps1')) {
|
||||
Copy-Item -LiteralPath (Join-Path $scriptsRoot $scriptName) `
|
||||
-Destination (Join-Path $rustDeskManagementRoot $scriptName) -Force
|
||||
}
|
||||
$rustDeskServerClient = & (Join-Path $scriptsRoot 'Install-SguRustDeskClient.ps1') `
|
||||
-ServerAddress $rustDeskDnsName `
|
||||
-ServerPublicKey $rustDeskServer.PublicKey
|
||||
$rustDeskPasswordPointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR(
|
||||
$rustDeskServerClient.AccessPassword)
|
||||
try {
|
||||
$rustDeskPassword = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($rustDeskPasswordPointer)
|
||||
& (Join-Path $rustDeskManagementRoot 'Register-SguRustDeskDevice.ps1') `
|
||||
-ComputerName $env:COMPUTERNAME `
|
||||
-RustDeskId $rustDeskServerClient.RustDeskId `
|
||||
-AccessPassword $rustDeskPassword | Out-Null
|
||||
}
|
||||
finally {
|
||||
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($rustDeskPasswordPointer)
|
||||
$rustDeskPassword = $null
|
||||
}
|
||||
|
||||
$validation = [ordered]@{
|
||||
CompletedAt = (Get-Date).ToString('o')
|
||||
ComputerName = $env:COMPUTERNAME
|
||||
@@ -637,6 +675,12 @@ $validation = [ordered]@{
|
||||
BrokerPortListening = [bool](Get-NetTCPConnection -LocalPort 8443 -State Listen -ErrorAction SilentlyContinue)
|
||||
WinRM = (Get-Service WinRM).Status.ToString()
|
||||
RemoteDesktop = (Get-Service TermService).Status.ToString()
|
||||
RustDeskServerAddress = $rustDeskServer.ServerAddress
|
||||
RustDeskHbbsTask = $rustDeskServer.HbbsTask
|
||||
RustDeskHbbrTask = $rustDeskServer.HbbrTask
|
||||
RustDeskHbbsListening = $rustDeskServer.HbbsListening
|
||||
RustDeskHbbrListening = $rustDeskServer.HbbrListening
|
||||
RustDeskServerClientId = $rustDeskServerClient.RustDeskId
|
||||
EventCollector = (Get-Service Wecsvc).Status.ToString()
|
||||
EventSubscription = @(& wecutil.exe enum-subscription) -contains 'SGU-Lab-Monitoring'
|
||||
MonitoringRetentionDays = 183
|
||||
@@ -652,6 +696,10 @@ if ($validation.BrokerService -ne 'Running' -or
|
||||
-not $validation.BrokerPortListening -or
|
||||
$validation.WinRM -ne 'Running' -or
|
||||
$validation.RemoteDesktop -ne 'Running' -or
|
||||
$validation.RustDeskHbbsTask -ne 'Running' -or
|
||||
$validation.RustDeskHbbrTask -ne 'Running' -or
|
||||
-not $validation.RustDeskHbbsListening -or
|
||||
-not $validation.RustDeskHbbrListening -or
|
||||
$validation.EventCollector -ne 'Running' -or
|
||||
-not $validation.EventSubscription) {
|
||||
throw 'Server finalization did not pass service validation. Review bootstrap.log and re-run the bootstrap.'
|
||||
|
||||
@@ -19,7 +19,9 @@ param(
|
||||
[ValidateRange(2, 90)]
|
||||
[int]$TimeoutSeconds = 90,
|
||||
[string]$RemoteDesktopPrincipal = 'LCI\SG-Laboratorio-Usuarios-RDP',
|
||||
[string]$DotNetRuntimeInstallerPath
|
||||
[string]$DotNetRuntimeInstallerPath,
|
||||
[string]$RustDeskServerAddress,
|
||||
[string]$RustDeskServerPublicKey
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
@@ -29,6 +31,7 @@ $sourceScripts = @(
|
||||
'Install-CredentialProvider.ps1',
|
||||
'Enable-LabRemoteAccess.ps1',
|
||||
'Enable-SguClientMonitoring.ps1',
|
||||
'Install-SguRustDeskClient.ps1',
|
||||
'Test-SguClientEnrollment.ps1',
|
||||
'Repair-SguClientEnrollment.ps1'
|
||||
)
|
||||
@@ -54,6 +57,10 @@ if ($DotNetRuntimeInstallerPath -and
|
||||
-not (Test-Path -LiteralPath $DotNetRuntimeInstallerPath -PathType Leaf)) {
|
||||
throw 'DotNetRuntimeInstallerPath does not exist.'
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($RustDeskServerAddress) -xor
|
||||
[string]::IsNullOrWhiteSpace($RustDeskServerPublicKey)) {
|
||||
throw 'RustDeskServerAddress and RustDeskServerPublicKey must be supplied together.'
|
||||
}
|
||||
|
||||
if ($PSCmdlet.ShouldProcess($enrollmentRoot, 'Install the SGU enrollment repair guard')) {
|
||||
New-Item -ItemType Directory -Path $enrollmentRoot -Force | Out-Null
|
||||
@@ -88,6 +95,8 @@ if ($PSCmdlet.ShouldProcess($enrollmentRoot, 'Install the SGU enrollment repair
|
||||
TimeoutSeconds = $TimeoutSeconds
|
||||
RemoteDesktopPrincipal = $RemoteDesktopPrincipal
|
||||
DotNetRuntimeInstallerPath = $guardRuntimeInstaller
|
||||
RustDeskServerAddress = $RustDeskServerAddress
|
||||
RustDeskServerPublicKey = $RustDeskServerPublicKey
|
||||
}
|
||||
$configurationPath = Join-Path $enrollmentRoot 'enrollment.json'
|
||||
[IO.File]::WriteAllText(
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
[CmdletBinding(SupportsShouldProcess)]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[ValidatePattern('^[A-Za-z0-9][A-Za-z0-9.-]*$')]
|
||||
[string]$ServerAddress,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[ValidatePattern('^[A-Za-z0-9+/=]+$')]
|
||||
[string]$ServerPublicKey,
|
||||
|
||||
[securestring]$AccessPassword,
|
||||
[string]$InstallRoot = "$env:ProgramFiles\RustDesk",
|
||||
[string]$StateRoot = "$env:ProgramData\SGU\RustDesk\Client",
|
||||
[string]$ClientVersion = '1.4.9',
|
||||
[uri]$DownloadUri = 'https://github.com/rustdesk/rustdesk/releases/download/1.4.9/rustdesk-1.4.9-x86_64.msi',
|
||||
[ValidatePattern('^[A-Fa-f0-9]{64}$')]
|
||||
[string]$ExpectedSha256 = 'C87D2F4CEF2A5ACD6003B6507DCFBF5D5168A256DB082CD90B54D35193224AAA'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
$downloadRoot = Join-Path $env:ProgramData 'SGU\RustDesk\Downloads'
|
||||
$installerPath = Join-Path $downloadRoot "rustdesk-$ClientVersion-x86_64.msi"
|
||||
$installerLogPath = Join-Path $downloadRoot "rustdesk-$ClientVersion-install.log"
|
||||
$secretPath = Join-Path $StateRoot 'access.secret'
|
||||
$devicePath = Join-Path $StateRoot 'device.json'
|
||||
|
||||
function Assert-Administrator {
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
|
||||
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
||||
throw 'Run this script from an elevated Windows PowerShell session.'
|
||||
}
|
||||
}
|
||||
|
||||
function Initialize-DataProtection {
|
||||
if (-not ('SguRustDeskDataProtection' -as [type])) {
|
||||
Add-Type -TypeDefinition @'
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
public static class SguRustDeskDataProtection {
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct DataBlob { public int cbData; public IntPtr pbData; }
|
||||
|
||||
[DllImport("crypt32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern bool CryptProtectData(ref DataBlob input, string description,
|
||||
IntPtr optionalEntropy, IntPtr reserved, IntPtr prompt, int flags, out DataBlob output);
|
||||
|
||||
[DllImport("crypt32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern bool CryptUnprotectData(ref DataBlob input, IntPtr description,
|
||||
IntPtr optionalEntropy, IntPtr reserved, IntPtr prompt, int flags, out DataBlob output);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern IntPtr LocalFree(IntPtr memory);
|
||||
|
||||
private const int CryptProtectLocalMachine = 0x4;
|
||||
|
||||
private static DataBlob ToBlob(byte[] value) {
|
||||
var blob = new DataBlob { cbData = value.Length, pbData = IntPtr.Zero };
|
||||
if (value.Length > 0) {
|
||||
blob.pbData = Marshal.AllocHGlobal(value.Length);
|
||||
Marshal.Copy(value, 0, blob.pbData, value.Length);
|
||||
}
|
||||
return blob;
|
||||
}
|
||||
|
||||
private static byte[] FromBlob(DataBlob blob) {
|
||||
var value = new byte[blob.cbData];
|
||||
if (blob.cbData > 0) Marshal.Copy(blob.pbData, value, 0, blob.cbData);
|
||||
return value;
|
||||
}
|
||||
|
||||
public static byte[] Protect(byte[] value) {
|
||||
var input = ToBlob(value); var output = new DataBlob();
|
||||
try {
|
||||
if (!CryptProtectData(ref input, null, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero,
|
||||
CryptProtectLocalMachine, out output)) {
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
return FromBlob(output);
|
||||
} finally {
|
||||
if (input.pbData != IntPtr.Zero) Marshal.FreeHGlobal(input.pbData);
|
||||
if (output.pbData != IntPtr.Zero) LocalFree(output.pbData);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] Unprotect(byte[] value) {
|
||||
var input = ToBlob(value); var output = new DataBlob();
|
||||
try {
|
||||
if (!CryptUnprotectData(ref input, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero,
|
||||
0, out output)) {
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
return FromBlob(output);
|
||||
} finally {
|
||||
if (input.pbData != IntPtr.Zero) Marshal.FreeHGlobal(input.pbData);
|
||||
if (output.pbData != IntPtr.Zero) LocalFree(output.pbData);
|
||||
}
|
||||
}
|
||||
}
|
||||
'@ -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
|
||||
function Set-PrivateDirectoryAcl {
|
||||
param([Parameter(Mandatory)][string]$Path)
|
||||
|
||||
New-Item -ItemType Directory -Path $Path -Force | Out-Null
|
||||
$acl = New-Object Security.AccessControl.DirectorySecurity
|
||||
$acl.SetAccessRuleProtection($true, $false)
|
||||
$inheritance = [Security.AccessControl.InheritanceFlags]'ContainerInherit,ObjectInherit'
|
||||
$allow = [Security.AccessControl.AccessControlType]::Allow
|
||||
foreach ($sid in @('S-1-5-18', 'S-1-5-32-544')) {
|
||||
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new(
|
||||
[Security.Principal.SecurityIdentifier]::new($sid),
|
||||
[Security.AccessControl.FileSystemRights]::FullControl,
|
||||
$inheritance,
|
||||
[Security.AccessControl.PropagationFlags]::None,
|
||||
$allow))
|
||||
}
|
||||
Set-Acl -LiteralPath $Path -AclObject $acl
|
||||
}
|
||||
|
||||
function ConvertTo-PlainText {
|
||||
param([Parameter(Mandatory)][securestring]$SecureString)
|
||||
|
||||
$pointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString)
|
||||
try {
|
||||
return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($pointer)
|
||||
}
|
||||
finally {
|
||||
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($pointer)
|
||||
}
|
||||
}
|
||||
|
||||
function New-RandomAccessPassword {
|
||||
$characters = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%*+-_'.ToCharArray()
|
||||
$bytes = New-Object byte[] 24
|
||||
$rng = [Security.Cryptography.RandomNumberGenerator]::Create()
|
||||
try {
|
||||
$rng.GetBytes($bytes)
|
||||
}
|
||||
finally {
|
||||
$rng.Dispose()
|
||||
}
|
||||
$value = -join ($bytes | ForEach-Object { $characters[$_ % $characters.Length] })
|
||||
return (ConvertTo-SecureString -String $value -AsPlainText -Force)
|
||||
}
|
||||
|
||||
function Save-AccessPassword {
|
||||
param([Parameter(Mandatory)][securestring]$Password)
|
||||
|
||||
$plainText = ConvertTo-PlainText -SecureString $Password
|
||||
try {
|
||||
$cipherText = [SguRustDeskDataProtection]::Protect(
|
||||
[Text.Encoding]::UTF8.GetBytes($plainText))
|
||||
[IO.File]::WriteAllBytes($secretPath, $cipherText)
|
||||
}
|
||||
finally {
|
||||
$plainText = $null
|
||||
}
|
||||
}
|
||||
|
||||
function Get-SavedAccessPassword {
|
||||
if (-not (Test-Path -LiteralPath $secretPath -PathType Leaf)) {
|
||||
return $null
|
||||
}
|
||||
$plainText = [Text.Encoding]::UTF8.GetString(
|
||||
[SguRustDeskDataProtection]::Unprotect(
|
||||
[IO.File]::ReadAllBytes($secretPath)))
|
||||
try {
|
||||
return (ConvertTo-SecureString -String $plainText -AsPlainText -Force)
|
||||
}
|
||||
finally {
|
||||
$plainText = $null
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-FileHash {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Path,
|
||||
[Parameter(Mandatory)][string]$ExpectedHash
|
||||
)
|
||||
|
||||
$actualHash = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash
|
||||
if (-not $actualHash.Equals($ExpectedHash, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "SHA-256 verification failed for $Path."
|
||||
}
|
||||
}
|
||||
|
||||
function Test-TcpConnection {
|
||||
param([Parameter(Mandatory)][string]$HostName, [Parameter(Mandatory)][int]$Port)
|
||||
|
||||
$client = [Net.Sockets.TcpClient]::new()
|
||||
try {
|
||||
$connect = $client.BeginConnect($HostName, $Port, $null, $null)
|
||||
if (-not $connect.AsyncWaitHandle.WaitOne(5000)) {
|
||||
return $false
|
||||
}
|
||||
$client.EndConnect($connect)
|
||||
return $true
|
||||
}
|
||||
catch {
|
||||
return $false
|
||||
}
|
||||
finally {
|
||||
$client.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
Assert-Administrator
|
||||
Initialize-DataProtection
|
||||
if (-not $PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Install and configure the managed RustDesk client')) {
|
||||
return
|
||||
}
|
||||
|
||||
Set-PrivateDirectoryAcl -Path $StateRoot
|
||||
New-Item -ItemType Directory -Path $downloadRoot -Force | Out-Null
|
||||
$rustDeskExecutable = Join-Path $InstallRoot 'RustDesk.exe'
|
||||
$installedVersion = if (Test-Path -LiteralPath $rustDeskExecutable -PathType Leaf) {
|
||||
[string](Get-Item -LiteralPath $rustDeskExecutable).VersionInfo.ProductVersion
|
||||
}
|
||||
else {
|
||||
''
|
||||
}
|
||||
if (-not $installedVersion.StartsWith($ClientVersion, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
if (-not (Test-Path -LiteralPath $installerPath -PathType Leaf) -or
|
||||
(Get-FileHash -LiteralPath $installerPath -Algorithm SHA256).Hash -ne $ExpectedSha256) {
|
||||
Invoke-WebRequest -Uri $DownloadUri -OutFile $installerPath -UseBasicParsing
|
||||
}
|
||||
Assert-FileHash -Path $installerPath -ExpectedHash $ExpectedSha256
|
||||
# The vendor's MSI is the supported path for managed, silent Windows
|
||||
# deployment. Unlike the GUI-oriented EXE it does not require an
|
||||
# interactive desktop, which matters for startup/bootstrap execution.
|
||||
$msiArguments = "/i `"$installerPath`" /qn /norestart " +
|
||||
"INSTALLFOLDER=`"$InstallRoot`" CREATESTARTMENUSHORTCUTS=`"N`" " +
|
||||
"CREATEDESKTOPSHORTCUTS=`"N`" INSTALLPRINTER=`"N`" /l*v `"$installerLogPath`""
|
||||
$installer = Start-Process -FilePath (Join-Path $env:WINDIR 'System32\msiexec.exe') `
|
||||
-ArgumentList $msiArguments -Wait -PassThru
|
||||
if ($installer.ExitCode -notin @(0, 3010)) {
|
||||
throw "RustDesk MSI installation failed with exit code $($installer.ExitCode). See $installerLogPath."
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $rustDeskExecutable -PathType Leaf)) {
|
||||
throw "RustDesk installation did not create $rustDeskExecutable."
|
||||
}
|
||||
|
||||
$rustDeskService = Get-Service -Name 'RustDesk' -ErrorAction SilentlyContinue
|
||||
if (-not $rustDeskService) {
|
||||
$serviceInstaller = Start-Process -FilePath $rustDeskExecutable -ArgumentList '--install-service' `
|
||||
-Wait -PassThru
|
||||
if ($serviceInstaller.ExitCode -ne 0) {
|
||||
throw "RustDesk service installation failed with exit code $($serviceInstaller.ExitCode)."
|
||||
}
|
||||
Start-Sleep -Seconds 2
|
||||
$rustDeskService = Get-Service -Name 'RustDesk' -ErrorAction SilentlyContinue
|
||||
}
|
||||
if (-not $rustDeskService) {
|
||||
throw 'RustDesk did not register its Windows service.'
|
||||
}
|
||||
|
||||
Set-Service -Name $rustDeskService.Name -StartupType Automatic
|
||||
if ($rustDeskService.Status -ne 'Stopped') {
|
||||
Stop-Service -Name $rustDeskService.Name -Force
|
||||
$rustDeskService.WaitForStatus('Stopped', (New-TimeSpan -Seconds 20))
|
||||
}
|
||||
|
||||
$rendezvousAddress = "$ServerAddress`:21116"
|
||||
$relayAddress = "$ServerAddress`:21117"
|
||||
$configuration = @"
|
||||
rendezvous_server = '$rendezvousAddress'
|
||||
nat_type = 1
|
||||
serial = 0
|
||||
|
||||
[options]
|
||||
custom-rendezvous-server = '$rendezvousAddress'
|
||||
relay-server = '$relayAddress'
|
||||
key = '$ServerPublicKey'
|
||||
"@
|
||||
$configurationPaths = @(
|
||||
(Join-Path $env:ProgramData 'RustDesk\config\RustDesk2.toml'),
|
||||
(Join-Path $env:WINDIR 'ServiceProfiles\LocalService\AppData\Roaming\RustDesk\config\RustDesk2.toml'),
|
||||
(Join-Path $env:WINDIR 'System32\config\systemprofile\AppData\Roaming\RustDesk\config\RustDesk2.toml'),
|
||||
(Join-Path $env:SystemDrive 'Users\Default\AppData\Roaming\RustDesk\config\RustDesk2.toml')
|
||||
)
|
||||
foreach ($configurationPath in $configurationPaths) {
|
||||
New-Item -ItemType Directory -Path (Split-Path $configurationPath -Parent) -Force | Out-Null
|
||||
[IO.File]::WriteAllText($configurationPath, $configuration, [Text.UTF8Encoding]::new($false))
|
||||
}
|
||||
|
||||
$existingPassword = Get-SavedAccessPassword
|
||||
if ($AccessPassword) {
|
||||
$managedPassword = $AccessPassword
|
||||
Save-AccessPassword -Password $managedPassword
|
||||
$passwordWasGenerated = $false
|
||||
}
|
||||
elseif ($existingPassword) {
|
||||
$managedPassword = $existingPassword
|
||||
$passwordWasGenerated = $false
|
||||
}
|
||||
else {
|
||||
$managedPassword = New-RandomAccessPassword
|
||||
Save-AccessPassword -Password $managedPassword
|
||||
$passwordWasGenerated = $true
|
||||
}
|
||||
|
||||
Start-Service -Name $rustDeskService.Name
|
||||
$rustDeskService = Get-Service -Name $rustDeskService.Name
|
||||
$rustDeskService.WaitForStatus('Running', (New-TimeSpan -Seconds 20))
|
||||
|
||||
$plainPassword = ConvertTo-PlainText -SecureString $managedPassword
|
||||
try {
|
||||
# RustDesk on Windows only reliably treats its CLI output path as a command
|
||||
# invocation when stdout is consumed. Without the pipeline it can attach
|
||||
# to the GUI instance and leave a non-interactive bootstrap waiting.
|
||||
$null = & $rustDeskExecutable --password $plainPassword | Out-String
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "RustDesk could not set the managed access password (exit code $LASTEXITCODE)."
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$plainPassword = $null
|
||||
}
|
||||
|
||||
$rustDeskId = ((& $rustDeskExecutable --get-id | Out-String).Trim() -split "`r?`n" |
|
||||
Select-Object -Last 1).Trim()
|
||||
if ($rustDeskId -notmatch '^\d+$') {
|
||||
throw "RustDesk returned an invalid device ID: $rustDeskId"
|
||||
}
|
||||
if (-not (Test-TcpConnection -HostName $ServerAddress -Port 21116)) {
|
||||
throw "The RustDesk rendezvous server $rendezvousAddress is not reachable from this client."
|
||||
}
|
||||
|
||||
$device = [ordered]@{
|
||||
ComputerName = $env:COMPUTERNAME
|
||||
RustDeskId = $rustDeskId
|
||||
ServerAddress = $ServerAddress
|
||||
ServerPublicKeySha256 = ([Security.Cryptography.SHA256]::Create().ComputeHash(
|
||||
[Text.Encoding]::UTF8.GetBytes($ServerPublicKey)) | ForEach-Object ToString x2) -join ''
|
||||
ConfiguredAt = (Get-Date).ToString('o')
|
||||
}
|
||||
[IO.File]::WriteAllText($devicePath, ($device | ConvertTo-Json), [Text.UTF8Encoding]::new($false))
|
||||
|
||||
[pscustomobject]@{
|
||||
RustDeskId = $rustDeskId
|
||||
ServerAddress = $ServerAddress
|
||||
ServiceName = $rustDeskService.Name
|
||||
ServiceStatus = (Get-Service -Name $rustDeskService.Name).Status.ToString()
|
||||
RendezvousReachable = $true
|
||||
AccessPassword = $managedPassword
|
||||
AccessPasswordWasGenerated = $passwordWasGenerated
|
||||
DevicePath = $devicePath
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
[CmdletBinding(SupportsShouldProcess)]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[ValidatePattern('^[A-Za-z0-9][A-Za-z0-9.-]*$')]
|
||||
[string]$ServerAddress,
|
||||
|
||||
[string]$InstallRoot = "$env:ProgramFiles\SGU\RustDeskServer",
|
||||
[string]$DataRoot = "$env:ProgramData\SGU\RustDesk\Server",
|
||||
[string]$FirewallRemoteAddress = '192.168.50.0/24',
|
||||
[uri]$DownloadUri = 'https://github.com/rustdesk/rustdesk-server/releases/download/1.1.16/rustdesk-server-windows-x86_64-unsigned.zip',
|
||||
[ValidatePattern('^[A-Fa-f0-9]{64}$')]
|
||||
[string]$ExpectedSha256 = 'B865A3A62FC8755B45480C508F1C4871C3338590408DDA8C58C7E9C373B7ADB0'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
$hbbsTaskName = 'SGU-RustDesk-hbbs'
|
||||
$hbbrTaskName = 'SGU-RustDesk-hbbr'
|
||||
$downloadRoot = Join-Path $env:ProgramData 'SGU\RustDesk\Downloads'
|
||||
$archivePath = Join-Path $downloadRoot 'rustdesk-server-windows-x86_64-1.1.16.zip'
|
||||
|
||||
function Assert-Administrator {
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
|
||||
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
||||
throw 'Run this script from an elevated Windows PowerShell session.'
|
||||
}
|
||||
}
|
||||
|
||||
function Set-PrivateDirectoryAcl {
|
||||
param([Parameter(Mandatory)][string]$Path)
|
||||
|
||||
New-Item -ItemType Directory -Path $Path -Force | Out-Null
|
||||
$acl = New-Object Security.AccessControl.DirectorySecurity
|
||||
$acl.SetAccessRuleProtection($true, $false)
|
||||
$inheritance = [Security.AccessControl.InheritanceFlags]'ContainerInherit,ObjectInherit'
|
||||
$allow = [Security.AccessControl.AccessControlType]::Allow
|
||||
foreach ($sid in @('S-1-5-18', 'S-1-5-32-544')) {
|
||||
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new(
|
||||
[Security.Principal.SecurityIdentifier]::new($sid),
|
||||
[Security.AccessControl.FileSystemRights]::FullControl,
|
||||
$inheritance,
|
||||
[Security.AccessControl.PropagationFlags]::None,
|
||||
$allow))
|
||||
}
|
||||
Set-Acl -LiteralPath $Path -AclObject $acl
|
||||
}
|
||||
|
||||
function Assert-FileHash {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Path,
|
||||
[Parameter(Mandatory)][string]$ExpectedHash
|
||||
)
|
||||
|
||||
$actualHash = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash
|
||||
if (-not $actualHash.Equals($ExpectedHash, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "SHA-256 verification failed for $Path."
|
||||
}
|
||||
}
|
||||
|
||||
function Copy-IfDifferent {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Source,
|
||||
[Parameter(Mandatory)][string]$Destination
|
||||
)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Destination -PathType Leaf) -or
|
||||
(Get-FileHash -LiteralPath $Source -Algorithm SHA256).Hash -ne
|
||||
(Get-FileHash -LiteralPath $Destination -Algorithm SHA256).Hash) {
|
||||
Copy-Item -LiteralPath $Source -Destination $Destination -Force
|
||||
return $true
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function Set-RustDeskFirewallRule {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Name,
|
||||
[Parameter(Mandatory)][ValidateSet('TCP', 'UDP')][string]$Protocol,
|
||||
[Parameter(Mandatory)][string]$LocalPort
|
||||
)
|
||||
|
||||
$rule = Get-NetFirewallRule -DisplayName $Name -ErrorAction SilentlyContinue
|
||||
if (-not $rule) {
|
||||
$rule = New-NetFirewallRule -DisplayName $Name -Group 'SGU RustDesk' `
|
||||
-Direction Inbound -Action Allow -Protocol $Protocol -LocalPort $LocalPort `
|
||||
-RemoteAddress $FirewallRemoteAddress -Profile Domain -Enabled True
|
||||
}
|
||||
else {
|
||||
$rule | Set-NetFirewallRule -Enabled True -Profile Domain -Action Allow | Out-Null
|
||||
$rule | Get-NetFirewallPortFilter | Set-NetFirewallPortFilter `
|
||||
-Protocol $Protocol -LocalPort $LocalPort | Out-Null
|
||||
$rule | Get-NetFirewallAddressFilter | Set-NetFirewallAddressFilter `
|
||||
-RemoteAddress $FirewallRemoteAddress | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
function Stop-RustDeskTasks {
|
||||
foreach ($taskName in @($hbbsTaskName, $hbbrTaskName)) {
|
||||
$task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
||||
if ($task -and $task.State -eq 'Running') {
|
||||
Stop-ScheduledTask -TaskName $taskName
|
||||
}
|
||||
}
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
|
||||
function Register-RustDeskServerTask {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$TaskName,
|
||||
[Parameter(Mandatory)][string]$Executable,
|
||||
[string]$Arguments
|
||||
)
|
||||
|
||||
# New-ScheduledTaskAction rejects an empty -Argument value. hbbr has no
|
||||
# command-line arguments, whereas hbbs needs the relay endpoint, so add
|
||||
# the parameter only when it is meaningful.
|
||||
$actionParameters = @{
|
||||
Execute = $Executable
|
||||
WorkingDirectory = $DataRoot
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($Arguments)) {
|
||||
$actionParameters.Argument = $Arguments
|
||||
}
|
||||
$action = New-ScheduledTaskAction @actionParameters
|
||||
$trigger = New-ScheduledTaskTrigger -AtStartup
|
||||
$trigger.Delay = 'PT30S'
|
||||
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable `
|
||||
-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
|
||||
-ExecutionTimeLimit ([TimeSpan]::Zero) `
|
||||
-RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1)
|
||||
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger `
|
||||
-Settings $settings -User 'SYSTEM' -RunLevel Highest -Force | Out-Null
|
||||
$registeredTask = Get-ScheduledTask -TaskName $TaskName -ErrorAction Stop
|
||||
if ($registeredTask.State -ne 'Running') {
|
||||
Start-ScheduledTask -TaskName $TaskName
|
||||
}
|
||||
}
|
||||
|
||||
function Wait-ForRustDeskServer {
|
||||
for ($attempt = 1; $attempt -le 30; $attempt++) {
|
||||
$hbbsListening = [bool](Get-NetTCPConnection -LocalPort 21116 -State Listen `
|
||||
-ErrorAction SilentlyContinue)
|
||||
$hbbrListening = [bool](Get-NetTCPConnection -LocalPort 21117 -State Listen `
|
||||
-ErrorAction SilentlyContinue)
|
||||
$publicKeyReady = Test-Path -LiteralPath (Join-Path $DataRoot 'id_ed25519.pub') -PathType Leaf
|
||||
if ($hbbsListening -and $hbbrListening -and $publicKeyReady) {
|
||||
return
|
||||
}
|
||||
Start-Sleep -Seconds 2
|
||||
}
|
||||
throw 'RustDesk hbbs/hbbr did not become ready within 60 seconds.'
|
||||
}
|
||||
|
||||
Assert-Administrator
|
||||
if (-not $PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Install and configure the RustDesk OSS rendezvous and relay server')) {
|
||||
return
|
||||
}
|
||||
|
||||
Set-PrivateDirectoryAcl -Path $DataRoot
|
||||
$managementRoot = Split-Path $DataRoot -Parent
|
||||
Set-PrivateDirectoryAcl -Path $managementRoot
|
||||
New-Item -ItemType Directory -Path $InstallRoot,$downloadRoot -Force | Out-Null
|
||||
|
||||
if (-not (Test-Path -LiteralPath $archivePath -PathType Leaf) -or
|
||||
(Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash -ne $ExpectedSha256) {
|
||||
Invoke-WebRequest -Uri $DownloadUri -OutFile $archivePath -UseBasicParsing
|
||||
}
|
||||
Assert-FileHash -Path $archivePath -ExpectedHash $ExpectedSha256
|
||||
|
||||
$stagingRoot = Join-Path $env:TEMP ('sgu-rustdesk-server-' + [Guid]::NewGuid().ToString('N'))
|
||||
try {
|
||||
Expand-Archive -LiteralPath $archivePath -DestinationPath $stagingRoot -Force
|
||||
$payloadRoot = Join-Path $stagingRoot 'x86_64'
|
||||
$sourceHbbs = Join-Path $payloadRoot 'hbbs.exe'
|
||||
$sourceHbbr = Join-Path $payloadRoot 'hbbr.exe'
|
||||
foreach ($required in @($sourceHbbs, $sourceHbbr)) {
|
||||
if (-not (Test-Path -LiteralPath $required -PathType Leaf)) {
|
||||
throw "The verified RustDesk archive is missing $required."
|
||||
}
|
||||
}
|
||||
|
||||
$targetHbbs = Join-Path $InstallRoot 'hbbs.exe'
|
||||
$targetHbbr = Join-Path $InstallRoot 'hbbr.exe'
|
||||
$requiresBinaryUpdate =
|
||||
-not (Test-Path -LiteralPath $targetHbbs) -or
|
||||
-not (Test-Path -LiteralPath $targetHbbr) -or
|
||||
(Get-FileHash -LiteralPath $sourceHbbs -Algorithm SHA256).Hash -ne
|
||||
(Get-FileHash -LiteralPath $targetHbbs -Algorithm SHA256).Hash -or
|
||||
(Get-FileHash -LiteralPath $sourceHbbr -Algorithm SHA256).Hash -ne
|
||||
(Get-FileHash -LiteralPath $targetHbbr -Algorithm SHA256).Hash
|
||||
if ($requiresBinaryUpdate) {
|
||||
Stop-RustDeskTasks
|
||||
Copy-IfDifferent -Source $sourceHbbs -Destination $targetHbbs | Out-Null
|
||||
Copy-IfDifferent -Source $sourceHbbr -Destination $targetHbbr | Out-Null
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (Test-Path -LiteralPath $stagingRoot) {
|
||||
Remove-Item -LiteralPath $stagingRoot -Recurse -Force
|
||||
}
|
||||
}
|
||||
|
||||
Set-RustDeskFirewallRule -Name 'SGU RustDesk hbbs (TCP)' -Protocol TCP -LocalPort '21115-21116'
|
||||
Set-RustDeskFirewallRule -Name 'SGU RustDesk hbbr (TCP)' -Protocol TCP -LocalPort '21117'
|
||||
Set-RustDeskFirewallRule -Name 'SGU RustDesk hbbs (UDP)' -Protocol UDP -LocalPort '21116'
|
||||
|
||||
Register-RustDeskServerTask -TaskName $hbbrTaskName -Executable (Join-Path $InstallRoot 'hbbr.exe')
|
||||
Register-RustDeskServerTask -TaskName $hbbsTaskName -Executable (Join-Path $InstallRoot 'hbbs.exe') `
|
||||
-Arguments "-r $ServerAddress`:21117"
|
||||
Wait-ForRustDeskServer
|
||||
|
||||
$publicKey = (Get-Content -LiteralPath (Join-Path $DataRoot 'id_ed25519.pub') -Raw).Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($publicKey)) {
|
||||
throw 'RustDesk generated an empty public key.'
|
||||
}
|
||||
$statusPath = Join-Path (Split-Path $DataRoot -Parent) 'server.json'
|
||||
$status = [ordered]@{
|
||||
ServerAddress = $ServerAddress
|
||||
PublicKey = $publicKey
|
||||
PublicKeySha256 = ([Security.Cryptography.SHA256]::Create().ComputeHash(
|
||||
[Text.Encoding]::UTF8.GetBytes($publicKey)) | ForEach-Object ToString x2) -join ''
|
||||
HbbsTaskName = $hbbsTaskName
|
||||
HbbrTaskName = $hbbrTaskName
|
||||
HbbsTcpPort = 21116
|
||||
HbbrTcpPort = 21117
|
||||
InstalledAt = (Get-Date).ToString('o')
|
||||
}
|
||||
[IO.File]::WriteAllText($statusPath, ($status | ConvertTo-Json), [Text.UTF8Encoding]::new($false))
|
||||
|
||||
[pscustomobject]@{
|
||||
ServerAddress = $ServerAddress
|
||||
PublicKey = $publicKey
|
||||
PublicKeySha256 = $status.PublicKeySha256
|
||||
HbbsTask = (Get-ScheduledTask -TaskName $hbbsTaskName).State.ToString()
|
||||
HbbrTask = (Get-ScheduledTask -TaskName $hbbrTaskName).State.ToString()
|
||||
HbbsListening = [bool](Get-NetTCPConnection -LocalPort 21116 -State Listen -ErrorAction SilentlyContinue)
|
||||
HbbrListening = [bool](Get-NetTCPConnection -LocalPort 21117 -State Listen -ErrorAction SilentlyContinue)
|
||||
StatusPath = $statusPath
|
||||
}
|
||||
@@ -124,6 +124,7 @@ $runtimeInstaller = Get-ChildItem (Join-Path $packageRoot 'payload\prerequisites
|
||||
Select-Object -First 1
|
||||
foreach ($requiredPath in @(
|
||||
(Join-Path $scriptsRoot 'Enroll-SguDomainClient.ps1'),
|
||||
(Join-Path $scriptsRoot 'Install-SguRustDeskClient.ps1'),
|
||||
(Join-Path $scriptsRoot 'Register-SguClientCertificate.ps1'),
|
||||
(Join-Path $providerPublishPath 'SGU.CredentialProvider.comhost.dll'))) {
|
||||
if (-not (Test-Path -LiteralPath $requiredPath -PathType Leaf)) {
|
||||
@@ -177,10 +178,23 @@ try {
|
||||
$serverIdentity = Invoke-Command -Session $session -ScriptBlock {
|
||||
$computer = Get-CimInstance Win32_ComputerSystem
|
||||
$brokerService = Get-Service SGUAuthBroker -ErrorAction SilentlyContinue
|
||||
$rustDeskStatusPath = Join-Path $env:ProgramData 'SGU\RustDesk\server.json'
|
||||
$rustDeskStatus = if (Test-Path -LiteralPath $rustDeskStatusPath -PathType Leaf) {
|
||||
Get-Content -LiteralPath $rustDeskStatusPath -Raw | ConvertFrom-Json
|
||||
}
|
||||
else {
|
||||
$null
|
||||
}
|
||||
$hbbsTask = Get-ScheduledTask -TaskName 'SGU-RustDesk-hbbs' -ErrorAction SilentlyContinue
|
||||
$hbbrTask = Get-ScheduledTask -TaskName 'SGU-RustDesk-hbbr' -ErrorAction SilentlyContinue
|
||||
[pscustomobject]@{
|
||||
ComputerName = $env:COMPUTERNAME
|
||||
Domain = $computer.Domain
|
||||
BrokerService = if ($brokerService) { $brokerService.Status.ToString() } else { 'Missing' }
|
||||
RustDeskServerAddress = if ($rustDeskStatus) { [string]$rustDeskStatus.ServerAddress } else { $null }
|
||||
RustDeskPublicKey = if ($rustDeskStatus) { [string]$rustDeskStatus.PublicKey } else { $null }
|
||||
RustDeskHbbsTask = if ($hbbsTask) { $hbbsTask.State.ToString() } else { 'Missing' }
|
||||
RustDeskHbbrTask = if ($hbbrTask) { $hbbrTask.State.ToString() } else { 'Missing' }
|
||||
}
|
||||
}
|
||||
if (-not $serverIdentity.Domain -or
|
||||
@@ -190,6 +204,12 @@ try {
|
||||
if ($serverIdentity.BrokerService -ne 'Running') {
|
||||
throw "The SGU Authentication Broker is not running on $($serverIdentity.ComputerName)."
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($serverIdentity.RustDeskServerAddress) -or
|
||||
[string]::IsNullOrWhiteSpace($serverIdentity.RustDeskPublicKey) -or
|
||||
$serverIdentity.RustDeskHbbsTask -ne 'Running' -or
|
||||
$serverIdentity.RustDeskHbbrTask -ne 'Running') {
|
||||
throw "The RustDesk server is not ready on $($serverIdentity.ComputerName). Run the current server bootstrap first."
|
||||
}
|
||||
|
||||
$certificateSubject = "CN=SGU Credential Provider Client $env:COMPUTERNAME"
|
||||
$clientCertificate = Get-ChildItem Cert:\LocalMachine\My |
|
||||
@@ -274,6 +294,8 @@ try {
|
||||
DomainDnsServerAddresses = @($DomainControllerIPv4Address.IPAddressToString)
|
||||
RemoteDesktopPrincipal = "$DomainNetbios\SG-Laboratorio-Usuarios-RDP"
|
||||
DotNetRuntimeInstallerPath = $runtimeInstaller.FullName
|
||||
RustDeskServerAddress = $serverIdentity.RustDeskServerAddress
|
||||
RustDeskServerPublicKey = $serverIdentity.RustDeskPublicKey
|
||||
SkipRestart = $true
|
||||
}
|
||||
if ($NewComputerName) {
|
||||
@@ -281,6 +303,31 @@ try {
|
||||
}
|
||||
|
||||
$result = & (Join-Path $scriptsRoot 'Enroll-SguDomainClient.ps1') @enrollmentParameters
|
||||
|
||||
$rustDeskEnrollment = $result.RustDesk
|
||||
if (-not $rustDeskEnrollment -or -not $rustDeskEnrollment.RustDeskId -or
|
||||
-not $rustDeskEnrollment.AccessPassword) {
|
||||
throw 'The client RustDesk enrollment did not provide an ID and protected access credential.'
|
||||
}
|
||||
$rustDeskPasswordPointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR(
|
||||
$rustDeskEnrollment.AccessPassword)
|
||||
try {
|
||||
$rustDeskPassword = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($rustDeskPasswordPointer)
|
||||
Invoke-Command -Session $session -ScriptBlock {
|
||||
param($ComputerName, $RustDeskId, $AccessPassword)
|
||||
$registrationScript = Join-Path $env:ProgramData 'SGU\RustDesk\Register-SguRustDeskDevice.ps1'
|
||||
if (-not (Test-Path -LiteralPath $registrationScript -PathType Leaf)) {
|
||||
throw 'The RustDesk device-registration script is missing on the domain controller.'
|
||||
}
|
||||
& $registrationScript -ComputerName $ComputerName -RustDeskId $RustDeskId `
|
||||
-AccessPassword $AccessPassword | Out-Null
|
||||
} -ArgumentList $env:COMPUTERNAME,$rustDeskEnrollment.RustDeskId,$rustDeskPassword
|
||||
}
|
||||
finally {
|
||||
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($rustDeskPasswordPointer)
|
||||
$rustDeskPassword = $null
|
||||
}
|
||||
$rustDeskEnrollment.PSObject.Properties.Remove('AccessPassword')
|
||||
}
|
||||
finally {
|
||||
if ($session) {
|
||||
@@ -319,6 +366,7 @@ if ($SkipRestart) {
|
||||
ClientCertificateRegistered = $true
|
||||
BrokerEndpoint = $brokerEndpoint
|
||||
RestartRequired = $true
|
||||
RustDesk = if ($result) { $result.RustDesk } else { $null }
|
||||
EnrollmentResult = $result
|
||||
}
|
||||
return
|
||||
|
||||
@@ -109,6 +109,7 @@ $clientScripts = @(
|
||||
'Enroll-SguDomainClient.ps1',
|
||||
'Install-CredentialProvider.ps1',
|
||||
'Install-SguEnrollmentGuard.ps1',
|
||||
'Install-SguRustDeskClient.ps1',
|
||||
'Register-SguClientCertificate.ps1',
|
||||
'Repair-SguClientEnrollment.ps1',
|
||||
'Test-SguClientEnrollment.ps1'
|
||||
@@ -166,9 +167,13 @@ $serverScripts = @(
|
||||
'Get-SguUsageReport.ps1',
|
||||
'Get-SguBrokerLog.ps1',
|
||||
'Install-SguDomainMonitoring.ps1',
|
||||
'Install-SguRustDeskClient.ps1',
|
||||
'Install-SguRustDeskServer.ps1',
|
||||
'Invoke-SguMonitoringMaintenance.ps1',
|
||||
'New-LabCertificate.ps1',
|
||||
'Get-SguRustDeskDevice.ps1',
|
||||
'Register-SguClientCertificate.ps1',
|
||||
'Register-SguRustDeskDevice.ps1',
|
||||
'Set-LabBrokerDns.ps1',
|
||||
'Set-SguDomainComputerPolicies.ps1',
|
||||
'Set-SguDomainUserPolicies.ps1'
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
[CmdletBinding(SupportsShouldProcess)]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[ValidatePattern('^[A-Za-z0-9][A-Za-z0-9-]{0,62}$')]
|
||||
[string]$ComputerName,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[ValidatePattern('^\d+$')]
|
||||
[string]$RustDeskId,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateLength(12, 256)]
|
||||
[string]$AccessPassword,
|
||||
|
||||
[string]$InventoryRoot = "$env:ProgramData\SGU\RustDesk\Devices"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Assert-Administrator {
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
|
||||
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
||||
throw 'Only a local administrator can register a RustDesk device credential.'
|
||||
}
|
||||
}
|
||||
|
||||
function Initialize-DataProtection {
|
||||
if (-not ('SguRustDeskDataProtection' -as [type])) {
|
||||
Add-Type -TypeDefinition @'
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
public static class SguRustDeskDataProtection {
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct DataBlob { public int cbData; public IntPtr pbData; }
|
||||
[DllImport("crypt32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern bool CryptProtectData(ref DataBlob input, string description, IntPtr entropy, IntPtr reserved, IntPtr prompt, int flags, out DataBlob output);
|
||||
[DllImport("crypt32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern bool CryptUnprotectData(ref DataBlob input, IntPtr description, IntPtr entropy, IntPtr reserved, IntPtr prompt, int flags, out DataBlob output);
|
||||
[DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr LocalFree(IntPtr memory);
|
||||
private const int CryptProtectLocalMachine = 0x4;
|
||||
private static DataBlob ToBlob(byte[] value) { var blob = new DataBlob { cbData = value.Length, pbData = IntPtr.Zero }; if (value.Length > 0) { blob.pbData = Marshal.AllocHGlobal(value.Length); Marshal.Copy(value, 0, blob.pbData, value.Length); } return blob; }
|
||||
private static byte[] FromBlob(DataBlob blob) { var value = new byte[blob.cbData]; if (blob.cbData > 0) Marshal.Copy(blob.pbData, value, 0, blob.cbData); return value; }
|
||||
public static byte[] Protect(byte[] value) { var input = ToBlob(value); var output = new DataBlob(); try { if (!CryptProtectData(ref input, null, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, CryptProtectLocalMachine, out output)) throw new Win32Exception(Marshal.GetLastWin32Error()); return FromBlob(output); } finally { if (input.pbData != IntPtr.Zero) Marshal.FreeHGlobal(input.pbData); if (output.pbData != IntPtr.Zero) LocalFree(output.pbData); } }
|
||||
public static byte[] Unprotect(byte[] value) { var input = ToBlob(value); var output = new DataBlob(); try { if (!CryptUnprotectData(ref input, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, 0, out output)) throw new Win32Exception(Marshal.GetLastWin32Error()); return FromBlob(output); } finally { if (input.pbData != IntPtr.Zero) Marshal.FreeHGlobal(input.pbData); if (output.pbData != IntPtr.Zero) LocalFree(output.pbData); } }
|
||||
}
|
||||
'@ -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
|
||||
function Set-PrivateDirectoryAcl {
|
||||
param([Parameter(Mandatory)][string]$Path)
|
||||
|
||||
New-Item -ItemType Directory -Path $Path -Force | Out-Null
|
||||
$acl = New-Object Security.AccessControl.DirectorySecurity
|
||||
$acl.SetAccessRuleProtection($true, $false)
|
||||
$inheritance = [Security.AccessControl.InheritanceFlags]'ContainerInherit,ObjectInherit'
|
||||
$allow = [Security.AccessControl.AccessControlType]::Allow
|
||||
foreach ($sid in @('S-1-5-18', 'S-1-5-32-544')) {
|
||||
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new(
|
||||
[Security.Principal.SecurityIdentifier]::new($sid),
|
||||
[Security.AccessControl.FileSystemRights]::FullControl,
|
||||
$inheritance,
|
||||
[Security.AccessControl.PropagationFlags]::None,
|
||||
$allow))
|
||||
}
|
||||
Set-Acl -LiteralPath $Path -AclObject $acl
|
||||
}
|
||||
|
||||
Assert-Administrator
|
||||
Initialize-DataProtection
|
||||
if (-not $PSCmdlet.ShouldProcess($ComputerName, 'Register the protected RustDesk management credential')) {
|
||||
return
|
||||
}
|
||||
|
||||
Set-PrivateDirectoryAcl -Path $InventoryRoot
|
||||
$normalizedName = $ComputerName.ToUpperInvariant()
|
||||
$secretPath = Join-Path $InventoryRoot "$normalizedName.secret"
|
||||
$metadataPath = Join-Path $InventoryRoot "$normalizedName.json"
|
||||
$protectedPassword = [SguRustDeskDataProtection]::Protect(
|
||||
[Text.Encoding]::UTF8.GetBytes($AccessPassword))
|
||||
[IO.File]::WriteAllBytes($secretPath, $protectedPassword)
|
||||
$metadata = [ordered]@{
|
||||
ComputerName = $normalizedName
|
||||
RustDeskId = $RustDeskId
|
||||
RegisteredAt = (Get-Date).ToString('o')
|
||||
SecretPath = $secretPath
|
||||
}
|
||||
[IO.File]::WriteAllText($metadataPath, ($metadata | ConvertTo-Json), [Text.UTF8Encoding]::new($false))
|
||||
|
||||
[pscustomobject]@{
|
||||
ComputerName = $normalizedName
|
||||
RustDeskId = $RustDeskId
|
||||
Registered = $true
|
||||
MetadataPath = $metadataPath
|
||||
}
|
||||
@@ -10,6 +10,7 @@ $testScript = Join-Path $enrollmentRoot 'Test-SguClientEnrollment.ps1'
|
||||
$installScript = Join-Path $enrollmentRoot 'Install-CredentialProvider.ps1'
|
||||
$remoteAccessScript = Join-Path $enrollmentRoot 'Enable-LabRemoteAccess.ps1'
|
||||
$monitoringScript = Join-Path $enrollmentRoot 'Enable-SguClientMonitoring.ps1'
|
||||
$rustDeskScript = Join-Path $enrollmentRoot 'Install-SguRustDeskClient.ps1'
|
||||
|
||||
$before = & $testScript
|
||||
if (-not $before.IsValid) {
|
||||
@@ -36,12 +37,21 @@ if ($computer.PartOfDomain) {
|
||||
& $monitoringScript | Out-Null
|
||||
}
|
||||
|
||||
if ($configuration.RustDeskServerAddress -and $configuration.RustDeskServerPublicKey) {
|
||||
& $rustDeskScript -ServerAddress ([string]$configuration.RustDeskServerAddress) `
|
||||
-ServerPublicKey ([string]$configuration.RustDeskServerPublicKey) | Out-Null
|
||||
}
|
||||
|
||||
$verificationParams = @{}
|
||||
if ($computer.PartOfDomain) {
|
||||
$verificationParams.RequireDomainJoined = $true
|
||||
$verificationParams.RequireRemoteAccess = $true
|
||||
$verificationParams.RemoteDesktopPrincipal = [string]$configuration.RemoteDesktopPrincipal
|
||||
}
|
||||
if ($configuration.RustDeskServerAddress) {
|
||||
$verificationParams.RequireRustDesk = $true
|
||||
$verificationParams.RustDeskServerAddress = [string]$configuration.RustDeskServerAddress
|
||||
}
|
||||
$after = & $testScript @verificationParams
|
||||
$after
|
||||
if (-not $after.IsValid) {
|
||||
|
||||
@@ -4,6 +4,8 @@ param(
|
||||
[switch]$RequireRemoteAccess,
|
||||
[switch]$RequireBrokerHealth,
|
||||
[string]$RemoteDesktopPrincipal = 'LCI\SG-Laboratorio-Usuarios-RDP',
|
||||
[switch]$RequireRustDesk,
|
||||
[string]$RustDeskServerAddress,
|
||||
[switch]$Enforce
|
||||
)
|
||||
|
||||
@@ -158,6 +160,42 @@ if ($RequireRemoteAccess) {
|
||||
}
|
||||
}
|
||||
|
||||
$rustDeskReady = $null
|
||||
$rustDeskId = $null
|
||||
if ($RequireRustDesk) {
|
||||
if ([string]::IsNullOrWhiteSpace($RustDeskServerAddress)) {
|
||||
$issues.Add('RustDesk validation requires RustDeskServerAddress.')
|
||||
}
|
||||
$rustDeskService = Get-Service -Name 'RustDesk' -ErrorAction SilentlyContinue
|
||||
$rustDeskStatePath = Join-Path $env:ProgramData 'SGU\RustDesk\Client\device.json'
|
||||
$rustDeskSecretPath = Join-Path $env:ProgramData 'SGU\RustDesk\Client\access.secret'
|
||||
$rustDeskConfigPath = Join-Path $env:WINDIR `
|
||||
'ServiceProfiles\LocalService\AppData\Roaming\RustDesk\config\RustDesk2.toml'
|
||||
$rustDeskConfig = if (Test-Path -LiteralPath $rustDeskConfigPath -PathType Leaf) {
|
||||
Get-Content -LiteralPath $rustDeskConfigPath -Raw
|
||||
}
|
||||
else {
|
||||
''
|
||||
}
|
||||
$rustDeskState = $null
|
||||
try {
|
||||
$rustDeskState = Get-Content -LiteralPath $rustDeskStatePath -Raw | ConvertFrom-Json
|
||||
$rustDeskId = [string]$rustDeskState.RustDeskId
|
||||
}
|
||||
catch {
|
||||
# The checks below report the missing or invalid state as one enrollment issue.
|
||||
}
|
||||
$rustDeskReady =
|
||||
$rustDeskService -and $rustDeskService.Status -eq 'Running' -and
|
||||
(Test-Path -LiteralPath $rustDeskSecretPath -PathType Leaf) -and
|
||||
$rustDeskState -and $rustDeskState.ServerAddress -eq $RustDeskServerAddress -and
|
||||
$rustDeskId -match '^\d+$' -and
|
||||
$rustDeskConfig -match [regex]::Escape("rendezvous_server = '$RustDeskServerAddress`:21116'")
|
||||
if (-not $rustDeskReady) {
|
||||
$issues.Add('RustDesk is not installed, running, or configured for the expected self-hosted server.')
|
||||
}
|
||||
}
|
||||
|
||||
$result = [pscustomobject]@{
|
||||
ComputerName = $env:COMPUTERNAME
|
||||
Domain = $computer.Domain
|
||||
@@ -175,6 +213,8 @@ $result = [pscustomobject]@{
|
||||
DotNetRuntimePresent = $dotNetRuntimePresent
|
||||
BrokerHealth = $brokerHealth
|
||||
RemoteAccessReady = $remoteAccessReady
|
||||
RustDeskReady = $rustDeskReady
|
||||
RustDeskId = $rustDeskId
|
||||
IsValid = $issues.Count -eq 0
|
||||
Issues = $issues.ToArray()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user