Wes Ellis./ a personal notebook
Technology. Stories. Side projects.
A few things worth writing down.
← Back to Script Library

SCRIPT LIBRARY · POWERSHELL

Setting DNS Client Settings on Domain Controllers with PowerShell (and Why Loopback Goes Last)

Point each domain controller at a partner DC first and itself (127.0.0.1) last, the way Microsoft recommends, across all your DCs in one pass.

AT A GLANCESet-DCDnsClientServer.ps1
What it does
Connects to each domain controller over CIM, finds its main network adapter, and sets its IPv4 DNS servers to one or more partner DCs followed by 127.0.0.1. Returns the before and after for every DC, and -WhatIf shows the change first.
Requires
  • Windows PowerShell 5.1 or PowerShell 7 on Windows
  • NetTCPIP and DnsClient modules (built into Windows Server)
  • WinRM reachable on each DC (on by default on Windows Server)
Permissions
Local administrator on the domain controllers, which in practice means a Tier 0 admin account.
Runs on
Windows Server 2016+ domain controllers, run from any Windows admin machine
Tested
Parse-checked and dry-run with mocked CIM cmdlets in PowerShell 7.4

Ask five admins how a domain controller's own DNS settings should look and you'll get about four answers. "Point it at itself." "Point it at the other DC." "127.0.0.1 first, obviously." Here's the version that lines up with Microsoft's guidance and the Best Practices Analyzer: another DC first, loopback last.

Why not itself first? When a DC boots, the DNS Server service needs AD DS to load its AD-integrated zones, and AD DS wants DNS to find its replication partners. A DC that asks only itself can end up waiting on itself, which makes for slow starts and replication that can't find anyone to replicate with. A partner DC in the same site answers right away. Loopback still goes in the list, because if that partner is down the DC should fall back to its own copy of the zone. And use 127.0.0.1 rather than the DC's own IP; loopback keeps working if the address ever changes.

The original script tried to do all this through WMI method calls that didn't exist, and bundled forwarder and root hint changes in too. This version uses Get-DnsClientServerAddress and Set-DnsClientServerAddress over a CIM session, does one job, and refuses setups that defeat the purpose, like listing the DC's own IP as its "partner".

Set-DCDnsClientServer.ps1Download
<#
.SYNOPSIS
    Sets a domain controller's own DNS client settings: partner DC(s) first, loopback last.
.DESCRIPTION
    Connects to each domain controller over CIM, finds the adapter that carries its default
    route (or the one you name), and sets its IPv4 DNS server list to the partner DNS servers
    you supply followed by 127.0.0.1. Returns the before and after for every DC. Supports -WhatIf.
.PARAMETER ComputerName
    The domain controller to configure. Accepts pipeline input by property name, so you can feed it a CSV.
.PARAMETER PartnerDnsServer
    IP address(es) of other DNS-hosting DCs, ideally in the same site. These go first.
.PARAMETER InterfaceAlias
    Adapter to change, e.g. Ethernet0. Defaults to the adapter with the default gateway.
.PARAMETER NoLoopback
    Don't append 127.0.0.1. For DCs that don't run the DNS Server role.
.PARAMETER Credential
    Alternate credentials for the CIM connection.
.EXAMPLE
    .\Set-DCDnsClientServer.ps1 -ComputerName DC01 -PartnerDnsServer 10.0.1.11 -WhatIf
.EXAMPLE
    Import-Csv .\dc-dns.csv | .\Set-DCDnsClientServer.ps1
#>
[CmdletBinding(SupportsShouldProcess)]
param(
    [Parameter(Mandatory, ValueFromPipelineByPropertyName)][string]$ComputerName,
    [Parameter(Mandatory, ValueFromPipelineByPropertyName)][string[]]$PartnerDnsServer,
    [Parameter(ValueFromPipelineByPropertyName)][string]$InterfaceAlias,
    [switch]$NoLoopback,
    [pscredential]$Credential
)

