Monday, March 13, 2017

LDAP server is unavailable error de-mystified

Background

Recently, I was presented with an issue when trying to connect to AD LDS (Active Directory Lightweight Directory Services (previously known as ADAM) using C# and System.DirectoryServices.AccountManagement.PrincipalContext. The integration intermittently resulted in an exception with message "LDAP server is unavailable".

In this blog, I am going to discuss how this problem was tackled.

The Quick Solution

To save you time, please ensure that the AD LDS servers machine is accessible by hostname from your connecting client's location.

The Nitty Gritty

Performing lots of searches on google to no avail, I decided to solve this issue the old fashioned way.

Given the following:
  • The problem only occurred on development machines and not on TeamCity Build Agent.
  • The same code was working/passing tests on development machine before.
  • The test AD LDS instance was configured on the same TeamCity Build Agent
  • The DirectorySearcher was able to connect to the AD LDS and return the properties of the username in question. 
  • The AD LDS configuration caused similar issues before, the issues were with user authentication as the password was not being set properly on the user in question.

The piece of code causing the issue was this line:

var result = context.ValidateCredentials(userId, password, ContextOptions.SimpleBind);

Where context is an instance of PrincipalContext and userId is the distinguished name of the user.

Thinking this may be an issue with the way the PrincipalContext was being instantiated with ContextOptions, I wrote a console application which dynamically generated a list of PrincipalContext constructors with all possible combinations of ContextOptions and all parameters required to work with ApplicationDirectory context type (which is a must if you are connecting with AD LDS). I then tried calling ValidateCredentials with each constructor (about 34 calls) all the calls failed with the same error (except in some cases with Unknown Error, basically due to SecuredSocket binding not being supported).

I then resorted to applying the all "ContextOptions" combinations with the ValidateCredentials calls for each context as well, which resulted in 34x63 calls. All calls also resulted in the same error.

With the peace of mind that this has nothing to do with the "ContextOptions" as I have already tested all possible combinations, I had to step my effort up. I decided to decompile the System.DirectoryServices assembly (3 of them) using JetBrains dotPeek and followed the stack-trace returned in the exception. I ran my code in debug mode when it broke on exception I did a dry run of the decompiled code by looking at internal private variables made accessible through Visual Studio Non-Public member view.

I noticed that the CredentialValidator class inside the assembly resolved the AD LDS' local machine name and was trying to connect to the server with that hostname. The development machine was trying to connect to the AD LDS server over the VPN using an IP Address and not the hostname. The development machine was unable to resolve the IP Address for that hostname.

The Solution

Adding an entry to "hosts" files and modified relevant test cases to confirm before running that if the hostname is not resolving to the known IP Address then it should fail at test setup with a detailed error notification. After performing these steps the code starting working properly.

Wishful Thinking

Given that principal context is provided an IP Address to connect to the server, either connecting by name should be disabled automatically or the error should mention the location it tried to access, this would have instantly allowed the developers to know what the problem could be.

Conclusion

There is nothing that cannot be debugged when you have a good decompiler at hand. Apart from that, please ensure that your AD or AD LDS servers are accessible by "hostname" as well as IP Addresses. You can update hosts file in the c:\windows\system32\drivers\etc folder, alternatively, update your DNS server so it can resolve the domain name to the proper IP Address.