If you want detailed information on how Active Directory sites and subnets work to help clients find their closest domain controller, there is good information on TechNet. In short, you need to ensure that you’ve defined (in Active Directory Sites and Services) Active Directory sites for all of your physical locations that have domain controllers. Additionally, you need to specify which subnets are in use for your AD clients. Then, you map every subnet to a corresponding AD site. Clients then magically find their nearest domain controller.

clip_image002

However, even the best plans go awry. For example, what if your network team decides to deploy a new subnet for their shiny new wireless infrastructure, but they conveniently overlook the need to tell you about it? You’ll eventually become aware of the situation when you receive the complaints. “Why is Active Directory so slow?” An investigation reveals that wireless clients in a conference room in your headquarters are using the services of a Domain Controller in your most remote location for authentication and group policy. There is no subnet defined in Active Directory for this new wireless subnet, so any clients on the undefined subnet will randomly roam the planet in search of a domain controller.

How can you proactively avoid these unpleasant scenarios? The ideal solution is to actually communicate with your network team regularly, so you know that all your AD sites/subnets are complete and accurate. And they know they should tell you when things change. But since most of us have never seen this ideal world of unicorns and fairies, you should be looking for signs of trouble.

 

Find the “Roamers” in the LogFiles

Every domain controller logs when they encounter a roaming client (a client that authenticates from an IP address that does not belong to defined AD subnet). This information is written to the Netlogon.log file (default location c:\windows\debug). If you crack open a Netlogon.log file you will see a list of roaming clients:

image

Simply look for lines in the log with NO_CLIENT_SITE, and note the hostname and IP address. Then you may be able to figure out the missing subnet definition, add the missing subnet definition to AD and associate it to the appropriate AD site. Alternatively, ask your network team about the IP address/subnet and let them tell you which AD site is appropriate.

 

Automate the Review of Log Files

If you have more than one domain controller (and I hope you do), reviewing log files manually is a chore you’re not likely to cherish. So now you should figure out how to script it.

 

Step 1: Supply or Dynamically Create a List of Domain Controllers

First, you need to know of all the domain controllers in the forest, so you can pull information from their netlogon.log files. You could just create a text file with all the DC names and read them in. I would prefer; however, to dynamically generate a list from Active Directory. Furthermore, I like to create a PowerShell function for this task, since it’s something you do often with AD-related scripts. Finally, my code remains a bit dated, because I’m never sure if a customer has old (2003-style) or new (2008R2) domain controllers. So I try to avoid some of the 2008R2 AD cmdlets that can make your code much simpler. The logic of the code is to find any Server objects in the configuration partition. Then, we validate that the server object has an NTDS Settings object associated with it. This should accurately identify any DCs.

