Skip to content
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

Implements Grouped Racer Client #24

Merged
merged 5 commits into from
Dec 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions misc/Ae.Dns.Console/DnsConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;

namespace Ae.Dns.Console
{
Expand All @@ -15,5 +16,6 @@ public sealed class DnsConfiguration
public string? DhcpdConfigFile { get; set; }
public string? DhcpdLeasesHostnameSuffix { get; set; }
public DnsInfluxDbConfiguration? InfluxDbMetrics { get; set; }
public Dictionary<string, string[]> ClientGroups { get; set; } = new Dictionary<string, string[]>();
}
}
28 changes: 26 additions & 2 deletions misc/Ae.Dns.Console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Serilog;
using System;
using System.Collections.Generic;
Expand Down Expand Up @@ -98,15 +99,38 @@ static DnsDelegatingHandler CreateDnsDelegatingHandler(IServiceProvider serviceP
_ = remoteFilter.AddRemoteBlockList(remoteBlockList);
}

var upstreams = provider.GetServices<IDnsClient>().ToArray();
IDnsClient[] upstreams = provider.GetServices<IDnsClient>().ToArray();
if (!upstreams.Any())
{
throw new Exception("No upstream DNS servers specified - you must specify at least one");
}

selfLogger.LogInformation("Using {UpstreamCount} DNS upstreams", upstreams.Length);

IDnsClient dnsClient = ActivatorUtilities.CreateInstance<DnsRacerClient>(provider, upstreams.AsEnumerable());
IDnsClient dnsClient;
if (dnsConfiguration.ClientGroups.Any())
{
IDnsClient FindUpstreamByTag(string tag)
{
var upstream = upstreams.SingleOrDefault(x => string.Equals(x.ToString(), tag));
if (upstream == null)
{
throw new Exception($"DNS upstream client with tag {tag} not found. Available tags: {string.Join(", ", upstreams.Select(x => x.ToString()))}");
}
return upstream;
}

var groupRacerOptions = new DnsGroupRacerClientOptions
{
DnsClientGroups = dnsConfiguration.ClientGroups.ToDictionary(x => x.Key, x => (IReadOnlyList<IDnsClient>)x.Value.Select(y => FindUpstreamByTag(y)).ToArray())
};

dnsClient = ActivatorUtilities.CreateInstance<DnsGroupRacerClient>(provider, Options.Create(groupRacerOptions));
}
else
{
dnsClient = ActivatorUtilities.CreateInstance<DnsRacerClient>(provider, upstreams.AsEnumerable());
}

dnsClient = ActivatorUtilities.CreateInstance<DnsRebindMitigationClient>(provider, dnsClient);

