Public/Ics/Get-IcsDnsFailureDiagnostics.ps1

<#
.NOTES
    Do not run this file directly. Dot-sourced by provision.ps1. Called by
    Test-IcsDnsProxyReachable when the proxy stays unreachable after repair.
#>


# ---------------------------------------------------------------------------
# Get-IcsDnsFailureDiagnostics
# Turns a dead ICS DNS proxy into a single next action. When the proxy
# probe fails (and Reset-IcsSharing did not recover it), three very
# different host states produce the SAME symptom, each with a
# different fix:
#
# 1. SharedAccess service not Running - ICS's proxy + NAT are this
# service; if it is stopped/hung nothing answers. Fix: start it.
# 2. Host's own upstream DNS also dead - the proxy has nothing to
# forward to. Fix: the host network (WiFi / no internet), NOT ICS.
# 3. Service Running and host DNS fine, but the proxy still does not
# answer - the proxy itself is wedged. Fix: restart + re-toggle,
# then reboot (ICS state is sticky).
#
# The terminal FAIL used to hand the operator a checklist of these to
# walk by hand; this probes the two distinguishing signals (service
# status + an upstream-side resolve) and returns the one verdict that
# applies, so the FAIL detail names the fix instead of the checklist.
#
# The verdict ships the commands that carry it out, one per line, so
# the operator can paste them rather than translate prose ("re-toggle
# sharing") back into a cmdlet call. Every list ends with the probe
# that decides whether the fix took, which is the step most easily
# skipped when the steps are only described.
#
# Read-only: Get-Service is a status read and Test-HostDnsReachable is
# a resolve. Safe to call on the failure path without changing host
# state further. Both signals degrade gracefully - a missing service
# reads as 'not found', a failed resolve as $false - so gathering
# diagnostics never masks the original proxy FAIL with an error.
# ---------------------------------------------------------------------------

function Get-IcsDnsFailureDiagnostics {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $DnsProbeTarget,

        # Adapter names only shape the Reset-IcsSharing example line, so they
        # stay optional: a caller that does not know them still gets the
        # verdict, with placeholders and a Get-NetAdapter hint in their place.
        [string] $WanAdapterName,

        [string] $LanAdapterName
    )

    # Commands are emitted one per line under the verdict prose. The indent
    # keeps the block visually attached to its finding once the caller embeds
    # it in a multi-finding error, where later lines start at column 0.
    $commandIndent = ' '

    # Read from the same source the probes resolve, so a printed command can
    # never verify a different name than the check measured.
    $probeName = Get-DnsProbeName

    # The verify step for every verdict: the same probe the check itself runs,
    # so "did the fix take" is answered without re-running the whole preflight.
    $probeCommand = "Resolve-DnsName $probeName -Server $DnsProbeTarget -DnsOnly"

    $resetCommand =
        if ($WanAdapterName -and $LanAdapterName) {
            "Reset-IcsSharing -WanInterfaceName '$WanAdapterName' " +
            "-LanInterfaceName '$LanAdapterName'"
        } else {
            "Reset-IcsSharing -WanInterfaceName '<wifi adapter>' " +
            "-LanInterfaceName '<vEthernet (switch)>' # names: Get-NetAdapter"
        }

    $svc       = Get-Service -Name 'SharedAccess' -ErrorAction SilentlyContinue
    $svcStatus = if ($svc) { [string]$svc.Status } else { 'not found' }
    $hostDnsOk = Test-HostDnsReachable

    # Order matters: a stopped service explains everything downstream, so
    # it is reported first; a dead upstream is the next most fundamental;
    # only when both are healthy is the proxy itself the culprit.
    if ($svcStatus -ne 'Running') {
        $verdict = "SharedAccess service is '$svcStatus' (not Running) - ICS's DNS " +
                   "proxy and NAT are that service, so nothing is listening. Start " +
                   "it (elevated), then re-probe:"
        $commands = @(
            'Start-Service SharedAccess',
            $probeCommand
        )
    }
    elseif (-not $hostDnsOk) {
        $verdict = "Host's own upstream DNS cannot resolve $probeName either - " +
                   "the fault is the host network (WiFi DNS / no internet), not ICS. " +
                   "Toggling sharing will not help; restore host connectivity first:"
        $commands = @(
            'Get-NetConnectionProfile # is the WAN link up',
            'Get-DnsClientServerAddress -AddressFamily IPv4 # what the host asks',
            "Resolve-DnsName $probeName -DnsOnly # host-side, no -Server",
            $probeCommand
        )
    }
    else {
        $verdict = "SharedAccess is Running and the host's own DNS resolves, but the " +
                   "proxy at $DnsProbeTarget does not answer - the ICS proxy is wedged. " +
                   "Run elevated, re-probing after each step:"
        $commands = @(
            'Restart-Service SharedAccess',
            $resetCommand,
            $probeCommand,
            'Restart-Computer # last resort - ICS state is sticky'
        )
    }

    $commandBlock = ($commands | ForEach-Object { "$commandIndent$_" }) -join "`n"
    $hostDnsLabel = if ($hostDnsOk) { 'OK' } else { 'FAIL' }

    "SharedAccess=$svcStatus; host upstream DNS=$hostDnsLabel. $verdict`n$commandBlock"
}