< Summary

Class:SharpHoundCommonLib.LdapConnectionPool
Assembly:SharpHoundCommonLib
File(s):D:\a\SharpHoundCommon\SharpHoundCommon\src\CommonLib\LdapConnectionPool.cs
Covered lines:99
Uncovered lines:521
Coverable lines:620
Total lines:911
Line coverage:15.9% (99 of 620)
Covered branches:37
Total branches:364
Branch coverage:10.1% (37 of 364)

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.cctor()100%100%
.ctor(...)83.33%60100%
GetLdapConnection()33.33%6080%
Query()11.94%6707.44%
PagedQuery()0%7400%
SetupLdapQuery()25%8039.13%
RangedRetrieval()0%7700%
GetNextBackoff(...)100%100%
CreateSearchRequest(...)0%2200%
CallDsGetDcName(...)0%400%
GetConnectionAsync()0%600%
GetConnectionForSpecificServerAsync(...)100%100%
GetGlobalCatalogConnectionAsync()50%6066.66%
ReleaseConnection(...)0%400%
Dispose()0%200%
CreateNewConnection()15.62%32024.19%
CreateNewConnectionForServer(...)0%200%
CreateLdapConnection(...)50%6060%
CreateBaseConnection(...)70%10084%
TestLdapConnection(...)20%10032.25%
CreateLDAPConnectionWithPortCheck()0%2200%
CreateSearchRequest(...)100%100%

File(s)

D:\a\SharpHoundCommon\SharpHoundCommon\src\CommonLib\LdapConnectionPool.cs

