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 device 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. If you need to tweak something, the endpoint IP's are constants at the top of each script.
When to run which one
Secure Connect / Sentry Reachability script 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.
Windows Connector Reachability script 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. 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.
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 common errors. 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: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-RelayReachability {
# TruGrid relays do not answer raw TCP 443 from outside their own protocol,
# so we probe via ICMP plus an ip-api.com geo lookup to expose what region
# the relay sits in. No customer data is sent, only the relay IP.
param([Parameter(Mandatory)] [string]$Target)
$result = [pscustomobject]@{
Target = $Target
PingSucceeded = $false
RttMs = $null
City = $null
Country = $null
AzureRegion = $null
Org = $null
Error = $null
}
try {
$ping = [System.Net.NetworkInformation.Ping]::new()
$reply = $ping.Send($Target, 2000)
if ($reply.Status -eq 'Success') {
$result.PingSucceeded = $true
$result.RttMs = $reply.RoundtripTime
}
else {
$result.Error = "ICMP status: $($reply.Status)"
}
$ping.Dispose()
}
catch {
$result.Error = $_.Exception.Message
}
try {
$uri = "http://ip-api.com/json/$Target" + "?fields=status,country,city,org"
$geo = Invoke-RestMethod -Uri $uri -TimeoutSec 5 -ErrorAction Stop
if ($geo.status -eq 'success') {
$result.City = $geo.city
$result.Country = $geo.country
$result.Org = $geo.org
if ($geo.org -match '\(([^)]+)\)') {
$result.AzureRegion = $matches[1]
}
}
}
catch {
# geo lookup failed, leave fields null
}
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
)
$bannerClass = 'banner-pass'
$verdict = 'Probes complete. Review the sections below for details.'
$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>")
[void]$sb.AppendLine("<p class='meta'>TruGrid relays do not respond to raw TCP 443 probes by design. Reachability is verified via ICMP, which confirms the relay host is alive on the network path the Connector uses. Geographic location is resolved via a public IP geolocation lookup (ip-api.com), which also exposes the Azure region the relay is hosted in. Latency and region together indicate whether your relay assignment is geographically sensible.</p>")
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>ICMP</th><th>RTT</th><th>Location</th><th>Azure Region</th><th>Provider</th><th>Notes</th></tr></thead><tbody>")
foreach ($r in $RelayResults) {
$pingClass = Get-StatusClass -Pass $r.PingSucceeded
$rtt = if ($null -ne $r.RttMs) { "$($r.RttMs) ms" } else { 'n/a' }
$loc = if ($r.City -and $r.Country) { "$($r.City), $($r.Country)" } elseif ($r.Country) { $r.Country } else { 'n/a' }
$region = if ($r.AzureRegion) { $r.AzureRegion } else { 'n/a' }
$org = if ($r.Org) { $r.Org } else { 'n/a' }
$notes = @()
if (-not $r.PingSucceeded) { $notes += 'Relay did not respond to ICMP. The relay may be deallocated, or the network path drops ICMP.' }
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='$pingClass'>$(if ($r.PingSucceeded) { 'PASS' } else { 'FAIL' })</td>")
[void]$sb.AppendLine("<td>$rtt</td>")
[void]$sb.AppendLine("<td>$(ConvertTo-HtmlEncoded $loc)</td>")
[void]$sb.AppendLine("<td class='mono'>$(ConvertTo-HtmlEncoded $region)</td>")
[void]$sb.AppendLine("<td>$(ConvertTo-HtmlEncoded $org)</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. Geolocation data from ip-api.com. Only the relay IP is sent in lookups, no customer data.</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-Host " Note: Connector log directory not found at $($Script:LogDir)." -ForegroundColor Yellow
Write-Host " Either the Connector has not been run from this user profile, or logs are kept elsewhere." -ForegroundColor DarkYellow
Write-Host " Edit `$Script:LogDir at the top of this script if your install is non-standard." -ForegroundColor DarkYellow
}
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-Host " Note: no files matching $($Script:LogFilePattern) found in $($Script:LogDir)." -ForegroundColor Yellow
Write-Host " This usually means the Connector has not been run from this user profile yet, or logs are kept elsewhere." -ForegroundColor DarkYellow
Write-Host " Static endpoints and loopback will still be probed. Report will be generated." -ForegroundColor DarkYellow
}
else {
Write-Host " Found $($logFiles.Count) log file(s)." -ForegroundColor Green
}
Write-Host "`n[3/5] Extracting latest relay IP from logs..." -ForegroundColor Yellow
$relayIps = @()
$relaySourceFiles = @()
if ($logFiles.Count -gt 0) {
$parseResult = Get-RelayIPsFromConnectorLogs -Files $logFiles -Last 1
$relayIps = @($parseResult.RelayIps)
$relaySourceFiles = @($parseResult.SourceFiles)
}
if ($relayIps.Count -eq 0) {
Write-Host " No relay IPs available from logs. Probing static endpoints only." -ForegroundColor DarkYellow
}
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-RelayReachability -Target $ip
$relayResults += $r
if ($r.PingSucceeded) {
$loc = if ($r.City) { ", $($r.City)" } else { '' }
$region = if ($r.AzureRegion) { ", $($r.AzureRegion)" } else { '' }
Write-Host " PASS ($($r.RttMs) ms$loc$region)" -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: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 = '(?:Attempting to connect to TruGrid Relay via TCP|TruGrid Relay: Connect(?:ing to|ion to) Instance):?\s?(\d{1,3}(?:\.\d{1,3}){3}):443'
$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-RelayReachability {
# TruGrid relays do not answer raw TCP 443 from outside their own protocol,
# so we probe via ICMP plus an ip-api.com geo lookup to expose what region
# the relay sits in. No customer data is sent, only the relay IP.
param([Parameter(Mandatory)] [string]$Target)
$result = [pscustomobject]@{
Target = $Target
PingSucceeded = $false
RttMs = $null
City = $null
Country = $null
AzureRegion = $null
Org = $null
Error = $null
}
try {
$ping = [System.Net.NetworkInformation.Ping]::new()
$reply = $ping.Send($Target, 2000)
if ($reply.Status -eq 'Success') {
$result.PingSucceeded = $true
$result.RttMs = $reply.RoundtripTime
}
else {
$result.Error = "ICMP status: $($reply.Status)"
}
$ping.Dispose()
}
catch {
$result.Error = $_.Exception.Message
}
try {
$uri = "http://ip-api.com/json/$Target" + "?fields=status,country,city,org"
$geo = Invoke-RestMethod -Uri $uri -TimeoutSec 5 -ErrorAction Stop
if ($geo.status -eq 'success') {
$result.City = $geo.city
$result.Country = $geo.country
$result.Org = $geo.org
if ($geo.org -match '\(([^)]+)\)') {
$result.AzureRegion = $matches[1]
}
}
}
catch {
# geo lookup failed, leave fields null
}
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
)
$bannerClass = 'banner-pass'
$verdict = 'Probes complete. Review the sections below for details.'
$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>")
[void]$sb.AppendLine("<p class='meta'>TruGrid relays do not respond to raw TCP 443 probes by design. Reachability is verified via ICMP, which confirms the relay host is alive on the network path the broker uses. Geographic location is resolved via a public IP geolocation lookup (ip-api.com), which also exposes the Azure region the relay is hosted in. Latency and region together indicate whether your relay assignment is geographically sensible.</p>")
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 relay IP from $(ConvertTo-HtmlEncoded $LogFile)</p>")
[void]$sb.AppendLine("<table><thead><tr><th>Relay IP</th><th>ICMP</th><th>RTT</th><th>Location</th><th>Azure Region</th><th>Provider</th><th>Notes</th></tr></thead><tbody>")
foreach ($r in $RelayResults) {
$pingClass = Get-StatusClass -Pass $r.PingSucceeded
$rtt = if ($null -ne $r.RttMs) { "$($r.RttMs) ms" } else { 'n/a' }
$loc = if ($r.City -and $r.Country) { "$($r.City), $($r.Country)" } elseif ($r.Country) { $r.Country } else { 'n/a' }
$region = if ($r.AzureRegion) { $r.AzureRegion } else { 'n/a' }
$org = if ($r.Org) { $r.Org } else { 'n/a' }
$notes = @()
if (-not $r.PingSucceeded) { $notes += 'Relay did not respond to ICMP. The relay may be deallocated, or the network path drops ICMP.' }
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='$pingClass'>$(if ($r.PingSucceeded) { 'PASS' } else { 'FAIL' })</td>")
[void]$sb.AppendLine("<td>$rtt</td>")
[void]$sb.AppendLine("<td>$(ConvertTo-HtmlEncoded $loc)</td>")
[void]$sb.AppendLine("<td class='mono'>$(ConvertTo-HtmlEncoded $region)</td>")
[void]$sb.AppendLine("<td>$(ConvertTo-HtmlEncoded $org)</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). Geolocation data from ip-api.com. Only the relay IP is sent in lookups, no customer data.</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-Host " Note: no broker log found. Expected one of:" -ForegroundColor Yellow
$Script:KnownLogPaths | ForEach-Object { Write-Host " $_" -ForegroundColor DarkYellow }
Write-Host " Confirm the broker is installed, or edit `$Script:KnownLogPaths at the top of this script." -ForegroundColor DarkYellow
Write-Host " Static endpoints will still be probed. Report will be generated." -ForegroundColor DarkYellow
$resolvedLog = '<not found>'
}
else {
Write-Host " Using: $resolvedLog" -ForegroundColor Green
}
Write-Host "`n[2/4] Extracting latest relay IP from log..." -ForegroundColor Yellow
$relayIps = @()
if ($resolvedLog -ne '<not found>') {
$relayIps = @(Get-RelayIPsFromBrokerLog -Path $resolvedLog -Last 1)
}
if ($relayIps.Count -eq 0) {
Write-Host " No relay IPs available from log. Probing static endpoints only." -ForegroundColor DarkYellow
}
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-RelayReachability -Target $ip
$relayResults += $r
if ($r.PingSucceeded) {
$loc = if ($r.City) { ", $($r.City)" } else { '' }
$region = if ($r.AzureRegion) { ", $($r.AzureRegion)" } else { '' }
Write-Host " PASS ($($r.RttMs) ms$loc$region)" -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: 15/05/2026
Thank you!
