TruGrid SecureRDP Reachability Diagnostic Scripts
TruGrid Reachability Diagnostic Scripts
Two PowerShell scripts that answer the question support keeps having to answer by hand: can this box actually reach TruGrid, and if not, where does it break.
Both scripts produce a self-contained HTML report next to themselves that you can attach straight to a ticket. Console output is colored and live, so you can also watch the run if you are sitting at the box. No parameters, no prompts, no flags. You run them, they run. If you need to tweak something, the knobs are constants at the top of each script.
When to run which one
Test-TruGridBrokerReachability.ps1 runs on the broker. That is the Sentry server in a Sentry deployment, or the host machine in a Secure Connect deployment where host equals broker. It auto-detects which of the two it is by checking for C:\Program Files\TruGrid\Sentry\Agent.log first, then C:\Program Files\TruGrid\Secure Connect\Secure Connect.log.
Test-TruGridConnectorReachability.ps1 runs on a workstation with the TruGrid Connector installed. Per-user, no admin needed, in fact you should run it as the affected user so the right profile is examined. It looks for headless logs in %APPDATA%\TruGrid Connector and walks up to seven of the most recent daily-rotated files until it finds a relay IP. Production Portable connector logs are not searched.
What gets probed
Static TruGrid front-end endpoints (same for every customer), with full TLS handshake and certificate chain validation:
ws.trugrid.comdc.applicationinsights.azure.comdc.applicationinsights.microsoft.comdc.services.visualstudio.com
Dynamic relay IPs the broker or Connector has actually been using, pulled out of the local log. The broker script grabs them from the Connection to <ip>:443 Relay has been secured. line in Agent.log. The Connector script grabs them from the Attempting to connect to TruGrid Relay via TCP:<ip>:443 line that the newer TruGridRelayProvider code path writes. Older Connector builds with a different log format will yield nothing, in which case the script reports that and probes only the statics.
Both scripts also capture the cert issuer on every TLS handshake, even on relays where SNI cannot be validated against an IP. If the issuer does not match a list of common public CAs (Microsoft, DigiCert, GlobalSign, Sectigo, Let's Encrypt, GeoTrust, Entrust, GoDaddy, Amazon, Comodo, Baltimore), the script flags it amber and notes a possible MITM proxy. This is how Netskope, ZScaler, and similar middleboxes get caught even when port reachability looks fine. If your customer base uses a CA that is not on that hint list and you keep getting false amber flags, edit the $trustedIssuerHints array near the top of the relevant script.
The Connector script adds two things the broker script does not need:
- A loopback sanity check that binds an ephemeral port on
127.0.0.1, connects to it, and confirms winsock loopback is healthy. This catches third-party LSPs (some AVs, VPN clients, proxy filters) that mangle loopback and break the Connector's local 127.0.0.1:43000 forwarding step. If it fails, the report tells the operator to trynetsh winsock resetfrom an elevated prompt and reboot. - Known-issue scanning of the walked logs for signatures we have seen before. Currently this catches the Logan Capital relay tunnel race condition (
ObjectDisposedExceptiononSslStreamduring relay teardown), its receive-callback variant, and the cosmetic RDP target server cert CN mismatch from a Sysprep that did not regenerate the RDP cert. These get flagged in their own report section so they do not get confused with actual reachability failures.
Windows Connector Reachability
Save the script as a .PS1 file and run it. Alternatively, copy this and paste it into Powershell ISE window and run it from there.
# Connector reachability check. Runs per-user on the workstation with TruGrid Connector installed.
# Walks daily-rotated headless logs in %APPDATA%\TruGrid Connector, probes statics + relays + loopback,
# scans for known-bad signatures, dumps an HTML report. PS 5.1+. No params. Production Portable not searched.
# Edit the constants in the config block below if you need to tweak something.
# --- config ---
$Script:LastNRelays = 5
$Script:MaxLogFilesToWalk = 7
$Script:LogDir = Join-Path $env:APPDATA 'TruGrid Connector'
$Script:OutputDir = [Environment]::GetFolderPath('Desktop')
$Script:StaticEndpoints = @(
'ws.trugrid.com'
'dc.applicationinsights.azure.com'
'dc.applicationinsights.microsoft.com'
'dc.services.visualstudio.com'
)
$Script:LogFilePattern = 'win_headless_logs_*.txt'
$Script:RelayRegex = 'Attempting to connect to TruGrid Relay via TCP:(\d{1,3}(?:\.\d{1,3}){3}):443'
# known-issue signatures: pattern + label + note shown in the report
$Script:KnownIssues = @(
@{
Pattern = 'ObjectDisposedException.*SslStream'
Label = 'Relay tunnel async disposal race'
Note = 'Async disposal race in the relay tunnel code. Dev-side issue, not your firewall. Escalate if persistent.'
}
@{
Pattern = 'OnDataReceiveFromRelay.*ObjectDisposedException'
Label = 'Relay receive callback disposal race'
Note = 'Related to the relay tunnel async disposal race. Symptom of tunnel teardown timing, not connectivity.'
}
@{
Pattern = 'Certificate name check failed\. Expected:.*Actual CN:'
Label = 'RDP target server cert CN mismatch'
Note = 'Sysprep on the target machine did not regenerate its RDP cert. Cosmetic warning, not a TruGrid issue.'
}
)
$Script:TcpTimeoutMs = 3000
$Script:TlsTimeoutMs = 5000
# --- probe helpers ---
function Test-TlsEndpoint {
param(
[Parameter(Mandatory)] [string]$Target,
[int]$Port = 443,
[int]$TimeoutMs = $Script:TlsTimeoutMs,
[string]$ServerName
)
if (-not $ServerName) { $ServerName = $Target }
$result = [pscustomobject]@{
Target = $Target
Port = $Port
ServerName = $ServerName
TcpSuccess = $false
TlsSuccess = $false
ChainValid = $false
CertSubject = $null
CertIssuer = $null
CertNotAfter = $null
SuspiciousIssuer = $false
LatencyMs = $null
Error = $null
}
$trustedIssuerHints = @(
'Microsoft', 'DigiCert', 'GlobalSign', 'Let''s Encrypt', 'Sectigo',
'GeoTrust', 'Entrust', 'GoDaddy', 'Amazon', 'Comodo', 'Baltimore'
)
$client = [System.Net.Sockets.TcpClient]::new()
$sslStream = $null
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
try {
$iar = $client.BeginConnect($Target, $Port, $null, $null)
$waited = $iar.AsyncWaitHandle.WaitOne($TimeoutMs, $false)
if (-not ($waited -and $client.Connected)) {
$result.Error = "TCP connect timed out after ${TimeoutMs}ms"
return $result
}
$client.EndConnect($iar)
$stopwatch.Stop()
$result.TcpSuccess = $true
$result.LatencyMs = [math]::Round($stopwatch.Elapsed.TotalMilliseconds, 2)
$sslStream = [System.Net.Security.SslStream]::new(
$client.GetStream(),
$false,
{ param($s, $c, $ch, $e) return $true }
)
$sslStream.AuthenticateAsClient(
$ServerName,
$null,
[System.Security.Authentication.SslProtocols]::Tls12,
$false
)
$remoteCert = $sslStream.RemoteCertificate
if ($remoteCert) {
$cert2 = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($remoteCert)
$result.CertSubject = $cert2.Subject
$result.CertIssuer = $cert2.Issuer
$result.CertNotAfter = $cert2.NotAfter
$result.TlsSuccess = $true
$chain = [System.Security.Cryptography.X509Certificates.X509Chain]::new()
$chain.ChainPolicy.RevocationMode = 'NoCheck'
$result.ChainValid = $chain.Build($cert2)
$issuer = $cert2.Issuer
$matched = $false
foreach ($hint in $trustedIssuerHints) {
if ($issuer -like "*$hint*") { $matched = $true; break }
}
$result.SuspiciousIssuer = -not $matched
}
}
catch {
$result.Error = $_.Exception.Message
}
finally {
if ($sslStream) { $sslStream.Dispose() }
$client.Close()
}
return $result
}
function Test-LoopbackHealth {
# listen on ephemeral port, connect to it. catches winsock LSP weirdness that breaks 127.0.0.1:43000.
$result = [pscustomobject]@{
Success = $false
PortUsed = 0
LatencyMs = $null
Error = $null
}
$listener = $null
$client = $null
try {
$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
$listener.Start()
$port = $listener.LocalEndpoint.Port
$result.PortUsed = $port
$client = [System.Net.Sockets.TcpClient]::new()
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
$iar = $client.BeginConnect('127.0.0.1', $port, $null, $null)
$waited = $iar.AsyncWaitHandle.WaitOne(2000, $false)
if ($waited -and $client.Connected) {
$client.EndConnect($iar)
$stopwatch.Stop()
$result.Success = $true
$result.LatencyMs = [math]::Round($stopwatch.Elapsed.TotalMilliseconds, 2)
}
else {
$result.Error = 'Loopback connect timed out. A winsock LSP may be interfering.'
}
}
catch {
$result.Error = $_.Exception.Message
}
finally {
if ($client) { $client.Close() }
if ($listener) { $listener.Stop() }
}
return $result
}
# --- log discovery and parsing ---
function Get-RecentLogFiles {
param(
[Parameter(Mandatory)] [string]$Directory,
[Parameter(Mandatory)] [string]$Pattern,
[int]$Max = 7
)
if (-not (Test-Path $Directory)) { return @() }
return Get-ChildItem -Path $Directory -Filter $Pattern -File -ErrorAction SilentlyContinue |
Sort-Object Name -Descending |
Select-Object -First $Max
}
function Get-RelayIPsFromConnectorLogs {
param([Parameter(Mandatory)] [array]$Files, [int]$Last = 5)
$allIps = @()
$sourceFiles = @()
foreach ($file in $Files) {
try {
$content = Get-Content -Path $file.FullName -Tail 5000 -ErrorAction Stop
$lineMatches = $content | Select-String -Pattern $Script:RelayRegex -AllMatches
if ($lineMatches) {
$ipsThisFile = $lineMatches | ForEach-Object { $_.Matches.Groups[1].Value }
$allIps += $ipsThisFile
$sourceFiles += $file.FullName
}
}
catch {
Write-Warning "Could not read $($file.FullName): $($_.Exception.Message)"
continue
}
if (($allIps | Select-Object -Unique).Count -ge $Last) { break }
}
$unique = @($allIps | Select-Object -Unique | Select-Object -Last $Last)
return [pscustomobject]@{
RelayIps = $unique
SourceFiles = $sourceFiles
}
}
function Find-KnownIssues {
param([Parameter(Mandatory)] [array]$Files)
$hits = @()
foreach ($file in $Files) {
try {
$content = Get-Content -Path $file.FullName -ErrorAction Stop -Raw
}
catch { continue }
foreach ($issue in $Script:KnownIssues) {
$regexMatches = [regex]::Matches($content, $issue.Pattern)
if ($regexMatches.Count -gt 0) {
$hits += [pscustomobject]@{
Label = $issue.Label
Note = $issue.Note
File = $file.Name
Count = $regexMatches.Count
}
}
}
}
return $hits
}
# --- environment fingerprint ---
function Get-EnvironmentFingerprint {
$os = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue
$proxy = & netsh winhttp show proxy 2>&1 | Out-String
$dnsServers = ''
try {
$dnsServers = (Get-DnsClientServerAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
Where-Object { $_.ServerAddresses } |
ForEach-Object { $_.ServerAddresses } |
Select-Object -Unique) -join ', '
}
catch { }
[pscustomobject]@{
Hostname = $env:COMPUTERNAME
Username = "$env:USERDOMAIN\$env:USERNAME"
OSCaption = if ($os) { $os.Caption } else { 'unknown' }
OSVersion = if ($os) { $os.Version } else { 'unknown' }
PSVersion = $PSVersionTable.PSVersion.ToString()
DotNetVersion = [System.Environment]::Version.ToString()
DnsServers = $dnsServers
WinHttpProxy = $proxy.Trim()
Timestamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss zzz')
ScriptVersion = '1.0.0'
}
}
# --- html rendering ---
function ConvertTo-HtmlEncoded {
param([string]$Text)
if ($null -eq $Text) { return '' }
return [System.Net.WebUtility]::HtmlEncode($Text)
}
function Get-StatusClass {
param([bool]$Pass, [bool]$Warn = $false)
if ($Warn) { return 'warn' }
if ($Pass) { return 'pass' }
return 'fail'
}
function Write-HtmlReport {
param(
[Parameter(Mandatory)] [pscustomobject]$Env,
[Parameter(Mandatory)] [string]$LogDir,
[Parameter(Mandatory)] [AllowEmptyCollection()] [array]$LogFilesWalked,
[Parameter(Mandatory)] [AllowEmptyCollection()] [array]$RelayIps,
[Parameter(Mandatory)] [AllowEmptyCollection()] [array]$RelaySourceFiles,
[Parameter(Mandatory)] [AllowEmptyCollection()] [array]$StaticResults,
[Parameter(Mandatory)] [AllowEmptyCollection()] [array]$RelayResults,
[Parameter(Mandatory)] [pscustomobject]$LoopbackResult,
[Parameter(Mandatory)] [AllowEmptyCollection()] [array]$KnownIssueHits,
[Parameter(Mandatory)] [string]$OutputPath
)
$totalTests = $StaticResults.Count + $RelayResults.Count + 1
$passedTests = ($StaticResults | Where-Object { $_.TcpSuccess -and $_.TlsSuccess -and $_.ChainValid -and -not $_.SuspiciousIssuer }).Count
$passedTests += ($RelayResults | Where-Object { $_.TcpSuccess }).Count
if ($LoopbackResult.Success) { $passedTests++ }
$failedTests = $totalTests - $passedTests
if ($failedTests -eq 0) {
$bannerClass = 'banner-pass'
$verdict = 'All probes passed. Connector should be able to reach TruGrid cloud and the assigned relays from this user profile.'
}
elseif ($failedTests -lt ($totalTests / 2)) {
$bannerClass = 'banner-warn'
$verdict = "$failedTests of $totalTests probes failed. Partial reachability. See sections below."
}
else {
$bannerClass = 'banner-fail'
$verdict = "$failedTests of $totalTests probes failed. Significant connectivity problem. See sections below."
}
$sb = [System.Text.StringBuilder]::new()
[void]$sb.AppendLine('<!DOCTYPE html>')
[void]$sb.AppendLine('<html lang="en"><head><meta charset="UTF-8">')
[void]$sb.AppendLine("<title>TruGrid Connector Reachability Report - $(ConvertTo-HtmlEncoded $Env.Hostname)</title>")
[void]$sb.AppendLine(@'
<style>
:root {
--bg: #1a1d23; --panel: #242830; --text: #e8eaed; --muted: #9aa0a6;
--pass: #4caf50; --fail: #ef5350; --warn: #ffa726; --skip: #757575;
--border: #3a3f47; --accent: #5e9eff;
}
* { box-sizing: border-box; }
body { background: var(--bg); color: var(--text); font-family: -apple-system, Segoe UI, Roboto, sans-serif;
margin: 0; padding: 24px; line-height: 1.5; }
h1 { margin: 0 0 8px; font-size: 22px; color: var(--accent); }
h2 { margin: 24px 0 12px; font-size: 16px; color: var(--accent);
border-bottom: 1px solid var(--border); padding-bottom: 6px; }
.meta { color: var(--muted); font-size: 12px; margin-bottom: 16px; }
.banner { padding: 14px 18px; border-radius: 6px; margin: 16px 0;
font-weight: 600; font-size: 14px; }
.banner-pass { background: rgba(76,175,80,0.15); border-left: 4px solid var(--pass); }
.banner-warn { background: rgba(255,167,38,0.15); border-left: 4px solid var(--warn); }
.banner-fail { background: rgba(239,83,80,0.15); border-left: 4px solid var(--fail); }
table { width: 100%; border-collapse: collapse; background: var(--panel);
border: 1px solid var(--border); font-size: 13px; }
th, td { padding: 8px 12px; text-align: left; border-bottom: 1px solid var(--border);
vertical-align: top; word-break: break-word; }
th { background: rgba(94,158,255,0.08); font-weight: 600; color: var(--accent); }
tr:last-child td { border-bottom: none; }
.pass { color: var(--pass); font-weight: 600; }
.fail { color: var(--fail); font-weight: 600; }
.warn { color: var(--warn); font-weight: 600; }
.skip { color: var(--skip); }
.mono { font-family: Consolas, Menlo, monospace; font-size: 12px; }
.env-grid { display: grid; grid-template-columns: 180px 1fr; gap: 4px 12px;
background: var(--panel); padding: 12px 16px; border: 1px solid var(--border);
border-radius: 4px; font-size: 13px; }
.env-grid .k { color: var(--muted); }
.env-grid .v { font-family: Consolas, Menlo, monospace; word-break: break-all; }
pre { background: var(--panel); border: 1px solid var(--border); padding: 10px;
font-size: 12px; overflow-x: auto; white-space: pre-wrap; border-radius: 4px; }
.footer { color: var(--muted); font-size: 11px; margin-top: 32px;
border-top: 1px solid var(--border); padding-top: 12px; }
ul { padding-left: 22px; }
@media print {
body { background: white; color: black; padding: 12px; }
table, .env-grid, pre { background: white; border-color: #aaa; }
th { background: #f0f0f0; color: black; }
.banner-pass, .banner-warn, .banner-fail { background: #f4f4f4; color: black; }
h1, h2 { color: black; }
}
</style>
'@)
[void]$sb.AppendLine('</head><body>')
[void]$sb.AppendLine("<h1>TruGrid Connector Reachability Report</h1>")
[void]$sb.AppendLine("<div class='meta'>Generated $(ConvertTo-HtmlEncoded $Env.Timestamp) | Script v$(ConvertTo-HtmlEncoded $Env.ScriptVersion)</div>")
[void]$sb.AppendLine("<div class='banner $bannerClass'>$verdict</div>")
[void]$sb.AppendLine("<h2>Environment</h2>")
[void]$sb.AppendLine("<div class='env-grid'>")
[void]$sb.AppendLine("<div class='k'>Hostname</div><div class='v'>$(ConvertTo-HtmlEncoded $Env.Hostname)</div>")
[void]$sb.AppendLine("<div class='k'>User</div><div class='v'>$(ConvertTo-HtmlEncoded $Env.Username)</div>")
[void]$sb.AppendLine("<div class='k'>OS</div><div class='v'>$(ConvertTo-HtmlEncoded $Env.OSCaption) ($(ConvertTo-HtmlEncoded $Env.OSVersion))</div>")
[void]$sb.AppendLine("<div class='k'>PowerShell</div><div class='v'>$(ConvertTo-HtmlEncoded $Env.PSVersion)</div>")
[void]$sb.AppendLine("<div class='k'>.NET</div><div class='v'>$(ConvertTo-HtmlEncoded $Env.DotNetVersion)</div>")
[void]$sb.AppendLine("<div class='k'>DNS Servers</div><div class='v'>$(ConvertTo-HtmlEncoded $Env.DnsServers)</div>")
[void]$sb.AppendLine("<div class='k'>Log Directory</div><div class='v'>$(ConvertTo-HtmlEncoded $LogDir)</div>")
if ($LogFilesWalked.Count -gt 0) {
$walkedNames = ($LogFilesWalked | ForEach-Object { $_.Name }) -join ', '
[void]$sb.AppendLine("<div class='k'>Log Files Walked</div><div class='v'>$(ConvertTo-HtmlEncoded $walkedNames)</div>")
}
[void]$sb.AppendLine("</div>")
[void]$sb.AppendLine("<h2>Static TruGrid Endpoints</h2>")
[void]$sb.AppendLine("<table><thead><tr><th>FQDN</th><th>TCP 443</th><th>TLS</th><th>Chain</th><th>Cert Issuer</th><th>Notes</th></tr></thead><tbody>")
foreach ($r in $StaticResults) {
$tcpClass = Get-StatusClass -Pass $r.TcpSuccess
$tlsClass = Get-StatusClass -Pass $r.TlsSuccess
$chainClass = Get-StatusClass -Pass $r.ChainValid
$issuerHtml = ConvertTo-HtmlEncoded $r.CertIssuer
$issuerClass = if ($r.SuspiciousIssuer) { 'warn' } else { '' }
$notes = @()
if ($r.SuspiciousIssuer -and $r.CertIssuer) { $notes += 'Issuer not on common CA hint list. Possible MITM proxy.' }
if ($r.Error) { $notes += $r.Error }
$notesHtml = ConvertTo-HtmlEncoded ($notes -join ' ')
[void]$sb.AppendLine("<tr>")
[void]$sb.AppendLine("<td class='mono'>$(ConvertTo-HtmlEncoded $r.Target)</td>")
[void]$sb.AppendLine("<td class='$tcpClass'>$(if ($r.TcpSuccess) { 'PASS' } else { 'FAIL' })</td>")
[void]$sb.AppendLine("<td class='$tlsClass'>$(if ($r.TlsSuccess) { 'PASS' } else { 'FAIL' })</td>")
[void]$sb.AppendLine("<td class='$chainClass'>$(if ($r.ChainValid) { 'VALID' } else { 'INVALID' })</td>")
[void]$sb.AppendLine("<td class='mono $issuerClass'>$issuerHtml</td>")
[void]$sb.AppendLine("<td>$notesHtml</td>")
[void]$sb.AppendLine("</tr>")
}
[void]$sb.AppendLine("</tbody></table>")
[void]$sb.AppendLine("<h2>Dynamic Relay Endpoints</h2>")
if ($RelayResults.Count -eq 0) {
[void]$sb.AppendLine("<p class='warn'>No relay IPs were probed. Either the Connector logs contain no recent relay connection entries (older build, or only local-network sessions made from this machine), or log parsing failed.</p>")
}
else {
$sources = if ($RelaySourceFiles) { ($RelaySourceFiles | ForEach-Object { Split-Path $_ -Leaf }) -join ', ' } else { 'n/a' }
[void]$sb.AppendLine("<p class='meta'>Last $($RelayResults.Count) unique relay IP(s) extracted from: $(ConvertTo-HtmlEncoded $sources)</p>")
[void]$sb.AppendLine("<table><thead><tr><th>Relay IP</th><th>TCP 443</th><th>Latency</th><th>TLS Issuer</th><th>Notes</th></tr></thead><tbody>")
foreach ($r in $RelayResults) {
$tcpClass = Get-StatusClass -Pass $r.TcpSuccess
$latency = if ($null -ne $r.LatencyMs) { "$($r.LatencyMs) ms" } else { 'n/a' }
$issuerHtml = ConvertTo-HtmlEncoded $r.CertIssuer
$issuerClass = if ($r.SuspiciousIssuer) { 'warn' } else { '' }
$notes = @()
if ($r.SuspiciousIssuer -and $r.CertIssuer) { $notes += 'Cert issuer not on common CA hint list. Possible MITM proxy.' }
if ($r.Error) { $notes += $r.Error }
$notesHtml = ConvertTo-HtmlEncoded ($notes -join ' ')
[void]$sb.AppendLine("<tr>")
[void]$sb.AppendLine("<td class='mono'>$(ConvertTo-HtmlEncoded $r.Target)</td>")
[void]$sb.AppendLine("<td class='$tcpClass'>$(if ($r.TcpSuccess) { 'PASS' } else { 'FAIL' })</td>")
[void]$sb.AppendLine("<td>$latency</td>")
[void]$sb.AppendLine("<td class='mono $issuerClass'>$issuerHtml</td>")
[void]$sb.AppendLine("<td>$notesHtml</td>")
[void]$sb.AppendLine("</tr>")
}
[void]$sb.AppendLine("</tbody></table>")
}
[void]$sb.AppendLine("<h2>Loopback Sanity (Winsock LSP Check)</h2>")
$lbClass = Get-StatusClass -Pass $LoopbackResult.Success
[void]$sb.AppendLine("<table><thead><tr><th>Result</th><th>Port</th><th>Latency</th><th>Notes</th></tr></thead><tbody><tr>")
[void]$sb.AppendLine("<td class='$lbClass'>$(if ($LoopbackResult.Success) { 'PASS' } else { 'FAIL' })</td>")
[void]$sb.AppendLine("<td class='mono'>127.0.0.1:$($LoopbackResult.PortUsed)</td>")
[void]$sb.AppendLine("<td>$(if ($null -ne $LoopbackResult.LatencyMs) { "$($LoopbackResult.LatencyMs) ms" } else { 'n/a' })</td>")
$lbNote = if ($LoopbackResult.Success) {
'Loopback path is healthy. Connector local TCP forwarding (127.0.0.1:43000) should work.'
} else {
"Loopback connect failed. A winsock LSP (third-party AV, VPN, proxy filter) may be interfering. Try 'netsh winsock reset' from an elevated prompt, then reboot. Error: $($LoopbackResult.Error)"
}
[void]$sb.AppendLine("<td>$(ConvertTo-HtmlEncoded $lbNote)</td>")
[void]$sb.AppendLine("</tr></tbody></table>")
[void]$sb.AppendLine("<h2>Known Issue Detection</h2>")
if ($KnownIssueHits.Count -eq 0) {
[void]$sb.AppendLine("<p class='pass'>No known issue signatures found in the walked logs.</p>")
}
else {
[void]$sb.AppendLine("<table><thead><tr><th>Signature</th><th>File</th><th>Hits</th><th>Note</th></tr></thead><tbody>")
foreach ($hit in $KnownIssueHits) {
[void]$sb.AppendLine("<tr>")
[void]$sb.AppendLine("<td class='warn'>$(ConvertTo-HtmlEncoded $hit.Label)</td>")
[void]$sb.AppendLine("<td class='mono'>$(ConvertTo-HtmlEncoded $hit.File)</td>")
[void]$sb.AppendLine("<td>$(ConvertTo-HtmlEncoded $hit.Count)</td>")
[void]$sb.AppendLine("<td>$(ConvertTo-HtmlEncoded $hit.Note)</td>")
[void]$sb.AppendLine("</tr>")
}
[void]$sb.AppendLine("</tbody></table>")
}
[void]$sb.AppendLine("<h2>WinHTTP Proxy Configuration</h2>")
[void]$sb.AppendLine("<pre>$(ConvertTo-HtmlEncoded $Env.WinHttpProxy)</pre>")
[void]$sb.AppendLine("<div class='footer'>Generated by Test-TruGridConnectorReachability.ps1 v$(ConvertTo-HtmlEncoded $Env.ScriptVersion). Production Portable connector logs are not searched. Run as the affected user, not as administrator, so the correct profile is examined.</div>")
[void]$sb.AppendLine("</body></html>")
Set-Content -Path $OutputPath -Value $sb.ToString() -Encoding UTF8
}
# --- main ---
Write-Host "TruGrid Connector Reachability Diagnostic" -ForegroundColor Cyan
Write-Host ("=" * 60) -ForegroundColor DarkGray
$envInfo = Get-EnvironmentFingerprint
Write-Host "`n[1/5] Locating Connector log directory..." -ForegroundColor Yellow
if (-not (Test-Path $Script:LogDir)) {
Write-Warning " Connector log directory not found: $Script:LogDir"
Write-Warning " Either Connector is not installed for this user or it has never run."
Write-Warning " Edit `$Script:LogDir at the top of this script if your install is non-standard."
}
else {
Write-Host " Using: $Script:LogDir" -ForegroundColor Green
}
Write-Host "`n[2/5] Walking up to $($Script:MaxLogFilesToWalk) recent log file(s)..." -ForegroundColor Yellow
$logFiles = Get-RecentLogFiles -Directory $Script:LogDir -Pattern $Script:LogFilePattern -Max $Script:MaxLogFilesToWalk
if ($logFiles.Count -eq 0) {
Write-Warning " No files matching $($Script:LogFilePattern) found."
}
else {
Write-Host " Found $($logFiles.Count) log file(s)." -ForegroundColor Green
}
Write-Host "`n[3/5] Extracting last $($Script:LastNRelays) unique relay IP(s) from logs..." -ForegroundColor Yellow
$relayIps = @()
$relaySourceFiles = @()
if ($logFiles.Count -gt 0) {
$parseResult = Get-RelayIPsFromConnectorLogs -Files $logFiles -Last $Script:LastNRelays
$relayIps = @($parseResult.RelayIps)
$relaySourceFiles = @($parseResult.SourceFiles)
}
if ($relayIps.Count -eq 0) {
Write-Warning " No relay IPs found in logs. Will probe static endpoints only."
}
else {
Write-Host " Found $($relayIps.Count) relay IP(s): $($relayIps -join ', ')" -ForegroundColor Green
}
$knownIssues = @()
if ($logFiles.Count -gt 0) {
$knownIssues = @(Find-KnownIssues -Files $logFiles)
}
Write-Host "`n[4/5] Probing $($Script:StaticEndpoints.Count) static endpoint(s), $($relayIps.Count) relay IP(s), and loopback..." -ForegroundColor Yellow
$staticResults = @()
foreach ($ep in $Script:StaticEndpoints) {
Write-Host " -> $ep" -NoNewline
$r = Test-TlsEndpoint -Target $ep
$staticResults += $r
if ($r.TcpSuccess -and $r.TlsSuccess -and $r.ChainValid -and -not $r.SuspiciousIssuer) {
Write-Host " PASS" -ForegroundColor Green
}
elseif ($r.SuspiciousIssuer) {
Write-Host " WARN (issuer: $($r.CertIssuer))" -ForegroundColor Yellow
}
else {
Write-Host " FAIL ($($r.Error))" -ForegroundColor Red
}
}
$relayResults = @()
foreach ($ip in $relayIps) {
Write-Host " -> $ip" -NoNewline
$r = Test-TlsEndpoint -Target $ip -ServerName $ip
$relayResults += $r
if ($r.TcpSuccess) {
if ($r.SuspiciousIssuer) {
Write-Host " WARN (issuer: $($r.CertIssuer))" -ForegroundColor Yellow
}
else {
Write-Host " PASS ($($r.LatencyMs) ms)" -ForegroundColor Green
}
}
else {
Write-Host " FAIL ($($r.Error))" -ForegroundColor Red
}
}
Write-Host " -> 127.0.0.1 loopback" -NoNewline
$loopback = Test-LoopbackHealth
if ($loopback.Success) {
Write-Host " PASS ($($loopback.LatencyMs) ms)" -ForegroundColor Green
}
else {
Write-Host " FAIL ($($loopback.Error))" -ForegroundColor Red
}
Write-Host "`n[5/5] Writing report..." -ForegroundColor Yellow
if (-not (Test-Path $Script:OutputDir)) {
New-Item -Path $Script:OutputDir -ItemType Directory -Force | Out-Null
}
$stamp = (Get-Date).ToString('yyyyMMdd_HHmmss')
$htmlPath = Join-Path $Script:OutputDir "TruGridConnectorReachability_$($envInfo.Hostname)_$stamp.html"
Write-HtmlReport -Env $envInfo -LogDir $Script:LogDir -LogFilesWalked $logFiles `
-RelayIps $relayIps -RelaySourceFiles $relaySourceFiles `
-StaticResults $staticResults -RelayResults $relayResults `
-LoopbackResult $loopback -KnownIssueHits $knownIssues -OutputPath $htmlPath
Write-Host " HTML report: $htmlPath" -ForegroundColor Green
Write-Host "`nDone." -ForegroundColor Cyan
Example output:

Secure Connect / Sentry Reachability
Save the script as a .PS1 file and run it. Alternatively, copy this and paste it into Powershell ISE window and run it from there.
# Broker reachability check. Run on the Sentry box or the SC host (where host = broker).
# Pulls the last N relay IPs out of Agent.log / Secure Connect.log, probes statics + relays,
# spits out an HTML report next to the script. PS 5.1+. No modules. No params.
# Edit the constants in the config block below if you need to tweak something.
# --- config ---
$Script:LastNRelays = 5
$Script:OutputDir = [Environment]::GetFolderPath('Desktop')
$Script:StaticEndpoints = @(
'ws.trugrid.com'
'dc.applicationinsights.azure.com'
'dc.applicationinsights.microsoft.com'
'dc.services.visualstudio.com'
)
$Script:KnownLogPaths = @(
'C:\Program Files\TruGrid\Sentry\Agent.log'
'C:\Program Files\TruGrid\Secure Connect\Secure Connect.log'
)
$Script:RelayRegex = 'Connection to (\d{1,3}(?:\.\d{1,3}){3}):443 Relay has been secured\.'
$Script:TcpTimeoutMs = 3000
$Script:TlsTimeoutMs = 5000
# --- probe helpers ---
function Test-TlsEndpoint {
param(
[Parameter(Mandatory)] [string]$Target,
[int]$Port = 443,
[int]$TimeoutMs = $Script:TlsTimeoutMs,
[string]$ServerName
)
if (-not $ServerName) { $ServerName = $Target }
$result = [pscustomobject]@{
Target = $Target
Port = $Port
ServerName = $ServerName
TcpSuccess = $false
TlsSuccess = $false
ChainValid = $false
CertSubject = $null
CertIssuer = $null
CertNotAfter = $null
SuspiciousIssuer = $false
LatencyMs = $null
Error = $null
}
$trustedIssuerHints = @(
'Microsoft', 'DigiCert', 'GlobalSign', 'Let''s Encrypt', 'Sectigo',
'GeoTrust', 'Entrust', 'GoDaddy', 'Amazon', 'Comodo', 'Baltimore'
)
$client = [System.Net.Sockets.TcpClient]::new()
$sslStream = $null
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
try {
$iar = $client.BeginConnect($Target, $Port, $null, $null)
$waited = $iar.AsyncWaitHandle.WaitOne($TimeoutMs, $false)
if (-not ($waited -and $client.Connected)) {
$result.Error = "TCP connect timed out after ${TimeoutMs}ms"
return $result
}
$client.EndConnect($iar)
$stopwatch.Stop()
$result.TcpSuccess = $true
$result.LatencyMs = [math]::Round($stopwatch.Elapsed.TotalMilliseconds, 2)
$sslStream = [System.Net.Security.SslStream]::new(
$client.GetStream(),
$false,
{ param($s, $c, $ch, $e) return $true }
)
$sslStream.AuthenticateAsClient(
$ServerName,
$null,
[System.Security.Authentication.SslProtocols]::Tls12,
$false
)
$remoteCert = $sslStream.RemoteCertificate
if ($remoteCert) {
$cert2 = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($remoteCert)
$result.CertSubject = $cert2.Subject
$result.CertIssuer = $cert2.Issuer
$result.CertNotAfter = $cert2.NotAfter
$result.TlsSuccess = $true
$chain = [System.Security.Cryptography.X509Certificates.X509Chain]::new()
$chain.ChainPolicy.RevocationMode = 'NoCheck'
$result.ChainValid = $chain.Build($cert2)
$issuer = $cert2.Issuer
$matched = $false
foreach ($hint in $trustedIssuerHints) {
if ($issuer -like "*$hint*") { $matched = $true; break }
}
$result.SuspiciousIssuer = -not $matched
}
}
catch {
$result.Error = $_.Exception.Message
}
finally {
if ($sslStream) { $sslStream.Dispose() }
$client.Close()
}
return $result
}
# --- log discovery and parsing ---
function Find-BrokerLog {
foreach ($p in $Script:KnownLogPaths) {
if (Test-Path $p) { return $p }
}
return $null
}
function Get-RelayIPsFromBrokerLog {
param([Parameter(Mandatory)] [string]$Path, [int]$Last = 5)
$ips = @()
try {
$lineMatches = Get-Content -Path $Path -Tail 2000 -ErrorAction Stop |
Select-String -Pattern $Script:RelayRegex -AllMatches
$ips = $lineMatches |
ForEach-Object { $_.Matches.Groups[1].Value } |
Select-Object -Unique |
Select-Object -Last $Last
}
catch {
Write-Warning "Could not read log file: $($_.Exception.Message)"
}
return @($ips)
}
# --- environment fingerprint for the report ---
function Get-EnvironmentFingerprint {
$os = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue
$proxy = & netsh winhttp show proxy 2>&1 | Out-String
$dnsServers = ''
try {
$dnsServers = (Get-DnsClientServerAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
Where-Object { $_.ServerAddresses } |
ForEach-Object { $_.ServerAddresses } |
Select-Object -Unique) -join ', '
}
catch { }
[pscustomobject]@{
Hostname = $env:COMPUTERNAME
Username = "$env:USERDOMAIN\$env:USERNAME"
OSCaption = if ($os) { $os.Caption } else { 'unknown' }
OSVersion = if ($os) { $os.Version } else { 'unknown' }
PSVersion = $PSVersionTable.PSVersion.ToString()
DotNetVersion = [System.Environment]::Version.ToString()
DnsServers = $dnsServers
WinHttpProxy = $proxy.Trim()
Timestamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss zzz')
ScriptVersion = '1.0.0'
}
}
# --- html rendering ---
function ConvertTo-HtmlEncoded {
param([string]$Text)
if ($null -eq $Text) { return '' }
return [System.Net.WebUtility]::HtmlEncode($Text)
}
function Get-StatusClass {
param([bool]$Pass, [bool]$Warn = $false)
if ($Warn) { return 'warn' }
if ($Pass) { return 'pass' }
return 'fail'
}
function Write-HtmlReport {
param(
[Parameter(Mandatory)] [pscustomobject]$Env,
[Parameter(Mandatory)] [string]$LogFile,
[Parameter(Mandatory)] [AllowEmptyCollection()] [array]$RelayIps,
[Parameter(Mandatory)] [AllowEmptyCollection()] [array]$StaticResults,
[Parameter(Mandatory)] [AllowEmptyCollection()] [array]$RelayResults,
[Parameter(Mandatory)] [string]$OutputPath
)
$totalTests = $StaticResults.Count + $RelayResults.Count
$passedTests = ($StaticResults | Where-Object { $_.TcpSuccess -and $_.TlsSuccess -and $_.ChainValid -and -not $_.SuspiciousIssuer }).Count
$passedTests += ($RelayResults | Where-Object { $_.TcpSuccess }).Count
$failedTests = $totalTests - $passedTests
if ($failedTests -eq 0) {
$bannerClass = 'banner-pass'
$verdict = 'All probes passed. Broker should be able to reach TruGrid cloud and assigned relays.'
}
elseif ($failedTests -lt ($totalTests / 2)) {
$bannerClass = 'banner-warn'
$verdict = "$failedTests of $totalTests probes failed. Partial reachability. See sections below."
}
else {
$bannerClass = 'banner-fail'
$verdict = "$failedTests of $totalTests probes failed. Significant connectivity problem. See sections below."
}
$sb = [System.Text.StringBuilder]::new()
[void]$sb.AppendLine('<!DOCTYPE html>')
[void]$sb.AppendLine('<html lang="en"><head><meta charset="UTF-8">')
[void]$sb.AppendLine("<title>TruGrid Broker Reachability Report - $(ConvertTo-HtmlEncoded $Env.Hostname)</title>")
[void]$sb.AppendLine(@'
<style>
:root {
--bg: #1a1d23; --panel: #242830; --text: #e8eaed; --muted: #9aa0a6;
--pass: #4caf50; --fail: #ef5350; --warn: #ffa726; --skip: #757575;
--border: #3a3f47; --accent: #5e9eff;
}
* { box-sizing: border-box; }
body { background: var(--bg); color: var(--text); font-family: -apple-system, Segoe UI, Roboto, sans-serif;
margin: 0; padding: 24px; line-height: 1.5; }
h1 { margin: 0 0 8px; font-size: 22px; color: var(--accent); }
h2 { margin: 24px 0 12px; font-size: 16px; color: var(--accent);
border-bottom: 1px solid var(--border); padding-bottom: 6px; }
.meta { color: var(--muted); font-size: 12px; margin-bottom: 16px; }
.banner { padding: 14px 18px; border-radius: 6px; margin: 16px 0;
font-weight: 600; font-size: 14px; }
.banner-pass { background: rgba(76,175,80,0.15); border-left: 4px solid var(--pass); }
.banner-warn { background: rgba(255,167,38,0.15); border-left: 4px solid var(--warn); }
.banner-fail { background: rgba(239,83,80,0.15); border-left: 4px solid var(--fail); }
table { width: 100%; border-collapse: collapse; background: var(--panel);
border: 1px solid var(--border); font-size: 13px; }
th, td { padding: 8px 12px; text-align: left; border-bottom: 1px solid var(--border);
vertical-align: top; word-break: break-word; }
th { background: rgba(94,158,255,0.08); font-weight: 600; color: var(--accent); }
tr:last-child td { border-bottom: none; }
.pass { color: var(--pass); font-weight: 600; }
.fail { color: var(--fail); font-weight: 600; }
.warn { color: var(--warn); font-weight: 600; }
.skip { color: var(--skip); }
.mono { font-family: Consolas, Menlo, monospace; font-size: 12px; }
.env-grid { display: grid; grid-template-columns: 180px 1fr; gap: 4px 12px;
background: var(--panel); padding: 12px 16px; border: 1px solid var(--border);
border-radius: 4px; font-size: 13px; }
.env-grid .k { color: var(--muted); }
.env-grid .v { font-family: Consolas, Menlo, monospace; word-break: break-all; }
pre { background: var(--panel); border: 1px solid var(--border); padding: 10px;
font-size: 12px; overflow-x: auto; white-space: pre-wrap; border-radius: 4px; }
.footer { color: var(--muted); font-size: 11px; margin-top: 32px;
border-top: 1px solid var(--border); padding-top: 12px; }
@media print {
body { background: white; color: black; padding: 12px; }
table, .env-grid, pre { background: white; border-color: #aaa; }
th { background: #f0f0f0; color: black; }
.banner-pass, .banner-warn, .banner-fail { background: #f4f4f4; color: black; }
h1, h2 { color: black; }
}
</style>
'@)
[void]$sb.AppendLine('</head><body>')
[void]$sb.AppendLine("<h1>TruGrid Broker Reachability Report</h1>")
[void]$sb.AppendLine("<div class='meta'>Generated $(ConvertTo-HtmlEncoded $Env.Timestamp) | Script v$(ConvertTo-HtmlEncoded $Env.ScriptVersion)</div>")
[void]$sb.AppendLine("<div class='banner $bannerClass'>$verdict</div>")
[void]$sb.AppendLine("<h2>Environment</h2>")
[void]$sb.AppendLine("<div class='env-grid'>")
[void]$sb.AppendLine("<div class='k'>Hostname</div><div class='v'>$(ConvertTo-HtmlEncoded $Env.Hostname)</div>")
[void]$sb.AppendLine("<div class='k'>User</div><div class='v'>$(ConvertTo-HtmlEncoded $Env.Username)</div>")
[void]$sb.AppendLine("<div class='k'>OS</div><div class='v'>$(ConvertTo-HtmlEncoded $Env.OSCaption) ($(ConvertTo-HtmlEncoded $Env.OSVersion))</div>")
[void]$sb.AppendLine("<div class='k'>PowerShell</div><div class='v'>$(ConvertTo-HtmlEncoded $Env.PSVersion)</div>")
[void]$sb.AppendLine("<div class='k'>.NET</div><div class='v'>$(ConvertTo-HtmlEncoded $Env.DotNetVersion)</div>")
[void]$sb.AppendLine("<div class='k'>DNS Servers</div><div class='v'>$(ConvertTo-HtmlEncoded $Env.DnsServers)</div>")
[void]$sb.AppendLine("<div class='k'>Log File</div><div class='v'>$(ConvertTo-HtmlEncoded $LogFile)</div>")
[void]$sb.AppendLine("</div>")
[void]$sb.AppendLine("<h2>Static TruGrid Endpoints</h2>")
[void]$sb.AppendLine("<table><thead><tr><th>FQDN</th><th>TCP 443</th><th>TLS</th><th>Chain</th><th>Cert Issuer</th><th>Notes</th></tr></thead><tbody>")
foreach ($r in $StaticResults) {
$tcpClass = Get-StatusClass -Pass $r.TcpSuccess
$tlsClass = Get-StatusClass -Pass $r.TlsSuccess
$chainClass = Get-StatusClass -Pass $r.ChainValid
$issuerHtml = ConvertTo-HtmlEncoded $r.CertIssuer
$issuerClass = if ($r.SuspiciousIssuer) { 'warn' } else { '' }
$notes = @()
if ($r.SuspiciousIssuer -and $r.CertIssuer) { $notes += 'Issuer not on common CA hint list. Possible MITM proxy.' }
if ($r.Error) { $notes += $r.Error }
$notesHtml = ConvertTo-HtmlEncoded ($notes -join ' ')
[void]$sb.AppendLine("<tr>")
[void]$sb.AppendLine("<td class='mono'>$(ConvertTo-HtmlEncoded $r.Target)</td>")
[void]$sb.AppendLine("<td class='$tcpClass'>$(if ($r.TcpSuccess) { 'PASS' } else { 'FAIL' })</td>")
[void]$sb.AppendLine("<td class='$tlsClass'>$(if ($r.TlsSuccess) { 'PASS' } else { 'FAIL' })</td>")
[void]$sb.AppendLine("<td class='$chainClass'>$(if ($r.ChainValid) { 'VALID' } else { 'INVALID' })</td>")
[void]$sb.AppendLine("<td class='mono $issuerClass'>$issuerHtml</td>")
[void]$sb.AppendLine("<td>$notesHtml</td>")
[void]$sb.AppendLine("</tr>")
}
[void]$sb.AppendLine("</tbody></table>")
[void]$sb.AppendLine("<h2>Dynamic Relay Endpoints</h2>")
if ($RelayResults.Count -eq 0) {
[void]$sb.AppendLine("<p class='warn'>No relay IPs were probed. Either the broker log contained no relay connection entries or log parsing failed.</p>")
}
else {
[void]$sb.AppendLine("<p class='meta'>Last $($RelayResults.Count) unique relay IP(s) extracted from $(ConvertTo-HtmlEncoded $LogFile)</p>")
[void]$sb.AppendLine("<table><thead><tr><th>Relay IP</th><th>TCP 443</th><th>Latency</th><th>TLS Issuer</th><th>Notes</th></tr></thead><tbody>")
foreach ($r in $RelayResults) {
$tcpClass = Get-StatusClass -Pass $r.TcpSuccess
$latency = if ($null -ne $r.LatencyMs) { "$($r.LatencyMs) ms" } else { 'n/a' }
$issuerHtml = ConvertTo-HtmlEncoded $r.CertIssuer
$issuerClass = if ($r.SuspiciousIssuer) { 'warn' } else { '' }
$notes = @()
if ($r.SuspiciousIssuer -and $r.CertIssuer) { $notes += 'Cert issuer not on common CA hint list. Possible MITM proxy.' }
if ($r.Error) { $notes += $r.Error }
$notesHtml = ConvertTo-HtmlEncoded ($notes -join ' ')
[void]$sb.AppendLine("<tr>")
[void]$sb.AppendLine("<td class='mono'>$(ConvertTo-HtmlEncoded $r.Target)</td>")
[void]$sb.AppendLine("<td class='$tcpClass'>$(if ($r.TcpSuccess) { 'PASS' } else { 'FAIL' })</td>")
[void]$sb.AppendLine("<td>$latency</td>")
[void]$sb.AppendLine("<td class='mono $issuerClass'>$issuerHtml</td>")
[void]$sb.AppendLine("<td>$notesHtml</td>")
[void]$sb.AppendLine("</tr>")
}
[void]$sb.AppendLine("</tbody></table>")
}
[void]$sb.AppendLine("<h2>WinHTTP Proxy Configuration</h2>")
[void]$sb.AppendLine("<pre>$(ConvertTo-HtmlEncoded $Env.WinHttpProxy)</pre>")
[void]$sb.AppendLine("<div class='footer'>Generated by Test-TruGridBrokerReachability.ps1 v$(ConvertTo-HtmlEncoded $Env.ScriptVersion).</div>")
[void]$sb.AppendLine("</body></html>")
Set-Content -Path $OutputPath -Value $sb.ToString() -Encoding UTF8
}
# --- main ---
Write-Host "TruGrid Broker Reachability Diagnostic" -ForegroundColor Cyan
Write-Host ("=" * 60) -ForegroundColor DarkGray
$envInfo = Get-EnvironmentFingerprint
Write-Host "`n[1/4] Locating broker log..." -ForegroundColor Yellow
$resolvedLog = Find-BrokerLog
if (-not $resolvedLog) {
Write-Warning "No broker log found. Expected one of:"
$Script:KnownLogPaths | ForEach-Object { Write-Warning " $_" }
Write-Warning "Confirm the broker is installed, or edit `$Script:KnownLogPaths at the top of this script."
$resolvedLog = '<not found>'
}
else {
Write-Host " Using: $resolvedLog" -ForegroundColor Green
}
Write-Host "`n[2/4] Extracting last $($Script:LastNRelays) unique relay IP(s) from log..." -ForegroundColor Yellow
$relayIps = @()
if ($resolvedLog -ne '<not found>') {
$relayIps = @(Get-RelayIPsFromBrokerLog -Path $resolvedLog -Last $Script:LastNRelays)
}
if ($relayIps.Count -eq 0) {
Write-Warning " No relay IPs found in log. Will probe static endpoints only."
}
else {
Write-Host " Found $($relayIps.Count) relay IP(s): $($relayIps -join ', ')" -ForegroundColor Green
}
Write-Host "`n[3/4] Probing $($Script:StaticEndpoints.Count) static endpoint(s) and $($relayIps.Count) relay IP(s)..." -ForegroundColor Yellow
$staticResults = @()
foreach ($ep in $Script:StaticEndpoints) {
Write-Host " -> $ep" -NoNewline
$r = Test-TlsEndpoint -Target $ep
$staticResults += $r
if ($r.TcpSuccess -and $r.TlsSuccess -and $r.ChainValid -and -not $r.SuspiciousIssuer) {
Write-Host " PASS" -ForegroundColor Green
}
elseif ($r.SuspiciousIssuer) {
Write-Host " WARN (issuer: $($r.CertIssuer))" -ForegroundColor Yellow
}
else {
Write-Host " FAIL ($($r.Error))" -ForegroundColor Red
}
}
$relayResults = @()
foreach ($ip in $relayIps) {
Write-Host " -> $ip" -NoNewline
$r = Test-TlsEndpoint -Target $ip -ServerName $ip
$relayResults += $r
if ($r.TcpSuccess) {
if ($r.SuspiciousIssuer) {
Write-Host " WARN (issuer: $($r.CertIssuer))" -ForegroundColor Yellow
}
else {
Write-Host " PASS ($($r.LatencyMs) ms)" -ForegroundColor Green
}
}
else {
Write-Host " FAIL ($($r.Error))" -ForegroundColor Red
}
}
Write-Host "`n[4/4] Writing report..." -ForegroundColor Yellow
if (-not (Test-Path $Script:OutputDir)) {
New-Item -Path $Script:OutputDir -ItemType Directory -Force | Out-Null
}
$stamp = (Get-Date).ToString('yyyyMMdd_HHmmss')
$htmlPath = Join-Path $Script:OutputDir "TruGridBrokerReachability_$($envInfo.Hostname)_$stamp.html"
Write-HtmlReport -Env $envInfo -LogFile $resolvedLog -RelayIps $relayIps `
-StaticResults $staticResults -RelayResults $relayResults -OutputPath $htmlPath
Write-Host " HTML report: $htmlPath" -ForegroundColor Green
Write-Host "`nDone." -ForegroundColor Cyan
Example output:

Reading the report
The HTML opens in any browser, no internet required.
The banner at the top is green, amber, or red based on the count of failed probes. Green means everything passed, amber means partial reachability, red means significant connectivity problems.
The environment block records hostname, user, OS, PowerShell, .NET, DNS servers, and the log file or directory the data came from. Half the time this section answers the question on its own ("oh, they are pointing at a DNS server that is no longer resolving correctly").
Static endpoints is the front-end probe table. Every row should be PASS / PASS / VALID. If TLS fails or chain is INVALID or the issuer is amber, you are looking at a MITM proxy, an outdated trust store, or a broken DNS path. If TCP fails, the firewall is blocking outbound 443 to the front end and nothing TruGrid will work from this box.
Dynamic relay endpoints is the table of relay IPs pulled from the log. If TCP fails on a relay but statics pass, the firewall is filtering by destination IP rather than by hostname or port.
Loopback (Connector script only) is a single-row table. PASS means winsock is healthy. FAIL means an LSP is in the way and the Connector's local forwarding will break even when external reachability is fine.
Known issue detection (Connector script only) lists any matched signatures with a count and a one-line note. An entry here is not a failure. It is information that lets you stop chasing the wrong rabbit.
WinHTTP proxy dumps netsh winhttp show proxy so you can see whether a system-level proxy is configured.
Updated on: 28/04/2026
Thank you!
