diff --git a/src/libraries/Common/src/Interop/Interop.Ldap.cs b/src/libraries/Common/src/Interop/Interop.Ldap.cs index 512242230093ff..986e72654992cc 100644 --- a/src/libraries/Common/src/Interop/Interop.Ldap.cs +++ b/src/libraries/Common/src/Interop/Interop.Ldap.cs @@ -156,6 +156,7 @@ internal enum LdapOption LDAP_OPT_SECURITY_CONTEXT = 0x99, LDAP_OPT_ROOTDSE_CACHE = 0x9a, // Not Supported in Linux LDAP_OPT_DEBUG_LEVEL = 0x5001, + LDAP_OPT_NETWORK_TIMEOUT = 0x5005, // Not Supported in Windows LDAP_OPT_URI = 0x5006, // Not Supported in Windows LDAP_OPT_X_TLS_CACERTDIR = 0x6003, // Not Supported in Windows LDAP_OPT_X_TLS_NEWCTX = 0x600F, // Not Supported in Windows diff --git a/src/libraries/Common/src/Interop/Linux/OpenLdap/Interop.Ldap.cs b/src/libraries/Common/src/Interop/Linux/OpenLdap/Interop.Ldap.cs index 7a49a0d85448c9..dcb97ac0d64f65 100644 --- a/src/libraries/Common/src/Interop/Linux/OpenLdap/Interop.Ldap.cs +++ b/src/libraries/Common/src/Interop/Linux/OpenLdap/Interop.Ldap.cs @@ -171,6 +171,9 @@ public static partial int ldap_search( [LibraryImport(Libraries.OpenLdap, EntryPoint = "ldap_set_option")] public static partial int ldap_set_option_referral(ConnectionHandle ldapHandle, LdapOption option, ref LdapReferralCallback outValue); + [LibraryImport(Libraries.OpenLdap, EntryPoint = "ldap_set_option")] + public static partial int ldap_set_option_timeval(ConnectionHandle ldapHandle, LdapOption option, ref LDAP_TIMEVAL inValue); + // Note that ldap_start_tls_s has a different signature across Windows LDAP and OpenLDAP [LibraryImport(Libraries.OpenLdap, EntryPoint = "ldap_start_tls_s")] public static partial int ldap_start_tls(ConnectionHandle ldapHandle, IntPtr serverControls, IntPtr clientControls); diff --git a/src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/Interop/LdapPal.Linux.cs b/src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/Interop/LdapPal.Linux.cs index a74bff68535120..5826cab2bb6402 100644 --- a/src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/Interop/LdapPal.Linux.cs +++ b/src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/Interop/LdapPal.Linux.cs @@ -115,6 +115,8 @@ internal static int SearchDirectory(ConnectionHandle ldapHandle, string dn, int internal static int SetReferralOption(ConnectionHandle ldapHandle, LdapOption option, ref LdapReferralCallback outValue) => Interop.Ldap.ldap_set_option_referral(ldapHandle, option, ref outValue); + internal static int SetTimevalOption(ConnectionHandle ldapHandle, LdapOption option, ref LDAP_TIMEVAL inValue) => Interop.Ldap.ldap_set_option_timeval(ldapHandle, option, ref inValue); + // This option is not supported in Linux, so it would most likely throw. internal static int SetServerCertOption(ConnectionHandle ldapHandle, LdapOption option, VERIFYSERVERCERT outValue) => Interop.Ldap.ldap_set_option_servercert(ldapHandle, option, outValue); diff --git a/src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/ldap/LdapConnection.Linux.cs b/src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/ldap/LdapConnection.Linux.cs index ba5d2aebb1af67..48c814225b3764 100644 --- a/src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/ldap/LdapConnection.Linux.cs +++ b/src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/ldap/LdapConnection.Linux.cs @@ -76,7 +76,21 @@ private int InternalConnectToServer() uris = $"{scheme}:{directoryIdentifier.PortNumber}"; } - return LdapPal.SetStringOption(_ldapHandle, LdapOption.LDAP_OPT_URI, uris); + int result = LdapPal.SetStringOption(_ldapHandle, LdapOption.LDAP_OPT_URI, uris); + if (result == 0) + { + // Set the network timeout option to honor the Timeout property + var timeout = new LDAP_TIMEVAL() + { + tv_sec = (int)(_connectionTimeOut.Ticks / TimeSpan.TicksPerSecond), + tv_usec = (int)((_connectionTimeOut.Ticks % TimeSpan.TicksPerSecond) / TimeSpan.TicksPerMicrosecond) // Convert 100ns ticks to microseconds + }; + + int timeoutResult = LdapPal.SetTimevalOption(_ldapHandle, LdapOption.LDAP_OPT_NETWORK_TIMEOUT, ref timeout); + ErrorChecking.CheckAndSetLdapError(timeoutResult); + } + + return result; } private int InternalBind(NetworkCredential tempCredential, SEC_WINNT_AUTH_IDENTITY_EX cred, BindMethod method) diff --git a/src/libraries/System.DirectoryServices.Protocols/tests/LdapConnectionTests.cs b/src/libraries/System.DirectoryServices.Protocols/tests/LdapConnectionTests.cs index bab7e8291f183f..6da23c32a0c537 100644 --- a/src/libraries/System.DirectoryServices.Protocols/tests/LdapConnectionTests.cs +++ b/src/libraries/System.DirectoryServices.Protocols/tests/LdapConnectionTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Net; +using System.Net.Sockets; using System.Threading; using Xunit; @@ -307,6 +308,56 @@ public void Dispose_MultipleTimes_Nop() connection.Dispose(); } + [Fact] + public void NetworkTimeout_Initialization_DoesNotThrow() + { + var connection = new LdapConnection("server") + { + Timeout = TimeSpan.FromSeconds(10) + }; + + Assert.Equal(TimeSpan.FromSeconds(10), connection.Timeout); + connection.Dispose(); + } + + [Fact] + public void NetworkTimeout_ZeroTimeout_DoesNotThrow() + { + var connection = new LdapConnection("server") + { + Timeout = TimeSpan.Zero + }; + + Assert.Equal(TimeSpan.Zero, connection.Timeout); + connection.Dispose(); + } + + [Fact] + public void NetworkTimeout_UnreachableServer_ThrowsTimeoutException() + { + // Use TEST-NET-1 address (192.0.2.x) which is reserved for documentation and testing + // and guaranteed to be unreachable, causing the connection to timeout + const string unreachableServer = "192.0.2.1"; + var connection = new LdapConnection(unreachableServer) + { + Timeout = TimeSpan.FromSeconds(2) // Short timeout to make test faster + }; + + try + { + // Attempt to bind should timeout due to unreachable server + var ex = Assert.ThrowsAny(() => connection.Bind()); + + // The exact exception type may vary by platform but should indicate a timeout/connection failure + Assert.True(ex is LdapException or TimeoutException or SocketException, + $"Expected LdapException, TimeoutException, or SocketException but got {ex.GetType().Name}"); + } + finally + { + connection.Dispose(); + } + } + public class CustomAsyncResult : IAsyncResult { public object AsyncState => throw new NotImplementedException();