### DCDiscovery - All DCs in the Forest
Function EnumerateDCs
{
   $arrServers =@()
   $rootdse=new-object directoryservices.directoryentry(LDAP://rootdse)
   $Configpath=$rootdse.configurationNamingContext
   $adsientry=new-object directoryservices.directoryentry(LDAP://cn=Sites,$Configpath)
   $adsisearcher=new-object directoryservices.directorysearcher($adsientry)
   $adsisearcher.pagesize=1000
   $adsisearcher.searchscope="subtree"
   $strfilter="(ObjectClass=Server)"
   $adsisearcher.filter=$strfilter
   $colAttributeList = "cn","dNSHostName","ServerReference","distinguishedname"
   Foreach ($c in $colAttributeList)
   {
      [void]$adsiSearcher.PropertiesToLoad.Add($c)
   }
   $objServers=$adsisearcher.findall()
   forEach ($objServer in $objServers)
   {
      $serverDN = $objServer.properties.item("distinguishedname")
      $ntdsDN = "CN=NTDS Settings,$serverDN"
      if ([adsi]::Exists(LDAP://$ntdsDN))
      {
         $serverdNSHostname = $objServer.properties.item("dNSHostname")
         $arrServers += "$serverDNSHostname"
      }
      $serverdNSHostname=""
   }
   $arrServers
}

Step 2: Reach Out and Grab the Netlogon.log Files from Each DC

For simplicity sake, let’s assume that the DCs have default settings for the OS installation, so the netlogon.log file can be found at C:\Windows\Debug\netlogon.log. The code does check that this location/file exist before proceeding. If the DC cannot be reached, or that location/file does not exist, write to the screen that the log file for that DC cannot be found DC and move on. Note how the cmdlet Test-Path can be used to validate connectivity to the log file. Any errors with connectivity, permissions, or missing path/file should be handled with Test-Path.

The code also includes some error checking (Try{}; Catch{}; Finally{}). If we fail to get content from the netlogon.log file (it’s empty, for example), the Try{};Catch{};Finally{} should handle the errors.

Finally, the code has to do some string gymnastics to pull out specific content from the netlogon.log files. Specifically:

1. Only the last 500 lines of the netlogon.log file will be analyzed
2. The line must contain “NO_CLIENT_SITE”
3. We only want the part of the line that has “HOSTNAME IPADDRESS”

The remaining lines containing only “HOSTNAME and IPADDRESS”, across all DCs, are combined into a single array.

### Collect the FQDN of all DCs in the Forest
$allDCsinForest = EnumerateDCs
### Collect the last 500 lines of the Netlogon.log file from each DC, pick out lines with NO_CLIENT_SITE and combine them
$combinedNetLogon = @()
ForEach ($DC in $allDCsinForest)
{
   $NetlogonPath = “\\$DC\c$\Windows\Debug\netlogon.log”
   If (Test-path $NetLogonPath)
   {
      Try {$netlogon = (Get-Content $NetLogonPath)[-1..-500]}
      Catch {write-host "Can't read Netlogon.log for $DC. It may be empty." -foregroundcolor red;$NetLogon=$null}
      Finally
      {
         If ($NetLogon)
         {
            forEach ($line in $Netlogon)
            {
               If ($line -like "*NO_CLIENT_SITE*")
               {
                  $startPos = $line.IndexOf("NO_CLIENT_SITE") + 16
                  $length = $line.length
                  $length = $length-$startPos
                  $line = $line.SubString($startPos,$length)
                  $combinedNetlogon += $line
               }
            }
         }
      }
   }
   Else {write-host "Can't find Netlogon.log for $DC" -foregroundcolor red}
}

 

Step 3: Reach Out and Grab the Netlogon.log Files from Each DC

To make data analysis/manipulation easier, create a new table/array which contains IP Address and Hostname of roaming clients. This involves splitting each line into IP Address and Hostname. Then, create an object with two properties – IP and Host. Finally, add this object to an array. The resulting array can then be sorted to remove any duplicate entries.

### Walk the array with NO_CLIENT_SITE hostnames/IPs. Create a new table/array of IP addresses and Hostname. Remove
### duplicates.
$colIPs = @()
ForEach ($line in $combinedNetLogon)
{
   $splitLine = $line.split(" ")
   $hostname = $splitLine[0]
   $IPadd = $splitLine[1]
   $objIP = New-Object System.Object
   $objIP | Add-member -type NoteProperty -name IP -value $IPadd
   $objIP | Add-member -type NoteProperty -name Host -value $hostname
   $colIPs += $objIP
}
$listofRoamingClients = $colIps | sort-object -property IP –unique

 

Step 4: Report the results

Now just spit out the results – either to the screen or to a CSV-formatted log file.

### Comment out the line below, to not display results on screen
$listofRoamingClients
### Dump list to a file
$listofRoamingClients | export-csv .\listofroamingclients.txt

clip_image005

 

Run/Re-Run Your Script, and Enjoy the Fruits of Your Labor

Now you’ve got no excuses for roaming clients. You can identify them as often as you wish and fix your subnet definitions appropriately. If you still find your network team less than helpful in resolving issues, simply create a new subnet definition for their workstation IP address(es) and associate it with the most remote AD site in your environment.

Happy cleaning.

Doug Symalla

 

Note:  The script has been updated and posted in a follow-up blog:  http://blogs.technet.com/b/askpfeplat/archive/2012/05/14/roaming-ad-clients-with-an-updated-script.aspx