Determining AD Site of Computer Objects

Active Directory is awesome. It is used to store many things about many things, and most of the time that comes in really handy. Recently a client asked, if it was possible to write a script that will discover all computer objects in a given AD site.

The short answer is yes, of course.

Then, there are many ways to do this – but querying Active Directory for some computer object attribute is not one of them. Computer-to-site mappings are transient and this type of information isn’t stored in AD. It is rather being determined at the time the operating system needs to locate itself on the network, relative to AD topology.

Potential ways to approach the task

  1. Write a script that queries AD site objects, AD site subnet objects, grabs a list of computer objects from AD, then resolves computer names to IP addresses, and finally compares IP information from DNS with IP information from AD site subnets. Sounds too complicated.
  2. Write a script that grabs a list of computer objects from AD, then resolves computer names to IP addresses, and finally dumps this information to a text file. Then you import the text file, and knowing your network’s IP addressing scheme you can probably figure what’s where using Excel. Sounds too low-tech.
  3. Write a script (or a VB.NET app) that will do what the operating system does when it needs to find out its location – call Windows API. More specifically, netapi32.dll has a function called DsAddressToSiteNames, which as the name implies, resolves a given IP address to an AD site name.
  4. Same as #3 but using nltest.exe

Number 3 peaked my interest. I wanted to completely offload the footwork to the Active Directory and not reinvent any wheels. #3 is the only option that determines site location of the computer THE SAME WAY Windows OS does, which also factors in how well AD sites and subnets are defined/maintained.

The code

I’ll have to admit that programming unmanaged, low-level OS API function calls and dealing with buffers and pointers is not my strong skill, so I had to look around. Eventually I found something that looked like what I was looking for, and adapted it further to suit my specific¬†needs. Code below uses VB.NET syntax, and I use Visual Studio Express 2012 IDE.

You can get the full Visual Studio project from the Downloads page. Feel free to modify and redistribute but use at your own risk.

Imports System.DirectoryServices
Imports System.Runtime.InteropServices

First let’s import the namespaces we are going to work with.

Module Module1

#Region "External Interface for resolving AD objects to AD Sites"
    '\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
    'external functions taken from http://www.dotnetmonster.com/Uwe/Forum.aspx/dotnet-interop/1513/DsAddressToSiteNames-VB-or-C
    '/////////////////////////////

    Const WSADESCRIPTION_LEN = 256

    <StructLayout(LayoutKind.Sequential)> _
    Private Structure WSADATA
        Public wVersion As Short
        Dim wHighVersion As Short
        <MarshalAs(UnmanagedType.ByValTStr, sizeConst:=WSADESCRIPTION_LEN + 1)> Public szDescription As String
        <MarshalAs(UnmanagedType.ByValTStr, sizeConst:=WSADESCRIPTION_LEN + 1)> Public szSystemStatus As String
        Public iMaxSockets As Integer
        Public iMaxUdpDg As Integer
        Public lpVenderInfo As IntPtr
    End Structure

    <StructLayout(LayoutKind.Sequential)> _
    Private Structure SockAddr
        Public Family As Short
        Public Port As Short
        <MarshalAs(UnmanagedType.ByValArray, SizeConst:=4)> Public Addr As Byte()
        <MarshalAs(UnmanagedType.ByValArray, SizeConst:=8)> Public Zero As Byte()
    End Structure

    <StructLayout(LayoutKind.Sequential)> _
    Private Structure SOCKET_ADDRESS
        Public lpSockaddr As IntPtr
        Public iSockaddrLength As Integer
    End Structure

    <DllImport("ws2_32.dll", exactspelling:=True, setlasterror:=False)> _
    Private Function WSAGetLastError() As Integer
    End Function

    <DllImport("ws2_32.dll", CharSet:=CharSet.Ansi, SetLastError:=True)> _
    Private Function WSAStringToAddress(ByVal addressString As String, ByVal addressfamily As System.Net.Sockets.AddressFamily, ByVal lpProtocolInfo As IntPtr, ByRef socketAddress As SockAddr, ByRef socketAddressSize As Integer) As Integer
    End Function

    <DllImport("wsock32")> _
    Private Function WSAStartup(ByVal wVersionRequired As Integer, ByRef lpWSADATA As WSADATA) As Integer
    End Function

    <DllImport("wsock32")> _
    Private Function WSACleanup() As Integer
    End Function

    <DllImport("netapi32.dll", CharSet:=CharSet.Auto)> _
    Private Function DsAddressToSiteNames(ByVal ComputerName As String, ByVal EntryCount As Integer, ByVal SocketAddresses() As SOCKET_ADDRESS, ByRef SiteNames As IntPtr) As Integer
    End Function

    <DllImport("netapi32.dll")> _
    Private Function NetApiBufferFree(ByVal Buffer As IntPtr) As Integer
    End Function
#End Region

This next sizeable piece of code defines external API functions and data structures that we are going to call after computer names are obtained from AD, and IP addresses are resolved in DNS. This code was adapted from the link referenced (Thanks Mattias and Brian for sharing).

    Public dir As DirectoryEntry

    Sub Main()

        BindToDirectory()
        ListComputerObjects()
        dir.Dispose()

    End Sub

Now we are setting up Sub Main – let’s define BindToDirectory and ListComputerObjects below.

    Private Sub BindToDirectory()

        dir = New DirectoryEntry("LDAP://" & Environment.GetEnvironmentVariable("USERDNSDOMAIN"))

    End Sub