Expand Down
74 changes: 48 additions & 26 deletions misc/Ae.Dns.Console/config.json
Original file line number Diff line number Diff line change
@@ -1,31 +1,53 @@
{
"serilog": {
"using": [ "Serilog.Sinks.Console" ],
"writeTo": [
{ "name": "Console" }
]
// Change level: "minimumLevel": "Warning"
},
"httpsUpstreams": [
"https://dns.google/",
"https://cloudflare-dns.com/",
"https://doh.opendns.com/"
],
"remoteBlocklists": [
// Examples:
// * https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
// * https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt
// A remote hosts file or simply a list of domain names
],
"disallowedDomainSuffixes": [
// Adding example.org here means *.example.org is blocked
"serilog": {
"using": [ "Serilog.Sinks.Console" ],
"writeTo": [
{ "name": "Console" }
]
// Change level: "minimumLevel": "Warning"
},
"httpsUpstreams": [
"https://8.8.8.8/",
"https://8.8.4.4/",
"https://1.1.1.1/",
"https://1.0.0.1/",
"https://146.112.41.2/",
"https://208.67.222.222/",
"https://208.67.220.220/"
],
"remoteBlocklists": [
// Examples:
// * https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
// * https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt
// A remote hosts file or simply a list of domain names
],
"disallowedDomainSuffixes": [
// Adding example.org here means *.example.org is blocked
],
"allowlistedDomains": [
// Adding google.com means google.com is explicitly allowed regardless of the blocklists
// It doesn't mean *.google.com is allowed, this is an exact match only
],
"hostFiles": [
// Host files to monitor for changes and incorporate into DNS lookups
// For example: "C:\\Windows\\System32\\drivers\\etc\\hosts"
],
"clientGroups": {
// When racing each upstream, pick one client from each group
// for redundancy. Optional section - if omitted, clients are
// not grouped and queries may be raced against the same provider.
"Google": [
"https://8.8.8.8/",
"https://8.8.4.4/"
],
"allowlistedDomains": [
// Adding google.com means google.com is explicitly allowed regardless of the blocklists
// It doesn't mean *.google.com is allowed, this is an exact match only
"CloudFlare": [
"https://1.1.1.1/",
"https://1.0.0.1/"
],
"hostFiles": [
// Host files to monitor for changes and incorporate into DNS lookups
// For example: "C:\\Windows\\System32\\drivers\\etc\\hosts"
"OpenDNS": [
"https://146.112.41.2/",
"https://208.67.222.222/",
"https://208.67.220.220/"
]
}
}
92 changes: 92 additions & 0 deletions src/Ae.Dns.Client/DnsGroupRacerClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using Ae.Dns.Client.Internal;
using Ae.Dns.Protocol;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Ae.Dns.Client
{
/// <summary>
/// A client consisting of multiple <see cref="IDnsClient"/> instances.
/// At query time, two clients are picked at random, and the DNS requests race against each other.
/// The first non-exceptional result is used, the slower result is discarded.
/// </summary>
public sealed class DnsGroupRacerClient : IDnsClient
{
private readonly ILogger<DnsGroupRacerClient> _logger;
private readonly DnsGroupRacerClientOptions _options;

/// <summary>
/// Create a new racer DNS client using the specified <see cref="IDnsClient"/> instances to delegate to.
/// </summary>
/// <param name="logger">The logger instance to use.</param>
/// <param name="options">The options for this racer client.</param>
[ActivatorUtilitiesConstructor]
public DnsGroupRacerClient(ILogger<DnsGroupRacerClient> logger, IOptions<DnsGroupRacerClientOptions> options)
{
_logger = logger;
_options = options.Value;
}

/// <inheritdoc/>
public async Task<DnsMessage> Query(DnsMessage query, CancellationToken token)
{
var sw = Stopwatch.StartNew();

// Pick the specified number of random groups
var randomisedGroups = _options.DnsClientGroups.Where(x => x.Value.Count > 0).OrderBy(x => Guid.NewGuid()).Take(_options.RandomGroupQueries);

// Randomise a client from each group
var randomisedClients = randomisedGroups.ToDictionary(x => x.Value.OrderBy(y => Guid.NewGuid()).First(), x => x.Key);

// Start the query tasks (and create a lookup from query to client)
var queries = randomisedClients.Keys.ToDictionary(client => client.Query(query, token), client => client);

// Select a winning task
var winningTask = await TaskRacer.RaceTasks(queries.Select(x => x.Key), async result => result.IsFaulted || (await result).EncounteredResolverError());

// If tasks faulted, log the reason
var faultedTasks = queries.Keys.Where(x => x.IsFaulted).ToArray();
if (faultedTasks.Any())
{
var faultedClients = faultedTasks.Select(x => queries[x]);
var faultedGroups = faultedClients.Select(x => randomisedClients[x]);

var faultedClientsString = string.Join(", ", faultedClients);
var faultedGroupsString = string.Join(", ", faultedGroups);

if (winningTask.IsFaulted)
{
_logger.LogError("All tasks using {FaultedClients} from groups {FaultedGroups} failed for query {Query} in {ElapsedMilliseconds}ms", faultedClientsString, faultedGroupsString, query, sw.ElapsedMilliseconds);
}
else
{
_logger.LogWarning("Tasks for clients {FaultedClients} from groups {FaultedGroups} failed for query {Query}, swapped with result for {SuccessfulClient} in {ElapsedMilliseconds}ms", faultedClientsString, faultedGroupsString, query, queries[winningTask], sw.ElapsedMilliseconds);
}
}

// Await the winning task (if it's faulted, it will throw at this point)
var winningAnswer = await winningTask;

// Only bother working out the logging if needed
if (_logger.IsEnabled(LogLevel.Information))
{
var winningClient = queries[winningTask];
var winningGroup = randomisedClients[winningClient];
_logger.LogInformation("Winning client was {WinningClient} from group {WinningGroup} for query {Query} in {ElapsedMilliseconds}ms", winningClient, winningGroup, query, sw.ElapsedMilliseconds);
}

return winningAnswer;
}

/// <inheritdoc/>
public void Dispose()
{
}
}
}
24 changes: 24 additions & 0 deletions src/Ae.Dns.Client/DnsGroupRacerClientOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Collections.Generic;
using Ae.Dns.Protocol;

namespace Ae.Dns.Client
{
/// <summary>
/// The options for <see cref="DnsRacerClient"/>.
/// </summary>
public sealed class DnsGroupRacerClientOptions
{
/// <summary>
/// The number of groups from which to randomly select a client for queries.
/// If a fewer number of groups are supplied to the <see cref="DnsGroupRacerClientOptions"/>,
/// all groups will start queries.
/// </summary>
public int RandomGroupQueries { get; set; } = 2;

/// <summary>
/// A dictionary of DNS clients grouped by an arbitrary identifier.
/// This can be used to group DNS clients of the same provider, for example.
/// </summary>
public IReadOnlyDictionary<string, IReadOnlyList<IDnsClient>> DnsClientGroups { get; set; } = new Dictionary<string, IReadOnlyList<IDnsClient>>();
}
}
12 changes: 9 additions & 3 deletions src/Ae.Dns.Client/Internal/TaskRacer.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Ae.Dns.Client.Internal
{
internal static class TaskRacer
{
public static async Task<Task<TResult>> RaceTasks<TResult>(IEnumerable<Task<TResult>> tasks)
public static async Task<Task<TResult>> RaceTasks<TResult>(IEnumerable<Task<TResult>> tasks, Func<Task<TResult>, Task<bool>> isFailed)
{
var queue = tasks.ToList();

Expand All @@ -16,9 +17,14 @@ public static async Task<Task<TResult>> RaceTasks<TResult>(IEnumerable<Task<TRes
task = await Task.WhenAny(queue);
queue.Remove(task);
}
while (task.IsFaulted && queue.Count > 0);
while (await isFailed(task) && queue.Count > 0);

return task;
}

public static async Task<Task<TResult>> RaceTasks<TResult>(IEnumerable<Task<TResult>> tasks)
{
return await RaceTasks(tasks, task => Task.FromResult(task.IsFaulted));
}
}
}
17 changes: 17 additions & 0 deletions src/Ae.Dns.Protocol/DnsMessageExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,23 @@ internal static class DnsMessageExtensions
return null;
}

/// <summary>
/// Returns true if the resolver encountered an error.
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
public static bool EncounteredResolverError(this DnsMessage message)
{
if (!message.Header.IsQueryResponse)
{
return false;
}

return message.Header.ResponseCode == DnsResponseCode.ServFail ||
message.Header.ResponseCode == DnsResponseCode.NotImp ||
message.Header.ResponseCode == DnsResponseCode.NotAuth;
}

public static bool TryParseIpAddressFromReverseLookup(this DnsMessage message, out IPAddress? address)
{
if (message.Header.QueryType == DnsQueryType.PTR && message.Header.Host.Count > 3
Expand Down
Loading