#LineLine coverage
 1using System;
 2using System.Collections.Concurrent;
 3using System.Collections.Generic;
 4using System.DirectoryServices.ActiveDirectory;
 5using System.DirectoryServices.Protocols;
 6using System.Linq;
 7using System.Net;
 8using System.Runtime.CompilerServices;
 9using System.Threading;
 10using System.Threading.Tasks;
 11using Microsoft.Extensions.Logging;
 12using SharpHoundCommonLib.Enums;
 13using SharpHoundCommonLib.Exceptions;
 14using SharpHoundCommonLib.LDAPQueries;
 15using SharpHoundCommonLib.Processors;
 16using SharpHoundRPC.NetAPINative;
 17
 18namespace SharpHoundCommonLib {
 19    internal class LdapConnectionPool : IDisposable{
 20        private readonly ConcurrentBag<LdapConnectionWrapper> _connections;
 21        private readonly ConcurrentBag<LdapConnectionWrapper> _globalCatalogConnection;
 22        private readonly SemaphoreSlim _semaphore;
 23        private readonly string _identifier;
 24        private readonly string _poolIdentifier;
 25        private readonly LdapConfig _ldapConfig;
 26        private readonly ILogger _log;
 27        private readonly PortScanner _portScanner;
 28        private readonly NativeMethods _nativeMethods;
 029        private static readonly TimeSpan MinBackoffDelay = TimeSpan.FromSeconds(2);
 030        private static readonly TimeSpan MaxBackoffDelay = TimeSpan.FromSeconds(20);
 31        private const int BackoffDelayMultiplier = 2;
 32        private const int MaxRetries = 3;
 033        private static readonly ConcurrentDictionary<string, NetAPIStructs.DomainControllerInfo?> DCInfoCache = new();
 34
 235        public LdapConnectionPool(string identifier, string poolIdentifier, LdapConfig config, PortScanner scanner = nul
 136            _connections = new ConcurrentBag<LdapConnectionWrapper>();
 137            _globalCatalogConnection = new ConcurrentBag<LdapConnectionWrapper>();
 38            //TODO: Re-enable this once we track down the semaphore deadlock
 39            // if (config.MaxConcurrentQueries > 0) {
 40            //     _semaphore = new SemaphoreSlim(config.MaxConcurrentQueries, config.MaxConcurrentQueries);
 41            // } else {
 42            //     //If MaxConcurrentQueries is 0, we'll just disable the semaphore entirely
 43            //     _semaphore = null;
 44            // }
 45
 146            _identifier = identifier;
 147            _poolIdentifier = poolIdentifier;
 148            _ldapConfig = config;
 149            _log = log ?? Logging.LogProvider.CreateLogger("LdapConnectionPool");
 150            _portScanner = scanner ?? new PortScanner();
 151            _nativeMethods = nativeMethods ?? new NativeMethods();
 152        }
 53
 154        private async Task<(bool Success, LdapConnectionWrapper ConnectionWrapper, string Message)> GetLdapConnection(bo
 255            if (globalCatalog) {
 156                return await GetGlobalCatalogConnectionAsync();
 57            }
 058            return await GetConnectionAsync();
 159        }
 60
 61        public async IAsyncEnumerable<LdapResult<IDirectoryObject>> Query(LdapQueryParameters queryParameters,
 162            [EnumeratorCancellation] CancellationToken cancellationToken = new()) {
 163            var setupResult = await SetupLdapQuery(queryParameters);
 64
 265            if (!setupResult.Success) {
 166                _log.LogInformation("Query - Failure during query setup: {Reason}\n{Info}", setupResult.Message,
 167                    queryParameters.GetQueryInfo());
 168                yield break;
 69            }
 70
 071            var searchRequest = setupResult.SearchRequest;
 072            var connectionWrapper = setupResult.ConnectionWrapper;
 73
 074            if (cancellationToken.IsCancellationRequested) {
 075                ReleaseConnection(connectionWrapper);
 076                yield break;
 77            }
 78
 079            var queryRetryCount = 0;
 080            var busyRetryCount = 0;
 081            LdapResult<IDirectoryObject> tempResult = null;
 082            var querySuccess = false;
 083            SearchResponse response = null;
 084            while (!cancellationToken.IsCancellationRequested) {
 85                //Grab our semaphore here to take one of our query slots
 086                if (_semaphore != null){
 087                    _log.LogTrace("Query entering semaphore with {Count} remaining for query {Info}", _semaphore.Current
 088                    await _semaphore.WaitAsync(cancellationToken);
 089                    _log.LogTrace("Query entered semaphore with {Count} remaining for query {Info}", _semaphore.CurrentC
 090                }
 091                try {
 092                    _log.LogTrace("Sending ldap request - {Info}", queryParameters.GetQueryInfo());
 093                    response = (SearchResponse)connectionWrapper.Connection.SendRequest(searchRequest);
 94
 095                    if (response != null) {
 096                        querySuccess = true;
 097                    } else if (queryRetryCount == MaxRetries) {
 098                        tempResult =
 099                            LdapResult<IDirectoryObject>.Fail($"Failed to get a response after {MaxRetries} attempts",
 0100                                queryParameters);
 0101                    } else {
 0102                        queryRetryCount++;
 0103                    }
 0104                } catch (LdapException le) when (le.ErrorCode == (int)LdapErrorCodes.ServerDown &&
 0105                                                 queryRetryCount < MaxRetries) {
 106                    /*
 107                     * A ServerDown exception indicates that our connection is no longer valid for one of many reasons.
 108                     * We'll want to release our connection back to the pool, but dispose it. We need a new connection,
 109                     * and because this is not a paged query, we can get this connection from anywhere.
 110                     *
 111                     * We use queryRetryCount here to prevent an infinite retry loop from occurring
 112                     *
 113                     * Release our connection in a faulted state since the connection is defunct. Attempt to get a new c
 114                     * since non-paged queries do not require same server connections
 115                     */
 0116                    queryRetryCount++;
 0117                    _log.LogDebug("Query - Attempting to recover from ServerDown for query {Info} (Attempt {Count})", qu
 0118                    ReleaseConnection(connectionWrapper, true);
 119
 0120                    for (var retryCount = 0; retryCount < MaxRetries; retryCount++) {
 0121                        var backoffDelay = GetNextBackoff(retryCount);
 0122                        await Task.Delay(backoffDelay, cancellationToken);
 0123                        var (success, newConnectionWrapper, _) =
 0124                            await GetLdapConnection(queryParameters.GlobalCatalog);
 0125                        if (success) {
 0126                            _log.LogDebug(
 0127                                "Query - Recovered from ServerDown successfully, connection made to {NewServer}",
 0128                                newConnectionWrapper.GetServer());
 0129                            connectionWrapper = newConnectionWrapper;
 0130                            break;
 131                        }
 132
 133                        //If we hit our max retries for making a new connection, set tempResult so we can yield it after
 0134                        if (retryCount == MaxRetries - 1) {
 0135                            _log.LogError("Query - Failed to get a new connection after ServerDown.\n{Info}",
 0136                                queryParameters.GetQueryInfo());
 0137                            tempResult =
 0138                                LdapResult<IDirectoryObject>.Fail(
 0139                                    "Query - Failed to get a new connection after ServerDown.", queryParameters);
 0140                        }
 0141                    }
 0142                } catch (LdapException le) when (le.ErrorCode == (int)ResultCode.Busy && busyRetryCount < MaxRetries) {
 143                    /*
 144                     * If we get a busy error, we want to do an exponential backoff, but maintain the current connection
 145                     * The expectation is that given enough time, the server should stop being busy and service our quer
 146                     */
 0147                    busyRetryCount++;
 0148                    _log.LogDebug("Query - Executing busy backoff for query {Info} (Attempt {Count})", queryParameters.G
 0149                    var backoffDelay = GetNextBackoff(busyRetryCount);
 0150                    await Task.Delay(backoffDelay, cancellationToken);
 0151                } catch (LdapException le) {
 152                    /*
 153                     * This is our fallback catch. If our retry counts have been exhausted this will trigger and break u
 154                     */
 0155                    tempResult = LdapResult<IDirectoryObject>.Fail(
 0156                        $"Query - Caught unrecoverable ldap exception: {le.Message} (ServerMessage: {le.ServerErrorMessa
 0157                        queryParameters);
 0158                } catch (Exception e) {
 159                    /*
 160                     * Generic exception handling for unforeseen circumstances
 161                     */
 0162                    tempResult =
 0163                        LdapResult<IDirectoryObject>.Fail($"Query - Caught unrecoverable exception: {e.Message}",
 0164                            queryParameters);
 0165                } finally {
 166                    // Always release our semaphore to prevent deadlocks
 0167                    if (_semaphore != null) {
 0168                        _log.LogTrace("Query releasing semaphore with {Count} remaining for query {Info}", _semaphore.Cu
 0169                        _semaphore.Release();
 0170                        _log.LogTrace("Query released semaphore with {Count} remaining for query {Info}", _semaphore.Cur
 0171                    }
 0172                }
 173
 174                //If we have a tempResult set it means we hit an error we couldn't recover from, so yield that result an
 0175                if (tempResult != null) {
 0176                    if (tempResult.ErrorCode == (int)LdapErrorCodes.ServerDown) {
 0177                        ReleaseConnection(connectionWrapper, true);
 0178                    } else {
 0179                        ReleaseConnection(connectionWrapper);
 0180                    }
 181
 0182                    yield return tempResult;
 0183                    yield break;
 184                }
 185
 186                //If we've successfully made our query, break out of the while loop
 0187                if (querySuccess) {
 0188                    break;
 189                }
 0190            }
 191
 0192            ReleaseConnection(connectionWrapper);
 0193            foreach (SearchResultEntry entry in response.Entries) {
 0194                yield return LdapResult<IDirectoryObject>.Ok(new SearchResultEntryWrapper(entry));
 0195            }
 1196        }
 197
 198        public async IAsyncEnumerable<LdapResult<IDirectoryObject>> PagedQuery(LdapQueryParameters queryParameters,
 0199            [EnumeratorCancellation] CancellationToken cancellationToken = new()) {
 0200            var setupResult = await SetupLdapQuery(queryParameters);
 201
 0202            if (!setupResult.Success) {
 0203                _log.LogInformation("PagedQuery - Failure during query setup: {Reason}\n{Info}", setupResult.Message,
 0204                    queryParameters.GetQueryInfo());
 0205                yield break;
 206            }
 207
 0208            var searchRequest = setupResult.SearchRequest;
 0209            var connectionWrapper = setupResult.ConnectionWrapper;
 0210            var serverName = setupResult.Server;
 211
 0212            if (serverName == null) {
 0213                _log.LogWarning("PagedQuery - Failed to get a server name for connection, retry not possible");
 0214            }
 215
 0216            var pageControl = new PageResultRequestControl(500);
 0217            searchRequest.Controls.Add(pageControl);
 218
 0219            PageResultResponseControl pageResponse = null;
 0220            var busyRetryCount = 0;
 0221            var queryRetryCount = 0;
 0222            LdapResult<IDirectoryObject> tempResult = null;
 223
 0224            while (!cancellationToken.IsCancellationRequested) {
 0225                if (_semaphore != null){
 0226                    _log.LogTrace("PagedQuery entering semaphore with {Count} remaining for query {Info}", _semaphore.Cu
 0227                    await _semaphore.WaitAsync(cancellationToken);
 0228                    _log.LogTrace("PagedQuery entered semaphore with {Count} remaining for query {Info}", _semaphore.Cur
 0229                }
 0230                SearchResponse response = null;
 0231                try {
 0232                    _log.LogTrace("Sending paged ldap request - {Info}", queryParameters.GetQueryInfo());
 0233                    response = (SearchResponse)connectionWrapper.Connection.SendRequest(searchRequest);
 0234                    if (response != null) {
 0235                        pageResponse = (PageResultResponseControl)response.Controls
 0236                            .Where(x => x is PageResultResponseControl).DefaultIfEmpty(null).FirstOrDefault();
 0237                        queryRetryCount = 0;
 0238                    } else if (queryRetryCount == MaxRetries) {
 0239                        tempResult = LdapResult<IDirectoryObject>.Fail(
 0240                            $"PagedQuery - Failed to get a response after {MaxRetries} attempts",
 0241                            queryParameters);
 0242                    } else {
 0243                        queryRetryCount++;
 0244                    }
 0245                } catch (LdapException le) when (le.ErrorCode == (int)LdapErrorCodes.ServerDown) {
 246                    /*
 247                    * A ServerDown exception indicates that our connection is no longer valid for one of many reasons.
 248                    * We'll want to release our connection back to the pool, but dispose it. We need a new connection,
 249                    * and because this is not a paged query, we can get this connection from anywhere.
 250                    *
 251                    * We use queryRetryCount here to prevent an infinite retry loop from occurring
 252                    *
 253                    * Release our connection in a faulted state since the connection is defunct.
 254                    * Paged queries require a connection to be made to the same server which we started the paged query 
 255                    */
 0256                    if (serverName == null) {
 0257                        _log.LogError(
 0258                            "PagedQuery - Received server down exception without a known servername. Unable to generate 
 0259                            queryParameters.GetQueryInfo());
 0260                        ReleaseConnection(connectionWrapper, true);
 0261                        yield break;
 262                    }
 263
 0264                    _log.LogDebug("PagedQuery - Attempting to recover from ServerDown for query {Info} (Attempt {Count})
 265
 0266                    ReleaseConnection(connectionWrapper, true);
 0267                    for (var retryCount = 0; retryCount < MaxRetries; retryCount++) {
 0268                        var backoffDelay = GetNextBackoff(retryCount);
 0269                        await Task.Delay(backoffDelay, cancellationToken);
 0270                        var (success, ldapConnectionWrapperNew, _) =
 0271                            GetConnectionForSpecificServerAsync(serverName, queryParameters.GlobalCatalog);
 272
 0273                        if (success) {
 0274                            _log.LogDebug("PagedQuery - Recovered from ServerDown successfully");
 0275                            connectionWrapper = ldapConnectionWrapperNew;
 0276                            break;
 277                        }
 278
 0279                        if (retryCount == MaxRetries - 1) {
 0280                            _log.LogError("PagedQuery - Failed to get a new connection after ServerDown.\n{Info}",
 0281                                queryParameters.GetQueryInfo());
 0282                            tempResult =
 0283                                LdapResult<IDirectoryObject>.Fail("Failed to get a new connection after serverdown",
 0284                                    queryParameters, le.ErrorCode);
 0285                        }
 0286                    }
 0287                } catch (LdapException le) when (le.ErrorCode == (int)ResultCode.Busy && busyRetryCount < MaxRetries) {
 288                    /*
 289                     * If we get a busy error, we want to do an exponential backoff, but maintain the current connection
 290                     * The expectation is that given enough time, the server should stop being busy and service our quer
 291                     */
 0292                    busyRetryCount++;
 0293                    _log.LogDebug("PagedQuery - Executing busy backoff for query {Info} (Attempt {Count})", queryParamet
 0294                    var backoffDelay = GetNextBackoff(busyRetryCount);
 0295                    await Task.Delay(backoffDelay, cancellationToken);
 0296                } catch (LdapException le) {
 0297                    tempResult = LdapResult<IDirectoryObject>.Fail(
 0298                        $"PagedQuery - Caught unrecoverable ldap exception: {le.Message} (ServerMessage: {le.ServerError
 0299                        queryParameters, le.ErrorCode);
 0300                } catch (Exception e) {
 0301                    tempResult =
 0302                        LdapResult<IDirectoryObject>.Fail($"PagedQuery - Caught unrecoverable exception: {e.Message}",
 0303                            queryParameters);
 0304                } finally {
 0305                    if (_semaphore != null) {
 0306                        _log.LogTrace("PagedQuery releasing semaphore with {Count} remaining for query {Info}", _semapho
 0307                        _semaphore.Release();
 0308                        _log.LogTrace("PagedQuery released semaphore with {Count} remaining for query {Info}", _semaphor
 0309                    }
 0310                }
 311
 0312                if (tempResult != null) {
 0313                    if (tempResult.ErrorCode == (int)LdapErrorCodes.ServerDown) {
 0314                        ReleaseConnection(connectionWrapper, true);
 0315                    } else {
 0316                        ReleaseConnection(connectionWrapper);
 0317                    }
 318
 0319                    yield return tempResult;
 0320                    yield break;
 321                }
 322
 0323                if (cancellationToken.IsCancellationRequested) {
 0324                    ReleaseConnection(connectionWrapper);
 0325                    yield break;
 326                }
 327
 328                //I'm not sure why this happens sometimes, but if we try the request again, it works sometimes, other ti
 0329                if (response == null || pageResponse == null) {
 0330                    continue;
 331                }
 332
 0333                foreach (SearchResultEntry entry in response.Entries) {
 0334                    if (cancellationToken.IsCancellationRequested) {
 0335                        ReleaseConnection(connectionWrapper);
 0336                        yield break;
 337                    }
 338
 0339                    yield return LdapResult<IDirectoryObject>.Ok(new SearchResultEntryWrapper(entry));
 0340                }
 341
 0342                if (pageResponse.Cookie.Length == 0 || response.Entries.Count == 0 ||
 0343                    cancellationToken.IsCancellationRequested) {
 0344                    ReleaseConnection(connectionWrapper);
 0345                    yield break;
 346                }
 347
 0348                pageControl.Cookie = pageResponse.Cookie;
 0349            }
 0350        }
 351
 1352        private async Task<LdapQuerySetupResult> SetupLdapQuery(LdapQueryParameters queryParameters) {
 1353            var result = new LdapQuerySetupResult();
 1354            var (success, connectionWrapper, message) =
 1355                await GetLdapConnection(queryParameters.GlobalCatalog);
 2356            if (!success) {
 1357                result.Success = false;
 1358                result.Message = $"Unable to create a connection: {message}";
 1359                return result;
 360            }
 361
 362            //This should never happen as far as I know, so just checking for safety
 0363            if (connectionWrapper.Connection == null) {
 0364                result.Success = false;
 0365                result.Message = "Connection object is null";
 0366                return result;
 367            }
 368
 0369            if (!CreateSearchRequest(queryParameters, connectionWrapper, out var searchRequest)) {
 0370                result.Success = false;
 0371                result.Message = "Failed to create search request";
 0372                ReleaseConnection(connectionWrapper);
 0373                return result;
 374            }
 375
 0376            result.Server = connectionWrapper.GetServer();
 0377            result.Success = true;
 0378            result.SearchRequest = searchRequest;
 0379            result.ConnectionWrapper = connectionWrapper;
 0380            return result;
 1381        }
 382
 383        public async IAsyncEnumerable<Result<string>> RangedRetrieval(string distinguishedName,
 0384            string attributeName, [EnumeratorCancellation] CancellationToken cancellationToken = new()) {
 0385            var domain = Helpers.DistinguishedNameToDomain(distinguishedName);
 386
 0387            var connectionResult = await GetConnectionAsync();
 0388            if (!connectionResult.Success) {
 0389                yield return Result<string>.Fail(connectionResult.Message);
 0390                yield break;
 391            }
 392
 0393            var index = 0;
 0394            var step = 0;
 395
 396            //Start by using * as our upper index, which will automatically give us the range size
 0397            var currentRange = $"{attributeName};range={index}-*";
 0398            var complete = false;
 399
 0400            var queryParameters = new LdapQueryParameters {
 0401                DomainName = domain,
 0402                LDAPFilter = $"{attributeName}=*",
 0403                Attributes = new[] { currentRange },
 0404                SearchScope = SearchScope.Base,
 0405                SearchBase = distinguishedName
 0406            };
 0407            var connectionWrapper = connectionResult.ConnectionWrapper;
 408
 0409            if (!CreateSearchRequest(queryParameters, connectionWrapper, out var searchRequest)) {
 0410                ReleaseConnection(connectionWrapper);
 0411                yield return Result<string>.Fail("Failed to create search request");
 0412                yield break;
 413            }
 414
 0415            var queryRetryCount = 0;
 0416            var busyRetryCount = 0;
 417
 0418            LdapResult<string> tempResult = null;
 419
 0420            while (!cancellationToken.IsCancellationRequested) {
 0421                SearchResponse response = null;
 0422                if (_semaphore != null){
 0423                    _log.LogTrace("RangedRetrieval entering semaphore with {Count} remaining for query {Info}", _semapho
 0424                    await _semaphore.WaitAsync(cancellationToken);
 0425                    _log.LogTrace("RangedRetrieval entered semaphore with {Count} remaining for query {Info}", _semaphor
 0426                }
 0427                try {
 0428                    response = (SearchResponse)connectionWrapper.Connection.SendRequest(searchRequest);
 0429                } catch (LdapException le) when (le.ErrorCode == (int)ResultCode.Busy && busyRetryCount < MaxRetries) {
 0430                    busyRetryCount++;
 0431                    _log.LogDebug("RangedRetrieval - Executing busy backoff for query {Info} (Attempt {Count})", queryPa
 0432                    var backoffDelay = GetNextBackoff(busyRetryCount);
 0433                    await Task.Delay(backoffDelay, cancellationToken);
 0434                } catch (LdapException le) when (le.ErrorCode == (int)LdapErrorCodes.ServerDown &&
 0435                                                 queryRetryCount < MaxRetries) {
 0436                    queryRetryCount++;
 0437                    _log.LogDebug("RangedRetrieval - Attempting to recover from ServerDown for query {Info} (Attempt {Co
 0438                    ReleaseConnection(connectionWrapper, true);
 0439                    for (var retryCount = 0; retryCount < MaxRetries; retryCount++) {
 0440                        var backoffDelay = GetNextBackoff(retryCount);
 0441                        await Task.Delay(backoffDelay, cancellationToken);
 0442                        var (success, newConnectionWrapper, message) =
 0443                            await GetLdapConnection(false);
 0444                        if (success) {
 0445                            _log.LogDebug(
 0446                                "RangedRetrieval - Recovered from ServerDown successfully, connection made to {NewServer
 0447                                newConnectionWrapper.GetServer());
 0448                            connectionWrapper = newConnectionWrapper;
 0449                            break;
 450                        }
 451
 452                        //If we hit our max retries for making a new connection, set tempResult so we can yield it after
 0453                        if (retryCount == MaxRetries - 1) {
 0454                            _log.LogError(
 0455                                "RangedRetrieval - Failed to get a new connection after ServerDown for path {Path}",
 0456                                distinguishedName);
 0457                            tempResult =
 0458                                LdapResult<string>.Fail(
 0459                                    "RangedRetrieval - Failed to get a new connection after ServerDown.",
 0460                                    queryParameters, le.ErrorCode);
 0461                        }
 0462                    }
 0463                } catch (LdapException le) {
 0464                    tempResult = LdapResult<string>.Fail(
 0465                        $"Caught unrecoverable ldap exception: {le.Message} (ServerMessage: {le.ServerErrorMessage}) (Er
 0466                        queryParameters, le.ErrorCode);
 0467                } catch (Exception e) {
 0468                    tempResult =
 0469                        LdapResult<string>.Fail($"Caught unrecoverable exception: {e.Message}", queryParameters);
 0470                } finally {
 0471                    if (_semaphore != null) {
 0472                        _log.LogTrace("RangedRetrieval releasing semaphore with {Count} remaining for query {Info}", _se
 0473                        _semaphore.Release();
 0474                        _log.LogTrace("RangedRetrieval released semaphore with {Count} remaining for query {Info}", _sem
 0475                    }
 0476                }
 477
 478                //If we have a tempResult set it means we hit an error we couldn't recover from, so yield that result an
 479                //We handle connection release in the relevant exception blocks
 0480                if (tempResult != null) {
 0481                    if (tempResult.ErrorCode == (int)LdapErrorCodes.ServerDown) {
 0482                        ReleaseConnection(connectionWrapper, true);
 0483                    } else {
 0484                        ReleaseConnection(connectionWrapper);
 0485                    }
 486
 0487                    yield return tempResult;
 0488                    yield break;
 489                }
 490
 0491                if (response?.Entries.Count == 1) {
 0492                    var entry = response.Entries[0];
 493                    //We dont know the name of our attribute, but there should only be one, so we're safe to just use a 
 0494                    foreach (string attr in entry.Attributes.AttributeNames) {
 0495                        currentRange = attr;
 0496                        complete = currentRange.IndexOf("*", 0, StringComparison.OrdinalIgnoreCase) > 0;
 0497                        step = entry.Attributes[currentRange].Count;
 0498                    }
 499
 500                    //Release our connection before we iterate
 0501                    if (complete) {
 0502                        ReleaseConnection(connectionWrapper);
 0503                    }
 504
 0505                    foreach (string dn in entry.Attributes[currentRange].GetValues(typeof(string))) {
 0506                        yield return Result<string>.Ok(dn);
 0507                        index++;
 0508                    }
 509
 0510                    if (complete) {
 0511                        yield break;
 512                    }
 513
 0514                    currentRange = $"{attributeName};range={index}-{index + step}";
 0515                    searchRequest.Attributes.Clear();
 0516                    searchRequest.Attributes.Add(currentRange);
 0517                } else {
 518                    //I dont know what can cause a RR to have multiple entries, but its nothing good. Break out
 0519                    ReleaseConnection(connectionWrapper);
 0520                    yield break;
 521                }
 0522            }
 523
 0524            ReleaseConnection(connectionWrapper);
 0525        }
 526
 0527        private static TimeSpan GetNextBackoff(int retryCount) {
 0528            return TimeSpan.FromSeconds(Math.Min(
 0529                MinBackoffDelay.TotalSeconds * Math.Pow(BackoffDelayMultiplier, retryCount),
 0530                MaxBackoffDelay.TotalSeconds));
 0531        }
 532
 533        private bool CreateSearchRequest(LdapQueryParameters queryParameters,
 0534            LdapConnectionWrapper connectionWrapper, out SearchRequest searchRequest) {
 535            string basePath;
 0536            if (!string.IsNullOrWhiteSpace(queryParameters.SearchBase)) {
 0537                basePath = queryParameters.SearchBase;
 0538            } else if (!connectionWrapper.GetSearchBase(queryParameters.NamingContext, out basePath)) {
 539                string tempPath;
 0540                if (CallDsGetDcName(queryParameters.DomainName, out var info) && info != null) {
 0541                    tempPath = Helpers.DomainNameToDistinguishedName(info.Value.DomainName);
 0542                    connectionWrapper.SaveContext(queryParameters.NamingContext, basePath);
 0543                } else if (LdapUtils.GetDomain(queryParameters.DomainName,_ldapConfig,  out var domainObject)) {
 0544                    tempPath = Helpers.DomainNameToDistinguishedName(domainObject.Name);
 0545                } else {
 0546                    searchRequest = null;
 0547                    return false;
 548                }
 549
 0550                basePath = queryParameters.NamingContext switch {
 0551                    NamingContext.Configuration => $"CN=Configuration,{tempPath}",
 0552                    NamingContext.Schema => $"CN=Schema,CN=Configuration,{tempPath}",
 0553                    NamingContext.Default => tempPath,
 0554                    _ => throw new ArgumentOutOfRangeException()
 0555                };
 556
 0557                connectionWrapper.SaveContext(queryParameters.NamingContext, basePath);
 0558            }
 559
 0560            if (string.IsNullOrWhiteSpace(queryParameters.SearchBase) && !string.IsNullOrWhiteSpace(queryParameters.Rela
 0561                basePath = $"{queryParameters.RelativeSearchBase},{basePath}";
 0562            }
 563
 0564            searchRequest = new SearchRequest(basePath, queryParameters.LDAPFilter, queryParameters.SearchScope,
 0565                queryParameters.Attributes);
 0566            searchRequest.Controls.Add(new SearchOptionsControl(SearchOption.DomainScope));
 0567            if (queryParameters.IncludeDeleted) {
 0568                searchRequest.Controls.Add(new ShowDeletedControl());
 0569            }
 570
 0571            if (queryParameters.IncludeSecurityDescriptor) {
 0572                searchRequest.Controls.Add(new SecurityDescriptorFlagControl {
 0573                    SecurityMasks = SecurityMasks.Dacl | SecurityMasks.Owner
 0574                });
 0575            }
 576
 0577            return true;
 0578        }
 579
 0580        private bool CallDsGetDcName(string domainName, out NetAPIStructs.DomainControllerInfo? info) {
 0581            if (DCInfoCache.TryGetValue(domainName.ToUpper().Trim(), out info)) return info != null;
 582
 0583            var apiResult = _nativeMethods.CallDsGetDcName(null, domainName,
 0584                (uint)(NetAPIEnums.DSGETDCNAME_FLAGS.DS_FORCE_REDISCOVERY |
 0585                       NetAPIEnums.DSGETDCNAME_FLAGS.DS_RETURN_DNS_NAME |
 0586                       NetAPIEnums.DSGETDCNAME_FLAGS.DS_DIRECTORY_SERVICE_REQUIRED));
 587
 0588            if (apiResult.IsFailed) {
 0589                DCInfoCache.TryAdd(domainName.ToUpper().Trim(), null);
 0590                return false;
 591            }
 592
 0593            info = apiResult.Value;
 0594            return true;
 0595        }
 596
 0597        public async Task<(bool Success, LdapConnectionWrapper ConnectionWrapper, string Message)> GetConnectionAsync() 
 0598            if (!_connections.TryTake(out var connectionWrapper)) {
 0599                var (success, connection, message) = await CreateNewConnection();
 0600                if (!success) {
 0601                    return (false, null, message);
 602                }
 603
 0604                connectionWrapper = connection;
 0605            }
 606
 0607            return (true, connectionWrapper, null);
 0608        }
 609
 610        public (bool Success, LdapConnectionWrapper connectionWrapper, string Message)
 0611            GetConnectionForSpecificServerAsync(string server, bool globalCatalog) {
 0612            return CreateNewConnectionForServer(server, globalCatalog);
 0613        }
 614
 1615        public async Task<(bool Success, LdapConnectionWrapper ConnectionWrapper, string Message)> GetGlobalCatalogConne
 2616            if (!_globalCatalogConnection.TryTake(out var connectionWrapper)) {
 1617                var (success, connection, message) = await CreateNewConnection(true);
 2618                if (!success) {
 619                    //If we didn't get a connection, immediately release the semaphore so we don't have hanging ones
 1620                    return (false, null, message);
 621                }
 622
 0623                connectionWrapper = connection;
 0624            }
 625
 0626            return (true, connectionWrapper, null);
 1627        }
 628
 0629        public void ReleaseConnection(LdapConnectionWrapper connectionWrapper, bool connectionFaulted = false) {
 0630            if (!connectionFaulted) {
 0631                if (connectionWrapper.GlobalCatalog) {
 0632                    _globalCatalogConnection.Add(connectionWrapper);
 0633                }
 0634                else {
 0635                    _connections.Add(connectionWrapper);
 0636                }
 0637            }
 0638            else {
 0639                connectionWrapper.Connection.Dispose();
 0640            }
 0641        }
 642
 0643        public void Dispose() {
 0644            while (_connections.TryTake(out var wrapper)) {
 0645                wrapper.Connection.Dispose();
 0646            }
 0647        }
 648
 1649        private async Task<(bool Success, LdapConnectionWrapper Connection, string Message)> CreateNewConnection(bool gl
 1650            try {
 1651                if (!string.IsNullOrWhiteSpace(_ldapConfig.Server)) {
 0652                    return CreateNewConnectionForServer(_ldapConfig.Server, globalCatalog);
 653                }
 654
 1655                if (CreateLdapConnection(_identifier.ToUpper().Trim(), globalCatalog, out var connectionWrapper)) {
 0656                    _log.LogDebug("Successfully created ldap connection for domain: {Domain} using strategy 1. SSL: {SSl
 0657                    return (true, connectionWrapper, "");
 658                }
 659
 660                string tempDomainName;
 661
 1662                var dsGetDcNameResult = _nativeMethods.CallDsGetDcName(null, _identifier,
 1663                    (uint)(NetAPIEnums.DSGETDCNAME_FLAGS.DS_FORCE_REDISCOVERY |
 1664                        NetAPIEnums.DSGETDCNAME_FLAGS.DS_RETURN_DNS_NAME |
 1665                        NetAPIEnums.DSGETDCNAME_FLAGS.DS_DIRECTORY_SERVICE_REQUIRED));
 1666                if (dsGetDcNameResult.IsSuccess) {
 0667                    tempDomainName = dsGetDcNameResult.Value.DomainName;
 668
 0669                    if (!tempDomainName.Equals(_identifier, StringComparison.OrdinalIgnoreCase) &&
 0670                        CreateLdapConnection(tempDomainName, globalCatalog, out connectionWrapper)) {
 0671                        _log.LogDebug(
 0672                            "Successfully created ldap connection for domain: {Domain} using strategy 2 with name {NewNa
 0673                            _identifier, tempDomainName);
 0674                        return (true, connectionWrapper, "");
 675                    }
 676
 0677                    var server = dsGetDcNameResult.Value.DomainControllerName.TrimStart('\\');
 678
 0679                    var result =
 0680                        await CreateLDAPConnectionWithPortCheck(server, globalCatalog);
 0681                    if (result.success) {
 0682                        _log.LogDebug(
 0683                            "Successfully created ldap connection for domain: {Domain} using strategy 3 to server {Serve
 0684                            _identifier, server);
 0685                        return (true, result.connection, "");
 686                    }
 0687                }
 688
 2689                if (!LdapUtils.GetDomain(_identifier, _ldapConfig, out var domainObject) || domainObject.Name == null) {
 690                    //If we don't get a result here, we effectively have no other ways to resolve this domain, so we'll 
 1691                    _log.LogDebug(
 1692                        "Could not get domain object from GetDomain, unable to create ldap connection for domain {Domain
 1693                        _identifier);
 1694                    return (false, null, "Unable to get domain object for further strategies");
 695                }
 0696                tempDomainName = domainObject.Name.ToUpper().Trim();
 697
 0698                if (!tempDomainName.Equals(_identifier, StringComparison.OrdinalIgnoreCase) &&
 0699                    CreateLdapConnection(tempDomainName, globalCatalog, out connectionWrapper)) {
 0700                    _log.LogDebug(
 0701                        "Successfully created ldap connection for domain: {Domain} using strategy 4 with name {NewName}"
 0702                        _identifier, tempDomainName);
 0703                    return (true, connectionWrapper, "");
 704                }
 705
 0706                var primaryDomainController = domainObject.PdcRoleOwner.Name;
 0707                var portConnectionResult =
 0708                    await CreateLDAPConnectionWithPortCheck(primaryDomainController, globalCatalog);
 0709                if (portConnectionResult.success) {
 0710                    _log.LogDebug(
 0711                        "Successfully created ldap connection for domain: {Domain} using strategy 5 with to pdc {Server}
 0712                        _identifier, primaryDomainController);
 0713                    return (true, portConnectionResult.connection, "");
 714                }
 715
 0716                foreach (DomainController dc in domainObject.DomainControllers) {
 0717                    portConnectionResult =
 0718                        await CreateLDAPConnectionWithPortCheck(dc.Name, globalCatalog);
 0719                    if (portConnectionResult.success) {
 0720                        _log.LogDebug(
 0721                            "Successfully created ldap connection for domain: {Domain} using strategy 6 with to pdc {Ser
 0722                            _identifier, primaryDomainController);
 0723                        return (true, portConnectionResult.connection, "");
 724                    }
 0725                }
 0726            } catch (Exception e) {
 0727                _log.LogInformation(e, "We will not be able to connect to domain {Domain} by any strategy, leaving it.",
 0728            }
 729
 0730            return (false, null, "All attempted connections failed");
 1731        }
 732
 0733        private (bool Success, LdapConnectionWrapper Connection, string Message ) CreateNewConnectionForServer(string id
 0734            if (CreateLdapConnection(identifier, globalCatalog, out var serverConnection)) {
 0735                return (true, serverConnection, "");
 736            }
 737
 0738            return (false, null, $"Failed to create ldap connection for {identifier}");
 0739        }
 740
 741        private bool CreateLdapConnection(string target, bool globalCatalog,
 1742            out LdapConnectionWrapper connection) {
 1743            var baseConnection = CreateBaseConnection(target, true, globalCatalog);
 1744            if (TestLdapConnection(baseConnection, out var result)) {
 0745                connection = new LdapConnectionWrapper(baseConnection, result.SearchResultEntry, globalCatalog, _poolIde
 0746                return true;
 747            }
 748
 1749            try {
 1750                baseConnection.Dispose();
 1751            }
 0752            catch {
 753                //this is just in case
 0754            }
 755
 1756            if (_ldapConfig.ForceSSL) {
 0757                connection = null;
 0758                return false;
 759            }
 760
 1761            baseConnection = CreateBaseConnection(target, false, globalCatalog);
 1762            if (TestLdapConnection(baseConnection, out result)) {
 0763                connection = new LdapConnectionWrapper(baseConnection, result.SearchResultEntry, globalCatalog, _poolIde
 0764                return true;
 765            }
 766
 1767            try {
 1768                baseConnection.Dispose();
 1769            }
 0770            catch {
 771                //this is just in case
 0772            }
 773
 1774            connection = null;
 1775            return false;
 1776        }
 777
 778        private LdapConnection CreateBaseConnection(string directoryIdentifier, bool ssl,
 2779            bool globalCatalog) {
 2780            _log.LogDebug("Creating connection for identifier {Identifier}", directoryIdentifier);
 2781            var port = globalCatalog ? _ldapConfig.GetGCPort(ssl) : _ldapConfig.GetPort(ssl);
 2782            var identifier = new LdapDirectoryIdentifier(directoryIdentifier, port, false, false);
 2783            var connection = new LdapConnection(identifier) { Timeout = new TimeSpan(0, 0, 5, 0) };
 784
 785            //These options are important!
 2786            connection.SessionOptions.ProtocolVersion = 3;
 787            //Referral chasing does not work with paged searches
 2788            connection.SessionOptions.ReferralChasing = ReferralChasingOptions.None;
 3789            if (ssl) connection.SessionOptions.SecureSocketLayer = true;
 790
 3791            if (_ldapConfig.DisableSigning || ssl) {
 1792                connection.SessionOptions.Signing = false;
 1793                connection.SessionOptions.Sealing = false;
 1794            }
 1795            else {
 1796                connection.SessionOptions.Signing = true;
 1797                connection.SessionOptions.Sealing = true;
 1798            }
 799
 2800            if (_ldapConfig.DisableCertVerification)
 0801                connection.SessionOptions.VerifyServerCertificate = (_, _) => true;
 802
 2803            if (_ldapConfig.Username != null) {
 0804                var cred = new NetworkCredential(_ldapConfig.Username, _ldapConfig.Password);
 0805                connection.Credential = cred;
 0806            }
 807
 2808            connection.AuthType = _ldapConfig.AuthType;
 809
 2810            return connection;
 2811        }
 812
 813        /// <summary>
 814        ///     Tests whether an LDAP connection is working
 815        /// </summary>
 816        /// <param name="connection">The ldap connection object to test</param>
 817        /// <param name="testResult">The results fo the connection test</param>
 818        /// <returns>True if connection was successful, false otherwise</returns>
 819        /// <exception cref="LdapAuthenticationException">Something is wrong with the supplied credentials</exception>
 820        /// <exception cref="NoLdapDataException">
 821        ///     A connection "succeeded" but no data was returned. This can be related to
 822        ///     kerberos auth across trusts or just simply lack of permissions
 823        /// </exception>
 2824        private bool TestLdapConnection(LdapConnection connection, out LdapConnectionTestResult testResult) {
 2825            testResult = new LdapConnectionTestResult();
 2826            try {
 827                //Attempt an initial bind. If this fails, likely auth is invalid, or its not a valid target
 2828                connection.Bind();
 0829            }
 4830            catch (LdapException e) {
 831                //TODO: Maybe look at this and find a better way?
 2832                if (e.ErrorCode is (int)LdapErrorCodes.InvalidCredentials or (int)ResultCode.InappropriateAuthentication
 0833                    connection.Dispose();
 0834                    throw new LdapAuthenticationException(e);
 835                }
 836
 2837                testResult.Message = e.Message;
 2838                testResult.ErrorCode = e.ErrorCode;
 2839                return false;
 840            }
 0841            catch (Exception e) {
 0842                testResult.Message = e.Message;
 0843                return false;
 844            }
 845
 846            SearchResponse response;
 0847            try {
 848                //Do an initial search request to get the rootDSE
 849                //This ldap filter is equivalent to (objectclass=*)
 0850                var searchRequest = CreateSearchRequest("", new LdapFilter().AddAllObjects().GetFilter(),
 0851                    SearchScope.Base, null);
 852
 0853                response = (SearchResponse)connection.SendRequest(searchRequest);
 0854            }
 0855            catch (LdapException e) {
 856                /*
 857                 * If we can't send the initial search request, its unlikely any other search requests will work so we w
 858                 */
 0859                testResult.Message = e.Message;
 0860                testResult.ErrorCode = e.ErrorCode;
 0861                return false;
 862            }
 863
 0864            if (response?.Entries == null || response.Entries.Count == 0) {
 865                /*
 866                 * This can happen for one of two reasons, either we dont have permission to query AD or we're authentic
 867                 * across external trusts with kerberos authentication without Forest Search Order properly configured.
 868                 * Either way, this connection isn't useful for us because we're not going to get data, so return false
 869                 */
 870
 0871                connection.Dispose();
 0872                throw new NoLdapDataException();
 873            }
 874
 0875            testResult.SearchResultEntry = new SearchResultEntryWrapper(response.Entries[0]);
 0876            testResult.Message = "";
 0877            return true;
 2878        }
 879
 880        private class LdapConnectionTestResult {
 2881            public string Message { get; set; }
 0882            public IDirectoryObject SearchResultEntry { get; set; }
 2883            public int ErrorCode { get; set; }
 884        }
 885
 886        private async Task<(bool success, LdapConnectionWrapper connection)> CreateLDAPConnectionWithPortCheck(
 0887            string target, bool globalCatalog) {
 0888            if (globalCatalog) {
 0889                if (await _portScanner.CheckPort(target, _ldapConfig.GetGCPort(true)) || (!_ldapConfig.ForceSSL &&
 0890                        await _portScanner.CheckPort(target, _ldapConfig.GetGCPort(false))))
 0891                    return (CreateLdapConnection(target, true, out var connection), connection);
 0892            }
 0893            else {
 0894                if (await _portScanner.CheckPort(target, _ldapConfig.GetPort(true)) || (!_ldapConfig.ForceSSL &&
 0895                        await _portScanner.CheckPort(target, _ldapConfig.GetPort(false))))
 0896                    return (CreateLdapConnection(target, true, out var connection), connection);
 0897            }
 898
 0899            return (false, null);
 0900        }
 901
 902        private SearchRequest CreateSearchRequest(string distinguishedName, string ldapFilter,
 903            SearchScope searchScope,
 0904            string[] attributes) {
 0905            var searchRequest = new SearchRequest(distinguishedName, ldapFilter,
 0906                searchScope, attributes);
 0907            searchRequest.Controls.Add(new SearchOptionsControl(SearchOption.DomainScope));
 0908            return searchRequest;
 0909        }
 910    }
 911}