Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Adds cache queries and setting to opt in to caching failure responses #82

Merged
merged 5 commits into from
Aug 1, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/DnsClient/DnsQueryExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -618,14 +618,14 @@ public static async Task<ServiceHostEntry[]> ResolveServiceAsync(this IDnsQuery
return ResolveServiceProcessResult(result);
}

private static string ConcatResolveServiceName(string baseDomain, string serviceName, string tag)
public static string ConcatResolveServiceName(string baseDomain, string serviceName, string tag)
MichaCo marked this conversation as resolved.
Show resolved Hide resolved
{
return string.IsNullOrWhiteSpace(tag) ?
$"{serviceName}.{baseDomain}." :
$"_{serviceName}._{tag}.{baseDomain}.";
}

private static ServiceHostEntry[] ResolveServiceProcessResult(IDnsQueryResponse result)
public static ServiceHostEntry[] ResolveServiceProcessResult(IDnsQueryResponse result)
{
var hosts = new List<ServiceHostEntry>();
if (result == null || result.HasError)
Expand Down
34 changes: 32 additions & 2 deletions src/DnsClient/DnsQueryOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,20 @@ public int ExtendedDnsBufferSize
/// Gets or sets a flag indicating whether EDNS should be enabled and the <c>DO</c> flag should be set.
/// Defaults to <c>False</c>.
/// </summary>
public bool RequestDnsSecRecords { get; set; } = false;
public bool RequestDnsSecRecords { get; set; } = false;

/// <summary>
/// Gets or sets a flag indicating whether the DNS failures are being cached. The purpose of caching
/// failures is to reduce repeated lookup attempts within a short space of time.
/// Defaults to <c>False</c>.
/// </summary>
public bool UseCacheForFailures { get; set; } = false;
sipsorcery marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Gets or sets the duration to cache failed lookups. Does not apply if failed lookups are not being cached.
/// Defaults to <c>5 seconds</c>.
/// </summary>
public TimeSpan CacheFailureDuration { get; set; } = s_defaultTimeout;

/// <summary>
/// Converts the query options into readonly settings.
Expand Down Expand Up @@ -614,6 +627,19 @@ public class DnsQuerySettings : IEquatable<DnsQuerySettings>
/// </summary>
public bool RequestDnsSecRecords { get; }

/// <summary>
/// Gets a flag indicating whether the DNS failures are being cached. The purpose of caching
/// failures is to reduce repeated lookup attempts within a short space of time.
/// Defaults to <c>False</c>.
/// </summary>
public bool UseCacheForFailures { get; }

/// <summary>
/// If failures are being cached this value indicates how long they will be held in the cache for.
/// Defaults to <c>5 seconds</c>.
/// </summary>
public TimeSpan CacheFailureDuration { get; }
sipsorcery marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Creates a new instance of <see cref="DnsQueryAndServerSettings"/>.
/// </summary>
Expand All @@ -637,6 +663,8 @@ public DnsQuerySettings(DnsQueryOptions options)
UseTcpOnly = options.UseTcpOnly;
ExtendedDnsBufferSize = options.ExtendedDnsBufferSize;
RequestDnsSecRecords = options.RequestDnsSecRecords;
UseCacheForFailures = options.UseCacheForFailures;
CacheFailureDuration = options.CacheFailureDuration;
}

/// <inheritdocs />
Expand Down Expand Up @@ -680,7 +708,9 @@ public bool Equals(DnsQuerySettings other)
UseTcpFallback == other.UseTcpFallback &&
UseTcpOnly == other.UseTcpOnly &&
ExtendedDnsBufferSize == other.ExtendedDnsBufferSize &&
RequestDnsSecRecords == other.RequestDnsSecRecords;
RequestDnsSecRecords == other.RequestDnsSecRecords &&
UseCacheForFailures == other.UseCacheForFailures &&
CacheFailureDuration.Equals(other.Timeout);
}
}

