Locating Domain Controllers
We have mentioned the Locator previously throughout the book, especially in Chapter 3 when we were discussing serverless binding. However, until now we have not really taken the time to explain what it is or what we can do with it. For example, when we use an ADsPath like LDAP://DC=mydomain,DC=com that contains no server name, we are in fact using the Locator service to find a domain controller from the mydomain.com domain. Most of the samples we find that demonstrate LDAP programming with Active Directory show the use of serverless binding, but rarely is any mention made about how it actually works.
When we speak of the Locator, we are talking about the Domain Controller Locator as implemented by the local Netlogon service. It is used by various parts of Windows, including LDAP, to automatically find the most appropriate domain controller to contact. The Domain Controller Locator, or simply Locator here, is responsible for making sure that network clients contact the servers that the domain administrators have designated for them to use based on the network topology of the organization. Without it, network clients would have no way to determine which domain controllers to use, so it is a fundamental underpinning of the Active Directory infrastructure.
How the Domain Controller Locator Works
Each domain controller in the domain registers itself in DNS along with the Active Directory site it is in and the sites that it includes. The Locator is called via a remote procedure call (RPC) to the Netlogon service using the DsGetDcName API. Once contacted, the client passes some information to the Locator, which in turn looks up a name and then sends a datagram (like a ping) to the domain controller to see if it is alive. For NetBIOS-style names, this datagram is in the form of a mailslot message. For DNS-style names, it is an LDAP search using UDP rather than TCP. Starting with the domain controllers that are in the site closest to the caller (as determined by looking up the client's site) the Locator sends a datagram to each domain controller and returns when the first one responds. Information about the "winning" domain controller is then cached such that subsequent uses of the Locator will return the cached information. However, eventually the cache will expire, or another process on the machine might force a refresh, and the Locator will initiate this process again. This last point is important to understand. We should not code any application to expect that the Locator will always return the same information. There is no guarantee that the same domain controller will always be contacted.
Finally, it may seem obvious, but the Locator is used only with Active Directory. ADAM requires a server name to be specified when constructing an LDAP string, as is the case with many other pure LDAP directories.
Using the Locator Service
The Locator can be accessed from the Domain, Forest, ApplicationPartition, DomainController, and GlobalCatalog classes. All of these classes contain methods used to find a single domain controller or to enumerate all domain controllers. Each method uses the Locator to accomplish this. Listing 9.4 demonstrates how to use the Locator to find a single domain controller.
Listing 9.4. Finding a Single Domain Controller
DirectoryContext ctx = new DirectoryContext( DirectoryContextType.Domain, "mydomain.org", ); using (DomainController dc = DomainController.FindOne(ctx)) { Console.WriteLine(dc.Name); Console.WriteLine(dc.OSVersion); Console.WriteLine(dc.SiteName); Console.WriteLine(dc.IPAddress); Console.WriteLine(dc.Forest); Console.WriteLine(dc.CurrentTime); } |
Under the hood, SDS.AD calls the DsGetDcName API to accomplish this. SDS.AD internally uses this function to get the name of a domain controller in the domain we specify. It then calls another function, called DsGetDomainControllerInfo, and it performs an LDAP search to pull all the information together about a particular domain controller.
Enumerating All Domain Controllers
In addition to returning information about a domain controller, we can use the Locator to enumerate all the domain controllers in the domain. Listing 9.5 shows one such example using the Domain class.
Listing 9.5. Enumerating All Domain Controllers
Domain domain = Domain.GetCurrentDomain(); using (domain) { foreach (DomainController dc in domain.FindAllDiscoverableDomainControllers()) { using (dc) { Console.WriteLine(dc.Name); Console.WriteLine(dc.OSVersion); Console.WriteLine(dc.SiteName); Console.WriteLine(dc.IPAddress); Console.WriteLine(dc.Forest); Console.WriteLine(dc.CurrentTime); } } } |
Advanced Locator Features
This is nice, but so far, we haven't shown much that we cannot accomplish with a simple serverless bind, a bind using a DNS domain name, or some basic LDAP searches to find all of the domain controllers in a domain. However, serverless binding does not allow us to harness the full power of the Locator. In fact, the underlying API, DsGetDcName, allows us to specify a variety of options that change its behavior. In SDS.AD, these are expressed in the LocatorOptions enumeration. The option that we think is most interesting is the ForceRediscovery value, as it can be used to purge the internal cache and locate a domain controller again. If we suspect that our domain controller has suddenly stopped responding, we can now proactively try to find a new one. Let's modify Listing 9.4 to show how easy this is. Listing 9.6 now forces a rediscovery.
Listing 9.6. Forcing Rediscovery
DirectoryContext ctx = new DirectoryContext( DirectoryContextType.Domain, "mydomain.org", ); //Notice the extra parameter here... using (DomainController dc = DomainController.FindOne( ctx, LocatorOptions.ForceRediscovery) ) { Console.WriteLine(dc.Name); Console.WriteLine(dc.OSVersion); Console.WriteLine(dc.SiteName); Console.WriteLine(dc.IPAddress); Console.WriteLine(dc.Forest); Console.WriteLine(dc.CurrentTime); } |
All we did was use the overloaded FindOne method that takes a LocatorOptions parameter, and then specify ForceRediscovery. Now, if our previous domain controller were truly down, we would get a brand-new result. Perhaps we can take the new server information and use that to recover gracefully from whatever problems our other code might have encountered when the previous domain controller went offline. Obviously, this depends on what we were doing and how we built our application, but at least we have some control!
In addition to specifying LocatorOptions, we can also use the feature in DsGetDcName that allows us to specify a specific Active Directory site name so that we can locate a domain controller in an arbitrary site. Serverless binding will always use the current site, so we don't have this kind of flexibility with it. Let's modify Listing 9.4 one more time to demonstrate this. Listing 9.7 shows how we specify a site to use.
Listing 9.7. Finding a Domain Controller by Site Name
DirectoryContext ctx = new DirectoryContext( DirectoryContextType.Domain, "mydomain.org", ); //Now we are specifying a site name using (DomainController dc = DomainController.FindOne( ctx, "othersite") ) { Console.WriteLine(dc.Name); Console.WriteLine(dc.OSVersion); Console.WriteLine(dc.SiteName); Console.WriteLine(dc.IPAddress); Console.WriteLine(dc.Forest); Console.WriteLine(dc.CurrentTime); } |
While our sample does not show this, we can also specify LocatorOptions in addition to a site name, as the FindOne method actually has four overloads that allow all of the combinations.
In addition to that, the Domain, Forest, GlobalCatalog, and ApplicationPartition classes all have similar methods that tie into the same infrastructure. No matter where we start or what we are looking for, we now have the full power of the Locator to find any kind of domain controller, global catalog server, or set of servers that we need.
DsGetDcName under the Hood
To replicate finding a single domain controller in .NET 1.1, we would need to wrap many of these API calls ourselves. In order to understand better what SDS.AD is doing, let's look more closely at the DsGetDcName C++ API declaration and its parameters:
DWORD DsGetDcName( LPCTSTR ComputerName, LPCTSTR DomainName, GUID* DomainGuid, LPCTSTR SiteName, ULONG Flags, PDOMAIN_CONTROLLER_INFO* DomainControllerInfo );
DsGetDcName is the core function used to invoke the Locator. We can call it using P/Invoke and return domain controllers by domain, site, or computer (the domain associated with a particular member server). Using some flags that we already described in the context of the LocatorOptions enumeration, we can additionally require that the returned domain controller have some specific characteristics. The output of this API is a DOMAIN_CONTROLLER_INFO structure. Listing 9.8 is a sample C# P/Invoke declaration for this structure and shows all of the useful information returned in it.
Listing 9.8. DOMAIN_CONTROLLER_INFO Structure
using LPTSTR = System.String; using BOOL = System.Int32; using GUID = System.Guid; using DWORD = System.Uint32; [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] internal struct DOMAIN_CONTROLLER_INFO { internal LPTSTR DomainControllerName; internal LPTSTR DomainControllerAddress; internal DWORD DomainControllerAddressType; internal GUID DomainGuid; internal LPTSTR DomainName; internal LPTSTR DnsForestName; internal DWORD Flags; internal LPTSTR DcSiteName; internal LPTSTR ClientSiteName; } |
Just as SDS.AD does under the hood, once we have the name of a domain controller, we can use DsGetDomainControllerInfo to return more information about the domain controller itself. Listing 9.9 shows another C# declaration for the returned structure.
Listing 9.9. C# Declaration of Structure Returned by DsGetDomainControllerInfo
using LPTSTR = System.String; using BOOL = System.Int32; using GUID = System.Guid; [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] internal struct DOMAIN_CONTROLLER_INFO_2 { internal LPTSTR NetbiosName; internal LPTSTR DnsHostName; internal LPTSTR SiteName; internal LPTSTR SiteObjectName; internal LPTSTR ComputerObjectName; internal LPTSTR ServerObjectName; internal LPTSTR NtdsDsaObjectName; internal BOOL fIsPdc; internal BOOL fDsEnabled; internal BOOL fIsGc; internal GUID SiteObjectGuid; internal GUID ComputerObjectGuid; internal GUID ServerObjectGuid; internal GUID NtdsDsaObjectGuid; } |
One thing to note about the DsGetDomainControllerInfo function is that it actually returns an array of structures shown in Listing 9.9. We need to iterate through the returned structures to find the one in which we are interested.
To enumerate multiple domain controllers, SDS.AD is using the DsGetDcOpen method to get a handle (pointer) to an enumeration context. When used in conjunction with DsGetDcNext and DsGetDcClose, these three API functions enumerate all of the available domain controllers. To replicate this functionality in .NET 1.1, not only would we need to wrap these three methods, but we would also need to ensure that our code was used only on the Windows XP platform or later. Some of these functions do not exist in previous versions of Windows. Internally, SDS.AD is making this decision for us and is using the DNS to generate the same information if our code executes on Windows 2000 or Windows NT4. Obviously, it would take considerable effort to build something this easy to use in version 1.x of the .NET Framework, so we are glad to have SDS.AD these days!
Applications for Locating Domain Controllers
So, why do we need this? We already discussed some of the applications for LocatorOptions for forcing rediscovery and we demonstrated how we could locate domain controllers in other sites. Sometimes we need to enumerate the domain controllers in the domain to inspect nonreplicated attribute values that will be different on every server. As an example, in order to accurately find the last time a user has logged into the domain, we need to inspect the nonreplicated lastLogon attribute. We demonstrate this in Chapter 10. Obviously, there are many other possible uses for the Locator than what we've demonstrated, but these are a few of our favorite applications.