process {
    # CSV input gives "10.0.1.11;10.0.2.11" as one string, so split and validate here.
    $partners = @($PartnerDnsServer -split '[;,\s]+' | Where-Object { $_ })
    foreach ($ip in $partners) {
        $parsed = $null
        if (-not [ipaddress]::TryParse($ip, [ref]$parsed) -or $parsed.AddressFamily -ne 'InterNetwork') {
            Write-Error "${ComputerName}: '$ip' isn't a valid IPv4 address. Skipping this DC."
            return
        }
        if ($ip -like '127.*') {
            Write-Error "${ComputerName}: leave loopback out of -PartnerDnsServer; the script adds it last. Skipping this DC."
            return
        }
    }

    $session = $null
    try {
        $cim = @{ ComputerName = $ComputerName; ErrorAction = 'Stop' }
        if ($Credential) { $cim.Credential = $Credential }
        $session = New-CimSession @cim

        if ($InterfaceAlias) {
            $ifIndex = (Get-NetAdapter -CimSession $session -Name $InterfaceAlias -ErrorAction Stop).ifIndex
        }
        else {
            $route = Get-NetRoute -CimSession $session -DestinationPrefix '0.0.0.0/0' -ErrorAction Stop | Sort-Object RouteMetric | Select-Object -First 1
            $ifIndex = $route.ifIndex
        }
        $adapter = Get-NetAdapter -CimSession $session -InterfaceIndex $ifIndex -ErrorAction Stop
        $ownIPs  = @((Get-NetIPAddress -CimSession $session -InterfaceIndex $ifIndex -AddressFamily IPv4 -ErrorAction Stop).IPAddress)
        $before  = @((Get-DnsClientServerAddress -CimSession $session -InterfaceIndex $ifIndex -AddressFamily IPv4 -ErrorAction Stop).ServerAddresses)

        # The DC's own address as a "partner" is just loopback in disguise, and in first place that's what we're avoiding.
        $self = @($partners | Where-Object { $ownIPs -contains $_ })
        if ($self) {
            Write-Error "${ComputerName}: $($self -join ', ') is this DC's own address. Use a different DC as the partner. Skipping this DC."
            return
        }

        $desired = @($partners)
        if (-not $NoLoopback) { $desired += '127.0.0.1' }

        $result = [pscustomobject]@{
            ComputerName = $ComputerName
            Adapter      = $adapter.Name
            Before       = $before -join ', '
            After        = $desired -join ', '
            Result       = ''
        }

        if (($before -join ',') -eq ($desired -join ',')) {
            $result.Result = 'Already correct'
        }
        elseif ($PSCmdlet.ShouldProcess("$ComputerName ($($adapter.Name))", "Set DNS servers to $($desired -join ', ')")) {
            Set-DnsClientServerAddress -CimSession $session -InterfaceIndex $ifIndex -ServerAddresses $desired -ErrorAction Stop
            $check = @((Get-DnsClientServerAddress -CimSession $session -InterfaceIndex $ifIndex -AddressFamily IPv4).ServerAddresses)
            $result.After  = $check -join ', '
            $result.Result = if (($check -join ',') -eq ($desired -join ',')) { 'Updated' } else { 'Updated, but readback differs' }
        }
        else {
            $result.Result = 'WhatIf'
        }
        $result
    }
    catch {
        [pscustomobject]@{ ComputerName = $ComputerName; Adapter = $null; Before = $null; After = $null; Result = "Failed: $($_.Exception.Message)" }
    }
    finally {
        if ($session) { Remove-CimSession -CimSession $session }
    }
}

Parameters

ParameterTypeDefaultWhat it's for
-ComputerNamestring—The domain controller to configure. Takes pipeline input by property name, so a CSV with ComputerName and PartnerDnsServer columns works.
-PartnerDnsServerstring[]—IPv4 address of one or more other DCs running DNS, ideally in the same site. From a CSV, separate several with semicolons.
-InterfaceAliasstringadapter with the default routeName of the adapter to change, if the default-route guess isn't the one you want.
-NoLoopbackswitch—Leave 127.0.0.1 off the end. Only for DCs that don't run the DNS Server role.
-Credentialpscredential—Alternate credentials for the CIM connection.
-WhatIfswitch—Connect, read the current settings and show the change, without applying it.