Expand Down
76 changes: 63 additions & 13 deletions src/DnsClient/LookupClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -376,12 +376,12 @@ internal LookupClient(LookupClientOptions options, DnsMessageHandler udpHandler
if (options.AutoResolveNameServers)
{
_resolvedNameServers = NameServer.ResolveNameServers(skipIPv6SiteLocal: true, fallbackToGooglePublicDns: false);
servers = servers.Concat(_resolvedNameServers).ToArray();
// This will periodically get triggered on Query calls and
// will perform the same check as on NetworkAddressChanged.
// The event doesn't seem to get fired on Linux for example...
// TODO: Maybe there is a better way, but this will work for now.
servers = servers.Concat(_resolvedNameServers).ToArray();
// This will periodically get triggered on Query calls and
// will perform the same check as on NetworkAddressChanged.
// The event doesn't seem to get fired on Linux for example...
// TODO: Maybe there is a better way, but this will work for now.
_skipper = new SkipWorker(
() =>
{
Expand All @@ -396,7 +396,7 @@ internal LookupClient(LookupClientOptions options, DnsMessageHandler udpHandler
}

Settings = new LookupClientSettings(options, servers);
Cache = new ResponseCache(true, Settings.MinimumCacheTimeout, Settings.MaximumCacheTimeout);
Cache = new ResponseCache(true, Settings.MinimumCacheTimeout, Settings.MaximumCacheTimeout, Settings.CacheFailureDuration);
}

private void CheckResolvedNameservers()
Expand Down Expand Up @@ -482,6 +482,22 @@ public IDnsQueryResponse Query(DnsQuestion question, DnsQueryAndServerOptions qu

var settings = GetSettings(queryOptions);
return QueryInternal(question, settings, settings.ShuffleNameServers());
}

/// <inheritdoc/>
public IDnsQueryResponse QueryCache(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN)
sipsorcery marked this conversation as resolved.
Show resolved Hide resolved
=> QueryCache(new DnsQuestion(query, queryType, queryClass));

/// <inheritdoc/>
public IDnsQueryResponse QueryCache(DnsQuestion question)
{
if (question is null)
{
throw new ArgumentNullException(nameof(question));
}

var settings = GetSettings();
return QueryCache(question, settings);
}

/// <inheritdoc/>
Expand Down Expand Up @@ -875,9 +891,14 @@ private IDnsQueryResponse ResolveQuery(
if (lastQueryResponse == null)
{
throw;
}

// If its the last server, return.
if (settings.UseCache && settings.UseCacheForFailures)
{
Cache.Add(cacheKey, lastQueryResponse, true);
}

// If its the last server, return.
return lastQueryResponse;
}
catch (Exception ex) when (
Expand Down Expand Up @@ -1126,9 +1147,14 @@ private async Task<IDnsQueryResponse> ResolveQueryAsync(
if (lastQueryResponse == null)
{
throw;
}

// If its the last server, return.
if (settings.UseCache && settings.UseCacheForFailures)
{
Cache.Add(cacheKey, lastQueryResponse, true);
}

// If its the last server, return.
return lastQueryResponse;
}
catch (Exception ex) when (
Expand Down Expand Up @@ -1200,6 +1226,30 @@ ex is TimeoutException timeoutEx
{
AuditTrail = audit?.Build()
};
}

private IDnsQueryResponse QueryCache(
DnsQuestion question,
DnsQuerySettings settings)
{
if (question == null)
{
throw new ArgumentNullException(nameof(question));
}

var head = new DnsRequestHeader(false, DnsOpCode.Query);
var request = new DnsRequestMessage(head, question);

var cacheKey = ResponseCache.GetCacheKey(request.Question);

if (TryGetCachedResult(cacheKey, request, settings, out var cachedResponse))
{
return cachedResponse;
}
else
{
return null;
}
}

private enum HandleError
Expand Down Expand Up @@ -1269,10 +1319,10 @@ private HandleError HandleDnsResponseException(DnsResponseException ex, DnsReque
private HandleError HandleDnsResponeParseException(DnsResponseParseException ex, DnsRequestMessage request, DnsMessageHandleType handleType, bool isLastServer)
{
// Don't try to fallback to TCP if we already are on TCP
if (handleType == DnsMessageHandleType.UDP
// Assuming that if we only got 512 or less bytes, its probably some network issue.
&& (ex.ResponseData.Length <= DnsQueryOptions.MinimumBufferSize
// Second assumption: If the parser tried to read outside the provided data, this might also be a network issue.
if (handleType == DnsMessageHandleType.UDP
// Assuming that if we only got 512 or less bytes, its probably some network issue.
&& (ex.ResponseData.Length <= DnsQueryOptions.MinimumBufferSize
// Second assumption: If the parser tried to read outside the provided data, this might also be a network issue.
|| ex.ReadLength + ex.Index > ex.ResponseData.Length))
{
// lets assume the response was truncated and retry with TCP.
Expand Down
94 changes: 61 additions & 33 deletions src/DnsClient/ResponseCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ internal class ResponseCache
private int _lastCleanup = 0;
private TimeSpan? _minimumTimeout;
private TimeSpan? _maximumTimeout;
private TimeSpan? _failureEntryTimeout;

public int Count => _cache.Count;

Expand Down Expand Up @@ -54,13 +55,29 @@ public TimeSpan? MaximumTimeout

_maximumTimeout = value;
}
}

public TimeSpan? FailureEntryTimeout
{
get { return _failureEntryTimeout; }
set
{
if (value.HasValue &&
(value < TimeSpan.Zero || value > s_maxTimeout) && value != s_infiniteTimeout)
{
throw new ArgumentOutOfRangeException(nameof(value));
}

_failureEntryTimeout = value;
}
}

public ResponseCache(bool enabled = true, TimeSpan? minimumTimout = null, TimeSpan? maximumTimeout = null)
public ResponseCache(bool enabled = true, TimeSpan? minimumTimout = null, TimeSpan? maximumTimeout = null, TimeSpan? failureEntryTimeout = null)
{
Enabled = enabled;
MinimumTimout = minimumTimout;
MaximumTimeout = maximumTimeout;
FailureEntryTimeout = failureEntryTimeout;
}

public static string GetCacheKey(DnsQuestion question)
Expand Down Expand Up @@ -101,43 +118,54 @@ public IDnsQueryResponse Get(string key, out double? effectiveTtl)
return null;
}

public bool Add(string key, IDnsQueryResponse response)
public bool Add(string key, IDnsQueryResponse response, bool cacheFailures = false)
{
if (key == null) throw new ArgumentNullException(key);

if (Enabled && response != null && !response.HasError && response.Answers.Count > 0)
if (Enabled && response != null && (cacheFailures || (!response.HasError && response.Answers.Count > 0)))
{
var all = response.AllRecords.Where(p => !(p is Protocol.Options.OptRecord));
if (all.Any())
{
// in millis
double minTtl = all.Min(p => p.InitialTimeToLive) * 1000d;

if (MinimumTimout == Timeout.InfiniteTimeSpan)
{
// TODO: Log warning once?
minTtl = s_maxTimeout.TotalMilliseconds;
}
else if (MinimumTimout.HasValue && minTtl < MinimumTimout.Value.TotalMilliseconds)
{
minTtl = MinimumTimout.Value.TotalMilliseconds;
}

// max TTL check which can limit the upper boundary
if (MaximumTimeout.HasValue && MaximumTimeout != Timeout.InfiniteTimeSpan && minTtl > MaximumTimeout.Value.TotalMilliseconds)
{
minTtl = MaximumTimeout.Value.TotalMilliseconds;
}

if (minTtl < 1d)
{
return false;
if (response.Answers.Count == 0 && FailureEntryTimeout.HasValue)
{
// Cache entry for a failure response.
var newEntry = new ResponseEntry(response, FailureEntryTimeout.Value.TotalMilliseconds);

StartCleanup();
return _cache.TryAdd(key, newEntry);
}
else
{
var all = response.AllRecords.Where(p => !(p is Protocol.Options.OptRecord));
if (all.Any())
{
// in millis
double minTtl = all.Min(p => p.InitialTimeToLive) * 1000d;

if (MinimumTimout == Timeout.InfiniteTimeSpan)
{
// TODO: Log warning once?
minTtl = s_maxTimeout.TotalMilliseconds;
}
else if (MinimumTimout.HasValue && minTtl < MinimumTimout.Value.TotalMilliseconds)
{
minTtl = MinimumTimout.Value.TotalMilliseconds;
}

// max TTL check which can limit the upper boundary
if (MaximumTimeout.HasValue && MaximumTimeout != Timeout.InfiniteTimeSpan && minTtl > MaximumTimeout.Value.TotalMilliseconds)
{
minTtl = MaximumTimeout.Value.TotalMilliseconds;
}

if (minTtl < 1d)
{
return false;
}

var newEntry = new ResponseEntry(response, minTtl);

StartCleanup();
return _cache.TryAdd(key, newEntry);
}

var newEntry = new ResponseEntry(response, minTtl);

StartCleanup();
return _cache.TryAdd(key, newEntry);
}
}

Expand Down