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 'srv-app01.corp.local' Resolves the name using the system default DNS server (positional). .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.1.0 Last Modified: 2026-03-22 Requires: PowerShell 5.1+ / Windows only Requires: DnsClient module (built-in on Windows 8+/Server 2012+) Permissions: No admin required .LINK https://github.com/k9fr4n/PSWinOps .LINK https://learn.microsoft.com/en-us/powershell/module/dnsclient/resolve-dnsname #> [CmdletBinding()] [OutputType('PSWinOps.DnsResolution')] param ( [Parameter(Mandatory = $true, Position = 0, 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 (Type=$Type)" # Collect all results to compute Consistent flag at the end $allResults = [System.Collections.Generic.List[PSObject]]::new() # Determine whether we query specific servers or system default $useDefaultDns = -not $PSBoundParameters.ContainsKey('DnsServer') } process { foreach ($dnsName in $Name) { # Build the list of servers to iterate over # Avoid @($null) sentinel — use explicit branching to prevent # foreach-over-null silently skipping on some PS versions if ($useDefaultDns) { $serverList = @([string]::Empty) } else { $serverList = $DnsServer } for ($i = 0; $i -lt $serverList.Count; $i++) { $server = $serverList[$i] $isDefault = [string]::IsNullOrEmpty($server) $serverLabel = if ($isDefault) { '(Default)' } else { $server } Write-Verbose "[$($MyInvocation.MyCommand)] Resolving '$dnsName' (Type=$Type) via $serverLabel" try { $resolveParams = @{ Name = $dnsName Type = $Type DnsOnly = $true ErrorAction = 'Stop' } if (-not $isDefault) { $resolveParams['Server'] = $server } $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() $dnsResult = Resolve-DnsName @resolveParams $stopwatch.Stop() $elapsedMs = [math]::Round($stopwatch.Elapsed.TotalMilliseconds, 1) # Extract values matching the requested record type $records = @($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 ($_.NameAdministrator) { $_.NameAdministrator } elseif ($_.NameTarget) { $_.NameTarget } elseif ($_.Strings) { $_.Strings -join '; ' } else { $_.ToString() } }) $hasRecords = $records.Count -gt 0 $resultObj = [PSCustomObject]@{ PSTypeName = 'PSWinOps.DnsResolution' Name = $dnsName DnsServer = $serverLabel QueryType = $Type Result = if ($hasRecords) { $records -join ', ' } else { $null } QueryTimeMs = $elapsedMs Success = $hasRecords Consistent = $null # Computed in end {} ErrorMessage = if (-not $hasRecords) { "No $Type records found in DNS response" } else { $null } Timestamp = Get-Date -Format 'o' } Write-Verbose "[$($MyInvocation.MyCommand)] '$dnsName' via $serverLabel — Success=$hasRecords, Records=$($records.Count), ${elapsedMs}ms" $allResults.Add($resultObj) } catch { Write-Verbose "[$($MyInvocation.MyCommand)] '$dnsName' via $serverLabel — FAILED: $_" $resultObj = [PSCustomObject]@{ PSTypeName = 'PSWinOps.DnsResolution' Name = $dnsName DnsServer = $serverLabel 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 (only meaningful with multiple servers) $multiServer = -not $useDefaultDns -and $DnsServer.Count -gt 1 $grouped = $allResults | Group-Object -Property Name foreach ($group in $grouped) { if (-not $multiServer) { # Single server or default: consistency is not applicable foreach ($r in $group.Group) { $r.Consistent = $null } } else { $successResults = @($group.Group | Where-Object { $_.Success }) if ($successResults.Count -le 1) { # Zero or one succeeded: cannot determine consistency foreach ($r in $group.Group) { $r.Consistent = $null } } 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 — $($allResults.Count) result(s)" } } |