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