-
Notifications
You must be signed in to change notification settings - Fork 53
[BED-6389] Add retry logic to GetDomains to make more resilient to network hiccups #242
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v4
Are you sure you want to change the base?
Changes from 9 commits
3d1d6a8
ed71778
7405405
795f1f3
e606ac4
9d17eb9
c43c449
257fe74
8d91bb6
1b32498
a4d369f
e9bea59
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -37,9 +37,6 @@ internal class LdapConnectionPool : IDisposable { | |||||||||
| private const int MaxRetries = 3; | ||||||||||
| private static readonly ConcurrentDictionary<string, NetAPIStructs.DomainControllerInfo?> DCInfoCache = new(); | ||||||||||
|
|
||||||||||
| // Tracks domains we know we've determined we shouldn't try to connect to | ||||||||||
| private static readonly ConcurrentHashSet _excludedDomains = new(); | ||||||||||
|
|
||||||||||
| public LdapConnectionPool(string identifier, string poolIdentifier, LdapConfig config, | ||||||||||
| IPortScanner scanner = null, NativeMethods nativeMethods = null, ILogger log = null) { | ||||||||||
| _connections = new ConcurrentBag<LdapConnectionWrapper>(); | ||||||||||
|
|
@@ -693,7 +690,7 @@ private bool CallDsGetDcName(string domainName, out NetAPIStructs.DomainControll | |||||||||
|
|
||||||||||
| public async Task<(bool Success, LdapConnectionWrapper ConnectionWrapper, string Message)> | ||||||||||
| GetConnectionAsync() { | ||||||||||
| if (_excludedDomains.Contains(_identifier)) { | ||||||||||
| if (LdapUtils.IsExcludedDomain(_identifier)) { | ||||||||||
| return (false, null, $"Identifier {_identifier} excluded for connection attempt"); | ||||||||||
| } | ||||||||||
|
|
||||||||||
|
|
@@ -727,7 +724,7 @@ private bool CallDsGetDcName(string domainName, out NetAPIStructs.DomainControll | |||||||||
|
|
||||||||||
| public async Task<(bool Success, LdapConnectionWrapper ConnectionWrapper, string Message)> | ||||||||||
| GetGlobalCatalogConnectionAsync() { | ||||||||||
| if (_excludedDomains.Contains(_identifier)) { | ||||||||||
| if (LdapUtils.IsExcludedDomain(_identifier)) { | ||||||||||
| return (false, null, $"Identifier {_identifier} excluded for connection attempt"); | ||||||||||
| } | ||||||||||
|
|
||||||||||
|
|
@@ -813,7 +810,7 @@ await CreateLdapConnection(tempDomainName, globalCatalog) is (true, var connecti | |||||||||
| _log.LogDebug( | ||||||||||
| "Could not get domain object from GetDomain, unable to create ldap connection for domain {Domain}", | ||||||||||
| _identifier); | ||||||||||
| _excludedDomains.Add(_identifier); | ||||||||||
| LdapUtils.AddExcludedDomain(_identifier); | ||||||||||
| return (false, null, "Unable to get domain object for further strategies"); | ||||||||||
| } | ||||||||||
|
|
||||||||||
|
|
@@ -852,7 +849,7 @@ await CreateLdapConnection(tempDomainName, globalCatalog) is (true, var connecti | |||||||||
| catch (Exception e) { | ||||||||||
| _log.LogInformation(e, "We will not be able to connect to domain {Domain} by any strategy, leaving it.", | ||||||||||
| _identifier); | ||||||||||
| _excludedDomains.Add(_identifier); | ||||||||||
| LdapUtils.AddExcludedDomain(_identifier); | ||||||||||
| } | ||||||||||
|
Comment on lines
+852
to
853
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don’t blanket‑exclude the domain on any exception at the end of CreateNewConnection. Catching Exception and permanently excluding can mask auth/config issues and reduce resiliency. Restrict exclusion to specific transient/network failures or remove it here and rely on upstream retry/strategy logic. Apply this change: - LdapUtils.AddExcludedDomain(_identifier);
+ // Avoid permanent exclusion on generic exceptions; rely on retries/strategies.📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
|
|
||||||||||
| return (false, null, "All attempted connections failed"); | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -30,6 +30,9 @@ public class LdapUtils : ILdapUtils { | |||||||||||||||||
| private static ConcurrentDictionary<string, Domain> _domainCache = new(); | ||||||||||||||||||
| private static ConcurrentHashSet _domainControllers = new(StringComparer.OrdinalIgnoreCase); | ||||||||||||||||||
| private static ConcurrentHashSet _unresolvablePrincipals = new(StringComparer.OrdinalIgnoreCase); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Tracks Domains we know we've determined we shouldn't try to connect to | ||||||||||||||||||
| private static ConcurrentHashSet _excludedDomains = new(StringComparer.OrdinalIgnoreCase); | ||||||||||||||||||
|
|
||||||||||||||||||
| private static readonly ConcurrentDictionary<string, string> DomainToForestCache = | ||||||||||||||||||
| new(StringComparer.OrdinalIgnoreCase); | ||||||||||||||||||
|
|
@@ -506,12 +509,14 @@ public bool GetDomain(string domainName, out Domain domain) { | |||||||||||||||||
| : new DirectoryContext(DirectoryContextType.Domain); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Blocking External Call | ||||||||||||||||||
| domain = Domain.GetDomain(context); | ||||||||||||||||||
| domain = Helpers.RetryOnException<ActiveDirectoryObjectNotFoundException, Domain>(() => Domain.GetDomain(context), 2).GetAwaiter().GetResult(); | ||||||||||||||||||
| if (domain == null) return false; | ||||||||||||||||||
| _domainCache.TryAdd(cacheKey, domain); | ||||||||||||||||||
| return true; | ||||||||||||||||||
| } | ||||||||||||||||||
| catch (Exception e) { | ||||||||||||||||||
| // The Static GetDomain Function ran into an issue requiring to exclude a domain as it would continuously | ||||||||||||||||||
| // try to connect to a domain that it could not connect to. This method may also need the same logic. | ||||||||||||||||||
| _log.LogDebug(e, "GetDomain call failed for domain name {Name}", domainName); | ||||||||||||||||||
| domain = null; | ||||||||||||||||||
| return false; | ||||||||||||||||||
|
|
@@ -520,6 +525,10 @@ public bool GetDomain(string domainName, out Domain domain) { | |||||||||||||||||
|
|
||||||||||||||||||
| public static bool GetDomain(string domainName, LdapConfig ldapConfig, out Domain domain) { | ||||||||||||||||||
| if (_domainCache.TryGetValue(domainName, out domain)) return true; | ||||||||||||||||||
| if (IsExcludedDomain(domainName)) { | ||||||||||||||||||
| Logging.Logger.LogDebug("Domain: {DomainName} has been excluded for collection. Skipping", domainName); | ||||||||||||||||||
| return false; | ||||||||||||||||||
| } | ||||||||||||||||||
|
Comment on lines
529
to
533
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainNice: early exit on excluded domains. Once null-safety and out-param assignment are applied, this will prevent thrashing unreachable domains. To confirm no call sites pass null to the static overload (which would otherwise bypass exclusion semantics), run: 🏁 Script executed: #!/bin/bash
# Find static calls where the first argument is null
rg -nP --type=cs -C2 '\bLdapUtils\.GetDomain\s*\(\s*null\s*,\s*'Length of output: 0 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "1) All occurrences of 'GetDomain(' in C# files:"
rg -n --type=cs '\bGetDomain\s*\(' -S -C2 || true
echo
echo "2) Occurrences of 'LdapUtils.GetDomain(' in C# files:"
rg -nP --type=cs '\bLdapUtils\.GetDomain\s*\(' -S -C2 || true
echo
echo "3) Calls to GetDomain(...) with a literal null as the first argument:"
rg -nP --type=cs '\b(?:LdapUtils\.)?GetDomain\s*\(\s*null\b' -S -C2 || true
echo
echo "4) Show src/CommonLib/LdapUtils.cs around the reported lines (480-560):"
if [ -f src/CommonLib/LdapUtils.cs ]; then
sed -n '480,560p' src/CommonLib/LdapUtils.cs || true
else
echo "src/CommonLib/LdapUtils.cs not found"
fiLength of output: 15200 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "1) Find references/definitions of _domainCache and _nullCacheKey"
rg -n --type=cs '_domainCache' -S -n -C3 || true
echo
rg -n --type=cs '_nullCacheKey' -S -n -C3 || true
echo
echo "2) Find IsExcludedDomain / AddExcludedDomain"
rg -n --type=cs 'IsExcludedDomain\(' -S -n -C3 || true
echo
rg -n --type=cs 'AddExcludedDomain\(' -S -n -C3 || true
echo
echo "3) Print top of src/CommonLib/LdapUtils.cs (fields / static members)"
if [ -f src/CommonLib/LdapUtils.cs ]; then
sed -n '1,240p' src/CommonLib/LdapUtils.cs || true
else
echo "src/CommonLib/LdapUtils.cs not found"
fi
echo
echo "4) Show the IsExcludedDomain and AddExcludedDomain function bodies (if found)"
files=$(rg -n --type=cs 'IsExcludedDomain\(|AddExcludedDomain\(' -S --hidden --no-line-number --line-number | cut -d: -f1 | sort -u || true)
for f in $files; do
echo "---- $f ----"
rg -n --type=cs 'IsExcludedDomain\(|AddExcludedDomain\(' -n -C6 "$f" || true
sed -n '1,400p' "$f" | sed -n '1,400p' || true
doneLength of output: 65751 Make the static GetDomain null-safe and consistent with the instance overload Static overload (src/CommonLib/LdapUtils.cs:526–531) calls _domainCache.TryGetValue(domainName, ...) and IsExcludedDomain(domainName) without guarding domainName; passing null will throw (ConcurrentDictionary disallows null keys) and also diverges from the instance GetDomain's use of _nullCacheKey. Normalize null handling (e.g. guard domainName, map null to a consistent null-cache key or delegate to the instance semantics) and ensure the out parameter is assigned on every return path. Call sites to review: src/CommonLib/LdapConnectionPool.cs:635, 808; src/CommonLib/ConnectionPoolManager.cs:152. |
||||||||||||||||||
|
|
||||||||||||||||||
| try { | ||||||||||||||||||
|
Comment on lines
526
to
535
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix out parameter assignment and null-safe caching/exclusion in static GetDomain.
Apply this diff: - public static bool GetDomain(string domainName, LdapConfig ldapConfig, out Domain domain) {
- if (_domainCache.TryGetValue(domainName, out domain)) return true;
- if (IsExcludedDomain(domainName)) {
- Logging.Logger.LogDebug("Domain: {DomainName} has been excluded for collection. Skipping", domainName);
- return false;
- }
+ public static bool GetDomain(string domainName, LdapConfig ldapConfig, out Domain domain) {
+ var cacheKey = domainName ?? _nullCacheKey;
+ if (_domainCache.TryGetValue(cacheKey, out domain)) return true;
+ if (IsExcludedDomain(domainName)) {
+ Logging.Logger.LogDebug("Domain: {DomainName} has been excluded for collection. Skipping", domainName ?? "<null>");
+ domain = null;
+ return false;
+ }
...
- if (domain == null) return false;
- _domainCache.TryAdd(domainName, domain);
+ if (domain == null) return false;
+ _domainCache.TryAdd(cacheKey, domain);
return true;Also applies to: 549-550 🤖 Prompt for AI Agents |
||||||||||||||||||
| DirectoryContext context; | ||||||||||||||||||
|
|
@@ -535,14 +544,17 @@ public static bool GetDomain(string domainName, LdapConfig ldapConfig, out Domai | |||||||||||||||||
| : new DirectoryContext(DirectoryContextType.Domain); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Blocking External Call | ||||||||||||||||||
| domain = Domain.GetDomain(context); | ||||||||||||||||||
| domain = Helpers.RetryOnException<ActiveDirectoryObjectNotFoundException, Domain>(() => Domain.GetDomain(context), 2).GetAwaiter().GetResult(); | ||||||||||||||||||
| if (domain == null) return false; | ||||||||||||||||||
| _domainCache.TryAdd(domainName, domain); | ||||||||||||||||||
| return true; | ||||||||||||||||||
| } | ||||||||||||||||||
| catch (Exception e) { | ||||||||||||||||||
| Logging.Logger.LogDebug("Static GetDomain call failed for domain {DomainName}: {Error}", domainName, | ||||||||||||||||||
| Logging.Logger.LogDebug("Static GetDomain call failed, adding to exclusion, for domain {DomainName}: {Error}", domainName, | ||||||||||||||||||
| e.Message); | ||||||||||||||||||
| // If a domain cannot be contacted, this will exclude the domain so that it does not continuously try to connect, and | ||||||||||||||||||
| // cause more timeouts. | ||||||||||||||||||
| AddExcludedDomain(domainName); | ||||||||||||||||||
| domain = null; | ||||||||||||||||||
| return false; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
@@ -565,11 +577,13 @@ public bool GetDomain(out Domain domain) { | |||||||||||||||||
| : new DirectoryContext(DirectoryContextType.Domain); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Blocking External Call | ||||||||||||||||||
| domain = Domain.GetDomain(context); | ||||||||||||||||||
| domain = Helpers.RetryOnException<ActiveDirectoryObjectNotFoundException, Domain>(() => Domain.GetDomain(context), 2).GetAwaiter().GetResult(); | ||||||||||||||||||
| _domainCache.TryAdd(_nullCacheKey, domain); | ||||||||||||||||||
| return true; | ||||||||||||||||||
| } | ||||||||||||||||||
| catch (Exception e) { | ||||||||||||||||||
| // The Static GetDomain Function ran into an issue requiring to exclude a domain as it would continuously | ||||||||||||||||||
| // try to connect to a domain that it could not connect to. This method may also need the same logic. | ||||||||||||||||||
| _log.LogDebug(e, "GetDomain call failed for blank domain"); | ||||||||||||||||||
| domain = null; | ||||||||||||||||||
| return false; | ||||||||||||||||||
|
|
@@ -1129,6 +1143,7 @@ public void ResetUtils() { | |||||||||||||||||
| _domainControllers = new ConcurrentHashSet(StringComparer.OrdinalIgnoreCase); | ||||||||||||||||||
| _connectionPool?.Dispose(); | ||||||||||||||||||
| _connectionPool = new ConnectionPoolManager(_ldapConfig, scanner: _portScanner); | ||||||||||||||||||
| _excludedDomains = new ConcurrentHashSet(StringComparer.OrdinalIgnoreCase); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| private IDirectoryObject CreateDirectoryEntry(string path) { | ||||||||||||||||||
|
|
@@ -1143,6 +1158,9 @@ public void Dispose() { | |||||||||||||||||
| _connectionPool?.Dispose(); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| public static bool IsExcludedDomain(string domain) => _excludedDomains.Contains(domain); | ||||||||||||||||||
| public static void AddExcludedDomain(string domain) => _excludedDomains.Add(domain); | ||||||||||||||||||
|
|
||||||||||||||||||
|
Comment on lines
+1163
to
+1165
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard exclusion helpers against null/empty inputs. Current code will throw ArgumentNullException on null domain. Also avoid polluting the set with empty strings. Apply this diff: -public static bool IsExcludedDomain(string domain) => _excludedDomains.Contains(domain);
-public static void AddExcludedDomain(string domain) => _excludedDomains.Add(domain);
+public static bool IsExcludedDomain(string domain) =>
+ !string.IsNullOrWhiteSpace(domain) && _excludedDomains.Contains(domain);
+public static void AddExcludedDomain(string domain) {
+ if (!string.IsNullOrWhiteSpace(domain))
+ _excludedDomains.Add(domain);
+}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| internal static bool ResolveLabel(string objectIdentifier, string distinguishedName, string samAccountType, | ||||||||||||||||||
| string[] objectClasses, int flags, out Label type) { | ||||||||||||||||||
| type = Label.Base; | ||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| using System; | ||
| using System.Threading; | ||
|
|
||
| namespace SharpHoundCommonLib; | ||
|
|
||
| public static class RandomUtils { | ||
| private static readonly ThreadLocal<Random> Random = new(() => new Random()); | ||
|
|
||
| public static double NextDouble() => Random.Value.NextDouble(); | ||
| public static double Between(double minValue, double maxValue) => Random.Value.NextDouble() * (maxValue - minValue) + minValue; | ||
| } | ||
|
Comment on lines
7
to
20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seed ThreadLocal securely and avoid symbol shadowing. Multiple threads can get identical sequences with new Random(); also the field name Random shadows the type. Apply this diff: +using System.Security.Cryptography;
public static class RandomUtils {
- private static readonly ThreadLocal<Random> Random = new(() => new Random());
-
- public static double NextDouble() => Random.Value.NextDouble();
- public static double Between(double minValue, double maxValue) => Random.Value.NextDouble() * (maxValue - minValue) + minValue;
+ private static readonly ThreadLocal<Random> ThreadRandom = new(() => {
+ // Per‑thread, cryptographically seeded RNG to avoid duplicate sequences across threads.
+ Span<byte> buf = stackalloc byte[8];
+ RandomNumberGenerator.Fill(buf);
+ var seed = BitConverter.ToInt32(buf);
+ return new Random(seed);
+ });
+
+ public static double NextDouble() => ThreadRandom.Value.NextDouble();
+ public static double Between(double minValue, double maxValue) {
+ if (maxValue < minValue) (minValue, maxValue) = (maxValue, minValue);
+ return ThreadRandom.Value.NextDouble() * (maxValue - minValue) + minValue;
+ }
}🤖 Prompt for AI Agents |
||
Uh oh!
There was an error while loading. Please reload this page.