Public/network/Test-DNSResolution.ps1

#Requires -Version 5.1

function Test-DNSResolution {
    <#
    .SYNOPSIS
        Tests DNS name resolution across one or more DNS servers.
    .DESCRIPTION
        Resolves a hostname against multiple DNS servers and compares the results.
        Useful for diagnosing DNS propagation issues, split-horizon DNS, or
        identifying inconsistent resolution between internal and external DNS.
 
        Uses Resolve-DnsName cmdlet under the hood.
    .PARAMETER Name
        One or more DNS names to resolve. Accepts pipeline input.
    .PARAMETER DnsServer
        One or more DNS server IP addresses or hostnames to query.
        If not specified, uses the system default DNS resolver.
    .PARAMETER Type
        DNS record type to query. Default: A.
        Valid values: A, AAAA, CNAME, MX, NS, PTR, SOA, SRV, TXT.
    .EXAMPLE
        Test-DNSResolution -Name 'srv-app01.corp.local'
 
        Resolves the name using the system default DNS server.
    .EXAMPLE
        Test-DNSResolution -Name 'google.com' -DnsServer '8.8.8.8', '1.1.1.1'
 
        Compares resolution of google.com across Google DNS and Cloudflare.
    .EXAMPLE
        Test-DNSResolution -Name 'srv01.corp.local' -DnsServer '10.0.0.1', '10.0.0.2', '8.8.8.8'
 
        Checks if internal name resolves consistently across internal and external DNS.
    .EXAMPLE
        'srv01', 'srv02', 'srv03' | Test-DNSResolution -DnsServer '10.0.0.1'
 
        Resolves multiple names via pipeline against a specific DNS server.
    .OUTPUTS
    PSWinOps.DnsResolution
    .NOTES
        Author: Franck SALLET
        Version: 1.0.0
        Last Modified: 2026-03-21
        Requires: PowerShell 5.1+ / Windows only
        Requires: DnsClient module (built-in on Windows 8+/Server 2012+)
        Permissions: No admin required
    .LINK
    https://docs.microsoft.com/en-us/powershell/module/dnsclient/resolve-dnsname
    #>

    [CmdletBinding()]
    [OutputType('PSWinOps.DnsResolution')]
    param (
        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string[]]$Name,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string[]]$DnsServer,

        [Parameter(Mandatory = $false)]
        [ValidateSet('A', 'AAAA', 'CNAME', 'MX', 'NS', 'PTR', 'SOA', 'SRV', 'TXT')]
        [string]$Type = 'A'
    )

    begin {
        Write-Verbose "[$($MyInvocation.MyCommand)] Starting DNS resolution tests"
        # Collect all results to compute Consistent flag at the end
        $allResults = [System.Collections.Generic.List[PSObject]]::new()
        $serversToQuery = if ($DnsServer) { $DnsServer } else { @($null) }
    }

    process {
        foreach ($dnsName in $Name) {
            foreach ($server in $serversToQuery) {
                try {
                    $resolveParams = @{
                        Name        = $dnsName
                        Type        = $Type
                        DnsOnly     = $true
                        ErrorAction = 'Stop'
                    }
                    if ($server) {
                        $resolveParams['Server'] = $server
                    }

                    $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
                    $dnsResult = Resolve-DnsName @resolveParams
                    $stopwatch.Stop()

                    # Extract IP addresses from the result
                    $ipAddresses = @($dnsResult | Where-Object {
                        $_.QueryType -eq $Type -or
                        ($Type -eq 'A' -and $_.Type -eq 1) -or
                        ($Type -eq 'AAAA' -and $_.Type -eq 28)
                    } | ForEach-Object {
                        if ($_.IPAddress) { $_.IPAddress }
                        elseif ($_.NameHost) { $_.NameHost }
                        elseif ($_.NameExchange) { $_.NameExchange }
                        elseif ($_.Strings) { $_.Strings -join '; ' }
                        else { $_.ToString() }
                    })

                    $resultObj = [PSCustomObject]@{
                        PSTypeName   = 'PSWinOps.DnsResolution'
                        Name         = $dnsName
                        DnsServer    = if ($server) { $server } else { '(Default)' }
                        QueryType    = $Type
                        Result       = ($ipAddresses -join ', ')
                        QueryTimeMs  = [math]::Round($stopwatch.Elapsed.TotalMilliseconds, 1)
                        Success      = $true
                        Consistent   = $null  # Computed in end {}
                        ErrorMessage = $null
                        Timestamp    = Get-Date -Format 'o'
                    }
                    $allResults.Add($resultObj)
                } catch {
                    $resultObj = [PSCustomObject]@{
                        PSTypeName   = 'PSWinOps.DnsResolution'
                        Name         = $dnsName
                        DnsServer    = if ($server) { $server } else { '(Default)' }
                        QueryType    = $Type
                        Result       = $null
                        QueryTimeMs  = $null
                        Success      = $false
                        Consistent   = $null
                        ErrorMessage = $_.Exception.Message
                        Timestamp    = Get-Date -Format 'o'
                    }
                    $allResults.Add($resultObj)
                }
            }
        }
    }

    end {
        # Compute consistency per DNS name: all successful results must match
        $grouped = $allResults | Group-Object -Property Name
        foreach ($group in $grouped) {
            $successResults = @($group.Group | Where-Object { $_.Success })
            if ($successResults.Count -le 1) {
                # Single server or all failed: consistent by definition
                foreach ($r in $group.Group) { $r.Consistent = $true }
            } else {
                $uniqueResults = @($successResults.Result | Sort-Object -Unique)
                $isConsistent = $uniqueResults.Count -eq 1
                foreach ($r in $group.Group) { $r.Consistent = $isConsistent }
            }
        }

        # Output all collected results
        foreach ($result in $allResults) {
            $result
        }

        Write-Verbose "[$($MyInvocation.MyCommand)] Completed DNS resolution tests"
    }
}