How to specify TimeOut for ldap bind in .Net
A few days ago, I was working with a .Net developer who was building a Directory Server (Active Directory) enabled application using the popular System.DirectoryService namespace. He was looking for some help to terminate the LDAP bind within a specified time in case there is some issue with connectivity. His test code was very was simple, and looked like
DirectoryEntry de = new DirectoryEntry(“LDAP://contoso.com”);
Object obDir = De.NativeObject // Forcing a bind to AD
He was experiencing a delay of 20-40 seconds on the second line above in case of binding failure and he wanted a way to control the time it takes before it throwsan exception (usually The server is not operational – 0x8007203A)
I did some research, digging how internally ADSI works to establish a connection. As I expected, it was calling the ldap_connection and the call stack which was on wait looked like below
0:000> kL
ChildEBP RetAddr
002dd514 74f20a91 ntdll!ZwWaitForSingleObject+0x15
002dd580 75421194 KERNELBASE!WaitForSingleObjectEx+0x98
002dd598 74a88fb6 KERNEL32!WaitForSingleObjectExImplementation+0x75
002dd5b4 74a88f67 RPCRT4!UTIL_WaitForSyncIO+0x20
002dd5d8 74a8a9ac RPCRT4!UTIL_GetOverlappedResultEx+0x1d
002dd5f4 74a8a95b RPCRT4!UTIL_GetOverlappedResult+0x17
002dd618 74a894af RPCRT4!NMP_SyncSendRecv+0xb0
002dd644 74a893dc RPCRT4!OSF_CCONNECTION::TransSendReceive+0x100
002dd6cc 74a8933f RPCRT4!OSF_CCONNECTION::SendFragment+0x297
002dd724 74a89682 RPCRT4!OSF_CCALL::SendNextFragment+0x2eb
002dd774 74a89edf RPCRT4!OSF_CCALL::FastSendReceive+0x24d
002dd794 74a8a3f7 RPCRT4!OSF_CCALL::SendReceiveHelper+0x5c
002dd7c4 74a77391 RPCRT4!OSF_CCALL::SendReceive+0x44
002dd7d4 74a7804b RPCRT4!I_RpcSendReceive+0x28
002dd7e8 74a7801a RPCRT4!NdrSendReceive+0x31
002dd7f4 74b10149 RPCRT4!NdrpSendReceive+0x9
002ddc08 72995076 RPCRT4!NdrClientCall2+0x1a6
002ddc20 72994d5d LOGONCLI!DsrGetDcNameEx2+0x19
002ddc94 729950be LOGONCLI!DsGetDcNameWithAccountW+0x17e
002ddcbc 757bc7bb LOGONCLI!DsGetDcNameW+0x20
002ddcf4 757bc994 WLDAP32!GetDefaultLdapServer+0x8e
002ddefc 757b97a2 WLDAP32!ConnectToSRVrecs+0xa7
002ddf54 757b9688 WLDAP32!OpenLdapServer+0x612
002ddf74 757bc1a8 WLDAP32!LdapConnect+0x2cf
002ddf98 72914455 WLDAP32!ldap_connect+0x28
002ddfbc 729141a8 adsldpc!LdapOpen+0x1e1
002ddfec 7291406c adsldpc!LdapOpenBindWithDefaultCredentials+0x1ef
002de458 67a62c75 adsldpc!LdapOpenObject2+0x130
002de6d8 67a615e6 adsldp!GetServerBasedObject+0x1b5
002deb30 67a62a49 adsldp!GetObjectW+0x87
002deb5c 729517b0 adsldp!CLDAPNamespace::OpenDSObject+0x34
002debb4 572f1a0c activeds!ADsOpenObject+0xcc
002dec4c 572f190d System_DirectoryServices_ni!DomainBoundILStubClass.IL_STUB_PInvoke(System.String, System.String, System.String, Int32, System.Guid ByRef, System.Object ByRef)+0xac
002dec88 572e8f91 System_DirectoryServices_ni!System.DirectoryServices.Interop.UnsafeNativeMethods.ADsOpenObject(System.String, System.String, System.String, Int32, System.Guid ByRef, System.Object ByRef)+0x2d
002decdc 572e8e25 System_DirectoryServices_ni!System.DirectoryServices.DirectoryEntry.Bind(Boolean)+0x151
002decec 572e9650 System_DirectoryServices_ni!System.DirectoryServices.DirectoryEntry.Bind()+0x25
002decfc 003200fb System_DirectoryServices_ni!System.DirectoryServices.DirectoryEntry.get_NativeObject()+0x20
002ded44 5a4021bb DSNative!DSNative.Program.Main(System.String[])+0x8b
002ded54 5a434227 clr!CallDescrWorker+0x33
002dedd0 5a4343c4 clr!CallDescrWorkerWithHandler+0x8e
002def08 5a4343f9 clr!MethodDesc::CallDescr+0x194
002def24 5a434419 clr!MethodDesc::CallTargetWorker+0x21
002def3c 5a55887a clr!MethodDescCallSite::Call+0x1c
002df0a0 5a558988 clr!ClassLoader::RunMain+0x24c
002df308 5a55879c clr!Assembly::ExecuteMainMethod+0xc1
002df7ec 5a558b91 clr!SystemDomain::ExecuteMainMethod+0x4ec
002df840 5a558a92 clr!ExecuteEXE+0x58
002df88c 5a553a30 clr!_CorExeMainInternal+0x19f
002df8c4 739955ab clr!_CorExeMain+0x4e
002df8d0 73a07f16 mscoreei!_CorExeMain+0x38
002df8e0 73a04de3 MSCOREE!ShellShim__CorExeMain+0x99
002df8e8 7542339a MSCOREE!_CorExeMain_Exported+0x8
002df8f4 76fa9ef2 KERNEL32!BaseThreadInitThunk+0xe
002df934 76fa9ec5 ntdll!__RtlUserThreadStart+0x70
002df94c 00000000 ntdll!_RtlUserThreadStart+0x1bSincerely
If you read the description of ldap_connection, it allows to specify a timeout value. If we specify a NULL value, it falls back to default Timeout, which is calculated in the ldap layer depending on various factors like Inter/Intra site connection attempts. It may take anywhere between 20-40 Seconds.
Unfortunately under the hood ADSI (S.DS under the hood uses ADSI for binding to AD) is hardcoding the TimeOut value as NULL so it will always waits the ‘Default’ Timeout period before it throws an exception in case of binding failure.
So, are we left with no options to control the timeout in .Net? Well, unfortunately Yes, if you are using System.DirectoryServices namespace. But we have an alternative namespace for AD operations in .Net, starting from .net 2.0. i.e System.DirectoryServices.Procotocols (S.DS.P, here onwards). S.DS.P built directly on top of raw ldap APIs, so it has better control on how it makes connection. It provides a class LDAPConnection for binding to AD. This class exposes a property Timeout through which one can specify the TimeInterval for a bind attempt. If it cannot successfully bind within this period, it times out.
A Sample code may look like below
LdapConnection ldc = new LdapConnection("xyz.com");
ldc.Timeout = new TimeSpan(0, 0, 5);
try
{
ldc.Bind();
}
catch (Exception ex)
{
Console.WriteLine("\tAn Exception ({0} ) Occurred...", ex.Message.ToString());
}
Note that the timeout interval is a suggestion while attempting an ldap connection and the underlying TCP connection. While it honors the timeout interval, it is not accurate. For example, if we specify a timeout of 5 seconds, it may not timeout exactly at 5 Seconds. In my observation, it ranges from 5-10 seconds but I think it is better than no control at all