Public/Get-ADAvailableDomainController.ps1
|
Function Get-ADAvailableDomainController() { <# .SYNOPSIS Find a Domain Controller using DNS that is available for PowerShell ActiveDirectory commands (using AD Web Services) .DESCRIPTION Rather than statically specifying a Domain Controller to use in a script this function will return a fully qualified domain name of an Active Directory Domain Controller that is able to response to PowerShell ActiveDirectory cmdlets (Get-ADUser, Get-ADDomain, etc). It does this by: - Performing a DNS lookup against the SRV record _ldap._tcp.dc._msdcs.<domain name> to find the fqdns of the Domain Controllers - Testing each domain controller by fqdn and check that a DefaultNamingContext is available Questions: Q - Why not just use the IP address to run cmdlets against? A - Exchange cmdlets don't allow use of an IP address in the parameter -DomainController and require the fqdn/server name Q - What does this function require to run? A - .Net 4.8 or above is required Q - What port is required for remote ActiveDirectory cmdlet operation? A - TCP port 389 and 9389 is required (and the Active Directory Web Services service has to be running and available) .PARAMETER DnsDomainName Mandatory. This string should specify the DNS domain (e.g. contoso.com) of the Active Directory domain .PARAMETER DomainCredential Optional. This parameter should be used when you require to use a credential to authenticate against Active Directory .EXAMPLE [string]$sAvailableDCName = Get-ADAvailableADDomainController -DnsDomainName "mydomain.com" # This example will attempt to discover a Domain Controller that is responding to ActiveDirectory module cmdlets using the current logged on users' credentials .EXAMPLE [PSCredential]$objADCredential = Get-Credential [string]$sAvailableDCName = Get-ADAvailableADDomainController -DnsDomainName "mydomain.com" -DomainCredential $objADCredential # This example will attempt to discover a Domain Controller that is responding to ActiveDirectory module cmdlets using credentials specified under the -DomainCredential parameter .NOTES MVogwell - Nov 2021 - Version 1.5 Version history: 1.0 - Initial tested release 1.1 - Updated to use DNS query to return Domain Controller fqdn 1.2 - Updated to use .net query for domain rather than the PowerShell ActiveDirectory module 1.3 - Added more detail to error messages 1.4 - Bug in previous version - sPwdClear was being cleared before the second attempt to access an available DC 1.5 - Updated error messages returned if no DCs are avaialble. Also found Error[0] was not referenced as $Global:Error[0] #> [CmdletBinding()] param ( [Parameter(Mandatory=$true)][string]$DnsDomainName, [Parameter(Mandatory=$false)][PSCredential]$DomainCredential ) $ErrorActionPreference = "Stop" # Variable initialisation $sAvailableDcName = $null # This is the return [System.Collections.ArrayList]$arrDcContactErrors = @() if (!($null -eq $DomainCredential)) { try { Write-Verbose "*** Extracting credential $($DomainCredential.UserName)" # Convert password to plain text -this is required for System.DirectoryServices.DirectoryEntry $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($DomainCredential.Password) $sPwdClear = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) if ([string]::IsNullOrEmpty($sPwdClear)) { throw "Password null or empty" } # Clear the BSTR value - no longer required Remove-Variable BSTR -ErrorAction "SilentlyContinue" Write-Verbose "`t+++ Success" } catch { Remove-Variable BSTR,sPwdClear -ErrorAction "SilentlyContinue" $sThrowMsg = ("Failed to extract credentials - unable to continue. Error: " + ($Global:Error[0].Exception.Message).Replace("`n"," ").Replace("`r","")) throw $sThrowMsg } } # Perform a DNS lookup to get the IP addresses of domain controllers for the domain try { Write-Verbose "*** Discovering domain controller names for domain $DnsDomainName" $sDnsPath = ("_ldap._tcp.dc._msdcs." + $DnsDomainName) $arrAdDCs = Resolve-DnsName -Type SRV -Name $sDnsPath -ErrorAction SilentlyContinue | Where-Object {$_.Type -eq 'SRV'} | Select-Object -ExpandProperty NameTarget #$arrAdDCs.gettype() Write-Verbose "`t+++ Success - $arrAdDCs" } catch { $sThrowMsg = ("Failed to perform DNS lookup on SRV record " + $sDnsPath + ". Failed to discover an available Domain Controller. Error: " + ($Global:Error[0].Exception.Message).Replace("`n"," ").Replace("`r","")) Write-Verbose "`t+++ $sThrowMsg" # Tidy up Remove-Variable DnsDomainName,DomainCredential,sPwdClear -ErrorAction "SilentlyContinue" throw $sThrowMsg # Exit point } # Fail the function if no domain controller could be discovered if ($null -eq $arrAdDCs) { $sThrowMsg = "Failed to discover an available Domain Controller - the DNS query did not return any servers (Query: " + $sDnsPath + ")" # Tidy up Remove-Variable DnsDomainName,DomainCredential,sPwdClear -ErrorAction "SilentlyContinue" throw $sThrowMsg # Exit point } # Loop through and test each of the DCs returned from the DNS lookup to find an available DC Write-Verbose "*** Testing access to Domain Controllers (by DNS Name):" foreach ($sReplicaDirectoryServer in $arrAdDCs) { try { # Set the path for the DirectoryEntry root $sDirectoryEntryPath = "LDAP://" + $sReplicaDirectoryServer + "/RootDSE" # Query the DC - use credentials if given in the params if ($null -eq $DomainCredential) { # Test access without specifying a credential Write-Verbose "`tTesting $($sDirectoryEntryPath): " # Attempt to pull data from the DC $objDirectoryEntry = New-Object System.DirectoryServices.DirectoryEntry($sDirectoryEntryPath) } else { # Test access using a credential Write-Verbose "`tTesting $($sDirectoryEntryPath) using Credential $($DomainCredential.UserName)" # Attempt to pull data from the DC using the credential $objDirectoryEntry = New-Object System.DirectoryServices.DirectoryEntry($sDirectoryEntryPath,$DomainCredential.UserName,$sPwdClear) } } catch { $sDcCheckErrMsg = $sReplicaDirectoryServer + ": Server not available " + ($Global:Error[0].Exception.Message).Replace("`n"," ").Replace("`r","") [void]$arrDcContactErrors.Add($sDcCheckErrMsg) } # Show the result to the user. Escape the loop on accessing a DC successfully if ($null -eq $objDirectoryEntry) { Write-Verbose "`t`t--- Server not available (null returned)" } elseif ((($objDirectoryEntry | Get-Member -MemberType Properties | Select-Object -ExpandProperty name) -contains "defaultNamingContext") -eq $false) { Write-Verbose "`t`t--- Server not available (defaultNamingContext not returned)" $sDcCheckErrMsg = $sReplicaDirectoryServer + ": Server not available (defaultNamingContext not returned probably caused by invalid credentials - please update the SeceretStore credentials for " + $DomainCredential.UserName + ")" [void]$arrDcContactErrors.Add($sDcCheckErrMsg) } else { $sDefNamingContext = $objDirectoryEntry.defaultNamingContext Write-Verbose "`t`t+++ Found responsive Domain Controller by FQDN (DefaultNamingContext = $($sDefNamingContext))" $sAvailableDcName = $sReplicaDirectoryServer # Clear the cleartext password Remove-Variable sPwdClear -ErrorAction "SilentlyContinue" # Exit the for loop as a working DC has been found break } Remove-Variable objDirectoryEntry,sDirectoryEntryPath,sPwdClear -ErrorAction "SilentlyContinue" } # End of: Loop through and test each of the DCs returned from the DNS lookup to find an available DC # If at least one DC hasn't been discovered then throw an error if ($null -eq $sAvailableDcName) { # Generate an error message including each of the error messages returned when attempting to access each DC in turn $sThrowMsg = "Failed to access to a Domain Controller for " + $DnsDomainName + ". The following errors were returned during DC tests: " + ($arrDcContactErrors -join " || ") # Tidy up Remove-Variable DnsDomainName,DomainCredential,sPwdClear -ErrorAction "SilentlyContinue" throw $sThrowMsg # Exit point } else { Write-Verbose "*** Returning Domain Controller DNS name: $sAvailableDcName" } Write-Verbose "`t**** $sPwdClear" # Tidy up Remove-Variable sPwdClear,DnsDomainName,DomainCredential -ErrorAction "SilentlyContinue" # return a string containing the DC DNS name return $sAvailableDcName } |