# TWT VPN installation and recovery — run through RMM or GPO as SYSTEM. # Installs missing clients, repairs incomplete installs, upgrades hourly and enforces a running guardian. # The guardian's saved VPN pause is preserved. This script never requests a VPN connection. # Disable this automation before intentionally uninstalling or retiring a PC. param( [string]$Portal = 'https://vpn.akirawood.com', [string]$ExpectedPublisher = '', [switch]$CheckForUpdates ) $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' $ServiceName = 'TwtVpnGuardian' $CacheDirectory = Join-Path $env:ProgramData 'TWT\RmmRepair' function Write-RecoveryLog([string]$Message) { $line = '{0} {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Message Write-Output $line $logPath = Join-Path $CacheDirectory 'repair.log' if ((Test-Path -LiteralPath $logPath) -and (Get-Item -LiteralPath $logPath).Length -ge 1MB) { Move-Item -LiteralPath $logPath -Destination ($logPath + '.previous') -Force } Add-Content -LiteralPath $logPath -Value $line -Encoding UTF8 } function Enable-Guardian { $current = Get-Service -Name $ServiceName if ($current.StartType -ne 'Automatic') { Set-Service -Name $ServiceName -StartupType Automatic; Write-RecoveryLog 'Guardian startup restored to Automatic.' } if ($current.Status -eq 'StopPending') { $current.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(30)); $current.Refresh() } if ($current.Status -eq 'StartPending') { $current.WaitForStatus('Running', [TimeSpan]::FromSeconds(30)); $current.Refresh() } if ($current.Status -eq 'Paused') { Resume-Service -Name $ServiceName; $current.WaitForStatus('Running', [TimeSpan]::FromSeconds(30)); $current.Refresh() } if ($current.Status -ne 'Running') { Write-RecoveryLog 'Starting the guardian. Saved VPN pause remains unchanged.' Start-Service -Name $ServiceName $current.WaitForStatus('Running', [TimeSpan]::FromSeconds(30)) } } function Get-MsiProperty([string]$Package, [string]$Property) { if ($Property -notin 'ProductCode','ProductVersion','UpgradeCode') { throw 'Unsupported installer property.' } $installer = New-Object -ComObject WindowsInstaller.Installer $database = $null; $view = $null; $record = $null try { $database = $installer.GetType().InvokeMember('OpenDatabase', 'InvokeMethod', $null, $installer, @($Package, 0)) $view = $database.GetType().InvokeMember('OpenView', 'InvokeMethod', $null, $database, @('SELECT `Value` FROM `Property` WHERE `Property`=''' + $Property + '''')) $view.GetType().InvokeMember('Execute', 'InvokeMethod', $null, $view, $null) | Out-Null $record = $view.GetType().InvokeMember('Fetch', 'InvokeMethod', $null, $view, $null) $value = $record.GetType().InvokeMember('StringData', 'GetProperty', $null, $record, @(1)) return $value } finally { foreach ($item in @($record, $view, $database, $installer)) { if ($null -ne $item) { [Runtime.InteropServices.Marshal]::FinalReleaseComObject($item) | Out-Null } } } } function Test-Package([string]$Package, $Manifest) { if (!(Test-Path -LiteralPath $Package -PathType Leaf)) { return $false } $signature = Get-AuthenticodeSignature -LiteralPath $Package return ((Get-FileHash -LiteralPath $Package -Algorithm SHA256).Hash -eq $Manifest.sha256 -and $signature.Status -eq 'Valid' -and $null -ne $signature.SignerCertificate -and $signature.SignerCertificate.Thumbprint -eq $Manifest.publisherThumbprint) } $mutex = $null; $ownsMutex = $false try { $portalUri = [Uri]$Portal if (!$portalUri.IsAbsoluteUri -or $portalUri.Scheme -ne 'https' -or $portalUri.UserInfo) { throw 'The portal must be an HTTPS address without credentials.' } $Portal = $Portal.TrimEnd('/') if ($ExpectedPublisher -and $ExpectedPublisher -notmatch '^[A-Fa-f0-9]{40}$') { throw 'Invalid pinned signing publisher.' } if ([Security.Principal.WindowsIdentity]::GetCurrent().User.Value -ne 'S-1-5-18') { throw 'Run this script as SYSTEM, not as a signed-in administrator.' } if ([Environment]::Is64BitOperatingSystem -and ![Environment]::Is64BitProcess) { if (!$PSCommandPath) { throw 'Select 64-bit PowerShell in the RMM. Inline scripts cannot relaunch themselves.' } & "$env:WINDIR\Sysnative\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -NonInteractive -File $PSCommandPath exit $LASTEXITCODE } New-Item -ItemType Directory -Path $CacheDirectory -Force | Out-Null $mutex = New-Object Threading.Mutex($false, 'Global\TWT.VPN.RmmRepair') try { $ownsMutex = $mutex.WaitOne(0) } catch [Threading.AbandonedMutexException] { $ownsMutex = $true } if (!$ownsMutex) { Write-RecoveryLog 'Another recovery run is active; skipped.'; exit 0 } Write-RecoveryLog 'Checking TWT VPN installation and guardian service.' $service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue $installation = Join-Path $env:ProgramFiles 'TWT VPN' $exe = Join-Path $installation 'TWT.Vpn.exe' $healthy = $null -ne $service foreach ($name in 'TWT.Vpn.exe','TWT.Vpn.dll','Twt.Vpn.Core.dll','Recovery.zip','recovery-manifest.json','product-code.txt') { if (!(Test-Path -LiteralPath (Join-Path $installation $name) -PathType Leaf)) { $healthy = $false } } if ($healthy) { $healthy = (Get-AuthenticodeSignature -LiteralPath $exe).Status -eq 'Valid' } $installedVersion = [Version]'0.0.0' if (Test-Path -LiteralPath (Join-Path $installation 'TWT.Vpn.dll')) { try { $installedVersion = [Version]([Diagnostics.FileVersionInfo]::GetVersionInfo((Join-Path $installation 'TWT.Vpn.dll')).FileVersion) } catch { $healthy = $false } } if ($healthy) { Enable-Guardian $localPublisher = (Get-AuthenticodeSignature -LiteralPath $exe).SignerCertificate.Thumbprint if (!$ExpectedPublisher) { $ExpectedPublisher = $localPublisher } if ($localPublisher -ne $ExpectedPublisher) { throw 'Installed executable does not match the pinned publisher.' } $lastCheck = Join-Path $CacheDirectory 'update-check.txt' if (!$CheckForUpdates -and (Test-Path -LiteralPath $lastCheck) -and (Get-Item -LiteralPath $lastCheck).LastWriteTimeUtc -gt [DateTime]::UtcNow.AddHours(-1)) { Write-RecoveryLog 'Healthy: guardian enabled and running; next update check is not due. Saved pause unchanged.' exit 0 } Set-Content -LiteralPath $lastCheck -Value ([DateTime]::UtcNow.ToString('o')) } [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Write-RecoveryLog 'Checking the approved installer manifest for installation, repair or upgrade.' $manifest = Invoke-RestMethod -Uri "$Portal/download/manifest" -TimeoutSec 30 if ($manifest.sha256 -notmatch '^[A-Fa-f0-9]{64}$' -or $manifest.publisherThumbprint -notmatch '^[A-Fa-f0-9]{40}$') { throw 'Invalid installer verification metadata.' } if ($ExpectedPublisher -and $manifest.publisherThumbprint -ne $ExpectedPublisher) { throw 'Update publisher does not match the installed/pinned publisher. Signing-certificate changes require an administrator-deployed update.' } $availableVersion = $null if (![Version]::TryParse([string]$manifest.version, [ref]$availableVersion)) { throw 'Invalid installer version.' } if ($healthy -and $availableVersion -le $installedVersion) { Write-RecoveryLog 'Healthy and current: no newer installer available. Saved VPN pause unchanged.' exit 0 } if ($availableVersion -lt $installedVersion) { throw 'Refusing to downgrade the installed agent.' } $msi = Join-Path $CacheDirectory 'TWT-VPN.msi' if (!(Test-Package $msi $manifest)) { Write-RecoveryLog 'Downloading the signed installer. This can take several minutes.' $temporary = Join-Path $CacheDirectory 'TWT-VPN.download.msi' Invoke-WebRequest -Uri "$Portal/download/TWT-VPN.msi" -OutFile $temporary -UseBasicParsing -TimeoutSec 600 if (!(Test-Package $temporary $manifest)) { throw 'Downloaded MSI failed SHA256, trusted-signature or publisher verification.' } Move-Item -LiteralPath $temporary -Destination $msi -Force } else { Write-RecoveryLog 'Reusing the verified cached installer.' } $productCode = Get-MsiProperty $msi 'ProductCode' if ($productCode -notmatch '^\{[A-Fa-f0-9-]{36}\}$') { throw 'Invalid installer ProductCode.' } if ((Get-MsiProperty $msi 'UpgradeCode') -ne '{C7B83839-5765-4D2C-B03E-1D2C95419AD4}') { throw 'Installer is not a TWT VPN upgrade.' } if ([Version](Get-MsiProperty $msi 'ProductVersion') -ne $availableVersion) { throw 'Installer version does not match its manifest.' } $registered = (Test-Path -LiteralPath "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$productCode") -or (Test-Path -LiteralPath "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\$productCode") $installerLog = Join-Path $CacheDirectory 'msi-install.log' $installerArguments = "/i `"$msi`" /qn /norestart /L*v `"$installerLog`"" if ($registered) { $installerArguments += ' REINSTALL=ALL REINSTALLMODE=vomus' Write-RecoveryLog 'Repairing the registered version.' } else { Write-RecoveryLog 'Performing a full install or MSI-managed upgrade; leftover app registry markers are ignored.' } $installerProcess = Start-Process -FilePath "$env:WINDIR\System32\msiexec.exe" -ArgumentList $installerArguments -Wait -PassThru -WindowStyle Hidden if ($installerProcess.ExitCode -eq 1618) { throw 'Another Windows Installer transaction is running. The next scheduled recovery run will retry.' } if ($installerProcess.ExitCode -notin 0,3010) { throw "Windows Installer returned $($installerProcess.ExitCode). See $installerLog." } if (!(Get-Service -Name $ServiceName -ErrorAction SilentlyContinue) -or !(Test-Path -LiteralPath $exe -PathType Leaf)) { throw "Installer completed but the guardian or executable is still missing. See $installerLog." } Enable-Guardian Write-RecoveryLog 'Installation/recovery complete. Guardian enabled and running; retained enrollment and saved VPN pause were preserved.' if ($installerProcess.ExitCode -eq 3010) { Write-RecoveryLog 'Windows Installer requests a reboot. No reboot was forced.' } exit 0 } catch { $failure = 'FAILED: ' + $_.Exception.Message if (Test-Path -LiteralPath $CacheDirectory) { Write-RecoveryLog $failure } else { Write-Output $failure } exit 1 } finally { if ($ownsMutex) { $mutex.ReleaseMutex() } if ($null -ne $mutex) { $mutex.Dispose() } }