Public/Test-GSAVpnSplitDnsInteroperability.ps1

#
# Function: Test-GSAVpnSplitDnsInteroperability
# Detects a known interoperability scenario where Cisco VPN + GSA NRPT/private DNS can
# produce REFUSED DNS answers for a private suffix unless Cisco allows source 6.6.0.0/16.
#
function Test-GSAVpnSplitDnsInteroperability {
    <#
.SYNOPSIS
    Detects the Cisco VPN + GSA private DNS split-DNS REFUSED scenario for a target DNS suffix.
.DESCRIPTION
    Checks for an active Cisco VPN adapter, verifies there is an NRPT rule that applies to the
    specified DNS suffix, and performs a DNS lookup for a probe hostname under that suffix.
    If the lookup fails with REFUSED while Cisco VPN is active, this matches a known scenario
    where Cisco split-DNS/ACL drops requests sourced from the GSA synthetic 6.6.0.0/16 range.
    Recommended fix in that case: add 6.6.0.0/16 to the Cisco split-DNS extended ACL source list.
    This is a local/offline client-side check and does not call Microsoft Graph.
.PARAMETER DnsSuffix
    Target DNS suffix/domain to test (for example: mydomain.com).
.PARAMETER ProbeHostLabel
    Hostname label used for the probe query under DnsSuffix. Defaults to _gsa_vpn_interop_probe.
    Using a dedicated probe label avoids relying on a specific production host record.
.PARAMETER CiscoAdapterPattern
    Regex pattern used to identify Cisco VPN adapters by Name/InterfaceDescription.
    Default matches Cisco AnyConnect and Cisco Secure Client.
.PARAMETER PassThru
    Stores the pass/fail result for orchestration without writing boolean output to the pipeline.
    By default nothing is returned - only logged.
.EXAMPLE
    Test-GSAVpnSplitDnsInteroperability -DnsSuffix mydomain.com
.EXAMPLE
    Test-GSAVpnSplitDnsInteroperability -DnsSuffix mydomain.com -PassThru
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$DnsSuffix,
        [string]$ProbeHostLabel = '_gsa_vpn_interop_probe',
        [string]$CiscoAdapterPattern = 'Cisco AnyConnect|Cisco Secure Client',
        [switch]$PassThru,
        [ValidateSet("ERROR", "WARNING", "INFO", "DEBUG", "SUCCESS")]
        [string]$LogLevel
    )

    if ($PSBoundParameters.ContainsKey('LogLevel'))
    { $script:LogLevel = $LogLevel }

    $normalizedSuffix = $DnsSuffix.Trim().TrimStart('.')
    $probeFqdn = "$ProbeHostLabel.$normalizedSuffix"
    $passed = $true

    Write-Log "Checking Cisco VPN + GSA split-DNS interoperability for suffix '$normalizedSuffix'..." "INFO"

    # Check for active Cisco VPN adapters.
    $ciscoAdapters = @(
        Get-NetAdapter -ErrorAction SilentlyContinue |
        Where-Object {
            $_.Status -eq 'Up' -and (
                $_.Name -match $CiscoAdapterPattern -or
                $_.InterfaceDescription -match $CiscoAdapterPattern
            )
        }
    )

    # If no active Cisco VPN adapters are found, the scenario is not applicable.
    if ($ciscoAdapters.Count -eq 0) {
        Write-Log "No active Cisco VPN adapter detected. Scenario is not currently applicable." "SUCCESS"
        if ($PassThru)
        { $script:_testResult = $true }
        return
    }

    Write-Log "Detected active Cisco VPN adapter(s): $($ciscoAdapters.Name -join ', ')." "INFO"

    # Retrieve the NRPT rules to check for split-DNS configuration.
    $nrptRules = @()
    try {
        $nrptRules = @(Get-DnsClientNrptRule -ErrorAction Stop)
    }
    catch {
        Write-Log "Could not read NRPT rules: $($_.Exception.Message)." "WARNING"
        Write-Log "This check is inconclusive without NRPT visibility." "WARNING"
        if ($PassThru)
        { $script:_testResult = $true }
        return
    }

    $matchingNrptRules = @()
    foreach ($rule in $nrptRules) {
        foreach ($namespace in @($rule.Namespace | Where-Object { $_ })) {
            $normalizedNamespace = $namespace.Trim().TrimStart('*').TrimStart('.')
            if ([string]::IsNullOrWhiteSpace($normalizedNamespace))
            { continue }

            if (
                $normalizedSuffix -ieq $normalizedNamespace -or
                $normalizedSuffix -ilike "*.$normalizedNamespace"
            ) {
                $matchingNrptRules += $rule
                break
            }
        }
    }

    # If no NRPT rules match the DNS suffix, the scenario is not detected.
    if ($matchingNrptRules.Count -eq 0) {
        Write-Log "No NRPT rules apply to '$normalizedSuffix'. This specific scenario was not detected." "INFO"
        if ($PassThru)
        { $script:_testResult = $true }
        return
    }

    # At this point, there are matching NRPT rules for the DNS suffix.
    Write-Log "Found $($matchingNrptRules.Count) NRPT rule(s) matching '$normalizedSuffix'." "INFO"
    Write-Log "Testing DNS query for probe host '$probeFqdn'..." "INFO"

    try {
        [void](Resolve-DnsName -Name $probeFqdn -ErrorAction Stop)
        Write-Log "Probe lookup succeeded. The Cisco/GSA split-DNS REFUSED scenario was not reproduced." "SUCCESS"
    }
    catch {
        $errorMessage = $_.Exception.Message

        if ($errorMessage -match 'REFUSED|query refused|DNS_ERROR_RCODE_REFUSED') {
            Write-Log "DNS lookup for '$probeFqdn' failed with REFUSED while Cisco VPN and matching NRPT rule(s) are active." "ERROR"
            Write-Log "Likely scenario detected: Cisco split-DNS/ACL is dropping DNS requests sourced from GSA synthetic range 6.6.0.0/16." "WARNING"
            Write-Log "Recommended action: In Cisco VPN split-DNS + extended ACL, add source network 6.6.0.0/16 so these DNS queries are allowed." "WARNING"
            $passed = $false
        }
        elseif ($errorMessage -match 'DNS name does not exist|NXDOMAIN|Non-existent domain') {
            # NXDOMAIN still indicates the DNS path is functioning; this probe name may simply not exist.
            Write-Log "Probe lookup returned NXDOMAIN (expected for a synthetic probe label). DNS path is reachable; scenario not reproduced." "SUCCESS"
        }
        else {
            Write-Log "Probe lookup for '$probeFqdn' failed with a non-REFUSED error: $errorMessage" "WARNING"
            Write-Log "Result is inconclusive for the Cisco/GSA split-DNS REFUSED scenario." "WARNING"
        }
    }

    if ($PassThru)
    { $script:_testResult = $passed }
}