Run it

Preview the change on one DC.

.\Set-DCDnsClientServer.ps1 -ComputerName DC01 -PartnerDnsServer 10.0.1.11 -WhatIf

Two DCs in the same site, each pointing at the other.

'DC01;10.0.1.11', 'DC02;10.0.1.10' | ConvertFrom-Csv -Delimiter ';' -Header ComputerName, PartnerDnsServer | .\Set-DCDnsClientServer.ps1

The whole estate from a CSV (ComputerName, PartnerDnsServer columns).

Import-Csv .\dc-dns.csv | .\Set-DCDnsClientServer.ps1 | Format-Table

Two partners, one local and one in the hub site.

.\Set-DCDnsClientServer.ps1 -ComputerName DC-BR01 -PartnerDnsServer 10.20.0.11, 10.0.1.10

What you'll see

Example outputvalues are illustrative
ComputerName Adapter   Before                        After                         Result
------------ -------   ------                        -----                         ------
DC01         Ethernet0 127.0.0.1, 10.0.1.11          10.0.1.11, 127.0.0.1          Updated
DC02         Ethernet0 10.0.1.10, 127.0.0.1          10.0.1.10, 127.0.0.1          Already correct
DC-BR01      Ethernet0 10.20.0.12, 8.8.8.8           10.20.0.11, 10.0.1.10, 127.0.0.1 Updated
DC-BR02                                                                            Failed: WinRM cannot complete the operation...

How it works

  1. Validate the input first. Partner addresses are split (so a CSV cell like 10.0.1.11;10.0.2.11 works), checked as real IPv4 addresses, and loopback is rejected there because the script adds it itself.
  2. Open a CIM session to the DC. Everything else happens over that one session, which gets cleaned up at the end whether the run worked or not.
  3. Find the right adapter. Unless you named one, it's whichever adapter carries the default route. The script also reads that adapter's own IPs, so it can refuse a "partner" that's really the DC itself.
  4. Compare, then change. If the list is already correct, nothing happens. Otherwise Set-DnsClientServerAddress applies the partners plus 127.0.0.1, and the script reads the settings back to confirm they stuck.
  5. Report. One object per DC with the adapter, before, after and result. A DC that can't be reached shows up as a Failed row instead of stopping the run.

Take it further

  • Check the server side too. Get-DnsServerForwarder -ComputerName DC01 shows where each DC sends internet queries, and Set-DnsServerForwarder fixes it. Keep that a separate, deliberate change.
  • Audit before you change anything. Run with -WhatIf against every DC (Get-ADDomainController -Filter * gives you the list) and you've got a report of which ones are misconfigured.
  • Run dcdiag afterward. dcdiag /test:dns /v on each DC is a quick sanity check that registration and resolution are happy with the new order.

Things that'll trip you up

  • Never put public DNS on a DC's adapter. 8.8.8.8 or your ISP's resolver can't answer for your AD zones, so any query that lands there fails in confusing ways. Internet names are the DNS server's job, through forwarders or root hints, not the client settings.
  • A single-DC domain has no partner. With only one DC there's nothing to point at, so this script isn't for you. Set it to 127.0.0.1 on its own, then go build a second DC. One DC is one bad update away from a very long day.
  • This only touches IPv4. With IPv6 enabled, Windows keeps a separate IPv6 DNS list, often ::1 by default. Leave IPv6 on (Microsoft doesn't recommend disabling it) and review that list with Get-DnsClientServerAddress -AddressFamily IPv6.
  • The adapter guess is the default route. On a DC with more than one NIC (a backup network, say), the script picks the adapter with the default gateway. If that's not the right one, name it with -InterfaceAlias.