Functions/Test-ADDNSHealth.ps1
|
<# .SYNOPSIS Validates DNS health for Active Directory-integrated zones and domain controller records. .DESCRIPTION This function checks that critical AD-related DNS records (SRV records for domain controllers, A records, and AD-integrated zone replication) are present and consistent across domain controllers. Helps catch DNS misconfigurations that can cause authentication, replication, or Group Policy failures. .PARAMETER OutputPath Optional path to export an HTML report. If omitted, results are only shown in console. .EXAMPLE Test-ADDNSHealth .EXAMPLE Test-ADDNSHealth -OutputPath "C:\Reports\ADDNSHealth.html" .NOTES Requires: ActiveDirectory and DnsServer PowerShell modules Requires: Read access to DNS zones and AD replication metadata Recommended: Run from or against a domain controller with DNS Server role installed #> function Test-ADDNSHealth { [CmdletBinding()] param( [Parameter(Mandatory = $false)] [string]$OutputPath ) foreach ($module in @('ActiveDirectory', 'DnsServer')) { if (-not (Get-Module -ListAvailable -Name $module)) { Write-Error "$module module not found. Install RSAT-AD-PowerShell / RSAT-DNS-Server and try again." return } Import-Module $module -ErrorAction Stop } $domain = Get-ADDomain $domainDNSName = $domain.DNSRoot $domainControllers = Get-ADDomainController -Filter * if (-not $domainControllers) { Write-Error "No domain controllers found. Check connectivity and permissions." return } $results = @() # --- Check 1: SRV records for domain controller location --- Write-Verbose "Checking critical SRV records..." $srvRecordsToCheck = @( "_ldap._tcp.dc._msdcs.$domainDNSName", "_kerberos._tcp.dc._msdcs.$domainDNSName", "_ldap._tcp.$domainDNSName", "_gc._tcp.$domainDNSName" ) foreach ($srv in $srvRecordsToCheck) { try { $srvResult = Resolve-DnsName -Name $srv -Type SRV -ErrorAction Stop $count = ($srvResult | Where-Object { $_.Type -eq 'SRV' }).Count $results += [PSCustomObject]@{ CheckType = "SRV Record" Target = $srv Status = if ($count -gt 0) { "OK" } else { "MISSING" } Detail = "$count SRV record(s) found" } } catch { $results += [PSCustomObject]@{ CheckType = "SRV Record" Target = $srv Status = "MISSING" Detail = "Resolution failed: $($_.Exception.Message)" } } } # --- Check 2: A record for each domain controller --- Write-Verbose "Checking A records for each domain controller..." foreach ($dc in $domainControllers) { try { $aResult = Resolve-DnsName -Name $dc.HostName -Type A -ErrorAction Stop $resolvedIP = ($aResult | Where-Object { $_.Type -eq 'A' } | Select-Object -First 1).IPAddress $status = "OK" $detail = "Resolves to $resolvedIP" if ($dc.IPv4Address -and $resolvedIP -ne $dc.IPv4Address) { $status = "MISMATCH" $detail = "DNS resolves to $resolvedIP, but AD reports IP as $($dc.IPv4Address)" } $results += [PSCustomObject]@{ CheckType = "A Record" Target = $dc.HostName Status = $status Detail = $detail } } catch { $results += [PSCustomObject]@{ CheckType = "A Record" Target = $dc.HostName Status = "MISSING" Detail = "Could not resolve: $($_.Exception.Message)" } } } # --- Check 3: Forward/reverse lookup consistency (PTR check) --- Write-Verbose "Checking PTR record consistency..." foreach ($dc in $domainControllers) { if (-not $dc.IPv4Address) { continue } try { $ptrResult = Resolve-DnsName -Name $dc.IPv4Address -Type PTR -ErrorAction Stop $ptrHost = ($ptrResult | Where-Object { $_.Type -eq 'PTR' } | Select-Object -First 1).NameHost $status = "OK" $detail = "PTR resolves to $ptrHost" if ($ptrHost -notlike "$($dc.HostName)*") { $status = "MISMATCH" $detail = "PTR resolves to '$ptrHost', expected '$($dc.HostName)'" } $results += [PSCustomObject]@{ CheckType = "PTR Record" Target = $dc.IPv4Address Status = $status Detail = $detail } } catch { $results += [PSCustomObject]@{ CheckType = "PTR Record" Target = $dc.IPv4Address Status = "MISSING" Detail = "No PTR record found: $($_.Exception.Message)" } } } # --- Check 4: AD-integrated zone replication scope consistency --- Write-Verbose "Checking AD-integrated DNS zone configuration on each DC..." foreach ($dc in $domainControllers) { try { $zone = Get-DnsServerZone -ComputerName $dc.HostName -Name $domainDNSName -ErrorAction Stop $status = if ($zone.ZoneType -eq 'Primary' -and $zone.IsDsIntegrated) { "OK" } else { "REVIEW" } $detail = "ZoneType: $($zone.ZoneType), DS-Integrated: $($zone.IsDsIntegrated), ReplicationScope: $($zone.ReplicationScope)" $results += [PSCustomObject]@{ CheckType = "DNS Zone Config" Target = $dc.HostName Status = $status Detail = $detail } } catch { $results += [PSCustomObject]@{ CheckType = "DNS Zone Config" Target = $dc.HostName Status = "ERROR" Detail = "Could not query DNS zone: $($_.Exception.Message)" } } } # --- Console summary --- $issues = $results | Where-Object { $_.Status -in @("MISSING", "MISMATCH", "ERROR", "REVIEW") } Write-Host "`n=== AD DNS Health Summary ===" -ForegroundColor Cyan Write-Host "Domain: $domainDNSName" Write-Host "Domain Controllers: $($domainControllers.Count)" Write-Host "Checks performed: $($results.Count)" Write-Host "Issues found: $($issues.Count)`n" if ($issues.Count -gt 0) { Write-Host "Issues detected:" -ForegroundColor Yellow $issues | Format-Table CheckType, Target, Status, Detail -AutoSize } else { Write-Host "No DNS issues detected." -ForegroundColor Green } # --- Optional HTML export --- if ($OutputPath) { $htmlHeader = @" <style> body { font-family: Segoe UI, Arial, sans-serif; } table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid #ccc; padding: 6px 10px; text-align: left; } th { background-color: #2c3e50; color: white; } tr.OK { background-color: #eafaf1; } tr.MISSING, tr.ERROR { background-color: #fdecea; } tr.MISMATCH, tr.REVIEW { background-color: #fff8e1; } </style> "@ $htmlBody = $results | ConvertTo-Html -Head $htmlHeader -Title "AD DNS Health Report - $(Get-Date)" -PreContent "<h2>AD DNS Health Report</h2><p>Domain: $domainDNSName</p><p>Generated: $(Get-Date)</p>" $htmlBody | Out-File -FilePath $OutputPath -Encoding UTF8 Write-Host "`nHTML report saved to: $OutputPath" -ForegroundColor Cyan } return $results } |