Simple as that… That’s all it takes to bind to an Active Directory – one line of code. Of course, we could make it more complex, as the user for input (domain name and/or credentials, etc) but for illustrative purposes we will keep it in line with assumption that this code is running on a domain-joined PC with administrative credentials (in theory admin isn’t needed to query AD so this should work with plain user account as well).

    Private Sub ListComputerObjects()

        Dim search As New DirectorySearcher(dir, "(&(objectCategory=computer)(!userAccountControl:1.2.840.113556.1.4.803:=8192))")
        Dim comps As SearchResultCollection = search.FindAll()
        Dim IP As String, compname As String

        Try

            For Each de As SearchResult In comps
                Try
                    compname = de.Properties("dnsHostName").Item(0)
                Catch ex As Exception
                    compname = de.Properties("name").Item(0)
                End Try

                Console.Write(compname & "; ")
                IP = GetIPfromHostname(compname)

                Try
                    Console.Write(de.Properties("operatingSystem").Item(0) & "; ")
                Catch ex As Exception
                    Console.Write("Operating system not set in AD; ")
                End Try

                If Not IsNothing(IP) Then
                    Console.Write(GetSite(IP) & "; ")
                Else
                    Console.Write("Can't resolve hostname in DNS; ")
                End If

                Try
                    Console.WriteLine(de.Properties("distinguishedName").Item(0) & "; ")
                Catch ex As Exception
                    Console.WriteLine("DN could not be determined; ")
                End Try
            Next

        Catch ex As Exception

        End Try

    End Sub

This piece of code does a lot more work. It calls Active directory, with LDAP string that finds all computer objects that ARE NOT domain controllers. Then we go into a loop through the result set and 1) GetIpFromHostname and 2) GetSite. This logic makes assumption that domain-joined computers come online occasionally and register their DNS names dynamically. Detecting sites will fail for laptops that have been offline for longer than DNS scavenging interval, and names of which cannot be resolved in internal DNS.

    Private Function GetIPfromHostname(ByVal hostname As String) As String

        Try
            Dim ipaddresses() As System.Net.IPAddress
            ipaddresses = System.Net.Dns.GetHostAddresses(hostname)
            Return ipaddresses(0).ToString
        Catch ex As Exception
            Return Nothing
        End Try

    End Function

Querying IP address from DNS is simply beautiful in managed code. We are making assumptions here that 1) computers do not have static records that associate more than one IP address with a computer name, and 2) computers aren’t multihomed. If these assumptions are not true, you may have to add some logic here to deal with IP address arrays that are potentially longer than one address.

   Private Function GetSite(ByVal IP As String) As String

        Dim rc As Integer
        Dim oSockAddr As SockAddr
        Dim SocketAddress(0) As SOCKET_ADDRESS
        Dim Data As New WSADATA
        Dim pSites As New IntPtr
        Dim pSockaddr As IntPtr
        Dim pSiteName As IntPtr
        Dim sSiteName As String = ""

        Try
            If WSAStartup(&H201, Data) = 0 Then
                rc = WSAStringToAddress(IP, Net.Sockets.AddressFamily.InterNetwork, Nothing, oSockAddr, Marshal.SizeOf(oSockAddr))

                WSACleanup()

                pSockaddr = Marshal.AllocHGlobal(Marshal.SizeOf(oSockAddr))
                Marshal.StructureToPtr(oSockAddr, pSockaddr, True)

                SocketAddress(0).lpSockaddr = pSockaddr
                SocketAddress(0).iSockaddrLength = Marshal.SizeOf(oSockAddr)

                rc = DsAddressToSiteNames("YOUR DOMAIN CONTROLLER FQDN", 1, SocketAddress, pSites)

                pSiteName = Marshal.ReadIntPtr(pSites, 0)
                sSiteName = Marshal.PtrToStringAuto(pSiteName)
                NetApiBufferFree(pSites)

                'Console.WriteLine(String.Format("This IP Address belongs to site {0}", sSiteName))

            End If
            Return sSiteName

        Catch ex As Exception
            Return Nothing
        End Try

    End Function

Finally, the secret sauce. GetSite. This is the code that makes the call into Windows API and performs IP to AD site name resolution. This function uses hardcoded FQDN DNS name of a domain controller that will be used to resolve site names. You can either type in FQDN of your closest domain controller, or using similar approach shown above, query the list of all domain controllers, determine the closest domain controller relative to IP address of your machine, and then dynamically select a domain controller. This way this code would be completely free of any AD-related hardcoding; a thing of beauty.

Results

Run adsibrowser.exe and redirect output to a text file (adsibrowser.exe > c:\output.txt). The resulting text file can be imported into Excel and filtered by AD site name, very quickly and to the point.

What if you don’t want to code

Then use a shortcut, in command line or powershell (on Windows Server 2012):

nltest /dsgetsite

This will fetch the current site of the machine you are on, using the same API call as described in the code above.

nltest /dsaddresstosite:x.x.x.x

This will resolve any IP address on the network to a matching AD site as per sites and subnets definition.

It’s that simple, but if you need more firepower then VB.NET saves the day. By the way, another way to do this is use VB.NET to query and manipulate AD data, but instead of writing complex API calls, you could just shell out and call NLTEST…

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *